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:
parent
275ac3cd0f
commit
756b6627a9
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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%;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
import Component from 'ember-component';
|
||||
import layout from '../../templates/components/card-hr';
|
||||
|
||||
export default Component.extend({
|
||||
layout
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
|
@ -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;
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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');
|
||||
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
|
18
ghost/admin/lib/gh-koenig/addon/options/key-commands.js
Normal file
18
ghost/admin/lib/gh-koenig/addon/options/key-commands.js
Normal 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);
|
||||
|
||||
}
|
@ -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;
|
@ -1,4 +1,3 @@
|
||||
|
||||
{{#if url}}
|
||||
<img src="{{url}}" />
|
||||
{{else if 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}}
|
||||
|
@ -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
|
||||
}}
|
@ -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>
|
||||
|
@ -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")
|
||||
|
@ -4,4 +4,4 @@
|
||||
{{koenig-menu-item tool=tool editor=editor range=range selected=tool.selected clicked=(action "clickedMenu")}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
@ -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>
|
@ -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}}
|
||||
|
@ -0,0 +1 @@
|
||||
export {default} from 'gh-koenig/components/koenig-title-input';
|
20
ghost/admin/lib/gh-koenig/test-support/.eslintrc.js
Normal file
20
ghost/admin/lib/gh-koenig/test-support/.eslintrc.js
Normal 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
|
||||
}
|
||||
};
|
@ -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
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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
|
||||
});
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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 () {
|
||||
|
@ -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() {}
|
||||
};
|
||||
|
@ -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 	    TEST</div> ');
|
||||
expect(title.html()).to.equal('TITLE TEST ');
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
}
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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();
|
||||
// });
|
||||
});
|
||||
});
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue
Block a user