From b4e7cbac4e2fa31042002cb3f98d7d07d11fc543 Mon Sep 17 00:00:00 2001 From: Cory Forsyth Date: Thu, 10 Sep 2015 10:43:47 -0400 Subject: [PATCH] Tests for forward-delete in empty and at end of list items fix #118 --- src/js/editor/post.js | 61 ++++++++++++- tests/acceptance/editor-list-test.js | 131 +++++++++++++++++++++++++++ tests/helpers/dom.js | 17 +++- 3 files changed, 201 insertions(+), 8 deletions(-) diff --git a/src/js/editor/post.js b/src/js/editor/post.js index 3380fb19b..7d14d00c4 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -29,8 +29,9 @@ function isListSection(section) { return section.type === LIST_SECTION_TYPE; } -// finds the immediately preceding section that is markerable -function findPreviousMarkerableSection(section) { +// finds the immediately preceding section that is markerable, +// returning null if there is none +function findImmediatelyPreviousMarkerableSection(section) { const prev = section.prev; if (!prev) { return null; } if (isMarkerable(prev)) { @@ -40,6 +41,21 @@ function findPreviousMarkerableSection(section) { } } +function findImmediatelyNextMarkerableSection(section) { + const next = section.next; + if (next) { + if (isMarkerable(next)) { + return next; + } else if (isListSection(next)) { + const firstListItem = next.items.head; + return firstListItem; + } + } else if (isListItem(section)) { + const listSection = section.parent; + return findImmediatelyNextMarkerableSection(listSection); + } +} + class PostEditor { constructor(editor) { this.editor = editor; @@ -231,7 +247,7 @@ class PostEditor { } else if (isListItem(section)) { nextPosition = this._convertListItemToMarkupSection(section); } else { - const prevSection = findPreviousMarkerableSection(section); + const prevSection = findImmediatelyPreviousMarkerableSection(section); if (prevSection) { const { beforeMarker } = prevSection.join(section); @@ -257,7 +273,44 @@ class PostEditor { * @private */ _deleteForwardFrom(position) { - return this._deleteForwardFromMarkerPosition(position); + const { section, offset } = position; + if (section.isBlank) { + // remove this section, focus on start of next markerable section + const nextPosition = position.clone(); + const next = findImmediatelyNextMarkerableSection(section); + if (next) { + this.removeSection(section); + nextPosition.section = next; + nextPosition.offset = 0; + } + return nextPosition; + } else if (offset === section.length) { + // join next markerable section to this one + return this._joinPositionToNextSection(position); + } else { + return this._deleteForwardFromMarkerPosition(position.markerPosition); + } + } + + _joinPositionToNextSection(position) { + const { section } = position; + let nextPosition = position.clone(); + + if (!isMarkerable(section)) { + throw new Error('Cannot join non-markerable section to next section'); + } else { + const next = findImmediatelyNextMarkerableSection(section); + if (next) { + section.join(next); + section.renderNode.markDirty(); + this.removeSection(next); + + this.scheduleRerender(); + this.scheduleDidUpdate(); + } + } + + return nextPosition; } _deleteForwardFromMarkerPosition(markerPosition) { diff --git a/tests/acceptance/editor-list-test.js b/tests/acceptance/editor-list-test.js index e6769d4c0..9e66907f9 100644 --- a/tests/acceptance/editor-list-test.js +++ b/tests/acceptance/editor-list-test.js @@ -245,6 +245,7 @@ test('deleting at start of empty section after list item joins it with list item }); createEditorWithMobiledoc(mobiledoc); + assert.hasElement('#editor p br', 'precond - br'); const node = $('#editor p br')[0]; Helpers.dom.moveCursorTo(node, 0); Helpers.dom.triggerDelete(editor); @@ -255,3 +256,133 @@ test('deleting at start of empty section after list item joins it with list item assert.hasElement('#editor li:contains(abcX)', 'inserts text at right spot'); }); + +test('forward-delete in empty list item with nothing after it does nothing', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem} = builder; + return post([ + listSection('ul', [listItem()]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + assert.hasElement('#editor li br', 'precond - br'); + const node = $('#editor li br')[0]; + Helpers.dom.moveCursorTo(node, 0); + Helpers.dom.triggerForwardDelete(editor); + + assert.hasElement('#editor li', 'li remains'); + + Helpers.dom.insertText(editor, 'X'); + + assert.hasElement('#editor li:contains(X)', 'inserts text at right spot'); +}); + +test('forward-delete in empty li with li after it joins with li', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem, marker} = builder; + return post([ + listSection('ul', [listItem(), listItem([marker('abc')])]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + assert.equal($('#editor li').length, 2, 'precond - 2 lis'); + assert.hasElement('#editor li br', 'precond - br'); + const node = $('#editor li br')[0]; + Helpers.dom.moveCursorTo(node, 0); + Helpers.dom.triggerForwardDelete(editor); + + assert.equal($('#editor li').length, 1, '1 li remains'); + assert.hasElement('#editor li:contains(abc)', 'correct li remains'); + + Helpers.dom.insertText(editor, 'X'); + + assert.hasElement('#editor li:contains(Xabc)', 'inserts text at right spot'); +}); + +test('forward-delete in empty li with markup section after it deletes li', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem, markupSection, marker} = builder; + return post([ + listSection('ul', [listItem()]), + markupSection('p', [marker('abc')]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + assert.hasElement('#editor li br', 'precond - br'); + const node = $('#editor li br')[0]; + Helpers.dom.moveCursorTo(node, 0); + Helpers.dom.triggerForwardDelete(editor); + + assert.hasNoElement('#editor li', 'li is removed'); + assert.hasElement('#editor p:contains(abc)', 'p remains'); + + Helpers.dom.insertText(editor, 'X'); + + assert.hasElement('#editor p:contains(Xabc)', 'inserts text at right spot'); + +}); + +test('forward-delete end of li with nothing after', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem, marker} = builder; + return post([ + listSection('ul', [listItem([marker('abc')])]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + const node = $('#editor li')[0].childNodes[0]; + Helpers.dom.moveCursorTo(node, 'abc'.length); + Helpers.dom.triggerForwardDelete(editor); + + assert.hasElement('#editor li:contains(abc)', 'li remains'); + Helpers.dom.insertText(editor, 'X'); + assert.hasElement('#editor li:contains(abcX)', 'inserts text at right spot'); +}); + +test('forward-delete end of li with li after', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem, marker} = builder; + return post([ + listSection('ul', [ + listItem([marker('abc')]), + listItem([marker('def')]) + ]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + assert.equal($('#editor li').length, 2, 'precond - 2 lis'); + const node = $('#editor li')[0].childNodes[0]; + Helpers.dom.moveCursorTo(node, 'abc'.length); + Helpers.dom.triggerForwardDelete(editor); + + assert.hasElement('#editor li:contains(abcdef)', 'li is joined'); + assert.equal($('#editor li').length, 1, 'only 1 li'); + Helpers.dom.insertText(editor, 'X'); + assert.hasElement('#editor li:contains(abcXdef)', 'inserts text at right spot'); +}); + +test('forward-delete end of li with markup section after', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(builder => { + const {post, listSection, listItem, marker, markupSection} = builder; + return post([ + listSection('ul', [listItem([marker('abc')])]), + markupSection('p', [marker('def')]) + ]); + }); + createEditorWithMobiledoc(mobiledoc); + + const node = $('#editor li')[0].childNodes[0]; + Helpers.dom.moveCursorTo(node, 'abc'.length); + Helpers.dom.triggerForwardDelete(editor); + + assert.hasElement('#editor li:contains(abcdef)', 'li is joined'); + assert.equal($('#editor li').length, 1, 'only 1 li'); + assert.hasNoElement('#editor p', 'p is removed'); + Helpers.dom.insertText(editor, 'X'); + assert.hasElement('#editor li:contains(abcXdef)', 'inserts text at right spot'); +}); diff --git a/tests/helpers/dom.js b/tests/helpers/dom.js index c2c9170c8..3fe88f7ee 100644 --- a/tests/helpers/dom.js +++ b/tests/helpers/dom.js @@ -3,7 +3,7 @@ const TEXT_NODE = 3; import { clearSelection } from 'content-kit-editor/utils/selection-utils'; import { walkDOMUntil } from 'content-kit-editor/utils/dom-utils'; import KEY_CODES from 'content-kit-editor/utils/keycodes'; -import { MODIFIERS } from 'content-kit-editor/utils/key'; +import { DIRECTION, MODIFIERS } from 'content-kit-editor/utils/key'; import isPhantom from './is-phantom'; function selectRange(startNode, startOffset, endNode, endOffset) { @@ -40,6 +40,7 @@ function selectText(startText, } function moveCursorTo(node, offset=0, endNode=node, endOffset=offset) { + if (!node) { throw new Error('Cannot moveCursorTo node without node'); } selectRange(node, offset, endNode, endOffset); } @@ -140,17 +141,24 @@ function getCursorPosition() { }; } -function triggerDelete(editor) { +function triggerDelete(editor, direction=DIRECTION.BACKWARD) { if (!editor) { throw new Error('Must pass `editor` to `triggerDelete`'); } + const keyCode = direction === DIRECTION.BACKWARD ? KEY_CODES.BACKSPACE : + KEY_CODES.DELETE; if (isPhantom()) { // simulate deletion for phantomjs - let event = { preventDefault() {} }; + let event = { keyCode, preventDefault() {} }; editor.handleDeletion(event); } else { - triggerKeyEvent(editor.element, 'keydown', KEY_CODES.BACKSPACE); + triggerKeyEvent(editor.element, 'keydown', keyCode); } } +function triggerForwardDelete(editor) { + if (!editor) { throw new Error('Must pass `editor` to `triggerForwardDelete`'); } + return triggerDelete(editor, DIRECTION.FORWARD); +} + function triggerEnter(editor) { if (!editor) { throw new Error('Must pass `editor` to `triggerEnter`'); } if (isPhantom()) { @@ -197,6 +205,7 @@ const DOMHelper = { getCursorPosition, getSelectedText, triggerDelete, + triggerForwardDelete, triggerEnter, insertText, triggerKeyCommand