From f553a4d3ff6e27a2caf25c3243ca595c0db4737a Mon Sep 17 00:00:00 2001 From: Richard Livsey Date: Sun, 13 Mar 2016 23:40:14 +0000 Subject: [PATCH] Adds createComponentAtom util to use Ember.Component as atoms --- .../components/mobiledoc-editor/component.js | 43 ++++++++- .../components/mobiledoc-editor/template.hbs | 11 +++ addon/utils/create-component-atom.js | 31 +++++++ addon/utils/create-component-card.js | 14 +-- .../mobiledoc-editor/component-test.js | 93 +++++++++++++++++++ .../unit/utils/create-component-atom-test.js | 33 +++++++ 6 files changed, 213 insertions(+), 12 deletions(-) create mode 100644 addon/utils/create-component-atom.js create mode 100644 tests/unit/utils/create-component-atom-test.js diff --git a/addon/components/mobiledoc-editor/component.js b/addon/components/mobiledoc-editor/component.js index 9525c2f..d290139 100644 --- a/addon/components/mobiledoc-editor/component.js +++ b/addon/components/mobiledoc-editor/component.js @@ -6,10 +6,13 @@ import { MOBILEDOC_VERSION } from 'mobiledoc-kit/renderers/mobiledoc'; let { computed, Component } = Ember; let { capitalize, camelize } = Ember.String; -export const ADD_HOOK = 'addComponent'; -export const REMOVE_HOOK = 'removeComponent'; +export const ADD_CARD_HOOK = 'addComponent'; +export const REMOVE_CARD_HOOK = 'removeComponent'; +export const ADD_ATOM_HOOK = 'addAtomComponent'; +export const REMOVE_ATOM_HOOK = 'removeAtomComponent'; export const WILL_CREATE_EDITOR_ACTION = 'will-create-editor'; export const DID_CREATE_EDITOR_ACTION = 'did-create-editor'; + const EDITOR_CARD_SUFFIX = '-editor'; const EMPTY_MOBILEDOC = { version: MOBILEDOC_VERSION, @@ -63,6 +66,7 @@ export default Component.extend({ this.set('mobiledoc', mobiledoc); } this.set('componentCards', Ember.A([])); + this.set('componentAtoms', Ember.A([])); this.set('linkOffsets', null); this.set('activeMarkupTagNames', {}); this.set('activeSectionTagNames', {}); @@ -154,7 +158,7 @@ export default Component.extend({ let editorOptions = this.get('editorOptions'); editorOptions.mobiledoc = mobiledoc; editorOptions.cardOptions = { - [ADD_HOOK]: ({env, options, payload}, isEditing=false) => { + [ADD_CARD_HOOK]: ({env, options, payload}, isEditing=false) => { let cardId = Ember.uuid(); let cardName = env.name; if (isEditing) { @@ -180,8 +184,35 @@ export default Component.extend({ }); return { card, element }; }, - [REMOVE_HOOK]: (card) => { + [ADD_ATOM_HOOK]: ({env, options, payload, value}) => { + let atomId = Ember.uuid(); + let atomName = env.name; + let destinationElementId = `mobiledoc-editor-atom-${atomId}`; + let element = document.createElement('span'); + element.id = destinationElementId; + + // The data must be copied to avoid sharing the reference + payload = Ember.copy(payload, true); + + let atom = Ember.Object.create({ + destinationElementId, + atomName, + payload, + value, + callbacks: env, + editor, + postModel: env.postModel + }); + Ember.run.schedule('afterRender', () => { + this.get('componentAtoms').pushObject(atom); + }); + return { atom, element }; + }, + [REMOVE_CARD_HOOK]: (card) => { this.get('componentCards').removeObject(card); + }, + [REMOVE_ATOM_HOOK]: (atom) => { + this.get('componentAtoms').removeObject(atom); } }; editor = new Editor(editorOptions); @@ -233,7 +264,9 @@ export default Component.extend({ willDestroyElement() { let editor = this.get('editor'); - editor.destroy(); + try { + editor.destroy(); + } catch(e) {} }, postDidChange(editor) { diff --git a/addon/components/mobiledoc-editor/template.hbs b/addon/components/mobiledoc-editor/template.hbs index 87b541a..274f2ef 100644 --- a/addon/components/mobiledoc-editor/template.hbs +++ b/addon/components/mobiledoc-editor/template.hbs @@ -39,3 +39,14 @@ removeCard=(action card.env.remove)}} {{/ember-wormhole}} {{/each}} + +{{#each componentAtoms as |atom|}} + {{#ember-wormhole to=atom.destinationElementId}} + {{component atom.atomName + editor=editor + postModel=atom.postModel + atomName=atom.atomName + payload=atom.payload + value=atom.value}} + {{/ember-wormhole}} +{{/each}} diff --git a/addon/utils/create-component-atom.js b/addon/utils/create-component-atom.js new file mode 100644 index 0000000..3712789 --- /dev/null +++ b/addon/utils/create-component-atom.js @@ -0,0 +1,31 @@ +const RENDER_TYPE = 'dom'; + +import { ADD_ATOM_HOOK, REMOVE_ATOM_HOOK } from '../components/mobiledoc-editor/component'; + +function renderFallback() { + let element = document.createElement('span'); + element.innerHTML = '[placeholder for Ember atom]'; + return element; +} + +export default function createComponentAtom(name) { + + return { + name, + type: RENDER_TYPE, + render(atomArg) { + let {env, options} = atomArg; + if (!options[ADD_ATOM_HOOK]) { + return renderFallback(); + } + + let { atom, element } = options[ADD_ATOM_HOOK](atomArg); + let { onTeardown } = env; + + onTeardown(() => options[REMOVE_ATOM_HOOK](atom)); + + return element; + } + }; + +} diff --git a/addon/utils/create-component-card.js b/addon/utils/create-component-card.js index cd2649d..a92fb85 100644 --- a/addon/utils/create-component-card.js +++ b/addon/utils/create-component-card.js @@ -1,6 +1,6 @@ const RENDER_TYPE = 'dom'; -import { ADD_HOOK, REMOVE_HOOK } from '../components/mobiledoc-editor/component'; +import { ADD_CARD_HOOK, REMOVE_CARD_HOOK } from '../components/mobiledoc-editor/component'; function renderFallback() { let element = document.createElement('div'); @@ -15,28 +15,28 @@ export default function createComponentCard(name) { type: RENDER_TYPE, render(cardArg) { let {env, options} = cardArg; - if (!options[ADD_HOOK]) { + if (!options[ADD_CARD_HOOK]) { return renderFallback(); } - let { card, element } = options[ADD_HOOK](cardArg); + let { card, element } = options[ADD_CARD_HOOK](cardArg); let { onTeardown } = env; - onTeardown(() => options[REMOVE_HOOK](card)); + onTeardown(() => options[REMOVE_CARD_HOOK](card)); return element; }, edit(cardArg) { let {env, options} = cardArg; - if (!options[ADD_HOOK]) { + if (!options[ADD_CARD_HOOK]) { return renderFallback(); } let isEditing = true; - let { card, element } = options[ADD_HOOK](cardArg, isEditing); + let { card, element } = options[ADD_CARD_HOOK](cardArg, isEditing); let { onTeardown } = env; - onTeardown(() => options[REMOVE_HOOK](card)); + onTeardown(() => options[REMOVE_CARD_HOOK](card)); return element; } diff --git a/tests/integration/components/mobiledoc-editor/component-test.js b/tests/integration/components/mobiledoc-editor/component-test.js index f7b6299..d74c4b0 100644 --- a/tests/integration/components/mobiledoc-editor/component-test.js +++ b/tests/integration/components/mobiledoc-editor/component-test.js @@ -2,6 +2,7 @@ import { moduleForComponent, test } from 'ember-qunit'; import { selectRange } from 'dummy/tests/helpers/selection'; import hbs from 'htmlbars-inline-precompile'; import createComponentCard from 'ember-mobiledoc-editor/utils/create-component-card'; +import createComponentAtom from 'ember-mobiledoc-editor/utils/create-component-atom'; import moveCursorTo from '../../../helpers/move-cursor-to'; import simulateMouseup from '../../../helpers/simulate-mouse-up'; import Ember from 'ember'; @@ -807,3 +808,95 @@ test('#activeSectionTagNames is correct when a card is selected', function(asser done(); }); }); + +test('wraps component-atom adding in runloop correctly', function(assert) { + assert.expect(3); + let mobiledoc = simpleMobileDoc('Howdy'); + let editor; + + this.set('mobiledoc', mobiledoc); + this.register('component:gather-editor', Ember.Component.extend({ + didRender() { + editor = this.get('editor'); + } + })); + this.registry.register('template:components/demo-atom', hbs` + demo-atom + `); + this.set('atoms', [createComponentAtom('demo-atom')]); + this.set('mobiledoc', simpleMobileDoc('')); + this.render(hbs` + {{#mobiledoc-editor mobiledoc=mobiledoc atoms=atoms as |editor|}} + {{gather-editor editor=editor.editor}} + {{/mobiledoc-editor}} + `); + + // Add an atom without being in a runloop + assert.ok(!Ember.run.currentRunLoop, 'precond - no run loop'); + editor.run((postEditor) => { + moveCursorTo(this, 'p:first'); + let position = editor.cursor.offsets.head; + let atom = postEditor.builder.createAtom('demo-atom', 'value', {}); + postEditor.insertMarkers(position, [atom]); + }); + assert.ok(!Ember.run.currentRunLoop, 'postcond - no run loop after editor.run'); + + assert.ok(this.$('#demo-atom').length, 'demo atom is added'); +}); + +test('throws on unknown atom when `unknownAtomHandler` is not passed', function(assert) { + this.set('mobiledoc', { + version: MOBILEDOC_VERSION, + atoms: [ + ['missing-atom', 'value', {}] + ], + markups: [], + cards: [], + sections: [ + [1, 'P', [ + [1, [], 0, 0]] + ] + ] + }); + this.set('unknownAtomHandler', undefined); + + assert.throws(() => { + this.render(hbs` + {{#mobiledoc-editor mobiledoc=mobiledoc + options=(hash unknownAtomHandler=unknownAtomHandler) as |editor|}} + {{/mobiledoc-editor}} + `); + }, /Unknown atom "missing-atom" found.*no unknownAtomHandler/); +}); + +test('calls `unknownAtomHandler` when it renders an unknown atom', function(assert) { + assert.expect(4); + let expectedPayload = {}; + + this.set('unknownAtomHandler', ({env, value, payload}) => { + assert.equal(env.name, 'missing-atom', 'correct env.name'); + assert.equal(value, 'value', 'correct name'); + assert.ok(!!env.onTeardown, 'has onTeardown hook'); + assert.deepEqual(payload, expectedPayload, 'has payload'); + }); + + this.set('mobiledoc', { + version: MOBILEDOC_VERSION, + atoms: [ + ['missing-atom', 'value', expectedPayload] + ], + markups: [], + cards: [], + sections: [ + [1, 'P', [ + [1, [], 0, 0]] + ] + ] + }); + + this.render(hbs` + {{#mobiledoc-editor mobiledoc=mobiledoc + options=(hash unknownAtomHandler=unknownAtomHandler) as |editor|}} + {{/mobiledoc-editor}} + `); +}); \ No newline at end of file diff --git a/tests/unit/utils/create-component-atom-test.js b/tests/unit/utils/create-component-atom-test.js new file mode 100644 index 0000000..2893857 --- /dev/null +++ b/tests/unit/utils/create-component-atom-test.js @@ -0,0 +1,33 @@ +import createComponentAtom from 'ember-mobiledoc-editor/utils/create-component-atom'; +import { module, test } from 'qunit'; +import MobiledocDOMRenderer from 'mobiledoc-dom-renderer'; + +module('Unit | Utility | create component atom'); + +test('it creates an atom', function(assert) { + var result = createComponentAtom('foo-atom'); + assert.ok(result.name === 'foo-atom' && + result.type === 'dom' && + typeof result.render === 'function', + 'created a named atom' + ); +}); + +test('it creates a renderable atom', function(assert) { + var atom = createComponentAtom('foo-atom'); + let renderer = new MobiledocDOMRenderer({atoms: [atom]}); + + let {result} = renderer.render({ + version: '0.3.0', + atoms: [ + ['foo-atom', '', {}] + ], + sections: [ + [1, 'P', [ + [1, [], 0, 0]] + ] + ] + }); + + assert.ok(result, 'atom rendered'); +});