mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 05:37:34 +03:00
Added first draft of email-only card
no issue - adds "Email" card to the card menus when developer experiments is turned on - adds `<KoenigTextReplacementHtmlInput>` implementing most of Koenig except: - uses html as it's input/output format - replaces backtick "code" formatting with `{replacement}` - no headings - no cards at all - minimal toolbar but all formatting is still available using markdown text expansions or keyboard shortcuts
This commit is contained in:
parent
07850fd319
commit
a9b9b2f3ae
@ -18,7 +18,8 @@
|
||||
-moz-font-feature-settings: "liga" on;
|
||||
}
|
||||
|
||||
.koenig-editor__editor.__has-no-content:after {
|
||||
.koenig-editor__editor.__has-no-content:after,
|
||||
.koenig-text-replacement-html-input__editor.__has-no-content:after{
|
||||
font-family: georgia,serif;
|
||||
font-weight: 300;
|
||||
letter-spacing: .02rem;
|
||||
@ -863,7 +864,6 @@
|
||||
left: -16px;
|
||||
}
|
||||
|
||||
|
||||
/* Cards
|
||||
/* --------------------------------------------------------------- */
|
||||
.kg-bookmark-card {
|
||||
@ -959,6 +959,10 @@
|
||||
line-height: 1.65em;
|
||||
}
|
||||
|
||||
.kg-email-card p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
|
||||
/* Codemirror overrides
|
||||
/* --------------------------------------------------------------- */
|
||||
|
@ -0,0 +1,82 @@
|
||||
import Browser from 'mobiledoc-kit/utils/browser';
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/koenig-card-email';
|
||||
import {isBlank} from '@ember/utils';
|
||||
import {run} from '@ember/runloop';
|
||||
import {set} from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
|
||||
// attrs
|
||||
payload: null,
|
||||
isSelected: false,
|
||||
isEditing: false,
|
||||
|
||||
// closure actions
|
||||
selectCard() {},
|
||||
deselectCard() {},
|
||||
editCard() {},
|
||||
saveCard() {},
|
||||
deleteCard() {},
|
||||
moveCursorToNextSection() {},
|
||||
moveCursorToPrevSection() {},
|
||||
addParagraphAfterCard() {},
|
||||
registerComponent() {},
|
||||
|
||||
init() {
|
||||
this._super(...arguments);
|
||||
this.registerComponent(this);
|
||||
},
|
||||
|
||||
actions: {
|
||||
updateHtml(html) {
|
||||
console.log('updateHtml', html);
|
||||
this._updatePayloadAttr('html', html);
|
||||
},
|
||||
|
||||
registerEditor(textReplacementEditor) {
|
||||
let commands = {
|
||||
'META+ENTER': run.bind(this, this._enter, 'meta'),
|
||||
'CTRL+ENTER': run.bind(this, this._enter, 'ctrl')
|
||||
};
|
||||
|
||||
Object.keys(commands).forEach((str) => {
|
||||
textReplacementEditor.registerKeyCommand({
|
||||
str,
|
||||
run() {
|
||||
return commands[str](textReplacementEditor, str);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this._textReplacementEditor = textReplacementEditor;
|
||||
},
|
||||
|
||||
leaveEditMode() {
|
||||
if (isBlank(this.payload.html)) {
|
||||
// afterRender is required to avoid double modification of `isSelected`
|
||||
// TODO: see if there's a way to avoid afterRender
|
||||
run.scheduleOnce('afterRender', this, this.deleteCard);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
_updatePayloadAttr(attr, value) {
|
||||
let payload = this.payload;
|
||||
let save = this.saveCard;
|
||||
|
||||
set(payload, attr, value);
|
||||
|
||||
// update the mobiledoc and stay in edit mode
|
||||
save(payload, false);
|
||||
},
|
||||
|
||||
/* key commands ----------------------------------------------------------*/
|
||||
|
||||
_enter(modifier) {
|
||||
if (this.isEditing && (modifier === 'meta' || (modifier === 'crtl' && Browser.isWin()))) {
|
||||
this.editCard();
|
||||
}
|
||||
}
|
||||
});
|
@ -1,9 +1,11 @@
|
||||
import Component from '@ember/component';
|
||||
import layout from '../templates/components/koenig-menu-content';
|
||||
import {inject as service} from '@ember/service';
|
||||
|
||||
export default Component.extend({
|
||||
layout,
|
||||
config: service(),
|
||||
|
||||
layout,
|
||||
tagName: '',
|
||||
|
||||
itemSections: null,
|
||||
|
@ -0,0 +1,405 @@
|
||||
import Component from '@ember/component';
|
||||
import Editor from 'mobiledoc-kit/editor/editor';
|
||||
import cleanTextReplacementHtml from '../lib/clean-text-replacement-html';
|
||||
import defaultAtoms from '../options/atoms';
|
||||
import layout from '../templates/components/koenig-text-replacement-html-input';
|
||||
import registerKeyCommands, {TEXT_REPLACEMENT_KEY_COMMANDS} from '../options/key-commands';
|
||||
import validator from 'validator';
|
||||
import {DRAG_DISABLED_DATA_ATTR} from '../lib/dnd/constants';
|
||||
import {arrayToMap, toggleSpecialFormatEditState} from './koenig-editor';
|
||||
import {assign} from '@ember/polyfills';
|
||||
import {computed} from '@ember/object';
|
||||
import {getContentFromPasteEvent} from 'mobiledoc-kit/utils/parse-utils';
|
||||
import {getLinkMarkupFromRange} from '../utils/markup-utils';
|
||||
import {registerTextReplacementTextExpansions} from '../options/text-expansions';
|
||||
import {run} from '@ember/runloop';
|
||||
|
||||
// TODO: extract core to share functionality between this and `{{koenig-editor}}`
|
||||
|
||||
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: '0.3.1',
|
||||
markups: [],
|
||||
atoms: [],
|
||||
cards: [],
|
||||
sections: [
|
||||
[1, 'p', [
|
||||
[0, [], 0, '']
|
||||
]]
|
||||
]
|
||||
};
|
||||
|
||||
// markups that should not be continued when typing and reverted to their
|
||||
// text expansion style when backspacing over final char of markup
|
||||
export const SPECIAL_MARKUPS = {
|
||||
S: '~~',
|
||||
CODE: '{', // this is different because we use <code> to represent {} replacements
|
||||
SUP: '^',
|
||||
SUB: '~'
|
||||
};
|
||||
|
||||
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() {},
|
||||
onFocus() {},
|
||||
onBlur() {},
|
||||
|
||||
/* computed properties -------------------------------------------------- */
|
||||
|
||||
cleanHTML: computed('html', function () {
|
||||
return cleanTextReplacementHtml(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 || [];
|
||||
|
||||
// add our default atoms and cards, we want the defaults to be first so
|
||||
// that they can be overridden by any passed-in atoms or cards.
|
||||
// Use Array.concat to avoid modifying any passed in array references
|
||||
atoms = defaultAtoms.concat(atoms);
|
||||
|
||||
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 = [];
|
||||
|
||||
editor = new Editor(editorOptions);
|
||||
|
||||
registerKeyCommands(editor, this, TEXT_REPLACEMENT_KEY_COMMANDS);
|
||||
registerTextReplacementTextExpansions(editor, this);
|
||||
|
||||
// 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.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);
|
||||
},
|
||||
|
||||
didInsertElement() {
|
||||
this._super(...arguments);
|
||||
let editorElement = this.element.querySelector('[data-kg="editor"]');
|
||||
|
||||
this._pasteHandler = run.bind(this, this.handlePaste);
|
||||
editorElement.addEventListener('paste', this._pasteHandler);
|
||||
|
||||
this.element.dataset[DRAG_DISABLED_DATA_ATTR] = 'true';
|
||||
},
|
||||
|
||||
// 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);
|
||||
|
||||
let editorElement = this.element.querySelector('[data-kg="editor"]');
|
||||
editorElement.removeEventListener('paste', this._pasteHandler);
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/* custom event handlers ------------------------------------------------ */
|
||||
|
||||
handlePaste(event) {
|
||||
let {editor, editor: {range}} = this;
|
||||
let {text} = getContentFromPasteEvent(event);
|
||||
|
||||
if (!editor.cursor.isAddressable(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text && validator.isURL(text)) {
|
||||
// if we have a text selection, make that selection a link
|
||||
if (range && !range.isCollapsed && range.headSection === range.tailSection && range.headSection.isMarkerable) {
|
||||
let linkMarkup = editor.builder.createMarkup('a', {href: text});
|
||||
editor.run((postEditor) => {
|
||||
postEditor.addMarkupToRange(range, linkMarkup);
|
||||
});
|
||||
editor.selectRange(range.tail);
|
||||
|
||||
// prevent mobiledoc's default paste event handler firing
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/* mobiledoc event handlers ----------------------------------------------*/
|
||||
|
||||
// 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/non-list 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) {
|
||||
return cleanTextReplacementHtml(this.editor.element.innerHTML);
|
||||
}
|
||||
}
|
||||
});
|
@ -0,0 +1,44 @@
|
||||
export default function cleanTextReplacementHtml(html = '', _options = {}) {
|
||||
const defaults = {};
|
||||
const options = Object.assign({}, defaults, _options);
|
||||
|
||||
if (!options.createDocument) {
|
||||
const Parser = (typeof DOMParser !== 'undefined' && DOMParser) || (typeof window !== 'undefined' && window.DOMParser);
|
||||
|
||||
if (!Parser) {
|
||||
throw new Error('cleanTextReplacementHtml() must be passed a `createDocument` function as an option when used in a non-browser environment');
|
||||
}
|
||||
|
||||
options.createDocument = function (html) {
|
||||
const parser = new Parser();
|
||||
return parser.parseFromString(html, 'text/html');
|
||||
};
|
||||
}
|
||||
|
||||
let cleanHtml = html
|
||||
.replace(/(\s| ){2,}/g, ' ')
|
||||
.trim()
|
||||
.replace(/^ | $/g, '')
|
||||
.trim();
|
||||
|
||||
// remove any elements that have a blank textContent
|
||||
if (cleanHtml) {
|
||||
let doc = options.createDocument(cleanHtml);
|
||||
|
||||
doc.body.querySelectorAll('*').forEach((element) => {
|
||||
if (!element.textContent.trim()) {
|
||||
if (element.textContent.length > 0) {
|
||||
// keep a single space to avoid collapsing spaces
|
||||
let space = doc.createTextNode(' ');
|
||||
element.replaceWith(space);
|
||||
} else {
|
||||
element.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
cleanHtml = doc.body.innerHTML.trim();
|
||||
}
|
||||
|
||||
return cleanHtml;
|
||||
}
|
@ -10,7 +10,8 @@ export const CARD_COMPONENT_MAP = {
|
||||
code: 'koenig-card-code',
|
||||
embed: 'koenig-card-embed',
|
||||
bookmark: 'koenig-card-bookmark',
|
||||
gallery: 'koenig-card-gallery'
|
||||
gallery: 'koenig-card-gallery',
|
||||
email: 'koenig-card-email'
|
||||
};
|
||||
|
||||
// map card names to generic icons (used for ghost elements when dragging)
|
||||
@ -23,7 +24,8 @@ export const CARD_ICON_MAP = {
|
||||
code: 'koenig/kg-card-type-gen-embed',
|
||||
embed: 'koenig/kg-card-type-gen-embed',
|
||||
bookmark: 'koenig/kg-card-type-bookmark',
|
||||
gallery: 'koenig/kg-card-type-gallery'
|
||||
gallery: 'koenig/kg-card-type-gallery',
|
||||
email: 'koenig/kg-card-type-gen-embed'
|
||||
};
|
||||
|
||||
// TODO: move koenigOptions directly into cards now that card components register
|
||||
@ -39,7 +41,8 @@ export default [
|
||||
return card.payload.imageSelector && !card.payload.src;
|
||||
}}),
|
||||
createComponentCard('markdown', {deleteIfEmpty: 'payload.markdown'}),
|
||||
createComponentCard('gallery', {hasEditMode: false})
|
||||
createComponentCard('gallery', {hasEditMode: false}),
|
||||
createComponentCard('email', {deleteIfEmpty: 'payload.html'})
|
||||
];
|
||||
|
||||
export const CARD_MENU = [
|
||||
@ -96,6 +99,14 @@ export const CARD_MENU = [
|
||||
type: 'card',
|
||||
replaceArg: 'bookmark',
|
||||
params: ['url']
|
||||
},
|
||||
{
|
||||
label: 'Email',
|
||||
icon: 'koenig/kg-card-type-html',
|
||||
matches: ['email'],
|
||||
type: 'card',
|
||||
replaceArg: 'email',
|
||||
developerExperiment: true
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
@ -419,6 +419,19 @@ export const BASIC_KEY_COMMANDS = DEFAULT_KEY_COMMANDS.filter((command) => {
|
||||
return basicCommands.includes(command.str);
|
||||
});
|
||||
|
||||
// key commands that are used in koenig-text-replacement-html-input
|
||||
export const TEXT_REPLACEMENT_KEY_COMMANDS = DEFAULT_KEY_COMMANDS.filter((command) => {
|
||||
let commands = [
|
||||
'BACKSPACE',
|
||||
'CTRL+K',
|
||||
'META+K',
|
||||
'CTRL+ALT+U',
|
||||
'ENTER',
|
||||
'SHIFT+ENTER'
|
||||
];
|
||||
return commands.includes(command.str);
|
||||
});
|
||||
|
||||
export default function registerKeyCommands(editor, koenig, commands = DEFAULT_KEY_COMMANDS) {
|
||||
commands.forEach((keyCommand) => {
|
||||
editor.registerKeyCommand({
|
||||
|
@ -53,9 +53,171 @@ export function replaceWithListSection(editor, matches, listTagName) {
|
||||
});
|
||||
}
|
||||
|
||||
function registerInlineMarkdownTextExpansions(editor) {
|
||||
/* inline markdown ------------------------------------------------------ */
|
||||
function _addMarkdownMarkup(_this, editor, matches, markupStr) {
|
||||
let {range} = editor;
|
||||
let match = matches[0].trim();
|
||||
let mdChars = (match.length - matches[1].length) / 2;
|
||||
|
||||
range = range.extend(-(match.length));
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let startPos = postEditor.deleteRange(range.head.toRange().extend(mdChars));
|
||||
let textRange = startPos.toRange().extend(matches[1].length);
|
||||
let markup = editor.builder.createMarkup(markupStr);
|
||||
postEditor.addMarkupToRange(textRange, markup);
|
||||
let endPos = postEditor.deleteRange(textRange.tail.toRange().extend(mdChars));
|
||||
postEditor.setRange(endPos.toRange());
|
||||
});
|
||||
|
||||
// must be scheduled so that the toggle isn't reset automatically
|
||||
// by mobiledoc-kit re-setting state after the range is updated
|
||||
run.later(_this, function () {
|
||||
editor.toggleMarkup(markupStr);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function _matchStrongStar(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)\*\*([^\s*]+|[^\s*][^*]*[^\s])\*\*$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'strong');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchStrongUnderscore(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)__([^\s_]+|[^\s_][^_]*[^\s])__$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'strong');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchEmStar(editor, text) {
|
||||
// (?:^|\s) - match beginning of input or a starting space (don't capture)
|
||||
// \* - match leading *
|
||||
// ( - start capturing group
|
||||
// [^\s*]+ - match a stretch with no spaces or * chars
|
||||
// | - OR
|
||||
// [^\s*] - match a single non-space or * char | this group will only match at
|
||||
// [^*]* - match zero or more non * chars | least two chars so we need the
|
||||
// [^\s] - match a single non-space char | [^\s*]+ to match single chars
|
||||
// ) - end capturing group
|
||||
// \* - match trailing *
|
||||
//
|
||||
// input = " *foo*"
|
||||
// matches[0] = " *foo*"
|
||||
// matches[1] = "foo"
|
||||
let matches = text.match(/(?:^|\s)\*([^\s*]+|[^\s*][^*]*[^\s])\*$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'em');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchEmUnderscore(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)_([^\s_]+|[^\s_][^_]*[^\s])_$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'em');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchSub(editor, text) {
|
||||
let matches = text.match(/(^|[^~])~([^\s~]+|[^\s~][^~]*[^\s~])~$/);
|
||||
if (matches) {
|
||||
// re-adjust the matches to remove the first matched char if it
|
||||
// exists, otherwise our length calculations are off. This is
|
||||
// different to other matchers because we match any char at the
|
||||
// beginning rather than a blank space and need to allow ~~ for
|
||||
// the strikethrough expansion
|
||||
let newMatches = [
|
||||
matches[1] ? matches[0].replace(matches[1], '').trim() : matches[0],
|
||||
matches[2]
|
||||
];
|
||||
_addMarkdownMarkup(this, editor, newMatches, 'sub');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchStrikethrough(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)~~([^\s~]+|[^\s~][^~]*[^\s])~~$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 's');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchCode(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)`([^\s`]+|[^\s`][^`]*[^\s`])`$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'code');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchSup(editor, text) {
|
||||
let matches = text.match(/\^([^\s^]+|[^\s^][^^]*[^\s^])\^$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'sup');
|
||||
}
|
||||
}
|
||||
|
||||
function _matchLink(editor, text) {
|
||||
let {range} = editor;
|
||||
let matches = text.match(/(?:^|\s)\[([^\s\]]*|[^\s\]][^\]]*[^\s\]])\]\(([^\s)]+|[^\s)][^)]*[^\s)])\)/);
|
||||
if (matches) {
|
||||
let url = matches[2];
|
||||
let text = matches[1] || url;
|
||||
let hasText = !!matches[1];
|
||||
let match = matches[0].trim();
|
||||
range = range.extend(-match.length);
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let startPos = postEditor.deleteRange(range.head.toRange().extend(hasText ? 1 : 3));
|
||||
let textRange = startPos.toRange().extend(text.length);
|
||||
let a = postEditor.builder.createMarkup('a', {href: url});
|
||||
postEditor.addMarkupToRange(textRange, a);
|
||||
let remainingRange = textRange.tail.toRange().extend(hasText ? (matches[2] || url).length + 3 : 1);
|
||||
let endPos = postEditor.deleteRange(remainingRange);
|
||||
postEditor.setRange(endPos.toRange());
|
||||
});
|
||||
|
||||
// must be scheduled so that the toggle isn't reset automatically
|
||||
run.schedule('actions', this, function () {
|
||||
editor.toggleMarkup('a');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function _matchImage(editor, text) {
|
||||
let matches = text.match(/^!\[(.*?)\]\((.*?)\)$/);
|
||||
if (matches) {
|
||||
let {range: {head, head: {section}}} = editor;
|
||||
let src = matches[2].trim();
|
||||
let alt = matches[1].trim();
|
||||
|
||||
// skip if cursor is not at end of section
|
||||
if (!head.isTail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mobiledoc lists don't support cards
|
||||
if (section.isListItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let card = postEditor.builder.createCardSection('image', {src, alt});
|
||||
// need to check the section before replacing else it will always
|
||||
// add a trailing paragraph
|
||||
let needsTrailingParagraph = !section.next;
|
||||
|
||||
editor.range.extend(-(matches[0].length));
|
||||
postEditor.replaceSection(editor.range.headSection, card);
|
||||
|
||||
if (needsTrailingParagraph) {
|
||||
let newSection = editor.builder.createMarkupSection('p');
|
||||
postEditor.insertSectionAtEnd(newSection);
|
||||
postEditor.setRange(newSection.tailPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function registerDashTextExpansions(editor) {
|
||||
// --\s = en dash –
|
||||
// ---. = em dash —
|
||||
// separate to the grouped replacement functions because we're matching on
|
||||
@ -104,7 +266,9 @@ function registerInlineMarkdownTextExpansions(editor) {
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerInlineMarkdownTextExpansions(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
|
||||
@ -119,194 +283,30 @@ function registerInlineMarkdownTextExpansions(editor) {
|
||||
|
||||
switch (matches[0]) {
|
||||
case '*':
|
||||
matchStrongStar(editor, text);
|
||||
matchEmStar(editor, text);
|
||||
_matchStrongStar(editor, text);
|
||||
_matchEmStar(editor, text);
|
||||
break;
|
||||
case '_':
|
||||
matchStrongUnderscore(editor, text);
|
||||
matchEmUnderscore(editor, text);
|
||||
_matchStrongUnderscore(editor, text);
|
||||
_matchEmUnderscore(editor, text);
|
||||
break;
|
||||
case ')':
|
||||
matchLink(editor, text);
|
||||
matchImage(editor, text);
|
||||
_matchLink(editor, text);
|
||||
_matchImage(editor, text);
|
||||
break;
|
||||
case '~':
|
||||
matchSub(editor, text);
|
||||
matchStrikethrough(editor, text);
|
||||
_matchSub(editor, text);
|
||||
_matchStrikethrough(editor, text);
|
||||
break;
|
||||
case '`':
|
||||
matchCode(editor, text);
|
||||
_matchCode(editor, text);
|
||||
break;
|
||||
case '^':
|
||||
matchSup(editor, text);
|
||||
_matchSup(editor, text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function _addMarkdownMarkup(_this, editor, matches, markupStr) {
|
||||
let {range} = editor;
|
||||
let match = matches[0].trim();
|
||||
let mdChars = (match.length - matches[1].length) / 2;
|
||||
|
||||
range = range.extend(-(match.length));
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let startPos = postEditor.deleteRange(range.head.toRange().extend(mdChars));
|
||||
let textRange = startPos.toRange().extend(matches[1].length);
|
||||
let markup = editor.builder.createMarkup(markupStr);
|
||||
postEditor.addMarkupToRange(textRange, markup);
|
||||
let endPos = postEditor.deleteRange(textRange.tail.toRange().extend(mdChars));
|
||||
postEditor.setRange(endPos.toRange());
|
||||
});
|
||||
|
||||
// must be scheduled so that the toggle isn't reset automatically
|
||||
// by mobiledoc-kit re-setting state after the range is updated
|
||||
run.later(_this, function () {
|
||||
editor.toggleMarkup(markupStr);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function matchStrongStar(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)\*\*([^\s*]+|[^\s*][^*]*[^\s])\*\*$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'strong');
|
||||
}
|
||||
}
|
||||
|
||||
function matchStrongUnderscore(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)__([^\s_]+|[^\s_][^_]*[^\s])__$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'strong');
|
||||
}
|
||||
}
|
||||
|
||||
function matchEmStar(editor, text) {
|
||||
// (?:^|\s) - match beginning of input or a starting space (don't capture)
|
||||
// \* - match leading *
|
||||
// ( - start capturing group
|
||||
// [^\s*]+ - match a stretch with no spaces or * chars
|
||||
// | - OR
|
||||
// [^\s*] - match a single non-space or * char | this group will only match at
|
||||
// [^*]* - match zero or more non * chars | least two chars so we need the
|
||||
// [^\s] - match a single non-space char | [^\s*]+ to match single chars
|
||||
// ) - end capturing group
|
||||
// \* - match trailing *
|
||||
//
|
||||
// input = " *foo*"
|
||||
// matches[0] = " *foo*"
|
||||
// matches[1] = "foo"
|
||||
let matches = text.match(/(?:^|\s)\*([^\s*]+|[^\s*][^*]*[^\s])\*$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'em');
|
||||
}
|
||||
}
|
||||
|
||||
function matchEmUnderscore(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)_([^\s_]+|[^\s_][^_]*[^\s])_$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'em');
|
||||
}
|
||||
}
|
||||
|
||||
function matchSub(editor, text) {
|
||||
let matches = text.match(/(^|[^~])~([^\s~]+|[^\s~][^~]*[^\s~])~$/);
|
||||
if (matches) {
|
||||
// re-adjust the matches to remove the first matched char if it
|
||||
// exists, otherwise our length calculations are off. This is
|
||||
// different to other matchers because we match any char at the
|
||||
// beginning rather than a blank space and need to allow ~~ for
|
||||
// the strikethrough expansion
|
||||
let newMatches = [
|
||||
matches[1] ? matches[0].replace(matches[1], '').trim() : matches[0],
|
||||
matches[2]
|
||||
];
|
||||
_addMarkdownMarkup(this, editor, newMatches, 'sub');
|
||||
}
|
||||
}
|
||||
|
||||
function matchStrikethrough(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)~~([^\s~]+|[^\s~][^~]*[^\s])~~$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 's');
|
||||
}
|
||||
}
|
||||
|
||||
function matchCode(editor, text) {
|
||||
let matches = text.match(/(?:^|\s)`([^\s`]+|[^\s`][^`]*[^\s`])`$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'code');
|
||||
}
|
||||
}
|
||||
|
||||
function matchSup(editor, text) {
|
||||
let matches = text.match(/\^([^\s^]+|[^\s^][^^]*[^\s^])\^$/);
|
||||
if (matches) {
|
||||
_addMarkdownMarkup(this, editor, matches, 'sup');
|
||||
}
|
||||
}
|
||||
|
||||
function matchLink(editor, text) {
|
||||
let {range} = editor;
|
||||
let matches = text.match(/(?:^|\s)\[([^\s\]]*|[^\s\]][^\]]*[^\s\]])\]\(([^\s)]+|[^\s)][^)]*[^\s)])\)/);
|
||||
if (matches) {
|
||||
let url = matches[2];
|
||||
let text = matches[1] || url;
|
||||
let hasText = !!matches[1];
|
||||
let match = matches[0].trim();
|
||||
range = range.extend(-match.length);
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let startPos = postEditor.deleteRange(range.head.toRange().extend(hasText ? 1 : 3));
|
||||
let textRange = startPos.toRange().extend(text.length);
|
||||
let a = postEditor.builder.createMarkup('a', {href: url});
|
||||
postEditor.addMarkupToRange(textRange, a);
|
||||
let remainingRange = textRange.tail.toRange().extend(hasText ? (matches[2] || url).length + 3 : 1);
|
||||
let endPos = postEditor.deleteRange(remainingRange);
|
||||
postEditor.setRange(endPos.toRange());
|
||||
});
|
||||
|
||||
// must be scheduled so that the toggle isn't reset automatically
|
||||
run.schedule('actions', this, function () {
|
||||
editor.toggleMarkup('a');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function matchImage(editor, text) {
|
||||
let matches = text.match(/^!\[(.*?)\]\((.*?)\)$/);
|
||||
if (matches) {
|
||||
let {range: {head, head: {section}}} = editor;
|
||||
let src = matches[2].trim();
|
||||
let alt = matches[1].trim();
|
||||
|
||||
// skip if cursor is not at end of section
|
||||
if (!head.isTail()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// mobiledoc lists don't support cards
|
||||
if (section.isListItem) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.run((postEditor) => {
|
||||
let card = postEditor.builder.createCardSection('image', {src, alt});
|
||||
// need to check the section before replacing else it will always
|
||||
// add a trailing paragraph
|
||||
let needsTrailingParagraph = !section.next;
|
||||
|
||||
editor.range.extend(-(matches[0].length));
|
||||
postEditor.replaceSection(editor.range.headSection, card);
|
||||
|
||||
if (needsTrailingParagraph) {
|
||||
let newSection = editor.builder.createMarkupSection('p');
|
||||
postEditor.insertSectionAtEnd(newSection);
|
||||
postEditor.setRange(newSection.tailPosition());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function (editor, koenig) {
|
||||
@ -435,6 +435,7 @@ export default function (editor, koenig) {
|
||||
|
||||
// must come after block expansions so that the smart hyphens expansion
|
||||
// doesn't break the divider card expansion
|
||||
registerDashTextExpansions(editor);
|
||||
registerInlineMarkdownTextExpansions(editor);
|
||||
}
|
||||
|
||||
@ -445,5 +446,96 @@ export function registerBasicTextExpansions(editor) {
|
||||
editor.unregisterTextInputHandler('ul');
|
||||
editor.unregisterTextInputHandler('ol');
|
||||
|
||||
registerDashTextExpansions(editor);
|
||||
registerInlineMarkdownTextExpansions(editor);
|
||||
}
|
||||
|
||||
// TODO: reduce duplication
|
||||
export function registerTextReplacementTextExpansions(editor, koenig) {
|
||||
// unregister mobiledoc-kit's block-level text handlers
|
||||
editor.unregisterTextInputHandler('heading');
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// as per registerInlineMarkdownTextExpansions but without ` for code and image matches
|
||||
editor.onTextInput({
|
||||
name: 'inline_markdown',
|
||||
match: /[*_)~^]$/,
|
||||
run(editor, matches) {
|
||||
let text = editor.range.head.section.textUntil(editor.range.head);
|
||||
|
||||
switch (matches[0]) {
|
||||
case '*':
|
||||
_matchStrongStar(editor, text);
|
||||
_matchEmStar(editor, text);
|
||||
break;
|
||||
case '_':
|
||||
_matchStrongUnderscore(editor, text);
|
||||
_matchEmUnderscore(editor, text);
|
||||
break;
|
||||
case ')':
|
||||
_matchLink(editor, text);
|
||||
break;
|
||||
case '~':
|
||||
_matchSub(editor, text);
|
||||
_matchStrikethrough(editor, text);
|
||||
break;
|
||||
case '^':
|
||||
_matchSup(editor, text);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editor.onTextInput({
|
||||
name: 'text_replacement',
|
||||
match: /\}$/,
|
||||
run(editor) {
|
||||
let text = editor.range.head.section.textUntil(editor.range.head);
|
||||
|
||||
let match = text.match(/(?:^|\s)\{([^\s{}]+|[^\s{}][^{}]*[^\s{}])\}$/);
|
||||
if (match) {
|
||||
_addMarkdownMarkup(this, editor, match, 'code');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
<KoenigCard
|
||||
@icon="koenig/card-indicator-html"
|
||||
@class={{concat (kg-style "container-card") " kg-email-card mih10 miw-100 relative"}}
|
||||
@style={{this.cardStyle}}
|
||||
@headerOffset={{this.headerOffset}}
|
||||
@toolbar={{this.toolbar}}
|
||||
@payload={{this.payload}}
|
||||
@isSelected={{this.isSelected}}
|
||||
@isEditing={{this.isEditing}}
|
||||
@selectCard={{action this.selectCard}}
|
||||
@deselectCard={{action this.deselectCard}}
|
||||
@editCard={{action this.editCard}}
|
||||
@saveCard={{action this.saveCard}}
|
||||
@onLeaveEdit={{action "leaveEditMode"}}
|
||||
@addParagraphAfterCard={{this.addParagraphAfterCard}}
|
||||
@moveCursorToPrevSection={{this.moveCursorToPrevSection}}
|
||||
@moveCursorToNextSection={{this.moveCursorToNextSection}}
|
||||
@editor={{this.editor}}
|
||||
as |card|
|
||||
>
|
||||
{{#if this.isEditing}}
|
||||
<KoenigTextReplacementHtmlInput
|
||||
@html={{this.payload.html}}
|
||||
@placeholder="Email only content..."
|
||||
@autofocus={{true}}
|
||||
@class="miw-100 bn bg-transparent"
|
||||
@onChange={{action "updateHtml"}}
|
||||
@onFocus={{action (mut this.isFocused) true}}
|
||||
@onBlur={{action (mut this.isFocused) false}}
|
||||
@didCreateEditor={{action "registerEditor"}}
|
||||
/>
|
||||
{{else}}
|
||||
<p>{{{this.payload.html}}}</p>
|
||||
<div class="koenig-card-click-overlay"></div>
|
||||
{{/if}}
|
||||
</KoenigCard>
|
@ -3,9 +3,11 @@
|
||||
{{section.title}}
|
||||
</div>
|
||||
{{#each section.items as |item|}}
|
||||
<div class="{{if item.selected "kg-cardmenu-card-selected"}} {{kg-style "cardmenu-card"}}" onclick={{action itemClicked item}} data-kg="cardmenu-card">
|
||||
<div class="{{kg-style "cardmenu-icon"}} {{item.iconClass}}">{{svg-jar item.icon class="w8 h8"}}</div>
|
||||
<div class="{{kg-style "cardmenu-label"}}">{{item.label}}</div>
|
||||
</div>
|
||||
{{#if (or (not item.developerExperiment) (and item.developerExperiment config.enableDeveloperExperiments))}}
|
||||
<div class="{{if item.selected "kg-cardmenu-card-selected"}} {{kg-style "cardmenu-card"}}" onclick={{action itemClicked item}} data-kg="cardmenu-card">
|
||||
<div class="{{kg-style "cardmenu-icon"}} {{item.iconClass}}">{{svg-jar item.icon class="w8 h8"}}</div>
|
||||
<div class="{{kg-style "cardmenu-label"}}">{{item.label}}</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
|
@ -0,0 +1,37 @@
|
||||
<div class="koenig-text-replacement-html-input__editor-wrappper" style="cursor: text">
|
||||
<div
|
||||
class="koenig-text-replacement-html-input__editor"
|
||||
data-gramm="false"
|
||||
data-kg="editor"
|
||||
data-kg-allow-clickthrough
|
||||
data-placeholder={{this.placeholder}}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<KoenigToolbar
|
||||
@basicOnly={{true}}
|
||||
@editor={{this.editor}}
|
||||
@editorRange={{this.selectedRange}}
|
||||
@activeMarkupTagNames={{this.activeMarkupTagNames}}
|
||||
@toggleMarkup={{action "toggleMarkup"}}
|
||||
@editLink={{action "editLink"}}
|
||||
/>
|
||||
|
||||
{{!-- pop-up link hover toolbar --}}
|
||||
<KoenigLinkToolbar
|
||||
@editor={{this.editor}}
|
||||
@container={{this.element}}
|
||||
@linkRange={{this.linkRange}}
|
||||
@selectedRange={{this.selectedRange}}
|
||||
@editLink={{action "editLink"}}
|
||||
/>
|
||||
|
||||
{{!-- pop-up link editing toolbar --}}
|
||||
{{#if this.linkRange}}
|
||||
<KoenigLinkInput
|
||||
@editor={{this.editor}}
|
||||
@linkRange={{this.linkRange}}
|
||||
@selectedRange={{this.selectedRange}}
|
||||
@cancel={{action "cancelEditLink"}}
|
||||
/>
|
||||
{{/if}}
|
@ -0,0 +1 @@
|
||||
export {default} from 'koenig-editor/components/koenig-card-email';
|
@ -0,0 +1 @@
|
||||
export {default} from 'koenig-editor/components/koenig-text-replacement-html-input';
|
Loading…
Reference in New Issue
Block a user