Upgrade to ExtJS 4.0.2 - Released 06/09/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.<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  * <b>Note: A KeyMap starts enabled</b>
56  */
57 Ext.define('Ext.util.KeyMap', {
58     alternateClassName: 'Ext.KeyMap',
59
60     /**
61      * Creates new KeyMap.
62      * @param {Mixed} el The element to bind to
63      * @param {Object} binding The binding (see {@link #addBinding})
64      * @param {String} eventName (optional) The event to bind to (defaults to "keydown")
65      */
66     constructor: function(el, binding, eventName){
67         var me = this;
68         
69         Ext.apply(me, {
70             el: Ext.get(el),
71             eventName: eventName || me.eventName,
72             bindings: []
73         });
74         if (binding) {
75             me.addBinding(binding);
76         }
77         me.enable();
78     },
79     
80     eventName: 'keydown',
81
82     /**
83      * Add a new binding to this KeyMap. The following config object properties are supported:
84      * <pre>
85 Property            Type             Description
86 ----------          ---------------  ----------------------------------------------------------------------
87 key                 String/Array     A single keycode or an array of keycodes to handle
88 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)
89 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)
90 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)
91 handler             Function         The function to call when KeyMap finds the expected key combination
92 fn                  Function         Alias of handler (for backwards-compatibility)
93 scope               Object           The scope of the callback function
94 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. 
95 </pre>
96      *
97      * Usage:
98      * <pre><code>
99 // Create a KeyMap
100 var map = new Ext.util.KeyMap(document, {
101     key: Ext.EventObject.ENTER,
102     fn: handleKey,
103     scope: this
104 });
105
106 //Add a new binding to the existing KeyMap later
107 map.addBinding({
108     key: 'abc',
109     shift: true,
110     fn: handleKey,
111     scope: this
112 });
113 </code></pre>
114      * @param {Object/Array} binding A single KeyMap config or an array of configs
115      */
116     addBinding : function(binding){
117         if (Ext.isArray(binding)) {
118             Ext.each(binding, this.addBinding, this);
119             return;
120         }
121         
122         var keyCode = binding.key,
123             processed = false,
124             key,
125             keys,
126             keyString,
127             i,
128             len;
129
130         if (Ext.isString(keyCode)) {
131             keys = [];
132             keyString = keyCode.toLowerCase();
133             
134             for (i = 0, len = keyString.length; i < len; ++i){
135                 keys.push(keyString.charCodeAt(i));
136             }
137             keyCode = keys;
138             processed = true;
139         }
140         
141         if (!Ext.isArray(keyCode)) {
142             keyCode = [keyCode];
143         }
144         
145         if (!processed) {
146             for (i = 0, len = keyCode.length; i < len; ++i) {
147                 key = keyCode[i];
148                 if (Ext.isString(key)) {
149                     keyCode[i] = key.toLowerCase().charCodeAt(0);
150                 }
151             }
152         }
153         
154         this.bindings.push(Ext.apply({
155             keyCode: keyCode
156         }, binding));
157     },
158     
159     /**
160      * Process any keydown events on the element
161      * @private
162      * @param {Ext.EventObject} event
163      */
164     handleKeyDown: function(event) {
165         if (this.enabled) { //just in case
166             var bindings = this.bindings,
167                 i = 0,
168                 len = bindings.length;
169                 
170             event = this.processEvent(event);
171             for(; i < len; ++i){
172                 this.processBinding(bindings[i], event);
173             }
174         }
175     },
176     
177     /**
178      * Ugly hack to allow this class to be tested. Currently WebKit gives
179      * no way to raise a key event properly with both
180      * a) A keycode
181      * b) The alt/ctrl/shift modifiers
182      * So we have to simulate them here. Yuk! 
183      * This is a stub method intended to be overridden by tests.
184      * More info: https://bugs.webkit.org/show_bug.cgi?id=16735
185      * @private
186      */
187     processEvent: function(event){
188         return event;
189     },
190     
191     /**
192      * Process a particular binding and fire the handler if necessary.
193      * @private
194      * @param {Object} binding The binding information
195      * @param {Ext.EventObject} event
196      */
197     processBinding: function(binding, event){
198         if (this.checkModifiers(binding, event)) {
199             var key = event.getKey(),
200                 handler = binding.fn || binding.handler,
201                 scope = binding.scope || this,
202                 keyCode = binding.keyCode,
203                 defaultEventAction = binding.defaultEventAction,
204                 i,
205                 len,
206                 keydownEvent = new Ext.EventObjectImpl(event);
207                 
208             
209             for (i = 0, len = keyCode.length; i < len; ++i) {
210                 if (key === keyCode[i]) {
211                     if (handler.call(scope, key, event) !== true && defaultEventAction) {
212                         keydownEvent[defaultEventAction]();
213                     }
214                     break;
215                 }
216             }
217         }
218     },
219     
220     /**
221      * Check if the modifiers on the event match those on the binding
222      * @private
223      * @param {Object} binding
224      * @param {Ext.EventObject} event
225      * @return {Boolean} True if the event matches the binding
226      */
227     checkModifiers: function(binding, e){
228         var keys = ['shift', 'ctrl', 'alt'],
229             i = 0,
230             len = keys.length,
231             val, key;
232             
233         for (; i < len; ++i){
234             key = keys[i];
235             val = binding[key];
236             if (!(val === undefined || (val === e[key + 'Key']))) {
237                 return false;
238             }
239         }
240         return true;
241     },
242
243     /**
244      * Shorthand for adding a single key listener
245      * @param {Number/Array/Object} key Either the numeric key code, array of key codes or an object with the
246      * following options:
247      * {key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)}
248      * @param {Function} fn The function to call
249      * @param {Object} scope (optional) The scope (<code>this</code> reference) in which the function is executed. Defaults to the browser window.
250      */
251     on: function(key, fn, scope) {
252         var keyCode, shift, ctrl, alt;
253         if (Ext.isObject(key) && !Ext.isArray(key)) {
254             keyCode = key.key;
255             shift = key.shift;
256             ctrl = key.ctrl;
257             alt = key.alt;
258         } else {
259             keyCode = key;
260         }
261         this.addBinding({
262             key: keyCode,
263             shift: shift,
264             ctrl: ctrl,
265             alt: alt,
266             fn: fn,
267             scope: scope
268         });
269     },
270
271     /**
272      * Returns true if this KeyMap is enabled
273      * @return {Boolean}
274      */
275     isEnabled : function(){
276         return this.enabled;
277     },
278
279     /**
280      * Enables this KeyMap
281      */
282     enable: function(){
283         if(!this.enabled){
284             this.el.on(this.eventName, this.handleKeyDown, this);
285             this.enabled = true;
286         }
287     },
288
289     /**
290      * Disable this KeyMap
291      */
292     disable: function(){
293         if(this.enabled){
294             this.el.removeListener(this.eventName, this.handleKeyDown, this);
295             this.enabled = false;
296         }
297     },
298
299     /**
300      * Convenience function for setting disabled/enabled by boolean.
301      * @param {Boolean} disabled
302      */
303     setDisabled : function(disabled){
304         if (disabled) {
305             this.disable();
306         } else {
307             this.enable();
308         }
309     },
310     
311     /**
312      * Destroys the KeyMap instance and removes all handlers.
313      * @param {Boolean} removeEl True to also remove the attached element
314      */
315     destroy: function(removeEl){
316         var me = this;
317         
318         me.bindings = [];
319         me.disable();
320         if (removeEl === true) {
321             me.el.remove();
322         }
323         delete me.el;
324     }
325 });