✨ Koenig - Added rich-text support to captions
refs https://github.com/TryGhost/Ghost/issues/9724 - added `{{koenig-basic-html-input}}` component - uses a stripped down version of Koenig - supports all inline formatting that Koenig supports - supports inline text expansions - supports inline key commands - limited to a single paragraph - serialises and deserialises from HTML rather than mobiledoc - updated `{{koenig-caption-input}}` to use `{{koenig-basic-html-input}}` - updated image and embed cards to calculate word counts correctly for html captions - bumped Spirit dependency to fix styling of toolbars within the editor canvas - fixed positioning in toolbar components to account for `parentElement` not necessarily being the closest element to position against
This commit is contained in:
parent
9836c91b19
commit
09743cfb2b
|
@ -0,0 +1,365 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import Editor from 'mobiledoc-kit/editor/editor';
|
||||||
|
import layout from '../templates/components/koenig-basic-html-input';
|
||||||
|
import parserPlugins from '../options/basic-html-parser-plugins';
|
||||||
|
import registerKeyCommands, {BASIC_KEY_COMMANDS} from '../options/key-commands';
|
||||||
|
import {MOBILEDOC_VERSION} from 'mobiledoc-kit/renderers/mobiledoc';
|
||||||
|
import {arrayToMap, toggleSpecialFormatEditState} from './koenig-editor';
|
||||||
|
import {assign} from '@ember/polyfills';
|
||||||
|
import {cleanBasicHtml} from '../helpers/clean-basic-html';
|
||||||
|
import {computed} from '@ember/object';
|
||||||
|
import {getLinkMarkupFromRange} from '../utils/markup-utils';
|
||||||
|
import {registerBasicTextExpansions} from '../options/text-expansions';
|
||||||
|
import {run} from '@ember/runloop';
|
||||||
|
|
||||||
|
const UNDO_DEPTH = 50;
|
||||||
|
|
||||||
|
// blank doc contains a single empty paragraph so that there's some content for
|
||||||
|
// the cursor to start in
|
||||||
|
const BLANK_DOC = {
|
||||||
|
version: MOBILEDOC_VERSION,
|
||||||
|
markups: [],
|
||||||
|
atoms: [],
|
||||||
|
cards: [],
|
||||||
|
sections: [
|
||||||
|
[1, 'p', [
|
||||||
|
[0, [], 0, '']
|
||||||
|
]]
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
layout,
|
||||||
|
|
||||||
|
// public attrs
|
||||||
|
autofocus: false,
|
||||||
|
html: null,
|
||||||
|
placeholder: '',
|
||||||
|
spellcheck: true,
|
||||||
|
|
||||||
|
// internal properties
|
||||||
|
activeMarkupTagNames: null,
|
||||||
|
editor: null,
|
||||||
|
linkRange: null,
|
||||||
|
mobiledoc: null,
|
||||||
|
selectedRange: null,
|
||||||
|
|
||||||
|
// private properties
|
||||||
|
_hasFocus: false,
|
||||||
|
_lastMobiledoc: null,
|
||||||
|
_startedRunLoop: false,
|
||||||
|
|
||||||
|
// closure actions
|
||||||
|
willCreateEditor() {},
|
||||||
|
didCreateEditor() {},
|
||||||
|
onChange() {},
|
||||||
|
onNewline() {},
|
||||||
|
onFocus() {},
|
||||||
|
onBlur() {},
|
||||||
|
|
||||||
|
/* computed properties -------------------------------------------------- */
|
||||||
|
|
||||||
|
cleanHTML: computed('html', function () {
|
||||||
|
return cleanBasicHtml(this.html);
|
||||||
|
}),
|
||||||
|
|
||||||
|
// merge in named options with any passed in `options` property data-bag
|
||||||
|
editorOptions: computed('cleanHTML', function () {
|
||||||
|
let options = this.options || {};
|
||||||
|
let atoms = this.atoms || [];
|
||||||
|
let cards = this.cards || [];
|
||||||
|
|
||||||
|
return assign({
|
||||||
|
html: `<p>${this.cleanHTML || ''}</p>`,
|
||||||
|
placeholder: this.placeholder,
|
||||||
|
spellcheck: this.spellcheck,
|
||||||
|
autofocus: this.autofocus,
|
||||||
|
cards,
|
||||||
|
atoms,
|
||||||
|
unknownCardHandler() {},
|
||||||
|
unknownAtomHandler() {}
|
||||||
|
}, options);
|
||||||
|
}),
|
||||||
|
|
||||||
|
/* lifecycle hooks ------------------------------------------------------ */
|
||||||
|
|
||||||
|
didReceiveAttrs() {
|
||||||
|
this._super(...arguments);
|
||||||
|
|
||||||
|
// reset local mobiledoc if html has been changed upstream so that
|
||||||
|
// the html will be re-parsed by the mobiledoc-kit editor
|
||||||
|
if (this.cleanHTML !== this._getHTML()) {
|
||||||
|
this.set('mobiledoc', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willRender() {
|
||||||
|
let mobiledoc = this.mobiledoc;
|
||||||
|
|
||||||
|
if (!mobiledoc && !this.cleanHTML) {
|
||||||
|
mobiledoc = BLANK_DOC;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mobiledocIsSame =
|
||||||
|
(this._lastMobiledoc && this._lastMobiledoc === mobiledoc);
|
||||||
|
let isEditingDisabledIsSame =
|
||||||
|
this._lastIsEditingDisabled === this.isEditingDisabled;
|
||||||
|
|
||||||
|
// no change to mobiledoc, no need to recreate the editor
|
||||||
|
if (mobiledocIsSame && isEditingDisabledIsSame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// update our internal references
|
||||||
|
this._lastIsEditingDisabled = this.isEditingDisabled;
|
||||||
|
|
||||||
|
// trigger the willCreateEditor closure action
|
||||||
|
this.willCreateEditor();
|
||||||
|
|
||||||
|
// teardown any old editor that might be around
|
||||||
|
let editor = this.editor;
|
||||||
|
if (editor) {
|
||||||
|
editor.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
// create a new editor
|
||||||
|
let editorOptions = this.editorOptions;
|
||||||
|
editorOptions.mobiledoc = mobiledoc;
|
||||||
|
editorOptions.showLinkTooltips = false;
|
||||||
|
editorOptions.undoDepth = UNDO_DEPTH;
|
||||||
|
editorOptions.parserPlugins = parserPlugins;
|
||||||
|
|
||||||
|
editor = new Editor(editorOptions);
|
||||||
|
|
||||||
|
registerKeyCommands(editor, this, BASIC_KEY_COMMANDS);
|
||||||
|
registerBasicTextExpansions(editor);
|
||||||
|
|
||||||
|
// set up editor hooks
|
||||||
|
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 runloop in `editor.willRender`,
|
||||||
|
// we must explicitly end it here
|
||||||
|
if (this._startedRunLoop) {
|
||||||
|
this._startedRunLoop = false;
|
||||||
|
run.end();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.willHandleNewline((event) => {
|
||||||
|
run.join(() => {
|
||||||
|
this.willHandleNewline(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.didUpdatePost((postEditor) => {
|
||||||
|
run.join(() => {
|
||||||
|
this.didUpdatePost(postEditor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.postDidChange(() => {
|
||||||
|
run.join(() => {
|
||||||
|
this.postDidChange(editor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.cursorDidChange(() => {
|
||||||
|
run.join(() => {
|
||||||
|
this.cursorDidChange(editor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.inputModeDidChange(() => {
|
||||||
|
if (this.isDestroyed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
run.join(() => {
|
||||||
|
this.inputModeDidChange(editor);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.isEditingDisabled) {
|
||||||
|
editor.disableEditing();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update mobiledoc reference to match initial editor state from parsed
|
||||||
|
// html. We use this value to compare on re-renders in case we need to
|
||||||
|
// re-parse from html
|
||||||
|
this.mobiledoc = editor.serialize();
|
||||||
|
this._lastMobiledoc = this.mobiledoc;
|
||||||
|
|
||||||
|
this.set('editor', editor);
|
||||||
|
this.didCreateEditor(editor);
|
||||||
|
},
|
||||||
|
|
||||||
|
// our ember component has rendered, now we need to render the mobiledoc
|
||||||
|
// editor itself if necessary
|
||||||
|
didRender() {
|
||||||
|
this._super(...arguments);
|
||||||
|
let {editor} = this;
|
||||||
|
if (!editor.hasRendered) {
|
||||||
|
let editorElement = this.element.querySelector('[data-kg="editor"]');
|
||||||
|
this._isRenderingEditor = true;
|
||||||
|
editor.render(editorElement);
|
||||||
|
this._isRenderingEditor = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
willDestroyElement() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.editor.destroy();
|
||||||
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
toggleMarkup(markupTagName, postEditor) {
|
||||||
|
(postEditor || this.editor).toggleMarkup(markupTagName);
|
||||||
|
},
|
||||||
|
|
||||||
|
// range should be set to the full extent of the selection or the
|
||||||
|
// appropriate <a> markup. If there's a selection when the link edit
|
||||||
|
// component renders it will re-select when finished which should
|
||||||
|
// trigger the normal toolbar
|
||||||
|
editLink(range) {
|
||||||
|
let linkMarkup = getLinkMarkupFromRange(range);
|
||||||
|
if ((!range.isCollapsed || linkMarkup) && range.headSection.isMarkerable) {
|
||||||
|
this.set('linkRange', range);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelEditLink() {
|
||||||
|
this.set('linkRange', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* ember event handlers --------------------------------------------------*/
|
||||||
|
|
||||||
|
// handle focusin/focusout at the component level so that we don't trigger blur
|
||||||
|
// actions when clicking on toolbar buttons
|
||||||
|
focusIn(event) {
|
||||||
|
if (!this._hasFocus) {
|
||||||
|
this._hasFocus = true;
|
||||||
|
run.scheduleOnce('actions', this, this.onFocus, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
focusOut(event) {
|
||||||
|
if (!event.relatedTarget || !this.element.contains(event.relatedTarget)) {
|
||||||
|
this._hasFocus = false;
|
||||||
|
run.scheduleOnce('actions', this, this.onBlur, event);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* mobiledoc event handlers ----------------------------------------------*/
|
||||||
|
|
||||||
|
willHandleNewline(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
this.onNewline();
|
||||||
|
},
|
||||||
|
|
||||||
|
// manipulate mobiledoc content before committing changes
|
||||||
|
// - only one section
|
||||||
|
// - first section must be a markerable section
|
||||||
|
// - if first section is a list, grab the content of the first list item
|
||||||
|
didUpdatePost(postEditor) {
|
||||||
|
let {builder, editor, editor: {post}} = postEditor;
|
||||||
|
|
||||||
|
// remove any non-markerable sections
|
||||||
|
post.sections.forEach((section) => {
|
||||||
|
if (!section.isMarkerable && !section.isListSection) {
|
||||||
|
let reposition = section === editor.activeSection;
|
||||||
|
postEditor.removeSection(section);
|
||||||
|
if (reposition) {
|
||||||
|
postEditor.setRange(post.sections.head.tailPosition());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// strip all sections other than the first
|
||||||
|
if (post.sections.length > 1) {
|
||||||
|
while (post.sections.length > 1) {
|
||||||
|
postEditor.removeSection(post.sections.tail);
|
||||||
|
}
|
||||||
|
postEditor.setRange(post.sections.head.tailPosition());
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert list section to a paragraph section
|
||||||
|
if (post.sections.head.isListSection) {
|
||||||
|
let list = post.sections.head;
|
||||||
|
let listItem = list.items.head;
|
||||||
|
let newMarkers = listItem.markers.map(m => m.clone());
|
||||||
|
let p = builder.createMarkupSection('p', newMarkers);
|
||||||
|
postEditor.replaceSection(list, p);
|
||||||
|
postEditor.setRange(post.sections.head.tailPosition());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
postDidChange() {
|
||||||
|
// trigger closure action
|
||||||
|
this.onChange(this._getHTML());
|
||||||
|
},
|
||||||
|
|
||||||
|
cursorDidChange(editor) {
|
||||||
|
// if we have `code` or ~strike~ formatting to the left but not the right
|
||||||
|
// then toggle the formatting - these formats should only be creatable
|
||||||
|
// through the text expansions
|
||||||
|
toggleSpecialFormatEditState(editor);
|
||||||
|
|
||||||
|
// pass the selected range through to the toolbar + menu components
|
||||||
|
this.set('selectedRange', editor.range);
|
||||||
|
},
|
||||||
|
|
||||||
|
// fired when the active section(s) or markup(s) at the current cursor
|
||||||
|
// position or selection have changed. We use this event to update the
|
||||||
|
// activeMarkup/section tag lists which control button states in our popup
|
||||||
|
// toolbar
|
||||||
|
inputModeDidChange(editor) {
|
||||||
|
let markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));
|
||||||
|
|
||||||
|
// On keyboard cursor movement our `cursorDidChange` toggle for special
|
||||||
|
// formats happens before mobiledoc's readstate updates the edit states
|
||||||
|
// so we have to re-do it here
|
||||||
|
// TODO: can we make the event order consistent in mobiledoc-kit?
|
||||||
|
toggleSpecialFormatEditState(editor);
|
||||||
|
|
||||||
|
// Avoid updating this component's properties synchronously while
|
||||||
|
// rendering the editor (after rendering the component) because it
|
||||||
|
// causes Ember to display deprecation warnings
|
||||||
|
if (this._isRenderingEditor) {
|
||||||
|
run.schedule('afterRender', () => {
|
||||||
|
this.set('activeMarkupTagNames', markupTags);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.set('activeMarkupTagNames', markupTags);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/* private methods -------------------------------------------------------*/
|
||||||
|
|
||||||
|
// rather than parsing mobiledoc to HTML we can grab the HTML directly from
|
||||||
|
// inside the editor element because we should only be dealing with
|
||||||
|
// inline markup that directly maps to HTML elements
|
||||||
|
_getHTML() {
|
||||||
|
if (this.editor && this.editor.element) {
|
||||||
|
let firstParagraph = this.editor.element.querySelector('p');
|
||||||
|
|
||||||
|
if (!firstParagraph) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = firstParagraph.innerHTML;
|
||||||
|
return cleanBasicHtml(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -22,7 +22,7 @@ export default Component.extend({
|
||||||
moveCursorToPrevSection() {},
|
moveCursorToPrevSection() {},
|
||||||
|
|
||||||
figCaptionClass: computed(function () {
|
figCaptionClass: computed(function () {
|
||||||
return `${kgStyle(['figcaption'])} w-100`;
|
return `${kgStyle(['figcaption'])} w-100 relative`;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
didReceiveAttrs() {
|
didReceiveAttrs() {
|
||||||
|
@ -42,21 +42,43 @@ export default Component.extend({
|
||||||
this._detachHandlers();
|
this._detachHandlers();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
actions: {
|
||||||
|
registerEditor(editor) {
|
||||||
|
let commands = {
|
||||||
|
ENTER: run.bind(this, this._enter),
|
||||||
|
ESC: run.bind(this, this._escape),
|
||||||
|
UP: run.bind(this, this._upOrLeft),
|
||||||
|
LEFT: run.bind(this, this._upOrLeft),
|
||||||
|
DOWN: run.bind(this, this._rightOrDown),
|
||||||
|
RIGHT: run.bind(this, this._rightOrDown)
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.keys(commands).forEach((str) => {
|
||||||
|
editor.registerKeyCommand({
|
||||||
|
str,
|
||||||
|
run() {
|
||||||
|
return commands[str](editor, str);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editor = editor;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleEnter() {
|
||||||
|
this.addParagraphAfterCard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
_attachHandlers() {
|
_attachHandlers() {
|
||||||
if (!this._keypressHandler) {
|
if (!this._keypressHandler) {
|
||||||
this._keypressHandler = run.bind(this, this._handleKeypress);
|
this._keypressHandler = run.bind(this, this._handleKeypress);
|
||||||
window.addEventListener('keypress', this._keypressHandler);
|
window.addEventListener('keypress', this._keypressHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this._keydownHandler) {
|
|
||||||
this._keydownHandler = run.bind(this, this._handleKeydown);
|
|
||||||
window.addEventListener('keydown', this._keydownHandler);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_detachHandlers() {
|
_detachHandlers() {
|
||||||
window.removeEventListener('keypress', this._keypressHandler);
|
window.removeEventListener('keypress', this._keypressHandler);
|
||||||
window.removeEventListener('keydown', this._keydownHandler);
|
|
||||||
this._keypressHandler = null;
|
this._keypressHandler = null;
|
||||||
this._keydownHandler = null;
|
this._keydownHandler = null;
|
||||||
},
|
},
|
||||||
|
@ -64,51 +86,58 @@ export default Component.extend({
|
||||||
// only fires if the card is selected, moves focus to the caption input so
|
// only fires if the card is selected, moves focus to the caption input so
|
||||||
// that it's possible to start typing without explicitly focusing the input
|
// that it's possible to start typing without explicitly focusing the input
|
||||||
_handleKeypress(event) {
|
_handleKeypress(event) {
|
||||||
let captionInput = this.element.querySelector('[name="caption"]');
|
|
||||||
let key = new Key(event);
|
let key = new Key(event);
|
||||||
|
let {editor} = this;
|
||||||
|
|
||||||
|
if (event.target.matches('[data-kg="editor"]') && editor && !editor._hasFocus() && key.isPrintableKey()) {
|
||||||
|
editor.focus();
|
||||||
|
editor.run((postEditor) => {
|
||||||
|
postEditor.insertText(editor.post.tailPosition(), event.key);
|
||||||
|
});
|
||||||
|
|
||||||
if (captionInput && captionInput !== document.activeElement && key.isPrintableKey()) {
|
|
||||||
captionInput.value = `${captionInput.value}${event.key}`;
|
|
||||||
captionInput.focus();
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// this will be fired for keydown events when the caption input is focused,
|
/* key commands ----------------------------------------------------------*/
|
||||||
// we look for cursor movements or the enter key to defocus and trigger the
|
|
||||||
// corresponding editor behaviour
|
|
||||||
_handleKeydown(event) {
|
|
||||||
let captionInput = this.element.querySelector('[name="caption"]');
|
|
||||||
|
|
||||||
if (event.target === captionInput) {
|
_enter() {
|
||||||
if (event.key === 'Escape') {
|
this.send('handleEnter');
|
||||||
captionInput.blur();
|
},
|
||||||
return;
|
|
||||||
|
_escape(editor) {
|
||||||
|
editor.blur();
|
||||||
|
},
|
||||||
|
|
||||||
|
_upOrLeft(editor, key) {
|
||||||
|
let {isCollapsed, head} = editor.range;
|
||||||
|
|
||||||
|
if (isCollapsed && head.isEqual(head.section.headPosition())) {
|
||||||
|
return this.moveCursorToPrevSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === 'Enter') {
|
// we're simulating a text input so up/down move the cursor to the
|
||||||
captionInput.blur();
|
// beginning/end of the input
|
||||||
this.addParagraphAfterCard();
|
if (isCollapsed && key === 'UP') {
|
||||||
event.preventDefault();
|
return editor.selectRange(head.section.headPosition().toRange());
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectionStart = captionInput.selectionStart;
|
return false;
|
||||||
let length = captionInput.value.length;
|
},
|
||||||
|
|
||||||
if ((event.key === 'ArrowUp' || event.key === 'ArrowLeft') && selectionStart === 0) {
|
_rightOrDown(editor, key) {
|
||||||
captionInput.blur();
|
let {isCollapsed, tail} = editor.range;
|
||||||
this.moveCursorToPrevSection();
|
|
||||||
event.preventDefault();
|
if (isCollapsed && tail.isEqual(tail.section.tailPosition())) {
|
||||||
return;
|
return this.moveCursorToNextSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((event.key === 'ArrowDown' || event.key === 'ArrowRight') && selectionStart === length) {
|
// we're simulating a text input so up/down move the cursor to the
|
||||||
captionInput.blur();
|
// beginning/end of the input
|
||||||
this.moveCursorToNextSection();
|
if (isCollapsed && key === 'DOWN') {
|
||||||
event.preventDefault();
|
return editor.selectRange(tail.section.tailPosition().toRange());
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import countWords from '../utils/count-words';
|
import countWords, {stripTags} from '../utils/count-words';
|
||||||
import layout from '../templates/components/koenig-card-embed';
|
import layout from '../templates/components/koenig-card-embed';
|
||||||
import noframe from 'noframe.js';
|
import noframe from 'noframe.js';
|
||||||
import {NO_CURSOR_MOVEMENT} from './koenig-editor';
|
import {NO_CURSOR_MOVEMENT} from './koenig-editor';
|
||||||
|
@ -38,7 +38,7 @@ export default Component.extend({
|
||||||
counts: computed('payload.{html,caption}', function () {
|
counts: computed('payload.{html,caption}', function () {
|
||||||
return {
|
return {
|
||||||
imageCount: this.payload.html ? 1 : 0,
|
imageCount: this.payload.html ? 1 : 0,
|
||||||
wordCount: countWords(this.payload.caption)
|
wordCount: countWords(stripTags(this.payload.caption))
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import countWords from '../utils/count-words';
|
import countWords, {stripTags} from '../utils/count-words';
|
||||||
import layout from '../templates/components/koenig-card-image';
|
import layout from '../templates/components/koenig-card-image';
|
||||||
import {
|
import {
|
||||||
IMAGE_EXTENSIONS,
|
IMAGE_EXTENSIONS,
|
||||||
|
@ -46,7 +46,7 @@ export default Component.extend({
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.payload.caption) {
|
if (this.payload.caption) {
|
||||||
wordCount += countWords(this.payload.caption);
|
wordCount += countWords(stripTags(this.payload.caption));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {wordCount, imageCount};
|
return {wordCount, imageCount};
|
||||||
|
|
|
@ -76,7 +76,7 @@ export const SPECIAL_MARKUPS = {
|
||||||
SUB: '~'
|
SUB: '~'
|
||||||
};
|
};
|
||||||
|
|
||||||
function arrayToMap(array) {
|
export function arrayToMap(array) {
|
||||||
let map = Object.create(null);
|
let map = Object.create(null);
|
||||||
array.forEach((key) => {
|
array.forEach((key) => {
|
||||||
if (key) { // skip undefined/falsy key values
|
if (key) { // skip undefined/falsy key values
|
||||||
|
@ -91,7 +91,7 @@ function arrayToMap(array) {
|
||||||
// toggled via markdown expansions then we want to ensure that the markup is
|
// toggled via markdown expansions then we want to ensure that the markup is
|
||||||
// removed from the edit state so that you can type without being stuck with
|
// removed from the edit state so that you can type without being stuck with
|
||||||
// the special formatting
|
// the special formatting
|
||||||
function toggleSpecialFormatEditState(editor) {
|
export function toggleSpecialFormatEditState(editor) {
|
||||||
let {head, isCollapsed} = editor.range;
|
let {head, isCollapsed} = editor.range;
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
Object.keys(SPECIAL_MARKUPS).forEach((tagName) => {
|
Object.keys(SPECIAL_MARKUPS).forEach((tagName) => {
|
||||||
|
|
|
@ -193,7 +193,7 @@ export default Component.extend({
|
||||||
|
|
||||||
// TODO: largely shared with {{koenig-toolbar}} code - extract to a shared util?
|
// TODO: largely shared with {{koenig-toolbar}} code - extract to a shared util?
|
||||||
_positionToolbar() {
|
_positionToolbar() {
|
||||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
let containerRect = this.element.offsetParent.getBoundingClientRect();
|
||||||
let rangeRect = this._windowRange.getBoundingClientRect();
|
let rangeRect = this._windowRange.getBoundingClientRect();
|
||||||
let {width, height} = this.element.getBoundingClientRect();
|
let {width, height} = this.element.getBoundingClientRect();
|
||||||
let newPosition = {};
|
let newPosition = {};
|
||||||
|
|
|
@ -90,6 +90,7 @@ export default Component.extend({
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
|
||||||
let container = this.container;
|
let container = this.container;
|
||||||
|
container.dataset.kgHasLinkToolbar = true;
|
||||||
this._addEventListener(container, 'mouseover', this._handleMouseover);
|
this._addEventListener(container, 'mouseover', this._handleMouseover);
|
||||||
this._addEventListener(container, 'mouseout', this._handleMouseout);
|
this._addEventListener(container, 'mouseout', this._handleMouseout);
|
||||||
},
|
},
|
||||||
|
@ -142,7 +143,7 @@ export default Component.extend({
|
||||||
_handleMouseover(event) {
|
_handleMouseover(event) {
|
||||||
if (this._canShowToolbar) {
|
if (this._canShowToolbar) {
|
||||||
let target = getEventTargetMatchingTag('a', event.target, this.container);
|
let target = getEventTargetMatchingTag('a', event.target, this.container);
|
||||||
if (target && target.isContentEditable) {
|
if (target && target.isContentEditable && target.closest('[data-kg-has-link-toolbar=true]') === this.container) {
|
||||||
this._timeout = run.later(this, function () {
|
this._timeout = run.later(this, function () {
|
||||||
this._showToolbar(target);
|
this._showToolbar(target);
|
||||||
}, DELAY);
|
}, DELAY);
|
||||||
|
@ -178,7 +179,7 @@ export default Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
_positionToolbar(target) {
|
_positionToolbar(target) {
|
||||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
let containerRect = this.element.offsetParent.getBoundingClientRect();
|
||||||
let targetRect = target.getBoundingClientRect();
|
let targetRect = target.getBoundingClientRect();
|
||||||
let {width, height} = this.element.getBoundingClientRect();
|
let {width, height} = this.element.getBoundingClientRect();
|
||||||
let newPosition = {};
|
let newPosition = {};
|
||||||
|
|
|
@ -26,6 +26,7 @@ export default Component.extend({
|
||||||
classNames: ['absolute', 'z-999'],
|
classNames: ['absolute', 'z-999'],
|
||||||
|
|
||||||
// public attrs
|
// public attrs
|
||||||
|
basicOnly: false,
|
||||||
editor: null,
|
editor: null,
|
||||||
editorRange: null,
|
editorRange: null,
|
||||||
activeMarkupTagNames: null,
|
activeMarkupTagNames: null,
|
||||||
|
@ -235,7 +236,7 @@ export default Component.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
_positionToolbar() {
|
_positionToolbar() {
|
||||||
let containerRect = this.element.parentNode.getBoundingClientRect();
|
let containerRect = this.element.offsetParent.getBoundingClientRect();
|
||||||
let range = window.getSelection().getRangeAt(0);
|
let range = window.getSelection().getRangeAt(0);
|
||||||
let rangeRect = range.getBoundingClientRect();
|
let rangeRect = range.getBoundingClientRect();
|
||||||
let {width, height} = this.element.getBoundingClientRect();
|
let {width, height} = this.element.getBoundingClientRect();
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
import {helper} from '@ember/component/helper';
|
||||||
|
import {isArray} from '@ember/array';
|
||||||
|
|
||||||
|
export function cleanBasicHtml(html = '') {
|
||||||
|
if (isArray(html)) {
|
||||||
|
html = html[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
let cleanHtml = html
|
||||||
|
.replace(/<br>/g, ' ')
|
||||||
|
.replace(/(\s| ){2,}/g, ' ')
|
||||||
|
.trim()
|
||||||
|
.replace(/^ | $/g, '')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
// remove any elements that have a blank textContent
|
||||||
|
if (cleanHtml) {
|
||||||
|
let doc = new DOMParser().parseFromString(cleanHtml, 'text/html');
|
||||||
|
|
||||||
|
doc.body.querySelectorAll('*').forEach((element) => {
|
||||||
|
if (!element.textContent.trim()) {
|
||||||
|
element.remove();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cleanHtml = doc.body.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleanHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default helper(cleanBasicHtml);
|
|
@ -0,0 +1,12 @@
|
||||||
|
export function removeBR(node, builder, {addMarkerable, nodeFinished}) {
|
||||||
|
if (node.nodeType !== 1 || node.tagName !== 'BR') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addMarkerable(builder.createMarker(' '));
|
||||||
|
nodeFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
export default [
|
||||||
|
removeBR
|
||||||
|
];
|
|
@ -406,8 +406,21 @@ export const DEFAULT_KEY_COMMANDS = [{
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
|
|
||||||
export default function registerKeyCommands(editor, koenig) {
|
// key commands that are used in koenig-basic-html-input
|
||||||
DEFAULT_KEY_COMMANDS.forEach((keyCommand) => {
|
export const BASIC_KEY_COMMANDS = DEFAULT_KEY_COMMANDS.filter((command) => {
|
||||||
|
let basicCommands = [
|
||||||
|
'BACKSPACE',
|
||||||
|
'CTRL+K',
|
||||||
|
'META+K',
|
||||||
|
'CTRL+ALT+U',
|
||||||
|
'CTRL+SHIFT+K',
|
||||||
|
'META+SHIFT+K'
|
||||||
|
];
|
||||||
|
return basicCommands.includes(command.str);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default function registerKeyCommands(editor, koenig, commands = DEFAULT_KEY_COMMANDS) {
|
||||||
|
commands.forEach((keyCommand) => {
|
||||||
editor.registerKeyCommand({
|
editor.registerKeyCommand({
|
||||||
str: keyCommand.str,
|
str: keyCommand.str,
|
||||||
run() {
|
run() {
|
||||||
|
|
|
@ -53,119 +53,7 @@ export function replaceWithListSection(editor, matches, listTagName) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function (editor, koenig) {
|
function registerInlineMarkdownTextExpansions(editor) {
|
||||||
/* block level markdown ------------------------------------------------- */
|
|
||||||
|
|
||||||
editor.unregisterTextInputHandler('heading');
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_heading',
|
|
||||||
match: /^(#{1,6}) /,
|
|
||||||
run(editor, matches) {
|
|
||||||
let hashes = matches[1];
|
|
||||||
let headingTag = `h${hashes.length}`;
|
|
||||||
let {range} = editor;
|
|
||||||
let text = range.head.section.textUntil(range.head);
|
|
||||||
|
|
||||||
// we don't want to convert to a heading if the user has not just
|
|
||||||
// finished typing the markdown (eg, they've made a previous
|
|
||||||
// heading expansion then Cmd-Z'ed it to get the text back then
|
|
||||||
// starts typing at the end of the heading)
|
|
||||||
if (text !== matches[0]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
editor.run((postEditor) => {
|
|
||||||
range = range.extend(-(matches[0].length));
|
|
||||||
let position = postEditor.deleteRange(range);
|
|
||||||
postEditor.setRange(position);
|
|
||||||
|
|
||||||
// toggleHeaderSection will remove all formatting except links
|
|
||||||
koenig.send('toggleHeaderSection', headingTag, postEditor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.unregisterTextInputHandler('ul');
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_ul',
|
|
||||||
match: /^\* |^- /,
|
|
||||||
run(editor, matches) {
|
|
||||||
replaceWithListSection(editor, matches, 'ul');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.unregisterTextInputHandler('ol');
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_ol',
|
|
||||||
match: /^1\.? /,
|
|
||||||
run(editor, matches) {
|
|
||||||
replaceWithListSection(editor, matches, 'ol');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_blockquote',
|
|
||||||
match: /^> /,
|
|
||||||
run(editor, matches) {
|
|
||||||
let {range} = editor;
|
|
||||||
let {head, head: {section}} = range;
|
|
||||||
let text = section.textUntil(head);
|
|
||||||
|
|
||||||
// ensure cursor is at the end of the matched text so we don't
|
|
||||||
// convert text the users wants to start with `> ` and that we're
|
|
||||||
// not already on a blockquote section
|
|
||||||
if (text === matches[0] && section.tagName !== 'blockquote') {
|
|
||||||
editor.run((postEditor) => {
|
|
||||||
range = range.extend(-(matches[0].length));
|
|
||||||
let position = postEditor.deleteRange(range);
|
|
||||||
postEditor.setRange(position);
|
|
||||||
|
|
||||||
koenig.send('toggleSection', 'blockquote', postEditor);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_hr',
|
|
||||||
match: /^---$/,
|
|
||||||
run(editor) {
|
|
||||||
let {range: {head, head: {section}}} = editor;
|
|
||||||
|
|
||||||
// Skip if cursor is not at end of section
|
|
||||||
if (!head.isTail()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if section is a list item
|
|
||||||
if (section.isListItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
koenig.send('replaceWithCardSection', 'hr', section.toRange());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
editor.onTextInput({
|
|
||||||
name: 'md_code',
|
|
||||||
match: /^```$/,
|
|
||||||
run(editor) {
|
|
||||||
let {range: {head, head: {section}}} = editor;
|
|
||||||
|
|
||||||
// Skip if cursor is not at end of section
|
|
||||||
if (!head.isTail()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip if section is a list item
|
|
||||||
if (section.isListItem) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
koenig.send('replaceWithCardSection', 'code', section.toRange());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
/* inline markdown ------------------------------------------------------ */
|
/* inline markdown ------------------------------------------------------ */
|
||||||
|
|
||||||
// --\s = en dash –
|
// --\s = en dash –
|
||||||
|
@ -487,3 +375,130 @@ export default function (editor, koenig) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default function (editor, koenig) {
|
||||||
|
/* inline markdown -------------------------------------------------------*/
|
||||||
|
registerInlineMarkdownTextExpansions(editor);
|
||||||
|
|
||||||
|
/* block level markdown ------------------------------------------------- */
|
||||||
|
|
||||||
|
editor.unregisterTextInputHandler('heading');
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_heading',
|
||||||
|
match: /^(#{1,6}) /,
|
||||||
|
run(editor, matches) {
|
||||||
|
let hashes = matches[1];
|
||||||
|
let headingTag = `h${hashes.length}`;
|
||||||
|
let {range} = editor;
|
||||||
|
let text = range.head.section.textUntil(range.head);
|
||||||
|
|
||||||
|
// we don't want to convert to a heading if the user has not just
|
||||||
|
// finished typing the markdown (eg, they've made a previous
|
||||||
|
// heading expansion then Cmd-Z'ed it to get the text back then
|
||||||
|
// starts typing at the end of the heading)
|
||||||
|
if (text !== matches[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.run((postEditor) => {
|
||||||
|
range = range.extend(-(matches[0].length));
|
||||||
|
let position = postEditor.deleteRange(range);
|
||||||
|
postEditor.setRange(position);
|
||||||
|
|
||||||
|
// toggleHeaderSection will remove all formatting except links
|
||||||
|
koenig.send('toggleHeaderSection', headingTag, postEditor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.unregisterTextInputHandler('ul');
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_ul',
|
||||||
|
match: /^\* |^- /,
|
||||||
|
run(editor, matches) {
|
||||||
|
replaceWithListSection(editor, matches, 'ul');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.unregisterTextInputHandler('ol');
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_ol',
|
||||||
|
match: /^1\.? /,
|
||||||
|
run(editor, matches) {
|
||||||
|
replaceWithListSection(editor, matches, 'ol');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_blockquote',
|
||||||
|
match: /^> /,
|
||||||
|
run(editor, matches) {
|
||||||
|
let {range} = editor;
|
||||||
|
let {head, head: {section}} = range;
|
||||||
|
let text = section.textUntil(head);
|
||||||
|
|
||||||
|
// ensure cursor is at the end of the matched text so we don't
|
||||||
|
// convert text the users wants to start with `> ` and that we're
|
||||||
|
// not already on a blockquote section
|
||||||
|
if (text === matches[0] && section.tagName !== 'blockquote') {
|
||||||
|
editor.run((postEditor) => {
|
||||||
|
range = range.extend(-(matches[0].length));
|
||||||
|
let position = postEditor.deleteRange(range);
|
||||||
|
postEditor.setRange(position);
|
||||||
|
|
||||||
|
koenig.send('toggleSection', 'blockquote', postEditor);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_hr',
|
||||||
|
match: /^---$/,
|
||||||
|
run(editor) {
|
||||||
|
let {range: {head, head: {section}}} = editor;
|
||||||
|
|
||||||
|
// Skip if cursor is not at end of section
|
||||||
|
if (!head.isTail()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if section is a list item
|
||||||
|
if (section.isListItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
koenig.send('replaceWithCardSection', 'hr', section.toRange());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
editor.onTextInput({
|
||||||
|
name: 'md_code',
|
||||||
|
match: /^```$/,
|
||||||
|
run(editor) {
|
||||||
|
let {range: {head, head: {section}}} = editor;
|
||||||
|
|
||||||
|
// Skip if cursor is not at end of section
|
||||||
|
if (!head.isTail()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip if section is a list item
|
||||||
|
if (section.isListItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
koenig.send('replaceWithCardSection', 'code', section.toRange());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reduce duplication
|
||||||
|
export function registerBasicTextExpansions(editor) {
|
||||||
|
// unregister mobiledoc-kit's block-level text handlers
|
||||||
|
editor.unregisterTextInputHandler('heading');
|
||||||
|
editor.unregisterTextInputHandler('ul');
|
||||||
|
editor.unregisterTextInputHandler('ol');
|
||||||
|
|
||||||
|
registerInlineMarkdownTextExpansions(editor);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
<div class="koenig-basic-html-input__editor-wrappper">
|
||||||
|
<div
|
||||||
|
class="koenig-basic-html-input__editor"
|
||||||
|
data-gramm="false"
|
||||||
|
data-kg="editor"
|
||||||
|
data-placeholder={{placeholder}}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{koenig-toolbar
|
||||||
|
basicOnly=true
|
||||||
|
editor=editor
|
||||||
|
editorRange=selectedRange
|
||||||
|
activeMarkupTagNames=activeMarkupTagNames
|
||||||
|
toggleMarkup=(action "toggleMarkup")
|
||||||
|
editLink=(action "editLink")
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{!-- pop-up link hover toolbar --}}
|
||||||
|
{{koenig-link-toolbar
|
||||||
|
editor=editor
|
||||||
|
container=element
|
||||||
|
linkRange=linkRange
|
||||||
|
selectedRange=selectedRange
|
||||||
|
editLink=(action "editLink")
|
||||||
|
}}
|
||||||
|
|
||||||
|
{{!-- pop-up link editing toolbar --}}
|
||||||
|
{{#if linkRange}}
|
||||||
|
{{koenig-link-input
|
||||||
|
editor=editor
|
||||||
|
linkRange=linkRange
|
||||||
|
selectedRange=selectedRange
|
||||||
|
cancel=(action "cancelEditLink")
|
||||||
|
}}
|
||||||
|
{{/if}}
|
|
@ -1,10 +1,11 @@
|
||||||
<input
|
{{koenig-basic-html-input
|
||||||
placeholder={{if isFocused "" placeholder}}
|
html=caption
|
||||||
value={{caption}}
|
placeholder=(if isFocused "" placeholder)
|
||||||
type="text"
|
|
||||||
class="miw-100 tc bn form-text bg-transparent"
|
class="miw-100 tc bn form-text bg-transparent"
|
||||||
name="caption"
|
name="caption"
|
||||||
oninput={{action update value="target.value"}}
|
onChange=(action update)
|
||||||
onfocus={{action (mut isFocused) true}}
|
onFocus=(action (mut isFocused) true)
|
||||||
onblur={{action (mut isFocused) false}}
|
onBlur=(action (mut isFocused) false)
|
||||||
>
|
onNewline=(action "handleEnter")
|
||||||
|
didCreateEditor=(action "registerEditor")
|
||||||
|
}}
|
|
@ -22,7 +22,7 @@
|
||||||
<div class="koenig-card-click-overlay ba b--white" data-kg-overlay></div>
|
<div class="koenig-card-click-overlay ba b--white" data-kg-overlay></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{#if (or isSelected payload.caption)}}
|
{{#if (or isSelected (clean-basic-html payload.caption))}}
|
||||||
{{card.captionInput
|
{{card.captionInput
|
||||||
caption=payload.caption
|
caption=payload.caption
|
||||||
update=(action "updateCaption")
|
update=(action "updateCaption")
|
||||||
|
|
|
@ -64,7 +64,7 @@
|
||||||
</div>
|
</div>
|
||||||
{{/gh-uploader}}
|
{{/gh-uploader}}
|
||||||
|
|
||||||
{{#if (or isSelected payload.caption)}}
|
{{#if (or isSelected (clean-basic-html payload.caption))}}
|
||||||
{{card.captionInput
|
{{card.captionInput
|
||||||
caption=payload.caption
|
caption=payload.caption
|
||||||
update=(action "updateCaption")
|
update=(action "updateCaption")
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
{{svg-jar "koenig/kg-italic" class=(concat (if (or activeMarkupTagNames.isEm activeMarkupTagNames.isI) "fill-blue-l2" "fill-white") " w4 h4")}}
|
{{svg-jar "koenig/kg-italic" class=(concat (if (or activeMarkupTagNames.isEm activeMarkupTagNames.isI) "fill-blue-l2" "fill-white") " w4 h4")}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{{#unless basicOnly}}
|
||||||
<li class="ma0 lh-solid">
|
<li class="ma0 lh-solid">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -26,7 +27,7 @@
|
||||||
class="dib dim-lite link h10 w9 nudge-top--1"
|
class="dib dim-lite link h10 w9 nudge-top--1"
|
||||||
{{action "toggleHeaderSection" "h1"}}
|
{{action "toggleHeaderSection" "h1"}}
|
||||||
>
|
>
|
||||||
{{svg-jar "koenig/kg-heading-1" class=(concat (if activeSectionTagNames.isH1 "fill-blue-l2" "fill-white") " w4 h4")}}
|
{{svg-jar "koenig/kg-heading-1" class=(concat (if activeSectionTagNames.isH1 "fill-blue-l2" "stroke-white") " w4 h4")}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="ma0 lh-solid">
|
<li class="ma0 lh-solid">
|
||||||
|
@ -36,12 +37,14 @@
|
||||||
class="dib dim-lite link h10 w9 nudge-top--1"
|
class="dib dim-lite link h10 w9 nudge-top--1"
|
||||||
{{action "toggleHeaderSection" "h2"}}
|
{{action "toggleHeaderSection" "h2"}}
|
||||||
>
|
>
|
||||||
{{svg-jar "koenig/kg-heading-2" class=(concat (if activeSectionTagNames.isH2 "fill-blue-l2" "fill-white") " w4 h4")}}
|
{{svg-jar "koenig/kg-heading-2" class=(concat (if activeSectionTagNames.isH2 "fill-blue-l2" "stroke-white") " w4 h4")}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{{/unless}}
|
||||||
|
|
||||||
<li class="ma0 lh-solid kg-action-bar-divider bg-darkgrey-l2 h5"></li>
|
<li class="ma0 lh-solid kg-action-bar-divider bg-darkgrey-l2 h5"></li>
|
||||||
|
|
||||||
|
{{#unless basicOnly}}
|
||||||
<li class="ma0 lh-solid">
|
<li class="ma0 lh-solid">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -49,9 +52,10 @@
|
||||||
class="dib dim-lite link h10 w9 nudge-top--1"
|
class="dib dim-lite link h10 w9 nudge-top--1"
|
||||||
{{action "toggleSection" "blockquote"}}
|
{{action "toggleSection" "blockquote"}}
|
||||||
>
|
>
|
||||||
{{svg-jar "koenig/kg-quote" class=(concat (if activeSectionTagNames.isBlockquote "fill-blue-l2" "fill-white") " w4 h4")}}
|
{{svg-jar "koenig/kg-quote" class=(concat (if activeSectionTagNames.isBlockquote "fill-blue-l2" "stroke-white") " w4 h4")}}
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
{{/unless}}
|
||||||
<li class="ma0 lh-solid">
|
<li class="ma0 lh-solid">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
export {default} from 'koenig-editor/components/koenig-basic-html-input';
|
|
@ -0,0 +1 @@
|
||||||
|
export {default, cleanBasicHtml} from 'koenig-editor/helpers/clean-basic-html';
|
|
@ -100,7 +100,7 @@
|
||||||
"eslint": "4.19.1",
|
"eslint": "4.19.1",
|
||||||
"eslint-plugin-ghost": "0.0.25",
|
"eslint-plugin-ghost": "0.0.25",
|
||||||
"fs-extra": "4.0.3",
|
"fs-extra": "4.0.3",
|
||||||
"ghost-spirit": "0.0.30",
|
"ghost-spirit": "0.0.31",
|
||||||
"glob": "7.1.2",
|
"glob": "7.1.2",
|
||||||
"google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost",
|
"google-caja-bower": "https://github.com/acburdine/google-caja-bower#ghost",
|
||||||
"grunt": "1.0.3",
|
"grunt": "1.0.3",
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {setupComponentTest} from 'ember-mocha';
|
||||||
|
|
||||||
|
describe('Integration: Component: koenig-basic-html-input', function () {
|
||||||
|
setupComponentTest('koenig-basic-html-input', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
it.skip('renders', function () {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
// Template block usage:
|
||||||
|
// this.render(hbs`
|
||||||
|
// {{#koenig-basic-html-input}}
|
||||||
|
// template content
|
||||||
|
// {{/koenig-basic-html-input}}
|
||||||
|
// `);
|
||||||
|
|
||||||
|
this.render(hbs`{{koenig-basic-html-input}}`);
|
||||||
|
expect(this.$()).to.have.length(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {expect} from 'chai';
|
||||||
|
import {setupComponentTest} from 'ember-mocha';
|
||||||
|
|
||||||
|
describe('Integration: Helper: clean-basic-html', function () {
|
||||||
|
setupComponentTest('clean-basic-html', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', function () {
|
||||||
|
// Set any properties with this.set('myProperty', 'value');
|
||||||
|
// Handle any actions with this.on('myAction', function(val) { ... });
|
||||||
|
// Template block usage:
|
||||||
|
// this.render(hbs`
|
||||||
|
// {{#clean-basic-html}}
|
||||||
|
// template content
|
||||||
|
// {{/clean-basic-html}}
|
||||||
|
// `);
|
||||||
|
this.set('inputValue', '1234');
|
||||||
|
|
||||||
|
this.render(hbs`{{clean-basic-html inputValue}}`);
|
||||||
|
|
||||||
|
expect(this.$().text().trim()).to.equal('1234');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -5241,9 +5241,9 @@ ghost-ignition@^2.7.0:
|
||||||
prettyjson "^1.1.3"
|
prettyjson "^1.1.3"
|
||||||
uuid "^3.0.0"
|
uuid "^3.0.0"
|
||||||
|
|
||||||
ghost-spirit@0.0.30:
|
ghost-spirit@0.0.31:
|
||||||
version "0.0.30"
|
version "0.0.31"
|
||||||
resolved "https://registry.yarnpkg.com/ghost-spirit/-/ghost-spirit-0.0.30.tgz#73f520363fbb0a4ac63483072bec9f01cc84d3a7"
|
resolved "https://registry.yarnpkg.com/ghost-spirit/-/ghost-spirit-0.0.31.tgz#2a79ffc22d546ba34200417d166496fa51373707"
|
||||||
dependencies:
|
dependencies:
|
||||||
autoprefixer "8.2.0"
|
autoprefixer "8.2.0"
|
||||||
bluebird "^3.4.6"
|
bluebird "^3.4.6"
|
||||||
|
|
Loading…
Reference in New Issue