vendor/assets/javascripts/mousetrap.js in mousetrap-rails-0.0.9 vs vendor/assets/javascripts/mousetrap.js in mousetrap-rails-0.0.10

- old
+ new

@@ -1,7 +1,8 @@ +/*global define:false */ /** - * Copyright 2012 Craig Campbell + * Copyright 2013 Craig Campbell * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * @@ -14,11 +15,11 @@ * limitations under the License. * * Mousetrap is a simple keyboard shortcut library for Javascript with * no external dependencies * - * @version 1.3.0 + * @version 1.4.1 * @url craig.is/killing/mice */ (function() { /** @@ -122,11 +123,12 @@ */ _SPECIAL_ALIASES = { 'option': 'alt', 'command': 'meta', 'return': 'enter', - 'escape': 'esc' + 'escape': 'esc', + 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' }, /** * variable to store the flipped version of _MAP from above * needed to check if we should use keypress or not when no action @@ -176,11 +178,11 @@ * are we currently inside of a sequence? * type of action ("keyup" or "keydown" or "keypress") or false * * @type {boolean|string} */ - _sequenceType = false; + _nextExpectedAction = false; /** * loop through the f keys, f1 to f19 and add them to the map * programatically */ @@ -220,11 +222,26 @@ */ function _characterFromEvent(e) { // 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 if (_MAP[e.which]) { return _MAP[e.which]; @@ -233,10 +250,14 @@ if (_KEYCODE_MAP[e.which]) { return _KEYCODE_MAP[e.which]; } // 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(); } /** * checks if two arrays are equal @@ -253,41 +274,42 @@ * resets all sequence counters except for the ones passed in * * @param {Object} doNotReset * @returns void */ - function _resetSequences(doNotReset, maxLevel) { + function _resetSequences(doNotReset) { doNotReset = doNotReset || {}; var activeSequences = false, key; for (key in _sequenceLevels) { - if (doNotReset[key] && _sequenceLevels[key] > maxLevel) { + if (doNotReset[key]) { activeSequences = true; continue; } _sequenceLevels[key] = 0; } if (!activeSequences) { - _sequenceType = false; + _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 {boolean=} remove - should we remove any matches + * @param {string=} sequenceName - name of the sequence we are looking for * @param {string=} combination + * @param {number=} level * @returns {Array} */ - function _getMatches(character, modifiers, e, remove, combination) { + function _getMatches(character, modifiers, e, sequenceName, combination, level) { var i, callback, matches = [], action = e.type; @@ -304,13 +326,13 @@ // 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) { + // 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 @@ -325,13 +347,18 @@ // 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) { + // 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) { _callbacks[character].splice(i, 1); } matches.push(callback); } @@ -401,63 +428,88 @@ /** * handles a character key event * * @param {string} character + * @param {Array} modifiers * @param {Event} e * @returns void */ - function _handleCharacter(character, e) { - var callbacks = _getMatches(character, _eventModifiers(e), e), + function _handleKey(character, modifiers, e) { + var callbacks = _getMatches(character, modifiers, e), i, doNotReset = {}, maxLevel = 0, 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) { - 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); + // 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); 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) { + if (!processedSequenceCallback) { _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); + // 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 + if (e.type == _nextExpectedAction && !_isModifier(character)) { + _resetSequences(doNotReset); } } /** * handles a keydown event * * @param {Event} e * @returns void */ - function _handleKey(e) { + 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; @@ -473,11 +525,11 @@ if (e.type == 'keyup' && _ignoreNextKeyup == character) { _ignoreNextKeyup = false; return; } - _handleCharacter(character, e); + Mousetrap.handleKey(character, _eventModifiers(e), e); } /** * determines if the keycode specified is a modifier key or not * @@ -563,94 +615,93 @@ // 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], []); - } - /** * callback to increase the sequence level for this sequence and reset * all other sequences that were active * - * @param {Event} e - * @returns void + * @param {string} nextAction + * @returns {Function} */ - var _increaseSequence = function(e) { - _sequenceType = action; + 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 - */ - _callbackAndReset = function(e) { - _fireCallback(callback, e, combo); + /** + * 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); - } + // 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; + // 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 - for (i = 0; i < keys.length; ++i) { - _bindSingle(keys[i], i < keys.length - 1 ? _increaseSequence : _callbackAndReset, action, combo, i); + // + // 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); } } /** - * binds a single keyboard combination + * Converts from a string key combination to an array * - * @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 like "command+shift+l" + * @return {Array} */ - function _bindSingle(combination, callback, action, sequenceName, level) { + function _keysFromString(combination) { + if (combination === '+') { + return ['+']; + } - // store a direct mapped reference for use with Mousetrap.trigger - _directMap[combination + ':' + action] = callback; + return combination.split('+'); + } - // make sure multiple spaces in a row become a single space - combination = combination.replace(/\s+/g, ' '); - - var sequence = combination.split(' '), - i, + /** + * Gets info for a specific key combination + * + * @param {string} combination key combination ("command+s" or "a" or "*") + * @param {string=} action + * @returns {Object} + */ + function _getKeyInfo(combination, action) { + var keys, key, - keys, + i, 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; - } - // 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]; // normalize key names @@ -674,29 +725,64 @@ // depending on what the key combination is // we will try to pick the best event for it action = _pickBestAction(key, modifiers, action); + return { + key: key, + modifiers: modifiers, + action: action + }; + } + + /** + * binds a single keyboard 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 + */ + 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(' '), + 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; + } + + info = _getKeyInfo(combination, action); + // make sure to initialize array if this is the first time // a callback is added for this key - if (!_callbacks[key]) { - _callbacks[key] = []; - } + _callbacks[info.key] = _callbacks[info.key] || []; // remove an existing match if there is one - _getMatches(key, modifiers, {type: action}, !sequenceName, combination); + _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 - _callbacks[key][sequenceName ? 'unshift' : 'push']({ + _callbacks[info.key][sequenceName ? 'unshift' : 'push']({ callback: callback, - modifiers: modifiers, - action: action, + modifiers: info.modifiers, + action: info.action, seq: sequenceName, level: level, combo: combination }); } @@ -714,13 +800,13 @@ _bindSingle(combinations[i], callback, action); } } // start! - _addEvent(document, 'keypress', _handleKey); - _addEvent(document, 'keydown', _handleKey); - _addEvent(document, 'keyup', _handleKey); + _addEvent(document, 'keypress', _handleKeyEvent); + _addEvent(document, 'keydown', _handleKeyEvent); + _addEvent(document, 'keyup', _handleKeyEvent); var Mousetrap = { /** * binds an event to mousetrap @@ -770,11 +856,11 @@ * @param {string=} action * @returns void */ trigger: function(keys, action) { if (_directMap[keys + ':' + action]) { - _directMap[keys + ':' + action](); + _directMap[keys + ':' + action]({}, keys); } return this; }, /** @@ -795,19 +881,24 @@ * * @param {Event} e * @param {Element} element * @return {boolean} */ - stopCallback: function(e, element, combo) { + stopCallback: function(e, element) { // if the element has the class "mousetrap" then no need to stop if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { return false; } // stop for input, select, and textarea return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || (element.contentEditable && element.contentEditable == 'true'); - } + }, + + /** + * exposes _handleKey publicly so it can be overwritten by extensions + */ + handleKey: _handleKey }; // expose mousetrap to the global object window.Mousetrap = Mousetrap;