UNPKG

wed

Version:

Wed is a schema-aware editor for XML documents.

225 lines 10.7 kB
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