Show More
This diff has been collapsed as it changes many lines, (931 lines changed) Show them Hide them | |||||
@@ -0,0 +1,931 b'' | |||||
|
1 | /*global define:false */ | |||
|
2 | /** | |||
|
3 | * Copyright 2013 Craig Campbell | |||
|
4 | * | |||
|
5 | * Licensed under the Apache License, Version 2.0 (the "License"); | |||
|
6 | * you may not use this file except in compliance with the License. | |||
|
7 | * You may obtain a copy of the License at | |||
|
8 | * | |||
|
9 | * http://www.apache.org/licenses/LICENSE-2.0 | |||
|
10 | * | |||
|
11 | * Unless required by applicable law or agreed to in writing, software | |||
|
12 | * distributed under the License is distributed on an "AS IS" BASIS, | |||
|
13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
|
14 | * See the License for the specific language governing permissions and | |||
|
15 | * limitations under the License. | |||
|
16 | * | |||
|
17 | * Mousetrap is a simple keyboard shortcut library for Javascript with | |||
|
18 | * no external dependencies | |||
|
19 | * | |||
|
20 | * @version 1.4.5 | |||
|
21 | * @url craig.is/killing/mice | |||
|
22 | */ | |||
|
23 | (function(window, document, undefined) { | |||
|
24 | ||||
|
25 | /** | |||
|
26 | * mapping of special keycodes to their corresponding keys | |||
|
27 | * | |||
|
28 | * everything in this dictionary cannot use keypress events | |||
|
29 | * so it has to be here to map to the correct keycodes for | |||
|
30 | * keyup/keydown events | |||
|
31 | * | |||
|
32 | * @type {Object} | |||
|
33 | */ | |||
|
34 | var _MAP = { | |||
|
35 | 8: 'backspace', | |||
|
36 | 9: 'tab', | |||
|
37 | 13: 'enter', | |||
|
38 | 16: 'shift', | |||
|
39 | 17: 'ctrl', | |||
|
40 | 18: 'alt', | |||
|
41 | 20: 'capslock', | |||
|
42 | 27: 'esc', | |||
|
43 | 32: 'space', | |||
|
44 | 33: 'pageup', | |||
|
45 | 34: 'pagedown', | |||
|
46 | 35: 'end', | |||
|
47 | 36: 'home', | |||
|
48 | 37: 'left', | |||
|
49 | 38: 'up', | |||
|
50 | 39: 'right', | |||
|
51 | 40: 'down', | |||
|
52 | 45: 'ins', | |||
|
53 | 46: 'del', | |||
|
54 | 91: 'meta', | |||
|
55 | 93: 'meta', | |||
|
56 | 224: 'meta' | |||
|
57 | }, | |||
|
58 | ||||
|
59 | /** | |||
|
60 | * mapping for special characters so they can support | |||
|
61 | * | |||
|
62 | * this dictionary is only used incase you want to bind a | |||
|
63 | * keyup or keydown event to one of these keys | |||
|
64 | * | |||
|
65 | * @type {Object} | |||
|
66 | */ | |||
|
67 | _KEYCODE_MAP = { | |||
|
68 | 106: '*', | |||
|
69 | 107: '+', | |||
|
70 | 109: '-', | |||
|
71 | 110: '.', | |||
|
72 | 111 : '/', | |||
|
73 | 186: ';', | |||
|
74 | 187: '=', | |||
|
75 | 188: ',', | |||
|
76 | 189: '-', | |||
|
77 | 190: '.', | |||
|
78 | 191: '/', | |||
|
79 | 192: '`', | |||
|
80 | 219: '[', | |||
|
81 | 220: '\\', | |||
|
82 | 221: ']', | |||
|
83 | 222: '\'' | |||
|
84 | }, | |||
|
85 | ||||
|
86 | /** | |||
|
87 | * this is a mapping of keys that require shift on a US keypad | |||
|
88 | * back to the non shift equivelents | |||
|
89 | * | |||
|
90 | * this is so you can use keyup events with these keys | |||
|
91 | * | |||
|
92 | * note that this will only work reliably on US keyboards | |||
|
93 | * | |||
|
94 | * @type {Object} | |||
|
95 | */ | |||
|
96 | _SHIFT_MAP = { | |||
|
97 | '~': '`', | |||
|
98 | '!': '1', | |||
|
99 | '@': '2', | |||
|
100 | '#': '3', | |||
|
101 | '$': '4', | |||
|
102 | '%': '5', | |||
|
103 | '^': '6', | |||
|
104 | '&': '7', | |||
|
105 | '*': '8', | |||
|
106 | '(': '9', | |||
|
107 | ')': '0', | |||
|
108 | '_': '-', | |||
|
109 | '+': '=', | |||
|
110 | ':': ';', | |||
|
111 | '\"': '\'', | |||
|
112 | '<': ',', | |||
|
113 | '>': '.', | |||
|
114 | '?': '/', | |||
|
115 | '|': '\\' | |||
|
116 | }, | |||
|
117 | ||||
|
118 | /** | |||
|
119 | * this is a list of special strings you can use to map | |||
|
120 | * to modifier keys when you specify your keyboard shortcuts | |||
|
121 | * | |||
|
122 | * @type {Object} | |||
|
123 | */ | |||
|
124 | _SPECIAL_ALIASES = { | |||
|
125 | 'option': 'alt', | |||
|
126 | 'command': 'meta', | |||
|
127 | 'return': 'enter', | |||
|
128 | 'escape': 'esc', | |||
|
129 | 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' | |||
|
130 | }, | |||
|
131 | ||||
|
132 | /** | |||
|
133 | * variable to store the flipped version of _MAP from above | |||
|
134 | * needed to check if we should use keypress or not when no action | |||
|
135 | * is specified | |||
|
136 | * | |||
|
137 | * @type {Object|undefined} | |||
|
138 | */ | |||
|
139 | _REVERSE_MAP, | |||
|
140 | ||||
|
141 | /** | |||
|
142 | * a list of all the callbacks setup via Mousetrap.bind() | |||
|
143 | * | |||
|
144 | * @type {Object} | |||
|
145 | */ | |||
|
146 | _callbacks = {}, | |||
|
147 | ||||
|
148 | /** | |||
|
149 | * direct map of string combinations to callbacks used for trigger() | |||
|
150 | * | |||
|
151 | * @type {Object} | |||
|
152 | */ | |||
|
153 | _directMap = {}, | |||
|
154 | ||||
|
155 | /** | |||
|
156 | * keeps track of what level each sequence is at since multiple | |||
|
157 | * sequences can start out with the same sequence | |||
|
158 | * | |||
|
159 | * @type {Object} | |||
|
160 | */ | |||
|
161 | _sequenceLevels = {}, | |||
|
162 | ||||
|
163 | /** | |||
|
164 | * variable to store the setTimeout call | |||
|
165 | * | |||
|
166 | * @type {null|number} | |||
|
167 | */ | |||
|
168 | _resetTimer, | |||
|
169 | ||||
|
170 | /** | |||
|
171 | * temporary state where we will ignore the next keyup | |||
|
172 | * | |||
|
173 | * @type {boolean|string} | |||
|
174 | */ | |||
|
175 | _ignoreNextKeyup = false, | |||
|
176 | ||||
|
177 | /** | |||
|
178 | * temporary state where we will ignore the next keypress | |||
|
179 | * | |||
|
180 | * @type {boolean} | |||
|
181 | */ | |||
|
182 | _ignoreNextKeypress = false, | |||
|
183 | ||||
|
184 | /** | |||
|
185 | * are we currently inside of a sequence? | |||
|
186 | * type of action ("keyup" or "keydown" or "keypress") or false | |||
|
187 | * | |||
|
188 | * @type {boolean|string} | |||
|
189 | */ | |||
|
190 | _nextExpectedAction = false; | |||
|
191 | ||||
|
192 | /** | |||
|
193 | * loop through the f keys, f1 to f19 and add them to the map | |||
|
194 | * programatically | |||
|
195 | */ | |||
|
196 | for (var i = 1; i < 20; ++i) { | |||
|
197 | _MAP[111 + i] = 'f' + i; | |||
|
198 | } | |||
|
199 | ||||
|
200 | /** | |||
|
201 | * loop through to map numbers on the numeric keypad | |||
|
202 | */ | |||
|
203 | for (i = 0; i <= 9; ++i) { | |||
|
204 | _MAP[i + 96] = i; | |||
|
205 | } | |||
|
206 | ||||
|
207 | /** | |||
|
208 | * cross browser add event method | |||
|
209 | * | |||
|
210 | * @param {Element|HTMLDocument} object | |||
|
211 | * @param {string} type | |||
|
212 | * @param {Function} callback | |||
|
213 | * @returns void | |||
|
214 | */ | |||
|
215 | function _addEvent(object, type, callback) { | |||
|
216 | if (object.addEventListener) { | |||
|
217 | object.addEventListener(type, callback, false); | |||
|
218 | return; | |||
|
219 | } | |||
|
220 | ||||
|
221 | object.attachEvent('on' + type, callback); | |||
|
222 | } | |||
|
223 | ||||
|
224 | /** | |||
|
225 | * takes the event and returns the key character | |||
|
226 | * | |||
|
227 | * @param {Event} e | |||
|
228 | * @return {string} | |||
|
229 | */ | |||
|
230 | function _characterFromEvent(e) { | |||
|
231 | ||||
|
232 | // for keypress events we should return the character as is | |||
|
233 | if (e.type == 'keypress') { | |||
|
234 | var character = String.fromCharCode(e.which); | |||
|
235 | ||||
|
236 | // if the shift key is not pressed then it is safe to assume | |||
|
237 | // that we want the character to be lowercase. this means if | |||
|
238 | // you accidentally have caps lock on then your key bindings | |||
|
239 | // will continue to work | |||
|
240 | // | |||
|
241 | // the only side effect that might not be desired is if you | |||
|
242 | // bind something like 'A' cause you want to trigger an | |||
|
243 | // event when capital A is pressed caps lock will no longer | |||
|
244 | // trigger the event. shift+a will though. | |||
|
245 | if (!e.shiftKey) { | |||
|
246 | character = character.toLowerCase(); | |||
|
247 | } | |||
|
248 | ||||
|
249 | return character; | |||
|
250 | } | |||
|
251 | ||||
|
252 | // for non keypress events the special maps are needed | |||
|
253 | if (_MAP[e.which]) { | |||
|
254 | return _MAP[e.which]; | |||
|
255 | } | |||
|
256 | ||||
|
257 | if (_KEYCODE_MAP[e.which]) { | |||
|
258 | return _KEYCODE_MAP[e.which]; | |||
|
259 | } | |||
|
260 | ||||
|
261 | // if it is not in the special map | |||
|
262 | ||||
|
263 | // with keydown and keyup events the character seems to always | |||
|
264 | // come in as an uppercase character whether you are pressing shift | |||
|
265 | // or not. we should make sure it is always lowercase for comparisons | |||
|
266 | return String.fromCharCode(e.which).toLowerCase(); | |||
|
267 | } | |||
|
268 | ||||
|
269 | /** | |||
|
270 | * checks if two arrays are equal | |||
|
271 | * | |||
|
272 | * @param {Array} modifiers1 | |||
|
273 | * @param {Array} modifiers2 | |||
|
274 | * @returns {boolean} | |||
|
275 | */ | |||
|
276 | function _modifiersMatch(modifiers1, modifiers2) { | |||
|
277 | return modifiers1.sort().join(',') === modifiers2.sort().join(','); | |||
|
278 | } | |||
|
279 | ||||
|
280 | /** | |||
|
281 | * resets all sequence counters except for the ones passed in | |||
|
282 | * | |||
|
283 | * @param {Object} doNotReset | |||
|
284 | * @returns void | |||
|
285 | */ | |||
|
286 | function _resetSequences(doNotReset) { | |||
|
287 | doNotReset = doNotReset || {}; | |||
|
288 | ||||
|
289 | var activeSequences = false, | |||
|
290 | key; | |||
|
291 | ||||
|
292 | for (key in _sequenceLevels) { | |||
|
293 | if (doNotReset[key]) { | |||
|
294 | activeSequences = true; | |||
|
295 | continue; | |||
|
296 | } | |||
|
297 | _sequenceLevels[key] = 0; | |||
|
298 | } | |||
|
299 | ||||
|
300 | if (!activeSequences) { | |||
|
301 | _nextExpectedAction = false; | |||
|
302 | } | |||
|
303 | } | |||
|
304 | ||||
|
305 | /** | |||
|
306 | * finds all callbacks that match based on the keycode, modifiers, | |||
|
307 | * and action | |||
|
308 | * | |||
|
309 | * @param {string} character | |||
|
310 | * @param {Array} modifiers | |||
|
311 | * @param {Event|Object} e | |||
|
312 | * @param {string=} sequenceName - name of the sequence we are looking for | |||
|
313 | * @param {string=} combination | |||
|
314 | * @param {number=} level | |||
|
315 | * @returns {Array} | |||
|
316 | */ | |||
|
317 | function _getMatches(character, modifiers, e, sequenceName, combination, level) { | |||
|
318 | var i, | |||
|
319 | callback, | |||
|
320 | matches = [], | |||
|
321 | action = e.type; | |||
|
322 | ||||
|
323 | // if there are no events related to this keycode | |||
|
324 | if (!_callbacks[character]) { | |||
|
325 | return []; | |||
|
326 | } | |||
|
327 | ||||
|
328 | // if a modifier key is coming up on its own we should allow it | |||
|
329 | if (action == 'keyup' && _isModifier(character)) { | |||
|
330 | modifiers = [character]; | |||
|
331 | } | |||
|
332 | ||||
|
333 | // loop through all callbacks for the key that was pressed | |||
|
334 | // and see if any of them match | |||
|
335 | for (i = 0; i < _callbacks[character].length; ++i) { | |||
|
336 | callback = _callbacks[character][i]; | |||
|
337 | ||||
|
338 | // if a sequence name is not specified, but this is a sequence at | |||
|
339 | // the wrong level then move onto the next match | |||
|
340 | if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) { | |||
|
341 | continue; | |||
|
342 | } | |||
|
343 | ||||
|
344 | // if the action we are looking for doesn't match the action we got | |||
|
345 | // then we should keep going | |||
|
346 | if (action != callback.action) { | |||
|
347 | continue; | |||
|
348 | } | |||
|
349 | ||||
|
350 | // if this is a keypress event and the meta key and control key | |||
|
351 | // are not pressed that means that we need to only look at the | |||
|
352 | // character, otherwise check the modifiers as well | |||
|
353 | // | |||
|
354 | // chrome will not fire a keypress if meta or control is down | |||
|
355 | // safari will fire a keypress if meta or meta+shift is down | |||
|
356 | // firefox will fire a keypress if meta or control is down | |||
|
357 | if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) { | |||
|
358 | ||||
|
359 | // when you bind a combination or sequence a second time it | |||
|
360 | // should overwrite the first one. if a sequenceName or | |||
|
361 | // combination is specified in this call it does just that | |||
|
362 | // | |||
|
363 | // @todo make deleting its own method? | |||
|
364 | var deleteCombo = !sequenceName && callback.combo == combination; | |||
|
365 | var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level; | |||
|
366 | if (deleteCombo || deleteSequence) { | |||
|
367 | _callbacks[character].splice(i, 1); | |||
|
368 | } | |||
|
369 | ||||
|
370 | matches.push(callback); | |||
|
371 | } | |||
|
372 | } | |||
|
373 | ||||
|
374 | return matches; | |||
|
375 | } | |||
|
376 | ||||
|
377 | /** | |||
|
378 | * takes a key event and figures out what the modifiers are | |||
|
379 | * | |||
|
380 | * @param {Event} e | |||
|
381 | * @returns {Array} | |||
|
382 | */ | |||
|
383 | function _eventModifiers(e) { | |||
|
384 | var modifiers = []; | |||
|
385 | ||||
|
386 | if (e.shiftKey) { | |||
|
387 | modifiers.push('shift'); | |||
|
388 | } | |||
|
389 | ||||
|
390 | if (e.altKey) { | |||
|
391 | modifiers.push('alt'); | |||
|
392 | } | |||
|
393 | ||||
|
394 | if (e.ctrlKey) { | |||
|
395 | modifiers.push('ctrl'); | |||
|
396 | } | |||
|
397 | ||||
|
398 | if (e.metaKey) { | |||
|
399 | modifiers.push('meta'); | |||
|
400 | } | |||
|
401 | ||||
|
402 | return modifiers; | |||
|
403 | } | |||
|
404 | ||||
|
405 | /** | |||
|
406 | * actually calls the callback function | |||
|
407 | * | |||
|
408 | * if your callback function returns false this will use the jquery | |||
|
409 | * convention - prevent default and stop propogation on the event | |||
|
410 | * | |||
|
411 | * @param {Function} callback | |||
|
412 | * @param {Event} e | |||
|
413 | * @returns void | |||
|
414 | */ | |||
|
415 | function _fireCallback(callback, e, combo) { | |||
|
416 | ||||
|
417 | // if this event should not happen stop here | |||
|
418 | if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo)) { | |||
|
419 | return; | |||
|
420 | } | |||
|
421 | ||||
|
422 | if (callback(e, combo) === false) { | |||
|
423 | if (e.preventDefault) { | |||
|
424 | e.preventDefault(); | |||
|
425 | } | |||
|
426 | ||||
|
427 | if (e.stopPropagation) { | |||
|
428 | e.stopPropagation(); | |||
|
429 | } | |||
|
430 | ||||
|
431 | e.returnValue = false; | |||
|
432 | e.cancelBubble = true; | |||
|
433 | } | |||
|
434 | } | |||
|
435 | ||||
|
436 | /** | |||
|
437 | * handles a character key event | |||
|
438 | * | |||
|
439 | * @param {string} character | |||
|
440 | * @param {Array} modifiers | |||
|
441 | * @param {Event} e | |||
|
442 | * @returns void | |||
|
443 | */ | |||
|
444 | function _handleKey(character, modifiers, e) { | |||
|
445 | var callbacks = _getMatches(character, modifiers, e), | |||
|
446 | i, | |||
|
447 | doNotReset = {}, | |||
|
448 | maxLevel = 0, | |||
|
449 | processedSequenceCallback = false; | |||
|
450 | ||||
|
451 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence | |||
|
452 | for (i = 0; i < callbacks.length; ++i) { | |||
|
453 | if (callbacks[i].seq) { | |||
|
454 | maxLevel = Math.max(maxLevel, callbacks[i].level); | |||
|
455 | } | |||
|
456 | } | |||
|
457 | ||||
|
458 | // loop through matching callbacks for this key event | |||
|
459 | for (i = 0; i < callbacks.length; ++i) { | |||
|
460 | ||||
|
461 | // fire for all sequence callbacks | |||
|
462 | // this is because if for example you have multiple sequences | |||
|
463 | // bound such as "g i" and "g t" they both need to fire the | |||
|
464 | // callback for matching g cause otherwise you can only ever | |||
|
465 | // match the first one | |||
|
466 | if (callbacks[i].seq) { | |||
|
467 | ||||
|
468 | // only fire callbacks for the maxLevel to prevent | |||
|
469 | // subsequences from also firing | |||
|
470 | // | |||
|
471 | // for example 'a option b' should not cause 'option b' to fire | |||
|
472 | // even though 'option b' is part of the other sequence | |||
|
473 | // | |||
|
474 | // any sequences that do not match here will be discarded | |||
|
475 | // below by the _resetSequences call | |||
|
476 | if (callbacks[i].level != maxLevel) { | |||
|
477 | continue; | |||
|
478 | } | |||
|
479 | ||||
|
480 | processedSequenceCallback = true; | |||
|
481 | ||||
|
482 | // keep a list of which sequences were matches for later | |||
|
483 | doNotReset[callbacks[i].seq] = 1; | |||
|
484 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); | |||
|
485 | continue; | |||
|
486 | } | |||
|
487 | ||||
|
488 | // if there were no sequence matches but we are still here | |||
|
489 | // that means this is a regular match so we should fire that | |||
|
490 | if (!processedSequenceCallback) { | |||
|
491 | _fireCallback(callbacks[i].callback, e, callbacks[i].combo); | |||
|
492 | } | |||
|
493 | } | |||
|
494 | ||||
|
495 | // if the key you pressed matches the type of sequence without | |||
|
496 | // being a modifier (ie "keyup" or "keypress") then we should | |||
|
497 | // reset all sequences that were not matched by this event | |||
|
498 | // | |||
|
499 | // this is so, for example, if you have the sequence "h a t" and you | |||
|
500 | // type "h e a r t" it does not match. in this case the "e" will | |||
|
501 | // cause the sequence to reset | |||
|
502 | // | |||
|
503 | // modifier keys are ignored because you can have a sequence | |||
|
504 | // that contains modifiers such as "enter ctrl+space" and in most | |||
|
505 | // cases the modifier key will be pressed before the next key | |||
|
506 | // | |||
|
507 | // also if you have a sequence such as "ctrl+b a" then pressing the | |||
|
508 | // "b" key will trigger a "keypress" and a "keydown" | |||
|
509 | // | |||
|
510 | // the "keydown" is expected when there is a modifier, but the | |||
|
511 | // "keypress" ends up matching the _nextExpectedAction since it occurs | |||
|
512 | // after and that causes the sequence to reset | |||
|
513 | // | |||
|
514 | // we ignore keypresses in a sequence that directly follow a keydown | |||
|
515 | // for the same character | |||
|
516 | var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress; | |||
|
517 | if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) { | |||
|
518 | _resetSequences(doNotReset); | |||
|
519 | } | |||
|
520 | ||||
|
521 | _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; | |||
|
522 | } | |||
|
523 | ||||
|
524 | /** | |||
|
525 | * handles a keydown event | |||
|
526 | * | |||
|
527 | * @param {Event} e | |||
|
528 | * @returns void | |||
|
529 | */ | |||
|
530 | function _handleKeyEvent(e) { | |||
|
531 | ||||
|
532 | // normalize e.which for key events | |||
|
533 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion | |||
|
534 | if (typeof e.which !== 'number') { | |||
|
535 | e.which = e.keyCode; | |||
|
536 | } | |||
|
537 | ||||
|
538 | var character = _characterFromEvent(e); | |||
|
539 | ||||
|
540 | // no character found then stop | |||
|
541 | if (!character) { | |||
|
542 | return; | |||
|
543 | } | |||
|
544 | ||||
|
545 | // need to use === for the character check because the character can be 0 | |||
|
546 | if (e.type == 'keyup' && _ignoreNextKeyup === character) { | |||
|
547 | _ignoreNextKeyup = false; | |||
|
548 | return; | |||
|
549 | } | |||
|
550 | ||||
|
551 | Mousetrap.handleKey(character, _eventModifiers(e), e); | |||
|
552 | } | |||
|
553 | ||||
|
554 | /** | |||
|
555 | * determines if the keycode specified is a modifier key or not | |||
|
556 | * | |||
|
557 | * @param {string} key | |||
|
558 | * @returns {boolean} | |||
|
559 | */ | |||
|
560 | function _isModifier(key) { | |||
|
561 | return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; | |||
|
562 | } | |||
|
563 | ||||
|
564 | /** | |||
|
565 | * called to set a 1 second timeout on the specified sequence | |||
|
566 | * | |||
|
567 | * this is so after each key press in the sequence you have 1 second | |||
|
568 | * to press the next key before you have to start over | |||
|
569 | * | |||
|
570 | * @returns void | |||
|
571 | */ | |||
|
572 | function _resetSequenceTimer() { | |||
|
573 | clearTimeout(_resetTimer); | |||
|
574 | _resetTimer = setTimeout(_resetSequences, 1000); | |||
|
575 | } | |||
|
576 | ||||
|
577 | /** | |||
|
578 | * reverses the map lookup so that we can look for specific keys | |||
|
579 | * to see what can and can't use keypress | |||
|
580 | * | |||
|
581 | * @return {Object} | |||
|
582 | */ | |||
|
583 | function _getReverseMap() { | |||
|
584 | if (!_REVERSE_MAP) { | |||
|
585 | _REVERSE_MAP = {}; | |||
|
586 | for (var key in _MAP) { | |||
|
587 | ||||
|
588 | // pull out the numeric keypad from here cause keypress should | |||
|
589 | // be able to detect the keys from the character | |||
|
590 | if (key > 95 && key < 112) { | |||
|
591 | continue; | |||
|
592 | } | |||
|
593 | ||||
|
594 | if (_MAP.hasOwnProperty(key)) { | |||
|
595 | _REVERSE_MAP[_MAP[key]] = key; | |||
|
596 | } | |||
|
597 | } | |||
|
598 | } | |||
|
599 | return _REVERSE_MAP; | |||
|
600 | } | |||
|
601 | ||||
|
602 | /** | |||
|
603 | * picks the best action based on the key combination | |||
|
604 | * | |||
|
605 | * @param {string} key - character for key | |||
|
606 | * @param {Array} modifiers | |||
|
607 | * @param {string=} action passed in | |||
|
608 | */ | |||
|
609 | function _pickBestAction(key, modifiers, action) { | |||
|
610 | ||||
|
611 | // if no action was picked in we should try to pick the one | |||
|
612 | // that we think would work best for this key | |||
|
613 | if (!action) { | |||
|
614 | action = _getReverseMap()[key] ? 'keydown' : 'keypress'; | |||
|
615 | } | |||
|
616 | ||||
|
617 | // modifier keys don't work as expected with keypress, | |||
|
618 | // switch to keydown | |||
|
619 | if (action == 'keypress' && modifiers.length) { | |||
|
620 | action = 'keydown'; | |||
|
621 | } | |||
|
622 | ||||
|
623 | return action; | |||
|
624 | } | |||
|
625 | ||||
|
626 | /** | |||
|
627 | * binds a key sequence to an event | |||
|
628 | * | |||
|
629 | * @param {string} combo - combo specified in bind call | |||
|
630 | * @param {Array} keys | |||
|
631 | * @param {Function} callback | |||
|
632 | * @param {string=} action | |||
|
633 | * @returns void | |||
|
634 | */ | |||
|
635 | function _bindSequence(combo, keys, callback, action) { | |||
|
636 | ||||
|
637 | // start off by adding a sequence level record for this combination | |||
|
638 | // and setting the level to 0 | |||
|
639 | _sequenceLevels[combo] = 0; | |||
|
640 | ||||
|
641 | /** | |||
|
642 | * callback to increase the sequence level for this sequence and reset | |||
|
643 | * all other sequences that were active | |||
|
644 | * | |||
|
645 | * @param {string} nextAction | |||
|
646 | * @returns {Function} | |||
|
647 | */ | |||
|
648 | function _increaseSequence(nextAction) { | |||
|
649 | return function() { | |||
|
650 | _nextExpectedAction = nextAction; | |||
|
651 | ++_sequenceLevels[combo]; | |||
|
652 | _resetSequenceTimer(); | |||
|
653 | }; | |||
|
654 | } | |||
|
655 | ||||
|
656 | /** | |||
|
657 | * wraps the specified callback inside of another function in order | |||
|
658 | * to reset all sequence counters as soon as this sequence is done | |||
|
659 | * | |||
|
660 | * @param {Event} e | |||
|
661 | * @returns void | |||
|
662 | */ | |||
|
663 | function _callbackAndReset(e) { | |||
|
664 | _fireCallback(callback, e, combo); | |||
|
665 | ||||
|
666 | // we should ignore the next key up if the action is key down | |||
|
667 | // or keypress. this is so if you finish a sequence and | |||
|
668 | // release the key the final key will not trigger a keyup | |||
|
669 | if (action !== 'keyup') { | |||
|
670 | _ignoreNextKeyup = _characterFromEvent(e); | |||
|
671 | } | |||
|
672 | ||||
|
673 | // weird race condition if a sequence ends with the key | |||
|
674 | // another sequence begins with | |||
|
675 | setTimeout(_resetSequences, 10); | |||
|
676 | } | |||
|
677 | ||||
|
678 | // loop through keys one at a time and bind the appropriate callback | |||
|
679 | // function. for any key leading up to the final one it should | |||
|
680 | // increase the sequence. after the final, it should reset all sequences | |||
|
681 | // | |||
|
682 | // if an action is specified in the original bind call then that will | |||
|
683 | // be used throughout. otherwise we will pass the action that the | |||
|
684 | // next key in the sequence should match. this allows a sequence | |||
|
685 | // to mix and match keypress and keydown events depending on which | |||
|
686 | // ones are better suited to the key provided | |||
|
687 | for (var i = 0; i < keys.length; ++i) { | |||
|
688 | var isFinal = i + 1 === keys.length; | |||
|
689 | var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action); | |||
|
690 | _bindSingle(keys[i], wrappedCallback, action, combo, i); | |||
|
691 | } | |||
|
692 | } | |||
|
693 | ||||
|
694 | /** | |||
|
695 | * Converts from a string key combination to an array | |||
|
696 | * | |||
|
697 | * @param {string} combination like "command+shift+l" | |||
|
698 | * @return {Array} | |||
|
699 | */ | |||
|
700 | function _keysFromString(combination) { | |||
|
701 | if (combination === '+') { | |||
|
702 | return ['+']; | |||
|
703 | } | |||
|
704 | ||||
|
705 | return combination.split('+'); | |||
|
706 | } | |||
|
707 | ||||
|
708 | /** | |||
|
709 | * Gets info for a specific key combination | |||
|
710 | * | |||
|
711 | * @param {string} combination key combination ("command+s" or "a" or "*") | |||
|
712 | * @param {string=} action | |||
|
713 | * @returns {Object} | |||
|
714 | */ | |||
|
715 | function _getKeyInfo(combination, action) { | |||
|
716 | var keys, | |||
|
717 | key, | |||
|
718 | i, | |||
|
719 | modifiers = []; | |||
|
720 | ||||
|
721 | // take the keys from this pattern and figure out what the actual | |||
|
722 | // pattern is all about | |||
|
723 | keys = _keysFromString(combination); | |||
|
724 | ||||
|
725 | for (i = 0; i < keys.length; ++i) { | |||
|
726 | key = keys[i]; | |||
|
727 | ||||
|
728 | // normalize key names | |||
|
729 | if (_SPECIAL_ALIASES[key]) { | |||
|
730 | key = _SPECIAL_ALIASES[key]; | |||
|
731 | } | |||
|
732 | ||||
|
733 | // if this is not a keypress event then we should | |||
|
734 | // be smart about using shift keys | |||
|
735 | // this will only work for US keyboards however | |||
|
736 | if (action && action != 'keypress' && _SHIFT_MAP[key]) { | |||
|
737 | key = _SHIFT_MAP[key]; | |||
|
738 | modifiers.push('shift'); | |||
|
739 | } | |||
|
740 | ||||
|
741 | // if this key is a modifier then add it to the list of modifiers | |||
|
742 | if (_isModifier(key)) { | |||
|
743 | modifiers.push(key); | |||
|
744 | } | |||
|
745 | } | |||
|
746 | ||||
|
747 | // depending on what the key combination is | |||
|
748 | // we will try to pick the best event for it | |||
|
749 | action = _pickBestAction(key, modifiers, action); | |||
|
750 | ||||
|
751 | return { | |||
|
752 | key: key, | |||
|
753 | modifiers: modifiers, | |||
|
754 | action: action | |||
|
755 | }; | |||
|
756 | } | |||
|
757 | ||||
|
758 | /** | |||
|
759 | * binds a single keyboard combination | |||
|
760 | * | |||
|
761 | * @param {string} combination | |||
|
762 | * @param {Function} callback | |||
|
763 | * @param {string=} action | |||
|
764 | * @param {string=} sequenceName - name of sequence if part of sequence | |||
|
765 | * @param {number=} level - what part of the sequence the command is | |||
|
766 | * @returns void | |||
|
767 | */ | |||
|
768 | function _bindSingle(combination, callback, action, sequenceName, level) { | |||
|
769 | ||||
|
770 | // store a direct mapped reference for use with Mousetrap.trigger | |||
|
771 | _directMap[combination + ':' + action] = callback; | |||
|
772 | ||||
|
773 | // make sure multiple spaces in a row become a single space | |||
|
774 | combination = combination.replace(/\s+/g, ' '); | |||
|
775 | ||||
|
776 | var sequence = combination.split(' '), | |||
|
777 | info; | |||
|
778 | ||||
|
779 | // if this pattern is a sequence of keys then run through this method | |||
|
780 | // to reprocess each pattern one key at a time | |||
|
781 | if (sequence.length > 1) { | |||
|
782 | _bindSequence(combination, sequence, callback, action); | |||
|
783 | return; | |||
|
784 | } | |||
|
785 | ||||
|
786 | info = _getKeyInfo(combination, action); | |||
|
787 | ||||
|
788 | // make sure to initialize array if this is the first time | |||
|
789 | // a callback is added for this key | |||
|
790 | _callbacks[info.key] = _callbacks[info.key] || []; | |||
|
791 | ||||
|
792 | // remove an existing match if there is one | |||
|
793 | _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); | |||
|
794 | ||||
|
795 | // add this call back to the array | |||
|
796 | // if it is a sequence put it at the beginning | |||
|
797 | // if not put it at the end | |||
|
798 | // | |||
|
799 | // this is important because the way these are processed expects | |||
|
800 | // the sequence ones to come first | |||
|
801 | _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ | |||
|
802 | callback: callback, | |||
|
803 | modifiers: info.modifiers, | |||
|
804 | action: info.action, | |||
|
805 | seq: sequenceName, | |||
|
806 | level: level, | |||
|
807 | combo: combination | |||
|
808 | }); | |||
|
809 | } | |||
|
810 | ||||
|
811 | /** | |||
|
812 | * binds multiple combinations to the same callback | |||
|
813 | * | |||
|
814 | * @param {Array} combinations | |||
|
815 | * @param {Function} callback | |||
|
816 | * @param {string|undefined} action | |||
|
817 | * @returns void | |||
|
818 | */ | |||
|
819 | function _bindMultiple(combinations, callback, action) { | |||
|
820 | for (var i = 0; i < combinations.length; ++i) { | |||
|
821 | _bindSingle(combinations[i], callback, action); | |||
|
822 | } | |||
|
823 | } | |||
|
824 | ||||
|
825 | // start! | |||
|
826 | _addEvent(document, 'keypress', _handleKeyEvent); | |||
|
827 | _addEvent(document, 'keydown', _handleKeyEvent); | |||
|
828 | _addEvent(document, 'keyup', _handleKeyEvent); | |||
|
829 | ||||
|
830 | var Mousetrap = { | |||
|
831 | ||||
|
832 | /** | |||
|
833 | * binds an event to mousetrap | |||
|
834 | * | |||
|
835 | * can be a single key, a combination of keys separated with +, | |||
|
836 | * an array of keys, or a sequence of keys separated by spaces | |||
|
837 | * | |||
|
838 | * be sure to list the modifier keys first to make sure that the | |||
|
839 | * correct key ends up getting bound (the last key in the pattern) | |||
|
840 | * | |||
|
841 | * @param {string|Array} keys | |||
|
842 | * @param {Function} callback | |||
|
843 | * @param {string=} action - 'keypress', 'keydown', or 'keyup' | |||
|
844 | * @returns void | |||
|
845 | */ | |||
|
846 | bind: function(keys, callback, action) { | |||
|
847 | keys = keys instanceof Array ? keys : [keys]; | |||
|
848 | _bindMultiple(keys, callback, action); | |||
|
849 | return this; | |||
|
850 | }, | |||
|
851 | ||||
|
852 | /** | |||
|
853 | * unbinds an event to mousetrap | |||
|
854 | * | |||
|
855 | * the unbinding sets the callback function of the specified key combo | |||
|
856 | * to an empty function and deletes the corresponding key in the | |||
|
857 | * _directMap dict. | |||
|
858 | * | |||
|
859 | * TODO: actually remove this from the _callbacks dictionary instead | |||
|
860 | * of binding an empty function | |||
|
861 | * | |||
|
862 | * the keycombo+action has to be exactly the same as | |||
|
863 | * it was defined in the bind method | |||
|
864 | * | |||
|
865 | * @param {string|Array} keys | |||
|
866 | * @param {string} action | |||
|
867 | * @returns void | |||
|
868 | */ | |||
|
869 | unbind: function(keys, action) { | |||
|
870 | return Mousetrap.bind(keys, function() {}, action); | |||
|
871 | }, | |||
|
872 | ||||
|
873 | /** | |||
|
874 | * triggers an event that has already been bound | |||
|
875 | * | |||
|
876 | * @param {string} keys | |||
|
877 | * @param {string=} action | |||
|
878 | * @returns void | |||
|
879 | */ | |||
|
880 | trigger: function(keys, action) { | |||
|
881 | if (_directMap[keys + ':' + action]) { | |||
|
882 | _directMap[keys + ':' + action]({}, keys); | |||
|
883 | } | |||
|
884 | return this; | |||
|
885 | }, | |||
|
886 | ||||
|
887 | /** | |||
|
888 | * resets the library back to its initial state. this is useful | |||
|
889 | * if you want to clear out the current keyboard shortcuts and bind | |||
|
890 | * new ones - for example if you switch to another page | |||
|
891 | * | |||
|
892 | * @returns void | |||
|
893 | */ | |||
|
894 | reset: function() { | |||
|
895 | _callbacks = {}; | |||
|
896 | _directMap = {}; | |||
|
897 | return this; | |||
|
898 | }, | |||
|
899 | ||||
|
900 | /** | |||
|
901 | * should we stop this event before firing off callbacks | |||
|
902 | * | |||
|
903 | * @param {Event} e | |||
|
904 | * @param {Element} element | |||
|
905 | * @return {boolean} | |||
|
906 | */ | |||
|
907 | stopCallback: function(e, element) { | |||
|
908 | ||||
|
909 | // if the element has the class "mousetrap" then no need to stop | |||
|
910 | if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { | |||
|
911 | return false; | |||
|
912 | } | |||
|
913 | ||||
|
914 | // stop for input, select, and textarea | |||
|
915 | return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; | |||
|
916 | }, | |||
|
917 | ||||
|
918 | /** | |||
|
919 | * exposes _handleKey publicly so it can be overwritten by extensions | |||
|
920 | */ | |||
|
921 | handleKey: _handleKey | |||
|
922 | }; | |||
|
923 | ||||
|
924 | // expose mousetrap to the global object | |||
|
925 | window.Mousetrap = Mousetrap; | |||
|
926 | ||||
|
927 | // expose mousetrap as an AMD module | |||
|
928 | if (typeof define === 'function' && define.amd) { | |||
|
929 | define(Mousetrap); | |||
|
930 | } | |||
|
931 | }) (window, document); |
General Comments 0
You need to be logged in to leave comments.
Login now