Skip to content

Commit

Permalink
Add #detectMarkupInRange to editor
Browse files Browse the repository at this point in the history
  • Loading branch information
mixonic committed Sep 23, 2015
1 parent 2a689a5 commit 93824a1
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 9 deletions.
11 changes: 10 additions & 1 deletion src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 16 additions & 6 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion tests/acceptance/editor-commands-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
14 changes: 14 additions & 0 deletions tests/acceptance/editor-post-editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
46 changes: 46 additions & 0 deletions tests/unit/editor/editor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -182,3 +183,48 @@ test('editor parses and renders DOM', (assert) => {
assert.equal(editorElement.innerHTML,
`<p>hello world</p>`);
});

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');
});
46 changes: 45 additions & 1 deletion tests/unit/editor/post-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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}) => {
Expand Down

0 comments on commit 93824a1

Please sign in to comment.