microsoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemail
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
536 lines
18 KiB
536 lines
18 KiB
/** |
|
* 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 |
|
* 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 and 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, |
|
* 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', |
|
|
|
/** |
|
* @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 {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.dom.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.event.Event.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, 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 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 (`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.crtl === item.crtl && 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; |
|
|
|
if (me.enabled) { |
|
bindings = me.bindings; |
|
i = 0; |
|
len = bindings.length; |
|
|
|
// 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 < len; ++i){ |
|
me.processBinding(bindings[i], event); |
|
} |
|
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)) { |
|
var key = event.getKey(), |
|
handler = binding.fn || binding.handler, |
|
scope = binding.scope || this, |
|
keyCode = binding.keyCode, |
|
defaultEventAction = binding.defaultEventAction, |
|
i, |
|
len; |
|
|
|
// keyCode is a regExp specifying acceptable characters. eg /[a-z]/ |
|
if (keyCode.test) { |
|
if (keyCode.test(String.fromCharCode(event.getCharCode()))) { |
|
if (handler.call(scope, key, event) !== true && defaultEventAction) { |
|
event[defaultEventAction](); |
|
} |
|
} |
|
} |
|
// Array of key codes |
|
else if (keyCode.length) { |
|
for (i = 0, len = keyCode.length; i < len; ++i) { |
|
if (key === keyCode[i]) { |
|
if (handler.call(scope, key, event) !== true && defaultEventAction) { |
|
event[defaultEventAction](); |
|
} |
|
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 = 0, |
|
len = keys.length, |
|
val, key; |
|
|
|
for (; 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}); |
|
me.enabled = true; |
|
} |
|
}, |
|
|
|
/** |
|
* Disable this KeyMap |
|
*/ |
|
disable: function() { |
|
var me = this; |
|
|
|
if (me.enabled) { |
|
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(); |
|
if (removeTarget) { |
|
target.destroy(); |
|
} |
|
delete me.target; |
|
} |
|
});
|
|
|