Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Editor#onTextInput hook, deprecate Editor#registerTextExpansion #368

Merged
merged 1 commit into from
Apr 26, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 20 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,24 +208,33 @@ editor.registerKeyCommand(enterKeyCommand);

To fall-back to the default behavior, return `false` from `run`.

### Configuring text expansions
### Responding to text input

Text expansions can also be registered with the editor. These are functions that
are run when a text string is entered and then a trigger character is entered.
For example, the text `"*"` followed by a space character triggers a function that
turns the current section into a list item. To register a text expansion call
`editor.registerExpansion` with an object that has `text`, `trigger` and `run`
properties, e.g.:
The editor exposes a hook `onTextInput` that can be used to programmatically react
to text that the user enters. Specify a handler object with `text` or `match`
properties and a `run` callback function, and the editor will invoke the callback
when the text before the cursor ends with `text` or matches `match`.
The callback is called after the matching text has been inserted. It is passed
the `editor` instance and an array of matches (either the result of `match.exec`
on the matching user-entered text, or an array containing only the `text`).

```javascript
const expansion = {
trigger: ' ',
editor.onTextInput({
text: 'X',
run(editor) {
// use the editor to programmatically change the post
// This callback is called after user types 'X'
}
};
});

editor.onTextInput({
match: /\d\dX$/, // Note the "$" end anchor
run(editor) {
// This callback is called after user types number-number-X
}
});
```
The editor has several default text input handlers that are defined in
`src/js/editor/text-input-handlers.js`.

### DOM Parsing hooks

Expand Down
62 changes: 32 additions & 30 deletions src/js/editor/editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ import Cursor from '../utils/cursor';
import Range from '../utils/cursor/range';
import Position from '../utils/cursor/position';
import PostNodeBuilder from '../models/post-node-builder';
import {
DEFAULT_TEXT_EXPANSIONS, findExpansion, validateExpansion
} from './text-expansions';
import { DEFAULT_TEXT_INPUT_HANDLERS } from './text-input-handlers';
import { convertExpansiontoHandler } from './text-expansion-handler';
import {
DEFAULT_KEY_COMMANDS, buildKeyCommand, findKeyCommands, validateKeyCommand
} from './key-commands';
Expand Down Expand Up @@ -129,7 +128,6 @@ class Editor {
mergeWithOptions(this, defaults, options);
this.cards.push(ImageCard);

DEFAULT_TEXT_EXPANSIONS.forEach(e => this.registerExpansion(e));
DEFAULT_KEY_COMMANDS.forEach(kc => this.registerKeyCommand(kc));

this._parser = new DOMParser(this.builder);
Expand All @@ -144,6 +142,9 @@ class Editor {
this._mutationHandler = new MutationHandler(this);
this._editState = new EditState(this);
this._callbacks = new LifecycleCallbacks(values(CALLBACK_QUEUES));

DEFAULT_TEXT_INPUT_HANDLERS.forEach(handler => this.onTextInput(handler));

this.hasRendered = false;
}

Expand Down Expand Up @@ -238,26 +239,24 @@ class Editor {
}));
}

get expansions() {
if (!this._expansions) { this._expansions = []; }
return this._expansions;
}

get keyCommands() {
if (!this._keyCommands) { this._keyCommands = []; }
return this._keyCommands;
}

/**
* @param {Object} expansion The text expansion to register. It must specify a
* trigger character (e.g. the `<space>` character) and a text string that precedes
* the trigger (e.g. "*"), and a `run` method that will be passed the
* editor instance when the text expansion is invoked
* Prefer {@link Editor#onTextInput} to `registerExpansion`.
* @param {Object} expansion
* @param {String} expansion.text
* @param {Function} expansion.run This callback will be invoked with an `editor` argument
* @param {Number} [expansion.trigger] The keycode (e.g. 32 for `<space>`) that will trigger the expansion after the text is entered
* @deprecated since v0.9.3
* @public
*/
registerExpansion(expansion) {
assert('Expansion is not valid', validateExpansion(expansion));
this.expansions.push(expansion);
deprecate('Use `Editor#onTextInput` instead of `registerExpansion`');
let handler = convertExpansiontoHandler(expansion);
this.onTextInput(handler);
}

/**
Expand Down Expand Up @@ -708,6 +707,24 @@ class Editor {
this.addCallback(CALLBACK_QUEUES.POST_DID_CHANGE, callback);
}

/**
* Register a handler that will be invoked by the editor after the user enters
* matching text.
* @param {Object} inputHandler
* @param {String} [inputHandler.text] Required if `match` is not provided
* @param {RegExp} [inputHandler.match] Required if `text` is not provided
* @param {Function} inputHandler.run This callback is invoked with the {@link Editor}
* instance and an array of matches. If `text` was provided,
* the matches array will equal [`text`], and if a `match`
* regex was provided the matches array will be the result of
* `match.exec` on the matching text. The callback is called
* after the matching text has been inserted.
* @public
*/
onTextInput(inputHandler) {
this._eventManager.registerInputHandler(inputHandler);
}

