Skip to content

Commit

Permalink
feat(UI): Export toggleLink (#491)
Browse files Browse the repository at this point in the history
* WIP: expose `toggleLink` as public method

* Object.keys flunks outside of browser env

* Unit tests for UI.toggleLink

* Export UI for reuse by editor kits

* DRY up renderIntoAndFocusTail

* Document UI.toggleLink
  • Loading branch information
joshfrench authored and bantic committed Sep 9, 2016
1 parent bbf3cfd commit 3335357
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 80 deletions.
4 changes: 0 additions & 4 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,10 +344,6 @@ class Editor {
});
}

showPrompt(message, defaultValue, callback) {
callback(window.prompt(message, defaultValue));
}

/**
* Notify the editor that the post did change, and run associated
* callbacks.
Expand Down
27 changes: 1 addition & 26 deletions src/js/editor/key-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { MODIFIERS, SPECIAL_KEYS } from '../utils/key';
import { filter, reduce } from '../utils/array-utils';
import assert from '../utils/assert';
import Browser from '../utils/browser';
import { toggleLink } from './ui';

function selectAll(editor) {
let { post } = editor;
Expand Down Expand Up @@ -37,32 +38,6 @@ function deleteToEndOfSection(editor) {
});
}

function toggleLink(editor) {
if (editor.range.isCollapsed) {
return;
}

let selectedText = editor.cursor.selectedText();
let defaultUrl = '';
if (selectedText.indexOf('http') !== -1) { defaultUrl = selectedText; }

let {range} = editor;
let hasLink = editor.detectMarkupInRange(range, 'a');

if (hasLink) {
editor.run(postEditor => postEditor.toggleMarkup('a'));
} else {
editor.showPrompt('Enter a URL', defaultUrl, url => {
if (!url) { return; }

editor.run(postEditor => {
let markup = postEditor.builder.createMarkup('a', {href: url});
postEditor.toggleMarkup(markup);
});
});
}
}

export const DEFAULT_KEY_COMMANDS = [{
str: 'META+B',
run(editor) {
Expand Down
69 changes: 69 additions & 0 deletions src/js/editor/ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @module UI
*/

/**
* @callback promptCallback
* @param {String} url The URL to pass back to the editor for linking
* to the selected text.
*/

/**
* @callback showPrompt
* @param {String} message The text of the prompt.
* @param {String} defaultValue The initial URL to display in the prompt.
* @param {module:UI~promptCallback} callback Once your handler has accepted a URL,
* it should pass it to `callback` so that the editor may link the
* selected text.
*/

/**
* Exposes the core behavior for linking and unlinking text, and allows for
* customization of the URL input handler.
* @param {Editor} editor An editor instance to operate on. If a range is selected,
* either prompt for a URL and add a link or un-link the
* currently linked text.
* @param {module:UI~showPrompt} [showPrompt] An optional custom input handler. Defaults
* to using `window.prompt`.
* @example
* let myPrompt = (message, defaultURL, promptCallback) => {
* let url = window.prompt("Overriding the defaults", "http://placekitten.com");
* promptCallback(url);
* };
*
* editor.registerKeyCommand({
* str: "META+K",
* run(editor) {
* toggleLink(editor, myPrompt);
* }
* });
* @public
*/

let defaultShowPrompt = (message, defaultValue, callback) => callback(window.prompt(message, defaultValue));

export function toggleLink(editor, showPrompt=defaultShowPrompt) {
if (editor.range.isCollapsed) {
return;
}

let selectedText = editor.cursor.selectedText();
let defaultUrl = '';
if (selectedText.indexOf('http') !== -1) { defaultUrl = selectedText; }

let {range} = editor;
let hasLink = editor.detectMarkupInRange(range, 'a');

if (hasLink) {
editor.run(postEditor => postEditor.toggleMarkup('a'));
} else {
showPrompt('Enter a URL', defaultUrl, url => {
if (!url) { return; }

editor.run(postEditor => {
let markup = postEditor.builder.createMarkup('a', {href: url});
postEditor.toggleMarkup(markup);
});
});
}
}
2 changes: 2 additions & 0 deletions src/js/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import Editor from './editor/editor';
import UI from './editor/ui';
import ImageCard from './cards/image';
import Range from './utils/cursor/range';
import Position from './utils/cursor/position';
Expand All @@ -7,6 +8,7 @@ import VERSION from './version';

