Skip to content

Commit

Permalink
Add postEditor#insertSection, #insertSectionAtEnd, #toggleMarkup
Browse files Browse the repository at this point in the history
fixes #126
  • Loading branch information
bantic committed Sep 16, 2015
1 parent d4f34d2 commit 5dffae5
Show file tree
Hide file tree
Showing 7 changed files with 217 additions and 49 deletions.
17 changes: 1 addition & 16 deletions src/js/commands/link.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import TextFormatCommand from './text-format';
import { any } from 'content-kit-editor/utils/array-utils';

export default class LinkCommand extends TextFormatCommand {
constructor(editor) {
Expand All @@ -10,24 +9,10 @@ export default class LinkCommand extends TextFormatCommand {
});
}

isActive() {
return any(this.editor.markupsInSelection, m => m.hasTag(this.tag));
}

exec(url) {
const range = this.editor.cursor.offsets;
this.editor.run(postEditor => {
const markup = postEditor.builder.createMarkup('a', ['href', url]);
postEditor.applyMarkupToRange(range, markup);
});
this.editor.moveToPosition(range.tail);
}

unexec() {
const range = this.editor.cursor.offsets;
this.editor.run(postEditor => {
postEditor.removeMarkupFromRange(range, markup => markup.hasTag('a'));
this.editor.run(postEditor => postEditor.toggleMarkup(markup));
});
this.editor.selectRange(range);
}
}
18 changes: 3 additions & 15 deletions src/js/commands/text-format.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,15 @@ export default class TextFormatCommand extends Command {
this.tag = options.tag;
}

get markup() {
if (this._markup) { return this._markup; }
this._markup = this.editor.builder.createMarkup(this.tag);
return this._markup;
}

isActive() {
return any(this.editor.markupsInSelection, m => m === this.markup);
return any(this.editor.markupsInSelection, m => m.hasTag(this.tag));
}

exec() {
const range = this.editor.cursor.offsets, { markup } = this;
this.editor.run(
postEditor => postEditor.applyMarkupToRange(range, markup));
this.editor.selectRange(range);
this.editor.run(postEditor => postEditor.toggleMarkup(this.tag));
}

unexec() {
const range = this.editor.cursor.offsets, { markup } = this;
this.editor.run(
postEditor => postEditor.removeMarkupFromRange(range, markup));
this.editor.selectRange(range);
this.editor.run(postEditor => postEditor.toggleMarkup(this.tag));
}
}
5 changes: 5 additions & 0 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,11 @@ class Editor {
return this.cursor.activeSections;
}

get activeSection() {
const { activeSections } = this;
return activeSections[activeSections.length - 1];
}

