diff --git a/ghost/admin/app/components/gh-koenig-editor.js b/ghost/admin/app/components/gh-koenig-editor.js new file mode 100644 index 0000000000..572b0b21fb --- /dev/null +++ b/ghost/admin/app/components/gh-koenig-editor.js @@ -0,0 +1,120 @@ +import Component from '@ember/component'; + +export default Component.extend({ + + // public attrs + tagName: '', + title: '', + titlePlaceholder: '', + body: null, + bodyPlaceholder: '', + bodyAutofocus: false, + + // internal properties + _title: null, + _editor: null, + + // closure actions + onTitleChange() {}, + onTitleBlur() {}, + onBodyChange() {}, + + actions: { + // triggered when a click is registered on .gh-koenig-editor-pane + focusEditor(event) { + // if a click occurs on the editor canvas, focus the editor and put + // the cursor at the end of the document. Allows for a much larger + // hit area for focusing the editor when it has no or little content + if (event.target.tagName === 'ARTICLE' && event.target.classList.contains('koenig-editor')) { + let {post} = this._editor; + let range = post.toRange(); + + event.preventDefault(); + + this._editor.focus(); + this._editor.run((postEditor) => { + postEditor.setRange(range.tail.section.tailPosition()); + }); + } + }, + + /* title related actions -------------------------------------------- */ + + onTitleCreated(titleElement) { + this._title = titleElement; + }, + + onTitleChange(newTitle) { + this.onTitleChange(newTitle); + }, + + onTitleFocusOut() { + this.onTitleBlur(); + }, + + onTitleKeydown(event) { + let value = event.target.value; + let selectionStart = event.target.selectionStart; + + // enter will always focus the editor + // down arrow will only focus the editor when the cursor is at the + // end of the input to preserve the default OS behaviour + if ( + event.key === 'Enter' || + event.key === 'Tab' || + (event.key === 'ArrowDown' && (!value || selectionStart === value.length)) + ) { + event.preventDefault(); + this._editor.focus(); + } + }, + + /* body related actions --------------------------------------------- */ + + onEditorCreated(editor) { + this._setupEditor(editor); + }, + + onBodyChange(newMobiledoc) { + this.onBodyChange(newMobiledoc); + } + }, + + /* public methods ------------------------------------------------------- */ + + /* internal methods ----------------------------------------------------- */ + + _setupEditor(editor) { + let component = this; + + this._editor = editor; + + // focus the title when pressing UP if cursor is at the beginning of doc + editor.registerKeyCommand({ + str: 'UP', + run(editor) { + let cursorHead = editor.cursor.offsets.head; + + if ( + editor.hasCursor() + && cursorHead.offset === 0 + && (!cursorHead.section || !cursorHead.section.prev) + ) { + component._title.focus(); + return true; + } + + return false; + } + }); + + // focus the title when pressing SHIFT+TAB + editor.registerKeyCommand({ + str: 'SHIFT+TAB', + run() { + component._title.focus(); + return true; + } + }); + } +}); diff --git a/ghost/admin/app/components/gh-textarea.js b/ghost/admin/app/components/gh-textarea.js index 9e02361041..1f747b9952 100644 --- a/ghost/admin/app/components/gh-textarea.js +++ b/ghost/admin/app/components/gh-textarea.js @@ -27,6 +27,10 @@ export default OneWayTextarea.extend(TextInputMixin, { if (this.get('autoExpand')) { run.scheduleOnce('afterRender', this, this._setupAutoExpand); } + + if (this.get('didCreateTextarea')) { + this.get('didCreateTextarea')(this.element); + } }, willDestroyElement() { diff --git a/ghost/admin/app/models/post.js b/ghost/admin/app/models/post.js index 6b4aacf53e..ccf8c86a63 100644 --- a/ghost/admin/app/models/post.js +++ b/ghost/admin/app/models/post.js @@ -5,7 +5,7 @@ import attr from 'ember-data/attr'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import moment from 'moment'; import {BLANK_DOC as BLANK_MARKDOWN} from 'ghost-admin/components/gh-markdown-editor'; -import {BLANK_DOC as BLANK_MOBILEDOC} from 'gh-koenig/components/gh-koenig'; +import {BLANK_DOC as BLANK_MOBILEDOC} from 'koenig-editor/components/koenig-editor'; import {belongsTo, hasMany} from 'ember-data/relationships'; import {compare} from '@ember/utils'; import {computed} from '@ember/object'; @@ -344,8 +344,8 @@ export default Model.extend(Comparable, ValidationEngine, { isCompatibleWithMarkdownEditor() { let mobiledoc = this.get('mobiledoc'); - if ( - mobiledoc.markups.length === 0 + if (mobiledoc + && mobiledoc.markups.length === 0 && mobiledoc.cards.length === 1 && mobiledoc.cards[0][0] === 'card-markdown' && mobiledoc.sections.length === 1 diff --git a/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css b/ghost/admin/app/styles/addons/gh-koenig--old/gh-koenig.css similarity index 100% rename from ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css rename to ghost/admin/app/styles/addons/gh-koenig--old/gh-koenig.css diff --git a/ghost/admin/app/styles/addons/gh-koenig/koenig-cardmenu.css b/ghost/admin/app/styles/addons/gh-koenig--old/koenig-cardmenu.css similarity index 100% rename from ghost/admin/app/styles/addons/gh-koenig/koenig-cardmenu.css rename to ghost/admin/app/styles/addons/gh-koenig--old/koenig-cardmenu.css diff --git a/ghost/admin/app/styles/addons/gh-koenig/koenig-menu.css b/ghost/admin/app/styles/addons/gh-koenig--old/koenig-menu.css similarity index 100% rename from ghost/admin/app/styles/addons/gh-koenig/koenig-menu.css rename to ghost/admin/app/styles/addons/gh-koenig--old/koenig-menu.css diff --git a/ghost/admin/app/styles/addons/gh-koenig/koenig-toolbar-blockitem.css b/ghost/admin/app/styles/addons/gh-koenig--old/koenig-toolbar-blockitem.css similarity index 100% rename from ghost/admin/app/styles/addons/gh-koenig/koenig-toolbar-blockitem.css rename to ghost/admin/app/styles/addons/gh-koenig--old/koenig-toolbar-blockitem.css diff --git a/ghost/admin/app/styles/addons/gh-koenig/koenig-toolbar.css b/ghost/admin/app/styles/addons/gh-koenig--old/koenig-toolbar.css similarity index 100% rename from ghost/admin/app/styles/addons/gh-koenig/koenig-toolbar.css rename to ghost/admin/app/styles/addons/gh-koenig--old/koenig-toolbar.css diff --git a/ghost/admin/app/styles/app-dark.css b/ghost/admin/app/styles/app-dark.css index 6895987324..ee149cb3c7 100644 --- a/ghost/admin/app/styles/app-dark.css +++ b/ghost/admin/app/styles/app-dark.css @@ -34,6 +34,7 @@ @import "components/popovers.css"; @import "components/tour.css"; @import "components/unsplash.css"; +@import "components/koenig"; /* Layouts: Groups of Components @@ -54,11 +55,6 @@ @import "layouts/subscribers.css"; -/* Addons: gh-koenig -/* ---------------------------------------------------------- */ -@import "addons/gh-koenig/gh-koenig.css"; - - :root { --darkgrey: #e5eff5; --midgrey: #738a94; diff --git a/ghost/admin/app/styles/app.css b/ghost/admin/app/styles/app.css index 023435a8fd..f5cce9e0ea 100644 --- a/ghost/admin/app/styles/app.css +++ b/ghost/admin/app/styles/app.css @@ -34,6 +34,7 @@ @import "components/popovers.css"; @import "components/tour.css"; @import "components/unsplash.css"; +@import "components/koenig"; /* Layouts: Groups of Components @@ -54,11 +55,6 @@ @import "layouts/subscribers.css"; -/* Addons: gh-koenig -/* ---------------------------------------------------------- */ -@import "addons/gh-koenig/gh-koenig.css"; - - /* ---------------------------✈️----------------------------- */ /* I COMMITTED THIS COMMENT FROM A PLANE: http://bit.ly/2on5pmq /* --------------------------------------------------------- */ diff --git a/ghost/admin/app/styles/components/koenig.css b/ghost/admin/app/styles/components/koenig.css new file mode 100644 index 0000000000..e0ffc6f587 --- /dev/null +++ b/ghost/admin/app/styles/components/koenig.css @@ -0,0 +1,345 @@ +/* TODO: move these back into the editor.css layout? */ + +/* scrollable container */ +.gh-koenig-editor { + position: relative; + z-index: 0; + width: 100%; + height: 100vh; + overflow-x: hidden; + overflow-y: auto; +} + +/* padded container housing title + editor canvas, scrollable content */ +.gh-koenig-editor-pane { + display: flex; + flex-direction: column; + min-height: 100%; + padding: 10vw 4vw; +} + +@media (max-width: 500px) { + .gh-koenig-editor-pane { + padding: 15vw 4vw; + } +} + +/* use flex-grow to fill the available vertical space so clicks outside the + editor content can trigger focus */ +.gh-koenig-editor-pane .koenig-editor { + width: 100%; + flex-grow: 1; + cursor: text; +} + +/* NOTE: everything from this point should be Koenig addon specific */ + +/* Editor canvas layout ----------------------------------------------------- */ + +.koenig-editor { + position: relative; /* necessary to position toolbar etc */ + max-width: 760px; + margin: 0 auto; +} + +/* Formatting Toolbar ------------------------------------------------------- */ + +.koenig-toolbar { + position: absolute; + display: flex; + align-items: center; + text-align: center; + user-select: none; + cursor: pointer; + color: color(var(--lightgrey) l(-10%)); + background: linear-gradient( + color(var(--darkgrey) l(-3%)), + color(var(--darkgrey) l(-8%)) + ); + border-radius: 5px; + box-shadow: 0 0 0 1px color(var(--darkgrey) l(-10%)), 0 8px 16px rgba(26,39,49,0.16), rgba(255,255,255,0.09) 0 1px 0 0 inset; + z-index:110; /* places it above the title */ + pointer-events: none !important; /* no interactivity when hidden */ + opacity: 0; + transition-property: opacity; + transition-duration: 300ms; +} + +.koenig-toolbar--visible { + pointer-events: auto !important; /* make sure the buttons work */ + opacity: 1; +} + +.koenig-toolbar:after { + display: block; + content: ""; + position: absolute; + bottom: -8px; + left: 50%; + margin-left: -10px; + width: 0; + height: 0; + border-left: transparent 10px solid; + border-right: transparent 10px solid; + border-top: color(var(--darkgrey) l(-10%)) 8px solid; + transition-property: left; + transition-duration: 100ms; +} +.koenig-toolbar.tick-above:after { + border: none; +} + +.koenig-toolbar.is-link { + width: 263px; + height: 40px; +} +.koenig-toolbar.is-link input { + width: 100%; + background-color: transparent; + outline: none; + border: none; + padding:5px; +} +.koenig-toolbar.is-touch { + position: fixed !important; + top: 70px; + right: 40px; +} + +.koenig-toolbar.is-touch:after { + border: none; +} + +.koenig-toolbar-btn { + display: flex; + justify-content: center; + align-items: center; + height: 40px; + width: 32px; + font-size: 1.6rem; + line-height: 40px; + transition: text-shadow 0.3s ease; +} + +.koenig-toolbar-btn:first-child { + width: 43px; + padding-left: 8px; +} +.koenig-toolbar-btn:last-child { + width: 43px; + padding-right: 8px; +} + +.koenig-toolbar-btn svg { + height: 1.4rem; +} + +.koenig-toolbar-btn svg g { + stroke-width: 2px; + stroke: color(var(--lightgrey) l(-10%)); +} + +.koenig-toolbar-btn:hover, +.koenig-toolbar-btn.selected { + color: #fff; + cursor: pointer; + text-shadow: #000 0 1px 6px; +} + +.koenig-toolbar-btn:hover svg g { + stroke: #fff; +} + +.koenig-toolbar-btn-bold { + font-weight: 700; +} + +.koenig-toolbar-btn-italic { + width: 31px; + font-size: 1.7rem; + text-indent: -1px; + font-style: italic; + font-family: Georgia, Times, serif; + font-weight: 500; +} + +.koenig-toolbar-btn-strike { + text-decoration: line-through; + font-weight: 400; + -webkit-font-smoothing: antialiased; +} + +.koenig-toolbar-btn-h1 { + font-variant: small-caps; + font-weight: 700; + -webkit-font-smoothing: antialiased; +} + +.koenig-toolbar-btn-h2 { + font-weight: 700; + font-size: 0.9em; + font-variant: small-caps; + line-height: 42px; + -webkit-font-smoothing: antialiased; +} + +.koenig-toolbar-btn-quote { + font-size: 4rem; + line-height: 62px; + font-family: Georgia, Times, serif; + -webkit-font-smoothing: antialiased; +} + +.koenig-toolbar-divider { + height: 40px; + width: 1px; + margin: 0 9px 0 8px; + background: color(var(--darkgrey) l(-10%)); + box-shadow: rgba(255,255,255,0.04) 1px 0 0 0; +} + +/* ⨁ menu ------------------------------------------------------------------ */ + +/* Slash shortcut menu ------------------------------------------------------ */ + +/* Menu items --------------------------------------------------------------- */ + +/* mobiledoc-kit base styles ------------------------------------------------ + * NOTE: adapted from https://github.com/bustle/mobiledoc-kit/blob/master/src/css/mobiledoc-kit.css + */ + +/** + * Editor + */ + +/* TODO: update to match Ghost styles */ +.__mobiledoc-editor { + position: relative; + font-family: var(--font-family); + font-size: 1.7rem; + resize: none; + font-weight: 200; + letter-spacing: 0.1px; + min-height: 1em; +} + +.__mobiledoc-editor:focus { + outline: none; +} + +.__mobiledoc-editor > * { + position: relative; +} + +.__mobiledoc-editor.__has-no-content:after { + content: attr(data-placeholder); + color: #bbb; + cursor: text; + position: absolute; + top: 0; +} + +.__mobiledoc-editor a { + color: #0b8bff; + white-space: nowrap; +} + +/* override of global.css block display */ +.__mobiledoc-editor i { + display: inline; +} + +.__mobiledoc-editor h1, +.__mobiledoc-editor h2, +.__mobiledoc-editor h3, +.__mobiledoc-editor h4, +.__mobiledoc-editor h5, +.__mobiledoc-editor h6 { + font-weight: 800; + letter-spacing: -0.02em; +} + +.__mobiledoc-editor blockquote { + border-left: 4px solid #0b8bff; + margin: 1em 0 1em -1.2em; + padding-left: 1.05em; + color: #a0a0a0; +} + +.__mobiledoc-editor img { + display: block; + max-width: 100%; + margin: 0 auto; +} + +.__mobiledoc-editor div, +.__mobiledoc-editor iframe { + max-width: 100%; +} + +/** + * Cards + */ + +.__mobiledoc-card { + display: inline-block; +} + +/** + * Tooltips + */ + +@-webkit-keyframes fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} +@keyframes fade-in { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +.__mobiledoc-tooltip { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 0.7em; + white-space: nowrap; + position: absolute; + background-color: rgba(43,43,43,0.9); + border-radius: 3px; + line-height: 1em; + padding: 0.7em 0.9em; + color: #FFF; + -webkit-animation: fade-in 0.2s; + animation: fade-in 0.2s; +} + +.__mobiledoc-tooltip:before { + content: ''; + position: absolute; + left: 50%; + width: 0; + height: 0; + border-left: 0.4em solid transparent; + border-right: 0.4em solid transparent; + border-bottom: 0.4em solid rgba(43,43,43,0.9); + top: -0.4em; + margin-left: -0.4em; +} + +/* help keeps mouseover state when moving from link to tooltip */ +.__mobiledoc-tooltip:after { + content: ''; + position: absolute; + left: 0; + right: 0; + top: -0.4em; + height: 0.4em; +} + +.__mobiledoc-tooltip a { + color: #FFF; + text-decoration: none; +} + +.__mobiledoc-tooltip a:hover { + text-decoration: underline; +} diff --git a/ghost/admin/app/templates/components/gh-koenig-editor.hbs b/ghost/admin/app/templates/components/gh-koenig-editor.hbs new file mode 100644 index 0000000000..f9700b897d --- /dev/null +++ b/ghost/admin/app/templates/components/gh-koenig-editor.hbs @@ -0,0 +1,25 @@ +{{!-- scrollable container --}} +
+ {{!-- full height content pane --}} +
+ {{gh-textarea title + class="gh-editor-title" + placeholder=titlePlaceholder + tabindex="1" + autoExpand=".gh-koenig-editor" + update=(action "onTitleChange") + focusOut=(action "onTitleFocusOut") + keyDown=(action "onTitleKeydown") + didCreateTextarea=(action "onTitleCreated") + }} + + {{koenig-editor + mobiledoc=body + placeholder=bodyPlaceholder + autofocus=bodyAutofocus + spellcheck=true + onChange=(action "onBodyChange") + didCreateEditor=(action "onEditorCreated") + }} +
+
diff --git a/ghost/admin/app/templates/editor.hbs b/ghost/admin/app/templates/editor.hbs index 40a466b882..1d03d87e1b 100644 --- a/ghost/admin/app/templates/editor.hbs +++ b/ghost/admin/app/templates/editor.hbs @@ -33,44 +33,20 @@ {{#if useKoenig}} -
-
- {{!-- - 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 - - TODO: note above is no longer correct, changed to readonly to - fix a persistent editor content bug that occurred due to the - editor not being re-rendered on edit->new transition. - - Needs perf investigation! - --}} - {{#gh-koenig - mobiledoc=(readonly model.scratch) - onChange=(action "updateScratch") - autofocus=shouldFocusEditor - tabindex="2" - titleSelector="#kg-title-input" - containerSelector=".gh-editor-container" - wordcountDidChange=(action "setWordcount") - as |koenig| - }} - {{koenig-title-input - id="koenig-title-input" - val=(readonly model.titleScratch) - onChange=(action "updateTitleScratch") - tabindex="1" - autofocus=shouldFocusTitle - focusOut=(action (perform saveTitle)) - editor=(readonly koenig.editor) - editorHasRendered=koenig.hasRendered - editorMenuIsOpen=koenig.isMenuOpen - }} - {{/gh-koenig}} -
-
-
{{pluralize wordcount 'word'}}.
+ {{!-- + gh-koenig-editor acts as a wrapper around the title input and + koenig editor canvas to support Ghost-specific editor behaviour + --}} + {{gh-koenig-editor + title=(readonly post.titleScratch) + titlePlaceholder="Story Title" + onTitleChange=(action "updateTitleScratch") + onTitleBlur=(action (perform saveTitle)) + body=(readonly post.scratch) + bodyPlaceholder="Begin writing your story..." + bodyAutofocus=shouldFocusEditor + onBodyChange=(action "updateScratch") + }} {{else}} diff --git a/ghost/admin/lib/gh-koenig/.gitignore b/ghost/admin/lib/gh-koenig--old/.gitignore similarity index 100% rename from ghost/admin/lib/gh-koenig/.gitignore rename to ghost/admin/lib/gh-koenig--old/.gitignore diff --git a/ghost/admin/lib/gh-koenig/README.md b/ghost/admin/lib/gh-koenig--old/README.md similarity index 100% rename from ghost/admin/lib/gh-koenig/README.md rename to ghost/admin/lib/gh-koenig--old/README.md diff --git a/ghost/admin/lib/gh-koenig/addon/.gitkeep b/ghost/admin/lib/gh-koenig--old/addon/.gitkeep similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/.gitkeep rename to ghost/admin/lib/gh-koenig--old/addon/.gitkeep diff --git a/ghost/admin/lib/gh-koenig/addon/cards/card-hr_dom.js b/ghost/admin/lib/gh-koenig--old/addon/cards/card-hr_dom.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/cards/card-hr_dom.js rename to ghost/admin/lib/gh-koenig--old/addon/cards/card-hr_dom.js diff --git a/ghost/admin/lib/gh-koenig/addon/cards/card-html_dom.js b/ghost/admin/lib/gh-koenig--old/addon/cards/card-html_dom.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/cards/card-html_dom.js rename to ghost/admin/lib/gh-koenig--old/addon/cards/card-html_dom.js diff --git a/ghost/admin/lib/gh-koenig/addon/cards/card-image_dom.js b/ghost/admin/lib/gh-koenig--old/addon/cards/card-image_dom.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/cards/card-image_dom.js rename to ghost/admin/lib/gh-koenig--old/addon/cards/card-image_dom.js diff --git a/ghost/admin/lib/gh-koenig/addon/cards/card-markdown_dom.js b/ghost/admin/lib/gh-koenig--old/addon/cards/card-markdown_dom.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/cards/card-markdown_dom.js rename to ghost/admin/lib/gh-koenig--old/addon/cards/card-markdown_dom.js diff --git a/ghost/admin/lib/gh-koenig/addon/cards/index.js b/ghost/admin/lib/gh-koenig--old/addon/cards/index.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/cards/index.js rename to ghost/admin/lib/gh-koenig--old/addon/cards/index.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js b/ghost/admin/lib/gh-koenig--old/addon/components/cards/card-hr.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/cards/card-hr.js rename to ghost/admin/lib/gh-koenig--old/addon/components/cards/card-hr.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/card-html.js b/ghost/admin/lib/gh-koenig--old/addon/components/cards/card-html.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/cards/card-html.js rename to ghost/admin/lib/gh-koenig--old/addon/components/cards/card-html.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/card-image.js b/ghost/admin/lib/gh-koenig--old/addon/components/cards/card-image.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/cards/card-image.js rename to ghost/admin/lib/gh-koenig--old/addon/components/cards/card-image.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/card-markdown.js b/ghost/admin/lib/gh-koenig--old/addon/components/cards/card-markdown.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/cards/card-markdown.js rename to ghost/admin/lib/gh-koenig--old/addon/components/cards/card-markdown.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js b/ghost/admin/lib/gh-koenig--old/addon/components/gh-koenig.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/gh-koenig.js rename to ghost/admin/lib/gh-koenig--old/addon/components/gh-koenig.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-card.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-card.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-card.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-card.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-menu-item.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-menu-item.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-menu-item.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-plus-menu.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-plus-menu.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-slash-menu.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-slash-menu.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-title-input.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-title-input.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-title-input.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-title-input.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-blockitem.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-blockitem.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-button.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-button.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-button.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-newitem.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-newitem.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar-newitem.js diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js b/ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js rename to ghost/admin/lib/gh-koenig--old/addon/components/koenig-toolbar.js diff --git a/ghost/admin/lib/gh-koenig/addon/lib/caja-sanitizers.js b/ghost/admin/lib/gh-koenig--old/addon/lib/caja-sanitizers.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/lib/caja-sanitizers.js rename to ghost/admin/lib/gh-koenig--old/addon/lib/caja-sanitizers.js diff --git a/ghost/admin/lib/gh-koenig/addon/lib/card-factory.js b/ghost/admin/lib/gh-koenig--old/addon/lib/card-factory.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/lib/card-factory.js rename to ghost/admin/lib/gh-koenig--old/addon/lib/card-factory.js diff --git a/ghost/admin/lib/gh-koenig/addon/lib/utils.js b/ghost/admin/lib/gh-koenig--old/addon/lib/utils.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/lib/utils.js rename to ghost/admin/lib/gh-koenig--old/addon/lib/utils.js diff --git a/ghost/admin/lib/gh-koenig/addon/options/default-tools.js b/ghost/admin/lib/gh-koenig--old/addon/options/default-tools.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/options/default-tools.js rename to ghost/admin/lib/gh-koenig--old/addon/options/default-tools.js diff --git a/ghost/admin/lib/gh-koenig/addon/options/key-commands.js b/ghost/admin/lib/gh-koenig--old/addon/options/key-commands.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/options/key-commands.js rename to ghost/admin/lib/gh-koenig--old/addon/options/key-commands.js diff --git a/ghost/admin/lib/gh-koenig/addon/options/text-expansions.js b/ghost/admin/lib/gh-koenig--old/addon/options/text-expansions.js similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/options/text-expansions.js rename to ghost/admin/lib/gh-koenig--old/addon/options/text-expansions.js diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-hr.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/card-hr.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/card-hr.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/card-hr.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-html.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/card-html.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/card-html.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/card-html.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/card-image.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/card-image.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/card-image.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/card-markdown.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/card-markdown.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/card-markdown.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/gh-koenig.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/gh-koenig.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/gh-koenig.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-card.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-card.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-card.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-menu-item.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-menu-item.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-plus-menu.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-plus-menu.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-slash-menu.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-slash-menu.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-title-input.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-title-input.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-title-input.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-title-input.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-blockitem.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-blockitem.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-blockitem.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-blockitem.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-button.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-button.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-button.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-button.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-newitem.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-newitem.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar-newitem.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar-newitem.hbs diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs b/ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar.hbs similarity index 100% rename from ghost/admin/lib/gh-koenig/addon/templates/components/koenig-toolbar.hbs rename to ghost/admin/lib/gh-koenig--old/addon/templates/components/koenig-toolbar.hbs diff --git a/ghost/admin/lib/gh-koenig/app/.gitkeep b/ghost/admin/lib/gh-koenig--old/app/.gitkeep similarity index 100% rename from ghost/admin/lib/gh-koenig/app/.gitkeep rename to ghost/admin/lib/gh-koenig--old/app/.gitkeep diff --git a/ghost/admin/lib/gh-koenig/app/components/card-hr.js b/ghost/admin/lib/gh-koenig--old/app/components/card-hr.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/card-hr.js rename to ghost/admin/lib/gh-koenig--old/app/components/card-hr.js diff --git a/ghost/admin/lib/gh-koenig/app/components/card-html.js b/ghost/admin/lib/gh-koenig--old/app/components/card-html.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/card-html.js rename to ghost/admin/lib/gh-koenig--old/app/components/card-html.js diff --git a/ghost/admin/lib/gh-koenig/app/components/card-image.js b/ghost/admin/lib/gh-koenig--old/app/components/card-image.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/card-image.js rename to ghost/admin/lib/gh-koenig--old/app/components/card-image.js diff --git a/ghost/admin/lib/gh-koenig/app/components/card-markdown.js b/ghost/admin/lib/gh-koenig--old/app/components/card-markdown.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/card-markdown.js rename to ghost/admin/lib/gh-koenig--old/app/components/card-markdown.js diff --git a/ghost/admin/lib/gh-koenig/app/components/gh-koenig.js b/ghost/admin/lib/gh-koenig--old/app/components/gh-koenig.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/gh-koenig.js rename to ghost/admin/lib/gh-koenig--old/app/components/gh-koenig.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-card.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-card.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-card.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-card.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-menu-item.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-menu-item.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-menu-item.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-menu-item.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-plus-menu.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-plus-menu.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-slash-menu.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-slash-menu.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-title-input.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-title-input.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-title-input.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-title-input.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-blockitem.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-blockitem.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-blockitem.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-blockitem.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-button.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-button.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-button.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-button.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-newitem.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-newitem.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-toolbar-newitem.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar-newitem.js diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-toolbar.js b/ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/components/koenig-toolbar.js rename to ghost/admin/lib/gh-koenig--old/app/components/koenig-toolbar.js diff --git a/ghost/admin/lib/gh-koenig/app/renderer.js b/ghost/admin/lib/gh-koenig--old/app/renderer.js similarity index 100% rename from ghost/admin/lib/gh-koenig/app/renderer.js rename to ghost/admin/lib/gh-koenig--old/app/renderer.js diff --git a/ghost/admin/lib/gh-koenig/index.js b/ghost/admin/lib/gh-koenig--old/index.js similarity index 100% rename from ghost/admin/lib/gh-koenig/index.js rename to ghost/admin/lib/gh-koenig--old/index.js diff --git a/ghost/admin/lib/gh-koenig/package.json b/ghost/admin/lib/gh-koenig--old/package.json similarity index 100% rename from ghost/admin/lib/gh-koenig/package.json rename to ghost/admin/lib/gh-koenig--old/package.json diff --git a/ghost/admin/lib/gh-koenig/public/tools/bold.svg b/ghost/admin/lib/gh-koenig--old/public/tools/bold.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/bold.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/bold.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/close.svg b/ghost/admin/lib/gh-koenig--old/public/tools/close.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/close.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/close.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/file-code-1.svg b/ghost/admin/lib/gh-koenig--old/public/tools/file-code-1.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/file-code-1.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/file-code-1.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/file-code-edit.svg b/ghost/admin/lib/gh-koenig--old/public/tools/file-code-edit.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/file-code-edit.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/file-code-edit.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/file-picture-add.svg b/ghost/admin/lib/gh-koenig--old/public/tools/file-picture-add.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/file-picture-add.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/file-picture-add.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/font-size.svg b/ghost/admin/lib/gh-koenig--old/public/tools/font-size.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/font-size.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/font-size.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/html-five.svg b/ghost/admin/lib/gh-koenig--old/public/tools/html-five.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/html-five.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/html-five.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/italic.svg b/ghost/admin/lib/gh-koenig--old/public/tools/italic.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/italic.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/italic.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/link-broken.svg b/ghost/admin/lib/gh-koenig--old/public/tools/link-broken.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/link-broken.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/link-broken.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/link.svg b/ghost/admin/lib/gh-koenig--old/public/tools/link.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/link.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/link.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/list-bullets.svg b/ghost/admin/lib/gh-koenig--old/public/tools/list-bullets.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/list-bullets.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/list-bullets.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/list-number.svg b/ghost/admin/lib/gh-koenig--old/public/tools/list-number.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/list-number.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/list-number.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/paragraph.svg b/ghost/admin/lib/gh-koenig--old/public/tools/paragraph.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/paragraph.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/paragraph.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/pullquote.svg b/ghost/admin/lib/gh-koenig--old/public/tools/pullquote.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/pullquote.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/pullquote.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/quote.svg b/ghost/admin/lib/gh-koenig--old/public/tools/quote.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/quote.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/quote.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/strikethrough.svg b/ghost/admin/lib/gh-koenig--old/public/tools/strikethrough.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/strikethrough.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/strikethrough.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/subscript.svg b/ghost/admin/lib/gh-koenig--old/public/tools/subscript.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/subscript.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/subscript.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/superscript.svg b/ghost/admin/lib/gh-koenig--old/public/tools/superscript.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/superscript.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/superscript.svg diff --git a/ghost/admin/lib/gh-koenig/public/tools/underline.svg b/ghost/admin/lib/gh-koenig--old/public/tools/underline.svg similarity index 100% rename from ghost/admin/lib/gh-koenig/public/tools/underline.svg rename to ghost/admin/lib/gh-koenig--old/public/tools/underline.svg diff --git a/ghost/admin/lib/gh-koenig/test-support/.eslintrc.js b/ghost/admin/lib/gh-koenig--old/test-support/.eslintrc.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/.eslintrc.js rename to ghost/admin/lib/gh-koenig--old/test-support/.eslintrc.js diff --git a/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-slashmenu-test.js b/ghost/admin/lib/gh-koenig--old/test-support/integration/components/gh-koenig-slashmenu-test.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-slashmenu-test.js rename to ghost/admin/lib/gh-koenig--old/test-support/integration/components/gh-koenig-slashmenu-test.js diff --git a/ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-test.js b/ghost/admin/lib/gh-koenig--old/test-support/integration/components/gh-koenig-test.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/integration/components/gh-koenig-test.js rename to ghost/admin/lib/gh-koenig--old/test-support/integration/components/gh-koenig-test.js diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js b/ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-button-test.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-button-test.js rename to ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-button-test.js diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js b/ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-newitem-test.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-newitem-test.js rename to ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-newitem-test.js diff --git a/ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js b/ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-test.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/unit/components/koenig-toolbar-test.js rename to ghost/admin/lib/gh-koenig--old/test-support/unit/components/koenig-toolbar-test.js diff --git a/ghost/admin/lib/gh-koenig/test-support/utils.js b/ghost/admin/lib/gh-koenig--old/test-support/utils.js similarity index 100% rename from ghost/admin/lib/gh-koenig/test-support/utils.js rename to ghost/admin/lib/gh-koenig--old/test-support/utils.js diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js new file mode 100644 index 0000000000..556cc24a30 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-editor.js @@ -0,0 +1,270 @@ +/* + * Based on ember-mobiledoc-editor + * https://github.com/bustle/ember-mobiledoc-editor + */ + +import Component from '@ember/component'; +import Editor from 'mobiledoc-kit/editor/editor'; +import Ember from 'ember'; +import layout from '../templates/components/koenig-editor'; +import registerTextExpansions from '../options/text-expansions'; +import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc'; +import {assign} from '@ember/polyfills'; +import {camelize, capitalize} from '@ember/string'; +import {computed} from '@ember/object'; +import {run} from '@ember/runloop'; + +// used in test helpers to grab a reference to the underlying mobiledoc editor +export const TESTING_EXPANDO_PROPERTY = '__mobiledoc_kit_editor'; + +// blank doc contains a single empty paragraph so that there's some content for +// the cursor to start in +export const BLANK_DOC = { + version: MOBILEDOC_VERSION, + markups: [], + atoms: [], + cards: [], + sections: [ + [1, 'p', [ + [0, [], 0, ''] + ]] + ] +}; + +function arrayToMap(array) { + let map = Object.create(null); + array.forEach((key) => { + if (key) { // skip undefined/falsy key values + key = `is${capitalize(camelize(key))}`; + map[key] = true; + } + }); + return map; +} + +export default Component.extend({ + layout, + + tagName: 'article', + classNames: ['koenig-editor'], + + // public attrs + mobiledoc: null, + placeholder: 'Write here...', + autofocus: false, + spellcheck: true, + options: null, + scrollContainer: '', + + // internal properties + editor: null, + activeMarkupTagNames: null, + activeSectionTagNames: null, + selectedRange: null, + + // private properties + _localMobiledoc: null, + _upstreamMobiledoc: null, + _startedRunLoop: false, + _lastIsEditingDisabled: false, + _isRenderingEditor: false, + + // closure actions + willCreateEditor() {}, + didCreateEditor() {}, + onChange() {}, + + /* computed properties -------------------------------------------------- */ + + // merge in named options with the `options` property data-bag + editorOptions: computed(function () { + let options = this.get('options') || {}; + + return assign({ + placeholder: this.get('placeholder'), + spellcheck: this.get('spellcheck'), + autofocus: this.get('autofocus') + }, options); + }), + + /* lifecycle hooks ------------------------------------------------------ */ + + init() { + this._super(...arguments); + + // set a blank mobiledoc if we didn't receive anything + let mobiledoc = this.get('mobiledoc'); + if (!mobiledoc) { + mobiledoc = BLANK_DOC; + this.set('mobiledoc', mobiledoc); + } + + this._startedRunLoop = false; + }, + + willRender() { + // use a default mobiledoc. If there are no changes then return early + let mobiledoc = this.get('mobiledoc') || BLANK_DOC; + let mobiledocIsSame = + (this._localMobiledoc && this._localMobiledoc === mobiledoc) || + (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc); + let isEditingDisabledIsSame = + this._lastIsEditingDisabled === this.get('isEditingDisabled'); + + // no change to mobiledoc, no need to recreate the editor + if (mobiledocIsSame && isEditingDisabledIsSame) { + return; + } + + // update our internal references + this._lastIsEditingDisabled = this.get('isEditingDisabled'); + this._upstreamMobiledoc = mobiledoc; + this._localMobiledoc = null; + + // trigger the willCreateEditor closure 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; + editor = new Editor(editorOptions); + + // set up key commands and text expansions (MD conversion) + registerTextExpansions(editor); + + // set up editor hooks + 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 runloop in `editor.willRender`, + // we must explicitly end it here + if (this._startedRunLoop) { + this._startedRunLoop = false; + run.end(); + } + }); + editor.postDidChange(() => { + run.join(() => { + this.postDidChange(editor); + }); + }); + editor.cursorDidChange(() => { + run.join(() => { + this.cursorDidChange(editor); + }); + }); + editor.inputModeDidChange(() => { + if (this.isDestroyed) { + return; + } + run.join(() => { + this.inputModeDidChange(editor); + }); + }); + + if (this.get('isEditingDisabled')) { + editor.disableEditing(); + } + + this.set('editor', editor); + this.didCreateEditor(editor); + }, + + // our ember component has rendered, now we need to render the mobiledoc + // editor itself if necessary + didRender() { + this._super(...arguments); + let editor = this.get('editor'); + if (!editor.hasRendered) { + let editorElement = this.element.querySelector('.koenig-editor__editor'); + this._isRenderingEditor = true; + editor.render(editorElement); + this._isRenderingEditor = false; + } + }, + + willDestroyElement() { + let editor = this.get('editor'); + editor.destroy(); + this._super(...arguments); + }, + + actions: { + toggleMarkup(markupTagName) { + let editor = this.get('editor'); + editor.toggleMarkup(markupTagName); + }, + + toggleSection(sectionTagName) { + let editor = this.get('editor'); + editor.toggleSection(sectionTagName); + } + }, + + /* public methods ------------------------------------------------------- */ + + postDidChange(editor) { + let serializeVersion = this.get('serializeVersion'); + let updatedMobiledoc = editor.serialize(serializeVersion); + this._localMobiledoc = updatedMobiledoc; + + // trigger closure action + this.onChange(updatedMobiledoc); + }, + + cursorDidChange(editor) { + this.set('selectedRange', editor.range); + }, + + // fired when the active section(s) or markup(s) at the current cursor + // position or selection have changed. We use this event to update the + // activeMarkup/section tag lists which control button states in our popup + // toolbar + inputModeDidChange(editor) { + let markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName)); + // editor.activeSections are leaf sections. + // Map parent section tag names (e.g. 'p', 'ul', 'ol') so that list buttons + // are updated. + // eslint-disable-next-line no-confusing-arrow + let sectionParentTagNames = editor.activeSections.map(s => s.isNested ? s.parent.tagName : s.tagName); + let sectionTags = arrayToMap(sectionParentTagNames); + + // Avoid updating this component's properties synchronously while + // rendering the editor (after rendering the component) because it + // causes Ember to display deprecation warnings + if (this._isRenderingEditor) { + run.schedule('afterRender', () => { + this.set('activeMarkupTagNames', markupTags); + this.set('activeSectionTagNames', sectionTags); + }); + } else { + this.set('activeMarkupTagNames', markupTags); + this.set('activeSectionTagNames', sectionTags); + } + }, + + /* internal methods ----------------------------------------------------- */ + + // store a reference to the editor for the acceptance test helpers + _setExpandoProperty(editor) { + if (this.element && Ember.testing) { + this.element[TESTING_EXPANDO_PROPERTY] = editor; + } + } +}); diff --git a/ghost/admin/lib/koenig-editor/addon/components/koenig-toolbar.js b/ghost/admin/lib/koenig-editor/addon/components/koenig-toolbar.js new file mode 100644 index 0000000000..3aa29e805c --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/components/koenig-toolbar.js @@ -0,0 +1,184 @@ +import Component from '@ember/component'; +import layout from '../templates/components/koenig-toolbar'; +import {computed} from '@ember/object'; +import {htmlSafe} from '@ember/string'; +import {task, timeout} from 'ember-concurrency'; + +// initially rendered offscreen with opacity 0 so that sizing is available +// shown when passed in an uncollapsed selected range +// display is delayed until the mouse button is lifted +// positioned so that it's always fully within the editor container +// animation occurs via CSS transitions +// position is kept after hiding, it's made inoperable by CSS pointer-events + +const TOOLBAR_TOP_MARGIN = 15; + +export default Component.extend({ + layout, + + attributeBindings: ['style'], + + // public attrs + classNames: ['koenig-toolbar'], + classNameBindings: ['showToolbar:koenig-toolbar--visible'], + selectedRange: null, + + // internal properties + showToolbar: false, + top: null, + left: -1000, + right: null, + + // private properties + _isMouseDown: false, + _onMousedownHandler: false, + _onMouseupHandler: false, + _hasSelectedRange: false, + + /* computed properties -------------------------------------------------- */ + + style: computed('top', 'left', 'right', function () { + let position = this.getProperties('top', 'left', 'right'); + let styles = Object.keys(position).map((style) => { + if (position[style] !== null) { + return `${style}: ${position[style]}px`; + } + }); + + return htmlSafe(styles.compact().join('; ')); + }), + + /* lifecycle hooks ------------------------------------------------------ */ + + init() { + this._super(...arguments); + + // track mousedown/mouseup on the window so that we're sure to get the + // events even when they start outside of this component or end outside + // the window + this._onMousedownHandler = (event) => { + // we only care about the left mouse button + if (event.which === 1) { + this._isMouseDown = true; + } + }; + this._onMouseupHandler = (event) => { + if (event.which === 1) { + this._isMouseDown = false; + this.get('_toggleVisibility').perform(); + } + }; + window.addEventListener('mousedown', this._onMousedownHandler); + window.addEventListener('mouseup', this._onMouseupHandler); + }, + + didReceiveAttrs() { + this._super(...arguments); + let range = this.get('editorRange'); + + if (range && !range.isCollapsed) { + this._hasSelectedRange = true; + } else { + this._hasSelectedRange = false; + } + + this.get('_toggleVisibility').perform(); + }, + + willDestroyElement() { + this._super(...arguments); + this._removeStyleElement(); + window.removeEventListener('mousedown', this._onMousedownHandler); + window.removeEventListener('mouseup', this._onMouseupHandler); + }, + + _toggleVisibility: task(function* () { + // debounce for 100ms to account for "click to deselect" otherwise we + // run twice and the fade out animation jumps position + yield timeout(50); + + // return early if the editorRange hasn't changed, this prevents + // re-rendering unnecessarily which can cause minor position jumps when + // styles are toggled because getBoundingClientRect on getSelection + // changes slightly depending on the style of selected text + if (this.get('editorRange') === this._lastRange) { + return; + } + + // if we have a range, show the toolbnar once the mouse is lifted + if (this._hasSelectedRange && !this._isMouseDown) { + let containerRect = this.element.parentNode.getBoundingClientRect(); + let range = window.getSelection().getRangeAt(0); + let rangeRect = range.getBoundingClientRect(); + let {width, height} = this.element.getBoundingClientRect(); + let newPosition = {}; + + // rangeRect is relative to the viewport so we need to subtract the + // container measurements to get a position relative to the container + newPosition = { + top: rangeRect.top - containerRect.top - height - TOOLBAR_TOP_MARGIN, + left: rangeRect.left - containerRect.left + rangeRect.width / 2 - width / 2, + right: null + }; + + let tickPosition = 50; + // don't overflow left boundary + if (newPosition.left < 0) { + newPosition.left = 0; + + // calculate the tick percentage position + let absTickPosition = rangeRect.left - containerRect.left + rangeRect.width / 2; + tickPosition = absTickPosition / width * 100; + if (tickPosition < 5) { + tickPosition = 5; + } + } + // same for right boundary + if (newPosition.left + width > containerRect.width) { + newPosition.left = null; + newPosition.right = 0; + + // calculate the tick percentage position + let absTickPosition = rangeRect.right - containerRect.right - rangeRect.width / 2; + tickPosition = 100 + absTickPosition / width * 100; + if (tickPosition > 95) { + tickPosition = 95; + } + } + + // the tick is a pseudo-element so we the only way we can affect it's + // style is by adding a style element to the head + this._removeStyleElement(); // reset to base styles + if (tickPosition !== 50) { + this._addStyleElement(`left: ${tickPosition}%`); + } + + // update the toolbar position and show it + this.setProperties(newPosition); + + // show the toolbar + this.set('showToolbar', true); + + // track displayed range so that we don't re-position unnecessarily + this._lastRange = this.get('editorRange'); + } else { + // hide the toolbar + this.set('showToolbar', false); + this._lastRange = null; + } + }).restartable(), + + _addStyleElement(styles) { + let styleElement = document.createElement('style'); + styleElement.id = `${this.elementId}-style`; + styleElement.innerHTML = `#${this.elementId}:after { ${styles} }`; + document.head.appendChild(styleElement); + }, + + _removeStyleElement() { + let styleElement = document.querySelector(`#${this.elementId}-style`); + if (styleElement) { + styleElement.remove(); + } + } +}); diff --git a/ghost/admin/lib/koenig-editor/addon/options/text-expansions.js b/ghost/admin/lib/koenig-editor/addon/options/text-expansions.js new file mode 100644 index 0000000000..4849bdc292 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/options/text-expansions.js @@ -0,0 +1,171 @@ +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 + +// TODO: this was copied from our old Koenig editor, it could do with some +// comments, cleanup, and refactoring + +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. + + editor.onTextInput({ + name: 'inline_markdown', + match: /[*_)~`]$/, + run(postEditor, matches) { + let text = postEditor.range.head.section.textUntil(postEditor.range.head); + + switch (matches[0]) { + case '*': + matchStrongStar(postEditor, text); + matchEmStar(postEditor, text); + break; + case '_': + matchStrongUnderscore(postEditor, text); + matchEmUnderscore(postEditor, text); + break; + case ')': + matchLink(postEditor, text); + break; + case '~': + matchStrikethrough(postEditor, text); + break; + case '`': + matchCode(postEditor, text); + break; + } + } + }); + + /* block level markdown ------------------------------------------------- */ + + // mobiledoc-kit has `* ` already built-in so we only need to add `- ` + editor.onTextInput({ + name: 'md_ul', + match: /^- $/, + run(editor) { + replaceWithListSection(editor, 'ul'); + } + }); + + editor.onTextInput({ + name: 'md_blockquote', + match: /^> $/, + run(editor) { + replaceWithHeaderSection(editor, 'blockquote'); + } + }); + + /* inline markdown ------------------------------------------------------ */ + + function matchStrongStar(editor, text) { + let {range} = editor; + let matches = text.match(/\*\*(.+?)\*\*$/); + if (matches) { + range = range.extend(-(matches[0].length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let bold = postEditor.builder.createMarkup('strong'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [bold]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); + }); + } + } + + function matchStrongUnderscore(editor, text) { + let {range} = editor; + let matches = text.match(/__(.+?)__$/); + if (matches) { + range = range.extend(-(matches[0].length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let bold = postEditor.builder.createMarkup('strong'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [bold]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); + }); + } + } + + function matchEmStar(editor, text) { + let {range} = editor; + let matches = text.match(/(^|[^*])\*([^*].*?)\*$/); + if (matches) { + let match = matches[0][0] === '*' ? matches[0] : matches[0].substr(1); + range = range.extend(-(match.length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let em = postEditor.builder.createMarkup('em'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[2], [em]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); + }); + } + } + + function matchEmUnderscore(editor, text) { + let {range} = editor; + let matches = text.match(/(^|[^_])_([^_].+?)_$/); + if (matches) { + let match = matches[0][0] === '_' ? matches[0] : matches[0].substr(1); + range = range.extend(-(match.length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let em = postEditor.builder.createMarkup('em'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[2], [em]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); + }); + } + } + + function matchLink(editor, text) { + let {range} = editor; + let matches = text.match(/(^|[^!])\[(.*?)\]\((.*?)\)$/); + if (matches) { + let url = matches[3]; + let text = matches[2]; + let match = matches[0][0] === '[' ? matches[0] : matches[0].substr(1); + range = range.extend(-match.length); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let a = postEditor.builder.createMarkup('a', {href: url}); + let nextPosition = postEditor.insertTextWithMarkup(position, text, [a]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); // insert the un-marked-up space + }); + } + } + + function matchStrikethrough(editor, text) { + let {range} = editor; + let matches = text.match(/~(.+?)~$/); + if (matches) { + range = range.extend(-(matches[0].length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let s = postEditor.builder.createMarkup('s'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [s]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); // insert the un-marked-up space + }); + } + } + + function matchCode(editor, text) { + let {range} = editor; + let matches = text.match(/`(.+?)`/); + if (matches) { + range = range.extend(-(matches[0].length)); + editor.run((postEditor) => { + let position = postEditor.deleteRange(range); + let code = postEditor.builder.createMarkup('code'); + let nextPosition = postEditor.insertTextWithMarkup(position, matches[1], [code]); + postEditor.insertTextWithMarkup(nextPosition, ' ', []); // insert the un-marked-up space + }); + } + } +} diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs new file mode 100644 index 0000000000..5aaa05d415 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-editor.hbs @@ -0,0 +1,67 @@ +
+
+
+ +{{!-- pop-up markup toolbar is shown when there's a selection --}} +{{#koenig-toolbar editorRange=selectedRange}} + {{!-- markup buttons --}} + + + + + + + + {{!-- block buttons --}} + + + +{{/koenig-toolbar}} diff --git a/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-toolbar.hbs b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-toolbar.hbs new file mode 100644 index 0000000000..fb5c4b157d --- /dev/null +++ b/ghost/admin/lib/koenig-editor/addon/templates/components/koenig-toolbar.hbs @@ -0,0 +1 @@ +{{yield}} \ No newline at end of file diff --git a/ghost/admin/lib/koenig-editor/app/components/koenig-editor.js b/ghost/admin/lib/koenig-editor/app/components/koenig-editor.js new file mode 100644 index 0000000000..7ca98ddbc4 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/app/components/koenig-editor.js @@ -0,0 +1 @@ +export {default} from 'koenig-editor/components/koenig-editor'; diff --git a/ghost/admin/lib/koenig-editor/app/components/koenig-toolbar.js b/ghost/admin/lib/koenig-editor/app/components/koenig-toolbar.js new file mode 100644 index 0000000000..560ad9e4b3 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/app/components/koenig-toolbar.js @@ -0,0 +1 @@ +export {default} from 'koenig-editor/components/koenig-toolbar'; diff --git a/ghost/admin/lib/koenig-editor/docs/specs/popup-toolbar.md b/ghost/admin/lib/koenig-editor/docs/specs/popup-toolbar.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ghost/admin/lib/koenig-editor/index.js b/ghost/admin/lib/koenig-editor/index.js new file mode 100644 index 0000000000..14305a239d --- /dev/null +++ b/ghost/admin/lib/koenig-editor/index.js @@ -0,0 +1,10 @@ +/* eslint-env node */ +'use strict'; + +module.exports = { + name: 'koenig-editor', + + isDevelopingAddon() { + return true; + } +}; diff --git a/ghost/admin/lib/koenig-editor/package.json b/ghost/admin/lib/koenig-editor/package.json new file mode 100644 index 0000000000..153febaeb0 --- /dev/null +++ b/ghost/admin/lib/koenig-editor/package.json @@ -0,0 +1,17 @@ +{ + "name": "koenig-editor", + "version": "0.0.1", + "description": "A mobiledoc-kit based editor for Ghost.", + "author": "Ghost Foundation", + "homepage": "http://ghost.org", + "bugs": "https://github.com/TryGhost/Ghost/issues", + "license": "MIT", + "private": true, + "keywords": [ + "ember-addon" + ], + "dependencies": { + "ember-cli-babel": "^6.11.0", + "ember-cli-htmlbars": "^2.0.3" + } +} diff --git a/ghost/admin/package.json b/ghost/admin/package.json index c861592751..aa0c0432e8 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -123,7 +123,7 @@ "ember-addon": { "paths": [ "lib/asset-delivery", - "lib/gh-koenig" + "lib/koenig-editor" ] } } diff --git a/ghost/admin/tests/helpers/editor-helpers.js b/ghost/admin/tests/helpers/editor-helpers.js deleted file mode 100644 index 75349e281d..0000000000 --- a/ghost/admin/tests/helpers/editor-helpers.js +++ /dev/null @@ -1,114 +0,0 @@ -import $ from 'jquery'; -import Ember from 'ember'; -import wait from 'ember-test-helpers/wait'; -import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc'; -import {TESTING_EXPANDO_PROPERTY} from 'gh-koenig/components/gh-koenig'; -import {find, findWithAssert, waitUntil} from 'ember-native-dom-helpers'; -import {run} from '@ember/runloop'; - -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]; - } - 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 = $('#koenig-title-input div'); - if (title[0]) { - return resolve(); - } else { - window.requestAnimationFrame(checkTitle); - } - } - checkTitle(); - }); -} - -// 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('#koenig-title-input div'); - run(() => el.innerHTML = HTML); - return (window.wait || wait)(); -} - -// 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) { - 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 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; - let rejectTimeout = window.setTimeout(() => { - expect(lastRender).to.equal(output); // we know this is false but include it for the output. - reject(lastRender); - isRejected = true; - }, 500); - 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(editor, input); - }); -} - -export function testEditorInputTimeout(input) { - let editor = findEditor(); - editor.element.focus(); - return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line - window.setTimeout(() => { - resolve(editor.element.innerHTML); - }, 300); - - inputText(editor, input); - }); -} - -export function waitForRender(selector) { - return waitUntil(() => find(selector)); -} diff --git a/ghost/admin/tests/integration/components/gh-koenig-editor-test.js b/ghost/admin/tests/integration/components/gh-koenig-editor-test.js new file mode 100644 index 0000000000..9b2c55a82d --- /dev/null +++ b/ghost/admin/tests/integration/components/gh-koenig-editor-test.js @@ -0,0 +1,24 @@ +import hbs from 'htmlbars-inline-precompile'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {setupComponentTest} from 'ember-mocha'; + +describe('Integration: Component: gh-koenig-editor', function () { + setupComponentTest('gh-koenig-editor', { + integration: true + }); + + it('renders', function () { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + // Template block usage: + // this.render(hbs` + // {{#gh-koenig-editor}} + // template content + // {{/gh-koenig-editor}} + // `); + + this.render(hbs`{{gh-koenig-editor}}`); + expect(this.$()).to.have.length(1); + }); +}); diff --git a/ghost/admin/tests/integration/components/koenig-editor-test.js b/ghost/admin/tests/integration/components/koenig-editor-test.js new file mode 100644 index 0000000000..5cd46293e8 --- /dev/null +++ b/ghost/admin/tests/integration/components/koenig-editor-test.js @@ -0,0 +1,24 @@ +import hbs from 'htmlbars-inline-precompile'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {setupComponentTest} from 'ember-mocha'; + +describe('Integration: Component: koenig-editor', function () { + setupComponentTest('koenig-editor', { + integration: true + }); + + it('renders', function () { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + // Template block usage: + // this.render(hbs` + // {{#koenig-editor}} + // template content + // {{/koenig-editor}} + // `); + + this.render(hbs`{{koenig-editor}}`); + expect(this.$()).to.have.length(1); + }); +}); diff --git a/ghost/admin/tests/integration/components/koenig-toolbar-test.js b/ghost/admin/tests/integration/components/koenig-toolbar-test.js new file mode 100644 index 0000000000..97e74e4cb5 --- /dev/null +++ b/ghost/admin/tests/integration/components/koenig-toolbar-test.js @@ -0,0 +1,24 @@ +import hbs from 'htmlbars-inline-precompile'; +import {describe, it} from 'mocha'; +import {expect} from 'chai'; +import {setupComponentTest} from 'ember-mocha'; + +describe('Integration: Component: koenig-toolbar', function () { + setupComponentTest('koenig-toolbar', { + integration: true + }); + + it('renders', function () { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.on('myAction', function(val) { ... }); + // Template block usage: + // this.render(hbs` + // {{#koenig-toolbar}} + // template content + // {{/koenig-toolbar}} + // `); + + this.render(hbs`{{koenig-toolbar}}`); + expect(this.$()).to.have.length(1); + }); +});