From 93824a1921a8d5d453a273b9aa9afaee5b5bfbaa Mon Sep 17 00:00:00 2001 From: Matthew Beale Date: Tue, 22 Sep 2015 14:42:25 -0400 Subject: [PATCH] Add #detectMarkupInRange to editor --- src/js/editor/editor.js | 11 ++++- src/js/editor/post.js | 22 +++++++--- tests/acceptance/editor-commands-test.js | 2 +- tests/acceptance/editor-post-editor-test.js | 14 +++++++ tests/unit/editor/editor-test.js | 46 +++++++++++++++++++++ tests/unit/editor/post-test.js | 46 ++++++++++++++++++++- 6 files changed, 132 insertions(+), 9 deletions(-) diff --git a/src/js/editor/editor.js b/src/js/editor/editor.js index f0893f6b8..b97d2633a 100644 --- a/src/js/editor/editor.js +++ b/src/js/editor/editor.js @@ -34,6 +34,8 @@ import LifecycleCallbacksMixin from '../utils/lifecycle-callbacks'; export const EDITOR_ELEMENT_CLASS_NAME = 'ck-editor'; +import { detect } from '../utils/array-utils'; + const defaults = { placeholder: 'Write here...', spellcheck: true, @@ -227,7 +229,7 @@ class Editor { this.cursor.moveToPosition(range.head); } else { const key = Key.fromEvent(event); - const nextPosition = this.run(postEditor => { + const nextPosition = this.run(postEditor => { return postEditor.deleteFrom(range.head, key.direction); }); this.cursor.moveToPosition(nextPosition); @@ -360,6 +362,13 @@ class Editor { return activeSections[activeSections.length - 1]; } + detectMarkupInRange(range, markupTagName) { + let markups = this.post.markupsInRange(range); + return detect(markups, markup => { + return markup.hasTag(markupTagName); + }); + } + get markupsInSelection() { if (this.cursor.hasSelection()) { const range = this.cursor.offsets; diff --git a/src/js/editor/post.js b/src/js/editor/post.js index ce4903724..5a6604a66 100644 --- a/src/js/editor/post.js +++ b/src/js/editor/post.js @@ -4,7 +4,7 @@ import { import { POST_TYPE, MARKUP_SECTION_TYPE, LIST_ITEM_TYPE } from '../models/types'; import Position from '../utils/cursor/position'; import { - isArrayEqual, any, forEach, filter, compact + isArrayEqual, forEach, filter, compact } from '../utils/array-utils'; import { DIRECTION } from '../utils/key'; @@ -543,6 +543,9 @@ class PostEditor { * @public */ applyMarkupToRange(range, markup) { + if (range.isCollapsed) { + return; + } this.splitMarkers(range).forEach(marker => { marker.addMarkup(markup); this._markDirty(marker); @@ -573,6 +576,9 @@ class PostEditor { * @private */ removeMarkupFromRange(range, markupOrMarkupCallback) { + if (range.isCollapsed) { + return; + } this.splitMarkers(range).forEach(marker => { marker.removeMarkup(markupOrMarkupCallback); this._markDirty(marker); @@ -602,15 +608,19 @@ class PostEditor { * or, if a string, the tag name of the markup (e.g. 'strong', 'em') to toggle. */ toggleMarkup(markupOrMarkupString) { + const range = this.editor.cursor.offsets; + if (range.isCollapsed) { + return; + } const markup = typeof markupOrMarkupString === 'string' ? this.builder.createMarkup(markupOrMarkupString) : markupOrMarkupString; - const range = this.editor.cursor.offsets; - const hasMarkup = m => m.hasTag(markup.tagName); - const rangeHasMarkup = any(this.editor.markupsInSelection, hasMarkup); - - if (rangeHasMarkup) { + const hasMarkup = this.editor.detectMarkupInRange(range, markup.tagName); + // FIXME: This implies only a single markup in a range. This may not be + // true for links (which are not the same object instance like multiple + // strong tags would be). + if (hasMarkup) { this.removeMarkupFromRange(range, hasMarkup); } else { this.applyMarkupToRange(range, markup); diff --git a/tests/acceptance/editor-commands-test.js b/tests/acceptance/editor-commands-test.js index 1518cdf7a..f16e431ee 100644 --- a/tests/acceptance/editor-commands-test.js +++ b/tests/acceptance/editor-commands-test.js @@ -227,7 +227,7 @@ Helpers.skipInPhantom('highlight text, click "link" button shows input for URL, const url = 'http://google.com'; $(input).val(url); Helpers.dom.triggerEnterKeyupEvent(input[0]); - + assert.toolbarHidden(); setTimeout(() => { diff --git a/tests/acceptance/editor-post-editor-test.js b/tests/acceptance/editor-post-editor-test.js index e2be5e0a6..d050a8efc 100644 --- a/tests/acceptance/editor-post-editor-test.js +++ b/tests/acceptance/editor-post-editor-test.js @@ -117,3 +117,17 @@ test('#toggleMarkup removes markup by tag name', (assert) => { assert.hasNoElement('#editor strong:contains(bcd)', 'markup removed from selection'); assert.hasElement('#editor strong:contains(e)', 'unselected text still bold'); }); + +test('#toggleMarkup does nothing with an empty selection', (assert) => { + const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => { + return post([ + markupSection('p', [marker('a')]) + ]); + }); + editor = new Editor({mobiledoc}); + editor.render(editorElement); + + editor.run(postEditor => postEditor.toggleMarkup('strong')); + + assert.hasNoElement('#editor strong', 'strong not added, nothing selected'); +}); diff --git a/tests/unit/editor/editor-test.js b/tests/unit/editor/editor-test.js index bc7512b6d..d18129216 100644 --- a/tests/unit/editor/editor-test.js +++ b/tests/unit/editor/editor-test.js @@ -2,6 +2,7 @@ import Editor from 'content-kit-editor/editor/editor'; import { EDITOR_ELEMENT_CLASS_NAME } from 'content-kit-editor/editor/editor'; import { normalizeTagName } from 'content-kit-editor/utils/dom-utils'; import { MOBILEDOC_VERSION } from 'content-kit-editor/renderers/mobiledoc'; +import Range from 'content-kit-editor/utils/cursor/range'; const { module, test } = window.QUnit; @@ -182,3 +183,48 @@ test('editor parses and renders DOM', (assert) => { assert.equal(editorElement.innerHTML, `

