Skip to content

Commit

Permalink
Add MutationHandler, reparse entire post when new nodes appear
Browse files Browse the repository at this point in the history
  * adds editor#_reparseSections and editor#_reparsePost
  * adds RenderTree#isDirty, use it to determine whether to rerender
    the cursor position after reparsing
  * tests for changing text and element nodes in the editor dom
  * Remove editor#reparse and #_reparseCurrentSection

fixes #300
  • Loading branch information
bantic committed Jan 28, 2016
1 parent 6f5033d commit 34ab629
Show file tree
Hide file tree
Showing 8 changed files with 399 additions and 131 deletions.
99 changes: 41 additions & 58 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
import { DIRECTION } from 'mobiledoc-kit/utils/key';
import { TAB, SPACE } from 'mobiledoc-kit/utils/characters';
import assert from '../utils/assert';
import MutationHandler from 'mobiledoc-kit/editor/mutation-handler';

export const EDITOR_ELEMENT_CLASS_NAME = '__mobiledoc-editor';

Expand Down Expand Up @@ -89,10 +90,6 @@ class Editor {
DEFAULT_TEXT_EXPANSIONS.forEach(e => this.registerExpansion(e));
DEFAULT_KEY_COMMANDS.forEach(kc => this.registerKeyCommand(kc));

this._mutationObserver = new MutationObserver(() => {
this.handleInput();
});
this._isMutationObserved = false;
this._parser = new DOMParser(this.builder);
this._renderer = new Renderer(this, this.cards, this.unknownCardHandler, this.cardOptions);

Expand Down Expand Up @@ -137,9 +134,9 @@ class Editor {
}

this.runCallbacks(CALLBACK_QUEUES.WILL_RENDER);
this.removeMutationObserver();
this._renderer.render(this._renderTree);
this.ensureMutationObserver();
this._mutationHandler.suspendObservation(() => {
this._renderer.render(this._renderTree);
});
this.runCallbacks(CALLBACK_QUEUES.DID_RENDER);
}

