From fb2fa3619d37211306b7b9cf8704c6f8ccb97585 Mon Sep 17 00:00:00 2001 From: Cory Forsyth Date: Wed, 18 Nov 2015 14:04:23 -0500 Subject: [PATCH] Implement card refactor for editor-dom renderer Update cards to be in format expected by dom renderer This is based on the changes to mobiledoc-dom-renderer in https://github.com/bustlelabs/mobiledoc-dom-renderer/pull/18 Reorganize demo cards into dom/ html/ and text/ directories fixes #236 fixes #239 --- demo/app/components/mobiledoc-dom-renderer.js | 26 +- .../app/components/mobiledoc-html-renderer.js | 21 +- .../app/components/mobiledoc-text-renderer.js | 38 +- demo/app/helpers/mobiledoc-cards-list.js | 4 +- demo/app/mobiledoc-cards/codemirror.js | 47 -- demo/app/mobiledoc-cards/dom.js | 13 + demo/app/mobiledoc-cards/dom/codemirror.js | 50 ++ demo/app/mobiledoc-cards/dom/image.js | 56 ++ demo/app/mobiledoc-cards/dom/input.js | 37 + demo/app/mobiledoc-cards/dom/selfie.js | 85 +++ demo/app/mobiledoc-cards/dom/simple.js | 17 + demo/app/mobiledoc-cards/html.js | 13 + demo/app/mobiledoc-cards/html/codemirror.js | 9 + demo/app/mobiledoc-cards/html/image.js | 9 + demo/app/mobiledoc-cards/html/input.js | 7 + demo/app/mobiledoc-cards/html/selfie.js | 7 + demo/app/mobiledoc-cards/html/simple.js | 7 + demo/app/mobiledoc-cards/image.js | 59 -- demo/app/mobiledoc-cards/index.js | 19 - demo/app/mobiledoc-cards/input.js | 43 -- demo/app/mobiledoc-cards/selfie.js | 92 --- demo/app/mobiledoc-cards/simple.js | 17 - demo/app/mobiledoc-cards/text.js | 13 + demo/app/mobiledoc-cards/text/codemirror.js | 9 + demo/app/mobiledoc-cards/text/image.js | 7 + demo/app/mobiledoc-cards/text/input.js | 7 + demo/app/mobiledoc-cards/text/selfie.js | 7 + demo/app/mobiledoc-cards/text/simple.js | 7 + demo/bower.json | 16 +- demo/package.json | 6 +- src/js/cards/image.js | 15 +- src/js/editor/editor.js | 3 +- src/js/models/card-node.js | 61 +- src/js/renderers/editor-dom.js | 67 +- tests/acceptance/basic-editor-test.js | 11 +- tests/acceptance/cursor-movement-test.js | 11 +- tests/acceptance/cursor-position-test.js | 11 +- tests/acceptance/editor-cards-test.js | 184 ++--- tests/acceptance/editor-copy-paste-test.js | 18 +- tests/acceptance/editor-post-editor-test.js | 61 +- tests/acceptance/editor-selections-test.js | 9 +- tests/unit/editor/card-lifecycle-test.js | 632 +++++++++++++----- tests/unit/renderers/editor-dom-test.js | 26 +- 43 files changed, 1087 insertions(+), 770 deletions(-) delete mode 100644 demo/app/mobiledoc-cards/codemirror.js create mode 100644 demo/app/mobiledoc-cards/dom.js create mode 100644 demo/app/mobiledoc-cards/dom/codemirror.js create mode 100644 demo/app/mobiledoc-cards/dom/image.js create mode 100644 demo/app/mobiledoc-cards/dom/input.js create mode 100644 demo/app/mobiledoc-cards/dom/selfie.js create mode 100644 demo/app/mobiledoc-cards/dom/simple.js create mode 100644 demo/app/mobiledoc-cards/html.js create mode 100644 demo/app/mobiledoc-cards/html/codemirror.js create mode 100644 demo/app/mobiledoc-cards/html/image.js create mode 100644 demo/app/mobiledoc-cards/html/input.js create mode 100644 demo/app/mobiledoc-cards/html/selfie.js create mode 100644 demo/app/mobiledoc-cards/html/simple.js delete mode 100644 demo/app/mobiledoc-cards/image.js delete mode 100644 demo/app/mobiledoc-cards/index.js delete mode 100644 demo/app/mobiledoc-cards/input.js delete mode 100644 demo/app/mobiledoc-cards/selfie.js delete mode 100644 demo/app/mobiledoc-cards/simple.js create mode 100644 demo/app/mobiledoc-cards/text.js create mode 100644 demo/app/mobiledoc-cards/text/codemirror.js create mode 100644 demo/app/mobiledoc-cards/text/image.js create mode 100644 demo/app/mobiledoc-cards/text/input.js create mode 100644 demo/app/mobiledoc-cards/text/selfie.js create mode 100644 demo/app/mobiledoc-cards/text/simple.js diff --git a/demo/app/components/mobiledoc-dom-renderer.js b/demo/app/components/mobiledoc-dom-renderer.js index c573ef260..64dce2214 100644 --- a/demo/app/components/mobiledoc-dom-renderer.js +++ b/demo/app/components/mobiledoc-dom-renderer.js @@ -1,22 +1,28 @@ import Ember from 'ember'; -import { cardsHash } from '../mobiledoc-cards/index'; +import cards from '../mobiledoc-cards/dom'; import Renderer from 'ember-mobiledoc-dom-renderer'; -let { computed, run } = Ember; +let { run } = Ember; + +let renderer = new Renderer({cards}); export default Ember.Component.extend({ - domRenderer: computed(function(){ - return new Renderer(); - }), didRender() { - let domRenderer = this.get('domRenderer'); let mobiledoc = this.get('mobiledoc'); + if (!mobiledoc) { + return; + } + run(() => { - let target = this.$(); - target.empty(); - if (mobiledoc) { - domRenderer.render(mobiledoc, target[0], cardsHash); + if (this._teardownRender) { + this._teardownRender(); + this._teardownRender = null; } + + let target = this.$(); + let { result, teardown } = renderer.render(mobiledoc); + target.append(result); + this._teardownRender = teardown; }); } }); diff --git a/demo/app/components/mobiledoc-html-renderer.js b/demo/app/components/mobiledoc-html-renderer.js index 5632a5d23..eb5b92303 100644 --- a/demo/app/components/mobiledoc-html-renderer.js +++ b/demo/app/components/mobiledoc-html-renderer.js @@ -1,23 +1,22 @@ import Ember from 'ember'; -import { cardsHash } from '../mobiledoc-cards/index'; +import cards from '../mobiledoc-cards/html'; import Renderer from 'ember-mobiledoc-html-renderer'; -let { computed, run } = Ember; +let { run } = Ember; + +let renderer = new Renderer({cards}); export default Ember.Component.extend({ - htmlRenderer: computed(function(){ - return new Renderer(); - }), didRender() { - let renderer = this.get('htmlRenderer'); let mobiledoc = this.get('mobiledoc'); + if (!mobiledoc) { + return; + } + run(() => { let target = this.$(); - target.empty(); - if (mobiledoc) { - let html = renderer.render(mobiledoc, cardsHash); - target.text(html); - } + let { result: html } = renderer.render(mobiledoc); + target.text(html); }); } }); diff --git a/demo/app/components/mobiledoc-text-renderer.js b/demo/app/components/mobiledoc-text-renderer.js index ea934445b..23df4b5fd 100644 --- a/demo/app/components/mobiledoc-text-renderer.js +++ b/demo/app/components/mobiledoc-text-renderer.js @@ -1,26 +1,36 @@ import Ember from 'ember'; -import { cardsHash } from '../mobiledoc-cards/index'; +import cards from '../mobiledoc-cards/text'; import Renderer from 'ember-mobiledoc-text-renderer'; -let { computed, run } = Ember; +let { run } = Ember; + +let renderer = new Renderer({cards}); + +let addHTMLEntitites = (str) => { + return str.replace(//g, '>') + .replace(/\n/g, '
'); +}; export default Ember.Component.extend({ - textRenderer: computed(function(){ - return new Renderer(); - }), didRender() { - let renderer = this.get('textRenderer'); let mobiledoc = this.get('mobiledoc'); + if (!mobiledoc) { + return; + } run(() => { - let target = this.$(); - target.empty(); - if (mobiledoc) { - let text = renderer.render(mobiledoc, cardsHash); - text = text.replace(//g,'>') - .replace(/\n/g, '
'); - target.html(text); + if (this._teardownRender) { + this._teardownRender(); + this._teardownRender = null; } + + let target = this.$(); + let {result: text, teardown} = renderer.render(mobiledoc); + + text = addHTMLEntitites(text); + target.html(text); + + this._teardownRender = teardown; }); } }); diff --git a/demo/app/helpers/mobiledoc-cards-list.js b/demo/app/helpers/mobiledoc-cards-list.js index 02a411719..8f00faec0 100644 --- a/demo/app/helpers/mobiledoc-cards-list.js +++ b/demo/app/helpers/mobiledoc-cards-list.js @@ -1,8 +1,8 @@ import Ember from 'ember'; -import { cardsList } from '../mobiledoc-cards/index'; +import cards from '../mobiledoc-cards/dom'; export function mobiledocCardsList() { - return cardsList; + return cards; } export default Ember.Helper.helper(mobiledocCardsList); diff --git a/demo/app/mobiledoc-cards/codemirror.js b/demo/app/mobiledoc-cards/codemirror.js deleted file mode 100644 index 787713646..000000000 --- a/demo/app/mobiledoc-cards/codemirror.js +++ /dev/null @@ -1,47 +0,0 @@ -/* global $, CodeMirror */ -let getCode = ({code}) => { - return code || 'let x = 3;'; -}; - -export default { - name: 'codemirror-card', - display: { - setup(element, options, env, payload) { - $(element).empty(); - let code = getCode(payload); - let button = $(''); - - if (env.edit) { - button.on('click', env.edit); - $(element).append(button); - } - - let ta = $(``); - $(element).append(ta); - CodeMirror.fromTextArea(ta[0], { - mode: 'javascript', - readOnly: 'nocursor' - }); - } - }, - edit: { - setup(element, options, env, payload) { - $(element).empty(); - - let code = getCode(payload); - let ta = $(``); - - let button = $(''); - $(element).append(button); - - $(element).append(ta); - let cm = CodeMirror.fromTextArea(ta[0], { - mode: 'javascript' - }); - button.on('click', () => { - let code = cm.getValue(); - env.save({code}); - }); - } - } -}; diff --git a/demo/app/mobiledoc-cards/dom.js b/demo/app/mobiledoc-cards/dom.js new file mode 100644 index 000000000..7f255bb00 --- /dev/null +++ b/demo/app/mobiledoc-cards/dom.js @@ -0,0 +1,13 @@ +import inputCard from './dom/input'; +import simpleCard from './dom/simple'; +import selfieCard from './dom/selfie'; +import imageCard from './dom/image'; +import codemirrorCard from './dom/codemirror'; + +export default [ + inputCard, + simpleCard, + selfieCard, + imageCard, + codemirrorCard +]; diff --git a/demo/app/mobiledoc-cards/dom/codemirror.js b/demo/app/mobiledoc-cards/dom/codemirror.js new file mode 100644 index 000000000..5d740d074 --- /dev/null +++ b/demo/app/mobiledoc-cards/dom/codemirror.js @@ -0,0 +1,50 @@ +/* global $, CodeMirror */ +const defaultCode = 'let x = 3;'; + +function codeMirror(element, code, readOnly=true, callback=()=>{}) { + setTimeout(() => { + let ta = $(``).appendTo(element); + let options = { + mode: 'javascript' + }; + if (readOnly) { + options.readOnly = 'nocursor'; + } + let cm = CodeMirror.fromTextArea(ta[0], options); + callback(cm); + }); +} + +export default { + name: 'codemirror-card', + type: 'dom', + + render({env, options, payload}) { + let element = $('
')[0]; + let code = payload.code || defaultCode; + + if (env.isInEditor) { + $('').appendTo(element).on('click', env.edit); + } + + let readOnly = true; + codeMirror(element, code, readOnly); + + return element; + }, + + edit({env, options, payload}) { + let element = $('
')[0]; + let code = payload.code || defaultCode; + + let saveButton = $('').appendTo(element); + + let readOnly = false; + let callback = (cm) => { + saveButton.on('click', () => env.save({code: cm.getValue()})); + }; + codeMirror(element, code, readOnly, callback); + + return element; + } +}; diff --git a/demo/app/mobiledoc-cards/dom/image.js b/demo/app/mobiledoc-cards/dom/image.js new file mode 100644 index 000000000..c3f6f8b52 --- /dev/null +++ b/demo/app/mobiledoc-cards/dom/image.js @@ -0,0 +1,56 @@ +/* global $ */ +const defaultSrc = 'http://placekitten.com/200/75'; + +function makeImageInWrapper(src=defaultSrc) { + return $('
').append(``)[0]; +} + +export default { + name: 'image-card', + type: 'dom', + render({env, payload}) { + let element = makeImageInWrapper(payload.src); + let { isInEditor } = env; + + if (isInEditor) { + $('').appendTo(element) + .on('click', env.edit); + } + + return element; + }, + edit({env, payload}) { + let element = makeImageInWrapper(payload.src); + + function importImage(event) { + let reader = new FileReader(); + let file = event.target.files[0]; + reader.onloadend = () => { + env.save({src: reader.result}); + }; + reader.readAsDataURL(file); + } + + $('').appendTo(element) + .on('change', importImage); + + $('').appendTo(element) + .on('click', () => { env.save(payload); }); + + return element; + } + /* FIXME: html and text + html: { + setup(buffer, options, env, payload) { + let src = payload.src || defaultSrc; + let html = ``; + buffer.push(html); + } + }, + text: { + setup(str, options, env, payload) { + return "[image]"; + } + } + */ +}; diff --git a/demo/app/mobiledoc-cards/dom/input.js b/demo/app/mobiledoc-cards/dom/input.js new file mode 100644 index 000000000..c5fb289d2 --- /dev/null +++ b/demo/app/mobiledoc-cards/dom/input.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; + +let { $ } = Ember; + +export default { + name: 'input-card', + type: 'dom', + render({env, payload}) { + var text = 'I am in display mode'; + if (payload.name) { + text = 'Hello, ' + payload.name + '!'; + } + var card = $(`
${text}
`); + var button = $(''); + button.on('click', env.edit); + + if (env.edit) { + card.append(button); + } + return card[0]; + }, + edit({env}) { + var card = $('
What is your name?
'); + card.innerHTML = 'What is your name?'; + + var input = $(''); + var button = $(''); + button.on('click', () => { + var name = input.val(); + env.save({name}); + }); + + card.append(input); + card.append(button); + return card[0]; + } +}; diff --git a/demo/app/mobiledoc-cards/dom/selfie.js b/demo/app/mobiledoc-cards/dom/selfie.js new file mode 100644 index 000000000..e5d592d58 --- /dev/null +++ b/demo/app/mobiledoc-cards/dom/selfie.js @@ -0,0 +1,85 @@ +import Ember from 'ember'; + +let { $ } = Ember; + +export default { + name: 'selfie-card', + type: 'dom', + render: ({env, payload}) => { + let element = $('
')[0]; + let { isInEditor } = env; + if (payload.src) { + $('
' + + '
' + + '
You look nice today.
' + + (isInEditor ? "
" : "") + + '
').appendTo(element); + } else { + $('
' + + 'Hello there!' + + (isInEditor ? "" : "") + + '
').appendTo(element); + } + + if (isInEditor) { + setTimeout(() => { + $('#go-edit').on('click', env.edit); + }); + } + + return element; + }, + + edit({env}) { + let element = $('
')[0]; + $('
' + + '' + + '' + + '' + + '
').appendTo(element); + + setTimeout(() => { + let canvas = document.getElementById("canvas"), + context = canvas.getContext("2d"), + video = document.getElementById("video"), + videoObj = { "video": true }, + errBack = () => alert('error getting video feed'); + + navigator.getMedia = (navigator.getUserMedia || + navigator.webkitGetUserMedia || + navigator.mozGetUserMedia || + navigator.msGetUserMedia); + + navigator.getMedia(videoObj, (stream) => { + let vendorURL; + if (navigator.mozGetUserMedia) { + video.mozSrcObject = stream; + } else { + vendorURL = window.URL || window.webkitURL; + video.src = vendorURL.createObjectURL(stream); + video.play(); + } + + $('#snap').click(() => { + context.drawImage(video, 0, 0, 400, 300); + let src = canvas.toDataURL('image/png'); + env.save({src}); + }); + }, errBack); + }); + + return element; + } + /* + html: { + setup(buffer, options, env, payload) { + buffer.push(`Remove card'); + button.on('click', env.remove); + return element; + } +}; diff --git a/demo/app/mobiledoc-cards/html.js b/demo/app/mobiledoc-cards/html.js new file mode 100644 index 000000000..aa3dfc3fc --- /dev/null +++ b/demo/app/mobiledoc-cards/html.js @@ -0,0 +1,13 @@ +import input from './html/input'; +import selfie from './html/selfie'; +import simple from './html/simple'; +import image from './html/image'; +import codemirror from './html/codemirror'; + +export default [ + input, + selfie, + simple, + image, + codemirror +]; diff --git a/demo/app/mobiledoc-cards/html/codemirror.js b/demo/app/mobiledoc-cards/html/codemirror.js new file mode 100644 index 000000000..68efe7ae3 --- /dev/null +++ b/demo/app/mobiledoc-cards/html/codemirror.js @@ -0,0 +1,9 @@ +export default { + name: 'codemirror-card', + type: 'html', + render({payload}) { + if (payload.code) { + return `${payload.code}`; + } + } +}; diff --git a/demo/app/mobiledoc-cards/html/image.js b/demo/app/mobiledoc-cards/html/image.js new file mode 100644 index 000000000..b36608fc2 --- /dev/null +++ b/demo/app/mobiledoc-cards/html/image.js @@ -0,0 +1,9 @@ +const defaultSrc = 'http://placekitten.com/200/75'; + +export default { + name: 'image-card', + type: 'html', + render({payload}) { + return ``; + } +}; diff --git a/demo/app/mobiledoc-cards/html/input.js b/demo/app/mobiledoc-cards/html/input.js new file mode 100644 index 000000000..69b172ee8 --- /dev/null +++ b/demo/app/mobiledoc-cards/html/input.js @@ -0,0 +1,7 @@ +export default { + name: 'input-card', + type: 'html', + render({payload}) { + return 'Hello, ' + (payload.name || 'unknown') + '!'; + } +}; diff --git a/demo/app/mobiledoc-cards/html/selfie.js b/demo/app/mobiledoc-cards/html/selfie.js new file mode 100644 index 000000000..b942392bc --- /dev/null +++ b/demo/app/mobiledoc-cards/html/selfie.js @@ -0,0 +1,7 @@ +export default { + name: 'selfie-card', + type: 'html', + render: ({env, payload}) => { + return ``; + } +}; diff --git a/demo/app/mobiledoc-cards/html/simple.js b/demo/app/mobiledoc-cards/html/simple.js new file mode 100644 index 000000000..3be25bef8 --- /dev/null +++ b/demo/app/mobiledoc-cards/html/simple.js @@ -0,0 +1,7 @@ +export default { + name: 'simple-card', + type: 'html', + render() { + return 'Hello, world'; + } +}; diff --git a/demo/app/mobiledoc-cards/image.js b/demo/app/mobiledoc-cards/image.js deleted file mode 100644 index 1b15df734..000000000 --- a/demo/app/mobiledoc-cards/image.js +++ /dev/null @@ -1,59 +0,0 @@ -/* global $ */ -const defaultSrc = 'http://placekitten.com/200/75'; - -function displayImage(element, src=defaultSrc) { - let img = $(``); - $(element).append(img); -} - -export default { - name: 'image-card', - display: { - setup(element, options, env, payload) { - displayImage(element, payload.src); - if (env.edit) { - $('').appendTo(element) - .on('click', env.edit); - } - return element; - }, - teardown(element) { - $(element).empty(); - } - }, - edit: { - setup(element, options, env, payload) { - function importImage(event) { - let reader = new FileReader(); - let file = event.target.files[0]; - reader.onloadend = () => { - env.save({src: reader.result}); - }; - reader.readAsDataURL(file); - } - - $('').appendTo(element) - .on('change', importImage); - - $('').appendTo(element) - .on('click', () => { env.save(payload); }); - - return element; - }, - teardown(element) { - $(element).empty(); - } - }, - html: { - setup(buffer, options, env, payload) { - let src = payload.src || defaultSrc; - let html = ``; - buffer.push(html); - } - }, - text: { - setup(/*str, options, env, payload*/) { - return "[image]"; - } - } -}; diff --git a/demo/app/mobiledoc-cards/index.js b/demo/app/mobiledoc-cards/index.js deleted file mode 100644 index 28a9d5888..000000000 --- a/demo/app/mobiledoc-cards/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import inputCard from './input'; -import simpleCard from './simple'; -import selfieCard from './selfie'; -import imageCard from './image'; -import codemirrorCard from './codemirror'; - -export let cardsList = [ - inputCard, - simpleCard, - selfieCard, - imageCard, - codemirrorCard -]; - -let cardsHash = {}; -cardsList.forEach(card => { - cardsHash[card.name] = card; -}); -export { cardsHash }; diff --git a/demo/app/mobiledoc-cards/input.js b/demo/app/mobiledoc-cards/input.js deleted file mode 100644 index b0a5b3bf2..000000000 --- a/demo/app/mobiledoc-cards/input.js +++ /dev/null @@ -1,43 +0,0 @@ -import Ember from 'ember'; - -let { $ } = Ember; - -export default { - name: 'input-card', - display: { - setup(element, options, env, payload) { - $(element).empty(); - - var text = 'I am in display mode'; - if (payload.name) { - text = 'Hello, ' + payload.name + '!'; - } - var card = $(`
${text}
`); - var button = $(''); - button.on('click', env.edit); - - if (env.edit) { - card.append(button); - } - $(element).append(card); - } - }, - edit: { - setup(element, options, env) { - $(element).empty(); - var card = $('
What is your name?
'); - card.innerHTML = 'What is your name?'; - - var input = $(''); - var button = $(''); - button.on('click', () => { - var name = input.val(); - env.save({name}); - }); - - card.append(input); - card.append(button); - $(element).append(card); - } - } -}; diff --git a/demo/app/mobiledoc-cards/selfie.js b/demo/app/mobiledoc-cards/selfie.js deleted file mode 100644 index dc6f5b6d7..000000000 --- a/demo/app/mobiledoc-cards/selfie.js +++ /dev/null @@ -1,92 +0,0 @@ -import Ember from 'ember'; - -let { $ } = Ember; - -export default { - name: 'selfie-card', - display: { - setup(element, options, env, payload) { - $(element).empty(); - - if (payload.src) { - element.appendChild( - $('' + - '
' + - '
' + - '
You look nice today.
' + - (env.edit ? "
" : "") + - '
' + - '')[0] - ); - } else { - element.appendChild($('' + - '
' + - 'Hello there!' + - (env.edit ? "" : "") + - '
')[0] - ); - } - - if (env.edit) { - $('#go-edit').click(function() { - env.edit(); - }); - } - } - }, - edit: { - setup(element, options, env) { - $(element).empty(); - - var vid = $('' + - '
' + - '' + - '' + - '' + - '
' + - ''); - element.appendChild(vid[0]); - - var canvas = document.getElementById("canvas"), - context = canvas.getContext("2d"), - - video = document.getElementById("video"), - videoObj = { "video": true }, - errBack = function() { - alert('error getting video feed'); - }; - - navigator.getMedia = (navigator.getUserMedia || - navigator.webkitGetUserMedia || - navigator.mozGetUserMedia || - navigator.msGetUserMedia); - - navigator.getMedia(videoObj, function(stream) { - var vendorURL; - if (navigator.mozGetUserMedia) { - video.mozSrcObject = stream; - } else { - vendorURL = window.URL || window.webkitURL; - video.src = vendorURL.createObjectURL(stream); - video.play(); - } - - $('#snap').click(function() { - context.drawImage(video, 0, 0, 400, 300); - var src = canvas.toDataURL('image/png'); - env.save({src: src}); - }); - }, errBack); - } - }, - html: { - setup(buffer, options, env, payload) { - buffer.push(`Remove card'); - button.on('click', env.remove); - $(element).append(button); - } - } -}; diff --git a/demo/app/mobiledoc-cards/text.js b/demo/app/mobiledoc-cards/text.js new file mode 100644 index 000000000..ed113e34a --- /dev/null +++ b/demo/app/mobiledoc-cards/text.js @@ -0,0 +1,13 @@ +import codemirrorCard from './text/codemirror'; +import simpleCard from './text/simple'; +import inputCard from './text/input'; +import imageCard from './text/image'; +import selfieCard from './text/selfie'; + +export default [ + codemirrorCard, + simpleCard, + inputCard, + imageCard, + selfieCard +]; diff --git a/demo/app/mobiledoc-cards/text/codemirror.js b/demo/app/mobiledoc-cards/text/codemirror.js new file mode 100644 index 000000000..bf280d407 --- /dev/null +++ b/demo/app/mobiledoc-cards/text/codemirror.js @@ -0,0 +1,9 @@ +export default { + name: 'codemirror-card', + type: 'text', + render({payload}) { + if (payload.code) { + return `[code] ${payload.code}`; + } + } +}; diff --git a/demo/app/mobiledoc-cards/text/image.js b/demo/app/mobiledoc-cards/text/image.js new file mode 100644 index 000000000..e968fd2c0 --- /dev/null +++ b/demo/app/mobiledoc-cards/text/image.js @@ -0,0 +1,7 @@ +export default { + name: 'image-card', + type: 'text', + render() { + return `[image]`; + } +}; diff --git a/demo/app/mobiledoc-cards/text/input.js b/demo/app/mobiledoc-cards/text/input.js new file mode 100644 index 000000000..c95edd6d2 --- /dev/null +++ b/demo/app/mobiledoc-cards/text/input.js @@ -0,0 +1,7 @@ +export default { + name: 'input-card', + type: 'text', + render({payload}) { + return 'Hello, ' + (payload.name || 'unknown') + '!'; + } +}; diff --git a/demo/app/mobiledoc-cards/text/selfie.js b/demo/app/mobiledoc-cards/text/selfie.js new file mode 100644 index 000000000..7517edb2d --- /dev/null +++ b/demo/app/mobiledoc-cards/text/selfie.js @@ -0,0 +1,7 @@ +export default { + name: 'selfie-card', + type: 'text', + render() { + return '[ :) ]'; + } +}; diff --git a/demo/app/mobiledoc-cards/text/simple.js b/demo/app/mobiledoc-cards/text/simple.js new file mode 100644 index 000000000..66229e985 --- /dev/null +++ b/demo/app/mobiledoc-cards/text/simple.js @@ -0,0 +1,7 @@ +export default { + name: 'simple-card', + type: 'text', + render() { + return 'Hello, world'; + } +}; diff --git a/demo/bower.json b/demo/bower.json index dcd7cb49f..570eee177 100644 --- a/demo/bower.json +++ b/demo/bower.json @@ -1,16 +1,16 @@ { "name": "mobiledoc-kit-demo", "dependencies": { - "ember": "1.13.7", - "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", - "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", - "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.5", - "ember-qunit": "0.4.9", + "ember": "^2.2.0", + "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.6", + "ember-cli-test-loader": "ember-cli-test-loader#0.2.1", + "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.7", + "ember-qunit": "^0.4.16", "ember-qunit-notifications": "0.0.7", "ember-resolver": "~0.1.18", - "jquery": "^1.11.3", - "loader.js": "ember-cli/loader.js#3.2.1", - "qunit": "~1.18.0", + "jquery": "~2.1.1", + "loader.js": "ember-cli/loader.js#3.3.0", + "qunit": "~1.20.0", "codemirror": "~5.8.0" } } diff --git a/demo/package.json b/demo/package.json index 99286a5d3..d43e64aa0 100644 --- a/demo/package.json +++ b/demo/package.json @@ -35,8 +35,8 @@ "ember-mobiledoc-editor": "0.2.0-beta1", "ember-disable-proxy-controllers": "^1.0.0", "ember-export-application-global": "^1.0.3", - "ember-mobiledoc-dom-renderer": "^0.1.1", - "ember-mobiledoc-html-renderer": "^0.1.0", - "ember-mobiledoc-text-renderer": "^0.1.0" + "ember-mobiledoc-dom-renderer": "^0.2.1", + "ember-mobiledoc-html-renderer": "^0.2.0", + "ember-mobiledoc-text-renderer": "^0.2.0" } } diff --git a/src/js/cards/image.js b/src/js/cards/image.js index a9bcf0daf..7244d26d1 100644 --- a/src/js/cards/image.js +++ b/src/js/cards/image.js @@ -2,16 +2,11 @@ import placeholderImageSrc from 'mobiledoc-kit/utils/placeholder-image-src'; export default { name: 'image', + type: 'dom', - display: { - setup(element, options, env, payload) { - let img = document.createElement('img'); - img.src = payload.src || placeholderImageSrc; - element.appendChild(img); - return img; - }, - teardown(element) { - element.parentNode.removeChild(element); - } + render({env, options, payload}) { + let img = document.createElement('img'); + img.src = payload.src || placeholderImageSrc; + return img; } }; diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index 93fe61f1e..bdec7b9c3 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -48,7 +48,7 @@ const defaults = { autofocus: true, cards: [], cardOptions: {}, - unknownCardHandler: (element, options, env) => { + unknownCardHandler: ({env}) => { throw new Error(`Unknown card encountered: ${env.name}`); }, mobiledoc: null, @@ -378,6 +378,7 @@ class Editor { this._isDestroyed = true; this.removeAllEventListeners(); this.removeAllViews(); + this._renderer.destroy(); } /** diff --git a/src/js/models/card-node.js b/src/js/models/card-node.js index 4882a2eeb..da729f0fc 100644 --- a/src/js/models/card-node.js +++ b/src/js/models/card-node.js @@ -1,13 +1,17 @@ +import assert from '../utils/assert'; + export default class CardNode { - constructor(editor, card, section, element, cardOptions) { - this.editor = editor; - this.card = card; + constructor(editor, card, section, element, options) { + this.editor = editor; + this.card = card; this.section = section; - this.cardOptions = cardOptions; this.element = element; + this.options = options; this.mode = null; - this.setupResult = null; + + this._teardownCallback = null; + this._rendered = null; } render(mode) { @@ -16,17 +20,34 @@ export default class CardNode { this.teardown(); this.mode = mode; - this.setupResult = this.card[mode].setup( - this.element, - this.cardOptions, - this.env, - this.section.payload - ); + + let method = mode === 'display' ? 'render' : 'edit'; + + let rendered = this.card[method]({ + env: this.env, + options: this.options, + payload: this.section.payload + }); + + this._validateAndAppendRenderResult(rendered); + } + + teardown() { + if (this._teardownCallback) { + this._teardownCallback(); + this._teardownCallback = null; + } + if (this._rendered) { + this.element.removeChild(this._rendered); + this._rendered = null; + } } get env() { return { name: this.card.name, + isInEditor: true, + onTeardown: (callback) => this._teardownCallback = callback, edit: () => this.edit(), save: (payload, transition=true) => { this.section.payload = payload; @@ -38,7 +59,7 @@ export default class CardNode { }, cancel: () => this.display(), remove: () => this.remove(), - section: this.section + postModel: this.section }; } @@ -54,11 +75,17 @@ export default class CardNode { this.editor.run(postEditor => postEditor.removeSection(this.section)); } - teardown() { - if (this.mode) { - if (this.card[this.mode].teardown) { - this.card[this.mode].teardown(this.setupResult); - } + _validateAndAppendRenderResult(rendered) { + if (!rendered) { + return; } + + let { card: { name } } = this; + assert( + `Card "${name}" must render dom (render value was: "${rendered}")`, + !!rendered.nodeType + ); + this.element.appendChild(rendered); + this._rendered = rendered; } } diff --git a/src/js/renderers/editor-dom.js b/src/js/renderers/editor-dom.js index 63bb912d7..c2e82c582 100644 --- a/src/js/renderers/editor-dom.js +++ b/src/js/renderers/editor-dom.js @@ -1,5 +1,5 @@ import CardNode from 'mobiledoc-kit/models/card-node'; -import { detect } from 'mobiledoc-kit/utils/array-utils'; +import { detect, forEach } from 'mobiledoc-kit/utils/array-utils'; import { POST_TYPE, MARKUP_SECTION_TYPE, @@ -12,6 +12,7 @@ import { import { startsWith, endsWith } from '../utils/string-utils'; import { addClassName } from '../utils/dom-utils'; import { MARKUP_SECTION_ELEMENT_NAMES } from '../models/markup-section'; +import assert from '../utils/assert'; export const NO_BREAK_SPACE = '\u00A0'; export const SPACE = ' '; @@ -155,19 +156,52 @@ function removeRenderNodeSectionFromParent(renderNode, section) { } function removeRenderNodeElementFromParent(renderNode) { - if (renderNode.element.parentNode) { + if (renderNode.element && renderNode.element.parentNode) { renderNode.element.parentNode.removeChild(renderNode.element); } } +function validateCards(cards=[]) { + forEach(cards, card => { + assert( + `Card "${card.name}" must define type "dom", has: "${card.type}"`, + card.type === 'dom' + ); + assert( + `Card "${card.name}" must define \`render\` method`, + !!card.render + ); + }); + return cards; +} + class Visitor { constructor(editor, cards, unknownCardHandler, options) { this.editor = editor; - this.cards = cards; + this.cards = validateCards(cards); this.unknownCardHandler = unknownCardHandler; this.options = options; } + _findCard(cardName) { + let card = detect(this.cards, card => card.name === cardName); + return card || this._createUnknownCard(cardName); + } + + _createUnknownCard(cardName) { + assert( + `Unknown card "${cardName}" found, but no unknownCardHandler is defined`, + !!this.unknownCardHandler + ); + + return { + name: cardName, + type: 'dom', + render: this.unknownCardHandler, + edit: this.unknownCardHandler + }; + } + [POST_TYPE](renderNode, post, visit) { if (!renderNode.element) { renderNode.element = document.createElement('div'); @@ -251,23 +285,19 @@ class Visitor { [CARD_TYPE](renderNode, section) { const originalElement = renderNode.element; const {editor, options} = this; - const card = detect(this.cards, card => card.name === section.name); + + const card = this._findCard(section.name); let { wrapper, cardElement } = renderCard(); renderNode.element = wrapper; attachRenderNodeElementToDOM(renderNode, originalElement); - if (card) { - const cardNode = new CardNode( - editor, card, section, cardElement, options); - renderNode.cardNode = cardNode; - const initialMode = section._initialMode; - cardNode[initialMode](); - } else { - const env = { name: section.name }; - this.unknownCardHandler( - cardElement, options, env, section.payload); - } + const cardNode = new CardNode( + editor, card, section, cardElement, options); + renderNode.cardNode = cardNode; + + const initialMode = section._initialMode; + cardNode[initialMode](); } } @@ -363,6 +393,12 @@ export default class Renderer { this.nodes = []; } + destroy() { + let renderNode = this.renderTree.rootNode; + let force = true; + removeDestroyedChildren(renderNode, force); + } + visit(renderTree, parentNode, postNodes, visitAll=false) { let previousNode; postNodes.forEach(postNode => { @@ -375,6 +411,7 @@ export default class Renderer { } render(renderTree) { + this.renderTree = renderTree; let renderNode = renderTree.rootNode; let method, postNode; diff --git a/tests/acceptance/basic-editor-test.js b/tests/acceptance/basic-editor-test.js index d4c4f2a0d..a90145fdb 100644 --- a/tests/acceptance/basic-editor-test.js +++ b/tests/acceptance/basic-editor-test.js @@ -5,14 +5,9 @@ const { test, module } = Helpers; const cards = [{ name: 'my-card', - display: { - setup() {}, - teardown() {} - }, - edit: { - setup() {}, - teardown() {} - } + type: 'dom', + render() {}, + edit() {} }]; let editor, editorElement; diff --git a/tests/acceptance/cursor-movement-test.js b/tests/acceptance/cursor-movement-test.js index d135d05c3..7cf5d7027 100644 --- a/tests/acceptance/cursor-movement-test.js +++ b/tests/acceptance/cursor-movement-test.js @@ -6,14 +6,9 @@ const { test, module } = Helpers; const cards = [{ name: 'my-card', - display: { - setup() {}, - teardown() {} - }, - edit: { - setup() {}, - teardown() {} - } + type: 'dom', + render() {}, + edit() {} }]; let editor, editorElement; diff --git a/tests/acceptance/cursor-position-test.js b/tests/acceptance/cursor-position-test.js index ed2e91c74..37f09f3d3 100644 --- a/tests/acceptance/cursor-position-test.js +++ b/tests/acceptance/cursor-position-test.js @@ -5,14 +5,9 @@ const { test, module } = Helpers; const cards = [{ name: 'my-card', - display: { - setup() {}, - teardown() {} - }, - edit: { - setup() {}, - teardown() {} - } + type: 'dom', + render() {}, + edit() {} }]; let editor, editorElement; diff --git a/tests/acceptance/editor-cards-test.js b/tests/acceptance/editor-cards-test.js index dd4628c66..25a723a18 100644 --- a/tests/acceptance/editor-cards-test.js +++ b/tests/acceptance/editor-cards-test.js @@ -15,30 +15,31 @@ const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { const simpleCard = { name: 'simple-card', - display: { - setup(element, options, env) { - let button = document.createElement('button'); - button.setAttribute('id', 'display-button'); - element.appendChild(button); - element.appendChild(document.createTextNode(cardText)); - button.onclick = env.edit; - return {button}; - }, - teardown({button}) { - button.parentNode.removeChild(button); - } + type: 'dom', + render({env}) { + let element = document.createElement('div'); + + let button = document.createElement('button'); + button.setAttribute('id', 'display-button'); + element.appendChild(button); + element.appendChild(document.createTextNode(cardText)); + button.onclick = env.edit; + + return element; }, - edit: { - setup(element, options, env) { - let button = document.createElement('button'); - button.setAttribute('id', 'edit-button'); - button.onclick = env.save; - element.appendChild(button); - return {button}; - }, - teardown({button}) { - button.parentNode.removeChild(button); - } + edit({env}) { + let button = document.createElement('button'); + button.setAttribute('id', 'edit-button'); + button.onclick = env.save; + return button; + } +}; + +const positionCard = { + name: 'simple-card', + type: 'dom', + render() { + return $('
')[0]; } }; @@ -98,12 +99,11 @@ test('removing last card from mobiledoc allows additional editing', (assert) => let button; const cards = [{ name: 'simple-card', - display: { - setup(element, options, env) { - button = $(''); - button.on('click', env.remove); - $(element).append(button); - } + type: 'dom', + render({env}) { + button = $(''); + button.on('click', env.remove); + return button[0]; } }]; editor = new Editor({mobiledoc, cards}); @@ -126,18 +126,10 @@ test('removing last card from mobiledoc allows additional editing', (assert) => test('delete when cursor is positioned at end of a card deletes card, replace with empty markup section', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([cardSection('simple-card')]); + return post([cardSection(positionCard.name)]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -154,19 +146,11 @@ test('delete when cursor is at start of a card and prev section is blank deletes const mobiledoc = Helpers.mobiledoc.build(({post, cardSection, markupSection}) => { return post([ markupSection('p'), - cardSection('simple-card') + cardSection(positionCard.name) ]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -181,18 +165,10 @@ test('delete when cursor is at start of a card and prev section is blank deletes test('forward-delete when cursor is positioned at start of a card deletes card, replace with empty markup section', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([cardSection('simple-card')]); + return post([cardSection(positionCard.name)]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -208,20 +184,12 @@ test('forward-delete when cursor is positioned at start of a card deletes card, test('forward-delete when cursor is positioned at end of a card and next section is blank deletes next section', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection, markupSection}) => { return post([ - cardSection('simple-card'), + cardSection(positionCard.name), markupSection() ]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -236,21 +204,10 @@ test('forward-delete when cursor is positioned at end of a card and next section test('selecting a card and deleting deletes the card', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([ - cardSection('simple-card') - ]); + return post([cardSection(positionCard.name)]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -266,21 +223,12 @@ test('selecting a card and deleting deletes the card', (assert) => { test('selecting a card and some text after and deleting deletes card and text', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection, markupSection, marker}) => { return post([ - cardSection('simple-card'), + cardSection(positionCard.name), markupSection('p', [marker('abc')]) ]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -299,21 +247,12 @@ test('selecting a card and some text after and deleting deletes card and text', test('deleting at start of empty markup section with prev card deletes the markup section', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection, markupSection}) => { return post([ - cardSection('simple-card'), + cardSection(positionCard.name), markupSection('p') ]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -334,21 +273,10 @@ test('deleting at start of empty markup section with prev card deletes the marku test('press enter at end of card inserts section after card', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([ - cardSection('simple-card') - ]); + return post([cardSection(positionCard.name)]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -371,21 +299,10 @@ test('press enter at end of card inserts section after card', (assert) => { test('press enter at start of card inserts section before card', (assert) => { const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([ - cardSection('simple-card') - ]); + return post([cardSection(positionCard.name)]); }); - const cards = [{ - name: 'simple-card', - display: { - setup(element) { - element.id = 'my-simple-card'; - } - } - }]; - - editor = new Editor({mobiledoc, cards}); + editor = new Editor({mobiledoc, cards: [positionCard]}); editor.render(editorElement); assert.hasElement('#my-simple-card', 'precond - renders card'); @@ -418,10 +335,9 @@ test('editor ignores events when focus is inside a card', (assert) => { const cards = [{ name: 'simple-card', - display: { - setup(element) { - $(element).append(''); - } + type: 'dom', + render() { + return $('')[0]; } }]; diff --git a/tests/acceptance/editor-copy-paste-test.js b/tests/acceptance/editor-copy-paste-test.js index 40126287b..e530f4aa4 100644 --- a/tests/acceptance/editor-copy-paste-test.js +++ b/tests/acceptance/editor-copy-paste-test.js @@ -5,14 +5,9 @@ const { test, module } = Helpers; const cards = [{ name: 'my-card', - display: { - setup() {}, - teardown() {} - }, - edit: { - setup() {}, - teardown() {} - } + type: 'dom', + render() {}, + edit() {} }]; let editor, editorElement; @@ -168,10 +163,9 @@ test('copy-paste can copy cards', (assert) => { }); let cards = [{ name: 'test-card', - display: { - setup(element, options, env, payload) { - $(element).append(`
${payload.foo}
`); - } + type: 'dom', + render({payload}) { + return $(`
${payload.foo}
`)[0]; } }]; editor = new Editor({mobiledoc, cards}); diff --git a/tests/acceptance/editor-post-editor-test.js b/tests/acceptance/editor-post-editor-test.js index 51b6783c7..80a49190f 100644 --- a/tests/acceptance/editor-post-editor-test.js +++ b/tests/acceptance/editor-post-editor-test.js @@ -86,14 +86,12 @@ test('#insertSection can insert card, render it in display mode', (assert) => { return post([markupSection('p', [marker('abc')])]); }); - let displayedCard = false; + let displayedCard, editedCard; let cards = [{ name: 'sample-card', - display: { - setup() { - displayedCard = true; - } - } + type: 'dom', + render() { displayedCard = true; }, + edit() { editedCard = true; } }]; editor = new Editor({mobiledoc, cards}); @@ -112,20 +110,12 @@ test('#insertSection inserts card, can render it in edit mode using #editCard', return post([markupSection('p', [marker('abc')])]); }); - let displayedCard = false, - editCard = false; + let displayedCard, editedCard; let cards = [{ name: 'sample-card', - display: { - setup() { - displayedCard = true; - } - }, - edit: { - setup() { - editCard = true; - } - } + type: 'dom', + render() { displayedCard = true; }, + edit() { editedCard = true; } }]; editor = new Editor({mobiledoc, cards}); @@ -137,7 +127,7 @@ test('#insertSection inserts card, can render it in edit mode using #editCard', editor.editCard(cardSection); }); - assert.ok(editCard, 'rendered card in edit mode'); + assert.ok(editedCard, 'rendered card in edit mode'); assert.ok(!displayedCard, 'did not render in display mode'); }); @@ -146,20 +136,12 @@ test('after inserting a section, can use editor#editCard to switch it to edit mo return post([cardSection('sample-card')]); }); - let displayedCard = false, - editedCard = false; + let displayedCard, editedCard; let cards = [{ name: 'sample-card', - display: { - setup() { - displayedCard = true; - } - }, - edit: { - setup() { - editedCard = true; - } - } + type: 'dom', + render() { displayedCard = true; }, + edit() { editedCard = true; } }]; editor = new Editor({mobiledoc, cards}); @@ -180,21 +162,12 @@ test('can call editor#displayCard to swtich card into display mode', (assert) => return post([cardSection('sample-card')]); }); - let displayedCard = false, - editedCard = false; - + let displayedCard, editedCard; let cards = [{ name: 'sample-card', - display: { - setup() { - displayedCard = true; - } - }, - edit: { - setup() { - editedCard = true; - } - } + type: 'dom', + render() { displayedCard = true; }, + edit() { editedCard = true; } }]; editor = new Editor({mobiledoc, cards}); diff --git a/tests/acceptance/editor-selections-test.js b/tests/acceptance/editor-selections-test.js index 09c78bd7d..3e2555666 100644 --- a/tests/acceptance/editor-selections-test.js +++ b/tests/acceptance/editor-selections-test.js @@ -402,12 +402,9 @@ test('selecting text that includes a card section and deleting deletes card sect ); const cards = [{ name: 'simple-card', - display: { - setup(element) { - const span = document.createElement('span'); - span.setAttribute('id', 'card-el'); - element.appendChild(span); - } + type: 'dom', + render() { + return $('')[0]; } }]; editor = new Editor({mobiledoc, cards}); diff --git a/tests/unit/editor/card-lifecycle-test.js b/tests/unit/editor/card-lifecycle-test.js index c1ced83ca..aa236cbf7 100644 --- a/tests/unit/editor/card-lifecycle-test.js +++ b/tests/unit/editor/card-lifecycle-test.js @@ -1,6 +1,5 @@ import Helpers from '../../test-helpers'; import { Editor } from 'mobiledoc-kit'; -import { containsNode } from 'mobiledoc-kit/utils/dom-utils'; let editorElement, editor; const { module, test } = Helpers; @@ -17,25 +16,48 @@ module('Unit: Editor: Card Lifecycle', { } }); -test('rendering a mobiledoc for editing calls card#setup', (assert) => { - assert.expect(4); +function makeEl(id) { + let el = document.createElement('div'); + el.id = id; + return el; +} + +function assertRenderArguments(assert, args, expected) { + let {env, options, payload} = args; + + assert.deepEqual(payload, expected.payload, 'correct payload'); + assert.deepEqual(options, expected.options, 'correct options'); + + // basic env + let {name, isInEditor, onTeardown} = env; + assert.equal(name, expected.name, 'correct name'); + assert.equal(isInEditor, expected.isInEditor, 'correct isInEditor'); + assert.ok(!!onTeardown, 'has onTeardown'); + + // editor env hooks + let {save, cancel, edit, remove} = env; + assert.ok(!!save && !!cancel && !!edit && !!remove, + 'has save, cancel, edit, remove hooks'); + + // postModel + let {postModel} = env; + assert.ok(postModel && postModel === expected.postModel, + 'correct postModel'); +} + +test('rendering a mobiledoc with card calls card#render', (assert) => { const payload = { foo: 'bar' }; const cardOptions = { boo: 'baz' }; + const cardName = 'test-card'; + + let renderArg; const card = { - name: 'test-card', - display: { - setup(element, options, env, setupPayload) { - assert.ok(containsNode(editorElement, element), - 'card element is part of the editor element'); - assert.deepEqual(setupPayload, payload, - 'the payload is passed to the card'); - assert.equal(env.name, 'test-card', - 'env.name is correct'); - assert.deepEqual(options, cardOptions, 'correct cardOptions'); - }, - teardown() {} + name: cardName, + type: 'dom', + render(_renderArg) { + renderArg = _renderArg; } }; @@ -44,249 +66,523 @@ test('rendering a mobiledoc for editing calls card#setup', (assert) => { ); editor = new Editor({mobiledoc, cards: [card], cardOptions}); editor.render(editorElement); -}); -test('rendered card env has `name`, `edit`, `save`, `remove`, `section', (assert) => { - let cardEnv; + let expected = { + name: cardName, + payload, + options: cardOptions, + isInEditor: true, + postModel: editor.post.sections.head + }; + assertRenderArguments(assert, renderArg, expected); +}); +test('rendering a mobiledoc with card appends result of card#render', (assert) => { const cardName = 'test-card'; - const cards = [{ + + const card = { name: cardName, - display: { - setup(element, options, env) { cardEnv = env; }, - teardown() {} + type: 'dom', + render() { + return makeEl('the-card'); } - }]; + }; - const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => { - return post([cardSection('test-card')]); - }); - editor = new Editor({mobiledoc, cards}); + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + editor = new Editor({mobiledoc, cards: [card]}); + assert.hasNoElement('#editor #the-card', 'precond - card not rendered'); editor.render(editorElement); + assert.hasElement('#editor #the-card'); +}); + +test('returning wrong type from render throws', (assert) => { + const cardName = 'test-card'; + + const card = { + name: cardName, + type: 'dom', + render() { + return 'string'; + } + }; - assert.ok(!!cardEnv, 'card env is present'); - assert.equal(cardEnv.name, cardName, 'env name is correct'); - assert.ok(!!cardEnv.edit, 'has edit hook'); - assert.ok(!!cardEnv.save, 'has save hook'); - assert.ok(!!cardEnv.cancel, 'has cancel hook'); - assert.ok(!!cardEnv.remove, 'has remove hook'); - const cardSection = editor.post.sections.head; - assert.ok(cardEnv.section && cardEnv.section === cardSection, 'has `section`'); + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + editor = new Editor({mobiledoc, cards: [card]}); + + assert.throws(() => { + editor.render(editorElement); + }, new RegExp(`Card "${cardName}" must render dom`)); }); -test('rendering a mobiledoc for editing calls #unknownCardHandler when it encounters an unknown card', (assert) => { - assert.expect(1); +test('returning undefined from render is ok', (assert) => { + const cardName = 'test-card'; - const cardName = 'my-card'; + const card = { + name: cardName, + type: 'dom', + render() {} + }; - const unknownCardHandler = (element, options, env /*,setupPayload*/) => { - assert.equal(env.name, cardName, 'includes card name in env'); + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection('test-card')]) + ); + editor = new Editor({mobiledoc, cards: [card]}); + editor.render(editorElement); + assert.ok(true, 'no errors are thrown'); +}); + +test('returning undefined from render is ok', (assert) => { + const cardName = 'test-card'; + let currentMode; + let editHook; + + const card = { + name: cardName, + type: 'dom', + render({env}) { + currentMode = 'display'; + editHook = env.edit; + }, + edit() { + currentMode = 'edit'; + } }; const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => post([cardSection(cardName)]) ); - - editor = new Editor({mobiledoc, unknownCardHandler}); + editor = new Editor({mobiledoc, cards: [card]}); editor.render(editorElement); + + assert.equal(currentMode, 'display', 'precond - display'); + editHook(); + assert.equal(currentMode, 'edit', 'edit mode, no errors when returning undefined'); +}); + +test('rendering card with wrong type throws', (assert) => { + const cardName = 'test-card'; + const card = { + name: cardName, + type: 'other', + render() {} + }; + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + + assert.throws(() => { + editor = new Editor({mobiledoc, cards: [card]}); + editor.render(editorElement); + }, new RegExp(`Card "${cardName}.* must define type`)); +}); + +test('rendering card without render method throws', (assert) => { + const cardName = 'test-card'; + const card = { + name: cardName, + type: 'dom' + }; + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + + assert.throws(() => { + editor = new Editor({mobiledoc, cards: [card]}); + editor.render(editorElement); + }, new RegExp(`Card "${cardName}.* must define.*render`)); }); -test('rendered card can fire edit hook to enter editing mode', (assert) => { - assert.expect(7); +test('card can call `env.edit` to render in edit mode', (assert) => { const payload = { foo: 'bar' }; const cardOptions = { boo: 'baz' }; + const cardName = 'test-card'; + + let editArg; + let editHook; + let currentMode; + let displayId = 'the-display-card'; + let editId = 'the-edit-card'; - let returnedSetupValue = {some: 'object'}; - let span; const card = { - name: 'test-card', - display: { - setup(element, options, env/*, setupPayload*/) { - span = document.createElement('span'); - span.onclick = function() { - assert.ok(true, 'precond - click occurred'); - env.edit(); - }; - element.appendChild(span); - return returnedSetupValue; - }, - teardown(passedValue) { - assert.ok(true, 'teardown called'); - assert.equal(passedValue, returnedSetupValue, - 'teardown called with return value of setup'); - } + name: cardName, + type: 'dom', + render(_renderArg) { + currentMode = 'display'; + editHook = _renderArg.env.edit; + return makeEl(displayId); }, - edit: { - setup(element, options, env, setupPayload) { - assert.ok(containsNode(editorElement, element), - 'card element is part of the editor element'); - assert.deepEqual(payload, setupPayload, - 'the payload is passed to the card'); - assert.equal(env.name, 'test-card', - 'env.name is correct'); - assert.deepEqual(options, cardOptions, 'correct cardOptions'); - } + edit(_editArg) { + currentMode = 'edit'; + editArg = _editArg; + return makeEl(editId); } }; const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => - post([cardSection('test-card', payload)]) + post([cardSection(cardName, payload)]) ); + editor = new Editor({mobiledoc, cards: [card], cardOptions}); + editor.render(editorElement); + + assert.hasElement(`#editor #${displayId}`, 'precond - display card'); + assert.hasNoElement(`#editor #${editId}`, 'precond - no edit card'); + assert.equal(currentMode, 'display'); - editor = new Editor({ mobiledoc, cards: [card], cardOptions }); + editHook(); + + assert.equal(currentMode, 'edit'); + assert.hasNoElement(`#editor #${displayId}`, 'no display card'); + assert.hasElement(`#editor #${editId}`, 'renders edit card'); + + let expected = { + name: cardName, + payload, + options: cardOptions, + isInEditor: true, + postModel: editor.post.sections.head + }; + assertRenderArguments(assert, editArg, expected); +}); + + +test('save hook updates payload when in display mode', (assert) => { + const cardName = 'test-card'; + let saveHook; + let postModel; + + const card = { + name: cardName, + type: 'dom', + render({env}) { + saveHook = env.save; + postModel = env.postModel; + } + }; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + editor = new Editor({ mobiledoc, cards: [card] }); editor.render(editorElement); - Helpers.dom.triggerEvent(span, 'click'); + let newPayload = {newPayload: true}; + saveHook(newPayload); + assert.deepEqual(postModel.payload, newPayload, + 'save updates payload when called without transition param'); + + let otherNewPayload = {otherNewPayload: true}; + saveHook(otherNewPayload, false); + assert.deepEqual(postModel.payload, otherNewPayload, + 'save updates payload when called with transition=false'); }); -test('rendered card can fire edit hook to enter editing mode, then save', (assert) => { - const setupPayloads = []; - const payload = { foo: 'bar' }; - const newPayload = {some: 'new values'}; - let cardEnv; + +test('save hook updates payload when in edit mode', (assert) => { + const cardName = 'test-card'; + let saveHook; + let editHook; + let postModel; + let currentMode; const card = { - name: 'test-card', - display: { - setup(element, options, env, setupPayload) { - cardEnv = env; - setupPayloads.push(setupPayload); - } + name: cardName, + type: 'dom', + render({env}) { + currentMode = 'display'; + editHook = env.edit; + postModel = env.postModel; }, - edit: { - setup() {} + edit({env}) { + currentMode = 'edit'; + saveHook = env.save; + postModel = env.postModel; } }; const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => - post([cardSection('test-card', payload)]) + post([cardSection(cardName)]) ); editor = new Editor({ mobiledoc, cards: [card] }); editor.render(editorElement); - assert.ok(cardEnv.edit, 'precond - card env has edit hook'); - assert.ok(cardEnv.save, 'precond - card env has save hook'); + assert.equal(currentMode, 'display', 'precond - display mode'); - cardEnv.edit(); - cardEnv.save(newPayload); + editHook(); - const [firstPayload, secondPayload] = setupPayloads; - assert.equal(firstPayload, payload, 'first display with mobiledoc payload'); - assert.equal(secondPayload, newPayload, 'second display with new payload'); + assert.equal(currentMode, 'edit', 'precond - edit mode'); + let newPayload = {newPayload: true}; + saveHook(newPayload, false); + + assert.equal(currentMode, 'edit', 'save with false does not transition'); + assert.deepEqual(postModel.payload, newPayload, 'updates payload'); + + let otherNewPayload = {otherNewPayload: true}; + saveHook(otherNewPayload); + assert.equal(currentMode, 'display', 'save hook transitions'); + assert.deepEqual(postModel.payload, otherNewPayload, 'updates payload'); }); -test('rendered card can fire edit hook to enter editing mode, then silently save', (assert) => { - const setupPayloads = []; - const payload = { foo: 'bar' }; - const newPayload = {some: 'new values'}; - let cardEnv; + +test('#cancel hook changes from edit->display, does not change payload', (assert) => { + const cardName = 'test-card'; + let cancelHook; + let editHook; + let postModel; + let currentMode; + let currentPayload; + let originalPayload = {foo: 'bar'}; const card = { - name: 'test-card', - display: { - setup() { - assert.ok(false, 'card should never be displayed in this test'); - } + name: cardName, + type: 'dom', + render({env, payload}) { + currentMode = 'display'; + editHook = env.edit; + postModel = env.postModel; + currentPayload = payload; }, - edit: { - setup(element, options, env, setupPayload) { - cardEnv = env; - setupPayloads.push(setupPayload); - } + edit({env}) { + currentMode = 'edit'; + cancelHook = env.cancel; + postModel = env.postModel; + } + }; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName, originalPayload)]) + ); + editor = new Editor({ mobiledoc, cards: [card] }); + editor.render(editorElement); + + assert.equal(currentMode, 'display', 'precond - display mode'); + + editHook(); + + assert.equal(currentMode, 'edit', 'precond - edit mode'); + + cancelHook(); + + assert.equal(currentMode, 'display', 'cancel hook transitions'); + assert.deepEqual(currentPayload, originalPayload, 'payload is the same'); +}); + + +test('#remove hook destroys card when in display mode, removes it from DOM and AT', (assert) => { + const cardName = 'test-card'; + let removeHook; + let elId = 'the-card'; + + const card = { + name: cardName, + type: 'dom', + render({env}) { + removeHook = env.remove; + return makeEl(elId); } }; const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => - post([ cardSection('test-card', payload) ]) + post([cardSection(cardName)]) ); editor = new Editor({ mobiledoc, cards: [card] }); - editor.editCard(editor.post.sections.head); editor.render(editorElement); - cardEnv.save(newPayload, false); + assert.hasElement(`#editor #${elId}`, 'precond - renders card'); + assert.ok(!!editor.post.sections.head, 'post has head section'); + + removeHook(); - const [firstPayload] = setupPayloads; - assert.equal(firstPayload, payload, 'first display with mobiledoc payload'); - let secondPayload = editor.post.sections.head.payload; - assert.equal(secondPayload, newPayload, 'second display with new payload'); + assert.hasNoElement(`#editor #${elId}`, 'removes rendered card'); + assert.ok(!editor.post.sections.head, 'post has no head section'); }); -test('rendered card can fire edit hook to enter editing mode, then cancel', (assert) => { - const setupPayloads = []; - let cardEnv; + +test('#remove hook destroys card when in edit mode, removes it from DOM and AT', (assert) => { + const cardName = 'test-card'; + let removeHook; + let editHook; + let currentMode; + let displayId = 'the-display-card'; + let editId = 'the-edit-card'; const card = { - name: 'test-card', - display: { - setup(element, options, env, setupPayload) { - setupPayloads.push(setupPayload); - cardEnv = env; - } + name: cardName, + type: 'dom', + render({env}) { + currentMode = 'display'; + editHook = env.edit; + return makeEl(displayId); }, - edit: { - setup() {} + edit({env}) { + currentMode = 'edit'; + removeHook = env.remove; + return makeEl(editId); } }; - const payload = { foo: 'bar' }; const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => - post([cardSection('test-card',payload)]) + post([cardSection(cardName)]) ); editor = new Editor({ mobiledoc, cards: [card] }); editor.render(editorElement); - assert.ok(cardEnv.edit, 'precond - env has #edit'); - assert.ok(cardEnv.cancel, 'precond - env has #cancel'); + assert.equal(currentMode, 'display', 'precond - display mode'); + assert.hasElement(`#editor #${displayId}`, 'precond - renders card in display'); - cardEnv.edit(); - cardEnv.cancel(); + editHook(); - let [firstPayload, secondPayload] = setupPayloads; - assert.equal(firstPayload, payload, 'first display with mobiledoc payload'); - assert.equal(secondPayload, payload, 'second display with mobiledoc payload'); + assert.equal(currentMode, 'edit', 'precond - edit mode'); + + assert.hasElement(`#editor #${editId}`, 'precond - renders card in edit'); + assert.hasNoElement(`#editor #${displayId}`, 'display card is removed'); + assert.ok(!!editor.post.sections.head, 'post has head section'); + + removeHook(); + + assert.hasNoElement(`#editor #${editId}`, 'removes rendered card'); + assert.hasNoElement(`#editor #${displayId}`, 'display card is not present'); + assert.ok(!editor.post.sections.head, 'post has no head section'); }); -test('#remove hook destroys card, removes it from DOM and AT', (assert) => { - let callbacks = []; - const cardEl = document.createElement('div'); - cardEl.setAttribute('id', 'the-card-el'); - let cardEnv; +test('rendering unknown card calls #unknownCardHandler', (assert) => { + const payload = { foo: 'bar' }; + const cardOptions = { boo: 'baz' }; + const cardName = 'test-card'; + + let unknownArg; + const unknownCardHandler = (_unknownArg) => { + unknownArg = _unknownArg; + }; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName, payload)]) + ); + + editor = new Editor({mobiledoc, unknownCardHandler, cardOptions}); + editor.render(editorElement); + + let expected = { + name: cardName, + payload, + options: cardOptions, + isInEditor: true, + postModel: editor.post.sections.head + }; + assertRenderArguments(assert, unknownArg, expected); +}); + +test('rendering unknown card without unknownCardHandler throws', (assert) => { + const cardName = 'test-card'; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + + editor = new Editor({mobiledoc, unknownCardHandler: undefined}); + + assert.throws(() => { + editor.render(editorElement); + }, new RegExp(`Unknown card "${cardName}".*no unknownCardHandler`)); +}); + +test('onTeardown hook is called when moving from display->edit and back', (assert) => { + const cardName = 'test-card'; + + let editHook; + let saveHook; + let currentMode; + let teardown; const card = { - name: 'removable-card', - display: { - setup(element, options, env) { - cardEnv = env; - callbacks.push('setup'); - element.appendChild(cardEl); - }, - teardown() { - callbacks.push('teardown'); - } + name: cardName, + type: 'dom', + render({env}) { + currentMode = 'display'; + editHook = env.edit; + env.onTeardown(() => teardown = 'display'); + }, + edit({env}) { + currentMode = 'edit'; + saveHook = env.save; + env.onTeardown(() => teardown = 'edit'); } }; - const mobiledoc = Helpers.mobiledoc.build( - ({post, markupSection, cardSection}) => - post([markupSection(), cardSection('removable-card')]) + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) ); + editor = new Editor({mobiledoc, cards: [card]}); + editor.render(editorElement); + + assert.equal(currentMode, 'display', 'precond - display mode'); + assert.ok(!teardown, 'no teardown called yet'); + + editHook(); + + assert.equal(currentMode, 'edit', 'edit mode'); + assert.equal(teardown, 'display', 'display onTeardown hook called'); + + saveHook(); + + assert.equal(currentMode, 'display', 'display mode'); + assert.equal(teardown, 'edit', 'edit onTeardown hook called'); +}); + +test('onTeardown hook is called when card removes itself', (assert) => { + const cardName = 'test-card'; + + let removeHook; + let teardown; - editor = new Editor({mobiledoc, cards:[card]}); + const card = { + name: cardName, + type: 'dom', + render({env}) { + removeHook = env.remove; + env.onTeardown(() => teardown = true); + } + }; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + editor = new Editor({mobiledoc, cards: [card]}); editor.render(editorElement); - assert.deepEqual(callbacks, ['setup'], 'setup callback called'); - assert.hasElement('#editor #the-card-el', 'renders card'); - assert.ok(cardEnv.remove, 'card env has #remove hook'); + assert.ok(!teardown, 'nothing torn down yet'); + + removeHook(); + + assert.ok(teardown, 'onTeardown hook called'); +}); + +test('onTeardown hook is called when editor is destroyed', (assert) => { + const cardName = 'test-card'; + + let teardown; + + const card = { + name: cardName, + type: 'dom', + render({env}) { + env.onTeardown(() => teardown = true); + } + }; + + const mobiledoc = Helpers.mobiledoc.build(({post, cardSection}) => + post([cardSection(cardName)]) + ); + editor = new Editor({mobiledoc, cards: [card]}); + editor.render(editorElement); - const post = editor.post; - assert.equal(post.sections.length, 2, 'precond - post has 2 sections'); + assert.ok(!teardown, 'nothing torn down yet'); - cardEnv.remove(); + editor.destroy(); - assert.deepEqual(callbacks, ['setup', 'teardown'], - 'teardown called when removing'); - assert.hasNoElement('#editor #the-card-el', 'removes card element'); - assert.equal(post.sections.length, 1, - 'removes the card section from the post'); + assert.ok(teardown, 'onTeardown hook called'); }); diff --git a/tests/unit/renderers/editor-dom-test.js b/tests/unit/renderers/editor-dom-test.js index 9e8334650..569071894 100644 --- a/tests/unit/renderers/editor-dom-test.js +++ b/tests/unit/renderers/editor-dom-test.js @@ -7,7 +7,7 @@ const { module, test } = Helpers; const ZWNJ = '\u200c'; -const DATA_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs="; +import placeholderImageSrc from 'mobiledoc-kit/utils/placeholder-image-src'; let builder; function render(renderTree, cards=[]) { @@ -16,7 +16,7 @@ function render(renderTree, cards=[]) { return renderer.render(renderTree); } -module("Unit: Renderer: Editor-Dom", { +module('Unit: Renderer: Editor-Dom', { beforeEach() { builder = new PostNodeBuilder(); } @@ -67,7 +67,7 @@ test("renders a dirty post with un-rendered sections", (assert) => { }, { name: 'image', - section: (builder) => builder.createImageSection(DATA_URL) + section: (builder) => builder.createImageSection(placeholderImageSrc) }, { name: 'card', @@ -162,7 +162,7 @@ test('renders a post with multiple markers', (assert) => { test('renders a post with image', (assert) => { - let url = DATA_URL; + let url = placeholderImageSrc; let post = builder.createPost(); let section = builder.createImageSection(url); post.sections.append(section); @@ -177,10 +177,9 @@ test('renders a card section', (assert) => { let cardSection = builder.createCardSection('my-card'); let card = { name: 'my-card', - display: { - setup(element) { - element.innerHTML = 'I am a card'; - } + type: 'dom', + render() { + return document.createTextNode('I am a card'); } }; post.sections.append(cardSection); @@ -517,19 +516,18 @@ test('includes card sections in renderTree element map', (assert) => { ); const cards = [{ name: 'simple-card', - display: { - setup(element) { - element.setAttribute('id', 'simple-card'); - } + type: 'dom', + render() { + return $('
')[0]; } }]; const renderTree = new RenderTree(post); render(renderTree, cards); - $('#qunit-fixture')[0].appendChild(renderTree.rootElement); + $('#qunit-fixture').append(renderTree.rootElement); - const element = $('#simple-card').parent()[0]; + const element = $('#simple-card')[0].parentNode.parentNode; assert.ok(!!element, 'precond - simple card is rendered'); assert.ok(!!renderTree.getElementRenderNode(element), 'has render node for card element');