const Mobiledoc = {
Editor,
UI,
ImageCard,
Range,
Position,
Expand Down
67 changes: 38 additions & 29 deletions tests/acceptance/editor-key-commands-test.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import { MODIFIERS } from 'mobiledoc-kit/utils/key';
import Keycodes from 'mobiledoc-kit/utils/keycodes';
import Helpers from '../test-helpers';
import Range from 'mobiledoc-kit/utils/cursor/range';
import Browser from 'mobiledoc-kit/utils/browser';
import { toggleLink } from 'mobiledoc-kit/editor/ui';

const { module, test, skip } = Helpers;

let editor, editorElement;

// In Firefox, if the window isn't active (which can happen when running tests
// at SauceLabs), the editor element won't have the selection. This helper method
// ensures that it has a cursor selection.
// See https://github.com/bustlelabs/mobiledoc-kit/issues/388
function renderIntoAndFocusTail(treeFn, options={}) {
let editor = Helpers.mobiledoc.renderInto(editorElement, treeFn, options);
editor.selectRange(new Range(editor.post.tailPosition()));
return editor;
function labelForModifier(key) {
switch (key) {
case MODIFIERS.META: return 'META';
case MODIFIERS.CTRL: return 'CTRL';
}
}

module('Acceptance: Editor: Key Commands', {
Expand All @@ -38,7 +35,7 @@ function testStatefulCommand({modifierName, key, command, markupName}) {
let modifier = MODIFIERS[modifierName];
let modifierKeyCode = Keycodes[modifierName];
let initialText = 'something';
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker(initialText)])
]));

Expand All @@ -64,7 +61,7 @@ function testStatefulCommand({modifierName, key, command, markupName}) {
let modifierKeyCode = Keycodes[modifierName];
let initialText = 'something';

editor =renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker(initialText)])
]));

Expand Down Expand Up @@ -156,7 +153,7 @@ testStatefulCommand({
if (Browser.isMac()) {
test(`[Mac] ctrl-k clears to the end of a line`, (assert) => {
let initialText = 'something';
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker(initialText)])
]));

Expand All @@ -181,7 +178,7 @@ if (Browser.isMac()) {

test(`[Mac] ctrl-k clears selected text`, (assert) => {
let initialText = 'something';
editor = renderIntoAndFocusTail( ({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker(initialText)])
]));

Expand Down Expand Up @@ -209,16 +206,21 @@ let toggleLinkTest = (assert, modifier) => {
assert.expect(3);

let url = 'http://bustle.com';
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

assert.ok(editor.hasCursor(), 'has cursor');
editor.registerKeyCommand({
str: `${labelForModifier(modifier)}+K`,
run(editor) {
toggleLink(editor, (prompt, defaultUrl, callback) => {
assert.ok(true, 'calls showPrompt');
callback(url);
});
}
});

editor.showPrompt = (prompt, defaultUrl, callback) => {
assert.ok(true, 'calls showPrompt');
callback(url);
};
assert.ok(editor.hasCursor(), 'has cursor');

Helpers.dom.selectText(editor ,'something', editorElement);
Helpers.dom.triggerKeyCommand(editor, 'K', modifier);
Expand All @@ -230,15 +232,22 @@ let toggleLinkUnlinkTest = (assert, modifier) => {
assert.expect(4);

let url = 'http://bustle.com';
editor = renderIntoAndFocusTail(({post, markupSection, marker, markup}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker, markup}) => post([
markupSection('p', [marker('something', [markup('a', {href:url})])])
]));

editor.registerKeyCommand({
str: `${labelForModifier(modifier)}+K`,
run(editor) {
toggleLink(editor, (prompt, defaultUrl, callback) => {
assert.ok(false, 'should not call showPrompt');
callback(url);
});
}
});

assert.ok(editor.hasCursor(), 'has cursor');

editor.showPrompt = () => {
assert.ok(false, 'should not call showPrompt');
};
assert.hasElement(`#editor a[href="${url}"]:contains(something)`,
'precond -- has link');

Expand Down Expand Up @@ -288,7 +297,7 @@ toggleTests.forEach(({precondition, msg, testFn, modifier}) => {
});

test('new key commands can be registered', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand All @@ -310,7 +319,7 @@ test('new key commands can be registered', (assert) => {
});

test('new key commands can be registered without modifiers', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand All @@ -336,7 +345,7 @@ test('new key commands can be registered without modifiers', (assert) => {
});

test('duplicate key commands can be registered with the last registered winning', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand All @@ -360,7 +369,7 @@ test('duplicate key commands can be registered with the last registered winning'
});

test('returning false from key command causes next match to run', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand All @@ -387,7 +396,7 @@ test('returning false from key command causes next match to run', (assert) => {
});

test('key commands can override built-in functionality', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand All @@ -410,7 +419,7 @@ test('key commands can override built-in functionality', (assert) => {
});

test('returning false from key command still runs built-in functionality', (assert) => {
editor = renderIntoAndFocusTail(({post, markupSection, marker}) => post([
editor = Helpers.mobiledoc.renderIntoAndFocusTail(editorElement, ({post, markupSection, marker}) => post([
markupSection('p', [marker('something')])
]));

Expand Down

0 comments on commit 3335357

Please sign in to comment.