Skip to content

Commit

Permalink
Initial atoms implementation
Browse files Browse the repository at this point in the history
* Atom model
* First stab at rendering cards with editor-dom renderer
* Atoms have length of 1
* Atoms use DocumentFragment to render
* Clear child nodes in AtomNode teardown
* Use -mobiledoc-kit__atom for atom className
* Atoms and Markers are both Markerupable
  • Loading branch information
rlivsey authored and mixonic committed Feb 2, 2016
1 parent 0a51e71 commit 11158e7
Show file tree
Hide file tree
Showing 12 changed files with 297 additions and 86 deletions.
3 changes: 2 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ const defaults = {
spellcheck: true,
autofocus: true,
cards: [],
atoms: [],
cardOptions: {},
unknownCardHandler: ({env}) => {
throw new Error(`Unknown card encountered: ${env.name}`);
Expand Down Expand Up @@ -91,7 +92,7 @@ class Editor {
DEFAULT_KEY_COMMANDS.forEach(kc => this.registerKeyCommand(kc));

this._parser = new DOMParser(this.builder);
this._renderer = new Renderer(this, this.cards, this.unknownCardHandler, this.cardOptions);
this._renderer = new Renderer(this, this.cards, this.atoms, this.unknownCardHandler, this.cardOptions);

this.post = this.loadPost();
this._renderTree = new RenderTree(this.post);
Expand Down
44 changes: 44 additions & 0 deletions src/js/models/atom-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { clearChildNodes } from '../utils/dom-utils';

export default class AtomNode {
constructor(editor, atom, model, element, atomOptions) {
this.editor = editor;
this.atom = atom;
this.model = model;
this.atomOptions = atomOptions;
this.element = element;

this._teardown = null;
}

render() {
this.teardown();

let fragment = document.createDocumentFragment();

this._teardown = this.atom.render({
options: this.atomOptions,
env: this.env,
value: this.model.value,
payload: this.model.payload,
fragment
});

this.element.appendChild(fragment);
}

get env() {
return {
name: this.atom.name
};
}

teardown() {
if (this._teardown) {
this._teardown();
}

clearChildNodes(this.element);
}

}
20 changes: 20 additions & 0 deletions src/js/models/atom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { ATOM_TYPE } from './types';
import mixin from '../utils/mixin';
import MarkuperableMixin from '../utils/markuperable';
import LinkedItem from '../utils/linked-item';

export default class Atom extends LinkedItem {
constructor(name, value, payload, markups=[]) {
super();
this.name = name;
this.value = value;
this.payload = payload;
this.type = ATOM_TYPE;
this.length = 1;

this.markups = [];
markups.forEach(m => this.addMarkup(m));
}
}

mixin(Atom, MarkuperableMixin);
78 changes: 6 additions & 72 deletions src/js/models/marker.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MARKER_TYPE } from './types';
import { normalizeTagName } from '../utils/dom-utils';
import { detect, commonItemLength, forEach, filter } from '../utils/array-utils';
import mixin from '../utils/mixin';
import MarkuperableMixin from '../utils/markuperable';
import LinkedItem from '../utils/linked-item';
import assert from '../utils/assert';

Expand Down Expand Up @@ -41,44 +41,8 @@ const Marker = class Marker extends LinkedItem {
return this.value.length;
}

clearMarkups() {
this.markups = [];
}

addMarkup(markup) {
this.markups.push(markup);
}

removeMarkup(markupOrMarkupCallback) {
let callback;
if (typeof markupOrMarkupCallback === 'function') {
callback = markupOrMarkupCallback;
} else {
let markup = markupOrMarkupCallback;
callback = (_markup) => _markup === markup;
}

forEach(
filter(this.markups, callback),
m => this._removeMarkup(m)
);
}

_removeMarkup(markup) {
const index = this.markups.indexOf(markup);
if (index !== -1) {
this.markups.splice(index, 1);
}
}

/**
* delete the character at this offset,
* update the value with the new value.
* This method mutates the marker.
*
* @return {Number} the length of the change
* (usually 1 but can be 2 when deleting an emoji, e.g.)
*/
// delete the character at this offset,
// update the value with the new value
deleteValueAtOffset(offset) {
assert('Cannot delete value at offset outside bounds',
offset >= 0 && offset <= this.length);
Expand All @@ -101,20 +65,6 @@ const Marker = class Marker extends LinkedItem {
return width;
}

hasMarkup(tagNameOrMarkup) {
return !!this.getMarkup(tagNameOrMarkup);
}

getMarkup(tagNameOrMarkup) {
if (typeof tagNameOrMarkup === 'string') {
let tagName = normalizeTagName(tagNameOrMarkup);
return detect(this.markups, markup => markup.tagName === tagName);
} else {
let targetMarkup = tagNameOrMarkup;
return detect(this.markups, markup => markup === targetMarkup);
}
}

join(other) {
const joined = this.builder.createMarker(this.value + other.value);
this.markups.forEach(m => joined.addMarkup(m));
Expand Down Expand Up @@ -155,24 +105,8 @@ const Marker = class Marker extends LinkedItem {
return [pre, post];
}

get openedMarkups() {
let count = 0;
if (this.prev) {
count = commonItemLength(this.markups, this.prev.markups);
}

return this.markups.slice(count);
}

get closedMarkups() {
let count = 0;
if (this.next) {
count = commonItemLength(this.markups, this.next.markups);
}

return this.markups.slice(count);
}

};

mixin(Marker, MarkuperableMixin);

export default Marker;
5 changes: 3 additions & 2 deletions src/js/models/post-node-builder.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Atom from '../models/atom';
import Post from '../models/post';
import MarkupSection from '../models/markup-section';
import ListSection from '../models/list-section';
Expand Down Expand Up @@ -102,8 +103,8 @@ export default class PostNodeBuilder {
return marker;
}

createAtom(name, text, payload={}) {
const atom = new Atom(name, text, payload);
createAtom(name, text, payload={}, markups=[]) {
const atom = new Atom(name, text, payload, markups);
atom.builder = this;
return atom;
}
Expand Down
1 change: 1 addition & 0 deletions src/js/models/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const POST_TYPE = 'post';
export const LIST_ITEM_TYPE = 'list-item';
export const CARD_TYPE = 'card-section';
export const IMAGE_SECTION_TYPE = 'image-section';
export const ATOM_TYPE = 'atom';
77 changes: 73 additions & 4 deletions src/js/renderers/editor-dom.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import CardNode from 'mobiledoc-kit/models/card-node';
import { detect, forEach } from 'mobiledoc-kit/utils/array-utils';
import AtomNode from 'mobiledoc-kit/models/atom-node';
import {
POST_TYPE,
MARKUP_SECTION_TYPE,
LIST_SECTION_TYPE,
LIST_ITEM_TYPE,
MARKER_TYPE,
IMAGE_SECTION_TYPE,
CARD_TYPE
CARD_TYPE,
ATOM_TYPE
} from '../models/types';
import { startsWith, endsWith } from '../utils/string-utils';
import { addClassName } from '../utils/dom-utils';
Expand Down Expand Up @@ -109,6 +111,21 @@ function renderCard() {
return { wrapper, cardElement };
}

function renderAtom(element, previousRenderNode) {
let atomElement = document.createElement('span');
addClassName(atomElement, '-mobiledoc-kit__atom');

if (previousRenderNode) {
let previousSibling = previousRenderNode.element;
let previousSiblingPenultimate = penultimateParentOf(previousSibling, element);
element.insertBefore(atomElement, previousSiblingPenultimate.nextSibling);
} else {
element.insertBefore(atomElement, element.firstChild);
}

return atomElement;
}

function getNextMarkerElement(renderNode) {
let element = renderNode.element.parentNode;
let marker = renderNode.postNode;
Expand Down Expand Up @@ -206,10 +223,25 @@ function validateCards(cards=[]) {
return cards;
}

function validateAtoms(atoms=[]) {
forEach(atoms, atom => {
assert(
`Atom "${atom.name}" must define type "dom", has: "${atom.type}"`,
atom.type === 'dom'
);
assert(
`Card "${atom.name}" must define \`render\` method`,
!!atom.render
);
});
return atoms;
}

class Visitor {
constructor(editor, cards, unknownCardHandler, options) {
constructor(editor, cards, atoms, unknownCardHandler, options) {
this.editor = editor;
this.cards = validateCards(cards);
this.atoms = validateAtoms(atoms);
this.unknownCardHandler = unknownCardHandler;
this.options = options;
}
Expand Down Expand Up @@ -334,6 +366,35 @@ class Visitor {
const initialMode = section._initialMode;
cardNode[initialMode]();
}

[ATOM_TYPE](renderNode, atomModel) {
let parentElement;

if (renderNode.prev) {
parentElement = getNextMarkerElement(renderNode.prev);
} else {
parentElement = renderNode.parent.element;
}

const {editor, options} = this;
const atomElement = renderAtom(parentElement, renderNode.prev);
const atom = detect(this.atoms, atom => atom.name === atomModel.name);

if (atom) {
const atomNode = new AtomNode(
editor, atom, atomModel, atomElement, options
);

atomNode.render();

renderNode.atomNode = atomNode;
renderNode.element = atomElement;
} else {
const env = { name: atomModel.name };
this.unknownAtomHandler( // TODO - pass this in...
atomElement, options, env, atomModel.payload);
}
}
}

let destroyHooks = {
Expand Down Expand Up @@ -384,6 +445,14 @@ let destroyHooks = {
removeRenderNodeSectionFromParent(renderNode, section);
removeRenderNodeElementFromParent(renderNode);
}

// [ATOM_TYPE](renderNode, atom) {
// if (renderNode.atomNode) {
// renderNode.atomNode.teardown();
// }
//
// // TODO - same/similar logic as markers?
// }
};

// removes children from parentNode (a RenderNode) that are scheduled for removal
Expand Down Expand Up @@ -416,9 +485,9 @@ function lookupNode(renderTree, parentNode, postNode, previousNode) {
}

export default class Renderer {
constructor(editor, cards, unknownCardHandler, options) {
constructor(editor, cards, atoms, unknownCardHandler, options) {
this.editor = editor;
this.visitor = new Visitor(editor, cards, unknownCardHandler, options);
this.visitor = new Visitor(editor, cards, atoms, unknownCardHandler, options);
this.nodes = [];
this.hasRendered = false;
}
Expand Down
Loading

0 comments on commit 11158e7

Please sign in to comment.