Skip to content

Commit

Permalink
Handle deletion (without selection) semantically
Browse files Browse the repository at this point in the history
  • Loading branch information
bantic committed Jul 31, 2015
1 parent be00508 commit 5febfc4
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 49 deletions.
4 changes: 4 additions & 0 deletions notes
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
editor actions:
* hitting enter multiple times to create arbitrary space (prevent or allow plugin-based validation of the AST)
* maintain header hierarchy (no h2 without a prior h1, no h3 w/out prior h2, etc)

abc|def|ghi

i=0, length=0, offset=3
Expand Down
105 changes: 97 additions & 8 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import EventEmitter from '../utils/event-emitter';

import MobiledocParser from "../parsers/mobiledoc";
import PostParser from '../parsers/post';
import Renderer from 'content-kit-editor/renderers/editor-dom';
import Renderer, { UNPRINTABLE_CHARACTER } from 'content-kit-editor/renderers/editor-dom';
import RenderTree from 'content-kit-editor/models/render-tree';
import MobiledocRenderer from '../renderers/mobiledoc';

Expand Down Expand Up @@ -279,6 +279,7 @@ class Editor {
this._renderer.render(this._renderTree);
}

// FIXME ensure we handle deletion when there is a selection
handleDeletion(event) {
let {
leftRenderNode,
Expand All @@ -287,25 +288,74 @@ class Editor {

// need to handle these cases:
// when cursor is:
// * in the middle of a marker
// * offset is 0 and there is a previous marker
// * offset is 0 and there is no previous marker
// * A in the middle of a marker -- just delete the character
// * B offset is 0 and there is a previous marker
// * delete last char of previous marker
// * C offset is 0 and there is no previous marker
// * join this section with previous section

const currentMarker = leftRenderNode.postNode;
let nextCursorMarker = currentMarker;
let nextCursorOffset = leftOffset - 1;

// A: in the middle of a marker
if (leftOffset !== 0) {
currentMarker.deleteValueAtOffset(leftOffset-1);
leftRenderNode.markDirty();
if (currentMarker.length === 0 && currentMarker.section.markers.length > 1) {
leftRenderNode.scheduleForRemoval();

let isFirstRenderNode = leftRenderNode === leftRenderNode.parentNode.firstChild;
if (isFirstRenderNode) {
// move cursor to start of next node
nextCursorMarker = leftRenderNode.nextSibling.postNode;
nextCursorOffset = 0;
} else {
// move cursor to end of prev node
nextCursorMarker = leftRenderNode.previousSibling.postNode;
nextCursorOffset = leftRenderNode.previousSibling.postNode.length;
}
} else {
leftRenderNode.markDirty();
}
} else {
let currentSection = currentMarker.section;
let previousMarker = currentMarker.previousSibling;
if (previousMarker) {
if (previousMarker) { // (B)
let markerLength = previousMarker.length;
previousMarker.deleteValueAtOffset(markerLength - 1);
} else { // (C)
// possible previous sections:
// * none -- do nothing
// * markup section -- join to it
// * non-markup section (card) -- select it? delete it?
let previousSection = this.post.getPreviousSection(currentSection);
if (previousSection) {
let isMarkupSection = previousSection.type === MARKUP_SECTION_TYPE;

if (isMarkupSection) {
let previousSectionMarkerLength = previousSection.markers.length;
previousSection.join(currentSection);
previousSection.renderNode.markDirty();
currentSection.renderNode.scheduleForRemoval();

nextCursorMarker = previousSection.markers[previousSectionMarkerLength];
nextCursorOffset = 0;
/*
} else {
// card section: ??
*/
}
} else { // no previous section -- do nothing
nextCursorMarker = currentMarker;
nextCursorOffset = 0;
}
}
}

this.rerender();

this.cursor.moveToNode(leftRenderNode.element, leftOffset-1);
this.cursor.moveToNode(nextCursorMarker.renderNode.element,
nextCursorOffset);

this.trigger('update');
event.preventDefault();
Expand Down Expand Up @@ -440,7 +490,7 @@ class Editor {
* 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();
this.trigger('update');
Expand Down Expand Up @@ -501,8 +551,47 @@ class Editor {
}
});

let {
leftRenderNode,
leftOffset,
rightRenderNode,
rightOffset
} = this.cursor.offsets;

// The cursor will lose its textNode if we have parsed (and thus rerendered)
// its section. Ensure the cursor is placed where it should be after render.
//
// New sections are presumed clean, and thus do not get rerendered and lose
// their cursor position.
//
let resetCursor = (leftRenderNode &&
sectionsWithCursor.indexOf(leftRenderNode.postNode.section) !== -1);

if (resetCursor) {
let unprintableOffset = leftRenderNode.element.textContent.indexOf(UNPRINTABLE_CHARACTER);
if (unprintableOffset !== -1) {
leftRenderNode.markDirty();
if (unprintableOffset < leftOffset) {
// FIXME: we should move backward/forward some number of characters
// with a method on markers that returns the relevent marker and
// offset (may not be the marker it was called with);
leftOffset--;
rightOffset--;
}
}
}

this.rerender();
this.trigger('update');

if (resetCursor) {
this.cursor.moveToNode(
leftRenderNode.element,
leftOffset,
rightRenderNode.element,
rightOffset
);
}
}

getSectionsWithCursor() {
Expand Down
9 changes: 9 additions & 0 deletions src/js/models/card.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const CARD_TYPE = 'card-section';

export default class Card {
constructor(name, payload) {
this.name = name;
this.payload = payload;
this.type = CARD_TYPE;
}
}
4 changes: 2 additions & 2 deletions src/js/models/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,10 @@ const Cursor = class Cursor {
selection.addRange(r);
}

moveToNode(node, offset=0) {
moveToNode(node, offset=0, endNode=node, endOffset=offset) {
let r = document.createRange();
r.setStart(node, offset);
r.setEnd(node, offset);
r.setEnd(endNode, endOffset);
const selection = this.selection;
if (selection.rangeCount > 0) {
selection.removeAllRanges();
Expand Down
5 changes: 5 additions & 0 deletions src/js/models/markup-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export default class Section {
];
}

// mutates this by appending the other section's (cloned) markers to it
join(otherSection) {
otherSection.markers.forEach(m => this.appendMarker(m.clone()));
}

/**
* A marker contains this offset if:
* * The offset is between the marker's start and end
Expand Down
1 change: 1 addition & 0 deletions src/js/models/render-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default class RenderNode {
this.postNode = postNode;

this.firstChild = null;
this.lastChild = null;
this.nextSibling = null;
this.previousSibling = null;
}
Expand Down
10 changes: 7 additions & 3 deletions src/js/parsers/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,13 @@ export default {

let renderNode = renderTree.elements.get(textNode);
if (renderNode) {
marker = renderNode.postNode;
marker.value = text;
marker.markups = markups;
if (text.length) {
marker = renderNode.postNode;
marker.value = text;
marker.markups = markups;
} else {
renderNode.scheduleForRemoval();
}
} else {
marker = generateBuilder().generateMarker(markups, text);

Expand Down
60 changes: 29 additions & 31 deletions src/js/renderers/editor-dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { POST_TYPE } from "../models/post";
import { MARKUP_SECTION_TYPE } from "../models/markup-section";
import { MARKER_TYPE } from "../models/marker";
import { IMAGE_SECTION_TYPE } from "../models/image";
import { CARD_TYPE } from "../models/card";
import { clearChildNodes } from '../utils/dom-utils';

export const UNPRINTABLE_CHARACTER = "\u200C";

Expand All @@ -18,10 +20,13 @@ function createElementFromMarkup(doc, markup) {
return element;
}

// ascends from element upward, returning the last parent node that is not
// parentElement
function penultimateParentOf(element, parentElement) {
while (parentElement &&
element.parentNode !== parentElement &&
element.parentElement !== document.body) {
element.parentElement !== document.body // ensure the while loop stops
) {
element = element.parentNode;
}
return element;
Expand All @@ -37,7 +42,7 @@ function isEmptyText(text) {
return text.trim() === '';
}

// pass in a renderNode's previousSiblin
// pass in a renderNode's previousSibling
function getNextMarkerElement(renderNode) {
let element = renderNode.element.parentNode;
let closedCount = renderNode.postNode.closedMarkups.length;
Expand Down Expand Up @@ -111,29 +116,24 @@ class Visitor {
}
renderNode.element = element;
}

// remove all elements so that we can rerender
clearChildNodes(renderNode.element);

const visitAll = true;
visit(renderNode, section.markers, visitAll);
}

[MARKER_TYPE](renderNode, marker) {
let parentElement;

// delete previously existing element
if (renderNode.element) {
const elementForRemoval = penultimateParentOf(renderNode.element, renderNode.attachedTo);
if (elementForRemoval.parentNode) {
elementForRemoval.parentNode.removeChild(elementForRemoval);
}
}

if (renderNode.previousSibling) {
parentElement = getNextMarkerElement(renderNode.previousSibling);
} else {
parentElement = renderNode.parentNode.element;
}
let textNode = renderMarker(marker, parentElement, renderNode.previousSibling);

renderNode.attachedTo = parentElement;
renderNode.element = textNode;
}

Expand All @@ -159,7 +159,7 @@ class Visitor {
}
}

card(renderNode, section) {
[CARD_TYPE](renderNode, section) {
const card = detect(this.cards, card => card.name === section.name);

const env = { name: section.name };
Expand Down Expand Up @@ -216,7 +216,7 @@ let destroyHooks = {
renderNode.element.parentNode.removeChild(renderNode.element);
},

card(renderNode, section) {
[CARD_TYPE](renderNode, section) {
if (renderNode.cardNode) {
renderNode.cardNode.teardown();
}
Expand All @@ -226,6 +226,7 @@ let destroyHooks = {
}
};

// removes children from parentNode that are scheduled for removal
function removeChildren(parentNode) {
let child = parentNode.firstChild;
while (child) {
Expand All @@ -252,33 +253,30 @@ function lookupNode(renderTree, parentNode, postNode, previousNode) {
}
}

function renderInternal(renderTree, visitor) {
let nodes = [renderTree.node];
function visit(parentNode, postNodes, visitAll=false) {
export default class Renderer {
constructor(cards, unknownCardHandler, options) {
this.visitor = new Visitor(cards, unknownCardHandler, options);
this.nodes = [];
}

visit(renderTree, parentNode, postNodes, visitAll=false) {
let previousNode;
postNodes.forEach(postNode => {
let node = lookupNode(renderTree, parentNode, postNode, previousNode);
if (node.isDirty || visitAll) {
nodes.push(node);
this.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 Renderer {
constructor(cards, unknownCardHandler, options) {
this.visitor = new Visitor(cards, unknownCardHandler, options);
}

render(renderTree) {
renderInternal(renderTree, this.visitor);
let node = renderTree.node;
while (node) {
removeChildren(node);
this.visitor[node.postNode.type](node, node.postNode, (...args) => this.visit(renderTree, ...args));
node.markClean();
node = this.nodes.shift();
}
}
}
3 changes: 2 additions & 1 deletion src/js/renderers/mobiledoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MARKUP_SECTION_TYPE } from "../models/markup-section";
import { IMAGE_SECTION_TYPE } from "../models/image";
import { MARKER_TYPE } from "../models/marker";
import { MARKUP_TYPE } from "../models/markup";
import { CARD_TYPE } from "../models/card";

export const MOBILEDOC_VERSION = '0.1';

Expand All @@ -19,7 +20,7 @@ let visitor = {
[IMAGE_SECTION_TYPE](node, opcodes) {
opcodes.push(['openImageSection', node.src]);
},
card(node, opcodes) {
[CARD_TYPE](node, opcodes) {
opcodes.push(['openCardSection', node.name, node.payload]);
},
[MARKER_TYPE](node, opcodes) {
Expand Down
4 changes: 2 additions & 2 deletions src/js/utils/post-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MarkupSection from "../models/markup-section";
import ImageSection from "../models/image";
import Marker from "../models/marker";
import Markup from "../models/markup";
import Card from "../models/card";

var builder = {
generatePost() {
Expand All @@ -23,8 +24,7 @@ var builder = {
return section;
},
generateCardSection(name, payload={}) {
const type = 'card';
return { name, payload, type };
return new Card(name, payload);
},
generateMarker(markups, value) {
return new Marker(value, markups);
Expand Down
Loading

0 comments on commit 5febfc4

Please sign in to comment.