Skip to content

Commit

Permalink
Add Editor#onTextInput to handle text or regex-match input
Browse files Browse the repository at this point in the history
Deprecate `Editor#registerTextExpansion` in favor of `onTextInput`

Fixes #367
  • Loading branch information
bantic committed Apr 26, 2016
1 parent e0913bd commit a0347b2
Show file tree
Hide file tree
Showing 16 changed files with 552 additions and 292 deletions.
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

0 comments on commit a0347b2

Please sign in to comment.