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