mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-24 14:43:08 +03:00
👯 ♥️ ♣️ ♦️ ♠️ New editor card menu (#580)
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.
This commit is contained in:
parent
1b4db78eae
commit
9a0b72071d
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -51,6 +51,7 @@
|
||||
apiRoot=apiRoot
|
||||
assetPath=assetPath
|
||||
tabindex=2
|
||||
containerSelector='.gh-editor-container'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
8
ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js
Normal file
8
ghost/admin/lib/gh-koenig/addon/cards/hr-card_dom.js
Normal file
@ -0,0 +1,8 @@
|
||||
export default {
|
||||
name: 'hr-card',
|
||||
label: 'HR Card',
|
||||
icon: '',
|
||||
genus: 'ember',
|
||||
buttons: {
|
||||
}
|
||||
};
|
@ -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);
|
||||
});
|
||||
|
@ -0,0 +1,5 @@
|
||||
import Component from 'ember-component';
|
||||
import layout from '../../templates/components/hr-card';
|
||||
export default Component.extend({
|
||||
layout
|
||||
});
|
@ -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() {
|
||||
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
@ -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;
|
||||
}
|
||||
});
|
172
ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js
Normal file
172
ghost/admin/lib/gh-koenig/addon/components/koenig-plus-menu.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
212
ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js
Normal file
212
ghost/admin/lib/gh-koenig/addon/components/koenig-slash-menu.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
@ -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(() => {
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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') {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -9,5 +9,6 @@
|
||||
|
||||
{{yield}}
|
||||
|
||||
{{koenig-toolbar editor=editor assetPath=assetPath}}
|
||||
{{koenig-menu editor=editor assetPath=assetPath}}
|
||||
{{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}}
|
@ -0,0 +1 @@
|
||||
<hr>
|
@ -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}}
|
||||
|
@ -1,3 +1,8 @@
|
||||
<div class="gh-cardmenu-icon">{{inline-svg tool.icon}}</div>
|
||||
<div class="gh-cardmenu-label">{{tool.label}}</div>
|
||||
|
||||
{{!--
|
||||
|
||||
{{#if selected}}
|
||||
<button {{action "select"}} class="selected">
|
||||
{{#if tool.icon}}
|
||||
@ -17,4 +22,4 @@
|
||||
{{tool.label}}
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
--}}
|
||||
|
@ -1,9 +0,0 @@
|
||||
<div class='koenig-menu'>
|
||||
<ul>
|
||||
{{#each toolbar as |tool index|}}
|
||||
{{koenig-menu-item tool=tool iconURL=iconURL editor=editor range=range selected=tool.selected}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="koenig-menu-button">+</div>
|
@ -0,0 +1,17 @@
|
||||
{{#if showButton}}
|
||||
<button id="gh-cardmenu-button" {{action "openMenu"}}>+</button>
|
||||
{{/if}}
|
||||
{{#if isOpen}}
|
||||
<div class="gh-cardmenu">
|
||||
<div class="gh-cardmenu-search">
|
||||
{{inline-svg "search.svg"}}
|
||||
{{gh-input query class="gh-input gh-cardmenu-search-input" placeholder="Search for a card..." type="text" update=(action (mut query)) key-press=(action "updateSelection")}}
|
||||
</div>
|
||||
<div class="gh-cardmenu-divider">
|
||||
Primary
|
||||
</div>
|
||||
{{#each toolbar as |tool index|}}
|
||||
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "closeMenu")}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
@ -0,0 +1,7 @@
|
||||
{{#if isOpen}}
|
||||
<div class="gh-cardmenu">
|
||||
{{#each toolbar as |tool index|}}
|
||||
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "closeMenu")}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
1
ghost/admin/lib/gh-koenig/app/components/hr-card.js
Normal file
1
ghost/admin/lib/gh-koenig/app/components/hr-card.js
Normal file
@ -0,0 +1 @@
|
||||
export {default} from 'gh-koenig/components/cards/hr-card';
|
@ -1 +0,0 @@
|
||||
export {default} from 'gh-koenig/components/koenig-menu';
|
@ -0,0 +1 @@
|
||||
export {default} from 'gh-koenig/components/koenig-plus-menu';
|
@ -0,0 +1 @@
|
||||
export {default} from 'gh-koenig/components/koenig-slash-menu';
|
@ -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",
|
||||
|
@ -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();
|
||||
});
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user