Ghost/ghost/admin/app/components/gh-koenig-editor.js
Kevin Ansfield cc2e20a486 Koenig - Added reading time and word count display
refs https://github.com/TryGhost/Ghost/issues/9724
- add `registerComponent` hook to cards so that `{{koenig-editor}}` can fetch properties from card components directly
- add word count and reading time utilities
- add throttled word count update routine to `{{koenig-editor}}` that walks all sections and counts text words or fetches word/image counts from card components
- add `wordCountDidChange` hook to `{{koenig-editor}}` so that word count + reading time can be exposed
- modify editor controller to update it's own word count property when koenig triggers it's action
- modified the editor template to show reading time + word count next to the post status
2018-07-20 15:53:21 +01:00

161 lines
5.1 KiB
JavaScript

import Component from '@ember/component';
export default Component.extend({
// public attrs
classNames: ['gh-koenig-editor', 'relative', 'w-100', 'vh-100', 'overflow-x-hidden', 'overflow-y-auto', 'z-0'],
title: '',
titlePlaceholder: '',
body: null,
bodyPlaceholder: '',
bodyAutofocus: false,
// internal properties
_title: null,
_editor: null,
_mousedownY: 0,
// closure actions
onTitleChange() {},
onTitleBlur() {},
onBodyChange() {},
onEditorCreated() {},
onWordCountChange() {},
actions: {
focusTitle() {
this._title.focus();
},
// triggered when a mousedown is registered on .gh-koenig-editor-pane
trackMousedown(event) {
this._mousedownY = event.clientY;
},
// triggered when a mouseup is registered on .gh-koenig-editor-pane
focusEditor(event) {
if (event.target.classList.contains('gh-koenig-editor-pane')) {
let editorCanvas = this._editor.element;
let {bottom} = editorCanvas.getBoundingClientRect();
// if a mousedown and subsequent mouseup occurs below the editor
// canvas, focus the editor and put the cursor at the end of the
// document
if (this._mousedownY > bottom && event.clientY > bottom) {
let {post} = this._editor;
let range = post.toRange();
let {tailSection} = range;
event.preventDefault();
this._editor.focus();
// we should always have a visible cursor when focusing
// at the bottom so create an empty paragraph if last
// section is a card
if (tailSection.isCardSection) {
this._editor.run((postEditor) => {
let newSection = postEditor.builder.createMarkupSection('p');
postEditor.insertSectionAtEnd(newSection);
tailSection = newSection;
});
}
this._editor.selectRange(tailSection.tailPosition());
// ensure we're scrolled to the bottom
this.element.scrollTop = this.element.scrollHeight;
}
}
},
/* title related actions -------------------------------------------- */
onTitleCreated(titleElement) {
this._title = titleElement;
},
onTitleChange(newTitle) {
this.onTitleChange(newTitle);
},
onTitleFocusOut() {
this.onTitleBlur();
},
onTitleKeydown(event) {
let value = event.target.value;
let selectionStart = event.target.selectionStart;
// enter will always focus the editor
// down arrow will only focus the editor when the cursor is at the
// end of the input to preserve the default OS behaviour
if (
event.key === 'Enter' ||
event.key === 'Tab' ||
((event.key === 'ArrowDown' || event.key === 'ArrowRight') && (!value || selectionStart === value.length))
) {
event.preventDefault();
// on Enter we also want to create a blank para if necessary
if (event.key === 'Enter') {
this._addParaAtTop();
}
this._editor.focus();
}
},
/* body related actions --------------------------------------------- */
onEditorCreated(koenig) {
this._setupEditor(koenig);
this.onEditorCreated(koenig);
},
onBodyChange(newMobiledoc) {
this.onBodyChange(newMobiledoc);
}
},
/* public methods ------------------------------------------------------- */
/* internal methods ----------------------------------------------------- */
_setupEditor(koenig) {
let component = this;
this._koenig = koenig;
this._editor = koenig.editor;
// focus the title when pressing SHIFT+TAB
this._editor.registerKeyCommand({
str: 'SHIFT+TAB',
run() {
component.send('focusTitle');
return true;
}
});
},
_addParaAtTop() {
if (!this._editor) {
return;
}
let editor = this._editor;
let section = editor.post.toRange().head.section;
// create a blank paragraph at the top of the editor unless it's already
// a blank paragraph
if (section.isListItem || !section.isBlank || section.text !== '') {
editor.run((postEditor) => {
let {builder} = postEditor;
let newPara = builder.createMarkupSection('p');
let sections = section.isListItem ? section.parent.parent.sections : section.parent.sections;
postEditor.insertSectionBefore(sections, newPara, section);
});
}
}
});