hello world

`); }); + +test('#detectMarkupInRange not found', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + sections: [ + [], + [ + [1, normalizeTagName('p'), [ + [[], 0, 'hello world'] + ]] + ] + ] + }; + editor = new Editor({mobiledoc}); + editor.render(editorElement); + + let section = editor.post.sections.head; + let range = Range.create(section, 0, section, section.text.length); + let markup = editor.detectMarkupInRange(range, 'strong'); + assert.ok(!markup, 'selection is not strong'); +}); + +test('#detectMarkupInRange matching bounds of marker', (assert) => { + const mobiledoc = { + version: MOBILEDOC_VERSION, + sections: [ + [ + ['strong'] + ], + [ + [1, normalizeTagName('p'), [ + [[0], 1, 'hello world'] + ]] + ] + ] + }; + editor = new Editor({mobiledoc}); + editor.render(editorElement); + + let section = editor.post.sections.head; + let range = Range.create(section, 0, section, section.text.length); + let markup = editor.detectMarkupInRange(range, 'strong'); + assert.ok(markup, 'selection has markup'); + assert.equal(markup.tagName, 'strong', 'detected markup is strong'); +}); diff --git a/tests/unit/editor/post-test.js b/tests/unit/editor/post-test.js index a54979002..21785e5b6 100644 --- a/tests/unit/editor/post-test.js +++ b/tests/unit/editor/post-test.js @@ -56,7 +56,7 @@ test('#deleteFrom in middle of marker deletes char before offset', (assert) => { const postEditor = postEditorWithMobiledoc(({post, markupSection, marker}) => post([ markupSection('P', [marker('abc def')]) - ]) + ]) ); const position = new Position(getSection(0), 4); @@ -614,6 +614,50 @@ test('markers with identical non-attribute markups get coalesced after applying assert.ok(section.markers.head.hasMarkup(strong), 'bold marker has bold'); }); +test('#removeMarkup silently does nothing when invoked with an empty range', (assert) => { + let section, markup; + const post = Helpers.postAbstract.build(({ + post, markupSection, marker, markup: buildMarkup + }) => { + markup = buildMarkup('strong'); + section = markupSection('p', [ + marker('abc') + ]); + return post([section]); + }); + renderBuiltAbstract(post); + + let range = Range.create(section, 1, section, 1); + postEditor.removeMarkupFromRange(range, markup); + postEditor.complete(); + + assert.equal(section.markers.length, 1, 'similar markers are coalesced'); + assert.equal(section.markers.head.value, 'abc', 'marker value is correct'); + assert.ok(!section.markers.head.hasMarkup(markup), 'marker has no markup'); +}); + +test('#applyMarkupToRange silently does nothing when invoked with an empty range', (assert) => { + let section, markup; + const post = Helpers.postAbstract.build(({ + post, markupSection, marker, markup: buildMarkup + }) => { + markup = buildMarkup('strong'); + section = markupSection('p', [ + marker('abc') + ]); + return post([section]); + }); + renderBuiltAbstract(post); + + let range = Range.create(section, 1, section, 1); + postEditor.applyMarkupToRange(range, markup); + postEditor.complete(); + + assert.equal(section.markers.length, 1, 'similar markers are coalesced'); + assert.equal(section.markers.head.value, 'abc', 'marker value is correct'); + assert.ok(!section.markers.head.hasMarkup(markup), 'marker has no markup'); +}); + test('markers with identical markups get coalesced after deletion', (assert) => { let strong, section; const post = Helpers.postAbstract.build(({post, markupSection, marker, markup}) => {