Skip to content

Commit

Permalink
Handle different types of deletion
Browse files Browse the repository at this point in the history
fixes #37
  • Loading branch information
bantic committed Aug 7, 2015
1 parent 10f0f72 commit 9998dbb
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 2 deletions.
38 changes: 37 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,38 @@ class Editor {
this._renderer.render(this._renderTree);
}

deleteSelection(event) {
event.preventDefault();

// types of selection deletion:
// * a selection starts at the beginning of a section
// -- cursor should end up at the beginning of that section
// -- if the section not longer has markers, add a blank one for the cursor to focus on
// * a selection is entirely within a section
// -- split the markers with the selection, remove those new markers from their section
// -- cursor goes at end of the marker before the selection start, or if the
// -- selection was at the start of the section, cursor goes at section start
// * a selection crosses multiple sections
// -- remove all the sections that are between (exclusive ) selection start and end
// -- join the start and end sections
// -- mark the end section for removal
// -- cursor goes at end of marker before the selection start

const markers = this.splitMarkersFromSelection();

const {changedSections, removedSections, currentMarker, currentOffset} = this.post.cutMarkers(markers);

changedSections.forEach(section => section.renderNode.markDirty());
removedSections.forEach(section => section.renderNode.scheduleForRemoval());

this.rerender();

let currentTextNode = currentMarker.renderNode.element;
this.cursor.moveToNode(currentTextNode, currentOffset);

this.trigger('update');
}

