- {{gh-editor-title
- val=(readonly model.titleScratch)
- onChange=(action (mut model.titleScratch))
- tabindex="1"
- shouldFocus=shouldFocusTitle
- focus-out="updateTitle"
- update=(action (perform updateTitle))
- id="gh-editor-title"
- koenigEditor=(readonly editor)
- editorMenuIsOpen=editorMenuIsOpen
- }}
- {{gh-koenig
- value=(readonly model.scratch)
+ {{!--
+ NOTE: the mobiledoc property is unbound so that the setting the
+ serialized version onChange doesn't cause a deserialization and
+ re-render of the editor on every key press / editor change
+ --}}
+ {{#gh-koenig
+ mobiledoc=(unbound model.scratch)
onChange=(action "updateScratch")
onFirstChange=(action "autoSaveNew")
- shouldFocusEditor=shouldFocusEditor
- apiRoot=apiRoot
- assetPath=assetPath
+ autofocus=shouldFocusEditor
tabindex="2"
- titleSelector=".gh-editor-title"
+ titleSelector="#kg-title-input"
containerSelector=".gh-editor-container"
- setEditor=(action "setEditor")
- menuIsOpen=(action "editorMenuIsOpen")
- menuIsClosed=(action "editorMenuIsClosed")
- wordcountDidChange=(action "wordcountDidChange")
+ wordcountDidChange=(action "setWordcount")
+ as |koenig|
}}
+ {{koenig-title-input
+ id="koenig-title-input"
+ val=(readonly model.titleScratch)
+ onChange=(action (mut model.titleScratch))
+ tabindex="1"
+ autofocus=shouldFocusTitle
+ focus-out="updateTitle"
+ update=(action (perform updateTitle))
+ editor=(readonly koenig.editor)
+ editorHasRendered=koenig.hasRendered
+ editorMenuIsOpen=koenig.isMenuOpen
+ }}
+ {{/gh-koenig}}
{{pluralize wordcount 'word'}}.
diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js b/ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js
index 99149a171e..8f55bc6200 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js
@@ -1,5 +1,6 @@
import Component from 'ember-component';
import layout from '../../templates/components/card-hr';
+
export default Component.extend({
layout
-});
\ No newline at end of file
+});
diff --git a/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js b/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
index 6ed469f64f..e00474b11e 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js
@@ -1,147 +1,245 @@
+import Ember from 'ember';
import Component from 'ember-component';
-import {A as emberA} from 'ember-array/utils';
import run from 'ember-runloop';
import layout from '../templates/components/gh-koenig';
-import Mobiledoc from 'mobiledoc-kit';
+import Editor from 'mobiledoc-kit/editor/editor';
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import createCardFactory from '../lib/card-factory';
-import defaultCommands from '../options/default-commands';
-import editorCards from '../cards/index';
-import {getCardFromDoc, checkIfClickEventShouldCloseCard, getPositionOnScreenFromRange} from '../lib/utils';
+import registerKeyCommands from '../options/key-commands';
+import registerTextExpansions from '../options/text-expansions';
+import defaultCards from '../cards/index';
+import {
+ getCardFromDoc,
+ checkIfClickEventShouldCloseCard,
+ getPositionOnScreenFromRange
+} from '../lib/utils';
import counter from 'ghost-admin/utils/word-count';
import $ from 'jquery';
+import computed from 'ember-computed';
+import {assign} from 'ember-platform';
+
+// ember-cli-shims doesn't export Ember.testing
+const {testing} = Ember;
+
+export const TESTING_EXPANDO_PROPERTY = '__koenig_editor';
export const BLANK_DOC = {
version: MOBILEDOC_VERSION,
- atoms: [],
markups: [],
+ atoms: [],
cards: [],
sections: [[1, 'p', [[0, [], 0, '']]]]
};
export default Component.extend({
layout,
- classNames: ['editor-holder'],
- emberCards: emberA([]),
- selectedCard: null,
+ classNames: ['gh-koenig-container'],
+
+ // exterally set properties
+ mobiledoc: null,
+ placeholder: 'Click here to start ...',
+ spellcheck: true,
+ autofocus: false,
+ cards: null,
+ atoms: null,
+ serializeVersion: MOBILEDOC_VERSION,
+ options: {},
+
+ // exposed properties
+ editor: null,
editedCard: null,
- keyDownHandler: [],
- resizeEvent: 0,
+ selectedCard: null,
+ emberCards: null,
+ isMenuOpen: false,
+ editorHasRendered: false,
+
+ // internal properties
+ _domContainer: null,
+ // TODO: keyDownHandler is assigned event handlers when a card is
+ // hard-selected, is there a better way of handling this?
+ _keyDownHandler: null,
+
+ // merge in named options with the `options` property data-bag
+ editorOptions: computed(function () {
+ let options = this.get('options');
+ let cards = this.get('cards') || [];
+ let atoms = this.get('atoms') || [];
+
+ // use our CardFactory to wrap our default and any user-supplied cards
+ // with Ghost specific functionality
+ // TODO: this also sets the emberCards property - do we need that indirection?
+ let createCard = createCardFactory.apply(this, {}); // need to pass the toolbar
+ cards = defaultCards.concat(cards).map((card) => createCard(card));
+
+ // add our default atoms
+ atoms.concat([{
+ name: 'soft-return',
+ type: 'dom',
+ render() {
+ return document.createElement('br');
+ }
+ }]);
+
+ return assign({
+ placeholder: this.get('placeholder'),
+ spellchack: this.get('spellcheck'),
+ autofocus: this.get('autofocus'),
+ // cardOptions: this.get('cardOptions'),
+ cards,
+ atoms
+ }, options);
+ }),
+
init() {
this._super(...arguments);
- let mobiledoc = this.get('value') || BLANK_DOC;
- let userCards = this.get('cards') || [];
+ // grab the supplied mobiledoc value - if it's empty set our default
+ // blank document, if it's a JSON string then deserialize it
+ let mobiledoc = this.get('mobiledoc');
+ if (!mobiledoc) {
+ mobiledoc = BLANK_DOC;
+ this.set('mobiledoc', mobiledoc);
+ }
if (typeof mobiledoc === 'string') {
mobiledoc = JSON.parse(mobiledoc);
+ this.set('mobiledoc', mobiledoc);
}
- // if the doc is cached then the editor is loaded and we don't need to continue.
- if (this._cachedDoc && this._cachedDoc === mobiledoc) {
- return;
- }
-
- let createCard = createCardFactory.apply(this, {}); // need to pass the toolbar
-
- let options = {
- mobiledoc,
- // temp
- cards: createCard(editorCards.concat(userCards)),
- atoms: [{
- name: 'soft-return',
- type: 'dom',
- render() {
- return document.createElement('br');
- }
- }],
- spellcheck: true,
- autofocus: this.get('shouldFocusEditor'),
- placeholder: 'Click here to start ...',
- unknownCardHandler: () => {
- // todo
- }
- };
-
- this.set('editor', new Mobiledoc.Editor(options));
+ this.set('emberCards', []);
+ this._keyDownHandler = [];
// we use css media width for most things but need to know if a device is touch
// to place the toolbar. Above the selected content on a mobile browser is the
// cut | copy | paste menu so we need to place our toolbar below.
+ // TODO: is this reliable enough? What about most Windows laptops now being touch enabled?
this.set('isTouch', 'ontouchstart' in document.documentElement);
- // window resize handler - throttled
- // window.onresize = () => {
- // let now = Date.now();
- // if (now - 2000 > this.get('resizeEvent')) {
- // this.set('resizeEvent', now);
- // }
- // };
-
- run.next(() => {
- if (this.get('setEditor')) {
- this.sendAction('setEditor', this.get('editor'));
- }
- });
+ this._startedRunLoop = false;
},
willRender() {
- if (this._rendered) {
+ // Use a default mobiledoc. If there are no changes, then return early.
+ let mobiledoc = this.get('mobiledoc') || BLANK_DOC;
+
+ let noMobiledocChanges
+ = (this._localMobiledoc && this._localMobiledoc === mobiledoc)
+ || (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc);
+
+ if (noMobiledocChanges) {
return;
}
+
+ // reset everything ready for an editor re-render
+ this._upstreamMobiledoc = mobiledoc;
+ this._localMobiledoc = null;
+
+ // trigger hook action
+ this._willCreateEditor();
+
+ // teardown any old editor that might be around
let editor = this.get('editor');
+ if (editor) {
+ editor.destroy();
+ }
+
+ // create a new editor
+ let editorOptions = this.get('editorOptions');
+ editorOptions.mobiledoc = mobiledoc;
+
+ // TODO: instantiate component hooks?
+ // https://github.com/bustlelabs/ember-mobiledoc-editor/blob/master/addon/components/mobiledoc-editor/component.js#L163-L227
+
+ editor = new Editor(editorOptions);
+
+ // set up our default key handling and text expansions to emulate MD behaviour
+ // TODO: better place to do this?
+ registerKeyCommands(editor);
+ registerTextExpansions(editor);
+
+ editor.willRender(() => {
+ // The editor's render/rerender will happen after this `editor.willRender`,
+ // so we explicitly start a runloop here if there is none, so that the
+ // add/remove card hooks happen inside a runloop.
+ // When pasting text that gets turned into a card, for example,
+ // the add card hook would run outside the runloop if we didn't begin a new
+ // one now.
+ if (!run.currentRunLoop) {
+ this._startedRunLoop = true;
+ run.begin();
+ }
+ });
editor.didRender(() => {
+ // If we had explicitly started a run loop in `editor.willRender`,
+ // we must explicitly end it here.
+ if (this._startedRunLoop) {
+ this._startedRunLoop = false;
+ run.end();
+ }
- this.sendAction('loaded', editor);
+ this.set('editorHasRendered', true);
});
- editor.postDidChange(()=> {
+
+ editor.postDidChange(() => {
run.join(() => {
- // store a cache of the local doc so that we don't need to reinitialise it.
- this._cachedDoc = editor.serialize(MOBILEDOC_VERSION);
- this.sendAction('onChange', this._cachedDoc);
- if (this._cachedDoc !== BLANK_DOC && !this._firstChange) {
- this._firstChange = true;
- this.sendAction('onFirstChange', this._cachedDoc);
- }
- this.processWordcount();
+ this.postDidChange(editor);
});
});
+
+ editor.cursorDidChange(() => {
+ if (this.isDestroyed) {
+ return;
+ }
+ run.join(() => {
+ this.cursorMoved();
+ });
+ });
+
+ this.set('editor', editor);
+
+ // trigger hook action
+ this._didCreateEditor(editor);
},
didRender() {
- // 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.
- return this.get('keyDownHandler').reduce((returnType, handler) => {
- let result = handler(event);
- if (returnType !== false) {
- return result;
- }
- return returnType;
- }, true);
- };
-
- if (this._rendered) {
- return;
+ // listen to keydown events outside of the editor, used to handle keydown
+ // events in the cards.
+ // TODO: is there a better way to handle this?
+ if (!document.onkeydown) {
+ document.onkeydown = (event) => {
+ // if any of the keydown handlers return false then we return false
+ // therefore stopping the event from propogating.
+ return this._keyDownHandler.reduce((returnType, handler) => {
+ let result = handler(event);
+ if (returnType !== false) {
+ return result;
+ }
+ return returnType;
+ }, true);
+ };
}
+
let editor = this.get('editor');
- let $editor = this.$('.surface');
- let [domContainer] = $editor.parents(this.get('containerSelector'));
- let [editorDom] = $editor;
- editorDom.tabindex = this.get('tabindex');
- this.set('domContainer', domContainer);
+ if (!editor.hasRendered) {
+ let $editor = this.$('.gh-koenig-surface');
+ let [domContainer] = $editor.parents(this.get('containerSelector'));
+ let [editorDom] = $editor;
- editor.render(editorDom);
- this.set('_rendered', true);
+ editorDom.tabindex = this.get('tabindex');
+ this._domContainer = domContainer;
- // set global editor for debugging and testing.
- window.editor = editor;
+ this._isRenderingEditor = true;
+ editor.render(editorDom);
+ this._isRenderingEditor = false;
+ }
+ this._setExpandoProperty(editor);
- defaultCommands(editor); // initialise the custom text handlers for MD, etc.
- // shouldFocusEditor is only true when transitioning from new to edit, otherwise it's false or undefined.
- // therefore, if it's true it's after the first lot of content is entered and we expect the caret to be at the
- // end of the document.
- if (this.get('shouldFocusEditor')) {
+ // autofocus is only true when transitioning from new to edit,
+ // otherwise it's false or undefined. therefore, if it's true it's after
+ // the first lot of content is entered and we expect the caret to be at
+ // the end of the document.
+ // TODO: can this be removed if we refactor the new/edit screens to not re-render?
+ if (this.get('autofocus')) {
let range = document.createRange();
range.selectNodeContents(this.editor.element);
range.collapse(false);
@@ -151,12 +249,13 @@ export default Component.extend({
editor._ensureFocus(); // PRIVATE API
}
- editor.cursorDidChange(() => this.cursorMoved());
this.processWordcount();
},
- // makes sure the cursor is on screen except when selection is happening in which case the browser mostly ensures it.
- // there is an issue with keyboard selection on some browsers though so the next step may be to record mouse and touch events.
+ // makes sure the cursor is on screen except when selection is happening in
+ // which case the browser mostly ensures it. there is an issue with keyboard
+ // selection on some browsers though so the next step may be to record mouse
+ // and touch events.
cursorMoved() {
let editor = this.get('editor');
@@ -172,9 +271,9 @@ export default Component.extend({
let windowHeight = window.innerHeight;
if (position.bottom > windowHeight) {
- this.domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
+ this._domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
} else if (position.top < 0) {
- this.domContainer.scrollTop += position.top - scrollBuffer;
+ this._domContainer.scrollTop += position.top - scrollBuffer;
}
if (editor.range && editor.range.headSection && editor.range.headSection.isCardSection) {
@@ -195,8 +294,10 @@ export default Component.extend({
this.send('deselectCard');
}
},
- // Note: This wordcount function doesn't count words that have been entered in cards.
- // We should either allow cards to report their own wordcount or use the DOM (innerText) to calculate the wordcount.
+
+ // NOTE: This wordcount function doesn't count words that have been entered in cards.
+ // We should either allow cards to report their own wordcount or use the DOM
+ // (innerText) to calculate the wordcount.
processWordcount() {
let wordcount = 0;
if (this.editor.post.sections.length) {
@@ -210,16 +311,49 @@ export default Component.extend({
}
this.sendAction('wordcountDidChange', wordcount);
},
- willDestroy() {
+
+ _willCreateEditor() {
+ this.sendAction('willCreateEditor');
+ },
+
+ _didCreateEditor(editor) {
+ this.sendAction('didCreateEditor', editor);
+ },
+
+ willDestroyElement() {
this.editor.destroy();
this.send('deselectCard');
+ // TODO: should we be killing all global onkeydown event handlers?
document.onkeydown = null;
- // window.oresize = null;
+ },
+
+ postDidChange(editor) {
+ // store a cache of the local doc so that we don't need to reinitialise it.
+ let serializeVersion = this.get('serializeVersion');
+ let updatedMobiledoc = editor.serialize(serializeVersion);
+ this._localMobiledoc = updatedMobiledoc;
+ this.sendAction('onChange', updatedMobiledoc);
+
+ // we need to trigger a first-change action so that we can trigger a
+ // save and transition from new-> edit
+ if (this._localMobiledoc !== BLANK_DOC && !this._hasChanged) {
+ this._hasChanged = true;
+ this.sendAction('onFirstChange', this._localMobiledoc);
+ }
+
+ this.processWordcount();
+ },
+
+ _setExpandoProperty(editor) {
+ // Store a reference to the editor for the acceptance test helpers
+ if (this.element && testing) {
+ this.element[TESTING_EXPANDO_PROPERTY] = editor;
+ }
},
actions: {
- // thin border, shows that a card is selected but the user cannot delete the card with
- // keyboard events.
+ // thin border, shows that a card is selected but the user cannot delete
+ // the card with keyboard events.
// used when the content of the card is selected and it is editing.
selectCard(cardId) {
if (!cardId) {
@@ -240,7 +374,7 @@ export default Component.extend({
cardHolder.addClass('selected');
cardHolder.removeClass('selected-hard');
this.set('selectedCard', card);
- this.get('keyDownHandler').length = 0;
+ this._keyDownHandler.length = 0;
// cardHolder.focus();
document.onclick = (event) => {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
@@ -248,6 +382,7 @@ export default Component.extend({
}
};
},
+
// thicker border and with keyboard events for moving around the editor
// creating blocks under the card and deleting the card.
// used when selecting the card with the keyboard or clicking on the toolbar.
@@ -283,8 +418,7 @@ export default Component.extend({
}
};
- let keyDownHandler = this.get('keyDownHandler');
- keyDownHandler.push((event) => {
+ this._keyDownHandler.push((event) => {
let editor = this.get('editor');
switch (event.keyCode) {
case 37: // arrow left
@@ -359,6 +493,7 @@ export default Component.extend({
}
});
},
+
deselectCard() {
let selectedCard = this.get('selectedCard');
if (selectedCard) {
@@ -367,16 +502,19 @@ export default Component.extend({
cardHolder.removeClass('selected-hard');
this.set('selectedCard', null);
}
- this.get('keyDownHandler').length = 0;
+ this._keyDownHandler.length = 0;
+ // TODO: do we want to kill all document onclick handlers?
document.onclick = null;
this.set('editedCard', null);
},
+
editCard(cardId) {
let card = this.get('emberCards').find((card) => card.id === cardId);
this.set('editedCard', card);
this.send('selectCard', cardId);
},
+
deleteCard(cardId, forwards = false) {
let editor = this.get('editor');
let card = this.get('emberCards').find((card) => card.id === cardId);
@@ -401,15 +539,19 @@ export default Component.extend({
editor.selectRange(range);
});
},
+
stopEditingCard() {
this.set('editedCard', null);
},
- menuIsOpen() {
- this.sendAction('menuIsOpen');
+
+ menuOpened() {
+ this.set('isMenuOpen', true);
},
- menuIsClosed() {
- this.sendAction('menuIsClosed');
+
+ menuClosed() {
+ this.set('isMenuOpen', false);
},
+
// drag and drop images onto the editor
dropImage(event) {
if (event.dataTransfer.files.length) {
@@ -420,6 +562,7 @@ export default Component.extend({
}
}
},
+
dragOver(event) {
// required for drop events to fire on markdown cards in firefox.
event.preventDefault();
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js
index 25288a86b6..2dfe6f0e73 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js
@@ -7,10 +7,12 @@ export default Component.extend({
tagName: 'div',
classNames: ['gh-cardmenu-card'],
classNameBindings: ['selected'],
+
init() {
this._super(...arguments);
this.set('selected', this.get('tool').selected);
},
+
click: function () { // eslint-disable-line
let {section, startOffset, endOffset} = this.get('range');
let editor = this.get('editor');
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js
index 1be4a50b8b..41da9f0277 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js
@@ -12,15 +12,17 @@ export default Component.extend({
layout,
isOpen: false,
isButton: false,
- showButton: computed('isOpen', 'isButton', function () {
- return this.get('isOpen') || this.get('isButton');
- }),
toolsLength: 0,
selected: -1,
selectedTool: null,
query: '',
range: null,
editor: null,
+
+ showButton: computed('isOpen', 'isButton', function () {
+ return this.get('isOpen') || this.get('isButton');
+ }),
+
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
@@ -58,15 +60,12 @@ export default Component.extend({
}
return tools;
}),
+
init() {
this._super(...arguments);
this.tools = new Tools(this.get('editor'), this);
},
- willDestroy() {
-
- },
-
didRender() {
let editor = this.get('editor');
let input = this.$('.gh-cardmenu-search-input');
@@ -110,8 +109,9 @@ export default Component.extend({
});
});
},
+
actions: {
- openMenu: function () { // eslint-disable-line
+ openMenu() {
let button = this.$('.gh-cardmenu-button'); // the ⊕ button.
let $editor = $(this.get('containerSelector'));
this.set('isOpen', true);
@@ -140,24 +140,28 @@ export default Component.extend({
});
this.sendAction('menuIsOpen');
},
- closeMenu: function () { // eslint-disable-line
+
+ closeMenu() {
this.set('isButton', false);
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
this.sendAction('menuIsClosed');
},
- closeMenuKeepButton: function () { // eslint-disable-line
+
+ closeMenuKeepButton() {
this.set('isOpen', false);
},
- selectTool: function () { // eslint-disable-line
+
+ selectTool() {
let {section} = this.get('range');
let editor = this.get('editor');
editor.range = Range.create(section, 0, section, 0);
this.get('selectedTool').onClick(editor);
this.send('closeMenuKeepButton');
},
- moveSelectionLeft: function () { // eslint-disable-line
+
+ moveSelectionLeft() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item > 0) {
@@ -166,7 +170,8 @@ export default Component.extend({
this.set('selected', length - 1);
}
},
- moveSelectionUp: function () { // eslint-disable-line
+
+ moveSelectionUp() {
let item = this.get('selected');
if (item > ROW_LENGTH) {
this.set('selected', item - ROW_LENGTH);
@@ -174,7 +179,8 @@ export default Component.extend({
this.set('selected', 0);
}
},
- moveSelectionRight: function () { // eslint-disable-line
+
+ moveSelectionRight() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item < length) {
@@ -183,7 +189,8 @@ export default Component.extend({
this.set('selected', 0);
}
},
- moveSelectionDown: function () { // eslint-disable-line
+
+ moveSelectionDown() {
let item = this.get('selected');
if (item < 0) {
item = 0;
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js
index 192280f3fb..0f6faf1f85 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js
@@ -17,6 +17,7 @@ export default Component.extend({
query: '',
range: null,
editor: null,
+
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
@@ -56,16 +57,13 @@ export default Component.extend({
return tools;
}),
+
init() {
this._super(...arguments);
let editor = this.get('editor');
this.set('tools', new Tools(editor, this));
},
- willDestroy() {
-
- },
-
didRender() {
let editor = this.get('editor');
let self = this;
@@ -80,6 +78,7 @@ export default Component.extend({
}
});
},
+
cursorChange() {
let editor = this.get('editor');
let range = this.get('range');
@@ -103,8 +102,9 @@ export default Component.extend({
}
}
},
+
actions: {
- openMenu: function () { // eslint-disable-line
+ openMenu() {
let holder = $(this.get('containerSelector'));
let editor = this.get('editor');
let self = this;
@@ -225,7 +225,8 @@ export default Component.extend({
this.sendAction('menuIsOpen');
},
- closeMenu: function () { // eslint-disable-line
+
+ closeMenu() {
let editor = this.get('editor');
// this.get('editor').unregisterKeyCommand('slash'); -- waiting for the next release for this
@@ -241,11 +242,12 @@ export default Component.extend({
});
this.sendAction('menuIsClosed');
},
- clickedMenu: function () { // eslint-disable-line
- // let{section, startOffset, endOffset} = this.get('range');
- window.editor.range.head.offset = this.get('range').startOffset - 1;
- window.editor.deleteRange(window.editor.range);
+ clickedMenu() {
+ let editor = this.get('editor');
+ // let{section, startOffset, endOffset} = this.get('range');
+ editor.range.head.offset = this.get('range').startOffset - 1;
+ editor.deleteRange(editor.range);
this.send('closeMenu');
}
}
diff --git a/ghost/admin/app/components/gh-editor-title.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-title-input.js
similarity index 54%
rename from ghost/admin/app/components/gh-editor-title.js
rename to ghost/admin/lib/gh-koenig/addon/components/koenig-title-input.js
index 2cddc50956..f29bc6fcf2 100644
--- a/ghost/admin/app/components/gh-editor-title.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-title-input.js
@@ -1,110 +1,25 @@
import Component from 'ember-component';
-import computed from 'ember-computed';
import run from 'ember-runloop';
import $ from 'jquery';
+import layout from '../templates/components/koenig-title-input';
export default Component.extend({
+ layout,
+
val: '',
- _cachedVal: '',
- _mutationObserver: null,
tagName: 'h2',
editor: null,
+ autofocus: false,
- koenigEditor: computed('editor', {
- get() {
- return this.get('editor');
- },
- set(key, value) {
- this.set('editor', value);
- }
- }),
+ _cachedVal: '',
+ _mutationObserver: null,
editorKeyDownListener: null,
- didRender() {
- let editor = this.get('editor');
+ _hasSetupEventListeners: false,
- let title = this.$('.gh-editor-title');
- if (!this.get('val')) {
- title.addClass('no-content');
- } else if (this.get('val') !== this.get('_cachedVal')) {
- title.html(this.get('val'));
- }
+ didInsertElement() {
+ this._super(...arguments);
- 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) {
- switch (event.keyCode) {
- case 66: // B
- case 98: // b
- case 73: // I
- case 105: // i
- case 85: // U
- case 117: // u
- return false;
- }
- }
- if (event.keyCode === 13) {
- // enter
- // on enter create a new paragraph at the top of the editor, this is because the first item may be a card.
- editor.run((postEditor) => {
- let marker = editor.builder.createMarker('');
- let newSection = editor.builder.createMarkupSection('p', [marker]);
- postEditor.insertSectionBefore(editor.post.sections, newSection, editor.post.sections.head);
-
- let range = newSection.toRange();
- range.tail.offset = 0; // colapse range
- postEditor.setRange(range);
- });
- return false;
- }
-
- // down key
- // if we're within ten pixels of the bottom of this element then we try and figure out where to position
- // the cursor in the editor.
- if (event.keyCode === 40) {
- if (!window.getSelection().rangeCount) {
- return;
- }
- let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
- let cursorPositionOnScreen = range.getBoundingClientRect();
-
- // in safari getBoundingClientRect on a range does not work if the range is collapsed.
- if (cursorPositionOnScreen.bottom === 0) {
- cursorPositionOnScreen = range.getClientRects()[0];
- }
-
- let offset = title.offset();
- let bottomOfHeading = offset.top + title.height();
- if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
- let editor = this.get('editor');
- let loc = editor.element.getBoundingClientRect();
-
- // if the first element is a card then that is always going to be selected.
- if (editor.post.sections.head && editor.post.sections.head.isCardSection) {
- run.next(() => {
- window.getSelection().removeAllRanges();
- $(editor.post.sections.head.renderNode.element).children('div').click();
- });
- return;
- }
- let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
- if (!cursorPositionInEditor || cursorPositionInEditor.isBlank) {
- editor.element.focus();
- } else {
- editor.selectRange(cursorPositionInEditor.toRange());
- }
- return false;
- }
- }
- };
+ let title = this.$('.kg-title-input');
// setup mutation observer
let mutationObserver = new MutationObserver(() => {
@@ -140,14 +55,108 @@ export default Component.extend({
mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
this.set('_mutationObserver', mutationObserver);
},
+
+ didReceiveAttrs() {
+ if (this.get('editorHasRendered') && !this._hasSetupEventListeners) {
+ let editor = this.get('editor');
+ let title = this.$('.kg-title-input');
+
+ 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) {
+ switch (event.keyCode) {
+ case 66: // B
+ case 98: // b
+ case 73: // I
+ case 105: // i
+ case 85: // U
+ case 117: // u
+ return false;
+ }
+ }
+ if (event.keyCode === 13) {
+ // enter
+ // on enter create a new paragraph at the top of the editor, this is because the first item may be a card.
+ editor.run((postEditor) => {
+ let marker = editor.builder.createMarker('');
+ let newSection = editor.builder.createMarkupSection('p', [marker]);
+ postEditor.insertSectionBefore(editor.post.sections, newSection, editor.post.sections.head);
+
+ let range = newSection.toRange();
+ range.tail.offset = 0; // colapse range
+ postEditor.setRange(range);
+ });
+ return false;
+ }
+
+ // down key
+ // if we're within ten pixels of the bottom of this element then we try and figure out where to position
+ // the cursor in the editor.
+ if (event.keyCode === 40) {
+ if (!window.getSelection().rangeCount) {
+ return;
+ }
+ let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
+ let cursorPositionOnScreen = range.getBoundingClientRect();
+
+ // in safari getBoundingClientRect on a range does not work if the range is collapsed.
+ if (cursorPositionOnScreen.bottom === 0) {
+ cursorPositionOnScreen = range.getClientRects()[0];
+ }
+
+ let offset = title.offset();
+ let bottomOfHeading = offset.top + title.height();
+ if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
+ let editor = this.get('editor');
+ let loc = editor.element.getBoundingClientRect();
+
+ // if the first element is a card then that is always going to be selected.
+ if (editor.post.sections.head && editor.post.sections.head.isCardSection) {
+ run.next(() => {
+ window.getSelection().removeAllRanges();
+ $(editor.post.sections.head.renderNode.element).children('div').click();
+ });
+ return;
+ }
+ let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
+ if (!cursorPositionInEditor || cursorPositionInEditor.isBlank) {
+ editor.element.focus();
+ } else {
+ editor.selectRange(cursorPositionInEditor.toRange());
+ }
+ return false;
+ }
+ }
+ };
+
+ this._hasSetupEventListeners = true;
+ }
+ },
+
+ didRender() {
+ let title = this.$('.kg-title-input');
+ if (!this.get('val')) {
+ title.addClass('no-content');
+ } else if (this.get('val') !== this.get('_cachedVal')) {
+ title.html(this.get('val'));
+ }
+ },
+
willDestroyElement() {
this.get('_mutationObserver').disconnect();
- this.$('.gh-editor-title')[0].onkeydown = null;
+ this.$('.kg-title-input')[0].onkeydown = null;
let editor = this.get('editor');
if (editor) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
},
+
editorKeyDown(event) {
// if the editor has a menu open then we don't want to capture inputs.
if (this.get('editorMenuIsOpen')) {
@@ -184,9 +193,10 @@ export default Component.extend({
}
}
},
+
// 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 [title] = this.$('.kg-title-input')[0].childNodes;
if (!title || !title.textContent) {
return 0;
}
@@ -201,8 +211,9 @@ export default Component.extend({
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`
+ // 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 i + (horizontalOffset >= (rect.left + rect.right) / 2 ? 1 : 0);
}
}
@@ -214,7 +225,7 @@ export default Component.extend({
// In Chrome it ignores the new range and places the cursor at the start of the element.
// in Firefox it places the cursor at the correct place but refuses to accept keyboard input.
setCursorAtOffset(offset) {
- let [title] = this.$('.gh-editor-title');
+ let [title] = this.$('.kg-title-input');
title.focus();
let selection = window.getSelection();
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js
index 3d04e8e335..7b3ee3cd43 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js
@@ -1,30 +1,48 @@
import Component from 'ember-component';
+import computed from 'ember-computed';
import layout from '../templates/components/koenig-toolbar-button';
export default Component.extend({
layout,
tagName: 'button',
- classNameBindings: ['selected', 'primary', 'secondary',
- 'gh-toolbar-btn-bold', 'gh-toolbar-btn-italic', 'gh-toolbar-btn-strike', 'gh-toolbar-btn-link', 'gh-toolbar-btn-h1', 'gh-toolbar-btn-h2', 'gh-toolbar-btn-quote'],
+
+ attributeBindings: ['title'],
classNames: ['gh-toolbar-btn'],
- attributesBindings: ['title'],
- title: 'bold',
+ // TODO: what do selected/primary/secondary classes relate to? Some tools
+ // have 'primary' added but none of them appear do anything/be used elsewhere
+ classNameBindings: [
+ 'selected',
+ 'buttonClass',
+ 'visibilityClass'
+ ],
+
+ // exernally set properties
+ tool: null,
+ editor: null,
+
+ buttonClass: computed('tool.class', function () {
+ return `gh-toolbar-btn-${this.get('tool.class')}`;
+ }),
+
+ // returns "primary" or null
+ visibilityClass: computed('tool.visibility', function() {
+ return this.get('tool.visibility');
+ }),
+
+ title: computed('tool.label', function () {
+ return this.get('tool.label');
+ }),
- // todo title="Bold", https://github.com/TryGhost/Ghost-Editor/commit/1133a9a7506f409b1b4fae6639c84c94c74dcebf
- // actions: {
click() {
- this.tool.onClick(this.editor);
+ this.tool.onClick(this.get('editor'));
},
- // },
- //
- willRender() {
- this.set(`gh-toolbar-btn-${this.tool.class}`, true);
- if (this.tool.selected) {
- this.set('selected', true);
- } else {
- this.set('selected', false);
- }
+ willRender() {
+ // TODO: "selected" doesn't appear to do anything for toolbar items -
+ // it's only used within card menus
+ this.set('selected', !!this.tool.selected);
+
+ // sets the primary/secondary/
if (this.tool.visibility) {
this.set(this.tool.visibility, true);
}
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js
index bb4828ac68..a15c10ec15 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js
@@ -21,6 +21,7 @@ export default Component.extend({
this.iconURL = `${this.get('assetPath')}/tools/`;
},
+
didRender() {
let $this = this.$();
let editor = this.get('editor');
@@ -32,7 +33,6 @@ export default Component.extend({
}
editor.cursorDidChange(() => {
-
// if there is no cursor:
if (!editor.range || !editor.range.head.section || !editor.range.head.section.isBlank
|| editor.range.head.section.renderNode._element.tagName.toLowerCase() !== 'p') {
@@ -69,7 +69,6 @@ export default Component.extend({
});
this.propertyDidChange('toolbar');
-
});
}
});
diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js
index d6adda8b3a..6022bf8374 100644
--- a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js
+++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js
@@ -10,11 +10,40 @@ import {getPositionFromRange} from '../lib/utils';
export default Component.extend({
layout,
classNames: ['gh-toolbar'],
- classNameBindings: ['isVisible', 'isLink', 'tickFullLeft', 'tickHalfLeft', 'tickFullRight', 'tickHalfRight', 'tickAbove', 'isTouch'],
- isVisible: false,
- tools: [],
+
+ // if any of the associated properties are true these class names will
+ // be dasherized and added to the element
+ classNameBindings: [
+ 'isVisible',
+ 'isLink',
+ 'tickFullLeft',
+ 'tickHalfLeft',
+ 'tickFullRight',
+ 'tickHalfRight',
+ 'tickAbove',
+ 'isTouch'
+ ],
+
+ // externally set properties
+ editor: null,
+ assetPath: null,
+ containerSelector: null,
+ isTouch: null,
+
+ // internal properties
hasRendered: false,
activeTags: null,
+ tools: [],
+ isVisible: false,
+ tickFullLeft: false,
+ tickFullRight: false,
+ tickHalfLeft: false,
+ tickHalfRight: false,
+ tickAbove: false,
+
+ _isLink: false,
+
+ // TODO: why is this not just a property?
isLink: computed({
get() {
return this._isLink;
@@ -190,6 +219,7 @@ export default Component.extend({
event.stopPropagation();
}
},
+
doLink(range) {
// if a link is already selected then we remove the links from within the range.
let currentLinks = this.get('activeTags').filter((element) => element.tagName === 'a');
@@ -210,9 +240,9 @@ export default Component.extend({
}
);
},
+
closeLink() {
this.set('isLink', false);
}
}
});
-
diff --git a/ghost/admin/lib/gh-koenig/addon/lib/card-factory.js b/ghost/admin/lib/gh-koenig/addon/lib/card-factory.js
index cb32249009..5ab8bdd64b 100644
--- a/ghost/admin/lib/gh-koenig/addon/lib/card-factory.js
+++ b/ghost/admin/lib/gh-koenig/addon/lib/card-factory.js
@@ -26,7 +26,7 @@ export default function createCardFactory(toolbar) {
// setupUI({env, options, payload});
- // todo setup non ember UI
+ // TODO: setup non ember UI
let payload = copy(_payload);
payload.card_name = env.name;
diff --git a/ghost/admin/lib/gh-koenig/addon/options/key-commands.js b/ghost/admin/lib/gh-koenig/addon/options/key-commands.js
new file mode 100644
index 0000000000..48e077cd1e
--- /dev/null
+++ b/ghost/admin/lib/gh-koenig/addon/options/key-commands.js
@@ -0,0 +1,18 @@
+// Key commands will run any time a particular key or key combination is pressed
+// https://github.com/bustlelabs/mobiledoc-kit#configuring-hot-keys
+
+export default function (editor) {
+
+ let softReturnKeyCommand = {
+ str: 'SHIFT+ENTER',
+
+ run(editor) {
+ editor.run((postEditor) => {
+ let softReturn = postEditor.builder.createAtom('soft-return');
+ postEditor.insertMarkers(editor.range.head, [softReturn]);
+ });
+ }
+ };
+ editor.registerKeyCommand(softReturnKeyCommand);
+
+}
diff --git a/ghost/admin/lib/gh-koenig/addon/options/default-commands.js b/ghost/admin/lib/gh-koenig/addon/options/text-expansions.js
similarity index 90%
rename from ghost/admin/lib/gh-koenig/addon/options/default-commands.js
rename to ghost/admin/lib/gh-koenig/addon/options/text-expansions.js
index 928dc76a58..bdb85fcb6d 100644
--- a/ghost/admin/lib/gh-koenig/addon/options/default-commands.js
+++ b/ghost/admin/lib/gh-koenig/addon/options/text-expansions.js
@@ -1,12 +1,20 @@
/* eslint-disable ember-suave/prefer-destructuring, no-unused-vars */
-import {replaceWithListSection, replaceWithHeaderSection} from 'mobiledoc-kit/editor/text-input-handlers';
+import {
+ replaceWithHeaderSection,
+ replaceWithListSection
+} from 'mobiledoc-kit/editor/text-input-handlers';
+
+// Text expansions watch text entry events and will look for matches, replacing
+// the matches with additional markup, atoms, or cards
+// https://github.com/bustlelabs/mobiledoc-kit#responding-to-text-input
export default function (editor) {
- // We don't want to run all our content rules on every text entry event, instead we check to see if this text entry
- // event could match a content rule, and only then run the rules.
- // Right now we only want to match content ending with *, _, ), ~, and `. This could increase as we support more
- // markdown.
+ // We don't want to run all our content rules on every text entry event,
+ // instead we check to see if this text entry event could match a content
+ // rule, and only then run the rules. Right now we only want to match
+ // content ending with *, _, ), ~, and `. This could increase as we support
+ // more markdown.
editor.onTextInput({
name: 'inline_markdown',
@@ -54,19 +62,6 @@ export default function (editor) {
}
});
- // soft return
- let softReturnKeyCommand = {
- str: 'SHIFT+ENTER',
-
- run(editor) {
- editor.run((postEditor) => {
- let mention = postEditor.builder.createAtom('soft-return');
- postEditor.insertMarkers(editor.range.head, [mention]);
- });
- }
- };
- editor.registerKeyCommand(softReturnKeyCommand);
-
// inline matches
function matchStrongStar(editor, text) {
let {range} = editor;
diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs
index c8db99338f..707954aac0 100644
--- a/ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs
+++ b/ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs
@@ -1,4 +1,3 @@
-
{{#if url}}
{{else if file}}
diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs
index 86f325738c..e04044047f 100644
--- a/ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs
+++ b/ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs
@@ -1,5 +1,12 @@
{{#if isEditing}}
-
+
{{else}}
{{{preview}}}
-{{/if}}
\ No newline at end of file
+{{/if}}
diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs
index eb0e242873..6bf2083957 100644
--- a/ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs
+++ b/ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs
@@ -1,50 +1,57 @@
+{{yield (hash
+ editor=editor
+ isMenuOpen=isMenuOpen
+ hasRendered=editorHasRendered
+)}}
+
+