From 9a0b72071d2094163ecb8933f2d19aedb7dec132 Mon Sep 17 00:00:00 2001 From: Ryan McCarvill Date: Wed, 15 Mar 2017 00:59:34 +1300 Subject: [PATCH] =?UTF-8?q?=F0=9F=91=AF=20=E2=99=A5=EF=B8=8F=20=E2=99=A3?= =?UTF-8?q?=EF=B8=8F=20=E2=99=A6=EF=B8=8F=20=E2=99=A0=EF=B8=8F=20New=20edi?= =?UTF-8?q?tor=20card=20menu=20(#580)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://github.com/TryGhost/Ghost/issues/8106, https://github.com/TryGhost/Ghost/issues/7429, requires https://github.com/TryGhost/Ghost/pull/8137 -Adds new "card" menus - Navigation with keyboard in both axis. - Search with keyboard in both menus. - Adds a "+" Menu for cards - Adds a "/" Menu for cards - if the block has content and it becomes a markdown or HTML Embed card then the content is included into the card. - Image and HR cards appear below the current section - Adds new toolbar with both inline and block styling. - Adds a new 'divider' card. --- .../app/styles/addons/gh-koenig/gh-koenig.css | 14 +- .../styles/addons/ghost-editor/cardmenu.css | 21 +- ghost/admin/app/templates/editor/edit.hbs | 1 + .../lib/gh-koenig/addon/cards/hr-card_dom.js | 8 + .../admin/lib/gh-koenig/addon/cards/index.js | 3 +- .../addon/components/cards/hr-card.js | 5 + .../addon/components/cards/html-card.js | 1 + .../addon/components/koenig-menu-item.js | 27 +-- .../gh-koenig/addon/components/koenig-menu.js | 200 ----------------- .../addon/components/koenig-plus-menu.js | 172 ++++++++++++++ .../addon/components/koenig-slash-menu.js | 212 ++++++++++++++++++ .../components/koenig-toolbar-blockitem.js | 2 +- .../addon/components/koenig-toolbar-button.js | 3 - .../components/koenig-toolbar-newitem.js | 2 +- .../addon/components/koenig-toolbar.js | 3 +- .../gh-koenig/addon/options/default-tools.js | 69 ++++-- .../addon/templates/components/gh-koenig.hbs | 5 +- .../addon/templates/components/hr-card.hbs | 1 + .../addon/templates/components/html-card.hbs | 4 +- .../templates/components/koenig-menu-item.hbs | 7 +- .../templates/components/koenig-menu.hbs | 9 - .../templates/components/koenig-plus-menu.hbs | 17 ++ .../components/koenig-slash-menu.hbs | 7 + .../lib/gh-koenig/app/components/hr-card.js | 1 + .../gh-koenig/app/components/koenig-menu.js | 1 - .../app/components/koenig-plus-menu.js | 1 + .../app/components/koenig-slash-menu.js | 1 + ghost/admin/package.json | 2 +- ghost/admin/tests/helpers/editor-helpers.js | 21 ++ .../components/gh-koenig-slashmenu-test.js | 59 +++++ 30 files changed, 612 insertions(+), 267 deletions(-) create mode 100644 ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js create mode 100644 ghost/admin/lib/gh-koenig/addon/components/cards/hr-card.js delete mode 100644 ghost/admin/lib/gh-koenig/addon/components/koenig-menu.js create mode 100644 ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js create mode 100644 ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js create mode 100644 ghost/admin/lib/gh-koenig/addon/templates/components/hr-card.hbs delete mode 100644 ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu.hbs create mode 100644 ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs create mode 100644 ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs create mode 100644 ghost/admin/lib/gh-koenig/app/components/hr-card.js delete mode 100644 ghost/admin/lib/gh-koenig/app/components/koenig-menu.js create mode 100644 ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js create mode 100644 ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js create mode 100644 ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js 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 51f24c7549..0420c8f051 100644 --- a/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css +++ b/ghost/admin/app/styles/addons/gh-koenig/gh-koenig.css @@ -1,6 +1,5 @@ @import "koenig-toolbar.css"; @import "koenig-menu.css"; -/* TODO: move/rename to match koenig naming */ @import "../ghost-editor/cardmenu.css"; .editor-holder { @@ -39,13 +38,16 @@ border-right: 66px solid #5ba4e5; } +.__mobiledoc-editor div { + +} + .__mobiledoc-card { - display: block; + display: inline-block; /* required for cursor movement around card */ border: 1px solid; -} -.__mobiledoc-card .koenig-card { - position: relative; -} + width: calc(100% - 20px); /* required for obvious cursor placmenet around card */ + margin:5px; + } .__mobiledoc-card .card-handle { position: absolute; diff --git a/ghost/admin/app/styles/addons/ghost-editor/cardmenu.css b/ghost/admin/app/styles/addons/ghost-editor/cardmenu.css index 7b8c7ff403..5a967d7bb0 100644 --- a/ghost/admin/app/styles/addons/ghost-editor/cardmenu.css +++ b/ghost/admin/app/styles/addons/ghost-editor/cardmenu.css @@ -14,8 +14,25 @@ text-transform: none; font-size: 1.4rem; font-weight: normal; + position: absolute; + z-index: 9999999; /* have to compete with codemirror */ } +#gh-cardmenu-button { + position:absolute; + width: 40px; + height: 40px; + background-color:pink; + font-size:40px; + line-height: 40px; + color: powderblue; + font-family: "Comic Sans MS", cursive, sans-serif; +} +#gh-cardmenu-button:hover { + background-color:red; + color: yellow; +} + .gh-cardmenu-search { position: relative; width: 350px; @@ -74,11 +91,11 @@ font-weight: 200; } -.gh-cardmenu-card:hover { +.gh-cardmenu-card:hover, .gh-cardmenu-card.selected { cursor: pointer; background: color(var(--lightgrey) l(+3%) s(-10%)); } -.gh-cardmenu-card:hover .gh-cardmenu-label { +.gh-cardmenu-card:hover .gh-cardmenu-label, .gh-cardmenu-card.selected .gh-cardmenu-label { color: var(--darkgrey); font-weight: 300; } diff --git a/ghost/admin/app/templates/editor/edit.hbs b/ghost/admin/app/templates/editor/edit.hbs index 8353499367..e39d5546aa 100644 --- a/ghost/admin/app/templates/editor/edit.hbs +++ b/ghost/admin/app/templates/editor/edit.hbs @@ -51,6 +51,7 @@ apiRoot=apiRoot assetPath=assetPath tabindex=2 + containerSelector='.gh-editor-container' }} diff --git a/ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js b/ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js new file mode 100644 index 0000000000..aaf5f0579a --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js @@ -0,0 +1,8 @@ +export default { + name: 'hr-card', + label: 'HR Card', + icon: '', + genus: 'ember', + buttons: { + } +}; diff --git a/ghost/admin/lib/gh-koenig/addon/cards/index.js b/ghost/admin/lib/gh-koenig/addon/cards/index.js index 026375be95..99425f158c 100644 --- a/ghost/admin/lib/gh-koenig/addon/cards/index.js +++ b/ghost/admin/lib/gh-koenig/addon/cards/index.js @@ -1,10 +1,11 @@ import htmlCard from 'gh-koenig/cards/html-card_dom'; import imageCard from 'gh-koenig/cards/image-card_dom'; import markdownCard from 'gh-koenig/cards/markdown-card_dom'; +import hrCard from 'gh-koenig/cards/hr-card_dom'; let cards = []; -[htmlCard, imageCard, markdownCard].forEach((_card) => { +[htmlCard, imageCard, markdownCard, hrCard].forEach((_card) => { _card.type = 'dom'; cards.push(_card); }); diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/hr-card.js b/ghost/admin/lib/gh-koenig/addon/components/cards/hr-card.js new file mode 100644 index 0000000000..50cdd351dd --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/components/cards/hr-card.js @@ -0,0 +1,5 @@ +import Component from 'ember-component'; +import layout from '../../templates/components/hr-card'; +export default Component.extend({ + layout +}); \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/addon/components/cards/html-card.js b/ghost/admin/lib/gh-koenig/addon/components/cards/html-card.js index 2caf48cc4c..f509328bed 100644 --- a/ghost/admin/lib/gh-koenig/addon/components/cards/html-card.js +++ b/ghost/admin/lib/gh-koenig/addon/components/cards/html-card.js @@ -26,6 +26,7 @@ export default Component.extend({ this._super(...arguments); let payload = this.get('payload'); this.isEditing = !payload.hasOwnProperty('html'); + this.isEditing = true; }, didRender() { 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 8312318a81..4cc742f0ba 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 @@ -1,28 +1,23 @@ import Component from 'ember-component'; import layout from '../templates/components/koenig-menu-item'; +import Range from 'mobiledoc-kit/utils/cursor/range'; export default Component.extend({ layout, - tagName: 'li', - + 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} = this.get('range'); + let editor = this.get('editor'); - actions: { - select() { - let {section/* , startOffset, endOffset */} = this.get('range'); - window.getSelection().removeAllRanges(); + editor.range = Range.create(section, 0, section, 0); - let range = document.createRange(); - - range.setStart(section.renderNode._element, 0); // startOffset-1); // todo - range.setEnd(section.renderNode._element, 0); // endOffset-1); - - let selection = window.getSelection(); - selection.addRange(range); - - this.get('tool').onClick(this.get('editor')); - } + this.get('tool').onClick(editor, section); + this.sendAction('clicked'); } }); diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-menu.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-menu.js deleted file mode 100644 index 123b42b0bb..0000000000 --- a/ghost/admin/lib/gh-koenig/addon/components/koenig-menu.js +++ /dev/null @@ -1,200 +0,0 @@ -import Component from 'ember-component'; -import computed from 'ember-computed'; -import run from 'ember-runloop'; -import $ from 'jquery'; -import Tools from '../options/default-tools'; -import layout from '../templates/components/koenig-menu'; - -export default Component.extend({ - layout, - range: null, - menuSelectedItem: 0, - toolsLength: 0, - selectedTool: null, - isActive: false, - isInputting: false, - isSetup: false, - - toolbar: computed(function () { - let tools = []; - let match = (this.query || '').trim().toLowerCase(); - let i = 0; - // todo cache active tools so we don't need to loop through them on selection change. - this.tools.forEach((tool) => { - - if ((tool.type === 'block' || tool.type === 'card') && (tool.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) { - - let t = { - label: tool.label, - name: tool.name, - icon: tool.icon, - selected: i === this.menuSelectedItem, - onClick: tool.onClick - }; - - if (i === this.menuSelectedItem) { - this.set('selectedTool', t); - } - - tools.push(t); - i++; - } - }); - this.set('toolsLength', i); - if (this.menuSelectedItem > this.toolsLength) { - this.set('menuSelectedItem', this.toolsLength - 1); - // this.propertyDidChange('toolbar'); - } - - if (tools.length < 1) { - this.isActive = false; - this.$('.koenig-menu').hide(); - } - - return tools; - }), - - init() { - this._super(...arguments); - this.tools = new Tools(this.get('editor'), this); - this.iconURL = `${this.get('assetPath')}/tools/`; - - this.editor.cursorDidChange(this.cursorChange.bind(this)); - let self = this; - this.editor.onTextInput({ - name: 'slash_menu', - text: '/', - run(editor) { - self.open(editor); - } - }); - }, - - willDestroy() { - this.editor.destroy(); - }, - - cursorChange() { - if (!this.editor.range.isCollapsed || this.editor.range.head.section !== this._node || this.editor.range.head.offset < 1 || !this.editor.range.head.section) { - this.close(); - } - - if (this.isActive && this.isInputting) { - this.query = this.editor.range.head.section.text.substring(this._offset, this.editor.range.head.offset); - this.set('range', { - section: this._node, - startOffset: this._offset, - endOffset: this.editor.range.head.offset - }); - this.propertyDidChange('toolbar'); - } - }, - - didRender() { - if (!this.isSetup) { - this.$('.koenig-menu-button').onClick = () => { - alert('CLICK'); - }; - this.isSetup = true; - } - }, - - /** - * - * @param {*} editor - * @param {*} notInputting is true if the user isn't typing to filter, this occurs - * if the menu is oppened via pressing + rather than typing in / - */ - open(editor, notInputting) { - let self = this; - let $this = this.$('.koenig-menu'); - let $editor = $('.gh-editor-container'); - - this._node = editor.range.head.section; - this._offset = editor.range.head.offset; - this.isActive = true; - this.isInputting = !notInputting; - this.cursorChange(); - let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM. - - let position = range.getBoundingClientRect(); - let edOffset = $editor.offset(); - - $this.show(); - - run.schedule('afterRender', this, () => { - $this.css('top', position.top + $editor.scrollTop() - edOffset.top + 20); // - edOffset.top+10 - $this.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left); - }); - - this.query = ''; - this.propertyDidChange('toolbar'); - - let downKeyCommand = { - str: 'DOWN', - _ghostName: 'slashdown', - run() { - let item = self.get('menuSelectedItem'); - if (item < self.get('toolsLength') - 1) { - self.set('menuSelectedItem', item + 1); - self.propertyDidChange('toolbar'); - } - } - }; - editor.registerKeyCommand(downKeyCommand); - - let upKeyCommand = { - str: 'UP', - _ghostName: 'slashup', - run() { - let item = self.get('menuSelectedItem'); - if (item > 0) { - self.set('menuSelectedItem', item - 1); - self.propertyDidChange('toolbar'); - } - } - }; - editor.registerKeyCommand(upKeyCommand); - - let enterKeyCommand = { - str: 'ENTER', - _ghostName: 'slashdown', - run(postEditor) { - - let {range} = postEditor; - - range.head.offset = self._offset - 1; - postEditor.deleteRange(range); - self.get('selectedTool').onClick(self.get('editor')); - self.close(); - } - }; - editor.registerKeyCommand(enterKeyCommand); - - let escapeKeyCommand = { - str: 'ESC', - _ghostName: 'slashesc', - run() { - self.close(); - } - }; - editor.registerKeyCommand(escapeKeyCommand); - }, - - close() { - this.isActive = false; - this.isInputting = false; - this.$('.koenig-menu').hide(); - // note: below is using a mobiledoc Private API. - // there is no way to unregister a keycommand when it's registered so we have to remove it ourselves. - // edit: I've put a PR in place and there is now a public API to remove, will add when released. - for (let i = this.editor._keyCommands.length - 1; i > -1; i--) { - let keyCommand = this.editor._keyCommands[i]; - - if (keyCommand._ghostName === 'slashdown' || keyCommand._ghostName === 'slashup' || keyCommand._ghostName === 'slashenter' || keyCommand._ghostName === 'slashesc') { - this.editor._keyCommands.splice(i, 1); - } - } - return; - } -}); 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 new file mode 100644 index 0000000000..3ba59abf5e --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js @@ -0,0 +1,172 @@ +import Component from 'ember-component'; +import computed from 'ember-computed'; +import run from 'ember-runloop'; +import Tools from '../options/default-tools'; +import layout from '../templates/components/koenig-plus-menu'; +import $ from 'jquery'; + +const ROW_LENGTH = 4; + +export default Component.extend({ + layout, + isOpen: false, + isButton: false, + showButton: computed('isOpen', 'isButton', function () { + return this.get('isOpen') || this.get('isButton'); + }), + toolsLength: 0, + selected: 0, + selectedTool: null, + query: '', + range: null, + editor: null, + toolbar: computed('query', 'range', 'selected', function () { + let tools = []; + let match = (this.query || '').trim().toLowerCase(); + let selected = this.get('selected'); + let i = 0; + // todo cache active tools so we don't need to loop through them on selection change. + this.tools.forEach((tool) => { + if ((tool.type === 'block' || tool.type === 'card') && tool.cardMenu === true && (tool.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) { + let t = { + label: tool.label, + name: tool.name, + icon: tool.icon, + onClick: tool.onClick, + range: this.get('range'), + order: tool.order, + selected: false + }; + + tools.push(t); + i++; + } + }); + this.set('toolsLength', i); + tools.sort((a, b) => a.order > b.order); + + let selectedTool = tools[selected] || tools[0]; + if (selectedTool) { + this.set('selectedTool', selectedTool); + selectedTool.selected = true; + } + + 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'); + let $editor = $(this.get('containerSelector')); + + input.blur(() => { + window.setTimeout(() => { + this.send('closeMenu'); + }, 200); + }); + + input.keydown(({keyCode}) => { + let item = this.get('selected'); + let length = this.get('toolsLength'); + switch (keyCode) { + case 27: // escape + return this.send('closeMenu'); + case 37: // left + if (item > 0) { + this.set('selected', item - 1); + } else { + this.set('selected', length - 1); + } + break; + case 38: // up + if (item > ROW_LENGTH) { + this.set('selected', item - ROW_LENGTH); + } else { + this.set('selected', 0); + } + break; + case 39: // right + if (item < length) { + this.set('selected', item + 1); + } else { + this.set('selected', 1); + } + break; + case 40: // down + if (item + ROW_LENGTH < length) { + this.set('selected', item + ROW_LENGTH); + } else { + this.set('selected', length - 1); + } + break; + case 13: // enter + alert('enter'); + } + }); + + editor.cursorDidChange(() => { + if (!editor.range || !editor.range.head.section) { + return; + } + + if (!editor.range.head.section.isBlank) { + this.send('closeMenu'); + return; + } + + let currentNode = editor.range.head.section.renderNode.element; + + let offset = this.$(currentNode).position(); + let editorOffset = $editor.offset(); + + this.set('isButton', true); + run.schedule('afterRender', this, + () => { + let button = this.$('#gh-cardmenu-button'); + button.css('top', offset.top + $editor.scrollTop() - editorOffset.top - 5); + if (currentNode.tagName.toLowerCase() === 'li') { + button.css('left', this.$(currentNode.parentNode).position().left + $editor.scrollLeft() - 90); + } else { + button.css('left', offset.left + $editor.scrollLeft() - 90); + } + }); + }); + }, + actions: { + openMenu: function () { // eslint-disable-line + let button = this.$('#gh-cardmenu-button'); + let editor = this.get('editor'); + this.set('isOpen', true); + + this.set('range', { + section: editor.range.head.section, + startOffset: editor.range.head.offset, + endOffset: editor.range.head.offset + }); + this.propertyDidChange('toolbar'); + + run.schedule('afterRender', this, + () => { + let menu = this.$('.gh-cardmenu'); + menu.css('top', button.css('top')); + menu.css('left', button.css('left') + button.width()); + this.$('.gh-cardmenu-search-input').focus(); + }); + }, + closeMenu: function () { // eslint-disable-line + this.set('isOpen', false); + this.set('isButton', false); + }, + updateSelection: function (event) { // eslint-disable-line + alert(event); + } + } +}); 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 new file mode 100644 index 0000000000..8ec5614dfe --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js @@ -0,0 +1,212 @@ +import Component from 'ember-component'; +import computed from 'ember-computed'; +import run from 'ember-runloop'; +import $ from 'jquery'; +import Tools from '../options/default-tools'; +import layout from '../templates/components/koenig-slash-menu'; + +const ROW_LENGTH = 4; + +export default Component.extend({ + layout, + isOpen: false, + toolsLength: 0, + selected: 0, + selectedTool: null, + query: '', + range: null, + editor: null, + toolbar: computed('query', 'range', 'selected', function () { + let tools = []; + let match = (this.query || '').trim().toLowerCase(); + let selected = this.get('selected'); + let i = 0; + // todo cache active tools so we don't need to loop through them on selection change. + this.tools.forEach((tool) => { + if ((tool.type === 'block' || tool.type === 'card') && tool.cardMenu === true && (tool.label.toLowerCase().startsWith(match) || tool.name.toLowerCase().startsWith(match))) { + let t = { + label: tool.label, + name: tool.name, + icon: tool.icon, + onClick: tool.onClick, + range: this.get('range'), + order: tool.order, + selected: false + }; + + tools.push(t); + i++; + } + }); + this.set('toolsLength', i); + tools.sort((a, b) => a.order > b.order); + + let selectedTool = tools[selected] || tools[0]; + if (selectedTool) { + this.set('selectedTool', selectedTool); + selectedTool.selected = true; + } + if (i === 0) { + alert('close'); + } + 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; + + editor.cursorDidChange(this.cursorChange.bind(this)); + + editor.onTextInput({ + name: 'slash_menu', + text: '/', + run() { + self.send('openMenu'); + } + }); + }, + cursorChange() { + let editor = this.get('editor'); + let range = this.get('range'); + if (!range || !editor.range.isCollapsed || editor.range.head.section !== range.section || this.editor.range.head.offset < 1 || !this.editor.range.head.section) { + this.send('closeMenu'); + return; + } + + if (this.get('isOpen')) { + let queryString = editor.range.head.section.text.substring(range.startOffset, editor.range.head.offset); + this.set('query', queryString); + if (queryString.length > 10) { + this.send('closeMenu'); + } + } + }, + actions: { + openMenu: function () { // eslint-disable-line + let $editor = $(this.get('containerSelector')); + let editor = this.get('editor'); + let self = this; + + this.set('query', ''); + this.set('isOpen', true); + + this.set('range', { + section: editor.range.head.section, + startOffset: editor.range.head.offset, + endOffset: editor.range.head.offset + }); + + editor.registerKeyCommand({ + str: 'LEFT', + name: 'slash', + run() { + let item = self.get('selected'); + let length = self.get('toolsLength'); + if (item > 0) { + self.set('selected', item - 1); + } else { + self.set('selected', length - 1); + } + } + }); + + editor.registerKeyCommand({ + str: 'RIGHT', + name: 'slash', + run() { + let item = self.get('selected'); + let length = self.get('toolsLength'); + if (item < length) { + self.set('selected', item + 1); + } else { + self.set('selected', 1); + } + } + }); + + editor.registerKeyCommand({ + str: 'UP', + name: 'slash', + run() { + let item = self.get('selected'); + if (item > ROW_LENGTH) { + self.set('selected', item - ROW_LENGTH); + } else { + self.set('selected', 0); + } + } + }); + + editor.registerKeyCommand({ + str: 'DOWN', + name: 'slash', + run() { + let item = self.get('selected'); + let length = self.get('toolsLength'); + if (item + ROW_LENGTH < length) { + self.set('selected', item + ROW_LENGTH); + } else { + self.set('selected', length - 1); + } + } + }); + + editor.registerKeyCommand({ + str: 'ENTER', + name: 'slash', + run(postEditor) { + + let {range} = postEditor; + + range.head.offset = self.get('range').startOffset - 1; + postEditor.deleteRange(range); + self.get('selectedTool').onClick(self.get('editor')); + self.send('closeMenu'); + } + }); + + editor.registerKeyCommand({ + str: 'ESC', + name: 'slash', + run() { + self.send('closeMenu'); + } + }); + + let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM. + + let position = range.getBoundingClientRect(); + let edOffset = $editor.offset(); + + run.schedule('afterRender', this, + () => { + let menu = this.$('.gh-cardmenu'); + menu.css('top', position.top + $editor.scrollTop() - edOffset.top + 20); + menu.css('left', position.left + (position.width / 2) + $editor.scrollLeft() - edOffset.left); + this.$('.gh-cardmenu-search-input').focus(); + }); + }, + closeMenu: function () { // eslint-disable-line + this.set('isOpen', false); + let editor = this.get('editor'); + // this.get('editor').unregisterKeyCommand('slash'); -- waiting for the next release for this + + for (let i = editor._keyCommands.length - 1; i > -1; i--) { + let keyCommand = editor._keyCommands[i]; + if (keyCommand.name === 'slash') { + editor._keyCommands.splice(i, 1); + } + } + } + } +}); diff --git a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js index 96821697ca..aa141f3a57 100644 --- a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js +++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar-blockitem.js @@ -39,7 +39,7 @@ export default Component.extend({ didRender() { let $this = this.$(); let {editor} = this; - let $editor = $('.gh-editor-container'); // TODO this is part of Ghost-Admin + let $editor = $(this.get('containerSelector')); // TODO this is part of Ghost-Admin editor.cursorDidChange(() => { 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 32a4c2e248..3d04e8e335 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 @@ -18,9 +18,6 @@ export default Component.extend({ // }, // willRender() { - // TODO: remove console.log - // eslint-disable-next-line no-console - this.set(`gh-toolbar-btn-${this.tool.class}`, true); if (this.tool.selected) { this.set('selected', 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 d4fce5b9da..bb4828ac68 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 @@ -24,7 +24,7 @@ export default Component.extend({ didRender() { let $this = this.$(); let editor = this.get('editor'); - let $editor = $('.gh-editor-container'); + let $editor = $(this.get('containerSelector')); if (!editor.range || !editor.range.head.section || !editor.range.head.section.isBlank || editor.range.head.section.renderNode._element.tagName.toLowerCase() !== 'p') { 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 552a01bd8a..f3c3c562b2 100644 --- a/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js +++ b/ghost/admin/lib/gh-koenig/addon/components/koenig-toolbar.js @@ -60,7 +60,7 @@ export default Component.extend({ didRender() { let $this = this.$(); let {editor} = this; - let $editor = $('.gh-editor-container'); // TODO - this element is part of ghost-admin, we need to separate them more. + let $editor = $(this.get('containerSelector')); // TODO - this element is part of ghost-admin, we need to separate them more. let isMousedown = false; if (!editor.range || editor.range.head.isBlank) { this.set('isVisible', false); @@ -101,7 +101,6 @@ export default Component.extend({ } }, doLink(range) { - this.set('isLink', true); this.set('linkRange', range); } diff --git a/ghost/admin/lib/gh-koenig/addon/options/default-tools.js b/ghost/admin/lib/gh-koenig/addon/options/default-tools.js index 0ccc03fdc5..2b05467dbe 100644 --- a/ghost/admin/lib/gh-koenig/addon/options/default-tools.js +++ b/ghost/admin/lib/gh-koenig/addon/options/default-tools.js @@ -56,10 +56,12 @@ export default function (editor, toolbar) { }, { name: 'p', - label: 'Paragraph', - icon: 'paragraph.svg', + label: 'Text', + icon: 'text.svg', selected: false, type: 'block', + order: 0, + cardMenu: true, onClick: (editor) => { editor.run((postEditor) => { postEditor.toggleSection('p'); @@ -88,10 +90,12 @@ export default function (editor, toolbar) { }, { name: 'ul', - label: 'List Unordered', - icon: 'list-bullets.svg', + label: 'Bullet List', + icon: 'list-bullet.svg', selected: false, type: 'block', + order: 5, + cardMenu: true, onClick: (editor) => { editor.run((postEditor) => { postEditor.toggleSection('ul'); @@ -103,10 +107,12 @@ export default function (editor, toolbar) { }, { name: 'ol', - label: 'List Ordered', + label: 'Number List', icon: 'list-number.svg', selected: false, type: 'block', + order: 6, + cardMenu: true, onClick: (editor) => { editor.run((postEditor) => { postEditor.toggleSection('ol'); @@ -129,8 +135,9 @@ export default function (editor, toolbar) { postEditor.toggleMarkup('strong'); }); }, - checkElements(elements) { - set(this, 'selected', elements.filter((element) => element.tagName === 'strong').length > 0); + checkElements(/* elements */) { + set(this, 'selected', true); + // set(this, 'selected', elements.filter((element) => element.tagName === 'strong').length > 0); } }, { @@ -186,12 +193,14 @@ export default function (editor, toolbar) { label: 'Image', selected: false, type: 'card', - icon: 'file-picture-add.svg', + icon: 'photos.svg', visibility: 'primary', + order: 2, + cardMenu: true, onClick: (editor) => { editor.run((postEditor) => { let card = postEditor.builder.createCardSection('image-card', {pos: 'top'}); - postEditor.replaceSection(editor.range.headSection, card); + postEditor.insertSection(card); }); }, @@ -201,32 +210,54 @@ export default function (editor, toolbar) { }, { name: 'html', - label: 'Embed HTML', + label: 'Embed', selected: false, type: 'card', - icon: 'html-five.svg', + icon: 'brackets.svg', visibility: 'primary', - onClick: (editor) => { + order: 3, + cardMenu: true, + onClick: (editor, section) => { editor.run((postEditor) => { - let card = postEditor.builder.createCardSection('html-card', {pos: 'top'}); - postEditor.replaceSection(editor.range.headSection, card); + let card = postEditor.builder.createCardSection('html-card', {pos: 'top', html: editor.range.headSection.text}); + postEditor.replaceSection(section || editor.range.headSection, card); }); }, checkElements() { } }, + { + name: 'hr', + label: 'Divider', + selected: false, + type: 'card', + icon: 'line.svg', + visibility: 'primary', + order: 4, + cardMenu: true, + onClick: (editor) => { + editor.run((postEditor) => { + let card = postEditor.builder.createCardSection('hr-card', {pos: 'top'}); + postEditor.insertSection(card); + }); + }, + checkElements() { + } + }, { name: 'md', - label: 'Embed Markdown', + label: 'Markdown', selected: false, type: 'card', visibility: 'primary', - icon: 'file-code-1.svg', - onClick: (editor) => { + icon: 'markdown.svg', + order: 1, + cardMenu: true, + onClick: (editor, section) => { editor.run((postEditor) => { - let card = postEditor.builder.createCardSection('markdown-card', {pos: 'top'}); - postEditor.replaceSection(editor.range.headSection, card); + let card = postEditor.builder.createCardSection('markdown-card', {pos: 'top', markdown: editor.range.headSection.text}); + postEditor.replaceSection(section || editor.range.headSection, card); }); }, checkElements() { 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 36862f93a5..c3b5533971 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 @@ -9,5 +9,6 @@ {{yield}} -{{koenig-toolbar editor=editor assetPath=assetPath}} -{{koenig-menu editor=editor assetPath=assetPath}} \ No newline at end of file +{{koenig-toolbar editor=editor assetPath=assetPath containerSelector=containerSelector}} +{{koenig-slash-menu editor=editor assetPath=assetPath containerSelector=containerSelector}} +{{koenig-plus-menu editor=editor assetPath=assetPath containerSelector=containerSelector}} \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/hr-card.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/hr-card.hbs new file mode 100644 index 0000000000..b5055ee674 --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/hr-card.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/html-card.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/html-card.hbs index 6c39d27f51..34bcc00ce6 100644 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/html-card.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/html-card.hbs @@ -1,5 +1,5 @@ {{#if isEditing}} - {{{value}}} -{{else}} {{gh-cm-editor value update=(action (mut value))}} {{!-- codemirror editor component from Ghost-Admin --}} +{{else}} + {{{value}}} {{/if}} diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs index e08d1f39b9..3e74212e94 100644 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu-item.hbs @@ -1,3 +1,8 @@ +
{{inline-svg tool.icon}}
+
{{tool.label}}
+ +{{!-- + {{#if selected}} {{/if}} - +--}} diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu.hbs deleted file mode 100644 index cf6809023f..0000000000 --- a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-menu.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
- -
- -
+
\ No newline at end of file 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 new file mode 100644 index 0000000000..3f961940dd --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-plus-menu.hbs @@ -0,0 +1,17 @@ +{{#if showButton}} + +{{/if}} +{{#if isOpen}} +
+ +
+ Primary +
+ {{#each toolbar as |tool index|}} + {{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "closeMenu")}} + {{/each}} +
+{{/if}} \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs new file mode 100644 index 0000000000..3a5290baa7 --- /dev/null +++ b/ghost/admin/lib/gh-koenig/addon/templates/components/koenig-slash-menu.hbs @@ -0,0 +1,7 @@ +{{#if isOpen}} +
+ {{#each toolbar as |tool index|}} + {{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "closeMenu")}} + {{/each}} +
+{{/if}} \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/app/components/hr-card.js b/ghost/admin/lib/gh-koenig/app/components/hr-card.js new file mode 100644 index 0000000000..52d3c63d65 --- /dev/null +++ b/ghost/admin/lib/gh-koenig/app/components/hr-card.js @@ -0,0 +1 @@ +export {default} from 'gh-koenig/components/cards/hr-card'; diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-menu.js b/ghost/admin/lib/gh-koenig/app/components/koenig-menu.js deleted file mode 100644 index 2214ffbd03..0000000000 --- a/ghost/admin/lib/gh-koenig/app/components/koenig-menu.js +++ /dev/null @@ -1 +0,0 @@ -export {default} from 'gh-koenig/components/koenig-menu'; diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js b/ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js new file mode 100644 index 0000000000..649cf63fda --- /dev/null +++ b/ghost/admin/lib/gh-koenig/app/components/koenig-plus-menu.js @@ -0,0 +1 @@ +export {default} from 'gh-koenig/components/koenig-plus-menu'; \ No newline at end of file diff --git a/ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js b/ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js new file mode 100644 index 0000000000..d51030f04f --- /dev/null +++ b/ghost/admin/lib/gh-koenig/app/components/koenig-slash-menu.js @@ -0,0 +1 @@ +export {default} from 'gh-koenig/components/koenig-slash-menu'; diff --git a/ghost/admin/package.json b/ghost/admin/package.json index 7083c20df8..8a0d82717e 100644 --- a/ghost/admin/package.json +++ b/ghost/admin/package.json @@ -97,7 +97,7 @@ "liquid-wormhole": "2.0.4", "loader.js": "4.2.3", "matchdep": "1.0.1", - "mobiledoc-kit": "0.10.14", + "mobiledoc-kit": "0.10.15", "moment": "2.17.1", "moment-timezone": "0.5.11", "password-generator": "2.1.0", diff --git a/ghost/admin/tests/helpers/editor-helpers.js b/ghost/admin/tests/helpers/editor-helpers.js index 714228f8a8..f000b3fd80 100644 --- a/ghost/admin/tests/helpers/editor-helpers.js +++ b/ghost/admin/tests/helpers/editor-helpers.js @@ -1,4 +1,5 @@ import Ember from 'ember'; +import $ from 'jquery'; // polls the editor until it's started. export function editorRendered() { @@ -42,4 +43,24 @@ export function testInput(input, output, expect) { }); inputText(window.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(); + }); } \ No newline at end of file diff --git a/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js b/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js new file mode 100644 index 0000000000..81163e0b33 --- /dev/null +++ b/ghost/admin/tests/integration/components/gh-koenig-slashmenu-test.js @@ -0,0 +1,59 @@ +/* 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, waitForRender, inputText} from '../../helpers/editor-helpers'; +import $ from 'jquery'; + +describe('Integration: Component: gh-cm-editor', function () { + setupComponentTest('gh-koenig', { + integration: true + }); + + it('thge slash menu appears on user input', function (done) { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.editor-holder' + }}`); + + 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.skip('searches when a user types', function (done) { + this.render(hbs`{{gh-koenig + apiRoot='/todo' + assetPath='/assets' + containerSelector='.editor-holder' + }}`); + + 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); + return testInput(' lis', '/ lis', expect); + }) + .then(() => { + let cardMenu = $('.gh-cardmenu'); + expect(cardMenu.children().length).to.equal(2); + done(); + }); + }); +});