// FIXME ensure we handle deletion when there is a selection
handleDeletion(event) {
let {
Expand All @@ -323,6 +355,11 @@ class Editor {
// * C offset is 0 and there is no previous marker
// * join this section with previous section

if (this.cursor.hasSelection()) {
this.deleteSelection(event);
return;
}

const currentMarker = leftRenderNode.postNode;
let nextCursorMarker = currentMarker;
let nextCursorOffset = leftOffset - 1;
Expand Down Expand Up @@ -558,7 +595,6 @@ class Editor {
this.hasSelection();
}


get cursor() {
return new Cursor(this);
}
Expand Down
6 changes: 6 additions & 0 deletions src/js/models/cursor.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,12 @@ export default class Cursor {
this.moveToNode(startNode, startOffset, endNode, endOffset);
}

/**
* @param {textNode} node
* @param {integer} offset
* @param {textNode} endNode (default: node)
* @param {integer} endOffset (default: offset)
*/
moveToNode(node, offset=0, endNode=node, endOffset=offset) {
let r = document.createRange();
r.setStart(node, offset);
Expand Down
4 changes: 4 additions & 0 deletions src/js/models/marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import {
import { detect } from 'content-kit-editor/utils/array-utils';

const Marker = class Marker {
static createBlank() {
return new Marker('');
}

constructor(value='', markups=[]) {
this.value = value;
this.markups = [];
Expand Down
4 changes: 4 additions & 0 deletions src/js/models/markup-section.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export default class Section {
return this._tagName;
}

isEmpty() {
return this.markers.length === 0;
}

setTagName(newTagName) {
newTagName = normalizeTagName(newTagName);
if (VALID_MARKUP_SECTION_TAGNAMES.indexOf(newTagName) === -1) {
Expand Down
50 changes: 49 additions & 1 deletion src/js/models/post.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Marker from './marker';
export const POST_TYPE = 'post';

// FIXME: making sections a linked-list would greatly improve this
Expand All @@ -19,6 +20,53 @@ export default class Post {
this.insertSectionAfter(newSection, section);
this.removeSection(section);
}
cutMarkers(markers) {
let firstSection = markers[0].section,
lastSection = markers[markers.length - 1].section;

let currentSection = firstSection;
let removedSections = [],
changedSections = [firstSection, lastSection];

let previousMarker = markers[0].previousSibling;

markers.forEach(marker => {
if (marker.section !== currentSection) { // this marker is in a section we haven't seen yet
if (marker.section !== firstSection &&
marker.section !== lastSection) {
// section is wholly contained by markers, and can be removed
removedSections.push(marker.section);
}
}

currentSection = marker.section;
currentSection.removeMarker(marker);
});

// add a blank marker to any sections that are now empty
changedSections.forEach(section => {
if (section.isEmpty()) {
section.appendMarker(Marker.createBlank());
}
});

let currentMarker, currentOffset;

if (previousMarker) {
currentMarker = previousMarker;
currentOffset = currentMarker.length;
} else {
currentMarker = firstSection.markers[0];
currentOffset = 0;
}

if (firstSection !== lastSection) {
firstSection.join(lastSection);
removedSections.push(lastSection);
}

return {changedSections, removedSections, currentMarker, currentOffset};
}
/**
* Invoke `callbackFn` for all markers between the startMarker and endMarker (inclusive),
* across sections
Expand All @@ -34,7 +82,7 @@ export default class Post {
currentMarker = currentMarker.nextSibling;
} else {
let nextSection = currentMarker.section.nextSibling;
currentMarker = nextSection.markers[0];
currentMarker = nextSection && nextSection.markers[0];
}
}
}
Expand Down
110 changes: 110 additions & 0 deletions tests/acceptance/editor-selections-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,113 @@ test('selecting across sections is possible', (assert) => {
done();
});
});

test('selecting an entire section and deleting removes it', (assert) => {
const done = assert.async();

editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections});

Helpers.dom.selectText('second section', editorElement);
Helpers.dom.triggerKeyEvent(document, 'keydown', Helpers.dom.KEY_CODES.DELETE);

assert.hasElement('p:contains(first section)');
assert.hasNoElement('p:contains(second section)', 'deletes contents of second section');
assert.equal($('#editor p').length, 2, 'still has 2 sections');

let textNode = editorElement
.childNodes[1] // second section p
.childNodes[0]; // textNode

assert.deepEqual(Helpers.dom.getCursorPosition(),
{node: textNode, offset: 0});

done();
});

test('selecting text in a section and deleting deletes it', (assert) => {
editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections});

Helpers.dom.selectText('cond sec', editorElement);
Helpers.dom.triggerKeyEvent(document, 'keydown', Helpers.dom.KEY_CODES.DELETE);

assert.hasElement('p:contains(first section)', 'first section unchanged');
assert.hasNoElement('p:contains(second section)', 'second section is no longer there');
assert.hasElement('p:contains(setion)', 'second section has correct text');

let textNode = $('p:contains(setion)')[0].childNodes[0];
assert.equal(textNode.textContent, 'se', 'precond - has correct text node');
let charOffset = 2; // after the 'e' in 'se'

assert.deepEqual(Helpers.dom.getCursorPosition(),
{node: textNode, offset: charOffset});
});

test('selecting text across sections and deleting joins sections', (assert) => {
editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections});

const firstSection = $('#editor p')[0],
secondSection = $('#editor p')[1];

Helpers.dom.selectText('t section', firstSection,
'second s', secondSection);
Helpers.dom.triggerKeyEvent(document, 'keydown', Helpers.dom.KEY_CODES.DELETE);

assert.hasElement('p:contains(firsection)');
assert.hasNoElement('p:contains(first section)');
assert.hasNoElement('p:contains(second section)');
assert.equal($('#editor p').length, 1, 'only 1 section after deleting to join');
});

function getToolbarButton(assert, name) {
let btnSelector = `.ck-toolbar-btn[title="${name}"]`;
return assert.hasElement(btnSelector);
}

function clickToolbarButton(assert, name) {
const button = getToolbarButton(assert, name);
Helpers.dom.triggerEvent(button[0], 'click');
}

test('selecting text across markers and deleting joins markers', (assert) => {
const done = assert.async();

editor = new Editor(editorElement, {mobiledoc: mobileDocWith2Sections});

Helpers.dom.selectText('rst sect', editorElement);
Helpers.dom.triggerEvent(document, 'mouseup');

setTimeout(() => {
clickToolbarButton(assert, 'bold');

let firstTextNode = editorElement
.childNodes[0] // p
.childNodes[1] // b
.childNodes[0]; // textNode containing "rst sect"
let secondTextNode = editorElement
.childNodes[0] // p
.childNodes[2]; // textNode containing "ion"

assert.equal(firstTextNode.textContent, 'rst sect', 'correct first text node');
assert.equal(secondTextNode.textContent, 'ion', 'correct second text node');
Helpers.dom.selectText('t sect', firstTextNode,
'ion', secondTextNode);
Helpers.dom.triggerKeyEvent(document, 'keydown', Helpers.dom.KEY_CODES.DELETE);

assert.hasElement('p:contains(firs)', 'deletes across markers');
assert.hasElement('strong:contains(rs)', 'maintains bold text');

firstTextNode = editorElement
.childNodes[0] // p
.childNodes[1] // b
.childNodes[0]; // textNode now containing "rs"

assert.deepEqual(Helpers.dom.getCursorPosition(),
{node: firstTextNode, offset: 2});

done();
});
});

// test selecting text across markers deletes intermediary markers
// test selecting text that includes entire sections deletes the sections
// test selecting text and hitting enter or keydown

0 comments on commit 9998dbb

Please sign in to comment.