wed
Version:
Wed is a schema-aware editor for XML documents.
225 lines • 10.7 kB
JavaScript
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
define(["require", "exports", "jquery", "./domtypeguards", "./domutil"], function (require, exports, jquery_1, domtypeguards_1, domutil_1) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
jquery_1 = __importDefault(jquery_1);
/**
* An InputTrigger listens to keyboard events and to DOM changes that insert
* text into an element. The object has to listen to both types of events
* because:
*
* - Listening only to keyboard events would miss modifications to the DOM tree
* that happen programmatically.
*
* - Listening only to DOM changes would not trap keyboard events that do not
* **inherently** modify the DOM tree like a backspace key hit at the start of
* an element.
*
* The portion of InputTrigger objects that handle keyboard events attaches
* itself to the editor to which the InputTrigger belongs in such a way that
* allows for suppressing the generic handling of such events. See
* [[addKeyHandler]] for more information.
*/
class InputTrigger {
/**
* @param editor The editor to which this ``InputTrigger`` belongs.
*
* @param mode The mode for which this ``InputTrigger`` is being created.
*
* @param selector This is a CSS selector which must be fit to be used in the
* GUI tree. (For instance by being the output of
* [["wed/domutil".toGUISelector]].)
*/
constructor(editor, mode, selector) {
// This is a map of keys that are actually text keys to their handlers. This
// map is in effect a submap of _key_to_handler. We want this for speed,
// because otherwise each text change event would require that the
// InputTrigger filter out all those keys that we don't care about. The keys
// that are "text input" keys are those that actually modify DOM text. So
// things like cursor movement keys or ENTER, BACKSPACE, or control keys do
// not appear *in* text and so are excluded from this map.
this.editor = editor;
this.mode = mode;
this.selector = selector;
// This is a map of all keys to their handlers.
this.keyToHandler = new Map();
this.textInputKeyToHandler = new Map();
editor.$guiRoot.on("wed-post-paste", this.pasteHandler.bind(this));
// Implementation note: getting keydown events to get fired on random HTML
// elements is finicky. For one thing, the element needs to be focusable,
// which is false for most elements unless tabindex is set. Even with
// tabindex set, browsers don't seem to consistently emit the events on the
// elements we want. An initial implementation attempted to set the keydown
// handler with $root.on("keydown", this._selector, ...) but this was not
// reliable. So we listen to all keydown events on $root and in the handler
// we filter out what we don't care about. More expensive but works
// reliably.
editor.$guiRoot.on("wed-input-trigger-keydown", this.keydownHandler.bind(this));
editor.$guiRoot.on("wed-input-trigger-keypress", this.keypressHandler.bind(this));
}
/**
* Adds a key handler to the object.
*
* The handler is called once per event. This means for instance that if a
* paste event introduces the text "a;b;c" and we are listening for ";", the
* handler will be called once, even though two ";" are added. It is up to the
* handler to detect that ";" has been added more than once.
*
* Handlers that wish to stop further processing or prevent the browser's
* default processing of an event must call the appropriate method on the
* ``event`` object.
*
* Although it is possible to add multiple handlers for the same key to the
* same ``InputTrigger`` object, the ``InputTrigger`` class does not define
* how one handler could prevent another handler from executing. Calling the
* methods on the ``event`` object does not in any way affect how an
* ``InputTrigger`` calls its handlers. However, as stated above, these
* methods can prevent further propagation of the JavaScript
* event. Consequently, if more than one handler should handle the same key on
* the same ``InputTrigger`` object, these handlers should either deal with
* orthogonal concerns (e.g. one modifies the data DOM tree and the other does
* logging), or provide their own mechanism to determine whether one can
* prevent the other from executing.
*
* @param key The key we are interested in.
*
* @param handler The handler that will process events related to that key.
*/
addKeyHandler(key, handler) {
let handlers = this.keyToHandler.get(key);
if (handlers === undefined) {
handlers = [];
this.keyToHandler.set(key, handlers);
}
handlers.push(handler);
// We could get here due to keys that are actually not text (e.g. ENTER,
// BACKSPACE).
if (key.anyModifier() || !key.keypress) {
return;
}
// We share the handlers array between the two maps.
if (!this.textInputKeyToHandler.has(key)) {
this.textInputKeyToHandler.set(key, handlers);
}
}
getNodeOfInterest() {
const caret = this.editor.caretManager.getDataCaret(true);
if (caret == null) {
return null;
}
if (this.editor.modeTree.getMode(caret.node) !== this.mode) {
// Outside our jurisdiction.
return null;
}
// We transit through the GUI tree to perform our match because CSS
// selectors cannot operate on XML namespace prefixes (or, at the time of
// writing, on XML namespaces, period).
const dataNode = domtypeguards_1.isText(caret.node) ? caret.node.parentNode : caret.node;
const guiNode = jquery_1.default.data(dataNode, "wed_mirror_node");
return domutil_1.closest(guiNode, this.selector.value, this.editor.guiRoot);
}
/**
* Handles ``keydown`` events.
*
* @param _wedEvent The DOM event wed generated to trigger this handler.
*
* @param e The original DOM event that wed received.
*/
keydownHandler(_wedEvent, e) {
const nodeOfInterest = this.getNodeOfInterest();
if (nodeOfInterest === null) {
return;
}
const dataNode = jquery_1.default.data(nodeOfInterest, "wed_mirror_node");
this.keyToHandler.forEach((handlers, key) => {
if (key.matchesEvent(e)) {
for (const handler of handlers) {
handler("keydown", dataNode, e);
}
}
});
}
/**
* Handles ``keypress`` events.
*
* @param _wedEvent The DOM event wed generated to trigger this handler.
*
* @param e The original DOM event that wed received.
*/
keypressHandler(_wedEvent, e) {
const nodeOfInterest = this.getNodeOfInterest();
if (nodeOfInterest === null) {
return;
}
const dataNode = jquery_1.default.data(nodeOfInterest, "wed_mirror_node");
this.keyToHandler.forEach((handlers, key) => {
if (key.matchesEvent(e)) {
for (const handler of handlers) {
handler("keypress", dataNode, e);
}
}
});
}
/**
* Handles ``paste`` events.
*
* @param _wedEvent The DOM event wed generated to trigger this handler.
*
* @param e The original DOM event that wed received.
*
* @param caret The data caret.
*
* @param data The data that the user wants to insert.
*/
pasteHandler(_wedEvent, e, caret, data) {
if (this.editor.undoingOrRedoing()) {
return;
}
const text = [];
let child = data.firstChild;
while (child !== null) {
if (domtypeguards_1.isText(child)) {
text.push(child);
}
child = child.nextSibling;
}
if (text.length === 0) {
return;
}
if (this.editor.modeTree.getMode(caret.node) !== this.mode) {
// Outside our jurisdiction.
return;
}
// We transit through the GUI tree to perform our match because CSS
// selectors cannot operate on XML namespace prefixes (or, at the time of
// writing, on XML namespaces, period).
const nodeOfInterest = (domtypeguards_1.isText(caret.node) ?
caret.node.parentNode : caret.node);
const guiNode = jquery_1.default.data(nodeOfInterest, "wed_mirror_node");
if (domutil_1.closest(guiNode, this.selector.value, this.editor.guiRoot) === null) {
return;
}
this.textInputKeyToHandler.forEach((handlers, key) => {
// We care only about text input
if (key.anyModifier()) {
return;
}
const ch = String.fromCharCode(key.which);
for (const node of text) {
// Skip those that are not in the tree anymore.
if (node.parentNode !== null && node.data.indexOf(ch) > -1) {
for (const handler of handlers) {
handler("paste", nodeOfInterest, e);
}
}
}
});
}
}
exports.InputTrigger = InputTrigger;
});
// LocalWords: InputTrigger keydown tabindex keypress submap jQuery focusable
// LocalWords: Dubeau MPL Mangalam gui html DOM
//# sourceMappingURL=input-trigger.js.map