👯 ♥️ ♣️ ♦️ ♠️ 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:
Ryan McCarvill 2017-03-15 00:59:34 +13:00 committed by Kevin Ansfield
parent 1b4db78eae
commit 9a0b72071d
30 changed files with 612 additions and 267 deletions

View File

@ -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;

View File

@ -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;
}

View File

@ -51,6 +51,7 @@
apiRoot=apiRoot
assetPath=assetPath
tabindex=2
containerSelector='.gh-editor-container'
}}
</div>
</div>

View File

@ -0,0 +1,8 @@
export default {
name: 'hr-card',
label: 'HR Card',
icon: '',
genus: 'ember',
buttons: {
}
};

View File

@ -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);
});

View File

@ -0,0 +1,5 @@
import Component from 'ember-component';
import layout from '../../templates/components/hr-card';
export default Component.extend({
layout
});

View File

@ -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() {

View File

@ -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');
}
});

View File

@ -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;
}
});

View 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);
}
}
});

View 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);
}
}
}
}
});

View File

@ -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(() => {

View File

@ -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);

View File

@ -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') {

View File

@ -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);
}

View File

@ -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() {

View File

@ -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}}

View File

@ -0,0 +1 @@
<hr>

View File

@ -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}}

View File

@ -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}}
--}}

View File

@ -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>

View File

@ -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}}

View File

@ -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}}

View File

@ -0,0 +1 @@
export {default} from 'gh-koenig/components/cards/hr-card';

View File

@ -1 +0,0 @@
export {default} from 'gh-koenig/components/koenig-menu';

View File

@ -0,0 +1 @@
export {default} from 'gh-koenig/components/koenig-plus-menu';

View File

@ -0,0 +1 @@
export {default} from 'gh-koenig/components/koenig-slash-menu';

View File

@ -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",

View File

@ -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();
});
}

View File

@ -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();
});
});
});