Skip to content
This repository has been archived by the owner on Nov 28, 2022. It is now read-only.

Commit

Permalink
Editor title improvements (#614)
Browse files Browse the repository at this point in the history
closes: TryGhost/Ghost#8292
- Title improvements:
- Clean up the connection between the editor and title.
- Encapsulate all title related events in the title component.
- Fix tab support.
- Fixed an issue where pressing up from an empty paragraph would select the title
- Ensure the empty content psuedo element is always below the cursor and make sure it always displays when the title is blank.
  • Loading branch information
disordinary authored and kevinansfield committed Apr 7, 2017
1 parent 4e3642c commit eb9e772
Show file tree
Hide file tree
Showing 8 changed files with 121 additions and 79 deletions.
110 changes: 96 additions & 14 deletions app/components/gh-editor-title.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,41 @@
import Component from 'ember-component';
import computed from 'ember-computed';

export default Component.extend({
val: '',
_cachedVal: '',
_mutationObserver: null,
tagName: 'h2',
didRender() {
if (this._rendered) {
return;
editor: null,

koenigEditor: computed('editor', {
get() {
return this.get('editor');
},
set(key, value) {
this.set('editor', value);
}
}),
editorKeyDownListener: null,
didRender() {
let editor = this.get('editor');

let title = this.$('.gh-editor-title');
if (!this.get('val')) {
title.addClass('no-content');
} else {
} else if (this.get('val') !== this.get('_cachedVal')) {
title.html(this.get('val'));
}

if (!editor) {
return;
}
if (this.get('editorKeyDownListener')) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
this.set('editorKeyDownListener', this.editorKeyDown.bind(this));
editor.element.addEventListener('keydown', this.get('editorKeyDownListener'));

title[0].onkeydown = (event) => {
// block the browser format keys.
if (event.ctrlKey || event.metaKey) {
Expand All @@ -31,7 +52,6 @@ export default Component.extend({
if (event.keyCode === 13) {
// enter
// on enter we want to split the title, create a new paragraph in the mobile doc and insert it into the content.
let {editor} = window;
let title = this.$('.gh-editor-title');
editor.run((postEditor) => {
let {anchorOffset, focusOffset} = window.getSelection();
Expand Down Expand Up @@ -73,13 +93,7 @@ export default Component.extend({
let offset = title.offset();
let bottomOfHeading = offset.top + title.height();
if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
let {editor} = window; // This isn't ideal.
// We need to pass the editor instance so that we can `this.get('editor');`
// but the editor instance is within the component and not exposed.
// there's also a dependency that the editor will have with the title and the title will have with the editor
// so that the cursor can move both ways (up and down) between them.
// see `lib/gh-koenig/addon/gh-koenig.js` and the function `findCursorPositionFromPixel` which should actually be
// encompassed here.
let editor = this.get('editor');
let loc = editor.element.getBoundingClientRect();

let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
Expand All @@ -92,7 +106,7 @@ export default Component.extend({
return false;
}
}
title.removeClass('no-content');
// title.removeClass('no-content');
};

// setup mutation observer
Expand All @@ -112,6 +126,7 @@ export default Component.extend({
// }

if (this.get('val') !== textContent) {
this.set('_cachedVal', textContent);
this.set('val', textContent);
this.sendAction('onChange', textContent);
this.sendAction('update', textContent);
Expand All @@ -120,9 +135,76 @@ export default Component.extend({

mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
this.set('_mutationObserver', mutationObserver);
this.set('_rendered', true);
},
willDestroyElement() {
this.get('_mutationObserver').disconnect();
this.$('.gh-editor-title')[0].onkeydown = null;
let editor = this.get('editor');
if (editor) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
},
editorKeyDown(event) {
let editor = this.get('editor');

if (event.keyCode === 38) { // up arrow
let selection = window.getSelection();
if (!selection.rangeCount) {
return;
}
let range = selection.getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
let topOfEditor = editor.element.getBoundingClientRect().top;

// if the current paragraph is empty then the position is 0
if (cursorPositionOnScreen.top === 0) {
cursorPositionOnScreen = editor.activeSection.renderNode.element.getBoundingClientRect();
}

if (cursorPositionOnScreen.top < topOfEditor + 33) {
let offset = this.getOffsetAtPosition(cursorPositionOnScreen.left);
this.setCursorAtOffset(offset);

return false;
}
}
},
// gets the character in the last line of the title that best matches the editor
getOffsetAtPosition(horizontalOffset) {
let [title] = this.$('.gh-editor-title')[0].childNodes;
let len = title.textContent.length;
let range = document.createRange();

for (let i = len - 1; i > -1; i--) {
// console.log(title);

This comment has been minimized.

Copy link
@littke

littke May 9, 2017

💃

range.setStart(title, i);
range.setEnd(title, i + 1);
let rect = range.getBoundingClientRect();
if (rect.top === rect.bottom) {
continue;
}
if (rect.left <= horizontalOffset && rect.right >= horizontalOffset) {
return i + (horizontalOffset >= (rect.left + rect.right) / 2 ? 1 : 0); // if the horizontalOffset is on the left hand side of the
// character then return `i`, if it's on the right return `i + 1`
}
}

return len;
},
setCursorAtOffset() {
let [title] = this.$('.gh-editor-title');
title.focus();
// the following code sets the start point based on the offest provided.
// it works in isolation of ghost-admin but in ghost-admin it doesn't work in Chrome
// and works in Firefox, but in firefox you can no longer edit the title once this has happened.
// It's either an issue with ghost-admin or mobiledoc and more investigation needs to be done.
// Probably after the beta release though.

// let range = document.createRange();
// let selection = window.getSelection();
// range.setStart(title.childNodes[0], offset);
// range.collapse(true);
// selection.removeAllRanges();
// selection.addRange(range);
}
});
7 changes: 6 additions & 1 deletion app/mixins/editor-base-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export default Mixin.create({
toolbar: [], // for apps
apiRoot: ghostPaths().apiRoot,
assetPath: ghostPaths().assetRoot,

editor: null,
title: null,
init() {
this._super(...arguments);
window.onbeforeunload = () => {
Expand Down Expand Up @@ -562,6 +563,10 @@ export default Mixin.create({

toggleReAuthenticateModal() {
this.toggleProperty('showReAuthenticateModal');
},

setEditor(editor) {
this.set('editor', editor);
}
}
});
2 changes: 2 additions & 0 deletions app/styles/layouts/editor.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
font-weight: bold;
font-size: 3.2rem;
line-height: 1.3em;
z-index: 1;
}

/* Place holder content that displays in the title if it is empty */
Expand All @@ -30,6 +31,7 @@
font-weight: bold;
line-height: 1.3em;
min-width: 30rem; /* hack it's defaulting just to enough width for the 'Your' in 'Your Post Title' */
z-index: -1;
}


Expand Down
2 changes: 1 addition & 1 deletion app/templates/components/gh-editor-title.hbs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<div contenteditable="true" data-placeholder="Your Post Title" class="gh-editor-title"></div>
<div contenteditable="true" data-placeholder="Your Post Title" class="gh-editor-title" tabindex={{tabindex}}></div>
9 changes: 5 additions & 4 deletions app/templates/editor/edit.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@
shouldFocus=shouldFocusTitle
focus-out="updateTitle"
update=(action (perform updateTitle))
id='gh-editor-title'
id="gh-editor-title"
koenigEditor=(readonly editor)
}}
{{#if scheduleCountdown}}
<time datetime="{{post.publishedAtUTC}}" class="gh-notification gh-notification-schedule">
Expand All @@ -56,9 +57,9 @@
shouldFocusEditor=shouldFocusEditor
apiRoot=apiRoot
assetPath=assetPath
tabindex=2
containerSelector='.gh-editor-container'
titleQuery='#gh-editor-title div'
tabindex="2"
containerSelector=".gh-editor-container"
setEditor=(action "setEditor")
}}
</div>
</div>
Expand Down
43 changes: 9 additions & 34 deletions lib/gh-koenig/addon/components/gh-koenig.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,19 @@ export default Component.extend({
}
};

this.editor = new Mobiledoc.Editor(options);
this.set('editor', new Mobiledoc.Editor(options));
run.next(() => {
if (this.get('setEditor')) {
this.sendAction('setEditor', this.get('editor'));
}
});
},

willRender() {
if (this._rendered) {
return;
}
let {editor} = this;
let editor = this.get('editor');

editor.didRender(() => {

Expand All @@ -92,6 +97,8 @@ export default Component.extend({
return;
}
let [editorDom] = this.$('.surface');
editorDom.tabindex = this.get('tabindex');

this.domContainer = editorDom.parentNode.parentNode.parentNode.parentNode; // nasty nasty nasty.
this.editor.render(editorDom);
this._rendered = true;
Expand All @@ -113,36 +120,6 @@ export default Component.extend({

this.editor.cursorDidChange(() => this.cursorMoved());

// hack to track key up to focus back on the title when the up key is pressed
this.editor.element.addEventListener('keydown', (event) => {
if (event.keyCode === 38) { // up arrow
let selection = window.getSelection();
if (!selection.rangeCount) {
return;
}
let range = selection.getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
let topOfEditor = this.editor.element.getBoundingClientRect().top;
if (cursorPositionOnScreen.top < topOfEditor + 33) {
let $title = $(this.titleQuery);

// // the code below will move the cursor to the correct part of the title when pressing the ⬆ arrow.
// // unfortunately it positions correctly in Firefox but you cannot edit, it doesn't position correctly in Chrome but you can.
// let offset = findCursorPositionFromPixel($title[0].firstChild, cursorPositionOnScreen.left);

// let newRange = document.createRange();
// newRange.collapse(true);
// newRange.setStart($title[0].firstChild, offset);
// newRange.setEnd($title[0].firstChild, offset);
// updateCursor(newRange);

$title[0].focus();

return false;
}
}
});

// listen to keydown events outside of the editor, used to handle keydown events in the cards.
document.onkeydown = (event) => {
// if any of the keydown handlers return false then we return false therefore stopping the event from propogating.
Expand Down Expand Up @@ -267,8 +244,6 @@ export default Component.extend({
range.tail.offset = 0;
editor.selectRange(range);
return;
} else {
$(this.titleQuery).focus();
}
}
});
Expand Down
3 changes: 2 additions & 1 deletion lib/gh-koenig/addon/components/koenig-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export default Component.extend({
mobiledocCard.removeClass('__mobiledoc-card');
mobiledocCard.addClass('kg-card');
mobiledocCard.addClass(name ? `kg-${name}` : '');
mobiledocCard.attr('tabindex', 3);

mobiledocCard.attr('tabindex', 4);
mobiledocCard.click(() => {
if (!this.get('isEditing')) {
this.send('selectCardHard');
Expand Down
24 changes: 0 additions & 24 deletions tests/integration/components/gh-editor-title-test.js

This file was deleted.

0 comments on commit eb9e772

Please sign in to comment.