get markupsInSelection() {
if (this.cursor.hasSelection()) {
const range = this.cursor.offsets;
Expand Down
78 changes: 73 additions & 5 deletions src/js/editor/post.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { POST_TYPE, MARKUP_SECTION_TYPE, LIST_ITEM_TYPE } from '../models/types';
import Position from '../utils/cursor/position';
import { filter, compact } from '../utils/array-utils';
import { any, filter, compact } from '../utils/array-utils';
import { DIRECTION } from '../utils/key';

function isMarkupSection(section) {
Expand All @@ -24,6 +24,7 @@ class PostEditor {
this.editor = editor;
this.builder = this.editor.builder;
this._completionWorkQueue = [];
this._afterRenderQueue = [];
this._didRerender = false;
this._didUpdate = false;
this._didComplete = false;
Expand Down Expand Up @@ -544,7 +545,7 @@ class PostEditor {
* @param {Range} range Object with offsets
* @param {Markup} markup A markup post abstract node
* @return {Array} of markers that are inside the split
* @public
* @private
*/
removeMarkupFromRange(range, markupOrMarkupCallback) {
const markers = this.splitMarkers(range);
Expand All @@ -556,6 +557,45 @@ class PostEditor {
return markers;
}

/**
* Toggle the given markup on the current selection. If anything in the current
* selection has the markup, it will be removed. If nothing in the selection
* has the markup, it will be added to everything in the selection.
*
* Usage:
*
* // Remove any 'strong' markup if it exists in the selection, otherwise
* // make it all 'strong'
* editor.run(postEditor => postEditor.toggleMarkup('strong'));
*
* // add/remove a link to 'bustle.com' to the selection
* editor.run(postEditor => {
* const linkMarkup = postEditor.builder.createMarkup('a', ['href', 'http://bustle.com']);
* postEditor.toggleMarkup(linkMarkup);
* });
*
* @method toggleMarkup
* @param {Markup|String} markupOrString Either a markup object created using
* the builder (useful when adding a markup with attributes, like an 'a' markup),
* or, if a string, the tag name of the markup (e.g. 'strong', 'em') to toggle.
*/
toggleMarkup(markupOrMarkupString) {
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) {
this.removeMarkupFromRange(range, hasMarkup);
} else {
this.applyMarkupToRange(range, markup);
}
this.scheduleAfterRender(() => this.editor.selectRange(range));
}

/**
* Insert a given section before another one, updating the post abstract
* and the rendered UI.
Expand All @@ -582,6 +622,31 @@ class PostEditor {
this._markDirty(section.parent);
}

/**
* Insert the given section after the current active section, or, if no
* section is active, at the end of the document.
* @method insertSection
* @param {Section} section
* @public
*/
insertSection(section) {
const activeSection = this.editor.activeSection;
const nextSection = activeSection && activeSection.next;

const collection = this.editor.post.sections;
this.insertSectionBefore(collection, section, nextSection);
}

/**
* Insert the given section at the end of the document.
* @method insertSectionAtEnd
* @param {Section} section
* @public
*/
insertSectionAtEnd(section) {
this.insertSectionBefore(this.editor.post.sections, section, null);
}

/**
* Remove a given section from the post abstract and the rendered UI.
*
Expand Down Expand Up @@ -661,6 +726,10 @@ class PostEditor {
});
}

scheduleAfterRender(callback) {
this._afterRenderQueue.push(callback);
}

/**
* Flush any work on the queue. `editor.run` already does this, calling this
* method directly should not be needed outside `editor.run`.
Expand All @@ -673,9 +742,8 @@ class PostEditor {
throw new Error('Post editing can only be completed once');
}
this._didComplete = true;
this._completionWorkQueue.forEach(callback => {
callback();
});
this._completionWorkQueue.forEach(cb => cb());
this._afterRenderQueue.forEach(cb => cb());
}
}

Expand Down
14 changes: 1 addition & 13 deletions tests/acceptance/editor-commands-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -232,19 +232,7 @@ Helpers.skipInPhantom('highlight text, click "link" button shows input for URL,

setTimeout(() => {
assert.hasElement(`#editor a[href="${url}"]:contains(${selectedText})`);

Helpers.dom.insertText(editor, 'X');

assert.hasElement(`#editor p:contains(${selectedText}X)`,
'inserts text after selected text');
assert.hasNoElement(`#editor a:contains(${selectedText}X)`,
'inserted text does not extend "a" tag');

Helpers.dom.insertText(editor, 'X');
assert.hasElement(`#editor p:contains(${selectedText}XX)`,
'inserts text after selected text again');

Helpers.dom.selectText(selectedText, editorElement);
assert.selectedText(selectedText, 'text remains selected');
Helpers.dom.triggerEvent(document, 'mouseup');

setTimeout(() => {
Expand Down
119 changes: 119 additions & 0 deletions tests/acceptance/editor-post-editor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Editor } from 'content-kit-editor';
import Helpers from '../test-helpers';

const { module, test } = Helpers;

let editor, editorElement;

module('Acceptance: Editor - PostEditor', {
beforeEach() {
editorElement = $('<div id="editor"></div>')[0];
$('#qunit-fixture').append($(editorElement));
},
afterEach() {
if (editor) { editor.destroy(); }
}
});

test('#insertSectionAtEnd inserts the section at the end', (assert) => {
let newSection;
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
newSection = markupSection('p', [marker('123')]);
return post([markupSection('p', [marker('abc')])]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

//precond
assert.hasElement('#editor p:contains(abc)');
assert.hasNoElement('#editor p:contains(123)');

editor.run(postEditor => postEditor.insertSectionAtEnd(newSection));
assert.hasElement('#editor p:eq(1):contains(123)', 'new section added at end');
});

test('#insertSection inserts after the cursor active section', (assert) => {
let newSection;
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
newSection = markupSection('p', [marker('123')]);
return post([
markupSection('p', [marker('abc')]),
markupSection('p', [marker('def')])
]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

//precond
assert.hasElement('#editor p:eq(0):contains(abc)');
assert.hasElement('#editor p:eq(1):contains(def)');
assert.hasNoElement('#editor p:contains(123)');

Helpers.dom.selectText('b', editorElement);

editor.run(postEditor => postEditor.insertSection(newSection));
assert.hasElement('#editor p:eq(0):contains(abc)', 'still has 1st section');
assert.hasElement('#editor p:eq(1):contains(123)',
'new section added after active section');
assert.hasElement('#editor p:eq(2):contains(def)', '2nd section -> 3rd spot');
});

test('#insertSection inserts at end when no active cursor section', (assert) => {
let newSection;
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
newSection = markupSection('p', [marker('123')]);
return post([
markupSection('p', [marker('abc')]),
markupSection('p', [marker('def')])
]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

//precond
assert.hasElement('#editor p:eq(0):contains(abc)');
assert.hasElement('#editor p:eq(1):contains(def)');
assert.hasNoElement('#editor p:contains(123)');

Helpers.dom.clearSelection();
editor.run(postEditor => postEditor.insertSection(newSection));
assert.hasElement('#editor p:eq(0):contains(abc)', 'still has 1st section');
assert.hasElement('#editor p:eq(2):contains(123)', 'new section added at end');
assert.hasElement('#editor p:eq(1):contains(def)', '2nd section -> same spot');
});

test('#toggleMarkup adds markup by tag name', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker}) => {
return post([
markupSection('p', [marker('abc'), marker('def')])
]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

//precond
assert.hasNoElement('#editor strong');

Helpers.dom.selectText('bc', editorElement, 'd', editorElement);
editor.run(postEditor => postEditor.toggleMarkup('strong'));
assert.hasElement('#editor strong:contains(bcd)');
});

test('#toggleMarkup removes markup by tag name', (assert) => {
const mobiledoc = Helpers.mobiledoc.build(({post, markupSection, marker, markup}) => {
const strong = markup('strong');
return post([
markupSection('p', [marker('a'), marker('bcde', [strong]), marker('f')])
]);
});
editor = new Editor({mobiledoc});
editor.render(editorElement);

//precond
assert.hasElement('#editor strong:contains(bcde)');

Helpers.dom.selectText('bc', editorElement, 'd', editorElement);
editor.run(postEditor => postEditor.toggleMarkup('strong'));
assert.hasNoElement('#editor strong:contains(bcd)', 'markup removed from selection');
assert.hasElement('#editor strong:contains(e)', 'unselected text still bold');
});
15 changes: 15 additions & 0 deletions tests/unit/editor/post-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -581,3 +581,18 @@ test('#replaceSection when section is null appends new section', (assert) => {
assert.equal(post.sections.length, 1, 'has 1 section');
assert.equal(post.sections.head.text, '', 'no text in new section');
});

test('#insertSectionAtEnd inserts the section at the end of the mobiledoc', (assert) => {
let newSection;
const post = Helpers.postAbstract.build(({post, markupSection, marker}) => {
newSection = markupSection('p', [marker('123')]);
return post([markupSection('p', [marker('abc')])]);
});
renderBuiltAbstract(post);

postEditor.insertSectionAtEnd(newSection);
postEditor.complete();

assert.equal(post.sections.length, 2, 'new section added');
assert.equal(post.sections.tail.text, '123', 'new section added at end');
});

0 comments on commit 5dffae5

Please sign in to comment.