/**
* @param {Function} callback Called when the editor's state (active markups or
* active sections) has changed, either via user input or programmatically
Expand Down Expand Up @@ -811,21 +828,6 @@ class Editor {
this.run(postEditor => postEditor.toggleSection(tagName, this.range));
}

/**
* Finds and runs first matching text expansion for this event
* @param {Event} event keyboard event
* @return {Boolean} True when an expansion was found and run
* @private
*/
handleExpansion(keyEvent) {
let expansion = findExpansion(this.expansions, keyEvent, this);
if (expansion) {
expansion.run(this);
return true;
}
return false;
}

/**
* Finds and runs the first matching key command for the event
*
Expand Down
15 changes: 9 additions & 6 deletions src/js/editor/event-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Range from 'mobiledoc-kit/utils/cursor/range';
import { filter, forEach, contains } from 'mobiledoc-kit/utils/array-utils';
import Key from 'mobiledoc-kit/utils/key';
import { TAB } from 'mobiledoc-kit/utils/characters';
import TextInputHandler from 'mobiledoc-kit/editor/text-input-handler';
import Logger from 'mobiledoc-kit/utils/logger';
let log = Logger.for('event-manager'); /* jshint ignore:line */

Expand All @@ -19,6 +20,7 @@ const DOCUMENT_EVENT_TYPES = ['mouseup'];
export default class EventManager {
constructor(editor) {
this.editor = editor;
this._textInputHandler = new TextInputHandler(editor);
this._listeners = [];
this.isShift = false;
}
Expand All @@ -36,6 +38,10 @@ export default class EventManager {
});
}

registerInputHandler(inputHandler) {
this._textInputHandler.register(inputHandler);
}

_addListener(context, type) {
assert(`Missing listener for ${type}`, !!this[type]);

Expand Down Expand Up @@ -64,6 +70,7 @@ export default class EventManager {
}

destroy() {
this._textInputHandler.destroy();
this._removeListeners();
this._listeners = [];
}
Expand All @@ -83,7 +90,7 @@ export default class EventManager {
}

keypress(event) {
let { editor } = this;
let { editor, _textInputHandler } = this;
if (!editor.hasCursor()) { return; }

let key = Key.fromEvent(event);
Expand All @@ -93,11 +100,7 @@ export default class EventManager {
event.preventDefault();
}

if (editor.handleExpansion(event)) {
return;
} else {
editor.insertText(key.toString());
}
_textInputHandler.handle(key.toString());
}

keydown(event) {
Expand Down
24 changes: 24 additions & 0 deletions src/js/editor/text-expansion-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// from lodash
const reRegExpChar = /[\\^$.*+?()[\]{}|]/g,
reHasRegExpChar = new RegExp(reRegExpChar.source);

// from lodash
function escapeForRegex(string) {
return (string && reHasRegExpChar.test(string)) ? string.replace(reRegExpChar, '\\$&') : string;
}

export function convertExpansiontoHandler(expansion) {
let { run: originalRun, text, trigger } = expansion;
if (!!trigger) {
text = text + String.fromCharCode(trigger);
}
let match = new RegExp('^' + escapeForRegex(text) + '$');
let run = (editor, ...args) => {
let { range: { head } } = editor;
if (head.isTail()) {
originalRun(editor, ...args);
}
};

return { match, run };
}
97 changes: 0 additions & 97 deletions src/js/editor/text-expansions.js

This file was deleted.

53 changes: 53 additions & 0 deletions src/js/editor/text-input-handler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { endsWith } from 'mobiledoc-kit/utils/string-utils';
import assert from 'mobiledoc-kit/utils/assert';

class TextInputHandler {
constructor(editor) {
this.editor = editor;
this._handlers = [];
}

register(handler) {
assert(`Input Handler is not valid`, this._validateHandler(handler));
this._handlers.push(handler);
}

handle(string) {
let { editor } = this;
editor.insertText(string);

let matchedHandler = this._findHandler();
if (matchedHandler) {
let [ handler, matches ] = matchedHandler;
handler.run(editor, matches);
}
}

_findHandler() {
let { editor: { range: { head, head: { section } } } } = this;
let preText = section.textUntil(head);

for (let i=0; i < this._handlers.length; i++) {
let handler = this._handlers[i];
let {text, match} = handler;

if (text && endsWith(preText, text)) {
return [handler, [text]];
} else if (match && match.test(preText)) {
return [handler, match.exec(preText)];
}
}
}

_validateHandler(handler) {
return !!handler.run && // has `run`
(!!handler.text || !!handler.match) && // and `text` or `match`
!(!!handler.text && !!handler.match); // not both `text` and `match`
}

destroy() {
this._handlers = [];
}
}

export default TextInputHandler;
Loading