diff --git a/src/js/editor/post.js b/src/js/editor/post.js index 569b609bb..2469c4a78 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -212,7 +212,10 @@ class PostEditor { while (marker && marker.next) { nextMarker = marker.next; - if (isArrayEqual(marker.markups, nextMarker.markups)) { + if ( + marker.type === nextMarker.type && + isArrayEqual(marker.markups, nextMarker.markups) + ) { nextMarker.value = marker.value + nextMarker.value; this._markDirty(nextMarker); this.removeMarker(marker); @@ -450,6 +453,15 @@ class PostEditor { return nextPosition; } + /** + * delete 1 character forward from the markerPosition, which in turn is + * a {marker, offset} object. + * + * @method _deleteForwardFromMarkerPosition + * @param {Object} markerPosition {marker, offset} + * @return {Position} The position the cursor should be put after this deletion + * @private + */ _deleteForwardFromMarkerPosition(markerPosition) { const {marker, offset} = markerPosition; const {section} = marker; @@ -472,6 +484,8 @@ class PostEditor { this.removeSection(nextSection); } } + } else if (marker.length === 1 && offset === 0) { + this.removeMarker(marker); } else { marker.deleteValueAtOffset(offset); this._markDirty(marker); @@ -501,22 +515,24 @@ class PostEditor { } } - let nextPosition = position.clone(); - // if position is end of a card, replace the card with a markup section if (section.isCardSection) { let newSection = this.builder.createMarkupSection(); this.replaceSection(section, newSection); return newSection.headPosition(); } - - const { marker, offset:markerOffset } = position.markerPosition; + let nextPosition = position.moveLeft(); + + const { marker, offset:markerOffset } = position.markerPosition; const offsetToDeleteAt = markerOffset - 1; - let lengthChange = marker.deleteValueAtOffset(offsetToDeleteAt); - nextPosition.offset -= lengthChange; - this._markDirty(marker); + if (marker.length === 1 && offsetToDeleteAt === 0) { + this.removeMarker(marker); + } else { + marker.deleteValueAtOffset(offsetToDeleteAt); + this._markDirty(marker); + } return nextPosition; } diff --git a/src/js/models/marker.js b/src/js/models/marker.js index 7b4fc341c..0d786b9b3 100644 --- a/src/js/models/marker.js +++ b/src/js/models/marker.js @@ -11,8 +11,8 @@ import assert from '../utils/assert'; // high- and low-surrogate characters. // See "high surrogate" and "low surrogate" on // https://en.wikipedia.org/wiki/Unicode_block -const HIGH_SURROGATE_RANGE = [0xD800, 0xDBFF]; -const LOW_SURROGATE_RANGE = [0xDC00, 0xDFFF]; +export const HIGH_SURROGATE_RANGE = [0xD800, 0xDBFF]; +export const LOW_SURROGATE_RANGE = [0xDC00, 0xDFFF]; const Marker = class Marker extends LinkedItem { constructor(value='', markups=[]) { diff --git a/src/js/utils/cursor/position.js b/src/js/utils/cursor/position.js index 3720157b0..85af76538 100644 --- a/src/js/utils/cursor/position.js +++ b/src/js/utils/cursor/position.js @@ -3,6 +3,10 @@ import { } from 'mobiledoc-kit/utils/dom-utils'; import { DIRECTION } from 'mobiledoc-kit/utils/key'; import assert from 'mobiledoc-kit/utils/assert'; +import { + HIGH_SURROGATE_RANGE, + LOW_SURROGATE_RANGE +} from 'mobiledoc-kit/models/marker'; function findParentSectionFromNode(renderTree, node) { let renderNode = renderTree.findRenderNodeFromElement( @@ -134,7 +138,14 @@ const Position = class Position { let prev = this.section.previousLeafSection(); return prev && prev.tailPosition(); } else { - return new Position(this.section, this.offset - 1); + let offset = this.offset - 1; + if (!this.section.isCardSection && this.marker.value) { + let code = this.marker.value.charCodeAt(offset); + if (code >= LOW_SURROGATE_RANGE[0] && code <= LOW_SURROGATE_RANGE[1]) { + offset = offset - 1; + } + } + return new Position(this.section, offset); } } @@ -146,7 +157,14 @@ const Position = class Position { let next = this.section.nextLeafSection(); return next && next.headPosition(); } else { - return new Position(this.section, this.offset + 1); + let offset = this.offset + 1; + if (!this.section.isCardSection && this.marker.value) { + let code = this.marker.value.charCodeAt(offset); + if (code >= HIGH_SURROGATE_RANGE[0] && code <= HIGH_SURROGATE_RANGE[1]) { + offset = offset + 1; + } + } + return new Position(this.section, offset); } } diff --git a/tests/acceptance/editor-atoms-test.js b/tests/acceptance/editor-atoms-test.js index 613843317..41c7d72f1 100644 --- a/tests/acceptance/editor-atoms-test.js +++ b/tests/acceptance/editor-atoms-test.js @@ -62,3 +62,53 @@ test('keystroke of character in section with atom keeps atom', (assert) => { assert.hasElement(`#editor #simple-atom`, 'still has atom'); }); + +test('keystroke of delete removes character after atom', (assert) => { + editor = new Editor({mobiledoc: mobiledocWithAtom, atoms: [simpleAtom]}); + editor.render(editorElement); + + let pNode = $('#editor p')[0]; + Helpers.dom.moveCursorTo(pNode.lastChild, 1); + Helpers.dom.triggerDelete(editor); + + assert.postIsSimilar(editor.post, Helpers.postAbstract.build( + ({post, markupSection, atom, marker}) => { + return post([markupSection('p', [ + marker('text before atom'), + atom('simple-atom', 'Bob'), + marker('ext after atom') + ])]); + })); +}); + +test('keystroke of delete removes atom', (assert) => { + editor = new Editor({mobiledoc: mobiledocWithAtom, atoms: [simpleAtom]}); + editor.render(editorElement); + + let pNode = $('#editor p')[0]; + Helpers.dom.moveCursorTo(pNode.lastChild, 0); + Helpers.dom.triggerDelete(editor); + + assert.postIsSimilar(editor.post, Helpers.postAbstract.build( + ({post, markupSection, atom, marker}) => { + return post([markupSection('p', [ + marker('text before atomtext after atom') + ])]); + })); +}); + +test('keystroke of forward delete removes atom', (assert) => { + editor = new Editor({mobiledoc: mobiledocWithAtom, atoms: [simpleAtom]}); + editor.render(editorElement); + + let pNode = $('#editor p')[0]; + Helpers.dom.moveCursorTo(pNode.firstChild, 16); + Helpers.dom.triggerForwardDelete(editor); + + assert.postIsSimilar(editor.post, Helpers.postAbstract.build( + ({post, markupSection, atom, marker}) => { + return post([markupSection('p', [ + marker('text before atomtext after atom') + ])]); + })); +}); diff --git a/tests/acceptance/editor-sections-test.js b/tests/acceptance/editor-sections-test.js index 9e6c9952a..463f2a510 100644 --- a/tests/acceptance/editor-sections-test.js +++ b/tests/acceptance/editor-sections-test.js @@ -276,7 +276,7 @@ test('keystroke of delete removes emoji character', (assert) => { editor = new Editor({mobiledoc}); editor.render(editorElement); let textNode = editorElement.firstChild. // section - firstChild; // marker + firstChild; // marker assert.equal(textNode.textContent, monkey, 'precond - correct text'); Helpers.dom.moveCursorTo(textNode, monkey.length); @@ -293,7 +293,7 @@ test('keystroke of forward delete removes emoji character', (assert) => { editor = new Editor({mobiledoc}); editor.render(editorElement); let textNode = editorElement.firstChild. // section - firstChild; // marker + firstChild; // marker assert.equal(textNode.textContent, monkey, 'precond - correct text'); Helpers.dom.moveCursorTo(textNode, 'monkey'.length); diff --git a/tests/helpers/assertions.js b/tests/helpers/assertions.js index 1e2c6fd96..6fc2c7bb1 100644 --- a/tests/helpers/assertions.js +++ b/tests/helpers/assertions.js @@ -10,7 +10,8 @@ import { POST_TYPE, LIST_ITEM_TYPE, CARD_TYPE, - IMAGE_SECTION_TYPE + IMAGE_SECTION_TYPE, + ATOM_TYPE } from 'mobiledoc-kit/models/types'; function comparePostNode(actual, expected, assert, path='root', deepCompare=false) { @@ -36,6 +37,7 @@ function comparePostNode(actual, expected, assert, path='root', deepCompare=fals }); } break; + case ATOM_TYPE: case MARKER_TYPE: if (actual.value !== expected.value) { assert.equal(actual.value, expected.value, `wrong value at ${path}`);