Expand All @@ -154,6 +151,8 @@ class Editor {
clearChildNodes(element);

this.element = element;
this._mutationHandler = new MutationHandler(this);
this._mutationHandler.startObserving();

if (this.isEditable === null) {
this.enableEditing();
Expand Down Expand Up @@ -317,33 +316,33 @@ class Editor {
setData(this.element, 'placeholder', placeholder);
}

/**
* types of input to handle:
* * delete from beginning of section
* joins 2 sections
* * delete when multiple sections selected
* removes wholly-selected sections,
* joins the partially-selected sections
* * hit enter (handled by capturing 'keydown' for enter key and `handleNewline`)
* if anything is selected, delete it first, then
* split the current marker at the cursor position,
* schedule removal of every marker after the split,
* create new section, append it to post
* append the after-split markers onto the new section
* rerender -- this should render the new section at the appropriate spot
*/
handleInput() {
this.reparse();
_reparsePost() {
this.post = this._parser.parse(this.element);
this._renderTree = new RenderTree(this.post);
clearChildNodes(this.element);
this.rerender();

this.runCallbacks(CALLBACK_QUEUES.DID_REPARSE);
this.didUpdate();
}

reparse() {
this._reparseCurrentSection();
_reparseSections(sections=[]) {
let currentRange;
sections.forEach(section => {
this._parser.reparseSection(section, this._renderTree);
});
this._removeDetachedSections();

// A call to `run` will trigger the didUpdatePostCallbacks hooks with a
// postEditor.
if (this._renderTree.isDirty) {
currentRange = this.range;
}

this.run(() => {});
this.rerender();
if (currentRange) {
this.selectRange(currentRange);
}

this.runCallbacks(CALLBACK_QUEUES.DID_REPARSE);
this.didUpdate();
}
Expand Down Expand Up @@ -389,13 +388,6 @@ class Editor {
}
}

_reparseCurrentSection() {
const {headSection:currentSection } = this.cursor.offsets;
if (currentSection) {
this._parser.reparseSection(currentSection, this._renderTree);
}
}

serialize() {
return mobiledocRenderers.render(this.post);
}
Expand All @@ -405,32 +397,15 @@ class Editor {
this._views = [];
}

ensureMutationObserver() {
if (!this._isMutationObserved) {
this._mutationObserver.observe(this.element, {
characterData: true,
childList: true,
subtree: true
});
this._isMutationObserved = true;
}
}

removeMutationObserver() {
if (this._isMutationObserved) {
this._mutationObserver.disconnect();
this._isMutationObserved = false;
}
}

destroy() {
this._isDestroyed = true;
if (this.cursor.hasCursor()) {
this.cursor.clearSelection();
this.element.blur();
}
this.removeMutationObserver();
this._mutationObserver = null;
if (this._mutationHandler) {
this._mutationHandler.destroy();
}
this.removeAllEventListeners();
this.removeAllViews();
this._renderer.destroy();
Expand Down Expand Up @@ -565,6 +540,16 @@ class Editor {
this.addCallback(CALLBACK_QUEUES.CURSOR_DID_CHANGE, callback);
}

/**
* @method didReparse
* @param {Function} callback This callback is called after any part of the
* post is reparsed
* @public
*/
didReparse(callback) {
this.addCallback(CALLBACK_QUEUES.DID_REPARSE, callback);
}

_setupListeners() {
ELEMENT_EVENTS.forEach(eventName => {
this.addEventListener(this.element, eventName,
Expand Down Expand Up @@ -665,8 +650,7 @@ class Editor {
this.handleNewline(event);
break;
case key.isPrintable():
{
let { offsets: range } = this.cursor;
let { range } = this;
let { isCollapsed } = range;
let nextPosition = range.head;

Expand Down Expand Up @@ -696,7 +680,6 @@ class Editor {
event.preventDefault();
}
break;
}
}
}

Expand Down
129 changes: 129 additions & 0 deletions src/js/editor/mutation-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import Set from 'mobiledoc-kit/utils/set';
import { forEach, filter } from 'mobiledoc-kit/utils/array-utils';
import assert from 'mobiledoc-kit/utils/assert';

const MUTATION = {
NODES_CHANGED: 'childList',
CHARACTER_DATA: 'characterData'
};

export default class MutationHandler {
constructor(editor) {
this.editor = editor;
this.renderTree = null;
this._isObserving = false;

this._observer = new MutationObserver((mutations) => {
this._handleMutations(mutations);
});
}

destroy() {
this.stopObserving();
this._observer = null;
}

suspendObservation(callback) {
this.stopObserving();
callback();
this.startObserving();
}

stopObserving() {
if (this._isObserving) {
this._isObserving = false;
this._observer.disconnect();
}
}

startObserving() {
if (!this._isObserving) {
let { editor } = this;
assert('Cannot observe un-rendered editor', editor.hasRendered);

this._isObserving = true;
this.renderTree = editor._renderTree;

this._observer.observe(editor.element, {
characterData: true,
childList: true,
subtree: true
});
}
}

reparsePost() {
this.editor._reparsePost();
}

reparseSections(sections) {
this.editor._reparseSections(sections);
}

/**
* for each mutation:
* * find the target nodes:
* * if nodes changed, target nodes are:
* * added nodes
* * the target from which removed nodes were removed
* * if character data changed
* * target node is the mutation event's target (text node)
* * filter out nodes that are no longer attached (parentNode is null)
* * for each remaining node:
* * find its section, add to sections-to-reparse
* * if no section, reparse all (and break)
*/
_handleMutations(mutations) {
let reparsePost = false;
let sections = new Set();

for (let i = 0; i < mutations.length; i++) {
if (reparsePost) {
break;
}

let nodes = this._findTargetNodes(mutations[i]);

for (let j=0; j < nodes.length; j++) {
let section = this._findSectionFromNode(nodes[j]);
if (section) {
sections.add(section);
} else {
reparsePost = true;
break;
}
}
}

if (reparsePost) {
this.reparsePost();
} else if (sections.length) {
this.reparseSections(sections.toArray());
}
}

_findTargetNodes(mutation) {
let nodes = [];
switch (mutation.type) {
case MUTATION.CHARACTER_DATA:
nodes.push(mutation.target);
break;
case MUTATION.NODES_CHANGED:
forEach(mutation.addedNodes, n => nodes.push(n));
if (mutation.removedNodes.length) {
nodes.push(mutation.target);
}
break;
}

let attachedNodes = filter(nodes, node => !!node.parentNode);
return attachedNodes;
}

_findSectionFromNode(node) {
let rn = this.renderTree.findRenderNodeFromElement(node, (rn) => {
return rn.postNode.isSection;
});
return rn && rn.postNode;
}
}
1 change: 1 addition & 0 deletions src/js/models/_section.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export default class Section extends LinkedItem {
super();
assert('Cannot create section without type', !!type);
this.type = type;
this.isSection = true;
this.isMarkerable = false;
this.isNested = false;
this.isSection = true;
Expand Down
6 changes: 6 additions & 0 deletions src/js/models/render-tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export default class RenderTree {
get rootNode() {
return this._rootNode;
}
/**
* @return {Boolean}
*/
get isDirty() {
return this.rootNode && this.rootNode.isDirty;
}
/*
* @return {DOMNode} The root DOM element in this tree
*/
Expand Down
1 change: 1 addition & 0 deletions src/js/parsers/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ export default class DOMParser {
renderNode = renderTree.buildRenderNode(marker);
renderNode.element = textNode;
renderNode.markClean();
section.renderNode.markDirty();

let previousRenderNode = previousMarker && previousMarker.renderNode;
section.markers.insertAfter(marker, previousMarker);
Expand Down
4 changes: 4 additions & 0 deletions src/js/utils/set.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ export default class Set {
}
}

get length() {
return this.items.length;
}

has(item) {
return this.items.indexOf(item) !== -1;
}
Expand Down
Loading

0 comments on commit 34ab629

Please sign in to comment.