From 20f65be651ac259d151eca016b2fa2e0faedc349 Mon Sep 17 00:00:00 2001 From: Zachary Vance Date: Wed, 20 May 2015 22:08:12 -0700 Subject: [PATCH] Support rebinding shortcuts --- Gruntfile.js | 3 + dist/_bower.js | 1037 +-------------------------------------- dist/flowy.css | 9 + dist/flowy.js | 93 +++- dist/flowy.unwrapped.js | 93 +++- dist/index.html | 3 + src/css/flowy.css | 9 + src/index.html | 3 + src/library/shortcut.js | 87 +++- src/views/app.js | 8 + 10 files changed, 272 insertions(+), 1073 deletions(-) diff --git a/Gruntfile.js b/Gruntfile.js index 9de876f..2b68fe6 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -22,6 +22,9 @@ module.exports = function(grunt) { cssDest: 'dist/_bower.css', dependencies: { 'backbone': ['jquery', 'lodash'] + }, + mainFiles: { + mousetrap: ['mousetrap.min.js', 'plugins/pause/mousetrap-pause.min.js', 'plugins/record/mousetrap-record.min.js'] } } }, diff --git a/dist/_bower.js b/dist/_bower.js index 76b70b8..aaddccc 100644 --- a/dist/_bower.js +++ b/dist/_bower.js @@ -27491,1027 +27491,22 @@ 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); +/* mousetrap v1.5.2 craig.is/killing/mice */ +(function(C,r,g){function t(a,b,h){a.addEventListener?a.addEventListener(b,h,!1):a.attachEvent("on"+b,h)}function x(a){if("keypress"==a.type){var b=String.fromCharCode(a.which);a.shiftKey||(b=b.toLowerCase());return b}return l[a.which]?l[a.which]:p[a.which]?p[a.which]:String.fromCharCode(a.which).toLowerCase()}function D(a){var b=[];a.shiftKey&&b.push("shift");a.altKey&&b.push("alt");a.ctrlKey&&b.push("ctrl");a.metaKey&&b.push("meta");return b}function u(a){return"shift"==a||"ctrl"==a||"alt"==a|| +"meta"==a}function y(a,b){var h,c,e,g=[];h=a;"+"===h?h=["+"]:(h=h.replace(/\+{2}/g,"+plus"),h=h.split("+"));for(e=0;em||l.hasOwnProperty(m)&&(k[l[m]]=m)}e=k[h]?"keydown":"keypress"}"keypress"==e&&g.length&&(e="keydown");return{key:c,modifiers:g,action:e}}function B(a,b){return a===r?!1:a===b?!0:B(a.parentNode,b)}function c(a){function b(a){a=a||{}; +var b=!1,n;for(n in q)a[n]?b=!0:q[n]=0;b||(v=!1)}function h(a,b,n,f,c,h){var g,e,l=[],m=n.type;if(!d._callbacks[a])return[];"keyup"==m&&u(a)&&(b=[a]);for(g=0;g":".","?":"/","|":"\\"},z={option:"alt",command:"meta","return":"enter",escape:"esc", +plus:"+",mod:/Mac|iPod|iPhone|iPad/.test(navigator.platform)?"meta":"ctrl"},k;for(g=1;20>g;++g)l[111+g]="f"+g;for(g=0;9>=g;++g)l[g+96]=g;c.prototype.bind=function(a,b,c){a=a instanceof Array?a:[a];this._bindMultiple.call(this,a,b,c);return this};c.prototype.unbind=function(a,b){return this.bind.call(this,a,function(){},b)};c.prototype.trigger=function(a,b){if(this._directMap[a+":"+b])this._directMap[a+":"+b]({},a);return this};c.prototype.reset=function(){this._callbacks={};this._directMap={};return this}; +c.prototype.stopCallback=function(a,b){return-1<(" "+b.className+" ").indexOf(" mousetrap ")||B(b,this.target)?!1:"INPUT"==b.tagName||"SELECT"==b.tagName||"TEXTAREA"==b.tagName||b.isContentEditable};c.prototype.handleKey=function(){return this._handleKey.apply(this,arguments)};c.init=function(){var a=c(r),b;for(b in a)"_"!==b.charAt(0)&&(c[b]=function(b){return function(){return a[b].apply(a,arguments)}}(b))};c.init();C.Mousetrap=c;"undefined"!==typeof module&&module.exports&&(module.exports=c);"function"=== +typeof define&&define.amd&&define(function(){return c})})(window,document); + +(function(a){var b=a.prototype.stopCallback;a.prototype.stopCallback=function(a,c,d){return this.paused?!0:b.call(this,a,c,d)};a.prototype.pause=function(){this.paused=!0};a.prototype.unpause=function(){this.paused=!1};a.init()})(Mousetrap); + +(function(d){function n(b,a,h){if(this.recording)if("keydown"==h.type){1===b.length&&g&&k();for(i=0;ib?1:-1}),b[a]= +b[a].join("+")}function q(){f&&(r(e),f(e));e=[];f=null;c=[]}var e=[],f=null,c=[],g=!1,m=null,p=d.prototype.handleKey;d.prototype.record=function(b){var a=this;a.recording=!0;f=function(){a.recording=!1;b.apply(a,arguments)}};d.prototype.handleKey=function(){n.apply(this,arguments)};d.init()})(Mousetrap); /*! * mustache.js - Logic-less {{mustache}} templates with JavaScript diff --git a/dist/flowy.css b/dist/flowy.css index 75f80e8..8eeadd5 100644 --- a/dist/flowy.css +++ b/dist/flowy.css @@ -16,3 +16,12 @@ .todo.collapsed .todo { display: none; } + +#shortcuts .editing { + color: red; + text-color: red; + border: 2px solid; +} + +#shortcuts button { +} diff --git a/dist/flowy.js b/dist/flowy.js index 15958a4..9f31450 100644 --- a/dist/flowy.js +++ b/dist/flowy.js @@ -77,6 +77,16 @@ var Shortcut = (function(document, _) { }); } }); + function isDescendant(parent, child) { + var node = child; + while (node !== null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } var globalMousetrap = new Mousetrap(); var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; @@ -107,6 +117,7 @@ var Shortcut = (function(document, _) { action: function() {}, boundObjects: [], ownedByUs: true, + description: shortcut.id || "", id: _.guid(), }); Object.defineProperty(shortcut, "keybinding", { @@ -131,6 +142,7 @@ var Shortcut = (function(document, _) { unbindShortcut: function(shortcut, object) { // Do not call directly with an object if (!object){ _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + return; } object.mousetrap.unbind(shortcut.keybinding); shortcut.boundObjects = _.without(shortcut.boundObjects, object); @@ -140,7 +152,7 @@ var Shortcut = (function(document, _) { var oldKeybinding = shortcut.keybinding; var objects = shortcut.boundObjects; shortcut.unbind(); - shorcut._keybinding = keybinding; + shortcut._keybinding = keybinding; _.each(objects, shortcut.bind, shortcut); if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); }, @@ -149,6 +161,8 @@ var Shortcut = (function(document, _) { options = _.defaults({}, options, { element: undefined, // If the user clicks outside this element, cancel onCancel: function() {}, + onRebind: function() {}, + onComplete: function() {}, }); var manager = this; var cancelled = false; @@ -156,15 +170,33 @@ var Shortcut = (function(document, _) { if (!cancelled) { cancelled = true; if (options.onCancel) options.onCancel(); + if (options.onComplete) options.onComplete(); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + } + }; + var onclickelsewhere = function(e) { + if (!isDescendant(options.element,e.target)) { + cancel(); } }; - if (element) element.addEventListener("blur", cancel); - Mousetrap.record(function (keybinding) { // TODO: requires plugin - if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + if (options.element) { + document.addEventListener("click", onclickelsewhere); + options.element.className = options.element.className + " editing"; + } + Mousetrap.record(function (keybinding) { + if (_.isEqual(keybinding, ["esc"])) { cancel(); } else { - manager.rebindShortcut(shortcut, keybinding); - if (element) element.removeEventListener("blur", cancel); + manager.rebindShortcut(shortcut, keybinding.join(" ")); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + if (options.onRebind) options.onRebind(); + if (options.onComplete) options.onComplete(); } }); }, @@ -208,7 +240,7 @@ var Shortcut = (function(document, _) { if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject); return this.globalObject; }, - pause: function() { // TODO: These require plugin, test + pause: function() { this.boundObjects.each(function(object) { object.mousetrap.pause(); }); @@ -218,25 +250,44 @@ var Shortcut = (function(document, _) { object.mousetrap.unpause(); }); }, - displayShortcuts: function(shortcuts, options) { + displayShortcuts: function(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) + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) // TODO + shortcuts: _.sortBy(this.shortcuts, 'description'), }); - if (!shortcuts) shortcuts = this.shortcuts; if (options.objectGrouping === "default") { - var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().size().value() > 1; options.objectGrouping = multipleObjectTypes; } - // TODO: Return HTML - return ""; + return '

Shortcuts

' + _.map(options.shortcuts, function(shortcut) { + return _.template("")({ + description: shortcut.description, + keybindingHtml: this._displayKeybinding(shortcut, options) + }); + }, this).join("") + "
<%= description %><%= keybindingHtml %>
"; }, - _displayKeybinding: function(keybinding, options) { - options = _.defaults({}, options, { displayMultiple: true }); - // TODO: Return HTML - return ""; + _displayKeybinding: function(shortcut, options) { + options = _.defaults({}, options, { displayMultiple: true, allowRebind: false }); + var keybindings = shortcut.keybinding.split(","); + if (!options.displayMultiple && keybindings.length > 1) { + keybindings = [keybindings[0]]; + } + + var keybindingHTML = _.map(keybindings, function(keybinding) { + return _.template("<%= keybinding %>")({keybinding: _.escape(keybinding)}); + }).join("
"); + if (options.allowRebind) { + // Ugh, way too magic, how do I use an actual function here + return _.template('
<%= keybindingHTML %>
')({ + keybindingHTML: keybindingHTML, + id: shortcut.id + }); + } else { + return keybindingHTML; + } }, }; return Shortcut; @@ -727,12 +778,20 @@ var AppView = Backbone.View.extend({ e.save(); }); }); + Shortcut.onRebindShortcut = function() { + self.renderShortcuts(); + }; this.views = {}; // A list of views for each element in the collection this.list.fetch(); + this.render(); }, render: function() { + this.renderShortcuts(); return this; }, + renderShortcuts: function() { + this.$("#shortcuts").html(Shortcut.displayShortcuts({allowRebind: true})); + }, addOne: function(todo) { this.renderTodo(todo); }, diff --git a/dist/flowy.unwrapped.js b/dist/flowy.unwrapped.js index b98be73..5ba3ba0 100644 --- a/dist/flowy.unwrapped.js +++ b/dist/flowy.unwrapped.js @@ -76,6 +76,16 @@ var Shortcut = (function(document, _) { }); } }); + function isDescendant(parent, child) { + var node = child; + while (node !== null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } var globalMousetrap = new Mousetrap(); var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; @@ -106,6 +116,7 @@ var Shortcut = (function(document, _) { action: function() {}, boundObjects: [], ownedByUs: true, + description: shortcut.id || "", id: _.guid(), }); Object.defineProperty(shortcut, "keybinding", { @@ -130,6 +141,7 @@ var Shortcut = (function(document, _) { unbindShortcut: function(shortcut, object) { // Do not call directly with an object if (!object){ _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + return; } object.mousetrap.unbind(shortcut.keybinding); shortcut.boundObjects = _.without(shortcut.boundObjects, object); @@ -139,7 +151,7 @@ var Shortcut = (function(document, _) { var oldKeybinding = shortcut.keybinding; var objects = shortcut.boundObjects; shortcut.unbind(); - shorcut._keybinding = keybinding; + shortcut._keybinding = keybinding; _.each(objects, shortcut.bind, shortcut); if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); }, @@ -148,6 +160,8 @@ var Shortcut = (function(document, _) { options = _.defaults({}, options, { element: undefined, // If the user clicks outside this element, cancel onCancel: function() {}, + onRebind: function() {}, + onComplete: function() {}, }); var manager = this; var cancelled = false; @@ -155,15 +169,33 @@ var Shortcut = (function(document, _) { if (!cancelled) { cancelled = true; if (options.onCancel) options.onCancel(); + if (options.onComplete) options.onComplete(); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + } + }; + var onclickelsewhere = function(e) { + if (!isDescendant(options.element,e.target)) { + cancel(); } }; - if (element) element.addEventListener("blur", cancel); - Mousetrap.record(function (keybinding) { // TODO: requires plugin - if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + if (options.element) { + document.addEventListener("click", onclickelsewhere); + options.element.className = options.element.className + " editing"; + } + Mousetrap.record(function (keybinding) { + if (_.isEqual(keybinding, ["esc"])) { cancel(); } else { - manager.rebindShortcut(shortcut, keybinding); - if (element) element.removeEventListener("blur", cancel); + manager.rebindShortcut(shortcut, keybinding.join(" ")); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + if (options.onRebind) options.onRebind(); + if (options.onComplete) options.onComplete(); } }); }, @@ -207,7 +239,7 @@ var Shortcut = (function(document, _) { if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject); return this.globalObject; }, - pause: function() { // TODO: These require plugin, test + pause: function() { this.boundObjects.each(function(object) { object.mousetrap.pause(); }); @@ -217,25 +249,44 @@ var Shortcut = (function(document, _) { object.mousetrap.unpause(); }); }, - displayShortcuts: function(shortcuts, options) { + displayShortcuts: function(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) + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) // TODO + shortcuts: _.sortBy(this.shortcuts, 'description'), }); - if (!shortcuts) shortcuts = this.shortcuts; if (options.objectGrouping === "default") { - var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().size().value() > 1; options.objectGrouping = multipleObjectTypes; } - // TODO: Return HTML - return ""; + return '

Shortcuts

' + _.map(options.shortcuts, function(shortcut) { + return _.template("")({ + description: shortcut.description, + keybindingHtml: this._displayKeybinding(shortcut, options) + }); + }, this).join("") + "
<%= description %><%= keybindingHtml %>
"; }, - _displayKeybinding: function(keybinding, options) { - options = _.defaults({}, options, { displayMultiple: true }); - // TODO: Return HTML - return ""; + _displayKeybinding: function(shortcut, options) { + options = _.defaults({}, options, { displayMultiple: true, allowRebind: false }); + var keybindings = shortcut.keybinding.split(","); + if (!options.displayMultiple && keybindings.length > 1) { + keybindings = [keybindings[0]]; + } + + var keybindingHTML = _.map(keybindings, function(keybinding) { + return _.template("<%= keybinding %>")({keybinding: _.escape(keybinding)}); + }).join("
"); + if (options.allowRebind) { + // Ugh, way too magic, how do I use an actual function here + return _.template('
<%= keybindingHTML %>
')({ + keybindingHTML: keybindingHTML, + id: shortcut.id + }); + } else { + return keybindingHTML; + } }, }; return Shortcut; @@ -726,12 +777,20 @@ var AppView = Backbone.View.extend({ e.save(); }); }); + Shortcut.onRebindShortcut = function() { + self.renderShortcuts(); + }; this.views = {}; // A list of views for each element in the collection this.list.fetch(); + this.render(); }, render: function() { + this.renderShortcuts(); return this; }, + renderShortcuts: function() { + this.$("#shortcuts").html(Shortcut.displayShortcuts({allowRebind: true})); + }, addOne: function(todo) { this.renderTodo(todo); }, diff --git a/dist/index.html b/dist/index.html index 6ec7bcb..2d1d4f3 100644 --- a/dist/index.html +++ b/dist/index.html @@ -11,6 +11,9 @@ +
+ default shortcut text (should not see) +
diff --git a/src/css/flowy.css b/src/css/flowy.css index 75f80e8..8eeadd5 100644 --- a/src/css/flowy.css +++ b/src/css/flowy.css @@ -16,3 +16,12 @@ .todo.collapsed .todo { display: none; } + +#shortcuts .editing { + color: red; + text-color: red; + border: 2px solid; +} + +#shortcuts button { +} diff --git a/src/index.html b/src/index.html index 6ec7bcb..2d1d4f3 100644 --- a/src/index.html +++ b/src/index.html @@ -11,6 +11,9 @@ +
+ default shortcut text (should not see) +
diff --git a/src/library/shortcut.js b/src/library/shortcut.js index 6fdd079..dfa58de 100644 --- a/src/library/shortcut.js +++ b/src/library/shortcut.js @@ -15,6 +15,16 @@ var Shortcut = (function(document, _) { }); } }); + function isDescendant(parent, child) { + var node = child; + while (node !== null) { + if (node == parent) { + return true; + } + node = node.parentNode; + } + return false; + } var globalMousetrap = new Mousetrap(); var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap}; @@ -45,6 +55,7 @@ var Shortcut = (function(document, _) { action: function() {}, boundObjects: [], ownedByUs: true, + description: shortcut.id || "", id: _.guid(), }); Object.defineProperty(shortcut, "keybinding", { @@ -69,6 +80,7 @@ var Shortcut = (function(document, _) { unbindShortcut: function(shortcut, object) { // Do not call directly with an object if (!object){ _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); + return; } object.mousetrap.unbind(shortcut.keybinding); shortcut.boundObjects = _.without(shortcut.boundObjects, object); @@ -78,7 +90,7 @@ var Shortcut = (function(document, _) { var oldKeybinding = shortcut.keybinding; var objects = shortcut.boundObjects; shortcut.unbind(); - shorcut._keybinding = keybinding; + shortcut._keybinding = keybinding; _.each(objects, shortcut.bind, shortcut); if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding); }, @@ -87,6 +99,8 @@ var Shortcut = (function(document, _) { options = _.defaults({}, options, { element: undefined, // If the user clicks outside this element, cancel onCancel: function() {}, + onRebind: function() {}, + onComplete: function() {}, }); var manager = this; var cancelled = false; @@ -94,15 +108,33 @@ var Shortcut = (function(document, _) { if (!cancelled) { cancelled = true; if (options.onCancel) options.onCancel(); + if (options.onComplete) options.onComplete(); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + } + }; + var onclickelsewhere = function(e) { + if (!isDescendant(options.element,e.target)) { + cancel(); } }; - if (element) element.addEventListener("blur", cancel); - Mousetrap.record(function (keybinding) { // TODO: requires plugin - if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test + if (options.element) { + document.addEventListener("click", onclickelsewhere); + options.element.className = options.element.className + " editing"; + } + Mousetrap.record(function (keybinding) { + if (_.isEqual(keybinding, ["esc"])) { cancel(); } else { - manager.rebindShortcut(shortcut, keybinding); - if (element) element.removeEventListener("blur", cancel); + manager.rebindShortcut(shortcut, keybinding.join(" ")); + if (options.element) { + document.removeEventListener("click", onclickelsewhere); + options.element.className = options.element.className.replace(/(\s+|^)editing(s|$)/g, ' '); + } + if (options.onRebind) options.onRebind(); + if (options.onComplete) options.onComplete(); } }); }, @@ -146,7 +178,7 @@ var Shortcut = (function(document, _) { if (!_.contains(this.boundObjects)) this.boundObjects.push(this.globalObject); return this.globalObject; }, - pause: function() { // TODO: These require plugin, test + pause: function() { this.boundObjects.each(function(object) { object.mousetrap.pause(); }); @@ -156,25 +188,44 @@ var Shortcut = (function(document, _) { object.mousetrap.unpause(); }); }, - displayShortcuts: function(shortcuts, options) { + displayShortcuts: function(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) + highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts) // TODO + shortcuts: _.sortBy(this.shortcuts, 'description'), }); - if (!shortcuts) shortcuts = this.shortcuts; if (options.objectGrouping === "default") { - var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1; + var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().size().value() > 1; options.objectGrouping = multipleObjectTypes; } - // TODO: Return HTML - return ""; - }, - _displayKeybinding: function(keybinding, options) { - options = _.defaults({}, options, { displayMultiple: true }); - // TODO: Return HTML - return ""; + return '

Shortcuts

' + _.map(options.shortcuts, function(shortcut) { + return _.template("")({ + description: shortcut.description, + keybindingHtml: this._displayKeybinding(shortcut, options) + }); + }, this).join("") + "
<%= description %><%= keybindingHtml %>
"; + }, + _displayKeybinding: function(shortcut, options) { + options = _.defaults({}, options, { displayMultiple: true, allowRebind: false }); + var keybindings = shortcut.keybinding.split(","); + if (!options.displayMultiple && keybindings.length > 1) { + keybindings = [keybindings[0]]; + } + + var keybindingHTML = _.map(keybindings, function(keybinding) { + return _.template("<%= keybinding %>")({keybinding: _.escape(keybinding)}); + }).join("
"); + if (options.allowRebind) { + // Ugh, way too magic, how do I use an actual function here + return _.template('
<%= keybindingHTML %>
')({ + keybindingHTML: keybindingHTML, + id: shortcut.id + }); + } else { + return keybindingHTML; + } }, }; return Shortcut; diff --git a/src/views/app.js b/src/views/app.js index 1bb884a..31d06dd 100644 --- a/src/views/app.js +++ b/src/views/app.js @@ -79,12 +79,20 @@ var AppView = Backbone.View.extend({ e.save(); }); }); + Shortcut.onRebindShortcut = function() { + self.renderShortcuts(); + }; this.views = {}; // A list of views for each element in the collection this.list.fetch(); + this.render(); }, render: function() { + this.renderShortcuts(); return this; }, + renderShortcuts: function() { + this.$("#shortcuts").html(Shortcut.displayShortcuts({allowRebind: true})); + }, addOne: function(todo) { this.renderTodo(todo); }, -- 2.47.3