diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69512ba..04a5719 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,10 @@ Changelog
* Update Vimari interface to allow users access to their configuration.
* Remove `closeTabReverse` action.
+* Normal mode now isolates keybindings from the underlying website, this means that to interact with the underlying website you need to enter insert mode.
+* You can enter insert mode by pressing i and exit the mode by pressing esc.
+* In insert mode Vimari keybindings are disabled (except for esc which brings you back to normal mode) allowing you to interact with the underlying website.
+
### 2.0.3 (2019-09-26)
* Fix newTabHintToggle to use shift+f instead of F
diff --git a/Vimari Extension/js/injected.js b/Vimari Extension/js/injected.js
index b1bd6cc..406a3d6 100644
--- a/Vimari Extension/js/injected.js
+++ b/Vimari Extension/js/injected.js
@@ -84,7 +84,7 @@ var actionMap = {
};
// Meant to be overridden, but still has to be copy/pasted from the original...
-Mousetrap.stopCallback = function(e, element, combo) {
+Mousetrap.prototype.stopCallback = function(e, element, combo) {
// Escape key is special, no need to stop. Vimari-specific.
if (combo === 'esc' || combo === 'ctrl+[') { return false; }
@@ -159,8 +159,27 @@ function executeAction(actionName) {
function unbindKeyCodes() {
Mousetrap.reset();
+ document.removeEventListener("keydown", stopSitePropagation);
}
+// Stops propagation of keyboard events in normal mode. Adding this
+// callback to the document using the useCapture flag allows us to
+// prevent custom key behaviour implemented by the underlying website.
+function stopSitePropagation() {
+ return function (e) {
+ if (insertMode == false && !isActiveElementEditable()) {
+ e.stopPropagation()
+ }
+ }
+}
+
+// Check whether the current active element is editable.
+function isActiveElementEditable() {
+ const el = document.activeElement;
+ return (el != null && isEditable(el))
+}
+
+
// Adds an optional modifier to the configured key code for the action
function getKeyCode(actionName) {
var keyCode = '';
@@ -210,24 +229,24 @@ function isEmbed(element) { return ["EMBED", "OBJECT"].indexOf(element.tagName)
// Message handling functions
// ==========================
+function messageHandler(event){
+ if (event.name == "updateSettingsEvent") {
+ setSettings(event.message);
+ }
+}
+
/*
* Callback to pass settings to injected script
*/
function setSettings(msg) {
settings = msg;
- bindKeyCodesToActions(msg);
+ activateExtension(settings);
}
-/*
- * Enable or disable the extension on this tab
- */
-function setActive(msg) {
- extensionActive = msg;
- if(msg) {
- bindKeyCodesToActions();
- } else {
- unbindKeyCodes();
- }
+function activateExtension(settings) {
+ // Stop keydown propagation
+ document.addEventListener("keydown", stopSitePropagation(), true);
+ bindKeyCodesToActions(settings);
}
function isExcludedUrl(storedExcludedUrls, currentUrl) {
@@ -275,12 +294,6 @@ function inIframe () {
if(!inIframe()){
extensionCommunicator.requestSettingsUpdate()
}
-
-function messageHandler(event){
- if (event.name == "updateSettingsEvent") {
- setSettings(event.message);
- }
-}
// Export to make it testable
window.isExcludedUrl = isExcludedUrl;
diff --git a/Vimari Extension/js/lib/mousetrap.js b/Vimari Extension/js/lib/mousetrap.js
index f8c080d..941372a 100644
--- a/Vimari Extension/js/lib/mousetrap.js
+++ b/Vimari Extension/js/lib/mousetrap.js
@@ -1,5 +1,9 @@
+// Custom behaviour for Vimari implemented at line 185. Be aware of this
+// when upgrading mousetrap.
+
+/*global define:false */
/**
- * Copyright 2012 Craig Campbell
+ * Copyright 2012-2017 Craig Campbell
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +20,15 @@
* Mousetrap is a simple keyboard shortcut library for Javascript with
* no external dependencies
*
- * @version 1.3.0
+ * @version 1.6.5
* @url craig.is/killing/mice
*/
-(function() {
+(function(window, document, undefined) {
+
+ // Check if mousetrap is used inside browser, if not, return
+ if (!window) {
+ return;
+ }
/**
* mapping of special keycodes to their corresponding keys
@@ -31,154 +40,112 @@
* @type {Object}
*/
var _MAP = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 16: 'shift',
- 17: 'ctrl',
- 18: 'alt',
- 20: 'capslock',
- 27: 'esc',
- 32: 'space',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 45: 'ins',
- 46: 'del',
- 91: 'meta',
- 93: 'meta',
- 224: 'meta'
- },
-
- /**
- * mapping for special characters so they can support
- *
- * this dictionary is only used incase you want to bind a
- * keyup or keydown event to one of these keys
- *
- * @type {Object}
- */
- _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- },
-
- /**
- * this is a mapping of keys that require shift on a US keypad
- * back to the non shift equivelents
- *
- * this is so you can use keyup events with these keys
- *
- * note that this will only work reliably on US keyboards
- *
- * @type {Object}
- */
- _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- },
-
- /**
- * this is a list of special strings you can use to map
- * to modifier keys when you specify your keyboard shortcuts
- *
- * @type {Object}
- */
- _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc'
- },
-
- /**
- * variable to store the flipped version of _MAP from above
- * needed to check if we should use keypress or not when no action
- * is specified
- *
- * @type {Object|undefined}
- */
- _REVERSE_MAP,
-
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- _callbacks = {},
-
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- _directMap = {},
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 16: 'shift',
+ 17: 'ctrl',
+ 18: 'alt',
+ 20: 'capslock',
+ 27: 'esc',
+ 32: 'space',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 45: 'ins',
+ 46: 'del',
+ 91: 'meta',
+ 93: 'meta',
+ 224: 'meta'
+ };
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- _sequenceLevels = {},
+ /**
+ * mapping for special characters so they can support
+ *
+ * this dictionary is only used incase you want to bind a
+ * keyup or keydown event to one of these keys
+ *
+ * @type {Object}
+ */
+ var _KEYCODE_MAP = {
+ 106: '*',
+ 107: '+',
+ 109: '-',
+ 110: '.',
+ 111 : '/',
+ 186: ';',
+ 187: '=',
+ 188: ',',
+ 189: '-',
+ 190: '.',
+ 191: '/',
+ 192: '`',
+ 219: '[',
+ 220: '\\',
+ 221: ']',
+ 222: '\''
+ };
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- _resetTimer,
+ /**
+ * this is a mapping of keys that require shift on a US keypad
+ * back to the non shift equivelents
+ *
+ * this is so you can use keyup events with these keys
+ *
+ * note that this will only work reliably on US keyboards
+ *
+ * @type {Object}
+ */
+ var _SHIFT_MAP = {
+ '~': '`',
+ '!': '1',
+ '@': '2',
+ '#': '3',
+ '$': '4',
+ '%': '5',
+ '^': '6',
+ '&': '7',
+ '*': '8',
+ '(': '9',
+ ')': '0',
+ '_': '-',
+ '+': '=',
+ ':': ';',
+ '\"': '\'',
+ '<': ',',
+ '>': '.',
+ '?': '/',
+ '|': '\\'
+ };
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- _ignoreNextKeyup = false,
+ /**
+ * this is a list of special strings you can use to map
+ * to modifier keys when you specify your keyboard shortcuts
+ *
+ * @type {Object}
+ */
+ var _SPECIAL_ALIASES = {
+ 'option': 'alt',
+ 'command': 'meta',
+ 'return': 'enter',
+ 'escape': 'esc',
+ 'plus': '+',
+ 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
+ };
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- _sequenceType = false;
+ /**
+ * variable to store the flipped version of _MAP from above
+ * needed to check if we should use keypress or not when no action
+ * is specified
+ *
+ * @type {Object|undefined}
+ */
+ var _REVERSE_MAP;
/**
* loop through the f keys, f1 to f19 and add them to the map
@@ -192,7 +159,13 @@
* loop through to map numbers on the numeric keypad
*/
for (i = 0; i <= 9; ++i) {
- _MAP[i + 96] = i;
+
+ // This needs to use a string cause otherwise since 0 is falsey
+ // mousetrap will never fire for numpad 0 pressed as part of a keydown
+ // event.
+ //
+ // @see https://github.com/ccampbell/mousetrap/pull/258
+ _MAP[i + 96] = i.toString();
}
/**
@@ -205,7 +178,10 @@
*/
function _addEvent(object, type, callback) {
if (object.addEventListener) {
- object.addEventListener(type, callback, false);
+ // VIMARI CUSTOMISATION:
+ // We set the useCapture to true such that events are handled before
+ // being dispatched to any EventTarget beneath it in the DOM tree.
+ object.addEventListener(type, callback, true);
return;
}
@@ -222,7 +198,22 @@
// for keypress events we should return the character as is
if (e.type == 'keypress') {
- return String.fromCharCode(e.which);
+ var character = String.fromCharCode(e.which);
+
+ // if the shift key is not pressed then it is safe to assume
+ // that we want the character to be lowercase. this means if
+ // you accidentally have caps lock on then your key bindings
+ // will continue to work
+ //
+ // the only side effect that might not be desired is if you
+ // bind something like 'A' cause you want to trigger an
+ // event when capital A is pressed caps lock will no longer
+ // trigger the event. shift+a will though.
+ if (!e.shiftKey) {
+ character = character.toLowerCase();
+ }
+
+ return character;
}
// for non keypress events the special maps are needed
@@ -235,6 +226,10 @@
}
// if it is not in the special map
+
+ // with keydown and keyup events the character seems to always
+ // come in as an uppercase character whether you are pressing shift
+ // or not. we should make sure it is always lowercase for comparisons
return String.fromCharCode(e.which).toLowerCase();
}
@@ -249,97 +244,6 @@
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
- /**
- * resets all sequence counters except for the ones passed in
- *
- * @param {Object} doNotReset
- * @returns void
- */
- function _resetSequences(doNotReset, maxLevel) {
- doNotReset = doNotReset || {};
-
- var activeSequences = false,
- key;
-
- for (key in _sequenceLevels) {
- if (doNotReset[key] && _sequenceLevels[key] > maxLevel) {
- activeSequences = true;
- continue;
- }
- _sequenceLevels[key] = 0;
- }
-
- if (!activeSequences) {
- _sequenceType = false;
- }
- }
-
- /**
- * finds all callbacks that match based on the keycode, modifiers,
- * and action
- *
- * @param {string} character
- * @param {Array} modifiers
- * @param {Event|Object} e
- * @param {boolean=} remove - should we remove any matches
- * @param {string=} combination
- * @returns {Array}
- */
- function _getMatches(character, modifiers, e, remove, combination) {
- var i,
- callback,
- matches = [],
- action = e.type;
-
- // if there are no events related to this keycode
- if (!_callbacks[character]) {
- return [];
- }
-
- // if a modifier key is coming up on its own we should allow it
- if (action == 'keyup' && _isModifier(character)) {
- modifiers = [character];
- }
-
- // loop through all callbacks for the key that was pressed
- // and see if any of them match
- for (i = 0; i < _callbacks[character].length; ++i) {
- callback = _callbacks[character][i];
-
- // if this is a sequence but it is not at the right level
- // then move onto the next match
- if (callback.seq && _sequenceLevels[callback.seq] != callback.level) {
- continue;
- }
-
- // if the action we are looking for doesn't match the action we got
- // then we should keep going
- if (action != callback.action) {
- continue;
- }
-
- // if this is a keypress event and the meta key and control key
- // are not pressed that means that we need to only look at the
- // character, otherwise check the modifiers as well
- //
- // chrome will not fire a keypress if meta or control is down
- // safari will fire a keypress if meta or meta+shift is down
- // firefox will fire a keypress if meta or control is down
- if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
-
- // remove is used so if you change your mind and call bind a
- // second time with a new function the first one is overwritten
- if (remove && callback.combo == combination) {
- _callbacks[character].splice(i, 1);
- }
-
- matches.push(callback);
- }
- }
-
- return matches;
- }
-
/**
* takes a key event and figures out what the modifiers are
*
@@ -369,113 +273,33 @@
}
/**
- * actually calls the callback function
+ * prevents default for this event
*
- * if your callback function returns false this will use the jquery
- * convention - prevent default and stop propogation on the event
- *
- * @param {Function} callback
* @param {Event} e
* @returns void
*/
- function _fireCallback(callback, e, combo) {
-
- // if this event should not happen stop here
- if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo)) {
+ function _preventDefault(e) {
+ if (e.preventDefault) {
+ e.preventDefault();
return;
}
- if (callback(e, combo) === false) {
- if (e.preventDefault) {
- e.preventDefault();
- }
-
- if (e.stopPropagation) {
- e.stopPropagation();
- }
-
- e.returnValue = false;
- e.cancelBubble = true;
- }
- }
-
- /**
- * handles a character key event
- *
- * @param {string} character
- * @param {Event} e
- * @returns void
- */
- function _handleCharacter(character, e) {
- var callbacks = _getMatches(character, _eventModifiers(e), e),
- i,
- doNotReset = {},
- maxLevel = 0,
- processedSequenceCallback = false;
-
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
-
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
- processedSequenceCallback = true;
-
- // as we loop through keep track of the max
- // any sequence at a lower level will be discarded
- maxLevel = Math.max(maxLevel, callbacks[i].level);
-
- // keep a list of which sequences were matches for later
- doNotReset[callbacks[i].seq] = 1;
- _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
- continue;
- }
-
- // if there were no sequence matches but we are still here
- // that means this is a regular match so we should fire that
- if (!processedSequenceCallback && !_sequenceType) {
- _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
- }
- }
-
- // if you are inside of a sequence and the key you are pressing
- // is not a modifier key then we should reset all sequences
- // that were not matched by this key event
- if (e.type == _sequenceType && !_isModifier(character)) {
- _resetSequences(doNotReset, maxLevel);
- }
+ e.returnValue = false;
}
/**
- * handles a keydown event
+ * stops propogation for this event
*
* @param {Event} e
* @returns void
*/
- function _handleKey(e) {
-
- // normalize e.which for key events
- // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
- if (typeof e.which !== 'number') {
- e.which = e.keyCode;
- }
-
- var character = _characterFromEvent(e);
-
- // no character found then stop
- if (!character) {
- return;
- }
-
- if (e.type == 'keyup' && _ignoreNextKeyup == character) {
- _ignoreNextKeyup = false;
+ function _stopPropagation(e) {
+ if (e.stopPropagation) {
+ e.stopPropagation();
return;
}
- _handleCharacter(character, e);
+ e.cancelBubble = true;
}
/**
@@ -488,19 +312,6 @@
return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
}
- /**
- * called to set a 1 second timeout on the specified sequence
- *
- * this is so after each key press in the sequence you have 1 second
- * to press the next key before you have to start over
- *
- * @returns void
- */
- function _resetSequenceTimer() {
- clearTimeout(_resetTimer);
- _resetTimer = setTimeout(_resetSequences, 1000);
- }
-
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
@@ -551,104 +362,36 @@
}
/**
- * binds a key sequence to an event
+ * Converts from a string key combination to an array
*
- * @param {string} combo - combo specified in bind call
- * @param {Array} keys
- * @param {Function} callback
- * @param {string=} action
- * @returns void
+ * @param {string} combination like "command+shift+l"
+ * @return {Array}
*/
- function _bindSequence(combo, keys, callback, action) {
-
- // start off by adding a sequence level record for this combination
- // and setting the level to 0
- _sequenceLevels[combo] = 0;
-
- // if there is no action pick the best one for the first key
- // in the sequence
- if (!action) {
- action = _pickBestAction(keys[0], []);
+ function _keysFromString(combination) {
+ if (combination === '+') {
+ return ['+'];
}
- /**
- * callback to increase the sequence level for this sequence and reset
- * all other sequences that were active
- *
- * @param {Event} e
- * @returns void
- */
- var _increaseSequence = function(e) {
- _sequenceType = action;
- ++_sequenceLevels[combo];
- _resetSequenceTimer();
- },
-
- /**
- * wraps the specified callback inside of another function in order
- * to reset all sequence counters as soon as this sequence is done
- *
- * @param {Event} e
- * @returns void
- */
- _callbackAndReset = function(e) {
- _fireCallback(callback, e, combo);
-
- // we should ignore the next key up if the action is key down
- // or keypress. this is so if you finish a sequence and
- // release the key the final key will not trigger a keyup
- if (action !== 'keyup') {
- _ignoreNextKeyup = _characterFromEvent(e);
- }
-
- // weird race condition if a sequence ends with the key
- // another sequence begins with
- setTimeout(_resetSequences, 10);
- },
- i;
-
- // loop through keys one at a time and bind the appropriate callback
- // function. for any key leading up to the final one it should
- // increase the sequence. after the final, it should reset all sequences
- for (i = 0; i < keys.length; ++i) {
- _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i);
- }
+ combination = combination.replace(/\+{2}/g, '+plus');
+ return combination.split('+');
}
/**
- * binds a single keyboard combination
+ * Gets info for a specific key combination
*
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequenceName - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
+ * @param {string} combination key combination ("command+s" or "a" or "*")
+ * @param {string=} action
+ * @returns {Object}
*/
- function _bindSingle(combination, callback, action, sequenceName, level) {
-
- // store a direct mapped reference for use with Mousetrap.trigger
- _directMap[combination + ':' + action] = callback;
-
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
-
- var sequence = combination.split(' '),
- i,
- key,
- keys,
- modifiers = [];
-
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- _bindSequence(combination, sequence, callback, action);
- return;
- }
+ function _getKeyInfo(combination, action) {
+ var keys;
+ var key;
+ var i;
+ var modifiers = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
- keys = combination === '+' ? ['+'] : combination.split('+');
+ keys = _keysFromString(combination);
for (i = 0; i < keys.length; ++i) {
key = keys[i];
@@ -676,144 +419,646 @@
// we will try to pick the best event for it
action = _pickBestAction(key, modifiers, action);
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- if (!_callbacks[key]) {
- _callbacks[key] = [];
+ return {
+ key: key,
+ modifiers: modifiers,
+ action: action
+ };
+ }
+
+ function _belongsTo(element, ancestor) {
+ if (element === null || element === document) {
+ return false;
}
- // remove an existing match if there is one
- _getMatches(key, modifiers, {type: action}, !sequenceName, combination);
+ if (element === ancestor) {
+ return true;
+ }
- // add this call back to the array
- // if it is a sequence put it at the beginning
- // if not put it at the end
- //
- // this is important because the way these are processed expects
- // the sequence ones to come first
- _callbacks[key][sequenceName ? 'unshift' : 'push']({
- callback: callback,
- modifiers: modifiers,
- action: action,
- seq: sequenceName,
- level: level,
- combo: combination
- });
+ return _belongsTo(element.parentNode, ancestor);
}
- /**
- * binds multiple combinations to the same callback
- *
- * @param {Array} combinations
- * @param {Function} callback
- * @param {string|undefined} action
- * @returns void
- */
- function _bindMultiple(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
+ function Mousetrap(targetElement) {
+ var self = this;
+
+ targetElement = targetElement || document;
+
+ if (!(self instanceof Mousetrap)) {
+ return new Mousetrap(targetElement);
}
- }
- // start!
- _addEvent(document, 'keypress', _handleKey);
- _addEvent(document, 'keydown', _handleKey);
- _addEvent(document, 'keyup', _handleKey);
+ /**
+ * element to attach key events to
+ *
+ * @type {Element}
+ */
+ self.target = targetElement;
- var Mousetrap = {
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
+ *
+ * @type {Object}
+ */
+ self._callbacks = {};
/**
- * binds an event to mousetrap
+ * direct map of string combinations to callbacks used for trigger()
*
- * can be a single key, a combination of keys separated with +,
- * an array of keys, or a sequence of keys separated by spaces
+ * @type {Object}
+ */
+ self._directMap = {};
+
+ /**
+ * keeps track of what level each sequence is at since multiple
+ * sequences can start out with the same sequence
+ *
+ * @type {Object}
+ */
+ var _sequenceLevels = {};
+
+ /**
+ * variable to store the setTimeout call
+ *
+ * @type {null|number}
+ */
+ var _resetTimer;
+
+ /**
+ * temporary state where we will ignore the next keyup
+ *
+ * @type {boolean|string}
+ */
+ var _ignoreNextKeyup = false;
+
+ /**
+ * temporary state where we will ignore the next keypress
+ *
+ * @type {boolean}
+ */
+ var _ignoreNextKeypress = false;
+
+ /**
+ * are we currently inside of a sequence?
+ * type of action ("keyup" or "keydown" or "keypress") or false
+ *
+ * @type {boolean|string}
+ */
+ var _nextExpectedAction = false;
+
+ /**
+ * resets all sequence counters except for the ones passed in
+ *
+ * @param {Object} doNotReset
+ * @returns void
+ */
+ function _resetSequences(doNotReset) {
+ doNotReset = doNotReset || {};
+
+ var activeSequences = false,
+ key;
+
+ for (key in _sequenceLevels) {
+ if (doNotReset[key]) {
+ activeSequences = true;
+ continue;
+ }
+ _sequenceLevels[key] = 0;
+ }
+
+ if (!activeSequences) {
+ _nextExpectedAction = false;
+ }
+ }
+
+ /**
+ * finds all callbacks that match based on the keycode, modifiers,
+ * and action
+ *
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event|Object} e
+ * @param {string=} sequenceName - name of the sequence we are looking for
+ * @param {string=} combination
+ * @param {number=} level
+ * @returns {Array}
+ */
+ function _getMatches(character, modifiers, e, sequenceName, combination, level) {
+ var i;
+ var callback;
+ var matches = [];
+ var action = e.type;
+
+ // if there are no events related to this keycode
+ if (!self._callbacks[character]) {
+ return [];
+ }
+
+ // if a modifier key is coming up on its own we should allow it
+ if (action == 'keyup' && _isModifier(character)) {
+ modifiers = [character];
+ }
+
+ // loop through all callbacks for the key that was pressed
+ // and see if any of them match
+ for (i = 0; i < self._callbacks[character].length; ++i) {
+ callback = self._callbacks[character][i];
+
+ // if a sequence name is not specified, but this is a sequence at
+ // the wrong level then move onto the next match
+ if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
+ continue;
+ }
+
+ // if the action we are looking for doesn't match the action we got
+ // then we should keep going
+ if (action != callback.action) {
+ continue;
+ }
+
+ // if this is a keypress event and the meta key and control key
+ // are not pressed that means that we need to only look at the
+ // character, otherwise check the modifiers as well
+ //
+ // chrome will not fire a keypress if meta or control is down
+ // safari will fire a keypress if meta or meta+shift is down
+ // firefox will fire a keypress if meta or control is down
+ if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
+
+ // when you bind a combination or sequence a second time it
+ // should overwrite the first one. if a sequenceName or
+ // combination is specified in this call it does just that
+ //
+ // @todo make deleting its own method?
+ var deleteCombo = !sequenceName && callback.combo == combination;
+ var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
+ if (deleteCombo || deleteSequence) {
+ self._callbacks[character].splice(i, 1);
+ }
+
+ matches.push(callback);
+ }
+ }
+
+ return matches;
+ }
+
+ /**
+ * actually calls the callback function
*
- * be sure to list the modifier keys first to make sure that the
- * correct key ends up getting bound (the last key in the pattern)
+ * if your callback function returns false this will use the jquery
+ * convention - prevent default and stop propogation on the event
*
- * @param {string|Array} keys
* @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @param {Event} e
* @returns void
*/
- bind: function(keys, callback, action) {
- keys = keys instanceof Array ? keys : [keys];
- _bindMultiple(keys, callback, action);
- return this;
- },
+ function _fireCallback(callback, e, combo, sequence) {
+
+ // if this event should not happen stop here
+ if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
+ return;
+ }
+
+ if (callback(e, combo) === false) {
+ _preventDefault(e);
+ _stopPropagation(e);
+ }
+ }
/**
- * unbinds an event to mousetrap
+ * handles a character key event
*
- * the unbinding sets the callback function of the specified key combo
- * to an empty function and deletes the corresponding key in the
- * _directMap dict.
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event} e
+ * @returns void
+ */
+ self._handleKey = function(character, modifiers, e) {
+ var callbacks = _getMatches(character, modifiers, e);
+ var i;
+ var doNotReset = {};
+ var maxLevel = 0;
+ var processedSequenceCallback = false;
+
+ // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
+ for (i = 0; i < callbacks.length; ++i) {
+ if (callbacks[i].seq) {
+ maxLevel = Math.max(maxLevel, callbacks[i].level);
+ }
+ }
+
+ // loop through matching callbacks for this key event
+ for (i = 0; i < callbacks.length; ++i) {
+
+ // fire for all sequence callbacks
+ // this is because if for example you have multiple sequences
+ // bound such as "g i" and "g t" they both need to fire the
+ // callback for matching g cause otherwise you can only ever
+ // match the first one
+ if (callbacks[i].seq) {
+
+ // only fire callbacks for the maxLevel to prevent
+ // subsequences from also firing
+ //
+ // for example 'a option b' should not cause 'option b' to fire
+ // even though 'option b' is part of the other sequence
+ //
+ // any sequences that do not match here will be discarded
+ // below by the _resetSequences call
+ if (callbacks[i].level != maxLevel) {
+ continue;
+ }
+
+ processedSequenceCallback = true;
+
+ // keep a list of which sequences were matches for later
+ doNotReset[callbacks[i].seq] = 1;
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
+ continue;
+ }
+
+ // if there were no sequence matches but we are still here
+ // that means this is a regular match so we should fire that
+ if (!processedSequenceCallback) {
+ _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+ }
+ }
+
+ // if the key you pressed matches the type of sequence without
+ // being a modifier (ie "keyup" or "keypress") then we should
+ // reset all sequences that were not matched by this event
+ //
+ // this is so, for example, if you have the sequence "h a t" and you
+ // type "h e a r t" it does not match. in this case the "e" will
+ // cause the sequence to reset
+ //
+ // modifier keys are ignored because you can have a sequence
+ // that contains modifiers such as "enter ctrl+space" and in most
+ // cases the modifier key will be pressed before the next key
+ //
+ // also if you have a sequence such as "ctrl+b a" then pressing the
+ // "b" key will trigger a "keypress" and a "keydown"
+ //
+ // the "keydown" is expected when there is a modifier, but the
+ // "keypress" ends up matching the _nextExpectedAction since it occurs
+ // after and that causes the sequence to reset
+ //
+ // we ignore keypresses in a sequence that directly follow a keydown
+ // for the same character
+ var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
+ if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
+ _resetSequences(doNotReset);
+ }
+
+ _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
+ };
+
+ /**
+ * handles a keydown event
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _handleKeyEvent(e) {
+
+ // normalize e.which for key events
+ // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+ if (typeof e.which !== 'number') {
+ e.which = e.keyCode;
+ }
+
+ var character = _characterFromEvent(e);
+
+ // no character found then stop
+ if (!character) {
+ return;
+ }
+
+ // need to use === for the character check because the character can be 0
+ if (e.type == 'keyup' && _ignoreNextKeyup === character) {
+ _ignoreNextKeyup = false;
+ return;
+ }
+
+ self.handleKey(character, _eventModifiers(e), e);
+ }
+
+ /**
+ * called to set a 1 second timeout on the specified sequence
*
- * TODO: actually remove this from the _callbacks dictionary instead
- * of binding an empty function
+ * this is so after each key press in the sequence you have 1 second
+ * to press the next key before you have to start over
*
- * the keycombo+action has to be exactly the same as
- * it was defined in the bind method
+ * @returns void
+ */
+ function _resetSequenceTimer() {
+ clearTimeout(_resetTimer);
+ _resetTimer = setTimeout(_resetSequences, 1000);
+ }
+
+ /**
+ * binds a key sequence to an event
*
- * @param {string|Array} keys
- * @param {string} action
+ * @param {string} combo - combo specified in bind call
+ * @param {Array} keys
+ * @param {Function} callback
+ * @param {string=} action
* @returns void
*/
- unbind: function(keys, action) {
- return Mousetrap.bind(keys, function() {}, action);
- },
+ function _bindSequence(combo, keys, callback, action) {
+
+ // start off by adding a sequence level record for this combination
+ // and setting the level to 0
+ _sequenceLevels[combo] = 0;
+
+ /**
+ * callback to increase the sequence level for this sequence and reset
+ * all other sequences that were active
+ *
+ * @param {string} nextAction
+ * @returns {Function}
+ */
+ function _increaseSequence(nextAction) {
+ return function() {
+ _nextExpectedAction = nextAction;
+ ++_sequenceLevels[combo];
+ _resetSequenceTimer();
+ };
+ }
+
+ /**
+ * wraps the specified callback inside of another function in order
+ * to reset all sequence counters as soon as this sequence is done
+ *
+ * @param {Event} e
+ * @returns void
+ */
+ function _callbackAndReset(e) {
+ _fireCallback(callback, e, combo);
+
+ // we should ignore the next key up if the action is key down
+ // or keypress. this is so if you finish a sequence and
+ // release the key the final key will not trigger a keyup
+ if (action !== 'keyup') {
+ _ignoreNextKeyup = _characterFromEvent(e);
+ }
+
+ // weird race condition if a sequence ends with the key
+ // another sequence begins with
+ setTimeout(_resetSequences, 10);
+ }
+
+ // loop through keys one at a time and bind the appropriate callback
+ // function. for any key leading up to the final one it should
+ // increase the sequence. after the final, it should reset all sequences
+ //
+ // if an action is specified in the original bind call then that will
+ // be used throughout. otherwise we will pass the action that the
+ // next key in the sequence should match. this allows a sequence
+ // to mix and match keypress and keydown events depending on which
+ // ones are better suited to the key provided
+ for (var i = 0; i < keys.length; ++i) {
+ var isFinal = i + 1 === keys.length;
+ var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
+ _bindSingle(keys[i], wrappedCallback, action, combo, i);
+ }
+ }
/**
- * triggers an event that has already been bound
+ * binds a single keyboard combination
*
- * @param {string} keys
+ * @param {string} combination
+ * @param {Function} callback
* @param {string=} action
+ * @param {string=} sequenceName - name of sequence if part of sequence
+ * @param {number=} level - what part of the sequence the command is
* @returns void
*/
- trigger: function(keys, action) {
- if (_directMap[keys + ':' + action]) {
- _directMap[keys + ':' + action]();
+ function _bindSingle(combination, callback, action, sequenceName, level) {
+
+ // store a direct mapped reference for use with Mousetrap.trigger
+ self._directMap[combination + ':' + action] = callback;
+
+ // make sure multiple spaces in a row become a single space
+ combination = combination.replace(/\s+/g, ' ');
+
+ var sequence = combination.split(' ');
+ var info;
+
+ // if this pattern is a sequence of keys then run through this method
+ // to reprocess each pattern one key at a time
+ if (sequence.length > 1) {
+ _bindSequence(combination, sequence, callback, action);
+ return;
}
- return this;
- },
+
+ info = _getKeyInfo(combination, action);
+
+ // make sure to initialize array if this is the first time
+ // a callback is added for this key
+ self._callbacks[info.key] = self._callbacks[info.key] || [];
+
+ // remove an existing match if there is one
+ _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
+
+ // add this call back to the array
+ // if it is a sequence put it at the beginning
+ // if not put it at the end
+ //
+ // this is important because the way these are processed expects
+ // the sequence ones to come first
+ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
+ callback: callback,
+ modifiers: info.modifiers,
+ action: info.action,
+ seq: sequenceName,
+ level: level,
+ combo: combination
+ });
+ }
/**
- * resets the library back to its initial state. this is useful
- * if you want to clear out the current keyboard shortcuts and bind
- * new ones - for example if you switch to another page
+ * binds multiple combinations to the same callback
*
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
* @returns void
*/
- reset: function() {
- _callbacks = {};
- _directMap = {};
- return this;
- },
-
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @param {Element} element
- * @return {boolean}
- */
- stopCallback: function(e, element, combo) {
-
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
+ self._bindMultiple = function(combinations, callback, action) {
+ for (var i = 0; i < combinations.length; ++i) {
+ _bindSingle(combinations[i], callback, action);
+ }
+ };
+
+ // start!
+ _addEvent(targetElement, 'keypress', _handleKeyEvent);
+ _addEvent(targetElement, 'keydown', _handleKeyEvent);
+ _addEvent(targetElement, 'keyup', _handleKeyEvent);
+ }
+
+ /**
+ * binds an event to mousetrap
+ *
+ * can be a single key, a combination of keys separated with +,
+ * an array of keys, or a sequence of keys separated by spaces
+ *
+ * be sure to list the modifier keys first to make sure that the
+ * correct key ends up getting bound (the last key in the pattern)
+ *
+ * @param {string|Array} keys
+ * @param {Function} callback
+ * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @returns void
+ */
+ Mousetrap.prototype.bind = function(keys, callback, action) {
+ var self = this;
+ keys = keys instanceof Array ? keys : [keys];
+ self._bindMultiple.call(self, keys, callback, action);
+ return self;
+ };
+
+ /**
+ * unbinds an event to mousetrap
+ *
+ * the unbinding sets the callback function of the specified key combo
+ * to an empty function and deletes the corresponding key in the
+ * _directMap dict.
+ *
+ * TODO: actually remove this from the _callbacks dictionary instead
+ * of binding an empty function
+ *
+ * the keycombo+action has to be exactly the same as
+ * it was defined in the bind method
+ *
+ * @param {string|Array} keys
+ * @param {string} action
+ * @returns void
+ */
+ Mousetrap.prototype.unbind = function(keys, action) {
+ var self = this;
+ return self.bind.call(self, keys, function() {}, action);
+ };
+
+ /**
+ * triggers an event that has already been bound
+ *
+ * @param {string} keys
+ * @param {string=} action
+ * @returns void
+ */
+ Mousetrap.prototype.trigger = function(keys, action) {
+ var self = this;
+ if (self._directMap[keys + ':' + action]) {
+ self._directMap[keys + ':' + action]({}, keys);
+ }
+ return self;
+ };
+
+ /**
+ * resets the library back to its initial state. this is useful
+ * if you want to clear out the current keyboard shortcuts and bind
+ * new ones - for example if you switch to another page
+ *
+ * @returns void
+ */
+ Mousetrap.prototype.reset = function() {
+ var self = this;
+ self._callbacks = {};
+ self._directMap = {};
+ return self;
+ };
+
+ /**
+ * should we stop this event before firing off callbacks
+ *
+ * @param {Event} e
+ * @param {Element} element
+ * @return {boolean}
+ */
+ Mousetrap.prototype.stopCallback = function(e, element) {
+ var self = this;
+
+ // if the element has the class "mousetrap" then no need to stop
+ if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+ return false;
+ }
+
+ if (_belongsTo(element, self.target)) {
+ return false;
+ }
+
+ // Events originating from a shadow DOM are re-targetted and `e.target` is the shadow host,
+ // not the initial event target in the shadow tree. Note that not all events cross the
+ // shadow boundary.
+ // For shadow trees with `mode: 'open'`, the initial event target is the first element in
+ // the event’s composed path. For shadow trees with `mode: 'closed'`, the initial event
+ // target cannot be obtained.
+ if ('composedPath' in e && typeof e.composedPath === 'function') {
+ // For open shadow trees, update `element` so that the following check works.
+ var initialEventTarget = e.composedPath()[0];
+ if (initialEventTarget !== e.target) {
+ element = initialEventTarget;
+ }
+ }
+
+ // stop for input, select, and textarea
+ return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
+ };
+
+ /**
+ * exposes _handleKey publicly so it can be overwritten by extensions
+ */
+ Mousetrap.prototype.handleKey = function() {
+ var self = this;
+ return self._handleKey.apply(self, arguments);
+ };
+
+ /**
+ * allow custom key mappings
+ */
+ Mousetrap.addKeycodes = function(object) {
+ for (var key in object) {
+ if (object.hasOwnProperty(key)) {
+ _MAP[key] = object[key];
}
+ }
+ _REVERSE_MAP = null;
+ };
- // stop for input, select, and textarea
- return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true');
+ /**
+ * Init the global mousetrap functions
+ *
+ * This method is needed to allow the global mousetrap functions to work
+ * now that mousetrap is a constructor function.
+ */
+ Mousetrap.init = function() {
+ var documentMousetrap = Mousetrap(document);
+ for (var method in documentMousetrap) {
+ if (method.charAt(0) !== '_') {
+ Mousetrap[method] = (function(method) {
+ return function() {
+ return documentMousetrap[method].apply(documentMousetrap, arguments);
+ };
+ } (method));
+ }
}
};
+ Mousetrap.init();
+
// expose mousetrap to the global object
window.Mousetrap = Mousetrap;
+ // expose as a common js module
+ if (typeof module !== 'undefined' && module.exports) {
+ module.exports = Mousetrap;
+ }
+
// expose mousetrap as an AMD module
if (typeof define === 'function' && define.amd) {
- define(Mousetrap);
+ define(function() {
+ return Mousetrap;
+ });
}
-}) ();
+}) (typeof window !== 'undefined' ? window : null, typeof window !== 'undefined' ? document : null);