Editor refactors (#679)

no issue

* split key commands and text expansions into separate files for easier file searches

* basic formatting, added a few comments

* move editor title input into addon
- the editor and title are now tightly integrated so that it's possible to use up/down cursor navigation so it makes more sense to keep them together
- start of a deeper component restructure so that we don't need to leak properties/actions to parent components

* first pass at refactor of gh-koenig and koenig-title-input
- remove need for editor reference to be held outside of the `gh-koenig` component by yielding it from the component so that the integrated title element can sit inside the container's scope
- refactor `gh-koenig` to more closely match the default ember mobiledoc addon
  - fixes runloop issues by starting/ending a manual runloop
- refactored the mutation observer and event handlers in `koenig-title-input` so that we're not doing unecessary work on every render/key press
- rename CSS classes to be more specific (these may still need more separation between `.gh` and `.kg` later)
  - `.editor-holder` to `.gh-koenig-container`
  - `.surface` to `.gh-koenig-surface`

* fix tests and start testing refactor

* move gh-koenig integration tests into addon, remove empty test files

* first-pass at component template cleanup

* first pass at koenig-toolbar-button refactor
This commit is contained in:
Kevin Ansfield 2017-05-08 10:44:02 +01:00 committed by Hannah Wolfe
parent 275ac3cd0f
commit 756b6627a9
42 changed files with 1167 additions and 1159 deletions

View File

@ -571,19 +571,7 @@ export default Mixin.create({
this.toggleProperty('showReAuthenticateModal');
},
setEditor(editor) {
this.set('editor', editor);
},
editorMenuIsOpen() {
this.set('editorMenuIsOpen', true);
},
editorMenuIsClosed() {
this.set('editorMenuIsOpen', false);
},
wordcountDidChange(wordcount) {
setWordcount(wordcount) {
this.set('wordcount', wordcount);
}
}

View File

@ -2,7 +2,7 @@
@import "koenig-menu.css";
@import "../ghost-editor/cardmenu.css";
.editor-holder {
.gh-koenig-container {
height: 100%;
}
.gh-koenig {
@ -10,6 +10,71 @@
-webkit-overflow-scrolling: touch;
}
/* Title
/* ---------------------------------------------------------- */
.kg-title-input {
flex-grow: 1;
margin-bottom: 2vw;
padding: 0;
margin: 0;
outline: none;
position: relative;
width: 100%;
letter-spacing: 0.8px;
font-weight: bold;
font-size: 3.2rem;
line-height: 1.3em;
min-height: 1.3em;
z-index: 1;
}
/* Place holder content that displays in the title if it is empty */
.kg-title-input.no-content:before {
content: attr(data-placeholder);
color: color(var(--midgrey) l(+35%));
cursor: text;
position: relative;
top: 0;
font-size: 3.2rem;
font-weight: bold;
line-height: 1.3em;
min-width: 30rem; /* hack it's defaulting just to enough width for the 'Your' in 'Your Post Title' */
z-index: -1;
}
.kg-title-input input {
margin: 0;
padding: 0;
width: 100%;
border: 0;
background: transparent;
color: var(--darkgrey);
font-size: 3.2rem;
font-weight: bold;
letter-spacing: 0.8px;
}
.kg-title-input input::-webkit-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.kg-title-input input:-ms-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.kg-title-input input:focus {
outline: 0;
}
/* Editor
/* ---------------------------------------------------------- */
.__mobiledoc-editor {
width: 100%;
height: 100%;

View File

@ -1,69 +1,6 @@
/* Editor /ghost/editor/
/* ---------------------------------------------------------- */
/* Title
/* ---------------------------------------------------------- */
.gh-editor-title {
flex-grow: 1;
margin-bottom: 2vw;
padding: 0;
margin: 0;
outline: none;
position: relative;
width: 100%;
letter-spacing: 0.8px;
font-weight: bold;
font-size: 3.2rem;
line-height: 1.3em;
min-height: 1.3em;
z-index: 1;
}
/* Place holder content that displays in the title if it is empty */
.gh-editor-title.no-content:before {
content: attr(data-placeholder);
color: color(var(--midgrey) l(+35%));
cursor: text;
position: relative;
top: 0;
font-size: 3.2rem;
font-weight: bold;
line-height: 1.3em;
min-width: 30rem; /* hack it's defaulting just to enough width for the 'Your' in 'Your Post Title' */
z-index: -1;
}
.gh-editor-title input {
margin: 0;
padding: 0;
width: 100%;
border: 0;
background: transparent;
color: var(--darkgrey);
font-size: 3.2rem;
font-weight: bold;
letter-spacing: 0.8px;
}
.gh-editor-title input::-webkit-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.gh-editor-title input:-ms-input-placeholder {
color: color(var(--midgrey) l(+25%));
font-weight: 400;
letter-spacing: 1.2px;
}
.gh-editor-title input:focus {
outline: 0;
}
.editor-options .dropdown-menu {
top: 35px;
right: 0;
@ -265,4 +202,4 @@
.gh-editor-wordcount {
display: none;
}
}
}

View File

@ -26,32 +26,35 @@
<div class="gh-editor-container needsclick">
<div class="gh-editor-inner">
{{gh-editor-title
val=(readonly model.titleScratch)
onChange=(action (mut model.titleScratch))
tabindex="1"
shouldFocus=shouldFocusTitle
focus-out="updateTitle"
update=(action (perform updateTitle))
id="gh-editor-title"
koenigEditor=(readonly editor)
editorMenuIsOpen=editorMenuIsOpen
}}
{{gh-koenig
value=(readonly model.scratch)
{{!--
NOTE: the mobiledoc property is unbound so that the setting the
serialized version onChange doesn't cause a deserialization and
re-render of the editor on every key press / editor change
--}}
{{#gh-koenig
mobiledoc=(unbound model.scratch)
onChange=(action "updateScratch")
onFirstChange=(action "autoSaveNew")
shouldFocusEditor=shouldFocusEditor
apiRoot=apiRoot
assetPath=assetPath
autofocus=shouldFocusEditor
tabindex="2"
titleSelector=".gh-editor-title"
titleSelector="#kg-title-input"
containerSelector=".gh-editor-container"
setEditor=(action "setEditor")
menuIsOpen=(action "editorMenuIsOpen")
menuIsClosed=(action "editorMenuIsClosed")
wordcountDidChange=(action "wordcountDidChange")
wordcountDidChange=(action "setWordcount")
as |koenig|
}}
{{koenig-title-input
id="koenig-title-input"
val=(readonly model.titleScratch)
onChange=(action (mut model.titleScratch))
tabindex="1"
autofocus=shouldFocusTitle
focus-out="updateTitle"
update=(action (perform updateTitle))
editor=(readonly koenig.editor)
editorHasRendered=koenig.hasRendered
editorMenuIsOpen=koenig.isMenuOpen
}}
{{/gh-koenig}}
</div>
</div>
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>

View File

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

View File

@ -1,147 +1,245 @@
import Ember from 'ember';
import Component from 'ember-component';
import {A as emberA} from 'ember-array/utils';
import run from 'ember-runloop';
import layout from '../templates/components/gh-koenig';
import Mobiledoc from 'mobiledoc-kit';
import Editor from 'mobiledoc-kit/editor/editor';
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import createCardFactory from '../lib/card-factory';
import defaultCommands from '../options/default-commands';
import editorCards from '../cards/index';
import {getCardFromDoc, checkIfClickEventShouldCloseCard, getPositionOnScreenFromRange} from '../lib/utils';
import registerKeyCommands from '../options/key-commands';
import registerTextExpansions from '../options/text-expansions';
import defaultCards from '../cards/index';
import {
getCardFromDoc,
checkIfClickEventShouldCloseCard,
getPositionOnScreenFromRange
} from '../lib/utils';
import counter from 'ghost-admin/utils/word-count';
import $ from 'jquery';
import computed from 'ember-computed';
import {assign} from 'ember-platform';
// ember-cli-shims doesn't export Ember.testing
const {testing} = Ember;
export const TESTING_EXPANDO_PROPERTY = '__koenig_editor';
export const BLANK_DOC = {
version: MOBILEDOC_VERSION,
atoms: [],
markups: [],
atoms: [],
cards: [],
sections: [[1, 'p', [[0, [], 0, '']]]]
};
export default Component.extend({
layout,
classNames: ['editor-holder'],
emberCards: emberA([]),
selectedCard: null,
classNames: ['gh-koenig-container'],
// exterally set properties
mobiledoc: null,
placeholder: 'Click here to start ...',
spellcheck: true,
autofocus: false,
cards: null,
atoms: null,
serializeVersion: MOBILEDOC_VERSION,
options: {},
// exposed properties
editor: null,
editedCard: null,
keyDownHandler: [],
resizeEvent: 0,
selectedCard: null,
emberCards: null,
isMenuOpen: false,
editorHasRendered: false,
// internal properties
_domContainer: null,
// TODO: keyDownHandler is assigned event handlers when a card is
// hard-selected, is there a better way of handling this?
_keyDownHandler: null,
// merge in named options with the `options` property data-bag
editorOptions: computed(function () {
let options = this.get('options');
let cards = this.get('cards') || [];
let atoms = this.get('atoms') || [];
// use our CardFactory to wrap our default and any user-supplied cards
// with Ghost specific functionality
// TODO: this also sets the emberCards property - do we need that indirection?
let createCard = createCardFactory.apply(this, {}); // need to pass the toolbar
cards = defaultCards.concat(cards).map((card) => createCard(card));
// add our default atoms
atoms.concat([{
name: 'soft-return',
type: 'dom',
render() {
return document.createElement('br');
}
}]);
return assign({
placeholder: this.get('placeholder'),
spellchack: this.get('spellcheck'),
autofocus: this.get('autofocus'),
// cardOptions: this.get('cardOptions'),
cards,
atoms
}, options);
}),
init() {
this._super(...arguments);
let mobiledoc = this.get('value') || BLANK_DOC;
let userCards = this.get('cards') || [];
// grab the supplied mobiledoc value - if it's empty set our default
// blank document, if it's a JSON string then deserialize it
let mobiledoc = this.get('mobiledoc');
if (!mobiledoc) {
mobiledoc = BLANK_DOC;
this.set('mobiledoc', mobiledoc);
}
if (typeof mobiledoc === 'string') {
mobiledoc = JSON.parse(mobiledoc);
this.set('mobiledoc', mobiledoc);
}
// if the doc is cached then the editor is loaded and we don't need to continue.
if (this._cachedDoc && this._cachedDoc === mobiledoc) {
return;
}
let createCard = createCardFactory.apply(this, {}); // need to pass the toolbar
let options = {
mobiledoc,
// temp
cards: createCard(editorCards.concat(userCards)),
atoms: [{
name: 'soft-return',
type: 'dom',
render() {
return document.createElement('br');
}
}],
spellcheck: true,
autofocus: this.get('shouldFocusEditor'),
placeholder: 'Click here to start ...',
unknownCardHandler: () => {
// todo
}
};
this.set('editor', new Mobiledoc.Editor(options));
this.set('emberCards', []);
this._keyDownHandler = [];
// we use css media width for most things but need to know if a device is touch
// to place the toolbar. Above the selected content on a mobile browser is the
// cut | copy | paste menu so we need to place our toolbar below.
// TODO: is this reliable enough? What about most Windows laptops now being touch enabled?
this.set('isTouch', 'ontouchstart' in document.documentElement);
// window resize handler - throttled
// window.onresize = () => {
// let now = Date.now();
// if (now - 2000 > this.get('resizeEvent')) {
// this.set('resizeEvent', now);
// }
// };
run.next(() => {
if (this.get('setEditor')) {
this.sendAction('setEditor', this.get('editor'));
}
});
this._startedRunLoop = false;
},
willRender() {
if (this._rendered) {
// Use a default mobiledoc. If there are no changes, then return early.
let mobiledoc = this.get('mobiledoc') || BLANK_DOC;
let noMobiledocChanges
= (this._localMobiledoc && this._localMobiledoc === mobiledoc)
|| (this._upstreamMobiledoc && this._upstreamMobiledoc === mobiledoc);
if (noMobiledocChanges) {
return;
}
// reset everything ready for an editor re-render
this._upstreamMobiledoc = mobiledoc;
this._localMobiledoc = null;
// trigger hook action
this._willCreateEditor();
// teardown any old editor that might be around
let editor = this.get('editor');
if (editor) {
editor.destroy();
}
// create a new editor
let editorOptions = this.get('editorOptions');
editorOptions.mobiledoc = mobiledoc;
// TODO: instantiate component hooks?
// https://github.com/bustlelabs/ember-mobiledoc-editor/blob/master/addon/components/mobiledoc-editor/component.js#L163-L227
editor = new Editor(editorOptions);
// set up our default key handling and text expansions to emulate MD behaviour
// TODO: better place to do this?
registerKeyCommands(editor);
registerTextExpansions(editor);
editor.willRender(() => {
// The editor's render/rerender will happen after this `editor.willRender`,
// so we explicitly start a runloop here if there is none, so that the
// add/remove card hooks happen inside a runloop.
// When pasting text that gets turned into a card, for example,
// the add card hook would run outside the runloop if we didn't begin a new
// one now.
if (!run.currentRunLoop) {
this._startedRunLoop = true;
run.begin();
}
});
editor.didRender(() => {
// If we had explicitly started a run loop in `editor.willRender`,
// we must explicitly end it here.
if (this._startedRunLoop) {
this._startedRunLoop = false;
run.end();
}
this.sendAction('loaded', editor);
this.set('editorHasRendered', true);
});
editor.postDidChange(()=> {
editor.postDidChange(() => {
run.join(() => {
// store a cache of the local doc so that we don't need to reinitialise it.
this._cachedDoc = editor.serialize(MOBILEDOC_VERSION);
this.sendAction('onChange', this._cachedDoc);
if (this._cachedDoc !== BLANK_DOC && !this._firstChange) {
this._firstChange = true;
this.sendAction('onFirstChange', this._cachedDoc);
}
this.processWordcount();
this.postDidChange(editor);
});
});
editor.cursorDidChange(() => {
if (this.isDestroyed) {
return;
}
run.join(() => {
this.cursorMoved();
});
});
this.set('editor', editor);
// trigger hook action
this._didCreateEditor(editor);
},
didRender() {
// listen to keydown events outside of the editor, used to handle keydown events in the cards.
document.onkeydown = (event) => {
// if any of the keydown handlers return false then we return false therefore stopping the event from propogating.
return this.get('keyDownHandler').reduce((returnType, handler) => {
let result = handler(event);
if (returnType !== false) {
return result;
}
return returnType;
}, true);
};
if (this._rendered) {
return;
// listen to keydown events outside of the editor, used to handle keydown
// events in the cards.
// TODO: is there a better way to handle this?
if (!document.onkeydown) {
document.onkeydown = (event) => {
// if any of the keydown handlers return false then we return false
// therefore stopping the event from propogating.
return this._keyDownHandler.reduce((returnType, handler) => {
let result = handler(event);
if (returnType !== false) {
return result;
}
return returnType;
}, true);
};
}
let editor = this.get('editor');
let $editor = this.$('.surface');
let [domContainer] = $editor.parents(this.get('containerSelector'));
let [editorDom] = $editor;
editorDom.tabindex = this.get('tabindex');
this.set('domContainer', domContainer);
if (!editor.hasRendered) {
let $editor = this.$('.gh-koenig-surface');
let [domContainer] = $editor.parents(this.get('containerSelector'));
let [editorDom] = $editor;
editor.render(editorDom);
this.set('_rendered', true);
editorDom.tabindex = this.get('tabindex');
this._domContainer = domContainer;
// set global editor for debugging and testing.
window.editor = editor;
this._isRenderingEditor = true;
editor.render(editorDom);
this._isRenderingEditor = false;
}
this._setExpandoProperty(editor);
defaultCommands(editor); // initialise the custom text handlers for MD, etc.
// shouldFocusEditor is only true when transitioning from new to edit, otherwise it's false or undefined.
// therefore, if it's true it's after the first lot of content is entered and we expect the caret to be at the
// end of the document.
if (this.get('shouldFocusEditor')) {
// autofocus is only true when transitioning from new to edit,
// otherwise it's false or undefined. therefore, if it's true it's after
// the first lot of content is entered and we expect the caret to be at
// the end of the document.
// TODO: can this be removed if we refactor the new/edit screens to not re-render?
if (this.get('autofocus')) {
let range = document.createRange();
range.selectNodeContents(this.editor.element);
range.collapse(false);
@ -151,12 +249,13 @@ export default Component.extend({
editor._ensureFocus(); // PRIVATE API
}
editor.cursorDidChange(() => this.cursorMoved());
this.processWordcount();
},
// makes sure the cursor is on screen except when selection is happening in which case the browser mostly ensures it.
// there is an issue with keyboard selection on some browsers though so the next step may be to record mouse and touch events.
// makes sure the cursor is on screen except when selection is happening in
// which case the browser mostly ensures it. there is an issue with keyboard
// selection on some browsers though so the next step may be to record mouse
// and touch events.
cursorMoved() {
let editor = this.get('editor');
@ -172,9 +271,9 @@ export default Component.extend({
let windowHeight = window.innerHeight;
if (position.bottom > windowHeight) {
this.domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
this._domContainer.scrollTop += position.bottom - windowHeight + scrollBuffer;
} else if (position.top < 0) {
this.domContainer.scrollTop += position.top - scrollBuffer;
this._domContainer.scrollTop += position.top - scrollBuffer;
}
if (editor.range && editor.range.headSection && editor.range.headSection.isCardSection) {
@ -195,8 +294,10 @@ export default Component.extend({
this.send('deselectCard');
}
},
// Note: This wordcount function doesn't count words that have been entered in cards.
// We should either allow cards to report their own wordcount or use the DOM (innerText) to calculate the wordcount.
// NOTE: This wordcount function doesn't count words that have been entered in cards.
// We should either allow cards to report their own wordcount or use the DOM
// (innerText) to calculate the wordcount.
processWordcount() {
let wordcount = 0;
if (this.editor.post.sections.length) {
@ -210,16 +311,49 @@ export default Component.extend({
}
this.sendAction('wordcountDidChange', wordcount);
},
willDestroy() {
_willCreateEditor() {
this.sendAction('willCreateEditor');
},
_didCreateEditor(editor) {
this.sendAction('didCreateEditor', editor);
},
willDestroyElement() {
this.editor.destroy();
this.send('deselectCard');
// TODO: should we be killing all global onkeydown event handlers?
document.onkeydown = null;
// window.oresize = null;
},
postDidChange(editor) {
// store a cache of the local doc so that we don't need to reinitialise it.
let serializeVersion = this.get('serializeVersion');
let updatedMobiledoc = editor.serialize(serializeVersion);
this._localMobiledoc = updatedMobiledoc;
this.sendAction('onChange', updatedMobiledoc);
// we need to trigger a first-change action so that we can trigger a
// save and transition from new-> edit
if (this._localMobiledoc !== BLANK_DOC && !this._hasChanged) {
this._hasChanged = true;
this.sendAction('onFirstChange', this._localMobiledoc);
}
this.processWordcount();
},
_setExpandoProperty(editor) {
// Store a reference to the editor for the acceptance test helpers
if (this.element && testing) {
this.element[TESTING_EXPANDO_PROPERTY] = editor;
}
},
actions: {
// thin border, shows that a card is selected but the user cannot delete the card with
// keyboard events.
// thin border, shows that a card is selected but the user cannot delete
// the card with keyboard events.
// used when the content of the card is selected and it is editing.
selectCard(cardId) {
if (!cardId) {
@ -240,7 +374,7 @@ export default Component.extend({
cardHolder.addClass('selected');
cardHolder.removeClass('selected-hard');
this.set('selectedCard', card);
this.get('keyDownHandler').length = 0;
this._keyDownHandler.length = 0;
// cardHolder.focus();
document.onclick = (event) => {
if (checkIfClickEventShouldCloseCard($(event.target), cardHolder)) {
@ -248,6 +382,7 @@ export default Component.extend({
}
};
},
// thicker border and with keyboard events for moving around the editor
// creating blocks under the card and deleting the card.
// used when selecting the card with the keyboard or clicking on the toolbar.
@ -283,8 +418,7 @@ export default Component.extend({
}
};
let keyDownHandler = this.get('keyDownHandler');
keyDownHandler.push((event) => {
this._keyDownHandler.push((event) => {
let editor = this.get('editor');
switch (event.keyCode) {
case 37: // arrow left
@ -359,6 +493,7 @@ export default Component.extend({
}
});
},
deselectCard() {
let selectedCard = this.get('selectedCard');
if (selectedCard) {
@ -367,16 +502,19 @@ export default Component.extend({
cardHolder.removeClass('selected-hard');
this.set('selectedCard', null);
}
this.get('keyDownHandler').length = 0;
this._keyDownHandler.length = 0;
// TODO: do we want to kill all document onclick handlers?
document.onclick = null;
this.set('editedCard', null);
},
editCard(cardId) {
let card = this.get('emberCards').find((card) => card.id === cardId);
this.set('editedCard', card);
this.send('selectCard', cardId);
},
deleteCard(cardId, forwards = false) {
let editor = this.get('editor');
let card = this.get('emberCards').find((card) => card.id === cardId);
@ -401,15 +539,19 @@ export default Component.extend({
editor.selectRange(range);
});
},
stopEditingCard() {
this.set('editedCard', null);
},
menuIsOpen() {
this.sendAction('menuIsOpen');
menuOpened() {
this.set('isMenuOpen', true);
},
menuIsClosed() {
this.sendAction('menuIsClosed');
menuClosed() {
this.set('isMenuOpen', false);
},
// drag and drop images onto the editor
dropImage(event) {
if (event.dataTransfer.files.length) {
@ -420,6 +562,7 @@ export default Component.extend({
}
}
},
dragOver(event) {
// required for drop events to fire on markdown cards in firefox.
event.preventDefault();

View File

@ -7,10 +7,12 @@ export default Component.extend({
tagName: 'div',
classNames: ['gh-cardmenu-card'],
classNameBindings: ['selected'],
init() {
this._super(...arguments);
this.set('selected', this.get('tool').selected);
},
click: function () { // eslint-disable-line
let {section, startOffset, endOffset} = this.get('range');
let editor = this.get('editor');

View File

@ -12,15 +12,17 @@ export default Component.extend({
layout,
isOpen: false,
isButton: false,
showButton: computed('isOpen', 'isButton', function () {
return this.get('isOpen') || this.get('isButton');
}),
toolsLength: 0,
selected: -1,
selectedTool: null,
query: '',
range: null,
editor: null,
showButton: computed('isOpen', 'isButton', function () {
return this.get('isOpen') || this.get('isButton');
}),
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
@ -58,15 +60,12 @@ export default Component.extend({
}
return tools;
}),
init() {
this._super(...arguments);
this.tools = new Tools(this.get('editor'), this);
},
willDestroy() {
},
didRender() {
let editor = this.get('editor');
let input = this.$('.gh-cardmenu-search-input');
@ -110,8 +109,9 @@ export default Component.extend({
});
});
},
actions: {
openMenu: function () { // eslint-disable-line
openMenu() {
let button = this.$('.gh-cardmenu-button'); // the ⊕ button.
let $editor = $(this.get('containerSelector'));
this.set('isOpen', true);
@ -140,24 +140,28 @@ export default Component.extend({
});
this.sendAction('menuIsOpen');
},
closeMenu: function () { // eslint-disable-line
closeMenu() {
this.set('isButton', false);
this.$('.gh-cardmenu').fadeOut('fast', () => {
this.set('isOpen', false);
});
this.sendAction('menuIsClosed');
},
closeMenuKeepButton: function () { // eslint-disable-line
closeMenuKeepButton() {
this.set('isOpen', false);
},
selectTool: function () { // eslint-disable-line
selectTool() {
let {section} = this.get('range');
let editor = this.get('editor');
editor.range = Range.create(section, 0, section, 0);
this.get('selectedTool').onClick(editor);
this.send('closeMenuKeepButton');
},
moveSelectionLeft: function () { // eslint-disable-line
moveSelectionLeft() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item > 0) {
@ -166,7 +170,8 @@ export default Component.extend({
this.set('selected', length - 1);
}
},
moveSelectionUp: function () { // eslint-disable-line
moveSelectionUp() {
let item = this.get('selected');
if (item > ROW_LENGTH) {
this.set('selected', item - ROW_LENGTH);
@ -174,7 +179,8 @@ export default Component.extend({
this.set('selected', 0);
}
},
moveSelectionRight: function () { // eslint-disable-line
moveSelectionRight() {
let item = this.get('selected');
let length = this.get('toolsLength');
if (item < length) {
@ -183,7 +189,8 @@ export default Component.extend({
this.set('selected', 0);
}
},
moveSelectionDown: function () { // eslint-disable-line
moveSelectionDown() {
let item = this.get('selected');
if (item < 0) {
item = 0;

View File

@ -17,6 +17,7 @@ export default Component.extend({
query: '',
range: null,
editor: null,
toolbar: computed('query', 'range', 'selected', function () {
let tools = [];
let match = (this.query || '').trim().toLowerCase();
@ -56,16 +57,13 @@ export default Component.extend({
return tools;
}),
init() {
this._super(...arguments);
let editor = this.get('editor');
this.set('tools', new Tools(editor, this));
},
willDestroy() {
},
didRender() {
let editor = this.get('editor');
let self = this;
@ -80,6 +78,7 @@ export default Component.extend({
}
});
},
cursorChange() {
let editor = this.get('editor');
let range = this.get('range');
@ -103,8 +102,9 @@ export default Component.extend({
}
}
},
actions: {
openMenu: function () { // eslint-disable-line
openMenu() {
let holder = $(this.get('containerSelector'));
let editor = this.get('editor');
let self = this;
@ -225,7 +225,8 @@ export default Component.extend({
this.sendAction('menuIsOpen');
},
closeMenu: function () { // eslint-disable-line
closeMenu() {
let editor = this.get('editor');
// this.get('editor').unregisterKeyCommand('slash'); -- waiting for the next release for this
@ -241,11 +242,12 @@ export default Component.extend({
});
this.sendAction('menuIsClosed');
},
clickedMenu: function () { // eslint-disable-line
// let{section, startOffset, endOffset} = this.get('range');
window.editor.range.head.offset = this.get('range').startOffset - 1;
window.editor.deleteRange(window.editor.range);
clickedMenu() {
let editor = this.get('editor');
// let{section, startOffset, endOffset} = this.get('range');
editor.range.head.offset = this.get('range').startOffset - 1;
editor.deleteRange(editor.range);
this.send('closeMenu');
}
}

View File

@ -1,110 +1,25 @@
import Component from 'ember-component';
import computed from 'ember-computed';
import run from 'ember-runloop';
import $ from 'jquery';
import layout from '../templates/components/koenig-title-input';
export default Component.extend({
layout,
val: '',
_cachedVal: '',
_mutationObserver: null,
tagName: 'h2',
editor: null,
autofocus: false,
koenigEditor: computed('editor', {
get() {
return this.get('editor');
},
set(key, value) {
this.set('editor', value);
}
}),
_cachedVal: '',
_mutationObserver: null,
editorKeyDownListener: null,
didRender() {
let editor = this.get('editor');
_hasSetupEventListeners: false,
let title = this.$('.gh-editor-title');
if (!this.get('val')) {
title.addClass('no-content');
} else if (this.get('val') !== this.get('_cachedVal')) {
title.html(this.get('val'));
}
didInsertElement() {
this._super(...arguments);
if (!editor) {
return;
}
if (this.get('editorKeyDownListener')) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
this.set('editorKeyDownListener', this.editorKeyDown.bind(this));
editor.element.addEventListener('keydown', this.get('editorKeyDownListener'));
title[0].onkeydown = (event) => {
// block the browser format keys.
if (event.ctrlKey || event.metaKey) {
switch (event.keyCode) {
case 66: // B
case 98: // b
case 73: // I
case 105: // i
case 85: // U
case 117: // u
return false;
}
}
if (event.keyCode === 13) {
// enter
// on enter create a new paragraph at the top of the editor, this is because the first item may be a card.
editor.run((postEditor) => {
let marker = editor.builder.createMarker('');
let newSection = editor.builder.createMarkupSection('p', [marker]);
postEditor.insertSectionBefore(editor.post.sections, newSection, editor.post.sections.head);
let range = newSection.toRange();
range.tail.offset = 0; // colapse range
postEditor.setRange(range);
});
return false;
}
// down key
// if we're within ten pixels of the bottom of this element then we try and figure out where to position
// the cursor in the editor.
if (event.keyCode === 40) {
if (!window.getSelection().rangeCount) {
return;
}
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
// in safari getBoundingClientRect on a range does not work if the range is collapsed.
if (cursorPositionOnScreen.bottom === 0) {
cursorPositionOnScreen = range.getClientRects()[0];
}
let offset = title.offset();
let bottomOfHeading = offset.top + title.height();
if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
let editor = this.get('editor');
let loc = editor.element.getBoundingClientRect();
// if the first element is a card then that is always going to be selected.
if (editor.post.sections.head && editor.post.sections.head.isCardSection) {
run.next(() => {
window.getSelection().removeAllRanges();
$(editor.post.sections.head.renderNode.element).children('div').click();
});
return;
}
let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
if (!cursorPositionInEditor || cursorPositionInEditor.isBlank) {
editor.element.focus();
} else {
editor.selectRange(cursorPositionInEditor.toRange());
}
return false;
}
}
};
let title = this.$('.kg-title-input');
// setup mutation observer
let mutationObserver = new MutationObserver(() => {
@ -140,14 +55,108 @@ export default Component.extend({
mutationObserver.observe(title[0], {childList: true, characterData: true, subtree: true});
this.set('_mutationObserver', mutationObserver);
},
didReceiveAttrs() {
if (this.get('editorHasRendered') && !this._hasSetupEventListeners) {
let editor = this.get('editor');
let title = this.$('.kg-title-input');
if (this.get('editorKeyDownListener')) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
this.set('editorKeyDownListener', this.editorKeyDown.bind(this));
editor.element.addEventListener('keydown', this.get('editorKeyDownListener'));
title[0].onkeydown = (event) => {
// block the browser format keys.
if (event.ctrlKey || event.metaKey) {
switch (event.keyCode) {
case 66: // B
case 98: // b
case 73: // I
case 105: // i
case 85: // U
case 117: // u
return false;
}
}
if (event.keyCode === 13) {
// enter
// on enter create a new paragraph at the top of the editor, this is because the first item may be a card.
editor.run((postEditor) => {
let marker = editor.builder.createMarker('');
let newSection = editor.builder.createMarkupSection('p', [marker]);
postEditor.insertSectionBefore(editor.post.sections, newSection, editor.post.sections.head);
let range = newSection.toRange();
range.tail.offset = 0; // colapse range
postEditor.setRange(range);
});
return false;
}
// down key
// if we're within ten pixels of the bottom of this element then we try and figure out where to position
// the cursor in the editor.
if (event.keyCode === 40) {
if (!window.getSelection().rangeCount) {
return;
}
let range = window.getSelection().getRangeAt(0); // get the actual range within the DOM.
let cursorPositionOnScreen = range.getBoundingClientRect();
// in safari getBoundingClientRect on a range does not work if the range is collapsed.
if (cursorPositionOnScreen.bottom === 0) {
cursorPositionOnScreen = range.getClientRects()[0];
}
let offset = title.offset();
let bottomOfHeading = offset.top + title.height();
if (cursorPositionOnScreen.bottom > bottomOfHeading - 13) {
let editor = this.get('editor');
let loc = editor.element.getBoundingClientRect();
// if the first element is a card then that is always going to be selected.
if (editor.post.sections.head && editor.post.sections.head.isCardSection) {
run.next(() => {
window.getSelection().removeAllRanges();
$(editor.post.sections.head.renderNode.element).children('div').click();
});
return;
}
let cursorPositionInEditor = editor.positionAtPoint(cursorPositionOnScreen.left, loc.top);
if (!cursorPositionInEditor || cursorPositionInEditor.isBlank) {
editor.element.focus();
} else {
editor.selectRange(cursorPositionInEditor.toRange());
}
return false;
}
}
};
this._hasSetupEventListeners = true;
}
},
didRender() {
let title = this.$('.kg-title-input');
if (!this.get('val')) {
title.addClass('no-content');
} else if (this.get('val') !== this.get('_cachedVal')) {
title.html(this.get('val'));
}
},
willDestroyElement() {
this.get('_mutationObserver').disconnect();
this.$('.gh-editor-title')[0].onkeydown = null;
this.$('.kg-title-input')[0].onkeydown = null;
let editor = this.get('editor');
if (editor) {
editor.element.removeEventListener('keydown', this.get('editorKeyDownListener'));
}
},
editorKeyDown(event) {
// if the editor has a menu open then we don't want to capture inputs.
if (this.get('editorMenuIsOpen')) {
@ -184,9 +193,10 @@ export default Component.extend({
}
}
},
// gets the character in the last line of the title that best matches the editor
getOffsetAtPosition(horizontalOffset) {
let [title] = this.$('.gh-editor-title')[0].childNodes;
let [title] = this.$('.kg-title-input')[0].childNodes;
if (!title || !title.textContent) {
return 0;
}
@ -201,8 +211,9 @@ export default Component.extend({
continue;
}
if (rect.left <= horizontalOffset && rect.right >= horizontalOffset) {
return i + (horizontalOffset >= (rect.left + rect.right) / 2 ? 1 : 0); // if the horizontalOffset is on the left hand side of the
// character then return `i`, if it's on the right return `i + 1`
// if the horizontalOffset is on the left hand side of the
// character then return `i`, if it's on the right return `i + 1`
return i + (horizontalOffset >= (rect.left + rect.right) / 2 ? 1 : 0);
}
}
@ -214,7 +225,7 @@ export default Component.extend({
// In Chrome it ignores the new range and places the cursor at the start of the element.
// in Firefox it places the cursor at the correct place but refuses to accept keyboard input.
setCursorAtOffset(offset) {
let [title] = this.$('.gh-editor-title');
let [title] = this.$('.kg-title-input');
title.focus();
let selection = window.getSelection();

View File

@ -1,30 +1,48 @@
import Component from 'ember-component';
import computed from 'ember-computed';
import layout from '../templates/components/koenig-toolbar-button';
export default Component.extend({
layout,
tagName: 'button',
classNameBindings: ['selected', 'primary', 'secondary',
'gh-toolbar-btn-bold', 'gh-toolbar-btn-italic', 'gh-toolbar-btn-strike', 'gh-toolbar-btn-link', 'gh-toolbar-btn-h1', 'gh-toolbar-btn-h2', 'gh-toolbar-btn-quote'],
attributeBindings: ['title'],
classNames: ['gh-toolbar-btn'],
attributesBindings: ['title'],
title: 'bold',
// TODO: what do selected/primary/secondary classes relate to? Some tools
// have 'primary' added but none of them appear do anything/be used elsewhere
classNameBindings: [
'selected',
'buttonClass',
'visibilityClass'
],
// exernally set properties
tool: null,
editor: null,
buttonClass: computed('tool.class', function () {
return `gh-toolbar-btn-${this.get('tool.class')}`;
}),
// returns "primary" or null
visibilityClass: computed('tool.visibility', function() {
return this.get('tool.visibility');
}),
title: computed('tool.label', function () {
return this.get('tool.label');
}),
// todo title="Bold", https://github.com/TryGhost/Ghost-Editor/commit/1133a9a7506f409b1b4fae6639c84c94c74dcebf
// actions: {
click() {
this.tool.onClick(this.editor);
this.tool.onClick(this.get('editor'));
},
// },
//
willRender() {
this.set(`gh-toolbar-btn-${this.tool.class}`, true);
if (this.tool.selected) {
this.set('selected', true);
} else {
this.set('selected', false);
}
willRender() {
// TODO: "selected" doesn't appear to do anything for toolbar items -
// it's only used within card menus
this.set('selected', !!this.tool.selected);
// sets the primary/secondary/
if (this.tool.visibility) {
this.set(this.tool.visibility, true);
}

View File

@ -21,6 +21,7 @@ export default Component.extend({
this.iconURL = `${this.get('assetPath')}/tools/`;
},
didRender() {
let $this = this.$();
let editor = this.get('editor');
@ -32,7 +33,6 @@ export default Component.extend({
}
editor.cursorDidChange(() => {
// if there is no cursor:
if (!editor.range || !editor.range.head.section || !editor.range.head.section.isBlank
|| editor.range.head.section.renderNode._element.tagName.toLowerCase() !== 'p') {
@ -69,7 +69,6 @@ export default Component.extend({
});
this.propertyDidChange('toolbar');
});
}
});

View File

@ -10,11 +10,40 @@ import {getPositionFromRange} from '../lib/utils';
export default Component.extend({
layout,
classNames: ['gh-toolbar'],
classNameBindings: ['isVisible', 'isLink', 'tickFullLeft', 'tickHalfLeft', 'tickFullRight', 'tickHalfRight', 'tickAbove', 'isTouch'],
isVisible: false,
tools: [],
// if any of the associated properties are true these class names will
// be dasherized and added to the element
classNameBindings: [
'isVisible',
'isLink',
'tickFullLeft',
'tickHalfLeft',
'tickFullRight',
'tickHalfRight',
'tickAbove',
'isTouch'
],
// externally set properties
editor: null,
assetPath: null,
containerSelector: null,
isTouch: null,
// internal properties
hasRendered: false,
activeTags: null,
tools: [],
isVisible: false,
tickFullLeft: false,
tickFullRight: false,
tickHalfLeft: false,
tickHalfRight: false,
tickAbove: false,
_isLink: false,
// TODO: why is this not just a property?
isLink: computed({
get() {
return this._isLink;
@ -190,6 +219,7 @@ export default Component.extend({
event.stopPropagation();
}
},
doLink(range) {
// if a link is already selected then we remove the links from within the range.
let currentLinks = this.get('activeTags').filter((element) => element.tagName === 'a');
@ -210,9 +240,9 @@ export default Component.extend({
}
);
},
closeLink() {
this.set('isLink', false);
}
}
});

View File

@ -26,7 +26,7 @@ export default function createCardFactory(toolbar) {
// setupUI({env, options, payload});
// todo setup non ember UI
// TODO: setup non ember UI
let payload = copy(_payload);
payload.card_name = env.name;

View File

@ -0,0 +1,18 @@
// Key commands will run any time a particular key or key combination is pressed
// https://github.com/bustlelabs/mobiledoc-kit#configuring-hot-keys
export default function (editor) {
let softReturnKeyCommand = {
str: 'SHIFT+ENTER',
run(editor) {
editor.run((postEditor) => {
let softReturn = postEditor.builder.createAtom('soft-return');
postEditor.insertMarkers(editor.range.head, [softReturn]);
});
}
};
editor.registerKeyCommand(softReturnKeyCommand);
}

View File

@ -1,12 +1,20 @@
/* eslint-disable ember-suave/prefer-destructuring, no-unused-vars */
import {replaceWithListSection, replaceWithHeaderSection} from 'mobiledoc-kit/editor/text-input-handlers';
import {
replaceWithHeaderSection,
replaceWithListSection
} from 'mobiledoc-kit/editor/text-input-handlers';
// Text expansions watch text entry events and will look for matches, replacing
// the matches with additional markup, atoms, or cards
// https://github.com/bustlelabs/mobiledoc-kit#responding-to-text-input
export default function (editor) {
// We don't want to run all our content rules on every text entry event, instead we check to see if this text entry
// event could match a content rule, and only then run the rules.
// Right now we only want to match content ending with *, _, ), ~, and `. This could increase as we support more
// markdown.
// We don't want to run all our content rules on every text entry event,
// instead we check to see if this text entry event could match a content
// rule, and only then run the rules. Right now we only want to match
// content ending with *, _, ), ~, and `. This could increase as we support
// more markdown.
editor.onTextInput({
name: 'inline_markdown',
@ -54,19 +62,6 @@ export default function (editor) {
}
});
// soft return
let softReturnKeyCommand = {
str: 'SHIFT+ENTER',
run(editor) {
editor.run((postEditor) => {
let mention = postEditor.builder.createAtom('soft-return');
postEditor.insertMarkers(editor.range.head, [mention]);
});
}
};
editor.registerKeyCommand(softReturnKeyCommand);
// inline matches
function matchStrongStar(editor, text) {
let {range} = editor;

View File

@ -1,4 +1,3 @@
{{#if url}}
<img src="{{url}}" />
{{else if file}}

View File

@ -1,5 +1,12 @@
{{#if isEditing}}
<textarea onfocus={{action "selectCard"}} ondrop={{action "didDrop"}} ondragover={{action "didDragOver"}} ondragleave={{action "didDragLeave"}}>{{value}}</textarea>
<textarea
onfocus={{action "selectCard"}}
ondrop={{action "didDrop"}}
ondragover={{action "didDragOver"}}
ondragleave={{action "didDragLeave"}}
>
{{value}}
</textarea>
{{else}}
{{{preview}}}
{{/if}}
{{/if}}

View File

@ -1,50 +1,57 @@
{{yield (hash
editor=editor
isMenuOpen=isMenuOpen
hasRendered=editorHasRendered
)}}
<div class='gh-koenig'>
<div class='gh-koenig-surface'
tabindex="{{tabindex}}"
ondrop={{action "dropImage"}}
ondragover={{action "dragOver"}} />
</div>
{{#each emberCards as |card index|}}
{{#ember-wormhole to=card.id}}
{{koenig-card
tabindex=index
card=card
apiRoot=apiRoot
assetPath=assetPath
selectCard=(action "selectCard")
selectCardHard=(action "selectCardHard")
deselectCard=(action "deselectCard")
edit=(action "editCard")
stopEdit=(action "stopEditingCard")
{{koenig-card
tabindex=index
card=card
apiRoot=apiRoot
assetPath=assetPath
selectCard=(action "selectCard")
selectCardHard=(action "selectCardHard")
deselectCard=(action "deselectCard")
edit=(action "editCard")
stopEdit=(action "stopEditingCard")
editedCard=editedCard
resize=resizeEvent
deleteCard=(action "deleteCard")
}}
{{/ember-wormhole}}
{{/each}}
<div class='gh-koenig'>
<div class='surface' tabindex="{{tabindex}}" ondrop={{action "dropImage"}} ondragover={{action "dragOver"}} />
</div>
{{yield}}
{{!-- Popup formatting toolbar when text is selected --}}
{{koenig-toolbar
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
}}
{{koenig-toolbar
editor=editor
assetPath=assetPath
containerSelector=containerSelector
{{koenig-slash-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
menuIsOpen=(action "menuOpened")
menuIsClosed=(action "menuClosed")
isTouch=isTouch
resize=resizeEvent
}}
{{koenig-slash-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
{{koenig-plus-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
menuIsOpen=(action "menuOpened")
menuIsClosed=(action "menuClosed")
isTouch=isTouch
menuIsOpen=(action "menuIsOpen")
menuIsClosed=(action "menuIsClosed")
resize=resizeEvent
}}
{{koenig-plus-menu
editor=editor
assetPath=assetPath
containerSelector=containerSelector
isTouch=isTouch
menuIsOpen=(action "menuIsOpen")
menuIsClosed=(action "menuIsClosed")
isTouch=isTouch
resize=resizeEvent
}}

View File

@ -28,7 +28,9 @@
Edit
</button>
{{/if}}
<button class='kg-card-button kg-card-delete' {{action "delete"}}>{{inline-svg "trash"}}</button>
<button class='kg-card-button kg-card-delete' {{action "delete"}}>
{{inline-svg "trash"}}
</button>
{{/if}}
</div>
</div>

View File

@ -5,7 +5,11 @@
<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))
{{gh-input query
class="gh-input gh-cardmenu-search-input"
placeholder="Search for a card..."
type="text"
update=(action (mut query))
keyEvents=(hash
27=(action "closeMenu")
13=(action "selectTool")

View File

@ -4,4 +4,4 @@
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "clickedMenu")}}
{{/each}}
</div>
{{/if}}
{{/if}}

View File

@ -1 +1 @@
<div contenteditable="true" data-placeholder="Your Post Title" class="gh-editor-title" tabindex={{tabindex}}></div>
<div contenteditable="true" data-placeholder="Your Post Title" class="kg-title-input" tabindex={{tabindex}}></div>

View File

@ -1,12 +1,21 @@
{{#if isLink}}
{{input keyDown=(action "linkKeyDown") keyPress=(action "linkKeyPress") autofocus=true placeholder="Enter a link"}}
{{input
placeholder="Enter a link"
keyDown=(action "linkKeyDown")
keyPress=(action "linkKeyPress")
autofocus=true
}}
<button class="gh-toolbar-btn" {{action 'closeLink'}}>x</button>
{{else}}
{{#each toolbar as |tool|}}
{{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}}
{{koenig-toolbar-button
tool=tool
editor=editor}}
{{/each}}
<div class="gh-toolbar-divider" role="presentation"></div>
{{#each toolbarBlocks as |tool|}}
{{koenig-toolbar-button tool=tool editor=editor iconURL=iconURL assetPath=assetPath}}
{{koenig-toolbar-button
tool=tool
editor=editor}}
{{/each}}
{{/if}}
{{/if}}

View File

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

View File

@ -0,0 +1,20 @@
module.exports = {
env: {
'embertest': true,
'mocha': true
},
globals: {
server: false,
expect: false,
fileUpload: false,
// ember-power-select test helpers
selectChoose: false,
selectSearch: false,
removeMultipleOption: false,
clearSelected: false,
// ember-power-datepicker test helpers
datepickerSelect: false
}
};

View File

@ -0,0 +1,91 @@
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {
findEditor,
focusEditor,
inputText,
waitForRender,
EMPTY_DOC
} from '../../helpers/editor-helpers';
import $ from 'jquery';
describe('gh-koenig: Integration: Component: gh-koenig-slashmenu', function () {
setupComponentTest('gh-koenig-slashmenu', {
integration: true
});
beforeEach(function () {
this.set('value', EMPTY_DOC);
});
it('shows menu when / is typed', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
});
it('filters tools when a user types', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
await inputText(editor, ' bul');
expect(cardMenu.children().length).to.equal(1);
});
it('inserts card/markup when clicked');
it('inserts card/markup when enter is pressed');
it.skip('ul tool', async function () {
this.set('editorMenuIsOpen', function () {});
this.set('editorMenuIsClosed', function () {});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
menuIsOpen=editorMenuIsOpen
menuIsClosed=editorMenuIsClosed
}}`);
let editor = findEditor();
await focusEditor();
await inputText(editor, '/');
await waitForRender('.gh-cardmenu');
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
await inputText(editor, ' bul');
expect(cardMenu.children().length).to.equal(1);
await click('.gh-cardmenu-card');
// TODO: check inner HTML
});
});

View File

@ -0,0 +1,309 @@
/* jshint expr:true */
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {
testEditorInput,
testEditorInputTimeout,
EMPTY_DOC
} from '../../helpers/editor-helpers';
import sinon from 'sinon';
describe('gh-koenig: Integration: Component: gh-koenig', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', EMPTY_DOC);
});
it('fires change and word-count events', async function () {
// set defaults
this.set('onFirstChange', sinon.spy());
this.set('onChange', sinon.spy());
this.set('wordcount', 0);
this.set('actions.wordcountDidChange', function (wordcount) {
this.set('wordcount', wordcount);
});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
onChange=(action onChange)
onFirstChange=(action onFirstChange)
wordcountDidChange=(action 'wordcountDidChange')
}}`);
await testEditorInput('abcd efg hijk lmnop', '<p>abcd efg hijk lmnop</p>', expect);
expect(this.get('onFirstChange').calledOnce, 'onFirstChanged called once').to.be.true;
expect(this.get('onChange').called, 'onChange called').to.be.true;
expect(this.get('wordcount'), 'wordcount').to.equal(4);
});
describe('Markerable markdown support.', function () {
it('plain text inputs (placebo)', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('abcdef', '<p>abcdef</p>', expect);
});
// bold
it('** bolds at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('**test**', '<p><strong>test</strong></p>', expect);
});
it('** bolds in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123**test**', '<p>123<strong>test</strong></p>', expect);
});
it('__ bolds at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('__test__', '<p><strong>test</strong></p>', expect);
});
it('__ bolds in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('__test__', '<p><strong>test</strong></p>', expect);
});
// italic
it('* italicises at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('*test*', '<p><em>test</em></p>', expect);
});
it('* italicises in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123*test*', '<p>123<em>test</em></p>', expect);
});
it('_ italicises at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('_test_', '<p><em>test</em></p>', expect);
});
it('_ italicises in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123_test_', '<p>123<em>test</em></p>', expect);
});
// strikethrough
it('~~ strikethroughs at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('~~test~~', '<p><s>test</s></p>', expect);
});
it('~~ strikethroughs in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('123~~test~~', '<p>123<s>test</s></p>', expect);
});
// links
it('[]() creates a link at start of line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput(
'[ghost](https://www.ghost.org/)',
'<p><a href="https://www.ghost.org/">ghost</a></p>',
expect);
});
it('[]() creates a link in a line', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput(
'123[ghost](https://www.ghost.org/)',
'<p>123<a href="https://www.ghost.org/">ghost</a></p>',
expect);
});
});
describe('Block markdown support', function () {
// headings
it('# creates an H1', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('# ', '<h1><br></h1>', expect);
});
it('## creates an H2', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('## ', '<h2><br></h2>', expect);
});
it('### creates an H3', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('### ', '<h3><br></h3>', expect);
});
// lists
it('* creates an UL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('* ', '<ul><li><br></li></ul>', expect);
});
it('- creates an UL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('- ', '<ul><li><br></li></ul>', expect);
});
it('1. creates an OL', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('1. ', '<ol><li><br></li></ol>', expect);
});
// quote
it('> creates an blockquote', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
await testEditorInput('> ', '<blockquote><br></blockquote>', expect);
});
});
describe('Card markdown support.', function () {
it('![]() creates an image card', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let value = await testEditorInputTimeout('![image of something](https://unsplash.it/200/300/?random)');
expect(value).to.have.string('kg-card-image');
});
it('``` creates a markdown card.', async function () {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.gh-koenig-container'
mobiledoc=value
}}`);
let value = await testEditorInputTimeout('```some code```');
expect(value).to.have.string('kg-card-markdown');
});
});
});

View File

@ -1,21 +0,0 @@
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import {editorShim} from '../../utils';
describe.skip('Unit: Component: koenig-menu', function () {
setupComponentTest('koenig-menu', {
unit: true
});
it('renders', function () {
let component = this.subject();
component.editor = editorShim;
expect(component._state).to.equal('preRender');
// renders the component on the page
this.render();
expect(component._state).to.equal('inDOM');
});
});

View File

@ -3,8 +3,7 @@ import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
describe.skip('Unit: Component: koenig-toolbar-button', function () {
describe('gh-koenig: Unit: Component: koenig-toolbar-button', function () {
setupComponentTest('koenig-toolbar-button', {
unit: true
});

View File

@ -4,8 +4,7 @@ import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import {editorShim} from '../../utils';
describe.skip('Unit: Component: koenig-toolbar-newitem', function () {
describe('gh-koenig: Unit: Component: koenig-toolbar-newitem', function () {
setupComponentTest('koenig-toolbar-newitem', {
unit: true,
needs: [
@ -14,7 +13,6 @@ describe.skip('Unit: Component: koenig-toolbar-newitem', function () {
});
it('renders', function () {
let component = this.subject();
component.editor = editorShim;
expect(component._state).to.equal('preRender');

View File

@ -1,22 +0,0 @@
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import sinon from 'sinon';
describe.skip('Unit: Component: koenig-toolbar', function () {
setupComponentTest('koenig-toolbar', {
unit: true
});
it('The toolbar is not rendered by default.', function () {
let component = this.subject();
expect(component.isVisible).to.be.false;
});
it('The toolbar contains tools.', function () {
let component = this.subject();
expect(component.get('toolbar').length).to.be.greaterThan(0); // the standard toolbar tools (strong, em, strikethrough, link)
expect(component.get('toolbarBlocks').length).to.be.greaterThan(0); // extended toolbar block bases tools (h1, h2, quote);
});
});

View File

@ -2,22 +2,23 @@
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import sinon from 'sinon';
describe.skip('Unit: Component: koenig-toolbar', function () {
describe('gh-koenig: Unit: Component: koenig-toolbar', function () {
setupComponentTest('koenig-toolbar', {
unit: true
});
it('The toolbar is not visible by default.', function () {
it('is not visible by default', function () {
let component = this.subject();
expect(component.isVisible).to.be.false;
});
it('The toolbar contains tools.', function () {
it('contains tools', function () {
let component = this.subject();
expect(component.get('toolbar').length).to.be.greaterThan(0); // the standard toolbar tools (strong, em, strikethrough, link)
expect(component.get('toolbarBlocks').length).to.be.greaterThan(0); // extended toolbar block bases tools (h1, h2, quote);
// the standard toolbar tools (strong, em, strikethrough, link)
expect(component.get('toolbar').length).to.be.greaterThan(0);
// extended toolbar block bases tools (h1, h2, quote);
expect(component.get('toolbarBlocks').length).to.be.greaterThan(0);
});
// it('The toolbar appears when a range is selected.', function () {

View File

@ -1,15 +1,15 @@
export let editorShim = {
range: {
head: {
section: {
export let editorShim = {
range: {
head: {
section: {
renderNode: {
_element: {
tagName: 'P'
}
},
isBlank: false
}
}
}
}
},
cursorDidChange: function() {}
cursorDidChange() {}
};

View File

@ -328,7 +328,7 @@ describe('Acceptance: Editor', function() {
titleRendered();
let title = find('#gh-editor-title div');
let title = find('#koenig-title-input div');
title.html(Array(160).join('a'));
await click(testSelector('publishmenu-trigger'));
@ -356,7 +356,7 @@ describe('Acceptance: Editor', function() {
titleRendered();
let title = find('#gh-editor-title div');
let title = find('#koenig-title-input div');
expect(title.data('placeholder')).to.equal('Your Post Title');
expect(title.hasClass('no-content')).to.be.false;
await title.html('');
@ -378,7 +378,7 @@ describe('Acceptance: Editor', function() {
titleRendered();
let title = find('#gh-editor-title div');
let title = find('#koenig-title-input div');
await replaceTitleHTML('<div>TITLE&nbsp;&#09;&nbsp;&thinsp;&ensp;&emsp;TEST</div>&nbsp;');
expect(title.html()).to.equal('TITLE TEST ');
});

View File

@ -2,27 +2,50 @@ import Ember from 'ember';
import $ from 'jquery';
import run from 'ember-runloop';
import wait from 'ember-test-helpers/wait';
import {findWithAssert} from 'ember-native-dom-helpers';
import {find, findWithAssert, waitUntil} from 'ember-native-dom-helpers';
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
import {TESTING_EXPANDO_PROPERTY} from 'gh-koenig/components/gh-koenig';
// polls the editor until it's started.
export function editorRendered() {
return Ember.Test.promise(function (resolve) { // eslint-disable-line
function checkEditor() {
if (window.editor) {
return resolve();
} else {
window.requestAnimationFrame(checkEditor);
}
export const EMPTY_DOC = {
version: MOBILEDOC_VERSION,
markups: [],
atoms: [],
cards: [],
sections: []
};
// traverse up the node tree looking for an editor instance
export function findEditor(element) {
if (!element) {
// TODO: get the selector from the editor component
element = findWithAssert('.gh-koenig-container');
}
if (typeof element === 'string') {
element = findWithAssert(element);
}
do {
if (element[TESTING_EXPANDO_PROPERTY]) {
return element[TESTING_EXPANDO_PROPERTY];
}
checkEditor();
});
element = element.parentNode;
} while (!!element); // eslint-disable-line
throw new Error('Unable to find gh-koenig editor from element');
}
export function focusEditor(element) {
let editor = findEditor(element);
run(() => editor.element.focus());
return (window.wait || wait);
}
// polls the title until it's started.
export function titleRendered() {
return Ember.Test.promise(function (resolve) { // eslint-disable-line
function checkTitle() {
let title = $('#gh-editor-title div');
let title = $('#koenig-title-input div');
if (title[0]) {
return resolve();
} else {
@ -36,21 +59,24 @@ export function titleRendered() {
// replaces the title text content with HTML and returns once the HTML has been placed.
// takes into account converting to plaintext.
export function replaceTitleHTML(HTML) {
let el = findWithAssert('#gh-editor-title div');
let el = findWithAssert('#koenig-title-input div');
run(() => el.innerHTML = HTML);
return (window.wait || wait)();
}
// simulates text inputs into the editor, unfortunately the helper Ember helper functions
// simulates text inputs into the editor, unfortunately the Ember helper functions
// don't work on content editable so we have to manipuate the text input event manager
// in mobiledoc-kit directly. This is a private API.
export function inputText(editor, text) {
editor._eventManager._textInputHandler.handle(text);
run(() => {
editor._eventManager._textInputHandler.handle(text);
});
}
// inputs text and waits for the editor to modify the dom with the desired result or timesout.
export function testInput(input, output, expect) {
window.editor.element.focus(); // for some reason the editor doesn't work until it's focused when run in ghost-admin.
export function testEditorInput(input, output, expect) {
let editor = findEditor();
editor.element.focus(); // for some reason the editor doesn't work until it's focused when run in ghost-admin.
return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line
let lastRender = '';
let isRejected = false;
@ -59,53 +85,32 @@ export function testInput(input, output, expect) {
reject(lastRender);
isRejected = true;
}, 500);
window.editor.didRender(() => {
lastRender = window.editor.element.innerHTML;
if (window.editor.element.innerHTML === output && !isRejected) {
editor.didRender(() => {
lastRender = editor.element.innerHTML;
if (editor.element.innerHTML === output && !isRejected) {
window.clearTimeout(rejectTimeout);
expect(lastRender).to.equal(output); // we know this is true but include it for the output.
return resolve(lastRender);
}
});
inputText(window.editor, input);
inputText(editor, input);
});
}
export function testInputTimeout(input) {
window.editor.element.focus();
export function testEditorInputTimeout(input) {
let editor = findEditor();
editor.element.focus();
return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line
window.setTimeout(() => {
resolve(window.editor.element.innerHTML);
resolve(editor.element.innerHTML);
}, 300);
inputText(window.editor, input);
inputText(editor, input);
});
}
export function waitForRender(selector) {
let isRejected = false;
return Ember.Test.promise(function (resolve, reject) { // eslint-disable-line
let rejectTimeout = window.setTimeout(() => {
reject('element didn\'t render');
isRejected = true;
}, 1500);
function checkIsRendered() {
if ($(selector)[0] && !isRejected) {
window.clearTimeout(rejectTimeout);
return resolve();
} else {
window.requestAnimationFrame(checkIsRendered);
}
}
checkIsRendered();
return waitUntil(() => {
return find(selector);
});
}
export function timeoutPromise(timeout) {
return Ember.Test.promise(function (resolve) { // eslint-disable-line
window.setTimeout(() => {
resolve();
}, timeout);
});
}

View File

@ -1,452 +0,0 @@
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {editorRendered, testInput, testInputTimeout} from '../../helpers/editor-helpers';
describe('Integration: Component: gh-koenig', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', {
version: '0.3.1',
atoms: [],
markups: [],
cards: [],
sections: []});
});
describe('Makerable markdown support.', function() {
it('plain text inputs (placebo)', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('abcdef', '<p>abcdef</p>', expect);
})
.then(() => {
done();
});
});
// bold
it('** bolds at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('**test**', '<p><strong>test</strong></p>', expect);
})
.then(() => {
done();
});
});
it('** bolds in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('123**test**', '<p>123<strong>test</strong></p>', expect);
})
.then(() => {
done();
});
});
it('__ bolds at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('__test__', '<p><strong>test</strong></p>', expect);
})
.then(() => {
done();
});
});
it('__ bolds in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('__test__', '<p><strong>test</strong></p>', expect);
})
.then(() => {
done();
});
});
// italic
it('* italicises at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('*test*', '<p><em>test</em></p>', expect);
})
.then(() => {
done();
});
});
it('* italicises in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('123*test*', '<p>123<em>test</em></p>', expect);
})
.then(() => {
done();
});
});
it('_ italicises at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('_test_', '<p><em>test</em></p>', expect);
})
.then(() => {
done();
});
});
it('_ italicises in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('123_test_', '<p>123<em>test</em></p>', expect);
})
.then(() => {
done();
});
});
// strikethrough
it('~~ strikethroughs at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('~~test~~', '<p><s>test</s></p>', expect);
})
.then(() => {
done();
});
});
it('~~ strikethroughs in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('123~~test~~', '<p>123<s>test</s></p>', expect);
})
.then(() => {
done();
});
});
// links
it('[]() creates a link at start of line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('[ghost](https://www.ghost.org/)', '<p><a href="https://www.ghost.org/">ghost</a></p>', expect);
})
.then(() => {
done();
});
});
it('[]() creates a link in a line', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('123[ghost](https://www.ghost.org/)', '<p>123<a href="https://www.ghost.org/">ghost</a></p>', expect);
})
.then(() => {
done();
});
});
});
describe('Block markdown support', function () {
// headings
it('# creates an H1', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('# ', '<h1><br></h1>', expect);
})
.then(() => {
done();
});
});
it('## creates an H2', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('## ', '<h2><br></h2>', expect);
})
.then(() => {
done();
});
});
it('### creates an H3', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('### ', '<h3><br></h3>', expect);
})
.then(() => {
done();
});
});
// lists
it('* creates an UL', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('* ', '<ul><li><br></li></ul>', expect);
})
.then(() => {
done();
});
});
it('- creates an UL', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('- ', '<ul><li><br></li></ul>', expect);
})
.then(() => {
done();
});
});
it('1. creates an OL', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('1. ', '<ol><li><br></li></ol>', expect);
})
.then(() => {
done();
});
});
// quote
it('> creates an blockquote', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('> ', '<blockquote><br></blockquote>', expect);
})
.then(() => {
done();
});
});
});
describe('Card markdown support.', function () {
it('![]() creates an image card', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInputTimeout('![image of something](https://unsplash.it/200/300/?random)');
})
.then((value) => {
expect(value).to.have.string('kg-card-image');
done();
});
});
it('``` creates a markdown card.', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInputTimeout('```some code```');
})
.then((value) => {
expect(value).to.have.string('kg-card-markdown');
done();
});
});
});
});

View File

@ -1,112 +0,0 @@
/* jshint expr:true */
import {expect} from 'chai';
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {editorRendered, waitForRender, inputText, timeoutPromise} from '../../helpers/editor-helpers';
import $ from 'jquery';
describe('Integration: Component: gh-koenig-slashmenu', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
this.set('value', {
version: '0.3.1',
atoms: [],
markups: [],
cards: [],
sections: []});
});
it('the slash menu appears on user input', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
inputText(editor, '/');
return waitForRender('.gh-cardmenu');
})
.then(() => {
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
done();
});
});
it('searches when a user types', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
inputText(editor, '/');
return waitForRender('.gh-cardmenu');
})
.then(() => {
let {editor} = window;
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
inputText(editor, ' bul');
return timeoutPromise(500);
})
.then(() => {
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(1);
done();
});
});
it.skip('ul tool', function (done) {
this.set('editorMenuIsOpen', function () {});
this.set('editorMenuIsClosed', function () {});
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
menuIsOpen=editorMenuIsOpen
menuIsClosed=editorMenuIsClosed
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
inputText(editor, '/');
return waitForRender('.gh-cardmenu');
})
.then(() => {
let {editor} = window;
let cardMenu = $('.gh-cardmenu');
expect(cardMenu.children().length).to.equal(7);
inputText(editor, ' bul');
return timeoutPromise(500);
})
.then(() => {
$('.gh-cardmenu-card').click();
done();
});
// .then(() => {
//
// })
// .then(() => {
// console.log(editor.element.innerHTML);
// done();
// });
});
});

View File

@ -1,56 +0,0 @@
/* jshint expr:true */
import {describe, it} from 'mocha';
import {setupComponentTest} from 'ember-mocha';
import hbs from 'htmlbars-inline-precompile';
import {editorRendered, testInput} from '../../helpers/editor-helpers';
import sinon from 'sinon';
describe.skip('Integration: Component: gh-koenig - General Editor Tests.', function () {
setupComponentTest('gh-koenig', {
integration: true
});
beforeEach(function () {
// set defaults
this.set('onFirstChange', sinon.spy());
this.set('onChange', sinon.spy());
this.set('wordcount', 0);
this.set('actions.wordcountDidChange', function (wordcount) {
this.set('wordcount', wordcount);
});
this.set('value', {
version: '0.3.1',
atoms: [],
markups: [],
cards: [],
sections: []});
});
it('Check that events have fired', function (done) {
this.render(hbs`{{gh-koenig
apiRoot='/todo'
assetPath='/assets'
containerSelector='.editor-holder'
value=value
onChange=(action onChange)
onFirstChange=(action onFirstChange)
wordcountDidChange=(action 'wordcountDidChange')
}}`);
editorRendered()
.then(() => {
let {editor} = window;
editor.element.focus();
return testInput('abcd efg hijk lmnop', '<p>abcd efg hijk lmnop</p>', expect);
})
.then(() => {
expect(this.get('onFirstChange').calledOnce).to.be.true;
expect(this.get('onChange').calledOnce).to.be.true;
expect(this.get('wordcount')).to.equal(4);
done();
});
});
});