/** * 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.EventObject 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.EventObject.ENTER * fn: 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", * fn: 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], * fn: function(){ alert("Return was pressed"); } * }, { * key: "abc", * fn: function(){ alert('a, b or c was pressed'); } * }, { * key: "\t", * ctrl:true, * shift:true, * fn: function(){ alert('Control + shift + tab was pressed.'); } * }] * }); * * Since 4.1.0, KeyMaps can bind to Components and process key-based events fired by Components. * * To bind to a Component, use the single parameter form of constructor: * * 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.EventObject.DELETE, * fn: 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); * e.view.el.focus(); * } * } * }); */ Ext.define('Ext.util.KeyMap', { alternateClassName: 'Ext.KeyMap', /** * @cfg {Ext.Component/Ext.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 * @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 of the callback function * @cfg {String} binding.defaultEventAction A default action to apply to the event. 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 {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 if ((arguments.length !== 1) || (typeof config === 'string') || config.dom || config.tagName || config === document || config.isComponent) { me.legacyConstructor.apply(me, arguments); return; } 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(); }, /** * @private * Old constructor signature * @param {String/HTMLElement/Ext.Element/Ext.Component} el The element or its ID, or Component to bind to * @param {Object} binding The binding (see {@link #addBinding}) * @param {String} [eventName="keydown"] The event to bind to */ legacyConstructor: function(el, binding, eventName){ var me = this; Ext.apply(me, { target: Ext.get(el), eventName: eventName || me.eventName, bindings: [] }); if (binding) { me.addBinding(binding); } me.enable(); }, /** * Add a new binding to this KeyMap. * * Usage: * * // Create a KeyMap * var map = new Ext.util.KeyMap(document, { * key: Ext.EventObject.ENTER, * fn: handleKey, * scope: this * }); * * //Add a new binding to the existing KeyMap later * map.addBinding({ * key: 'abc', * shift: true, * fn: handleKey, * scope: this * }); * * @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. * @param {Boolean} binding.shift True to handle key only when shift is pressed, * False to handle the keyonly 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 of the callback function. * @param {String} binding.defaultEventAction A default action to apply to the event. * Possible values are: stopEvent, stopPropagation, preventDefault. If no value is * set no action is performed.. */ addBinding : function(binding){ var keyCode = binding.key, processed = false, key, keys, keyString, i, len; if (Ext.isArray(binding)) { for (i = 0, len = binding.length; i < len; i++) { this.addBinding(binding[i]); } return; } 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; } 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); } } } this.bindings.push(Ext.apply({ keyCode: keyCode }, binding)); }, /** * Process any keydown events on the element * @private * @param {Ext.EventObject} event */ handleKeyDown: function(event) { var me = this, bindings, i, len; if (this.enabled) { //just in case bindings = this.bindings; i = 0; len = bindings.length; // Process the event event = me.processEvent.apply(me||me.processEventScope, arguments); // 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; } for(; i < len; ++i){ this.processBinding(bindings[i], event); } } }, /** * @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 s complex event signature. Extra information from * the event arguments may be injected into the event for use by the handler functions before returning it. */ processEvent: function(event){ return event; }, /** * Process a particular binding and fire the handler if necessary. * @private * @param {Object} binding The binding information * @param {Ext.EventObject} event */ processBinding: function(binding, event){ if (this.checkModifiers(binding, event)) { var key = event.getKey(), handler = binding.fn || binding.handler, scope = binding.scope || this, keyCode = binding.keyCode, defaultEventAction = binding.defaultEventAction, i, len, keydownEvent = new Ext.EventObjectImpl(event); for (i = 0, len = keyCode.length; i < len; ++i) { if (key === keyCode[i]) { if (handler.call(scope, key, event) !== true && defaultEventAction) { keydownEvent[defaultEventAction](); } break; } } } }, /** * Check if the modifiers on the event match those on the binding * @private * @param {Object} binding * @param {Ext.EventObject} event * @return {Boolean} True if the event matches the binding */ checkModifiers: function(binding, e) { var keys = ['shift', 'ctrl', 'alt'], i = 0, len = keys.length, val, key; for (; i < len; ++i){ key = keys[i]; val = binding[key]; if (!(val === undefined || (val === e[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 }); }, /** * 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.handleKeyDown, me); me.enabled = true; } }, /** * Disable this KeyMap */ disable: function() { var me = this; if (me.enabled) { me.target.removeListener(me.eventName, me.handleKeyDown, 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; me.bindings = []; me.disable(); if (removeTarget === true) { me.target.isComponent ? me.target.destroy() : me.target.remove(); } delete me.target; } });