diff --git a/.gitignore b/.gitignore
index 9ad0bd2d1..d3dc6f386 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,7 @@ coverage
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
+.env
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
diff --git a/README.md b/README.md
index 37813df0a..96266bdf9 100644
--- a/README.md
+++ b/README.md
@@ -125,3 +125,11 @@ export EMBEDLY_KEY=XXXXXX
Also, set the `bucketName` in `server/config.json` with the name of your AWS
S3 bucket for uploading files.
+
+Then to boot the server:
+
+```
+node server/index.js
+```
+
+And visit http://localhost:5000/dist/demo/index.html
diff --git a/package.json b/package.json
index c6c4c6f64..26d08573c 100644
--- a/package.json
+++ b/package.json
@@ -45,8 +45,8 @@
"broccoli-test-builder": "^0.1.0",
"content-kit-utils": "^0.2.0",
"jquery": "^2.1.4",
- "mobiledoc-dom-renderer": "^0.1.3",
- "mobiledoc-html-renderer": "^0.1.0",
+ "mobiledoc-dom-renderer": "^0.1.4",
+ "mobiledoc-html-renderer": "^0.1.1",
"testem": "^0.8.4"
}
}
diff --git a/server/config.json b/server/config.json
index 2ebd149aa..3c7e6d010 100644
--- a/server/config.json
+++ b/server/config.json
@@ -1,5 +1,5 @@
-{
+{
"s3" : {
- "bucketName" : "content-kit"
+ "bucketName" : "201-bustle-demo"
}
-}
\ No newline at end of file
+}
diff --git a/server/index.js b/server/index.js
index 4547b4e6e..73e1e5989 100644
--- a/server/index.js
+++ b/server/index.js
@@ -4,7 +4,6 @@ var EmbedService = require('./services/embed');
// Express app
var app = express();
-app.use(express.static('demo'));
app.use('/dist', express.static('dist'));
// Enable cors
diff --git a/src/js/commands/image.js b/src/js/commands/image.js
index 1f0a5e553..e3b46abb0 100644
--- a/src/js/commands/image.js
+++ b/src/js/commands/image.js
@@ -2,37 +2,12 @@ import Command from './base';
import Message from '../views/message';
import { inherit } from 'content-kit-utils';
import { FileUploader } from '../utils/http-utils';
+import { generateBuilder } from '../utils/post-builder';
-function createFileInput(command) {
- var fileInput = document.createElement('input');
- fileInput.type = 'file';
- fileInput.accept = 'image/*';
- fileInput.className = 'ck-file-input';
- // FIXME should this listener be torn down when the ImageCommand is not active?
- fileInput.addEventListener('change', function(e) {
- command.handleFile(e);
- });
- return fileInput;
-}
-
-function injectImageBlock(/* src, editor, index */) {
- throw new Error('Unimplemented: BlockModel and Type.IMAGE are no longer things');
- /*
- var imageModel = BlockModel.createWithType(Type.IMAGE, { attributes: { src: src } });
- editor.replaceBlock(imageModel, index);
- */
-}
-
-function renderFromFile(file, editor, index) {
- if (file && window.FileReader) {
- var reader = new FileReader();
- reader.onload = function(e) {
- var base64Src = e.target.result;
- injectImageBlock(base64Src, editor, index);
- editor.renderBlockAt(index, true);
- };
- reader.readAsDataURL(file);
- }
+function readFromFile(file, callback) {
+ var reader = new FileReader();
+ reader.onload = ({target}) => callback(target.result);
+ reader.readAsDataURL(file);
}
function ImageCommand(options) {
@@ -40,44 +15,61 @@ function ImageCommand(options) {
name: 'image',
button: ''
});
- this.uploader = new FileUploader({ url: options.serviceUrl, maxFileSize: 5000000 });
+ this.uploader = new FileUploader({
+ url: options.serviceUrl,
+ maxFileSize: 5000000
+ });
}
inherit(ImageCommand, Command);
ImageCommand.prototype = {
- exec: function() {
+ exec() {
ImageCommand._super.prototype.exec.call(this);
- var fileInput = this.fileInput;
- if (!fileInput) {
- fileInput = this.fileInput = createFileInput(this);
- document.body.appendChild(fileInput);
- }
+ var fileInput = this.getFileInput();
fileInput.dispatchEvent(new MouseEvent('click', { bubbles: false }));
},
- handleFile: function(e) {
- var fileInput = e.target;
- var file = fileInput.files && fileInput.files[0];
- var editor = this.editorContext;
- var embedIntent = this.embedIntent;
- var currentEditingIndex = editor.getCurrentBlockIndex();
+ getFileInput() {
+ if (this._fileInput) {
+ return this._fileInput;
+ }
+
+ var fileInput = document.createElement('input');
+ fileInput.type = 'file';
+ fileInput.accept = 'image/*';
+ fileInput.className = 'ck-file-input';
+ fileInput.addEventListener('change', e => this.handleFile(e));
+ document.body.appendChild(fileInput);
+
+ return fileInput;
+ },
+ handleFile({target: fileInput}) {
+ let imageSection;
+
+ let file = fileInput.files[0];
+ readFromFile(file, (base64Image) => {
+ imageSection = generateBuilder().generateImageSection(base64Image);
+ this.editorContext.insertSectionAtCursor(imageSection);
+ this.editorContext.rerender();
+ });
- embedIntent.showLoading();
- renderFromFile(file, editor, currentEditingIndex); // render image immediately client-side
this.uploader.upload({
- fileInput: fileInput,
- complete: function(response, error) {
- embedIntent.hideLoading();
- if (error || !response || !response.url) {
- setTimeout(function() {
- editor.removeBlockAt(currentEditingIndex);
- editor.syncVisual();
- }, 1000);
- return new Message().showError(error.message || 'Error uploading image');
+ fileInput,
+ complete: (response, error) => {
+ if (!imageSection) {
+ throw new Error('Upload completed before the image was read into memory');
+ }
+ if (!error && response && response.url) {
+ imageSection.src = response.url;
+ imageSection.renderNode.markDirty();
+ this.editorContext.rerender();
+ this.editorContext.trigger('update');
+ } else {
+ this.editorContext.removeSection(imageSection);
+ new Message().showError(error.message || 'Error uploading image');
}
- injectImageBlock(response.url, editor, currentEditingIndex);
+ this.editorContext.rerender();
}
});
- fileInput.value = null; // reset file input
}
};
diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js
index 1d175e365..1f422cbda 100644
--- a/src/js/editor/editor.js
+++ b/src/js/editor/editor.js
@@ -20,7 +20,8 @@ import EventEmitter from '../utils/event-emitter';
import MobiledocParser from "../parsers/mobiledoc";
import DOMParser from "../parsers/dom";
-import EditorDOMRenderer from "../renderers/editor-dom";
+import Renderer from 'content-kit-editor/renderers/editor-dom';
+import RenderTree from 'content-kit-editor/models/render-tree';
import MobiledocRenderer from '../renderers/mobiledoc';
import { toArray, merge, mergeWithOptions } from 'content-kit-utils';
@@ -53,7 +54,9 @@ var defaults = {
new UnorderedListCommand(),
new OrderedListCommand()
],
- cards: {},
+ cards: [],
+ cardOptions: {},
+ unknownCardHandler: () => { throw new Error('Unknown card encountered'); },
mobiledoc: null
};
@@ -88,7 +91,7 @@ function bindContentEditableTypingListeners(editor) {
var sanitizedHTML = pastedHTML && editor._renderer.rerender(pastedHTML);
if (sanitizedHTML) {
document.execCommand('insertHTML', false, sanitizedHTML);
- editor.syncVisual();
+ editor.rerender();
}
e.preventDefault();
return false;
@@ -175,8 +178,8 @@ function Editor(element, options) {
// FIXME: This should merge onto this.options
mergeWithOptions(this, defaults, options);
- this._renderer = new EditorDOMRenderer(window.document, this.cards);
this._parser = new DOMParser();
+ this._renderer = new Renderer(this.cards, this.unknownCardHandler, this.cardOptions);
this.applyClassName();
this.applyPlaceholder();
@@ -193,12 +196,12 @@ function Editor(element, options) {
}
clearChildNodes(element);
- this.syncVisual();
+ this.rerender();
bindContentEditableTypingListeners(this);
bindAutoTypingListeners(this);
bindDragAndDrop(this);
- this.addEventListener(element, 'input', () => this.handleInput(...arguments));
+ this.addEventListener(element, 'input', () => this.handleInput());
initEmbedCommands(this);
this.addView(new TextFormatToolbar({
@@ -227,22 +230,34 @@ merge(Editor.prototype, {
loadModel(post) {
this.post = post;
- this.syncVisual();
+ this.rerender();
this.trigger('update');
},
parseModelFromDOM(element) {
this.post = this._parser.parse(element);
+ this._renderTree = new RenderTree();
+ let node = this._renderTree.buildRenderNode(this.post);
+ this._renderTree.node = node;
this.trigger('update');
},
parseModelFromMobiledoc(mobiledoc) {
this.post = new MobiledocParser().parse(mobiledoc);
+ this._renderTree = new RenderTree();
+ let node = this._renderTree.buildRenderNode(this.post);
+ this._renderTree.node = node;
this.trigger('update');
},
- syncVisual() {
- this._renderer.render(this.post, this.element);
+ rerender() {
+ let postRenderNode = this.post.renderNode;
+ if (!postRenderNode.element) {
+ postRenderNode.element = this.element;
+ postRenderNode.markDirty();
+ }
+
+ this._renderer.render(this._renderTree);
},
getCurrentBlockIndex() {
@@ -344,21 +359,28 @@ merge(Editor.prototype, {
let newSections = [];
let previousSection;
forEachChildNode(this.element, (node) => {
- let section = this.post.getElementSection(node);
- if (!section) {
- section = this._parser.parseSection(
+ let sectionRenderNode = this._renderTree.getElementRenderNode(node);
+ if (!sectionRenderNode) {
+ let section = this._parser.parseSection(
previousSection,
node
);
- this.post.setSectionElement(section, node);
newSections.push(section);
+
+ sectionRenderNode = this._renderTree.buildRenderNode(section);
+ sectionRenderNode.element = node;
+ sectionRenderNode.markClean();
+
if (previousSection) {
this.post.insertSectionAfter(section, previousSection);
+ this._renderTree.node.insertAfter(sectionRenderNode, previousSection.renderNode);
} else {
this.post.prependSection(section);
+ this._renderTree.node.insertAfter(sectionRenderNode, null);
}
}
// may cause duplicates to be included
+ let section = sectionRenderNode.postNode;
sectionsInDOM.push(section);
previousSection = section;
});
@@ -368,7 +390,11 @@ merge(Editor.prototype, {
for (i=this.post.sections.length-1;i>=0;i--) {
let section = this.post.sections[i];
if (sectionsInDOM.indexOf(section) === -1) {
- this.post.removeSection(section);
+ if (section.renderNode) {
+ section.renderNode.scheduleForRemoval();
+ } else {
+ throw new Error('All sections are expected to have a renderNode');
+ }
}
}
@@ -388,9 +414,18 @@ merge(Editor.prototype, {
this.reparseSection(section);
}
});
+
+ this.rerender();
+ this.trigger('update');
},
getSectionsWithCursor() {
+ return this.getRenderNodesWithCursor().map( renderNode => {
+ return renderNode.postNode;
+ });
+ },
+
+ getRenderNodesWithCursor() {
const selection = document.getSelection();
if (selection.rangeCount === 0) {
return null;
@@ -400,26 +435,32 @@ merge(Editor.prototype, {
let { startContainer:startElement, endContainer:endElement } = range;
- let getElementSection = (e) => this.post.getElementSection(e);
- let { result:startSection } = detectParentNode(startElement, getElementSection);
- let { result:endSection } = detectParentNode(endElement, getElementSection);
-
- let startIndex = this.post.sections.indexOf(startSection),
- endIndex = this.post.sections.indexOf(endSection);
+ let getElementRenderNode = (e) => {
+ return this._renderTree.getElementRenderNode(e);
+ };
+ let { result:startRenderNode } = detectParentNode(startElement, getElementRenderNode);
+ let { result:endRenderNode } = detectParentNode(endElement, getElementRenderNode);
+
+ let nodes = [];
+ let node = startRenderNode;
+ while (node && (!endRenderNode.nextSibling || endRenderNode.nextSibling !== node)) {
+ nodes.push(node);
+ node = node.nextSibling;
+ }
- return this.post.sections.slice(startIndex, endIndex+1);
+ return nodes;
},
reparseSection(section) {
- let sectionElement = this.post.getSectionElement(section);
+ let sectionRenderNode = section.renderNode;
+ let sectionElement = sectionRenderNode.element;
let previousSection = this.post.getPreviousSection(section);
var newSection = this._parser.parseSection(
previousSection,
sectionElement
);
- this.post.replaceSection(section, newSection);
- this.post.setSectionElement(newSection, sectionElement);
+ section.markers = newSection.markers;
this.trigger('update');
},
@@ -433,6 +474,20 @@ merge(Editor.prototype, {
this._views = [];
},
+ insertSectionAtCursor(newSection) {
+ let newRenderNode = this._renderTree.buildRenderNode(newSection);
+ let renderNodes = this.getRenderNodesWithCursor();
+ let lastRenderNode = renderNodes[renderNodes.length-1];
+ lastRenderNode.parentNode.insertAfter(newRenderNode, lastRenderNode);
+ this.post.insertSectionAfter(newSection, lastRenderNode.postNode);
+ renderNodes.forEach(renderNode => renderNode.scheduleForRemoval());
+ this.trigger('update');
+ },
+
+ removeSection(section) {
+ this.post.removeSection(section);
+ },
+
destroy() {
this.removeAllEventListeners();
this.removeAllViews();
diff --git a/src/js/models/card-node.js b/src/js/models/card-node.js
new file mode 100644
index 000000000..a5997fd5a
--- /dev/null
+++ b/src/js/models/card-node.js
@@ -0,0 +1,53 @@
+export default class CardNode {
+ constructor(card, section, element, cardOptions) {
+ this.card = card;
+ this.section = section;
+ this.cardOptions = cardOptions;
+ this.element = element;
+
+ this.mode = null;
+ this.setupResult = null;
+ }
+
+ render(mode) {
+ if (this.mode === mode) { return; }
+
+ this.teardown();
+
+ this.mode = mode;
+ this.setupResult = this.card[mode].setup(
+ this.element,
+ this.cardOptions,
+ this.env,
+ this.section.payload
+ );
+ }
+
+ get env() {
+ return {
+ name: this.card.name,
+ edit: () => { this.edit(); },
+ save: (payload) => {
+ this.section.payload = payload;
+ this.display();
+ },
+ cancel: () => { this.display(); }
+ };
+ }
+
+ display() {
+ this.render('display');
+ }
+
+ edit() {
+ this.render('edit');
+ }
+
+ teardown() {
+ if (this.mode) {
+ if (this.card[this.mode].teardown) {
+ this.card[this.mode].teardown(this.setupResult);
+ }
+ }
+ }
+}
diff --git a/src/js/models/image.js b/src/js/models/image.js
new file mode 100644
index 000000000..b78610114
--- /dev/null
+++ b/src/js/models/image.js
@@ -0,0 +1,6 @@
+export default class Image {
+ constructor() {
+ this.type = 'imageSection';
+ this.src = null;
+ }
+}
diff --git a/src/js/models/post.js b/src/js/models/post.js
index 8638393a3..512ec22a3 100644
--- a/src/js/models/post.js
+++ b/src/js/models/post.js
@@ -1,11 +1,8 @@
-import ElementMap from "../utils/element-map";
-
// FIXME: making sections a linked-list would greatly improve this
export default class Post {
constructor() {
this.type = 'post';
this.sections = [];
- this.sectionElementMap = new ElementMap();
}
appendSection(section) {
this.sections.push(section);
@@ -27,18 +24,7 @@ export default class Post {
}
throw new Error('Previous section was not found in post.sections');
}
- setSectionElement(section, element) {
- section.element = element;
- this.sectionElementMap.set(element, section);
- }
- getSectionElement(section) {
- return section && section.element;
- }
- getElementSection(element) {
- return this.sectionElementMap.get(element);
- }
removeSection(section) {
- this.sectionElementMap.remove(section.element);
var i, l;
for (i=0,l=this.sections.length;i card.name === section.name);
+
+ const env = { name: section.name };
+ renderNode.element = document.createElement('div');
+ renderNode.parentNode.element.appendChild(renderNode.element);
+
+ if (card) {
+ let cardNode = new CardNode(card, section, renderNode.element, this.options);
+ renderNode.cardNode = cardNode;
+ cardNode.display();
+ } else {
+ this.unknownCardHandler(renderNode.element, this.options, env, section.payload);
+ }
}
- this.cards = cards;
}
-NewDOMRenderer.prototype.render = function NewDOMRenderer_render(post, target) {
- var sections = post.sections;
- var i, l, section, node;
- for (i=0, l=sections.length;i {
+ let node = lookupNode(renderTree, parentNode, section, previousNode);
+ if (node.isDirty) {
+ nodes.push(node);
+ }
+ previousNode = node;
+ });
+ }
+ let node = nodes.shift();
+ while (node) {
+ removeChildren(node);
+ visitor[node.postNode.type](node, node.postNode, visit);
+ node.markClean();
+ node = nodes.shift();
+ }
+}
+
+export default class Render {
+ constructor(cards, unknownCardHandler, options) {
+ this.visitor = new Visitor(cards, unknownCardHandler, options);
+ }
+
+ render(renderTree) {
+ renderInternal(renderTree, this.visitor);
+ }
+}
diff --git a/src/js/renderers/mobiledoc.js b/src/js/renderers/mobiledoc.js
index bac38670a..e967d0526 100644
--- a/src/js/renderers/mobiledoc.js
+++ b/src/js/renderers/mobiledoc.js
@@ -9,6 +9,12 @@ let visitor = {
opcodes.push(['openMarkupSection', node.tagName]);
visitArray(visitor, node.markers, opcodes);
},
+ imageSection(node, opcodes) {
+ opcodes.push(['openImageSection', node.src]);
+ },
+ card(node, opcodes) {
+ opcodes.push(['openCardSection', node.name, node.payload]);
+ },
marker(node, opcodes) {
opcodes.push(['openMarker', node.close, node.value]);
visitArray(visitor, node.open, opcodes);
@@ -24,13 +30,19 @@ let postOpcodeCompiler = {
this.markers.push([
this.markupMarkerIds,
closeCount,
- value
+ value || ''
]);
},
openMarkupSection(tagName) {
this.markers = [];
this.sections.push([1, tagName, this.markers]);
},
+ openImageSection(url) {
+ this.sections.push([2, url]);
+ },
+ openCardSection(name, payload) {
+ this.sections.push([10, name, payload]);
+ },
openPost() {
this.markerTypes = [];
this.sections = [];
diff --git a/src/js/utils/array-utils.js b/src/js/utils/array-utils.js
new file mode 100644
index 000000000..9cb929864
--- /dev/null
+++ b/src/js/utils/array-utils.js
@@ -0,0 +1,11 @@
+function detect(array, callback) {
+ for (let i=0; i {
+ assert.expect(4);
+
+ const payload = {
+ foo: 'bar'
+ };
+ const cardOptions = { boo: 'baz' };
+
+ const card = {
+ name: 'test-card',
+ display: {
+ setup(element, options, env, setupPayload) {
+ assert.ok(editorElement.contains(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');
+ },
+ teardown() {
+ }
+ }
+ };
+
+ const mobiledoc = [
+ [],
+ [
+ [10, 'test-card', payload]
+ ]
+ ];
+ editor = new Editor(editorElement, {
+ mobiledoc,
+ cards: [card],
+ cardOptions
+ });
+});
+
+test('rendering a mobiledoc for editing calls #unknownCardHandler when it encounters an unknown card', (assert) => {
+ assert.expect(1);
+
+ const cardName = 'my-card';
+
+ const unknownCardHandler = (element, options, env /*,setupPayload*/) => {
+ assert.equal(env.name, cardName, 'includes card name in env');
+ };
+
+ const mobiledoc = [
+ [],
+ [
+ [10, cardName, {}]
+ ]
+ ];
+
+ editor = new Editor(editorElement, {mobiledoc, unknownCardHandler});
+});
+
+test('rendered card can fire edit hook to enter editing mode', (assert) => {
+ assert.expect(7);
+
+ const payload = { foo: 'bar' };
+ const cardOptions = { boo: 'baz' };
+
+ 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');
+ }
+ },
+ edit: {
+ setup(element, options, env, setupPayload) {
+ assert.ok(editorElement.contains(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');
+ }
+ }
+ };
+
+ const mobiledoc = [
+ [],
+ [
+ [10, 'test-card', payload]
+ ]
+ ];
+ editor = new Editor(editorElement, {
+ mobiledoc,
+ cards: [card],
+ cardOptions
+ });
+
+ Helpers.dom.triggerEvent(span, 'click');
+});
+
+test('rendered card can fire edit hook to enter editing mode, then save', (assert) => {
+ assert.expect(3);
+
+ let setupPayloads = [];
+ let newPayload = {some: 'new values'};
+ let doEdit, doSave;
+ const card = {
+ name: 'test-card',
+ display: {
+ setup(element, options, env, setupPayload) {
+ setupPayloads.push(setupPayload);
+ doEdit = () => {
+ env.edit();
+ };
+ }
+ },
+ edit: {
+ setup(element, options, env) {
+ assert.ok(env.save,
+ 'env exposes save hook');
+ doSave = () => {
+ env.save(newPayload);
+ };
+ }
+ }
+ };
+
+ const payload = { foo: 'bar' };
+ const mobiledoc = [
+ [],
+ [
+ [10, 'test-card', payload]
+ ]
+ ];
+ editor = new Editor(editorElement, {
+ mobiledoc,
+ cards: [card]
+ });
+
+ doEdit();
+ doSave();
+ let [firstPayload, secondPayload] = setupPayloads;
+ assert.equal(firstPayload, payload, 'first display with mobiledoc payload');
+ assert.equal(secondPayload, newPayload, 'second display with new payload');
+});
+
+test('rendered card can fire edit hook to enter editing mode, then cancel', (assert) => {
+ assert.expect(3);
+
+ let setupPayloads = [];
+ let doEdit, doCancel;
+ const card = {
+ name: 'test-card',
+ display: {
+ setup(element, options, env, setupPayload) {
+ setupPayloads.push(setupPayload);
+ doEdit = () => {
+ env.edit();
+ };
+ }
+ },
+ edit: {
+ setup(element, options, env) {
+ assert.ok(env.cancel, 'env exposes cancel hook');
+ doCancel = () => {
+ env.cancel();
+ };
+ }
+ }
+ };
+
+ const payload = { foo: 'bar' };
+ const mobiledoc = [
+ [],
+ [
+ [10, 'test-card', payload]
+ ]
+ ];
+ editor = new Editor(editorElement, {
+ mobiledoc,
+ cards: [card]
+ });
+
+ doEdit();
+ doCancel();
+ let [firstPayload, secondPayload] = setupPayloads;
+ assert.equal(firstPayload, payload, 'first display with mobiledoc payload');
+ assert.equal(secondPayload, payload, 'second display with mobiledoc payload');
+});
diff --git a/tests/unit/editor/editor-test.js b/tests/unit/editor/editor-test.js
index 86f05e7ca..b6dca0375 100644
--- a/tests/unit/editor/editor-test.js
+++ b/tests/unit/editor/editor-test.js
@@ -13,7 +13,9 @@ module('Unit: Editor', {
fixture.appendChild(editorElement);
},
afterEach: function() {
- editor.destroy();
+ if (editor) {
+ editor.destroy();
+ }
fixture.removeChild(editorElement);
}
});
diff --git a/tests/unit/parsers/mobiledoc-test.js b/tests/unit/parsers/mobiledoc-test.js
index d12a479b5..53d1a737b 100644
--- a/tests/unit/parsers/mobiledoc-test.js
+++ b/tests/unit/parsers/mobiledoc-test.js
@@ -1,6 +1,7 @@
import MobiledocParser from 'content-kit-editor/parsers/mobiledoc';
import { generateBuilder } from 'content-kit-editor/utils/post-builder';
+const DATA_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
const { module, test } = window.QUnit;
let parser, builder, post;
@@ -77,3 +78,38 @@ test('#parse doc with marker type', (assert) => {
);
});
+test('#parse doc with image section', (assert) => {
+ const mobiledoc = [
+ [],
+ [
+ [2, DATA_URL]
+ ]
+ ];
+
+ const parsed = parser.parse(mobiledoc);
+
+ let section = builder.generateImageSection(DATA_URL);
+ post.appendSection(section);
+ assert.deepEqual(
+ parsed,
+ post
+ );
+});
+
+test('#parse doc with custom card type', (assert) => {
+ const mobiledoc = [
+ [],
+ [
+ [10, 'custom-card', {}]
+ ]
+ ];
+
+ const parsed = parser.parse(mobiledoc);
+
+ let section = builder.generateCardSection('custom-card');
+ post.appendSection(section);
+ assert.deepEqual(
+ parsed,
+ post
+ );
+});
diff --git a/tests/unit/renderers/editor-dom-test.js b/tests/unit/renderers/editor-dom-test.js
new file mode 100644
index 000000000..df0d07507
--- /dev/null
+++ b/tests/unit/renderers/editor-dom-test.js
@@ -0,0 +1,169 @@
+import { generateBuilder } from 'content-kit-editor/utils/post-builder';
+const { module, test } = window.QUnit;
+import Renderer from 'content-kit-editor/renderers/render';
+import RenderNode from 'content-kit-editor/models/render-node';
+import RenderTree from 'content-kit-editor/models/render-tree';
+
+const DATA_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
+let builder;
+
+function render(renderTree) {
+ let renderer = new Renderer([]);
+ return renderer.render(renderTree);
+}
+
+module("Unit: Renderer", {
+ beforeEach() {
+ builder = generateBuilder();
+ }
+});
+
+test("It renders a dirty post", (assert) => {
+ /*
+ * renderTree is:
+ *
+ * renderNode
+ *
+ */
+ let renderNode = new RenderNode(builder.generatePost());
+ let renderTree = new RenderTree(renderNode);
+ renderNode.renderTree = renderTree;
+
+ render(renderTree);
+
+ assert.ok(renderTree.node.element, 'renderTree renders element for post');
+ assert.ok(!renderTree.node.isDirty, 'dirty node becomes clean');
+ assert.equal(renderTree.node.element.tagName, 'DIV', 'renderTree renders element for post');
+});
+
+test("It renders a dirty post with un-rendered sections", (assert) => {
+ let post = builder.generatePost();
+ let sectionA = builder.generateSection('P');
+ post.appendSection(sectionA);
+ let sectionB = builder.generateSection('P');
+ post.appendSection(sectionB);
+
+ let renderNode = new RenderNode(post);
+ let renderTree = new RenderTree(renderNode);
+ renderNode.renderTree = renderTree;
+
+ render(renderTree);
+
+ assert.equal(renderTree.node.element.outerHTML, '',
+ 'correct HTML is rendered');
+
+ assert.ok(renderTree.node.firstChild,
+ 'sectionA creates a first child');
+ assert.equal(renderTree.node.firstChild.postNode, sectionA,
+ 'sectionA is first renderNode child');
+ assert.ok(!renderTree.node.firstChild.isDirty, 'sectionA node is clean');
+ assert.equal(renderTree.node.lastChild.postNode, sectionB,
+ 'sectionB is second renderNode child');
+ assert.ok(!renderTree.node.lastChild.isDirty, 'sectionB node is clean');
+});
+
+[
+ {
+ name: 'markup',
+ section: (builder) => builder.generateSection('P')
+ },
+ {
+ name: 'image',
+ section: (builder) => builder.generateImageSection(DATA_URL)
+ },
+ {
+ name: 'card',
+ section: (builder) => builder.generateCardSection('new-card')
+ }
+].forEach((testInfo) => {
+ test(`Remove nodes with ${testInfo.name} section`, (assert) => {
+ let post = builder.generatePost();
+ let section = testInfo.section(builder);
+ post.appendSection(section);
+
+ let postElement = document.createElement('div');
+ let sectionElement = document.createElement('p');
+ postElement.appendChild(sectionElement);
+
+ let postRenderNode = new RenderNode(post);
+
+ let renderTree = new RenderTree(postRenderNode);
+ postRenderNode.renderTree = renderTree;
+ postRenderNode.element = postElement;
+
+ let sectionRenderNode = renderTree.buildRenderNode(section);
+ sectionRenderNode.element = sectionElement;
+ sectionRenderNode.scheduleForRemoval();
+ postRenderNode.appendChild(sectionRenderNode);
+
+ render(renderTree);
+
+ assert.equal(renderTree.node.element, postElement,
+ 'post element remains');
+
+ assert.equal(renderTree.node.element.firstChild, null,
+ 'section element removed');
+
+ assert.equal(renderTree.node.firstChild, null,
+ 'section renderNode is removed');
+ });
+});
+
+test('renders a post with marker', (assert) => {
+ let post = builder.generatePost();
+ let section = builder.generateSection('P');
+ post.appendSection(section);
+ section.markers.push(
+ builder.generateMarker([
+ builder.generateMarkerType('STRONG')
+ ], 1, 'Hi')
+ );
+
+ let node = new RenderNode(post);
+ let renderTree = new RenderTree(node);
+ node.renderTree = renderTree;
+ render(renderTree);
+ assert.equal(node.element.innerHTML, 'Hi
');
+});
+
+test('renders a post with image', (assert) => {
+ let url = DATA_URL;
+ let post = builder.generatePost();
+ let section = builder.generateImageSection(url);
+ post.appendSection(section);
+
+ let node = new RenderNode(post);
+ let renderTree = new RenderTree(node);
+ node.renderTree = renderTree;
+ render(renderTree);
+ assert.equal(node.element.innerHTML, `
`);
+});
+
+/*
+test("It renders a renderTree with rendered dirty section", (assert) => {
+ /*
+ * renderTree is:
+ *
+ * post
+ * / \
+ * / \
+ * section section
+ *
+ let post = builder.generatePost
+ let postRenderNode = {
+ element: null,
+ parent: null,
+ isDirty: true,
+ postNode: builder.generatePost()
+ }
+ let renderTree = {
+ node: renderNode
+ }
+
+ render(renderTree);
+
+ assert.ok(renderTree.node.element, 'renderTree renders element for post');
+ assert.ok(!renderTree.node.isDirty, 'dirty node becomes clean');
+ assert.equal(renderTree.node.element.tagName, 'DIV', 'renderTree renders element for post');
+});
+*/
diff --git a/tests/unit/renderers/mobiledoc-test.js b/tests/unit/renderers/mobiledoc-test.js
index 12145c7f2..f31da6c27 100644
--- a/tests/unit/renderers/mobiledoc-test.js
+++ b/tests/unit/renderers/mobiledoc-test.js
@@ -39,3 +39,48 @@ test('renders a post with marker', (assert) => {
]
]);
});
+
+test('renders a post with image', (assert) => {
+ let url = "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=";
+ let post = builder.generatePost();
+ let section = builder.generateImageSection(url);
+ post.appendSection(section);
+
+ let mobiledoc = render(post);
+ assert.deepEqual(mobiledoc, [
+ [],
+ [
+ [2, url]
+ ]
+ ]);
+});
+
+test('renders a post with image and null src', (assert) => {
+ let post = builder.generatePost();
+ let section = builder.generateImageSection();
+ post.appendSection(section);
+
+ let mobiledoc = render(post);
+ assert.deepEqual(mobiledoc, [
+ [],
+ [
+ [2, null]
+ ]
+ ]);
+});
+
+test('renders a post with card', (assert) => {
+ let cardName = 'super-card';
+ let payload = { bar: 'baz' };
+ let post = builder.generatePost();
+ let section = builder.generateCardSection(cardName, payload);
+ post.appendSection(section);
+
+ let mobiledoc = render(post);
+ assert.deepEqual(mobiledoc, [
+ [],
+ [
+ [10, cardName, payload]
+ ]
+ ]);
+});