Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / util / KeyMap.js
1 /*
2
3 This file is part of Ext JS 4
4
5 Copyright (c) 2011 Sencha Inc
6
7 Contact:  http://www.sencha.com/contact
8
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
11
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
13
14 */
15 /**
16  * @class Ext.util.KeyMap
17  * Handles mapping keys to actions for an element. One key map can be used for multiple actions.
18  * The constructor accepts the same config object as defined by {@link #addBinding}.
19  * If you bind a callback function to a KeyMap, anytime the KeyMap handles an expected key
20  * combination it will call the function with this signature (if the match is a multi-key
21  * combination the callback will still be called only once): (String key, Ext.EventObject e)
22  * A KeyMap can also handle a string representation of keys. By default KeyMap starts enabled.<br />
23  * Usage:
24  <pre><code>
25 // map one key by key code
26 var map = new Ext.util.KeyMap("my-element", {
27     key: 13, // or Ext.EventObject.ENTER
28     fn: myHandler,
29     scope: myObject
30 });
31
32 // map multiple keys to one action by string
33 var map = new Ext.util.KeyMap("my-element", {
34     key: "a\r\n\t",
35     fn: myHandler,
36     scope: myObject
37 });
38
39 // map multiple keys to multiple actions by strings and array of codes
40 var map = new Ext.util.KeyMap("my-element", [
41     {
42         key: [10,13],
43         fn: function(){ alert("Return was pressed"); }
44     }, {
45         key: "abc",
46         fn: function(){ alert('a, b or c was pressed'); }
47     }, {
48         key: "\t",
49         ctrl:true,
50         shift:true,
51         fn: function(){ alert('Control + shift + tab was pressed.'); }
52     }
53 ]);
54 </code></pre>
55  */
56 Ext.define('Ext.util.KeyMap', {
57     alternateClassName: 'Ext.KeyMap',
58
59     /**
60      * Creates new KeyMap.
61      * @param {String/HTMLElement/Ext.Element} el The element or its ID to bind to
62      * @param {Object} binding The binding (see {@link #addBinding})
63      * @param {String} [eventName="keydown"] The event to bind to
64      */
65     constructor: function(el, binding, eventName){
66         var me = this;
67
68         Ext.apply(me, {
69             el: Ext.get(el),
70             eventName: eventName || me.eventName,
71             bindings: []
72         });
73         if (binding) {
74             me.addBinding(binding);
75         }
76         me.enable();
77     },
78
79     eventName: 'keydown',
80
81     /**
82      * Add a new binding to this KeyMap. The following config object properties are supported:
83      * <pre>
84 Property            Type             Description
85 ----------          ---------------  ----------------------------------------------------------------------
86 key                 String/Array     A single keycode or an array of keycodes to handle
87 shift               Boolean          True to handle key only when shift is pressed, False to handle the key only when shift is not pressed (defaults to undefined)
88 ctrl                Boolean          True to handle key only when ctrl is pressed, False to handle the key only when ctrl is not pressed (defaults to undefined)
89 alt                 Boolean          True to handle key only when alt is pressed, False to handle the key only when alt is not pressed (defaults to undefined)
90 handler             Function         The function to call when KeyMap finds the expected key combination
91 fn                  Function         Alias of handler (for backwards-compatibility)
92 scope               Object           The scope of the callback function
93 defaultEventAction  String           A default action to apply to the event. Possible values are: stopEvent, stopPropagation, preventDefault. If no value is set no action is performed.
94 </pre>
95      *
96      * Usage:
97      * <pre><code>
98 // Create a KeyMap
99 var map = new Ext.util.KeyMap(document, {
100     key: Ext.EventObject.ENTER,
101     fn: handleKey,
102     scope: this
103 });
104
105 //Add a new binding to the existing KeyMap later
106 map.addBinding({
107     key: 'abc',
108     shift: true,
109     fn: handleKey,
110     scope: this
111 });
112 </code></pre>
113      * @param {Object/Object[]} binding A single KeyMap config or an array of configs
114      */
115     addBinding : function(binding){
116         if (Ext.isArray(binding)) {
117             Ext.each(binding, this.addBinding, this);
118             return;
119         }
120
121         var keyCode = binding.key,
122             processed = false,
123             key,
124             keys,
125             keyString,
126             i,
127             len;
128
129         if (Ext.isString(keyCode)) {
130             keys = [];
131             keyString = keyCode.toUpperCase();
132
133             for (i = 0, len = keyString.length; i < len; ++i){
134                 keys.push(keyString.charCodeAt(i));
135             }
136             keyCode = keys;
137             processed = true;
138         }
139
140         if (!Ext.isArray(keyCode)) {
141             keyCode = [keyCode];
142         }
143
144         if (!processed) {
145             for (i = 0, len = keyCode.length; i < len; ++i) {
146                 key = keyCode[i];
147                 if (Ext.isString(key)) {
148                     keyCode[i] = key.toUpperCase().charCodeAt(0);
149                 }
150             }
151         }
152
153         this.bindings.push(Ext.apply({
154             keyCode: keyCode
155         }, binding));
156     },
157
158     /**
159      * Process any keydown events on the element
160      * @private
161      * @param {Ext.EventObject} event
162      */
163     handleKeyDown: function(event) {
164         if (this.enabled) { //just in case
165             var bindings = this.bindings,
166                 i = 0,
167                 len = bindings.length;
168
169             event = this.processEvent(event);
170             for(; i < len; ++i){
171                 this.processBinding(bindings[i], event);
172             }
173         }
174     },
175
176     /**
177      * Ugly hack to allow this class to be tested. Currently WebKit gives
178      * no way to raise a key event properly with both
179      * a) A keycode
180      * b) The alt/ctrl/shift modifiers
181      * So we have to simulate them here. Yuk!
182      * This is a stub method intended to be overridden by tests.
183      * More info: https://bugs.webkit.org/show_bug.cgi?id=16735
184      * @private
185      */
186     processEvent: function(event){
187         return event;
188     },
189
190     /**
191      * Process a particular binding and fire the handler if necessary.
192      * @private
193      * @param {Object} binding The binding information
194      * @param {Ext.EventObject} event
195      */
196     processBinding: function(binding, event){
197         if (this.checkModifiers(binding, event)) {
198             var key = event.getKey(),
199                 handler = binding.fn || binding.handler,
200                 scope = binding.scope || this,
201                 keyCode = binding.keyCode,
202                 defaultEventAction = binding.defaultEventAction,
203                 i,
204                 len,
205                 keydownEvent = new Ext.EventObjectImpl(event);
206
207
208             for (i = 0, len = keyCode.length; i < len; ++i) {
209                 if (key === keyCode[i]) {
210                     if (handler.call(scope, key, event) !== true && defaultEventAction) {
211                         keydownEvent[defaultEventAction]();
212                     }
213                     break;
214                 }
215             }
216         }
217     },
218
219     /**
220      * Check if the modifiers on the event match those on the binding
221      * @private
222      * @param {Object} binding
223      * @param {Ext.EventObject} event
224      * @return {Boolean} True if the event matches the binding
225      */
226     checkModifiers: function(binding, e){
227         var keys = ['shift', 'ctrl', 'alt'],
228             i = 0,
229             len = keys.length,
230             val, key;
231
232         for (; i < len; ++i){
233             key = keys[i];
234             val = binding[key];
235             if (!(val === undefined || (val === e[key + 'Key']))) {
236                 return false;
237             }
238         }
239         return true;
240     },
241
242     /**
243      * Shorthand for adding a single key listener
244      * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes or an object with the
245      * following options:
246      * {key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)}
247      * @param {Function} fn The function to call
248      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
249      */
250     on: function(key, fn, scope) {
251         var keyCode, shift, ctrl, alt;
252         if (Ext.isObject(key) && !Ext.isArray(key)) {
253             keyCode = key.key;
254             shift = key.shift;
255             ctrl = key.ctrl;
256             alt = key.alt;
257         } else {
258             keyCode = key;
259         }
260         this.addBinding({
261             key: keyCode,
262             shift: shift,
263             ctrl: ctrl,
264             alt: alt,
265             fn: fn,
266             scope: scope
267         });
268     },
269
270     /**
271      * Returns true if this KeyMap is enabled
272      * @return {Boolean}
273      */
274     isEnabled : function(){
275         return this.enabled;
276     },
277
278     /**
279      * Enables this KeyMap
280      */
281     enable: function(){
282         var me = this;
283         
284         if (!me.enabled) {
285             me.el.on(me.eventName, me.handleKeyDown, me);
286             me.enabled = true;
287         }
288     },
289
290     /**
291      * Disable this KeyMap
292      */
293     disable: function(){
294         var me = this;
295         
296         if (me.enabled) {
297             me.el.removeListener(me.eventName, me.handleKeyDown, me);
298             me.enabled = false;
299         }
300     },
301
302     /**
303      * Convenience function for setting disabled/enabled by boolean.
304      * @param {Boolean} disabled
305      */
306     setDisabled : function(disabled){
307         if (disabled) {
308             this.disable();
309         } else {
310             this.enable();
311         }
312     },
313
314     /**
315      * Destroys the KeyMap instance and removes all handlers.
316      * @param {Boolean} removeEl True to also remove the attached element
317      */
318     destroy: function(removeEl){
319         var me = this;
320
321         me.bindings = [];
322         me.disable();
323         if (removeEl === true) {
324             me.el.remove();
325         }
326         delete me.el;
327     }
328 });