Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add forward and backward deletion to PostEditor #83

Merged
merged 1 commit into from
Aug 18, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


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
12 changes: 12 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,14 @@ const Key = class Key {
this.keyCode === Keycodes.DELETE;
}

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

get 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
28 changes: 28 additions & 0 deletions tests/helpers/mobiledoc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import PostNodeBuilder from 'content-kit-editor/models/post-node-builder';
import MobiledocRenderer from 'content-kit-editor/renderers/mobiledoc';

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

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

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

export default {
build
};
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