/** * Handles mapping key events to handling functions for an element or a Component. * One KeyMap can be used for multiple actions. * * A KeyMap must be configured with a {@link #target} as an event source which may be * an Element or a Component. * * If the target is an element, then the `keydown` event will trigger the invocation * of {@link #binding}s. * * It is possible to configure the KeyMap with a custom {@link #eventName} to listen for. * This may be useful when the {@link #target} is a Component. * * The KeyMap's event handling requires that the first parameter passed is a key event. * So if the Component's event signature is different, specify a {@link #processEvent} * configuration which accepts the event's parameters and returns a key event. * * Functions specified in {@link #binding}s are called with this signature: * `(String key, Ext.event.Event e)` (if the match is a multi-key combination * the callback will still be called only once). A KeyMap can also handle a string * representation of keys. By default KeyMap starts enabled. * * Usage: * * // map one key by key code * var map = new Ext.util.KeyMap({ * target: "my-element", * key: 13, // or Ext.event.Event.ENTER * handler: myHandler, * scope: myObject * }); * * // map multiple keys to one action by string * var map = new Ext.util.KeyMap({ * target: "my-element", * key: "a\r\n\t", * handler: myHandler, * scope: myObject * }); * * // map multiple keys to multiple actions by strings and array of codes * var map = new Ext.util.KeyMap({ * target: "my-element", * binding: [{ * key: [10, 13], * handler: function() { * alert("Return was pressed"); * } * }, { * key: "abc", * handler: function() { * alert('a, b or c was pressed'); * } * }, { * key: "\t", * ctrl: true, * shift: true, * handler: function() { * alert('Control + shift + tab was pressed.'); * } * }] * }); * * KeyMaps can also bind to Components and process key-based events fired by Components. * * To bind to a Component, include the Component event name to listen for, and a `processEvent` * implementation which returns the key event for further processing by the KeyMap: * * var map = new Ext.util.KeyMap({ * target: myGridView, * eventName: 'itemkeydown', * processEvent: function(view, record, node, index, event) { * // Load the event with the extra information needed by the mappings * event.view = view; * event.store = view.getStore(); * event.record = record; * event.index = index; * return event; * }, * binding: { * key: Ext.event.Event.DELETE, * handler: function(keyCode, e) { * e.store.remove(e.record); * * // Attempt to select the record that's now in its place * e.view.getSelectionModel().select(e.index); * } * } * }); */Ext.define('Ext.util.KeyMap', { alternateClassName: 'Ext.KeyMap', /** * @property {Ext.event.Event} lastKeyEvent * The last key event that this KeyMap handled. */ /** * @cfg {Ext.Component/Ext.dom.Element/HTMLElement/String} target * The object on which to listen for the event specified by the {@link #eventName} * config option. */ /** * @cfg {Object/Object[][]} binding * Either a single object describing a handling function for s specified key (or set of keys), * or an array of such objects. * @cfg {String/String[]} binding.key A single keycode or an array of keycodes to handle, or * a RegExp which specifies characters to handle, eg `/[a-z]/`. * @cfg {Boolean} binding.shift `true` to handle key only when shift is pressed, `false` * to handle the key only when shift is not pressed (defaults to undefined) * @cfg {Boolean} binding.ctrl `true` to handle key only when ctrl is pressed, `false` * to handle the key only when ctrl is not pressed (defaults to undefined) * @cfg {Boolean} binding.alt `true` to handle key only when alt is pressed, `false` * to handle the key only when alt is not pressed (defaults to undefined) * @cfg {Function} binding.handler The function to call when KeyMap finds the expected * key combination * @cfg {Function} binding.fn Alias of handler (for backwards-compatibility) * @cfg {Object} binding.scope The scope (`this` context) in which the handler function * is executed. * @cfg {String} binding.defaultEventAction A default action to apply to the event * *when the handler returns `true`*. Possible values are: stopEvent, stopPropagation, * preventDefault. If no value is set no action is performed. */ /** * @cfg {Object} [processEventScope=this] * The scope (`this` context) in which the {@link #processEvent} method is executed. */ /** * @cfg {Boolean} [ignoreInputFields=false] * Configure this as `true` if there are any input fields within the {@link #target}, * and this KeyNav should not process events from input fields * (`<input>`,`<textarea> and elements with `contentEditable="true"`) */ /** * @cfg {Number} [priority] * The priority to set on this KeyMap's listener. Listeners with a higher priority * are fired before those with lower priority. */ /** * @cfg {String} eventName * The event to listen for to pick up key events. */ eventName: 'keydown', constructor: function(config) { var me = this; // Handle legacy arg list in which the first argument is the target. // TODO: Deprecate in V5 //<debug> if ((arguments.length !== 1) || (typeof config === 'string') || config.dom || config.tagName || config === document || config.isComponent) { Ext.raise("Legacy multi-argument KeyMap constructor is removed. " + "Use a config object instead."); } //</debug> Ext.apply(me, config); me.bindings = []; if (!me.target.isComponent) { me.target = Ext.get(me.target); } if (me.binding) { me.addBinding(me.binding); } else if (config.key) { me.addBinding(config); } me.enable(); }, /** * Add a new binding to this KeyMap. * * Usage: * * // Create a KeyMap * var map = new Ext.util.KeyMap({ * target: Ext.getDoc(), * key: Ext.event.Event.ENTER, * handler: handleKey * }); * * // Add a new binding to the existing KeyMap later * map.addBinding({ * key: 'abc', * shift: true, * handler: handleKey * }); * * @param {Object/Object[]} binding A single KeyMap config or an array of configs. * The following config object properties are supported: * * @param {String/Array} binding.key A single keycode or an array of keycodes to handle, * or a RegExp which specifies characters to handle, eg `/[a-z]/`. * @param {Boolean} binding.shift `true` to handle key only when shift is pressed, * `false` to handle the key only when shift is not pressed (defaults to undefined). * @param {Boolean} binding.ctrl `true` to handle key only when ctrl is pressed, * `false` to handle the key only when ctrl is not pressed (defaults to undefined). * @param {Boolean} binding.alt `true` to handle key only when alt is pressed, * `false` to handle the key only when alt is not pressed (defaults to undefined). * @param {Function} binding.handler The function to call when KeyMap finds the * expected key combination. * @param {Function} binding.fn Alias of handler (for backwards-compatibility). * @param {Object} binding.scope The scope (`this` context) in which the handler function * is executed. * @param {String} binding.defaultEventAction A default action to apply to the event * *when the handler returns `true`*. Possible values are: stopEvent, stopPropagation, * preventDefault. If no value is set no action is performed. */ addBinding: function(binding) { var me = this, keyCode = binding.key, i, len; if (me.processing) { me.bindings = me.bindings.slice(0); } if (Ext.isArray(binding)) { for (i = 0, len = binding.length; i < len; i++) { me.addBinding(binding[i]); } return; } me.bindings.push(Ext.apply({ keyCode: me.processKeys(keyCode) }, binding)); }, /** * Remove a binding from this KeyMap. * @param {Object} binding See {@link #addBinding for options} */ removeBinding: function(binding) { var me = this, bindings = me.bindings, len = bindings.length, i, item, keys; if (me.processing) { me.bindings = bindings.slice(0); } keys = me.processKeys(binding.key); for (i = 0; i < len; i++) { item = bindings[i]; if ((item.fn || item.handler) === (binding.fn || binding.handler) && item.scope === binding.scope) { if (binding.alt === item.alt && binding.ctrl === item.ctrl && binding.shift === item.shift) { if (Ext.Array.equals(item.keyCode, keys)) { Ext.Array.erase(me.bindings, i, 1); return; } } } } }, processKeys: function(keyCode) { var processed = false, key, keys, keyString, len, i; // A RegExp to match typed characters if (keyCode.test) { return keyCode; } // A String of characters to match if (Ext.isString(keyCode)) { keys = []; keyString = keyCode.toUpperCase(); for (i = 0, len = keyString.length; i < len; i++) { keys.push(keyString.charCodeAt(i)); } keyCode = keys; processed = true; } // Numeric key code if (!Ext.isArray(keyCode)) { keyCode = [keyCode]; } if (!processed) { for (i = 0, len = keyCode.length; i < len; i++) { key = keyCode[i]; if (Ext.isString(key)) { keyCode[i] = key.toUpperCase().charCodeAt(0); } } } return keyCode; }, /** * Process the {@link #eventName event} from the {@link #target}. * @private * @param {Ext.event.Event} event */ handleTargetEvent: function(event) { var me = this, bindings, i, len, result; if (me.enabled) { bindings = me.bindings; // Process the event event = me.processEvent.apply(me.processEventScope || me, arguments); // A custom processEvent implementation may return falsy to stop the KeyMap's processing if (event) { me.lastKeyEvent = event; // Ignore events from input fields if configured to do so if (me.ignoreInputFields && Ext.fly(event.target).isInputField()) { return; } // If the processor does not return a keyEvent, we can't process it. // Allow them to return false to cancel processing of the event if (!event.getKey) { return event; } me.processing = true; for (i = 0, len = bindings.length; i < len; i++) { result = me.processBinding(bindings[i], event); if (result === false) { me.processing = false; return result; } } me.processing = false; } } }, /** * @cfg {Function} processEvent * An optional event processor function which accepts the argument list provided by the * {@link #eventName configured event} of the {@link #target}, and returns a keyEvent * for processing by the KeyMap. * * This may be useful when the {@link #target} is a Component with a complex event signature, * where the event is not the first parameter. Extra information from the event arguments * may be injected into the event for use by the handler functions before returning it. * * If `null` is returned the KeyMap stops processing the event. */ processEvent: Ext.identityFn, /** * Process a particular binding and fire the handler if necessary. * @private * @param {Object} binding The binding information * @param {Ext.event.Event} event */ processBinding: function(binding, event) { if (this.checkModifiers(binding, event)) { // eslint-disable-next-line vars-on-top var key = event.getKey(), handler = binding.fn || binding.handler, scope = binding.scope || this, keyCode = binding.keyCode, defaultEventAction = binding.defaultEventAction, i, len, result; // keyCode is a regExp specifying acceptable characters. eg /[a-z]/ if (keyCode.test) { if (keyCode.test(String.fromCharCode(event.getCharCode()))) { result = handler.call(scope, key, event); if (result !== true && defaultEventAction) { event[defaultEventAction](); } if (result === false) { return result; } } } // Array of key codes else if (keyCode.length) { for (i = 0, len = keyCode.length; i < len; i++) { if (key === keyCode[i]) { result = handler.call(scope, key, event); if (result !== true && defaultEventAction) { event[defaultEventAction](); } if (result === false) { return result; } break; } } } } }, /** * Check if the modifiers on the event match those on the binding * @private * @param {Object} binding * @param {Ext.event.Event} event * @return {Boolean} True if the event matches the binding */ checkModifiers: function(binding, event) { var keys = ['shift', 'ctrl', 'alt'], i, len, val, key; for (i = 0, len = keys.length; i < len; i++) { key = keys[i]; val = binding[key]; if (!(val === undefined || (val === event[key + 'Key']))) { return false; } } return true; }, /** * Shorthand for adding a single key listener. * * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes * or an object with the following options: `{key: (number or array), shift: (true/false), * ctrl: (true/false), alt: (true/false)}` * @param {Function} fn The function to call * @param {Object} [scope] The scope (`this` reference) in which the function is executed. * Defaults to the browser window. */ on: function(key, fn, scope) { var keyCode, shift, ctrl, alt; if (Ext.isObject(key) && !Ext.isArray(key)) { keyCode = key.key; shift = key.shift; ctrl = key.ctrl; alt = key.alt; } else { keyCode = key; } this.addBinding({ key: keyCode, shift: shift, ctrl: ctrl, alt: alt, fn: fn, scope: scope }); }, /** * Shorthand for removing a single key listener. * * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes * or an object with the following options: `{key: (number or array), shift: (true/false), * ctrl: (true/false), alt: (true/false)}` * @param {Function} fn The function to call * @param {Object} [scope] The scope (`this` reference) in which the function is executed. * Defaults to the browser window. */ un: function(key, fn, scope) { var keyCode, shift, ctrl, alt; if (Ext.isObject(key) && !Ext.isArray(key)) { keyCode = key.key; shift = key.shift; ctrl = key.ctrl; alt = key.alt; } else { keyCode = key; } this.removeBinding({ key: keyCode, shift: shift, ctrl: ctrl, alt: alt, fn: fn, scope: scope }); }, /** * Returns true if this KeyMap is enabled * @return {Boolean} */ isEnabled: function() { return !!this.enabled; }, /** * Enables this KeyMap */ enable: function() { var me = this; if (!me.enabled) { me.target.on(me.eventName, me.handleTargetEvent, me, { capture: me.capture, priority: me.priority }); me.enabled = true; } }, /** * Disable this KeyMap */ disable: function() { var me = this; if (me.enabled) { if (!me.target.destroyed) { me.target.removeListener(me.eventName, me.handleTargetEvent, me); } me.enabled = false; } }, /** * Convenience function for setting disabled/enabled by boolean. * @param {Boolean} disabled */ setDisabled: function(disabled) { if (disabled) { this.disable(); } else { this.enable(); } }, /** * Destroys the KeyMap instance and removes all handlers. * @param {Boolean} removeTarget True to also remove the {@link #target} */ destroy: function(removeTarget) { var me = this, target = me.target; me.bindings = []; me.disable(); me.target = null; if (removeTarget) { target.destroy(); Ext.raise("Using removeTarget argument in KeyMap destructor is not supported."); } me.callParent(); }});