extjs-gpl
Version:
GPL licensed version of Sencha Ext JS
417 lines (363 loc) • 15.4 kB
JavaScript
/**
* A mixin for components that need to interact with the keyboard. The primary config
* for this class is the `{@link #keyMap keyMap}` config. This config is an object
* with key names as its properties and with values that describe how the key event
* should be handled.
*
* Key names may key name as documented in `Ext.event.Event`, numbers (which are treated
* as `keyCode` values), single characters (for those that are not defined in
* `Ext.event.Event`) or `charCode` values prefixed by '#' (e.g., "#65" for `charCode=65`).
*
* Entries that use a `keyCode` will be processed in a `keydown` event listener, while
* those that use a `charCode` will be processed in `keypress`. This can be overridden
* if the `keyMap` entry specifies an `event` property.
*
* Key names may be preceded by key modifiers. The modifier keys can be specified
* by prepending the modifier name to the key name separated by `+` or `-` (e.g.,
* "Ctrl+A" or "Ctrl-A"). Only one of these delimiters can be used in a given
* entry.
*
* Valid modifier names are:
*
* - Alt
* - Shift
* - Control (or "Ctrl" for short)
* - Command (or "Cmd" or "Meta")
* - CommandOrControl (or "CmdOrCtrl") for Cmd on Mac, Ctrl otherwise.
*
* For example:
*
* Ext.define('MyChartPanel', {
* extend: 'Ext.panel.Panel',
* controller: 'mycontroller',
*
* // Map keys to methods (typically in a ViewController):
* keyMap: {
* ENTER: 'onEnterKey',
*
* "ALT+PRINT_SCREEN": 'doScreenshot',
*
* // Cmd on Mac OS X, Ctrl on Windows/Linux.
* "CmdOrCtrl+C": 'doCopy',
*
* // This one is handled by a class method.
* ESC: {
* handler: 'destroy',
* scope: 'this',
* event: 'keypress' // default would be keydown
* }
* }
* });
*
* The method names are interpreted in the same way that event listener names are
* interpreted.
*
* @since 6.2.0
*/
Ext.define('Ext.mixin.Keyboard', function (Keyboard) { return {
extend: 'Ext.Mixin',
mixinConfig: {
id: 'keyboard'
},
config: {
/**
* @cfg {Object} keyMap
* An object containing handlers for keyboard events. The property names of this
* object are the key name and any modifiers. The values of the properties are the
* descriptors of how to handle each event.
*
* The handler descriptor can be simply the handler function (either the
* literal function or the method name), or it can be an object with these
* properties:
*
* - `handler`: The function or its name to call to handle the event.
* - `scope`: The this pointer context (can be "this" or "controller").
* - `event`: An optional override of the key event to which to listen.
*
* **Important:** Calls to `setKeyMap` do not replace the entire `keyMap` but
* instead update the provided mappings. That is, unless `null` is passed as the
* value of the `keyMap` which will clear the `keyMap` of all entries.
*
* @cfg {String} [keyMap.scope] The default scope to apply to key handlers
* which do not specify a scope. This is processed the same way as the scope of
* {@link #cfg-listeners}. It defaults to the `"controller"`, but using `'this'`
* means that an instance method will be used.
*/
keyMap: {
$value: null,
cached: true,
merge: function (value, baseValue, cls, mixin) {
// Allow nulling out parent class config
if (value === null) {
return value;
}
// We promote all values into objects but these objects do not get
// merged with base class values. Further, the keys get toUpperCased
// to normalize this aspect ('esc' vs 'ESC' vs 'Esc').
var ret = baseValue ? Ext.Object.chain(baseValue) : {},
key, ucKey, v, vs;
for (key in value) {
if (key !== 'scope') {
ucKey = key.toUpperCase();
if (!mixin || ret[ucKey] === undefined) {
// Promote to an object so we can always store the scope.
v = value[key];
if (v) {
if (typeof v === 'string' || typeof v === 'function') {
v = {
handler: v
};
} else {
v = Ext.apply({
handler: v.fn // overwritten by v.handler
}, v);
}
vs = v.scope || value.scope || 'self';
v.scope = (vs === 'controller') ? 'self.controller' : vs;
}
ret[ucKey] = v;
}
}
}
return ret;
}
},
/**
* @cfg {Boolean} keyMapEnabled
* Enables or disables processing keys in the `keyMap`. This value starts as
* `null` and if it is `null` when `initKeyMap` is called, it will automatically
* be set to `true`. Since `initKeyMap` is called by `Ext.Component` at the
* proper time, this is not something application code normally handles.
*/
keyMapEnabled: null
},
/**
* @cfg {Boolean} keyMapTarget
* The name of the member that should be used to listen for keydown/keypress events.
* This is intended to be controlled at the class level not per instance.
* @protected
*/
keyMapTarget: 'el',
applyKeyMap: function (keyMap, existingKeyMap) {
var me = this,
defaultScope, entry, key, mapping;
if (keyMap) {
if (existingKeyMap && me.isConfiguring) {
// As a cached config, we can be created with an existing value, but
// we do not want to modify that shared instance, so make a copy.
existingKeyMap = Ext.apply({}, existingKeyMap);
}
defaultScope = keyMap.scope || 'controller';
for (key in keyMap) {
if (key === 'scope') {
continue;
}
if (!(mapping = keyMap[key])) {
//<debug>
if (mapping === undefined) {
Ext.raise('keyMap entry "' + key + '" is undefined');
}
//</debug>
// if we have no mapping (eg, "ESC: null") and no mappings to
// overwrite, we can skip over it.
if (!existingKeyMap) {
continue;
}
} else {
if (typeof mapping === 'string' || typeof mapping === 'function') {
// Direct calls to setKeyMap() can get here because instance and
// class configs go through merge
mapping = {
handler: mapping,
scope: defaultScope
};
} else if (mapping) {
mapping = Ext.apply({
handler: mapping.fn, // mapping.handler will override
scope: defaultScope // mapping.scope will override
// all other properties of mapping are kept
}, mapping);
}
existingKeyMap = existingKeyMap || {}; // we'll need a keyMap
}
if (Keyboard.parseEntry(key, entry = mapping || {})) {
// So we end up with an object like this:
//
// "ALT+PRINT_SCREEN": {
// handler: 'doSummat',
// scope: 'controller',
// altKey: true
// }
//
existingKeyMap[entry.name] = mapping;
}
//<debug>
else {
Ext.raise('Invalid keyMap key specification "' + key + '"');
}
//</debug>
}
} else {
existingKeyMap = null; // nulling out the whole keyMap
}
if (me._keyMapReady) {
me.setKeyMapListener(existingKeyMap && me.getKeyMapEnabled());
}
return existingKeyMap || null;
},
/**
* This method should be called when the instance is ready to start listening for
* keyboard events. This is called automatically for `Ext.Component` and derived
* classes. In Classic Toolkit, this is done after the component is rendered.
* @protected
*/
initKeyMap: function () {
var me = this,
enabled = me.getKeyMapEnabled();
me._keyMapReady = true;
if (enabled === null) {
me.setKeyMapEnabled(true);
} else {
me.setKeyMapListener(enabled && me.getKeyMap());
}
},
updateKeyMapEnabled: function (enabled) {
this.setKeyMapListener(enabled && this._keyMapReady && this.getKeyMap());
},
privates: {
//<debug>
_keyMapListenCount: 0,
//</debug>
_keyMapReady: false,
callKeyMapEntry: function (entry, e) {
return entry && Ext.callback(entry.handler, entry.scope, [e, this], 0, this);
},
findKeyMapEntry: function (e) {
var me = this,
keyMap = me.getKeyMap(),
key, entry;
if (keyMap) {
for (key in keyMap) {
// If the key code and the modifier flags match, call the handler
// Cast the mapping's flag because they will be undefined if not
// true. Case metaKey because it's undefined on some platforms.
if (Keyboard.matchEntry(key, entry = keyMap[key], e)) {
return entry;
}
}
}
return null;
},
onKeyMapEvent: function (e) {
var me = this,
entry = me.getKeyMapEnabled() ? me.findKeyMapEntry(e) : null;
return me.callKeyMapEntry(entry, e);
},
setKeyMapListener: function (enabled) {
var me = this,
listener = me._keyMapListener,
eventSource;
//<debug>
++me._keyMapListenCount;
//</debug>
if (listener) {
// We always destroy the old listener since the eventSource could be
// different now...
listener.destroy();
listener = null;
}
if (enabled) {
eventSource = me[me.keyMapTarget];
if (typeof eventSource === 'function') {
eventSource = eventSource.call(me); // eg, 'getFocusEl'
}
listener = eventSource.on({
destroyable: true,
scope: me,
keydown: 'onKeyMapEvent',
keypress: 'onKeyMapEvent'
});
}
me._keyMapListener = listener || null;
},
statics: {
_charCodeRe: /^#([\d]+)$/,
_hyphenRe: /^[a-z]+\-/i, // eg "Ctrl-" (instead of "Ctrl+")
_keyMapEvents: {
charCode: 'keypress',
keyCode: 'keydown'
},
matchEntry: function (key, entry, e) {
var ev = e.browserEvent,
code;
if (e.type !== entry.event) {
return false;
}
if (!(code = entry.charCode)) {
if (entry.keyCode !== e.keyCode || !entry.shiftKey !== !ev.shiftKey) {
// when using keyCode, SHIFT must match too
return false;
}
}
else if (e.getCharCode() !== code) {
return false;
}
// NOTE: All modifier key properties are !-ed to ensure boolean-ness since
// they can be undefined...
return !entry.ctrlKey === !ev.ctrlKey &&
!entry.altKey === !ev.altKey &&
!entry.metaKey === !ev.metaKey;
},
parseEntry: function (key, entry) {
key = key.toUpperCase();
var me = this,
Event = Ext.event.Event,
keyFlags = Event.keyFlags,
delim = me._hyphenRe.test(key) ? '-' : '+',
keySpec = (key === delim) ? [delim] : key.split(delim),
n = keySpec.length - 1,
k = keySpec[n],
name = k,
type = 'keyCode',
code, i, match;
// Set the ctrlKey, altKey, metaKey, shiftKey flags
for (i = 0; i < n; i++) {
//<debug>
if (!keyFlags[keySpec[i]]) {
return false;
}
//</debug>
entry[keyFlags[keySpec[i]]] = true;
}
// Produce the canonical name of the key:
if (n) { // if (we have modifiers)
name = entry.ctrlKey ? 'CTRL+' : '';
name += entry.altKey ? 'ALT+' : '';
name += entry.metaKey ? 'META+' : '';
name += entry.shiftKey ? 'SHIFT+' : '';
name += k;
}
entry.name = name;
// Set the keyCode from the 'PRINT_SCREEN' key name.
if (isNaN(code = Event[k])) {
// Support charCode from a single letter or '#65' format.
if (!(match = me._charCodeRe.exec(k))) {
if (k.length === 1) {
code = k.charCodeAt(0);
}
} else {
code = +match[1]; // #42
}
if (code) {
type = 'charCode';
} else {
// Last chance! Is it just a number (a keyCode) like "27: 'onEscape'"?
code = +k;
}
}
entry.event = entry.event || me._keyMapEvents[type];
return !isNaN(code) && (entry[type] = code);
}
} // statics
} // privates
}});