Форк Rambox
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

/**
* 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;
}
});