From: Zachary Vance Date: Tue, 19 May 2015 22:46:49 +0000 (-0700) Subject: Make global shortcuts work X-Git-Url: https://git.za3k.com/?a=commitdiff_plain;h=aa5cd0557bbe28d5fc42934a9e4f9ca269cadf84;p=flowy.git Make global shortcuts work --- diff --git a/dist/_bower.js b/dist/_bower.js index 718b2aa..76b70b8 100644 --- a/dist/_bower.js +++ b/dist/_bower.js @@ -1,5 +1,5 @@ /*! - * jQuery JavaScript Library v2.1.3 + * jQuery JavaScript Library v2.1.4 * http://jquery.com/ * * Includes Sizzle.js @@ -9,7 +9,7 @@ * Released under the MIT license * http://jquery.org/license * - * Date: 2014-12-18T15:11Z + * Date: 2015-04-28T16:01Z */ (function( global, factory ) { @@ -67,7 +67,7 @@ var // Use the correct document accordingly with window argument (sandbox) document = window.document, - version = "2.1.3", + version = "2.1.4", // Define a local copy of jQuery jQuery = function( selector, context ) { @@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli }); function isArraylike( obj ) { - var length = obj.length, + + // Support: iOS 8.2 (not reproducible in simulator) + // `in` check used to prevent JIT error (gh-2145) + // hasOwn isn't used here due to false negatives + // regarding Nodelist length in IE + var length = "length" in obj && obj.length, type = jQuery.type( obj ); if ( type === "function" || jQuery.isWindow( obj ) ) { @@ -27486,6 +27491,1028 @@ if ((typeof define !== "undefined" && define !== null) && (define.amd != null)) }).call(this); +/*global define:false */ +/** + * Copyright 2015 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * Mousetrap is a simple keyboard shortcut library for Javascript with + * no external dependencies + * + * @version 1.5.2 + * @url craig.is/killing/mice + */ +(function(window, document, undefined) { + + /** + * mapping of special keycodes to their corresponding keys + * + * everything in this dictionary cannot use keypress events + * so it has to be here to map to the correct keycodes for + * keyup/keydown events + * + * @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} + */ + var _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} + */ + var _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} + */ + var _SPECIAL_ALIASES = { + 'option': 'alt', + 'command': 'meta', + 'return': 'enter', + 'escape': 'esc', + 'plus': '+', + '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 + * is specified + * + * @type {Object|undefined} + */ + var _REVERSE_MAP; + + /** + * loop through the f keys, f1 to f19 and add them to the map + * programatically + */ + for (var i = 1; i < 20; ++i) { + _MAP[111 + i] = 'f' + i; + } + + /** + * loop through to map numbers on the numeric keypad + */ + for (i = 0; i <= 9; ++i) { + _MAP[i + 96] = i; + } + + /** + * cross browser add event method + * + * @param {Element|HTMLDocument} object + * @param {string} type + * @param {Function} callback + * @returns void + */ + function _addEvent(object, type, callback) { + if (object.addEventListener) { + object.addEventListener(type, callback, false); + return; + } + + object.attachEvent('on' + type, callback); + } + + /** + * takes the event and returns the key character + * + * @param {Event} e + * @return {string} + */ + function _characterFromEvent(e) { + + // for keypress events we should return the character as is + if (e.type == 'keypress') { + 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]; + } + + 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 + * + * @param {Array} modifiers1 + * @param {Array} modifiers2 + * @returns {boolean} + */ + function _modifiersMatch(modifiers1, modifiers2) { + return modifiers1.sort().join(',') === modifiers2.sort().join(','); + } + + /** + * takes a key event and figures out what the modifiers are + * + * @param {Event} e + * @returns {Array} + */ + function _eventModifiers(e) { + var modifiers = []; + + if (e.shiftKey) { + modifiers.push('shift'); + } + + if (e.altKey) { + modifiers.push('alt'); + } + + if (e.ctrlKey) { + modifiers.push('ctrl'); + } + + if (e.metaKey) { + modifiers.push('meta'); + } + + return modifiers; + } + + /** + * prevents default for this event + * + * @param {Event} e + * @returns void + */ + function _preventDefault(e) { + if (e.preventDefault) { + e.preventDefault(); + return; + } + + e.returnValue = false; + } + + /** + * stops propogation for this event + * + * @param {Event} e + * @returns void + */ + function _stopPropagation(e) { + if (e.stopPropagation) { + e.stopPropagation(); + return; + } + + e.cancelBubble = true; + } + + /** + * determines if the keycode specified is a modifier key or not + * + * @param {string} key + * @returns {boolean} + */ + function _isModifier(key) { + return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta'; + } + + /** + * reverses the map lookup so that we can look for specific keys + * to see what can and can't use keypress + * + * @return {Object} + */ + function _getReverseMap() { + if (!_REVERSE_MAP) { + _REVERSE_MAP = {}; + for (var key in _MAP) { + + // pull out the numeric keypad from here cause keypress should + // be able to detect the keys from the character + if (key > 95 && key < 112) { + continue; + } + + if (_MAP.hasOwnProperty(key)) { + _REVERSE_MAP[_MAP[key]] = key; + } + } + } + return _REVERSE_MAP; + } + + /** + * picks the best action based on the key combination + * + * @param {string} key - character for key + * @param {Array} modifiers + * @param {string=} action passed in + */ + function _pickBestAction(key, modifiers, action) { + + // if no action was picked in we should try to pick the one + // that we think would work best for this key + if (!action) { + action = _getReverseMap()[key] ? 'keydown' : 'keypress'; + } + + // modifier keys don't work as expected with keypress, + // switch to keydown + if (action == 'keypress' && modifiers.length) { + action = 'keydown'; + } + + return action; + } + + /** + * Converts from a string key combination to an array + * + * @param {string} combination like "command+shift+l" + * @return {Array} + */ + function _keysFromString(combination) { + if (combination === '+') { + return ['+']; + } + + combination = combination.replace(/\+{2}/g, '+plus'); + return combination.split('+'); + } + + /** + * 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; + var key; + var i; + var modifiers = []; + + // take the keys from this pattern and figure out what the actual + // pattern is all about + keys = _keysFromString(combination); + + for (i = 0; i < keys.length; ++i) { + key = keys[i]; + + // normalize key names + if (_SPECIAL_ALIASES[key]) { + key = _SPECIAL_ALIASES[key]; + } + + // if this is not a keypress event then we should + // be smart about using shift keys + // this will only work for US keyboards however + if (action && action != 'keypress' && _SHIFT_MAP[key]) { + key = _SHIFT_MAP[key]; + modifiers.push('shift'); + } + + // if this key is a modifier then add it to the list of modifiers + if (_isModifier(key)) { + modifiers.push(key); + } + } + + // 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 + }; + } + + function _belongsTo(element, ancestor) { + if (element === document) { + return false; + } + + if (element === ancestor) { + return true; + } + + return _belongsTo(element.parentNode, ancestor); + } + + function Mousetrap(targetElement) { + var self = this; + + targetElement = targetElement || document; + + if (!(self instanceof Mousetrap)) { + return new Mousetrap(targetElement); + } + + /** + * element to attach key events to + * + * @type {Element} + */ + self.target = targetElement; + + /** + * a list of all the callbacks setup via Mousetrap.bind() + * + * @type {Object} + */ + self._callbacks = {}; + + /** + * direct map of string combinations to callbacks used for trigger() + * + * @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 + * + * 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, 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); + } + } + + /** + * handles a character key event + * + * @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 + * + * 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); + } + + /** + * binds a key sequence to an event + * + * @param {string} combo - combo specified in bind call + * @param {Array} keys + * @param {Function} callback + * @param {string=} action + * @returns void + */ + 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); + } + } + + /** + * 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 + 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; + } + + 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 + }); + } + + /** + * binds multiple combinations to the same callback + * + * @param {Array} combinations + * @param {Function} callback + * @param {string|undefined} action + * @returns void + */ + 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; + } + + // 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); + }; + + /** + * 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(function() { + return Mousetrap; + }); + } +}) (window, document); + /*! * mustache.js - Logic-less {{mustache}} templates with JavaScript * http://github.com/janl/mustache.js diff --git a/dist/flowy.js b/dist/flowy.js index 3d81e66..83ad047 100644 --- a/dist/flowy.js +++ b/dist/flowy.js @@ -377,10 +377,172 @@ var FlowyDocModel = Backbone.Collection.extend({ comparator: 'id', }); +// TODO: None of this code has ever been run and doesn't work +// TODO: More documentation + +// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); }); +// firstShortcut.rebind("Control + m"); +// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }}); +// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding +// o.remove() // Once we lose textbox focus, stop keeping track of it, for example + +var Shortcut = (function(document, _) { + var globalMousetrap = new Mousetrap(); + var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; + var Shortcut = { + globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked. + globalMousetrap: globalMousetrap, + boundObjects: [globalObject], + shortcuts: [], + registerShortcut: function(shortcut) { + if (arguments.length > 1) { + shortcut = { + keybinding: arguments[0], + action: arguments[1], + object: arguments[2], + }; + } + var manager = this; + if (!shortcut.ownedByUs) shortcut = _.clone(shortcut); + _.defaults(shortcut, { + register: function() { manager.registerShortcut(this); }, + unregister: function() { manager.unregisterShortcut(this); }, + bind: function(object) { manager.bindShortcut(this, object); }, + unbind: function(object) { manager.unbindShortcut(this, object); }, + rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); }, + object: "global", + keybinding: "", + action: function() {}, + boundObjects: [], + ownedByUs: true + }); + this.shortcuts.push(shortcut); + _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value(); + return shortcut; + }, + unregisterShortcut: function(shortcut) { + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method + }, + bindShortcut: function(shortcut, object) { // Do not call directly + object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object)); + shortcut.boundObjects.push(object); + }, + unbindShortcut: function(shortcut, object) { // Do not call directly with an object + if (!object){ + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + } + object.mousetrap.unbind(shortcut.keybinding); + shortcut.boundObjects = _.without(shortcut.boundObjects, object); + }, + rebindShortcut: function(shortcut, keybinding) { + // TODO: figure out a way to do this with 'shorcut.keybinding=' binding + if (keybinding === shortcut.keybinding) return; + var oldKeybinding = shortcut.keybinding; + var objects = shortcut.boundObjects; + shortcut.unbind(); + shorcut.keybinding = keybinding; + _.each(objects, shortcut.bind, shortcut); + if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); + }, + userRebindShortcut: function(shortcut, options) { + // Cancels on focus loss or if the user presses the escape key + options = _.defaults({}, options, { + element: undefined, // If the user clicks outside this element, cancel + onCancel: function() {}, + }); + var manager = this; + var cancelled = false; + var cancel = function() { + if (!cancelled) { + cancelled = true; + if (options.onCancel) options.onCancel(); + } + }; + if (element) element.addEventListener("blur", cancel); + Mousetrap.record(function (keybinding) { // TODO: requires plugin + if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + cancel(); + } else { + manager.rebindShortcut(shortcut, keybinding); + if (element) element.removeEventListener("blur", cancel); + } + }); + }, + shortcutPressed: function(shortcut, object) { + if (shortcut.action) shortcut.action(object, object, shortcut); + }, + onRebindShortcut: function(shortcut, keybinding, oldKeybinding) { + // NOTE: You may want to hook into this if you want to save/load user preferences + // shortcut.id is probably what you want and not the whole shortcut + }, + addObject: function(element, type) { + var manager = this; + object = { + element:element, + type:type, + mousetrap: new Mousetrap(element), + remove: function() { manager.removeObject(this); } + }; + _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return object; + }, + removeObject: function(object) { + _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this); + this.boundObjects = _.without(this.boundObjects, object); + }, + changeGlobalObject: function(newObject) { + // TODO: Figure out how to do this with 'this.globalObject=' binding + this.removeObject(this.globalObject, "global"); + this.globalObject = { + element: element, + type: type, + isGlobal: true, + mousetrap: this.globalMousetrap, + }; + _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return this.globalObject; + }, + pause: function() { // TODO: These require plugin, test + this.boundObjects.each(function(object) { + object.mousetrap.pause(); + }); + }, + unpause: function() { + this.boundObjects.each(function(object) { + object.mousetrap.unpause(); + }); + }, + displayShortcuts: function(shortcuts, options) { + options = _.defaults({}, options, { + objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default" + displayMultiple: true, // Display multiple shortcuts or just the canonical one? + allowRebind: false, // Insert javascript to allow rebinding shortcuts + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) + }); + if (!shortcuts) shortcuts = this.shortcuts; + if (options.objectGrouping === "default") { + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + options.objectGrouping = multipleObjectTypes; + } + // TODO: Return HTML + return ""; + }, + _displayKeybinding: function(keybinding, options) { + options = _.defaults({}, options, { displayMultiple: true }); + // TODO: Return HTML + return ""; + }, + }; + return Shortcut; +})(document, _); + /** * @depend ../views/todo.js * @depend ../models/flowyDoc.js * @depend ../models/todo.js + * @depend ../library/shortcut.js */ var testTodos = [ @@ -502,6 +664,11 @@ var AppView = Backbone.View.extend({ }, }); +// DEBUGGING +window.Backbone = Backbone; +window.Shortcut = Shortcut; +window.AppView = AppView; + /** * @depend views/app.js */ diff --git a/dist/flowy.unwrapped.js b/dist/flowy.unwrapped.js index 1d2033a..1c19230 100644 --- a/dist/flowy.unwrapped.js +++ b/dist/flowy.unwrapped.js @@ -376,10 +376,172 @@ var FlowyDocModel = Backbone.Collection.extend({ comparator: 'id', }); +// TODO: None of this code has ever been run and doesn't work +// TODO: More documentation + +// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); }); +// firstShortcut.rebind("Control + m"); +// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }}); +// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding +// o.remove() // Once we lose textbox focus, stop keeping track of it, for example + +var Shortcut = (function(document, _) { + var globalMousetrap = new Mousetrap(); + var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; + var Shortcut = { + globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked. + globalMousetrap: globalMousetrap, + boundObjects: [globalObject], + shortcuts: [], + registerShortcut: function(shortcut) { + if (arguments.length > 1) { + shortcut = { + keybinding: arguments[0], + action: arguments[1], + object: arguments[2], + }; + } + var manager = this; + if (!shortcut.ownedByUs) shortcut = _.clone(shortcut); + _.defaults(shortcut, { + register: function() { manager.registerShortcut(this); }, + unregister: function() { manager.unregisterShortcut(this); }, + bind: function(object) { manager.bindShortcut(this, object); }, + unbind: function(object) { manager.unbindShortcut(this, object); }, + rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); }, + object: "global", + keybinding: "", + action: function() {}, + boundObjects: [], + ownedByUs: true + }); + this.shortcuts.push(shortcut); + _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value(); + return shortcut; + }, + unregisterShortcut: function(shortcut) { + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method + }, + bindShortcut: function(shortcut, object) { // Do not call directly + object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object)); + shortcut.boundObjects.push(object); + }, + unbindShortcut: function(shortcut, object) { // Do not call directly with an object + if (!object){ + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + } + object.mousetrap.unbind(shortcut.keybinding); + shortcut.boundObjects = _.without(shortcut.boundObjects, object); + }, + rebindShortcut: function(shortcut, keybinding) { + // TODO: figure out a way to do this with 'shorcut.keybinding=' binding + if (keybinding === shortcut.keybinding) return; + var oldKeybinding = shortcut.keybinding; + var objects = shortcut.boundObjects; + shortcut.unbind(); + shorcut.keybinding = keybinding; + _.each(objects, shortcut.bind, shortcut); + if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); + }, + userRebindShortcut: function(shortcut, options) { + // Cancels on focus loss or if the user presses the escape key + options = _.defaults({}, options, { + element: undefined, // If the user clicks outside this element, cancel + onCancel: function() {}, + }); + var manager = this; + var cancelled = false; + var cancel = function() { + if (!cancelled) { + cancelled = true; + if (options.onCancel) options.onCancel(); + } + }; + if (element) element.addEventListener("blur", cancel); + Mousetrap.record(function (keybinding) { // TODO: requires plugin + if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + cancel(); + } else { + manager.rebindShortcut(shortcut, keybinding); + if (element) element.removeEventListener("blur", cancel); + } + }); + }, + shortcutPressed: function(shortcut, object) { + if (shortcut.action) shortcut.action(object, object, shortcut); + }, + onRebindShortcut: function(shortcut, keybinding, oldKeybinding) { + // NOTE: You may want to hook into this if you want to save/load user preferences + // shortcut.id is probably what you want and not the whole shortcut + }, + addObject: function(element, type) { + var manager = this; + object = { + element:element, + type:type, + mousetrap: new Mousetrap(element), + remove: function() { manager.removeObject(this); } + }; + _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return object; + }, + removeObject: function(object) { + _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this); + this.boundObjects = _.without(this.boundObjects, object); + }, + changeGlobalObject: function(newObject) { + // TODO: Figure out how to do this with 'this.globalObject=' binding + this.removeObject(this.globalObject, "global"); + this.globalObject = { + element: element, + type: type, + isGlobal: true, + mousetrap: this.globalMousetrap, + }; + _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return this.globalObject; + }, + pause: function() { // TODO: These require plugin, test + this.boundObjects.each(function(object) { + object.mousetrap.pause(); + }); + }, + unpause: function() { + this.boundObjects.each(function(object) { + object.mousetrap.unpause(); + }); + }, + displayShortcuts: function(shortcuts, options) { + options = _.defaults({}, options, { + objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default" + displayMultiple: true, // Display multiple shortcuts or just the canonical one? + allowRebind: false, // Insert javascript to allow rebinding shortcuts + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) + }); + if (!shortcuts) shortcuts = this.shortcuts; + if (options.objectGrouping === "default") { + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + options.objectGrouping = multipleObjectTypes; + } + // TODO: Return HTML + return ""; + }, + _displayKeybinding: function(keybinding, options) { + options = _.defaults({}, options, { displayMultiple: true }); + // TODO: Return HTML + return ""; + }, + }; + return Shortcut; +})(document, _); + /** * @depend ../views/todo.js * @depend ../models/flowyDoc.js * @depend ../models/todo.js + * @depend ../library/shortcut.js */ var testTodos = [ @@ -501,6 +663,11 @@ var AppView = Backbone.View.extend({ }, }); +// DEBUGGING +window.Backbone = Backbone; +window.Shortcut = Shortcut; +window.AppView = AppView; + /** * @depend views/app.js */ diff --git a/src/library/shortcut.js b/src/library/shortcut.js new file mode 100644 index 0000000..6bd0860 --- /dev/null +++ b/src/library/shortcut.js @@ -0,0 +1,160 @@ +// TODO: None of this code has ever been run and doesn't work +// TODO: More documentation + +// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); }); +// firstShortcut.rebind("Control + m"); +// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }}); +// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding +// o.remove() // Once we lose textbox focus, stop keeping track of it, for example + +var Shortcut = (function(document, _) { + var globalMousetrap = new Mousetrap(); + var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; + var Shortcut = { + globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked. + globalMousetrap: globalMousetrap, + boundObjects: [globalObject], + shortcuts: [], + registerShortcut: function(shortcut) { + if (arguments.length > 1) { + shortcut = { + keybinding: arguments[0], + action: arguments[1], + object: arguments[2], + }; + } + var manager = this; + if (!shortcut.ownedByUs) shortcut = _.clone(shortcut); + _.defaults(shortcut, { + register: function() { manager.registerShortcut(this); }, + unregister: function() { manager.unregisterShortcut(this); }, + bind: function(object) { manager.bindShortcut(this, object); }, + unbind: function(object) { manager.unbindShortcut(this, object); }, + rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); }, + object: "global", + keybinding: "", + action: function() {}, + boundObjects: [], + ownedByUs: true + }); + this.shortcuts.push(shortcut); + _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value(); + return shortcut; + }, + unregisterShortcut: function(shortcut) { + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method + }, + bindShortcut: function(shortcut, object) { // Do not call directly + object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object)); + shortcut.boundObjects.push(object); + }, + unbindShortcut: function(shortcut, object) { // Do not call directly with an object + if (!object){ + _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + } + object.mousetrap.unbind(shortcut.keybinding); + shortcut.boundObjects = _.without(shortcut.boundObjects, object); + }, + rebindShortcut: function(shortcut, keybinding) { + // TODO: figure out a way to do this with 'shorcut.keybinding=' binding + if (keybinding === shortcut.keybinding) return; + var oldKeybinding = shortcut.keybinding; + var objects = shortcut.boundObjects; + shortcut.unbind(); + shorcut.keybinding = keybinding; + _.each(objects, shortcut.bind, shortcut); + if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); + }, + userRebindShortcut: function(shortcut, options) { + // Cancels on focus loss or if the user presses the escape key + options = _.defaults({}, options, { + element: undefined, // If the user clicks outside this element, cancel + onCancel: function() {}, + }); + var manager = this; + var cancelled = false; + var cancel = function() { + if (!cancelled) { + cancelled = true; + if (options.onCancel) options.onCancel(); + } + }; + if (element) element.addEventListener("blur", cancel); + Mousetrap.record(function (keybinding) { // TODO: requires plugin + if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + cancel(); + } else { + manager.rebindShortcut(shortcut, keybinding); + if (element) element.removeEventListener("blur", cancel); + } + }); + }, + shortcutPressed: function(shortcut, object) { + if (shortcut.action) shortcut.action(object, object, shortcut); + }, + onRebindShortcut: function(shortcut, keybinding, oldKeybinding) { + // NOTE: You may want to hook into this if you want to save/load user preferences + // shortcut.id is probably what you want and not the whole shortcut + }, + addObject: function(element, type) { + var manager = this; + object = { + element:element, + type:type, + mousetrap: new Mousetrap(element), + remove: function() { manager.removeObject(this); } + }; + _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return object; + }, + removeObject: function(object) { + _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this); + this.boundObjects = _.without(this.boundObjects, object); + }, + changeGlobalObject: function(newObject) { + // TODO: Figure out how to do this with 'this.globalObject=' binding + this.removeObject(this.globalObject, "global"); + this.globalObject = { + element: element, + type: type, + isGlobal: true, + mousetrap: this.globalMousetrap, + }; + _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value(); + if (!_.contains(this.boundObjects)) this.boundObjects.push(object); + return this.globalObject; + }, + pause: function() { // TODO: These require plugin, test + this.boundObjects.each(function(object) { + object.mousetrap.pause(); + }); + }, + unpause: function() { + this.boundObjects.each(function(object) { + object.mousetrap.unpause(); + }); + }, + displayShortcuts: function(shortcuts, options) { + options = _.defaults({}, options, { + objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default" + displayMultiple: true, // Display multiple shortcuts or just the canonical one? + allowRebind: false, // Insert javascript to allow rebinding shortcuts + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) + }); + if (!shortcuts) shortcuts = this.shortcuts; + if (options.objectGrouping === "default") { + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + options.objectGrouping = multipleObjectTypes; + } + // TODO: Return HTML + return ""; + }, + _displayKeybinding: function(keybinding, options) { + options = _.defaults({}, options, { displayMultiple: true }); + // TODO: Return HTML + return ""; + }, + }; + return Shortcut; +})(document, _); diff --git a/src/views/app.js b/src/views/app.js index d8d900b..fba6451 100644 --- a/src/views/app.js +++ b/src/views/app.js @@ -2,6 +2,7 @@ * @depend ../views/todo.js * @depend ../models/flowyDoc.js * @depend ../models/todo.js + * @depend ../library/shortcut.js */ var testTodos = [ @@ -122,3 +123,8 @@ var AppView = Backbone.View.extend({ this.list.each(this.addOne, this); }, }); + +// DEBUGGING +window.Backbone = Backbone; +window.Shortcut = Shortcut; +window.AppView = AppView;