]> git.za3k.com Git - flowy.git/commitdiff
Support rebinding shortcuts
authorZachary Vance <vanceza@gmail.com>
Thu, 21 May 2015 05:08:12 +0000 (22:08 -0700)
committerZachary Vance <vanceza@gmail.com>
Thu, 21 May 2015 05:08:12 +0000 (22:08 -0700)
Gruntfile.js
dist/_bower.js
dist/flowy.css
dist/flowy.js
dist/flowy.unwrapped.js
dist/index.html
src/css/flowy.css
src/index.html
src/library/shortcut.js
src/views/app.js

index 9de876fb18f9516a868ec5d0ca2bd48327d744cb..2b68fe6bed8248700a04c3dece6b74d083de50d7 100644 (file)
@@ -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']
         }
       }
     },
index 76b70b8682a48298bc0bda5f1420bda57751bcc5..aaddcccf29bb23e435f093e20e08712fa6db03b7 100644 (file)
@@ -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;e<h.length;++e)c=h[e],z[c]&&(c=z[c]),b&&"keypress"!=b&&A[c]&&(c=A[c],g.push("shift")),u(c)&&g.push(c);h=c;e=b;if(!e){if(!k){k={};for(var m in l)95<m&&112>m||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<d._callbacks[a].length;++g)if(e=d._callbacks[a][g],(f||!e.seq||q[e.seq]==e.level)&&m==e.action){var k;(k="keypress"==m&&!n.metaKey&&!n.ctrlKey)||(k=e.modifiers,k=b.sort().join(",")===k.sort().join(","));k&&(k=f&&e.seq==f&&e.level==h,(!f&&e.combo==c||k)&&d._callbacks[a].splice(g,1),l.push(e))}return l}function g(a,b,n,f){d.stopCallback(b,b.target||
+b.srcElement,n,f)||!1!==a(b,n)||(b.preventDefault?b.preventDefault():b.returnValue=!1,b.stopPropagation?b.stopPropagation():b.cancelBubble=!0)}function e(a){"number"!==typeof a.which&&(a.which=a.keyCode);var b=x(a);b&&("keyup"==a.type&&w===b?w=!1:d.handleKey(b,D(a),a))}function l(a,c,n,f){function e(c){return function(){v=c;++q[a];clearTimeout(k);k=setTimeout(b,1E3)}}function h(c){g(n,c,a);"keyup"!==f&&(w=x(c));setTimeout(b,10)}for(var d=q[a]=0;d<c.length;++d){var p=d+1===c.length?h:e(f||y(c[d+1]).action);
+m(c[d],p,f,a,d)}}function m(a,b,c,f,e){d._directMap[a+":"+c]=b;a=a.replace(/\s+/g," ");var g=a.split(" ");1<g.length?l(a,g,b,c):(c=y(a,c),d._callbacks[c.key]=d._callbacks[c.key]||[],h(c.key,c.modifiers,{type:c.action},f,a,e),d._callbacks[c.key][f?"unshift":"push"]({callback:b,modifiers:c.modifiers,action:c.action,seq:f,level:e,combo:a}))}var d=this;a=a||r;if(!(d instanceof c))return new c(a);d.target=a;d._callbacks={};d._directMap={};var q={},k,w=!1,p=!1,v=!1;d._handleKey=function(a,c,e){var f=h(a,
+c,e),d;c={};var k=0,l=!1;for(d=0;d<f.length;++d)f[d].seq&&(k=Math.max(k,f[d].level));for(d=0;d<f.length;++d)f[d].seq?f[d].level==k&&(l=!0,c[f[d].seq]=1,g(f[d].callback,e,f[d].combo,f[d].seq)):l||g(f[d].callback,e,f[d].combo);f="keypress"==e.type&&p;e.type!=v||u(a)||f||b(c);p=l&&"keydown"==e.type};d._bindMultiple=function(a,b,c){for(var d=0;d<a.length;++d)m(a[d],b,c)};t(a,"keypress",e);t(a,"keydown",e);t(a,"keyup",e)}var l={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"},p={106:"*",107:"+",109:"-",110:".",111:"/",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},A={"~":"`","!":"1","@":"2","#":"3",$:"4","%":"5","^":"6","&":"7","*":"8","(":"9",")":"0",_:"-","+":"=",":":";",'"':"'","<":",",">":".","?":"/","|":"\\"},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;i<a.length;++i)l(a[i]);l(b)}else"keyup"==h.type&&0<c.length&&k();else p.apply(this,arguments)}function l(b){var a;for(a=0;a<c.length;++a)if(c[a]===b)return;c.push(b);1===b.length&&(g=!0)}function k(){e.push(c);c=[];g=!1;clearTimeout(m);m=setTimeout(q,1E3)}function r(b){var a;for(a=0;a<b.length;++a)b[a].sort(function(a,b){return 1<a.length&&1===b.length?-1:1===a.length&&1<b.length?1:a>b?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
index 75f80e8021586e67979997ab762c44ccc8aeba4d..8eeadd5fecea54e5b20ce33ccd87ba1e15a6a329 100644 (file)
 .todo.collapsed .todo {
     display: none;
 }
+
+#shortcuts .editing {
+    color: red;
+    text-color: red;
+    border: 2px solid;
+}
+
+#shortcuts button {
+}
index 15958a4763df238e9c111d835243784d93fb0424..9f314505a6eeb115f21cee72b19f70b1941e2847 100644 (file)
@@ -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 '<h3>Shortcuts</h3><table>' + _.map(options.shortcuts, function(shortcut) {
+                return _.template("<tr><td><%= description %></td><td><%= keybindingHtml %></td></tr>")({
+                    description: shortcut.description,
+                    keybindingHtml: this._displayKeybinding(shortcut, options)
+                });
+            }, this).join("") + "</table>";
         },
-        _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("<code><%= keybinding %></code>")({keybinding: _.escape(keybinding)});
+            }).join("<br/>");
+            if (options.allowRebind) {
+                // Ugh, way too magic, how do I use an actual function here
+                return _.template('<div onclick="Shortcut.userRebindShortcut(Shortcut.shortcuts[\'<%= id %>\'], {element: this })"><%= keybindingHTML %></div>')({
+                    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);
     },
index b98be73b068413248b8115b1ff81b7f02d254f50..5ba3ba087e5b2fe3cab3b189ce077d07a2c53771 100644 (file)
@@ -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 '<h3>Shortcuts</h3><table>' + _.map(options.shortcuts, function(shortcut) {
+                return _.template("<tr><td><%= description %></td><td><%= keybindingHtml %></td></tr>")({
+                    description: shortcut.description,
+                    keybindingHtml: this._displayKeybinding(shortcut, options)
+                });
+            }, this).join("") + "</table>";
         },
-        _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("<code><%= keybinding %></code>")({keybinding: _.escape(keybinding)});
+            }).join("<br/>");
+            if (options.allowRebind) {
+                // Ugh, way too magic, how do I use an actual function here
+                return _.template('<div onclick="Shortcut.userRebindShortcut(Shortcut.shortcuts[\'<%= id %>\'], {element: this })"><%= keybindingHTML %></div>')({
+                    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);
     },
index 6ec7bcbf444a0fe81baccfa43f350c5252364f07..2d1d4f32137a4dce924438331d89f6b9729381a2 100644 (file)
@@ -11,6 +11,9 @@
 
     </div>
     <button id="reset-button">Everyone heard me say "reset button", right?</button>
+    <div id="shortcuts">
+        default shortcut text (should not see)
+    </div>
 </div>
 </body>
 </html>
index 75f80e8021586e67979997ab762c44ccc8aeba4d..8eeadd5fecea54e5b20ce33ccd87ba1e15a6a329 100644 (file)
 .todo.collapsed .todo {
     display: none;
 }
+
+#shortcuts .editing {
+    color: red;
+    text-color: red;
+    border: 2px solid;
+}
+
+#shortcuts button {
+}
index 6ec7bcbf444a0fe81baccfa43f350c5252364f07..2d1d4f32137a4dce924438331d89f6b9729381a2 100644 (file)
@@ -11,6 +11,9 @@
 
     </div>
     <button id="reset-button">Everyone heard me say "reset button", right?</button>
+    <div id="shortcuts">
+        default shortcut text (should not see)
+    </div>
 </div>
 </body>
 </html>
index 6fdd0793b9158cf6da51ade3d28344c9e419298f..dfa58decf9ba4617779dc19a3e40db31a0401f77 100644 (file)
@@ -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 '<h3>Shortcuts</h3><table>' + _.map(options.shortcuts, function(shortcut) {
+                return _.template("<tr><td><%= description %></td><td><%= keybindingHtml %></td></tr>")({
+                    description: shortcut.description,
+                    keybindingHtml: this._displayKeybinding(shortcut, options)
+                });
+            }, this).join("") + "</table>";
+        },
+        _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("<code><%= keybinding %></code>")({keybinding: _.escape(keybinding)});
+            }).join("<br/>");
+            if (options.allowRebind) {
+                // Ugh, way too magic, how do I use an actual function here
+                return _.template('<div onclick="Shortcut.userRebindShortcut(Shortcut.shortcuts[\'<%= id %>\'], {element: this })"><%= keybindingHTML %></div>')({
+                    keybindingHTML: keybindingHTML,
+                    id: shortcut.id
+                });
+            } else {
+                return keybindingHTML;
+            }
         },
     };
     return Shortcut;
index 1bb884a2b24de156cf0c544b85f3249cb6fd0530..31d06ddf1d10d0bf0f51c7c38ede6fd58659324f 100644 (file)
@@ -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);
     },