Prototyped component based atoms in the editor

refs https://github.com/TryGhost/Team/issues/931

- adds handling for component atoms
- add prototype button atom to test atom behaviour
- add `Cmd+Shift+B` keyboard shortcut to create a dummy button atom when the `emailCardSegments` feature is enabled
This commit is contained in:
Kevin Ansfield 2021-07-22 18:54:35 +01:00
parent 8c23a9b3bf
commit b9d262ffa6
9 changed files with 164 additions and 2 deletions

View File

@ -0,0 +1,21 @@
<a href="https://ghost.org" class="gh-btn"
{{on "mouseover" (fn (mut this.isHovered) true)}}
{{on "mouseleave" (fn (mut this.isHovered) false)}}
>
<span>{{@atom.value}}</span>
</a>
{{#if this.isHovered}}
<KgActionBar @class="absolute" @style={{this.toolbarStyle}} @isVisible={{true}} @instantClose={{this.koenigUi.inputHasFocus}}>
<li class="ma0 lh-solid">
<button
type="button"
title="Delete button"
class="dib dim-lite link h9 w9 nudge-top--1 justify-center"
{{on "click" this.deleteButton}}
>
{{svg-jar "koenig/kg-trash" class="fill-white w4 h4"}}
</button>
</li>
</KgActionBar>
{{/if}}

View File

@ -0,0 +1,12 @@
import Component from '@glimmer/component';
import {action} from '@ember/object';
import {tracked} from '@glimmer/tracking';
export default class KoenigAtomButtonComponent extends Component {
@tracked isHovered = false;
@action
deleteButton() {
// noop
}
}

View File

@ -93,4 +93,16 @@
registerComponent=(action (mut card.component))
}}
{{/in-element}}
{{/each}}
{{!-- all component atoms wormholed into the editor canvas --}}
{{#each this.componentAtoms as |atom|}}
{{#in-element atom.destinationElement}}
{{component atom.componentName
editor=this.editor
atom=atom
saveAtom=(fn atom.env.save atom.env.value atom.env.payload)
registerComponent=(action (mut atom.component))
}}
{{/in-element}}
{{/each}}

View File

@ -10,7 +10,7 @@ import EmberObject, {computed, get} from '@ember/object';
import Key from 'mobiledoc-kit/utils/key';
import MobiledocRange from 'mobiledoc-kit/utils/cursor/range';
import calculateReadingTime from '../utils/reading-time';
import defaultAtoms from '../options/atoms';
import defaultAtoms, {ATOM_COMPONENT_MAP} from '../options/atoms';
import defaultCards, {CARD_COMPONENT_MAP, CARD_ICON_MAP} from '../options/cards';
import formatMarkdown from 'ghost-admin/utils/format-markdown';
import registerKeyCommands from '../options/key-commands';
@ -38,6 +38,8 @@ const UNDO_DEPTH = 100;
export const ADD_CARD_HOOK = 'addComponent';
export const REMOVE_CARD_HOOK = 'removeComponent';
export const ADD_ATOM_HOOK = 'addAtomComponent';
export const REMOVE_ATOM_HOOK = 'removeAtomComponent';
// used in test helpers to grab a reference to the underlying mobiledoc editor
export const TESTING_EXPANDO_PROPERTY = '__mobiledoc_kit_editor';
@ -170,7 +172,9 @@ function insertImageCards(files, postEditor) {
}
export default Component.extend({
feature: service(),
koenigDragDropHandler: service(),
koenigUi: service(),
tagName: 'article',
classNames: ['koenig-editor', 'w-100', 'flex-grow', 'relative', 'center', 'mb0', 'mt0'],
@ -192,6 +196,7 @@ export default Component.extend({
activeMarkupTagNames: null,
activeSectionTagNames: null,
selectedRange: null,
componentAtoms: null,
componentCards: null,
linkRange: null,
selectedCard: null,
@ -256,6 +261,7 @@ export default Component.extend({
this.set('mobiledoc', mobiledoc);
}
this.set('componentAtoms', A([]));
this.set('componentCards', A([]));
this.set('activeMarkupTagNames', {});
this.set('activeSectionTagNames', {});
@ -367,6 +373,46 @@ export default Component.extend({
// triggered when a card section is removed from the mobiledoc
[REMOVE_CARD_HOOK]: (card) => {
this.componentCards.removeObject(card);
},
[ADD_ATOM_HOOK]: ({env, options, value, payload}) => {
const atomName = env.name;
const componentName = ATOM_COMPONENT_MAP[atomName];
const payloadCopy = new TrackedObject(JSON.parse(JSON.stringify(payload || null)));
const atom = EmberObject.create({
atomName,
componentName,
value,
payload: payloadCopy,
env,
options,
editor
});
// the desination element is the container that gets rendered
// inside the editor, once rendered we use {{in-element}} to
// wormhole in the actual ember component
let atomId = guidFor(atom);
let destinationElementId = `koenig-editor-atom-${atomId}`;
let destinationElement = document.createElement('div');
destinationElement.id = destinationElementId;
destinationElement.classList.add('dib');
atom.setProperties({
destinationElementId,
destinationElement
});
run.schedule('afterRender', () => {
this.componentAtoms.pushObject(atom);
});
// render the destination element inside the editor
return {atom, element: destinationElement};
},
[REMOVE_ATOM_HOOK]: (atom) => {
this.componentAtoms.removeObject(atom);
}
};
editorOptions.cardOptions = componentHooks;

View File

@ -1,6 +1,12 @@
// Atoms are effectively read-only inline cards
// Full docs: https://github.com/bustle/mobiledoc-kit/blob/master/ATOMS.md
import createComponentAtom from '../utils/create-component-atom';
export const ATOM_COMPONENT_MAP = {
button: 'koenig-atom-button'
};
export default [
// soft-return is triggered by SHIFT+ENTER and allows for line breaks
// without creating paragraphs
@ -10,5 +16,6 @@ export default [
render() {
return document.createElement('br');
}
}
},
createComponentAtom('button')
];

View File

@ -419,6 +419,31 @@ export const DEFAULT_KEY_COMMANDS = [{
return false;
}
}, {
str: 'META+SHIFT+B',
run(editor, koenig) {
if (!koenig.feature.emailCardSegments || !editor.range.headSection.isMarkerable) {
return;
}
editor.run((postEditor) => {
const texts = [
'Hit me!',
'Hit me!',
'Hit me!',
'Hit me slowly',
'Hit me quick',
'Hit me with your rhythm stick!'
];
const buttonText = texts[koenig.koenigUi.buttonCount % texts.length];
koenig.koenigUi.buttonCount += 1;
const button = postEditor.builder.createAtom('button', buttonText);
const endPos = postEditor.insertMarkers(editor.range.head, [button]);
postEditor.insertText(endPos, ' ');
});
}
}];
// key commands that are used in koenig-basic-html-input

View File

@ -6,6 +6,8 @@ export default class KoenigUiService extends Service {
@tracked inputHasFocus = false;
@tracked isDragging = false;
buttonCount = 0;
#focusedCaption = null;
captionGainedFocus(caption) {

View File

@ -0,0 +1,36 @@
import {
ADD_ATOM_HOOK,
REMOVE_ATOM_HOOK
} from '../components/koenig-editor';
const RENDER_TYPE = 'dom';
function renderFallback(doc) {
let element = doc.createElement('span');
let text = doc.createTextNode('[placeholder for Ember component atom]');
element.appendChild(text);
return element;
}
// sets up boilderplate for an Ember component atom
export default function createComponentAtom(name, doc = window.document) {
return {
name,
type: RENDER_TYPE,
// Called when the atom is added to a mobiledoc document.
render(atomArgs) {
const {env, options} = atomArgs;
if (!options[ADD_ATOM_HOOK]) {
return renderFallback(doc);
}
const {atom, element} = options[ADD_ATOM_HOOK](atomArgs);
env.onTeardown(() => options[REMOVE_ATOM_HOOK](atom));
return element;
}
};
}

View File

@ -0,0 +1 @@
export {default} from 'koenig-editor/components/koenig-atom-button';