Skip to content

Commit

Permalink
Add forward and backward deletion to PostEditor
Browse files Browse the repository at this point in the history
fixes #36
  • Loading branch information
bantic committed Aug 17, 2015
1 parent b7b9694 commit faf6a63
Show file tree
Hide file tree
Showing 8 changed files with 327 additions and 99 deletions.
49 changes: 22 additions & 27 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,13 @@ class Editor {
element.setAttribute('contentEditable', true);

if (this.mobiledoc) {
this.parseModelFromMobiledoc(this.mobiledoc);
this.post = new MobiledocParser(this.builder).parse(this.mobiledoc);
} else {
this.parseModelFromDOM(this.element);
this.post = new DOMParser(this.builder).parse(this.element);
}

this._renderTree = this.prepareRenderTree(this.post);

clearChildNodes(element);
this.rerender();

Expand Down Expand Up @@ -276,21 +278,11 @@ class Editor {
this._views.push(view);
}

parseModelFromDOM(element) {
let parser = new DOMParser(this.builder);
this.post = 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(this.builder).parse(mobiledoc);
this._renderTree = new RenderTree();
let node = this._renderTree.buildRenderNode(this.post);
this._renderTree.node = node;
this.trigger('update');
prepareRenderTree(post) {
let renderTree = new RenderTree();
let node = renderTree.buildRenderNode(post);
renderTree.node = node;
return renderTree;
}

rerender() {
Expand All @@ -308,20 +300,23 @@ class Editor {
handleDeletion(event) {
event.preventDefault();

let offsets = this.cursor.offsets;
let currentMarker, currentOffset;
this.run((postEditor) => {
let results;

const results = this.run(postEditor => {
const offsets = this.cursor.offsets;

if (this.cursor.hasSelection()) {
results = postEditor.deleteRange(offsets);
return postEditor.deleteRange(offsets);
} else {
// FIXME: perhaps this should accept this.cursor.offsets?
results = postEditor.deleteCharAt(offsets.headMarker, offsets.headOffset-1);
const {headMarker, headOffset} = offsets;
const key = Key.fromEvent(event);

const deletePosition = {marker: headMarker, offset: headOffset},
direction = key.direction;
return postEditor.deleteFrom(deletePosition, direction);
}
currentMarker = results.currentMarker;
currentOffset = results.currentOffset;
});
this.cursor.moveToMarker(currentMarker, currentOffset);

this.cursor.moveToMarker(results.currentMarker, results.currentOffset);
}

handleNewline(event) {
Expand Down
165 changes: 96 additions & 69 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { MARKUP_SECTION_TYPE } from '../models/markup-section';

import { DIRECTION } from '../utils/key';

function isMarkupSection(section) {
return section.type === MARKUP_SECTION_TYPE;
}

class PostEditor {
constructor(editor) {
this.editor = editor;
Expand Down Expand Up @@ -68,94 +75,114 @@ class PostEditor {
}

/**
* Remove a character from a marker.
* Remove a character from a {marker, offset} position, in either
* forward or backward (default) direction.
*
* Usage:
*
* let marker = editor.post.sections.head.markers.head;
* // marker has text of "Howdy!"
* editor.run((postEditor) => {
* postEditor.deleteCharAt(marker, 3);
* postEditor.deleteFrom({marker, offset: 3});
* });
* // marker has text of "Hody!"
*
* `deleteCharAt` may remove a character from a different marker or section
* if the position of the deletion is at the 0th offset. Offset behaves like
* a cursor position, with the deletion going to the previous character.
* `deleteFrom` may remove a character from a different marker or join the
* marker's section with the previous/next section (depending on the
* deletion direction) if direction is `BACKWARD` and the offset is 0,
* or direction is `FORWARD` and the offset is equal to the length of the
* marker.
*
* @method deleteCharAt
* @param {Object} marker the marker to delete the character from
* @param {Object} offset offset in the text of the marker to delete, first character is 1
* @return {Object} {currentMarker, currentOffset} for cursor
* @method deleteFrom
* @param {Object} position object with {marker, offset} the marker and offset to delete from
* @param {Number} direction The direction to delete in (default is BACKWARD)
* @return {Object} {currentMarker, currentOffset} for positioning the cursor
* @public
*/
deleteCharAt(marker, offset) {
// need to handle these cases:
// when cursor is:
// * 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 = marker;
let nextCursorMarker = currentMarker;
let nextCursorOffset = offset;
let renderNode = marker.renderNode;

// A: in the middle of a marker
if (offset >= 0) {
currentMarker.deleteValueAtOffset(offset);
if (currentMarker.length === 0 && currentMarker.section.markers.length > 1) {
if (marker.renderNode) {
marker.renderNode.scheduleForRemoval();
}
deleteFrom({marker, offset}, direction=DIRECTION.BACKWARD) {
if (direction === DIRECTION.BACKWARD) {
return this._deleteBackwardFrom({marker, offset});
} else {
return this._deleteForwardFrom({marker, offset});
}
}

let isFirstRenderNode = renderNode === renderNode.parent.childNodes.head;
if (isFirstRenderNode) {
// move cursor to start of next node
nextCursorMarker = renderNode.next.postNode;
nextCursorOffset = 0;
} else {
// move cursor to end of prev node
nextCursorMarker = renderNode.prev.postNode;
nextCursorOffset = renderNode.prev.postNode.length;
}
/**
* delete 1 character in the FORWARD direction from the given position
* @method _deleteForwardFrom
* @private
*/
_deleteForwardFrom({marker, offset}) {
const nextCursorMarker = marker,
nextCursorOffset = offset;

if (offset === marker.length) {
const nextMarker = marker.next;

if (nextMarker) {
this._deleteForwardFrom({marker: nextMarker, offset: 0});
} else {
renderNode.markDirty();
const nextSection = marker.section.next;
if (nextSection && isMarkupSection(nextSection)) {
const currentSection = marker.section;

currentSection.join(nextSection);

currentSection.renderNode.markDirty();
nextSection.renderNode.scheduleForRemoval();
}
}
} else {
let currentSection = currentMarker.section;
let previousMarker = currentMarker.prev;
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 = currentSection.prev;
if (previousSection) {
let isMarkupSection = previousSection.type === MARKUP_SECTION_TYPE;

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

nextCursorMarker = lastPreviousMarker.next;
nextCursorOffset = 0;
/*
} else {
// card section: ??
*/
marker.deleteValueAtOffset(offset);
marker.renderNode.markDirty();
}

this.scheduleRerender();
this.scheduleDidUpdate();

return {
currentMarker: nextCursorMarker,
currentOffset: nextCursorOffset
};
}

/**
* delete 1 character in the BACKWARD direction from the given position
* @method _deleteBackwardFrom
* @private
*/
_deleteBackwardFrom({marker, offset}) {
let nextCursorMarker = marker,
nextCursorOffset = offset;

if (offset === 0) {
const prevMarker = marker.prev;

if (prevMarker) {
return this._deleteBackwardFrom({marker: prevMarker, offset: prevMarker.length});
} else {
const prevSection = marker.section.prev;

if (prevSection) {
if (isMarkupSection(prevSection)) {
nextCursorMarker = prevSection.markers.tail;
nextCursorOffset = nextCursorMarker.length;

prevSection.join(marker.section);
prevSection.renderNode.markDirty();
marker.section.renderNode.scheduleForRemoval();
}
} else { // no previous section -- do nothing
nextCursorMarker = currentMarker;
nextCursorOffset = 0;
// ELSE: FIXME: card section -- what should deleting into it do?
}
}

} else if (offset <= marker.length) {
const offsetToDeleteAt = offset - 1;

marker.deleteValueAtOffset(offsetToDeleteAt);
marker.renderNode.markDirty();

nextCursorOffset = offsetToDeleteAt;
}

this.scheduleRerender();
Expand Down
5 changes: 4 additions & 1 deletion src/js/models/post-node-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ export default class PostNodeBuilder {
this.markupCache = {};
}

createPost() {
createPost(sections=[]) {
const post = new Post();
post.builder = this;

sections.forEach(s => post.sections.append(s));

return post;
}

Expand Down
16 changes: 16 additions & 0 deletions src/js/utils/key.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import Keycodes from './keycodes';
export const DIRECTION = {
FORWARD: 1,
BACKWARD: 2
};

/**
* An abstraction around a KeyEvent
Expand All @@ -24,6 +28,18 @@ const Key = class Key {
this.keyCode === Keycodes.DELETE;
}

isForwardDelete() {
return this.keyCode === Keycodes.DELETE;
}


get direction() {
if (!this.isDelete()) {
throw new Error('Only delete keys have direction');
}
return this.isForwardDelete() ? DIRECTION.FORWARD : DIRECTION.BACKWARD;
}

isSpace() {
return this.keyCode === Keycodes.SPACE;
}
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function triggerDelete(editor) {
let event = { preventDefault() {} };
editor.handleDeletion(event);
} else {
triggerKeyEvent(document, 'keydown', KEY_CODES.DELETE);
triggerKeyEvent(document, 'keydown', KEY_CODES.BACKSPACE);
}
}

Expand Down
26 changes: 26 additions & 0 deletions tests/helpers/mobiledoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import PostNodeBuilder from 'content-kit-editor/models/post-node-builder';
import MobiledocRenderer from 'content-kit-editor/renderers/mobiledoc';

/*
* makeMD({post, section, marker, markup} =>
post([
section('P', [
marker('some text', [markup('B')])
])
})
*/
function makeMD(treeFn) {
let builder = new PostNodeBuilder();

const post = (...args) => builder.createPost(...args);
const section = (...args) => builder.createMarkupSection(...args);
const markup = (...args) => builder.createMarkup(...args);
const marker = (...args) => builder.createMarker(...args);

let builtPost = treeFn({post, section, markup, marker});
return MobiledocRenderer.render(builtPost);
}

export default {
makeMD
};
4 changes: 3 additions & 1 deletion tests/test-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ registerAssertions();
import DOMHelpers from './helpers/dom';
import ToolbarHelpers from './helpers/toolbar';
import skipInPhantom from './helpers/skip-in-phantom';
import MobiledocHelpers from './helpers/mobiledoc';

export default {
dom: DOMHelpers,
toolbar: ToolbarHelpers,
skipInPhantom
skipInPhantom,
mobiledoc: MobiledocHelpers
};
Loading

0 comments on commit faf6a63

Please sign in to comment.