diff --git a/ghost/admin/app/mixins/editor-base-controller.js b/ghost/admin/app/mixins/editor-base-controller.js index 5906b8634a..2733dca161 100644 --- a/ghost/admin/app/mixins/editor-base-controller.js +++ b/ghost/admin/app/mixins/editor-base-controller.js @@ -571,19 +571,7 @@ export default Mixin.create({ this.toggleProperty('showReAuthenticateModal'); }, - setEditor(editor) { - this.set('editor', editor); - }, - - editorMenuIsOpen() { - this.set('editorMenuIsOpen', true); - }, - - editorMenuIsClosed() { - this.set('editorMenuIsOpen', false); - }, - - wordcountDidChange(wordcount) { + setWordcount(wordcount) { this.set('wordcount', wordcount); } } diff --git a/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css b/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css index 6eb729fdfe..625edca54c 100644 --- a/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css +++ b/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css @@ -2,7 +2,7 @@ @import "koenig-menu.css"; @import "../ghost-editor/cardmenu.css"; -.editor-holder { +.gh-koenig-container { height: 100%; } .gh-koenig { @@ -10,6 +10,71 @@ -webkit-overflow-scrolling: touch; } +/* Title +/* ---------------------------------------------------------- */ + +.kg-title-input { + flex-grow: 1; + margin-bottom: 2vw; + padding: 0; + margin: 0; + outline: none; + position: relative; + width: 100%; + letter-spacing: 0.8px; + font-weight: bold; + font-size: 3.2rem; + line-height: 1.3em; + min-height: 1.3em; + z-index: 1; +} + +/* Place holder content that displays in the title if it is empty */ +.kg-title-input.no-content:before { + content: attr(data-placeholder); + color: color(var(--midgrey) l(+35%)); + cursor: text; + position: relative; + top: 0; + font-size: 3.2rem; + 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; +} + + +.kg-title-input input { + margin: 0; + padding: 0; + width: 100%; + border: 0; + background: transparent; + color: var(--darkgrey); + font-size: 3.2rem; + font-weight: bold; + letter-spacing: 0.8px; +} + +.kg-title-input input::-webkit-input-placeholder { + color: color(var(--midgrey) l(+25%)); + font-weight: 400; + letter-spacing: 1.2px; +} + +.kg-title-input input:-ms-input-placeholder { + color: color(var(--midgrey) l(+25%)); + font-weight: 400; + letter-spacing: 1.2px; +} + +.kg-title-input input:focus { + outline: 0; +} + +/* Editor +/* ---------------------------------------------------------- */ + .__mobiledoc-editor { width: 100%; height: 100%; diff --git a/ghost/admin/app/styles/layouts/editor.css b/ghost/admin/app/styles/layouts/editor.css index 631e159e35..dd0082be3b 100644 --- a/ghost/admin/app/styles/layouts/editor.css +++ b/ghost/admin/app/styles/layouts/editor.css @@ -1,69 +1,6 @@ /* Editor /ghost/editor/ /* ---------------------------------------------------------- */ - -/* Title -/* ---------------------------------------------------------- */ - -.gh-editor-title { - flex-grow: 1; - margin-bottom: 2vw; - padding: 0; - margin: 0; - outline: none; - position: relative; - width: 100%; - letter-spacing: 0.8px; - font-weight: bold; - font-size: 3.2rem; - line-height: 1.3em; - min-height: 1.3em; - z-index: 1; -} - -/* Place holder content that displays in the title if it is empty */ -.gh-editor-title.no-content:before { - content: attr(data-placeholder); - color: color(var(--midgrey) l(+35%)); - cursor: text; - position: relative; - top: 0; - font-size: 3.2rem; - 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; -} - - -.gh-editor-title input { - margin: 0; - padding: 0; - width: 100%; - border: 0; - background: transparent; - color: var(--darkgrey); - font-size: 3.2rem; - font-weight: bold; - letter-spacing: 0.8px; -} - -.gh-editor-title input::-webkit-input-placeholder { - color: color(var(--midgrey) l(+25%)); - font-weight: 400; - letter-spacing: 1.2px; -} - -.gh-editor-title input:-ms-input-placeholder { - color: color(var(--midgrey) l(+25%)); - font-weight: 400; - letter-spacing: 1.2px; -} - -.gh-editor-title input:focus { - outline: 0; -} - .editor-options .dropdown-menu { top: 35px; right: 0; @@ -265,4 +202,4 @@ .gh-editor-wordcount { display: none; } -} \ No newline at end of file +} diff --git a/ghost/admin/app/templates/editor/edit.hbs b/ghost/admin/app/templates/editor/edit.hbs index 30ea664d82..3d3488e80f 100644 --- a/ghost/admin/app/templates/editor/edit.hbs +++ b/ghost/admin/app/templates/editor/edit.hbs @@ -26,32 +26,35 @@
- {{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 +)}} + +
+
+
+ {{#each emberCards as |card index|}} {{#ember-wormhole to=card.id}} - {{koenig-card - tabindex=index - card=card - apiRoot=apiRoot - assetPath=assetPath - selectCard=(action "selectCard") - selectCardHard=(action "selectCardHard") - deselectCard=(action "deselectCard") - edit=(action "editCard") - stopEdit=(action "stopEditingCard") + {{koenig-card + tabindex=index + card=card + apiRoot=apiRoot + assetPath=assetPath + selectCard=(action "selectCard") + selectCardHard=(action "selectCardHard") + deselectCard=(action "deselectCard") + edit=(action "editCard") + stopEdit=(action "stopEditingCard") editedCard=editedCard - resize=resizeEvent deleteCard=(action "deleteCard") }} {{/ember-wormhole}} {{/each}} -
-
-
-{{yield}} +{{!-- Popup formatting toolbar when text is selected --}} +{{koenig-toolbar + editor=editor + assetPath=assetPath + containerSelector=containerSelector + isTouch=isTouch +}} -{{koenig-toolbar - editor=editor - assetPath=assetPath - containerSelector=containerSelector +{{koenig-slash-menu + editor=editor + assetPath=assetPath + containerSelector=containerSelector + menuIsOpen=(action "menuOpened") + menuIsClosed=(action "menuClosed") isTouch=isTouch - resize=resizeEvent }} -{{koenig-slash-menu - editor=editor - assetPath=assetPath - containerSelector=containerSelector + +{{koenig-plus-menu + editor=editor + assetPath=assetPath + containerSelector=containerSelector + isTouch=isTouch + menuIsOpen=(action "menuOpened") + menuIsClosed=(action "menuClosed") isTouch=isTouch - menuIsOpen=(action "menuIsOpen") - menuIsClosed=(action "menuIsClosed") - resize=resizeEvent }} -{{koenig-plus-menu - editor=editor - assetPath=assetPath - containerSelector=containerSelector - isTouch=isTouch - menuIsOpen=(action "menuIsOpen") - menuIsClosed=(action "menuIsClosed") - isTouch=isTouch - resize=resizeEvent -}} \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs index 6e615f5546..0630370579 100644 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs @@ -28,7 +28,9 @@ Edit {{/if}} - + {{/if}}
diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs index fff08433c1..77c3e576da 100644 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs @@ -5,7 +5,11 @@
-{{/if}} \ No newline at end of file +{{/if}} diff --git a/ghost/admin/app/templates/components/gh-editor-title.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-title-input.hbs similarity index 55% rename from ghost/admin/app/templates/components/gh-editor-title.hbs rename to ghost/admin/lib/gh-koenig/addon/templates/components/koenig-title-input.hbs index 043356c835..dc3069066f 100644 --- a/ghost/admin/app/templates/components/gh-editor-title.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-title-input.hbs @@ -1 +1 @@ -
\ No newline at end of file +
diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs index 2c484733f3..56c4649294 100644 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs @@ -1,12 +1,21 @@ {{#if isLink}} - {{input keyDown=(action "linkKeyDown") keyPress=(action "linkKeyPress") autofocus=true placeholder="Enter a link"}} + {{input + placeholder="Enter a link" + keyDown=(action "linkKeyDown") + keyPress=(action "linkKeyPress") + autofocus=true + }} {{else}} {{#each toolbar as |tool|}} - {{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}} + {{koenig-toolbar-button + tool=tool + editor=editor}} {{/each}} {{#each toolbarBlocks as |tool|}} - {{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}} + {{koenig-toolbar-button + tool=tool + editor=editor}} {{/each}} -{{/if}} \ No newline at end of file +{{/if}} diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-title-input.js b/ghost/admin/lib/gh-koenig/app/components/koenig-title-input.js new file mode 100644 index 0000000000..a1246be474 --- /dev/null +++ b/ghost/admin/lib/gh-koenig/app/components/koenig-title-input.js @@ -0,0 +1 @@ +export {default} from 'gh-koenig/components/koenig-title-input'; diff --git a/ghost/admin/lib/gh-koenig/test-support/.eslintrc.js b/ghost/admin/lib/gh-koenig/test-support/.eslintrc.js new file mode 100644 index 0000000000..46f3bb1902 --- /dev/null +++ b/ghost/admin/lib/gh-koenig/test-support/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + env: { + 'embertest': true, + 'mocha': true + }, + globals: { + server: false, + expect: false, + fileUpload: false, + + // ember-power-select test helpers + selectChoose: false, + selectSearch: false, + removeMultipleOption: false, + clearSelected: false, + + // ember-power-datepicker test helpers + datepickerSelect: false + } +}; diff --git a/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-slashmenu-test.js b/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-slashmenu-test.js new file mode 100644 index 0000000000..09ffa0c99b --- /dev/null +++ b/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-slashmenu-test.js @@ -0,0 +1,91 @@ +/* jshint expr:true */ +import {expect} from 'chai'; +import {describe, it} from 'mocha'; +import {setupComponentTest} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; +import { + findEditor, + focusEditor, + inputText, + waitForRender, + EMPTY_DOC +} from '../../helpers/editor-helpers'; +import $ from 'jquery'; + +describe('gh-koenig: Integration: Component: gh-koenig-slashmenu', function () { + setupComponentTest('gh-koenig-slashmenu', { + integration: true + }); + + beforeEach(function () { + this.set('value', EMPTY_DOC); + }); + + it('shows menu when / is typed', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + let editor = findEditor(); + await focusEditor(); + await inputText(editor, '/'); + await waitForRender('.gh-cardmenu'); + + let cardMenu = $('.gh-cardmenu'); + expect(cardMenu.children().length).to.equal(7); + }); + + it('filters tools when a user types', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + let editor = findEditor(); + await focusEditor(); + await inputText(editor, '/'); + await waitForRender('.gh-cardmenu'); + + let cardMenu = $('.gh-cardmenu'); + expect(cardMenu.children().length).to.equal(7); + + await inputText(editor, ' bul'); + expect(cardMenu.children().length).to.equal(1); + }); + + it('inserts card/markup when clicked'); + it('inserts card/markup when enter is pressed'); + + it.skip('ul tool', async function () { + this.set('editorMenuIsOpen', function () {}); + this.set('editorMenuIsClosed', function () {}); + + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + menuIsOpen=editorMenuIsOpen + menuIsClosed=editorMenuIsClosed + }}`); + + let editor = findEditor(); + await focusEditor(); + await inputText(editor, '/'); + await waitForRender('.gh-cardmenu'); + + let cardMenu = $('.gh-cardmenu'); + expect(cardMenu.children().length).to.equal(7); + + await inputText(editor, ' bul'); + expect(cardMenu.children().length).to.equal(1); + + await click('.gh-cardmenu-card'); + // TODO: check inner HTML + }); +}); diff --git a/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-test.js b/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-test.js new file mode 100644 index 0000000000..c257b95ace --- /dev/null +++ b/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-test.js @@ -0,0 +1,309 @@ +/* jshint expr:true */ +import {describe, it} from 'mocha'; +import {setupComponentTest} from 'ember-mocha'; +import hbs from 'htmlbars-inline-precompile'; +import { + testEditorInput, + testEditorInputTimeout, + EMPTY_DOC +} from '../../helpers/editor-helpers'; +import sinon from 'sinon'; + +describe('gh-koenig: Integration: Component: gh-koenig', function () { + setupComponentTest('gh-koenig', { + integration: true + }); + + beforeEach(function () { + this.set('value', EMPTY_DOC); + }); + + it('fires change and word-count events', async function () { + // set defaults + this.set('onFirstChange', sinon.spy()); + this.set('onChange', sinon.spy()); + + this.set('wordcount', 0); + this.set('actions.wordcountDidChange', function (wordcount) { + this.set('wordcount', wordcount); + }); + + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + onChange=(action onChange) + onFirstChange=(action onFirstChange) + wordcountDidChange=(action 'wordcountDidChange') + }}`); + + await testEditorInput('abcd efg hijk lmnop', '

abcd efg hijk lmnop

', expect); + + expect(this.get('onFirstChange').calledOnce, 'onFirstChanged called once').to.be.true; + expect(this.get('onChange').called, 'onChange called').to.be.true; + expect(this.get('wordcount'), 'wordcount').to.equal(4); + }); + + describe('Markerable markdown support.', function () { + it('plain text inputs (placebo)', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('abcdef', '

abcdef

', expect); + }); + + // bold + it('** bolds at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('**test**', '

test

', expect); + }); + + it('** bolds in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('123**test**', '

123test

', expect); + }); + + it('__ bolds at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('__test__', '

test

', expect); + }); + + it('__ bolds in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('__test__', '

test

', expect); + }); + + // italic + it('* italicises at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('*test*', '

test

', expect); + }); + + it('* italicises in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('123*test*', '

123test

', expect); + }); + + it('_ italicises at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('_test_', '

test

', expect); + }); + + it('_ italicises in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('123_test_', '

123test

', expect); + }); + + // strikethrough + it('~~ strikethroughs at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('~~test~~', '

test

', expect); + }); + it('~~ strikethroughs in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('123~~test~~', '

123test

', expect); + }); + + // links + it('[]() creates a link at start of line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput( + '[ghost](https://www.ghost.org/)', + '

ghost

', + expect); + }); + + it('[]() creates a link in a line', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput( + '123[ghost](https://www.ghost.org/)', + '

123ghost

', + expect); + }); + }); + + describe('Block markdown support', function () { + // headings + it('# creates an H1', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('# ', '


', expect); + }); + + it('## creates an H2', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('## ', '


', expect); + }); + + it('### creates an H3', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('### ', '


', expect); + }); + + // lists + it('* creates an UL', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('* ', '', expect); + }); + + it('- creates an UL', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('- ', '', expect); + }); + + it('1. creates an OL', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('1. ', '

', expect); + }); + + // quote + it('> creates an blockquote', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + await testEditorInput('> ', '

', expect); + }); + }); + + describe('Card markdown support.', function () { + it('![]() creates an image card', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + let value = await testEditorInputTimeout('![image of something](https://unsplash.it/200/300/?random)'); + expect(value).to.have.string('kg-card-image'); + }); + + it('``` creates a markdown card.', async function () { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.gh-koenig-container' + mobiledoc=value + }}`); + + let value = await testEditorInputTimeout('```some code```'); + expect(value).to.have.string('kg-card-markdown'); + }); + }); +}); diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/gh-koenig-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/gh-koenig-test.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-card-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-card-test.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-menu-item-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-menu-item-test.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-menu-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-menu-test.js deleted file mode 100644 index caa33ee872..0000000000 --- a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-menu-test.js +++ /dev/null @@ -1,21 +0,0 @@ -/* jshint expr:true */ -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {setupComponentTest} from 'ember-mocha'; -import {editorShim} from '../../utils'; - -describe.skip('Unit: Component: koenig-menu', function () { - setupComponentTest('koenig-menu', { - unit: true - }); - - it('renders', function () { - let component = this.subject(); - component.editor = editorShim; - expect(component._state).to.equal('preRender'); - - // renders the component on the page - this.render(); - expect(component._state).to.equal('inDOM'); - }); -}); diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js index 409dd642ac..9aa09717a6 100644 --- a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js +++ b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js @@ -3,8 +3,7 @@ import {expect} from 'chai'; import {describe, it} from 'mocha'; import {setupComponentTest} from 'ember-mocha'; - -describe.skip('Unit: Component: koenig-toolbar-button', function () { +describe('gh-koenig: Unit: Component: koenig-toolbar-button', function () { setupComponentTest('koenig-toolbar-button', { unit: true }); diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js index 347f40950b..9e66f4e1f3 100644 --- a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js +++ b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js @@ -4,8 +4,7 @@ import {describe, it} from 'mocha'; import {setupComponentTest} from 'ember-mocha'; import {editorShim} from '../../utils'; - -describe.skip('Unit: Component: koenig-toolbar-newitem', function () { +describe('gh-koenig: Unit: Component: koenig-toolbar-newitem', function () { setupComponentTest('koenig-toolbar-newitem', { unit: true, needs: [ @@ -14,7 +13,6 @@ describe.skip('Unit: Component: koenig-toolbar-newitem', function () { }); it('renders', function () { - let component = this.subject(); component.editor = editorShim; expect(component._state).to.equal('preRender'); diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem.js deleted file mode 100644 index 62e7048886..0000000000 --- a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem.js +++ /dev/null @@ -1,22 +0,0 @@ -/* jshint expr:true */ -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {setupComponentTest} from 'ember-mocha'; -import sinon from 'sinon'; - -describe.skip('Unit: Component: koenig-toolbar', function () { - setupComponentTest('koenig-toolbar', { - unit: true - }); - - it('The toolbar is not rendered by default.', function () { - let component = this.subject(); - expect(component.isVisible).to.be.false; - }); - - it('The toolbar contains tools.', function () { - let component = this.subject(); - expect(component.get('toolbar').length).to.be.greaterThan(0); // the standard toolbar tools (strong, em, strikethrough, link) - expect(component.get('toolbarBlocks').length).to.be.greaterThan(0); // extended toolbar block bases tools (h1, h2, quote); - }); -}); diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js index b40678a97a..3bc053f62a 100644 --- a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js +++ b/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js @@ -2,22 +2,23 @@ import {expect} from 'chai'; import {describe, it} from 'mocha'; import {setupComponentTest} from 'ember-mocha'; -import sinon from 'sinon'; -describe.skip('Unit: Component: koenig-toolbar', function () { +describe('gh-koenig: Unit: Component: koenig-toolbar', function () { setupComponentTest('koenig-toolbar', { unit: true }); - it('The toolbar is not visible by default.', function () { + it('is not visible by default', function () { let component = this.subject(); expect(component.isVisible).to.be.false; }); - it('The toolbar contains tools.', function () { + it('contains tools', function () { let component = this.subject(); - expect(component.get('toolbar').length).to.be.greaterThan(0); // the standard toolbar tools (strong, em, strikethrough, link) - expect(component.get('toolbarBlocks').length).to.be.greaterThan(0); // extended toolbar block bases tools (h1, h2, quote); + // the standard toolbar tools (strong, em, strikethrough, link) + expect(component.get('toolbar').length).to.be.greaterThan(0); + // extended toolbar block bases tools (h1, h2, quote); + expect(component.get('toolbarBlocks').length).to.be.greaterThan(0); }); // it('The toolbar appears when a range is selected.', function () { diff --git a/ghost/admin/lib/gh-koenig/test-support/utils.js b/ghost/admin/lib/gh-koenig/test-support/utils.js index 11de8ec826..5e10829b93 100644 --- a/ghost/admin/lib/gh-koenig/test-support/utils.js +++ b/ghost/admin/lib/gh-koenig/test-support/utils.js @@ -1,15 +1,15 @@ -export let editorShim = { - range: { - head: { - section: { +export let editorShim = { + range: { + head: { + section: { renderNode: { _element: { tagName: 'P' } }, isBlank: false - } - } + } + } }, - cursorDidChange: function() {} + cursorDidChange() {} }; diff --git a/ghost/admin/tests/acceptance/editor-test.js b/ghost/admin/tests/acceptance/editor-test.js index 226b63e113..90ebb4b254 100644 --- a/ghost/admin/tests/acceptance/editor-test.js +++ b/ghost/admin/tests/acceptance/editor-test.js @@ -328,7 +328,7 @@ describe('Acceptance: Editor', function() { titleRendered(); - let title = find('#gh-editor-title div'); + let title = find('#koenig-title-input div'); title.html(Array(160).join('a')); await click(testSelector('publishmenu-trigger')); @@ -356,7 +356,7 @@ describe('Acceptance: Editor', function() { titleRendered(); - let title = find('#gh-editor-title div'); + let title = find('#koenig-title-input div'); expect(title.data('placeholder')).to.equal('Your Post Title'); expect(title.hasClass('no-content')).to.be.false; await title.html(''); @@ -378,7 +378,7 @@ describe('Acceptance: Editor', function() { titleRendered(); - let title = find('#gh-editor-title div'); + let title = find('#koenig-title-input div'); await replaceTitleHTML('
TITLE      TEST
 '); expect(title.html()).to.equal('TITLE TEST '); }); diff --git a/ghost/admin/tests/helpers/editor-helpers.js b/ghost/admin/tests/helpers/editor-helpers.js index 9bb92d711b..c35bd084c6 100644 --- a/ghost/admin/tests/helpers/editor-helpers.js +++ b/ghost/admin/tests/helpers/editor-helpers.js @@ -2,27 +2,50 @@ import Ember from 'ember'; import $ from 'jquery'; import run from 'ember-runloop'; import wait from 'ember-test-helpers/wait'; -import {findWithAssert} from 'ember-native-dom-helpers'; +import {find, findWithAssert, waitUntil} from 'ember-native-dom-helpers'; +import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc'; +import {TESTING_EXPANDO_PROPERTY} from 'gh-koenig/components/gh-koenig'; -// polls the editor until it's started. -export function editorRendered() { - return Ember.Test.promise(function (resolve) { // eslint-disable-line - function checkEditor() { - if (window.editor) { - return resolve(); - } else { - window.requestAnimationFrame(checkEditor); - } +export const EMPTY_DOC = { + version: MOBILEDOC_VERSION, + markups: [], + atoms: [], + cards: [], + sections: [] +}; + +// traverse up the node tree looking for an editor instance +export function findEditor(element) { + if (!element) { + // TODO: get the selector from the editor component + element = findWithAssert('.gh-koenig-container'); + } + + if (typeof element === 'string') { + element = findWithAssert(element); + } + + do { + if (element[TESTING_EXPANDO_PROPERTY]) { + return element[TESTING_EXPANDO_PROPERTY]; } - checkEditor(); - }); + element = element.parentNode; + } while (!!element); // eslint-disable-line + + throw new Error('Unable to find gh-koenig editor from element'); +} + +export function focusEditor(element) { + let editor = findEditor(element); + run(() => editor.element.focus()); + return (window.wait || wait); } // polls the title until it's started. export function titleRendered() { return Ember.Test.promise(function (resolve) { // eslint-disable-line function checkTitle() { - let title = $('#gh-editor-title div'); + let title = $('#koenig-title-input div'); if (title[0]) { return resolve(); } else { @@ -36,21 +59,24 @@ export function titleRendered() { // replaces the title text content with HTML and returns once the HTML has been placed. // takes into account converting to plaintext. export function replaceTitleHTML(HTML) { - let el = findWithAssert('#gh-editor-title div'); + let el = findWithAssert('#koenig-title-input div'); run(() => el.innerHTML = HTML); return (window.wait || wait)(); } -// simulates text inputs into the editor, unfortunately the helper Ember helper functions +// simulates text inputs into the editor, unfortunately the Ember helper functions // don't work on content editable so we have to manipuate the text input event manager // in mobiledoc-kit directly. This is a private API. export function inputText(editor, text) { - editor._eventManager._textInputHandler.handle(text); + run(() => { + editor._eventManager._textInputHandler.handle(text); + }); } // inputs text and waits for the editor to modify the dom with the desired result or timesout. -export function testInput(input, output, expect) { - window.editor.element.focus(); // for some reason the editor doesn't work until it's focused when run in ghost-admin. +export function testEditorInput(input, output, expect) { + let editor = findEditor(); + editor.element.focus(); // for some reason the editor doesn't work until it's focused when run in ghost-admin. return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line let lastRender = ''; let isRejected = false; @@ -59,53 +85,32 @@ export function testInput(input, output, expect) { reject(lastRender); isRejected = true; }, 500); - window.editor.didRender(() => { - lastRender = window.editor.element.innerHTML; - if (window.editor.element.innerHTML === output && !isRejected) { + editor.didRender(() => { + lastRender = editor.element.innerHTML; + if (editor.element.innerHTML === output && !isRejected) { window.clearTimeout(rejectTimeout); expect(lastRender).to.equal(output); // we know this is true but include it for the output. return resolve(lastRender); } }); - inputText(window.editor, input); + inputText(editor, input); }); } -export function testInputTimeout(input) { - window.editor.element.focus(); +export function testEditorInputTimeout(input) { + let editor = findEditor(); + editor.element.focus(); return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line window.setTimeout(() => { - resolve(window.editor.element.innerHTML); + resolve(editor.element.innerHTML); }, 300); - inputText(window.editor, input); + inputText(editor, input); }); } export function waitForRender(selector) { - let isRejected = false; - return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line - let rejectTimeout = window.setTimeout(() => { - reject('element didn\'t render'); - isRejected = true; - }, 1500); - - function checkIsRendered() { - if ($(selector)[0] && !isRejected) { - window.clearTimeout(rejectTimeout); - return resolve(); - } else { - window.requestAnimationFrame(checkIsRendered); - } - } - checkIsRendered(); + return waitUntil(() => { + return find(selector); }); } - -export function timeoutPromise(timeout) { - return Ember.Test.promise(function (resolve) { // eslint-disable-line - window.setTimeout(() => { - resolve(); - }, timeout); - }); -} \ No newline at end of file diff --git a/ghost/admin/tests/integration/components/gh-koenig-markdown-test.js b/ghost/admin/tests/integration/components/gh-koenig-markdown-test.js deleted file mode 100644 index 0508c86bbd..0000000000 --- a/ghost/admin/tests/integration/components/gh-koenig-markdown-test.js +++ /dev/null @@ -1,452 +0,0 @@ -/* jshint expr:true */ -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {setupComponentTest} from 'ember-mocha'; -import hbs from 'htmlbars-inline-precompile'; -import {editorRendered, testInput, testInputTimeout} from '../../helpers/editor-helpers'; - -describe('Integration: Component: gh-koenig', function () { - setupComponentTest('gh-koenig', { - integration: true - }); - - beforeEach(function () { - this.set('value', { - version: '0.3.1', - atoms: [], - markups: [], - cards: [], - sections: []}); - }); - - describe('Makerable markdown support.', function() { - it('plain text inputs (placebo)', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('abcdef', '

abcdef

', expect); - }) - .then(() => { - done(); - }); - }); - - // bold - it('** bolds at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('**test**', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('** bolds in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('123**test**', '

123test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('__ bolds at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('__test__', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('__ bolds in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('__test__', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - - // italic - it('* italicises at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('*test*', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('* italicises in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('123*test*', '

123test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('_ italicises at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('_test_', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - - it('_ italicises in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('123_test_', '

123test

', expect); - }) - .then(() => { - done(); - }); - }); - - // strikethrough - it('~~ strikethroughs at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('~~test~~', '

test

', expect); - }) - .then(() => { - done(); - }); - }); - it('~~ strikethroughs in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('123~~test~~', '

123test

', expect); - }) - .then(() => { - done(); - }); - }); - - // links - it('[]() creates a link at start of line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('[ghost](https://www.ghost.org/)', '

ghost

', expect); - }) - .then(() => { - done(); - }); - }); - - it('[]() creates a link in a line', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('123[ghost](https://www.ghost.org/)', '

123ghost

', expect); - }) - .then(() => { - done(); - }); - }); - }); - describe('Block markdown support', function () { - - // headings - it('# creates an H1', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('# ', '


', expect); - }) - .then(() => { - done(); - }); - }); - - it('## creates an H2', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('## ', '


', expect); - }) - .then(() => { - done(); - }); - }); - - it('### creates an H3', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('### ', '


', expect); - }) - .then(() => { - done(); - }); - }); - - // lists - it('* creates an UL', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('* ', '', expect); - }) - .then(() => { - done(); - }); - }); - - it('- creates an UL', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('- ', '', expect); - }) - .then(() => { - done(); - }); - }); - - it('1. creates an OL', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('1. ', '

', expect); - }) - .then(() => { - done(); - }); - }); - - // quote - it('> creates an blockquote', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('> ', '

', expect); - }) - .then(() => { - done(); - }); - }); - }); - - describe('Card markdown support.', function () { - it('![]() creates an image card', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInputTimeout('![image of something](https://unsplash.it/200/300/?random)'); - }) - .then((value) => { - expect(value).to.have.string('kg-card-image'); - done(); - }); - }); - it('``` creates a markdown card.', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInputTimeout('```some code```'); - }) - .then((value) => { - expect(value).to.have.string('kg-card-markdown'); - done(); - }); - }); - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js b/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js deleted file mode 100644 index 0170e86d83..0000000000 --- a/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js +++ /dev/null @@ -1,112 +0,0 @@ -/* jshint expr:true */ -import {expect} from 'chai'; -import {describe, it} from 'mocha'; -import {setupComponentTest} from 'ember-mocha'; -import hbs from 'htmlbars-inline-precompile'; -import {editorRendered, waitForRender, inputText, timeoutPromise} from '../../helpers/editor-helpers'; -import $ from 'jquery'; - -describe('Integration: Component: gh-koenig-slashmenu', function () { - setupComponentTest('gh-koenig', { - integration: true - }); - beforeEach(function () { - this.set('value', { - version: '0.3.1', - atoms: [], - markups: [], - cards: [], - sections: []}); - }); - - it('the slash menu appears on user input', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - inputText(editor, '/'); - return waitForRender('.gh-cardmenu'); - }) - .then(() => { - let cardMenu = $('.gh-cardmenu'); - expect(cardMenu.children().length).to.equal(7); - done(); - }); - }); - - it('searches when a user types', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - inputText(editor, '/'); - return waitForRender('.gh-cardmenu'); - }) - .then(() => { - let {editor} = window; - let cardMenu = $('.gh-cardmenu'); - expect(cardMenu.children().length).to.equal(7); - inputText(editor, ' bul'); - return timeoutPromise(500); - }) - .then(() => { - let cardMenu = $('.gh-cardmenu'); - expect(cardMenu.children().length).to.equal(1); - done(); - }); - }); - - it.skip('ul tool', function (done) { - this.set('editorMenuIsOpen', function () {}); - this.set('editorMenuIsClosed', function () {}); - - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - menuIsOpen=editorMenuIsOpen - menuIsClosed=editorMenuIsClosed - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - inputText(editor, '/'); - return waitForRender('.gh-cardmenu'); - }) - .then(() => { - let {editor} = window; - let cardMenu = $('.gh-cardmenu'); - expect(cardMenu.children().length).to.equal(7); - inputText(editor, ' bul'); - return timeoutPromise(500); - }) - .then(() => { - $('.gh-cardmenu-card').click(); - done(); - }); - // .then(() => { - // - // }) - // .then(() => { - // console.log(editor.element.innerHTML); - // done(); - // }); - }); -}); diff --git a/ghost/admin/tests/integration/components/gh-koenig-test.js b/ghost/admin/tests/integration/components/gh-koenig-test.js deleted file mode 100644 index 5f03025e21..0000000000 --- a/ghost/admin/tests/integration/components/gh-koenig-test.js +++ /dev/null @@ -1,56 +0,0 @@ -/* jshint expr:true */ -import {describe, it} from 'mocha'; -import {setupComponentTest} from 'ember-mocha'; -import hbs from 'htmlbars-inline-precompile'; -import {editorRendered, testInput} from '../../helpers/editor-helpers'; -import sinon from 'sinon'; - -describe.skip('Integration: Component: gh-koenig - General Editor Tests.', function () { - setupComponentTest('gh-koenig', { - integration: true - }); - - beforeEach(function () { - // set defaults - this.set('onFirstChange', sinon.spy()); - this.set('onChange', sinon.spy()); - - this.set('wordcount', 0); - this.set('actions.wordcountDidChange', function (wordcount) { - this.set('wordcount', wordcount); - }); - - this.set('value', { - version: '0.3.1', - atoms: [], - markups: [], - cards: [], - sections: []}); - - }); - - it('Check that events have fired', function (done) { - this.render(hbs`{{gh-koenig - apiRoot='/todo' - assetPath='/assets' - containerSelector='.editor-holder' - value=value - onChange=(action onChange) - onFirstChange=(action onFirstChange) - wordcountDidChange=(action 'wordcountDidChange') - }}`); - - editorRendered() - .then(() => { - let {editor} = window; - editor.element.focus(); - return testInput('abcd efg hijk lmnop', '

abcd efg hijk lmnop

', expect); - }) - .then(() => { - expect(this.get('onFirstChange').calledOnce).to.be.true; - expect(this.get('onChange').calledOnce).to.be.true; - expect(this.get('wordcount')).to.equal(4); - done(); - }); - }); -}); \ No newline at end of file