]> git.za3k.com Git - flowy.git/commitdiff
Make global shortcuts work
authorZachary Vance <vanceza@gmail.com>
Tue, 19 May 2015 22:46:49 +0000 (15:46 -0700)
committerZachary Vance <vanceza@gmail.com>
Tue, 19 May 2015 22:46:49 +0000 (15:46 -0700)
dist/_bower.js
dist/flowy.js
dist/flowy.unwrapped.js
src/library/shortcut.js [new file with mode: 0644]
src/views/app.js

index 718b2aadfef878a779aa1a97c92e980375e7a677..76b70b8682a48298bc0bda5f1420bda57751bcc5 100644 (file)
@@ -1,5 +1,5 @@
 /*!
- * jQuery JavaScript Library v2.1.3
+ * jQuery JavaScript Library v2.1.4
  * http://jquery.com/
  *
  * Includes Sizzle.js
@@ -9,7 +9,7 @@
  * Released under the MIT license
  * http://jquery.org/license
  *
- * Date: 2014-12-18T15:11Z
+ * Date: 2015-04-28T16:01Z
  */
 
 (function( global, factory ) {
@@ -67,7 +67,7 @@ var
        // Use the correct document accordingly with window argument (sandbox)
        document = window.document,
 
-       version = "2.1.3",
+       version = "2.1.4",
 
        // Define a local copy of jQuery
        jQuery = function( selector, context ) {
@@ -531,7 +531,12 @@ jQuery.each("Boolean Number String Function Array Date RegExp Object Error".spli
 });
 
 function isArraylike( obj ) {
-       var length = obj.length,
+
+       // Support: iOS 8.2 (not reproducible in simulator)
+       // `in` check used to prevent JIT error (gh-2145)
+       // hasOwn isn't used here due to false negatives
+       // regarding Nodelist length in IE
+       var length = "length" in obj && obj.length,
                type = jQuery.type( obj );
 
        if ( type === "function" || jQuery.isWindow( obj ) ) {
@@ -27486,6 +27491,1028 @@ if ((typeof define !== "undefined" && define !== null) && (define.amd != null))
 
 }).call(this);
 
+/*global define:false */
+/**
+ * Copyright 2015 Craig Campbell
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ * Mousetrap is a simple keyboard shortcut library for Javascript with
+ * no external dependencies
+ *
+ * @version 1.5.2
+ * @url craig.is/killing/mice
+ */
+(function(window, document, undefined) {
+
+    /**
+     * mapping of special keycodes to their corresponding keys
+     *
+     * everything in this dictionary cannot use keypress events
+     * so it has to be here to map to the correct keycodes for
+     * keyup/keydown events
+     *
+     * @type {Object}
+     */
+    var _MAP = {
+        8: 'backspace',
+        9: 'tab',
+        13: 'enter',
+        16: 'shift',
+        17: 'ctrl',
+        18: 'alt',
+        20: 'capslock',
+        27: 'esc',
+        32: 'space',
+        33: 'pageup',
+        34: 'pagedown',
+        35: 'end',
+        36: 'home',
+        37: 'left',
+        38: 'up',
+        39: 'right',
+        40: 'down',
+        45: 'ins',
+        46: 'del',
+        91: 'meta',
+        93: 'meta',
+        224: 'meta'
+    };
+
+    /**
+     * mapping for special characters so they can support
+     *
+     * this dictionary is only used incase you want to bind a
+     * keyup or keydown event to one of these keys
+     *
+     * @type {Object}
+     */
+    var _KEYCODE_MAP = {
+        106: '*',
+        107: '+',
+        109: '-',
+        110: '.',
+        111 : '/',
+        186: ';',
+        187: '=',
+        188: ',',
+        189: '-',
+        190: '.',
+        191: '/',
+        192: '`',
+        219: '[',
+        220: '\\',
+        221: ']',
+        222: '\''
+    };
+
+    /**
+     * this is a mapping of keys that require shift on a US keypad
+     * back to the non shift equivelents
+     *
+     * this is so you can use keyup events with these keys
+     *
+     * note that this will only work reliably on US keyboards
+     *
+     * @type {Object}
+     */
+    var _SHIFT_MAP = {
+        '~': '`',
+        '!': '1',
+        '@': '2',
+        '#': '3',
+        '$': '4',
+        '%': '5',
+        '^': '6',
+        '&': '7',
+        '*': '8',
+        '(': '9',
+        ')': '0',
+        '_': '-',
+        '+': '=',
+        ':': ';',
+        '\"': '\'',
+        '<': ',',
+        '>': '.',
+        '?': '/',
+        '|': '\\'
+    };
+
+    /**
+     * this is a list of special strings you can use to map
+     * to modifier keys when you specify your keyboard shortcuts
+     *
+     * @type {Object}
+     */
+    var _SPECIAL_ALIASES = {
+        'option': 'alt',
+        'command': 'meta',
+        'return': 'enter',
+        'escape': 'esc',
+        'plus': '+',
+        'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
+    };
+
+    /**
+     * variable to store the flipped version of _MAP from above
+     * needed to check if we should use keypress or not when no action
+     * is specified
+     *
+     * @type {Object|undefined}
+     */
+    var _REVERSE_MAP;
+
+    /**
+     * loop through the f keys, f1 to f19 and add them to the map
+     * programatically
+     */
+    for (var i = 1; i < 20; ++i) {
+        _MAP[111 + i] = 'f' + i;
+    }
+
+    /**
+     * loop through to map numbers on the numeric keypad
+     */
+    for (i = 0; i <= 9; ++i) {
+        _MAP[i + 96] = i;
+    }
+
+    /**
+     * cross browser add event method
+     *
+     * @param {Element|HTMLDocument} object
+     * @param {string} type
+     * @param {Function} callback
+     * @returns void
+     */
+    function _addEvent(object, type, callback) {
+        if (object.addEventListener) {
+            object.addEventListener(type, callback, false);
+            return;
+        }
+
+        object.attachEvent('on' + type, callback);
+    }
+
+    /**
+     * takes the event and returns the key character
+     *
+     * @param {Event} e
+     * @return {string}
+     */
+    function _characterFromEvent(e) {
+
+        // for keypress events we should return the character as is
+        if (e.type == 'keypress') {
+            var character = String.fromCharCode(e.which);
+
+            // if the shift key is not pressed then it is safe to assume
+            // that we want the character to be lowercase.  this means if
+            // you accidentally have caps lock on then your key bindings
+            // will continue to work
+            //
+            // the only side effect that might not be desired is if you
+            // bind something like 'A' cause you want to trigger an
+            // event when capital A is pressed caps lock will no longer
+            // trigger the event.  shift+a will though.
+            if (!e.shiftKey) {
+                character = character.toLowerCase();
+            }
+
+            return character;
+        }
+
+        // for non keypress events the special maps are needed
+        if (_MAP[e.which]) {
+            return _MAP[e.which];
+        }
+
+        if (_KEYCODE_MAP[e.which]) {
+            return _KEYCODE_MAP[e.which];
+        }
+
+        // if it is not in the special map
+
+        // with keydown and keyup events the character seems to always
+        // come in as an uppercase character whether you are pressing shift
+        // or not.  we should make sure it is always lowercase for comparisons
+        return String.fromCharCode(e.which).toLowerCase();
+    }
+
+    /**
+     * checks if two arrays are equal
+     *
+     * @param {Array} modifiers1
+     * @param {Array} modifiers2
+     * @returns {boolean}
+     */
+    function _modifiersMatch(modifiers1, modifiers2) {
+        return modifiers1.sort().join(',') === modifiers2.sort().join(',');
+    }
+
+    /**
+     * takes a key event and figures out what the modifiers are
+     *
+     * @param {Event} e
+     * @returns {Array}
+     */
+    function _eventModifiers(e) {
+        var modifiers = [];
+
+        if (e.shiftKey) {
+            modifiers.push('shift');
+        }
+
+        if (e.altKey) {
+            modifiers.push('alt');
+        }
+
+        if (e.ctrlKey) {
+            modifiers.push('ctrl');
+        }
+
+        if (e.metaKey) {
+            modifiers.push('meta');
+        }
+
+        return modifiers;
+    }
+
+    /**
+     * prevents default for this event
+     *
+     * @param {Event} e
+     * @returns void
+     */
+    function _preventDefault(e) {
+        if (e.preventDefault) {
+            e.preventDefault();
+            return;
+        }
+
+        e.returnValue = false;
+    }
+
+    /**
+     * stops propogation for this event
+     *
+     * @param {Event} e
+     * @returns void
+     */
+    function _stopPropagation(e) {
+        if (e.stopPropagation) {
+            e.stopPropagation();
+            return;
+        }
+
+        e.cancelBubble = true;
+    }
+
+    /**
+     * determines if the keycode specified is a modifier key or not
+     *
+     * @param {string} key
+     * @returns {boolean}
+     */
+    function _isModifier(key) {
+        return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
+    }
+
+    /**
+     * reverses the map lookup so that we can look for specific keys
+     * to see what can and can't use keypress
+     *
+     * @return {Object}
+     */
+    function _getReverseMap() {
+        if (!_REVERSE_MAP) {
+            _REVERSE_MAP = {};
+            for (var key in _MAP) {
+
+                // pull out the numeric keypad from here cause keypress should
+                // be able to detect the keys from the character
+                if (key > 95 && key < 112) {
+                    continue;
+                }
+
+                if (_MAP.hasOwnProperty(key)) {
+                    _REVERSE_MAP[_MAP[key]] = key;
+                }
+            }
+        }
+        return _REVERSE_MAP;
+    }
+
+    /**
+     * picks the best action based on the key combination
+     *
+     * @param {string} key - character for key
+     * @param {Array} modifiers
+     * @param {string=} action passed in
+     */
+    function _pickBestAction(key, modifiers, action) {
+
+        // if no action was picked in we should try to pick the one
+        // that we think would work best for this key
+        if (!action) {
+            action = _getReverseMap()[key] ? 'keydown' : 'keypress';
+        }
+
+        // modifier keys don't work as expected with keypress,
+        // switch to keydown
+        if (action == 'keypress' && modifiers.length) {
+            action = 'keydown';
+        }
+
+        return action;
+    }
+
+    /**
+     * Converts from a string key combination to an array
+     *
+     * @param  {string} combination like "command+shift+l"
+     * @return {Array}
+     */
+    function _keysFromString(combination) {
+        if (combination === '+') {
+            return ['+'];
+        }
+
+        combination = combination.replace(/\+{2}/g, '+plus');
+        return combination.split('+');
+    }
+
+    /**
+     * Gets info for a specific key combination
+     *
+     * @param  {string} combination key combination ("command+s" or "a" or "*")
+     * @param  {string=} action
+     * @returns {Object}
+     */
+    function _getKeyInfo(combination, action) {
+        var keys;
+        var key;
+        var i;
+        var modifiers = [];
+
+        // take the keys from this pattern and figure out what the actual
+        // pattern is all about
+        keys = _keysFromString(combination);
+
+        for (i = 0; i < keys.length; ++i) {
+            key = keys[i];
+
+            // normalize key names
+            if (_SPECIAL_ALIASES[key]) {
+                key = _SPECIAL_ALIASES[key];
+            }
+
+            // if this is not a keypress event then we should
+            // be smart about using shift keys
+            // this will only work for US keyboards however
+            if (action && action != 'keypress' && _SHIFT_MAP[key]) {
+                key = _SHIFT_MAP[key];
+                modifiers.push('shift');
+            }
+
+            // if this key is a modifier then add it to the list of modifiers
+            if (_isModifier(key)) {
+                modifiers.push(key);
+            }
+        }
+
+        // depending on what the key combination is
+        // we will try to pick the best event for it
+        action = _pickBestAction(key, modifiers, action);
+
+        return {
+            key: key,
+            modifiers: modifiers,
+            action: action
+        };
+    }
+
+    function _belongsTo(element, ancestor) {
+        if (element === document) {
+            return false;
+        }
+
+        if (element === ancestor) {
+            return true;
+        }
+
+        return _belongsTo(element.parentNode, ancestor);
+    }
+
+    function Mousetrap(targetElement) {
+        var self = this;
+
+        targetElement = targetElement || document;
+
+        if (!(self instanceof Mousetrap)) {
+            return new Mousetrap(targetElement);
+        }
+
+        /**
+         * element to attach key events to
+         *
+         * @type {Element}
+         */
+        self.target = targetElement;
+
+        /**
+         * a list of all the callbacks setup via Mousetrap.bind()
+         *
+         * @type {Object}
+         */
+        self._callbacks = {};
+
+        /**
+         * direct map of string combinations to callbacks used for trigger()
+         *
+         * @type {Object}
+         */
+        self._directMap = {};
+
+        /**
+         * keeps track of what level each sequence is at since multiple
+         * sequences can start out with the same sequence
+         *
+         * @type {Object}
+         */
+        var _sequenceLevels = {};
+
+        /**
+         * variable to store the setTimeout call
+         *
+         * @type {null|number}
+         */
+        var _resetTimer;
+
+        /**
+         * temporary state where we will ignore the next keyup
+         *
+         * @type {boolean|string}
+         */
+        var _ignoreNextKeyup = false;
+
+        /**
+         * temporary state where we will ignore the next keypress
+         *
+         * @type {boolean}
+         */
+        var _ignoreNextKeypress = false;
+
+        /**
+         * are we currently inside of a sequence?
+         * type of action ("keyup" or "keydown" or "keypress") or false
+         *
+         * @type {boolean|string}
+         */
+        var _nextExpectedAction = false;
+
+        /**
+         * resets all sequence counters except for the ones passed in
+         *
+         * @param {Object} doNotReset
+         * @returns void
+         */
+        function _resetSequences(doNotReset) {
+            doNotReset = doNotReset || {};
+
+            var activeSequences = false,
+                key;
+
+            for (key in _sequenceLevels) {
+                if (doNotReset[key]) {
+                    activeSequences = true;
+                    continue;
+                }
+                _sequenceLevels[key] = 0;
+            }
+
+            if (!activeSequences) {
+                _nextExpectedAction = false;
+            }
+        }
+
+        /**
+         * finds all callbacks that match based on the keycode, modifiers,
+         * and action
+         *
+         * @param {string} character
+         * @param {Array} modifiers
+         * @param {Event|Object} e
+         * @param {string=} sequenceName - name of the sequence we are looking for
+         * @param {string=} combination
+         * @param {number=} level
+         * @returns {Array}
+         */
+        function _getMatches(character, modifiers, e, sequenceName, combination, level) {
+            var i;
+            var callback;
+            var matches = [];
+            var action = e.type;
+
+            // if there are no events related to this keycode
+            if (!self._callbacks[character]) {
+                return [];
+            }
+
+            // if a modifier key is coming up on its own we should allow it
+            if (action == 'keyup' && _isModifier(character)) {
+                modifiers = [character];
+            }
+
+            // loop through all callbacks for the key that was pressed
+            // and see if any of them match
+            for (i = 0; i < self._callbacks[character].length; ++i) {
+                callback = self._callbacks[character][i];
+
+                // if a sequence name is not specified, but this is a sequence at
+                // the wrong level then move onto the next match
+                if (!sequenceName && callback.seq && _sequenceLevels[callback.seq] != callback.level) {
+                    continue;
+                }
+
+                // if the action we are looking for doesn't match the action we got
+                // then we should keep going
+                if (action != callback.action) {
+                    continue;
+                }
+
+                // if this is a keypress event and the meta key and control key
+                // are not pressed that means that we need to only look at the
+                // character, otherwise check the modifiers as well
+                //
+                // chrome will not fire a keypress if meta or control is down
+                // safari will fire a keypress if meta or meta+shift is down
+                // firefox will fire a keypress if meta or control is down
+                if ((action == 'keypress' && !e.metaKey && !e.ctrlKey) || _modifiersMatch(modifiers, callback.modifiers)) {
+
+                    // when you bind a combination or sequence a second time it
+                    // should overwrite the first one.  if a sequenceName or
+                    // combination is specified in this call it does just that
+                    //
+                    // @todo make deleting its own method?
+                    var deleteCombo = !sequenceName && callback.combo == combination;
+                    var deleteSequence = sequenceName && callback.seq == sequenceName && callback.level == level;
+                    if (deleteCombo || deleteSequence) {
+                        self._callbacks[character].splice(i, 1);
+                    }
+
+                    matches.push(callback);
+                }
+            }
+
+            return matches;
+        }
+
+        /**
+         * actually calls the callback function
+         *
+         * if your callback function returns false this will use the jquery
+         * convention - prevent default and stop propogation on the event
+         *
+         * @param {Function} callback
+         * @param {Event} e
+         * @returns void
+         */
+        function _fireCallback(callback, e, combo, sequence) {
+
+            // if this event should not happen stop here
+            if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
+                return;
+            }
+
+            if (callback(e, combo) === false) {
+                _preventDefault(e);
+                _stopPropagation(e);
+            }
+        }
+
+        /**
+         * handles a character key event
+         *
+         * @param {string} character
+         * @param {Array} modifiers
+         * @param {Event} e
+         * @returns void
+         */
+        self._handleKey = function(character, modifiers, e) {
+            var callbacks = _getMatches(character, modifiers, e);
+            var i;
+            var doNotReset = {};
+            var maxLevel = 0;
+            var processedSequenceCallback = false;
+
+            // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
+            for (i = 0; i < callbacks.length; ++i) {
+                if (callbacks[i].seq) {
+                    maxLevel = Math.max(maxLevel, callbacks[i].level);
+                }
+            }
+
+            // loop through matching callbacks for this key event
+            for (i = 0; i < callbacks.length; ++i) {
+
+                // fire for all sequence callbacks
+                // this is because if for example you have multiple sequences
+                // bound such as "g i" and "g t" they both need to fire the
+                // callback for matching g cause otherwise you can only ever
+                // match the first one
+                if (callbacks[i].seq) {
+
+                    // only fire callbacks for the maxLevel to prevent
+                    // subsequences from also firing
+                    //
+                    // for example 'a option b' should not cause 'option b' to fire
+                    // even though 'option b' is part of the other sequence
+                    //
+                    // any sequences that do not match here will be discarded
+                    // below by the _resetSequences call
+                    if (callbacks[i].level != maxLevel) {
+                        continue;
+                    }
+
+                    processedSequenceCallback = true;
+
+                    // keep a list of which sequences were matches for later
+                    doNotReset[callbacks[i].seq] = 1;
+                    _fireCallback(callbacks[i].callback, e, callbacks[i].combo, callbacks[i].seq);
+                    continue;
+                }
+
+                // if there were no sequence matches but we are still here
+                // that means this is a regular match so we should fire that
+                if (!processedSequenceCallback) {
+                    _fireCallback(callbacks[i].callback, e, callbacks[i].combo);
+                }
+            }
+
+            // if the key you pressed matches the type of sequence without
+            // being a modifier (ie "keyup" or "keypress") then we should
+            // reset all sequences that were not matched by this event
+            //
+            // this is so, for example, if you have the sequence "h a t" and you
+            // type "h e a r t" it does not match.  in this case the "e" will
+            // cause the sequence to reset
+            //
+            // modifier keys are ignored because you can have a sequence
+            // that contains modifiers such as "enter ctrl+space" and in most
+            // cases the modifier key will be pressed before the next key
+            //
+            // also if you have a sequence such as "ctrl+b a" then pressing the
+            // "b" key will trigger a "keypress" and a "keydown"
+            //
+            // the "keydown" is expected when there is a modifier, but the
+            // "keypress" ends up matching the _nextExpectedAction since it occurs
+            // after and that causes the sequence to reset
+            //
+            // we ignore keypresses in a sequence that directly follow a keydown
+            // for the same character
+            var ignoreThisKeypress = e.type == 'keypress' && _ignoreNextKeypress;
+            if (e.type == _nextExpectedAction && !_isModifier(character) && !ignoreThisKeypress) {
+                _resetSequences(doNotReset);
+            }
+
+            _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
+        };
+
+        /**
+         * handles a keydown event
+         *
+         * @param {Event} e
+         * @returns void
+         */
+        function _handleKeyEvent(e) {
+
+            // normalize e.which for key events
+            // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion
+            if (typeof e.which !== 'number') {
+                e.which = e.keyCode;
+            }
+
+            var character = _characterFromEvent(e);
+
+            // no character found then stop
+            if (!character) {
+                return;
+            }
+
+            // need to use === for the character check because the character can be 0
+            if (e.type == 'keyup' && _ignoreNextKeyup === character) {
+                _ignoreNextKeyup = false;
+                return;
+            }
+
+            self.handleKey(character, _eventModifiers(e), e);
+        }
+
+        /**
+         * called to set a 1 second timeout on the specified sequence
+         *
+         * this is so after each key press in the sequence you have 1 second
+         * to press the next key before you have to start over
+         *
+         * @returns void
+         */
+        function _resetSequenceTimer() {
+            clearTimeout(_resetTimer);
+            _resetTimer = setTimeout(_resetSequences, 1000);
+        }
+
+        /**
+         * binds a key sequence to an event
+         *
+         * @param {string} combo - combo specified in bind call
+         * @param {Array} keys
+         * @param {Function} callback
+         * @param {string=} action
+         * @returns void
+         */
+        function _bindSequence(combo, keys, callback, action) {
+
+            // start off by adding a sequence level record for this combination
+            // and setting the level to 0
+            _sequenceLevels[combo] = 0;
+
+            /**
+             * callback to increase the sequence level for this sequence and reset
+             * all other sequences that were active
+             *
+             * @param {string} nextAction
+             * @returns {Function}
+             */
+            function _increaseSequence(nextAction) {
+                return function() {
+                    _nextExpectedAction = nextAction;
+                    ++_sequenceLevels[combo];
+                    _resetSequenceTimer();
+                };
+            }
+
+            /**
+             * wraps the specified callback inside of another function in order
+             * to reset all sequence counters as soon as this sequence is done
+             *
+             * @param {Event} e
+             * @returns void
+             */
+            function _callbackAndReset(e) {
+                _fireCallback(callback, e, combo);
+
+                // we should ignore the next key up if the action is key down
+                // or keypress.  this is so if you finish a sequence and
+                // release the key the final key will not trigger a keyup
+                if (action !== 'keyup') {
+                    _ignoreNextKeyup = _characterFromEvent(e);
+                }
+
+                // weird race condition if a sequence ends with the key
+                // another sequence begins with
+                setTimeout(_resetSequences, 10);
+            }
+
+            // loop through keys one at a time and bind the appropriate callback
+            // function.  for any key leading up to the final one it should
+            // increase the sequence. after the final, it should reset all sequences
+            //
+            // if an action is specified in the original bind call then that will
+            // be used throughout.  otherwise we will pass the action that the
+            // next key in the sequence should match.  this allows a sequence
+            // to mix and match keypress and keydown events depending on which
+            // ones are better suited to the key provided
+            for (var i = 0; i < keys.length; ++i) {
+                var isFinal = i + 1 === keys.length;
+                var wrappedCallback = isFinal ? _callbackAndReset : _increaseSequence(action || _getKeyInfo(keys[i + 1]).action);
+                _bindSingle(keys[i], wrappedCallback, action, combo, i);
+            }
+        }
+
+        /**
+         * binds a single keyboard combination
+         *
+         * @param {string} combination
+         * @param {Function} callback
+         * @param {string=} action
+         * @param {string=} sequenceName - name of sequence if part of sequence
+         * @param {number=} level - what part of the sequence the command is
+         * @returns void
+         */
+        function _bindSingle(combination, callback, action, sequenceName, level) {
+
+            // store a direct mapped reference for use with Mousetrap.trigger
+            self._directMap[combination + ':' + action] = callback;
+
+            // make sure multiple spaces in a row become a single space
+            combination = combination.replace(/\s+/g, ' ');
+
+            var sequence = combination.split(' ');
+            var info;
+
+            // if this pattern is a sequence of keys then run through this method
+            // to reprocess each pattern one key at a time
+            if (sequence.length > 1) {
+                _bindSequence(combination, sequence, callback, action);
+                return;
+            }
+
+            info = _getKeyInfo(combination, action);
+
+            // make sure to initialize array if this is the first time
+            // a callback is added for this key
+            self._callbacks[info.key] = self._callbacks[info.key] || [];
+
+            // remove an existing match if there is one
+            _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
+
+            // add this call back to the array
+            // if it is a sequence put it at the beginning
+            // if not put it at the end
+            //
+            // this is important because the way these are processed expects
+            // the sequence ones to come first
+            self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
+                callback: callback,
+                modifiers: info.modifiers,
+                action: info.action,
+                seq: sequenceName,
+                level: level,
+                combo: combination
+            });
+        }
+
+        /**
+         * binds multiple combinations to the same callback
+         *
+         * @param {Array} combinations
+         * @param {Function} callback
+         * @param {string|undefined} action
+         * @returns void
+         */
+        self._bindMultiple = function(combinations, callback, action) {
+            for (var i = 0; i < combinations.length; ++i) {
+                _bindSingle(combinations[i], callback, action);
+            }
+        };
+
+        // start!
+        _addEvent(targetElement, 'keypress', _handleKeyEvent);
+        _addEvent(targetElement, 'keydown', _handleKeyEvent);
+        _addEvent(targetElement, 'keyup', _handleKeyEvent);
+    }
+
+    /**
+     * binds an event to mousetrap
+     *
+     * can be a single key, a combination of keys separated with +,
+     * an array of keys, or a sequence of keys separated by spaces
+     *
+     * be sure to list the modifier keys first to make sure that the
+     * correct key ends up getting bound (the last key in the pattern)
+     *
+     * @param {string|Array} keys
+     * @param {Function} callback
+     * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+     * @returns void
+     */
+    Mousetrap.prototype.bind = function(keys, callback, action) {
+        var self = this;
+        keys = keys instanceof Array ? keys : [keys];
+        self._bindMultiple.call(self, keys, callback, action);
+        return self;
+    };
+
+    /**
+     * unbinds an event to mousetrap
+     *
+     * the unbinding sets the callback function of the specified key combo
+     * to an empty function and deletes the corresponding key in the
+     * _directMap dict.
+     *
+     * TODO: actually remove this from the _callbacks dictionary instead
+     * of binding an empty function
+     *
+     * the keycombo+action has to be exactly the same as
+     * it was defined in the bind method
+     *
+     * @param {string|Array} keys
+     * @param {string} action
+     * @returns void
+     */
+    Mousetrap.prototype.unbind = function(keys, action) {
+        var self = this;
+        return self.bind.call(self, keys, function() {}, action);
+    };
+
+    /**
+     * triggers an event that has already been bound
+     *
+     * @param {string} keys
+     * @param {string=} action
+     * @returns void
+     */
+    Mousetrap.prototype.trigger = function(keys, action) {
+        var self = this;
+        if (self._directMap[keys + ':' + action]) {
+            self._directMap[keys + ':' + action]({}, keys);
+        }
+        return self;
+    };
+
+    /**
+     * resets the library back to its initial state.  this is useful
+     * if you want to clear out the current keyboard shortcuts and bind
+     * new ones - for example if you switch to another page
+     *
+     * @returns void
+     */
+    Mousetrap.prototype.reset = function() {
+        var self = this;
+        self._callbacks = {};
+        self._directMap = {};
+        return self;
+    };
+
+    /**
+     * should we stop this event before firing off callbacks
+     *
+     * @param {Event} e
+     * @param {Element} element
+     * @return {boolean}
+     */
+    Mousetrap.prototype.stopCallback = function(e, element) {
+        var self = this;
+
+        // if the element has the class "mousetrap" then no need to stop
+        if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
+            return false;
+        }
+
+        if (_belongsTo(element, self.target)) {
+            return false;
+        }
+
+        // stop for input, select, and textarea
+        return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
+    };
+
+    /**
+     * exposes _handleKey publicly so it can be overwritten by extensions
+     */
+    Mousetrap.prototype.handleKey = function() {
+        var self = this;
+        return self._handleKey.apply(self, arguments);
+    };
+
+    /**
+     * Init the global mousetrap functions
+     *
+     * This method is needed to allow the global mousetrap functions to work
+     * now that mousetrap is a constructor function.
+     */
+    Mousetrap.init = function() {
+        var documentMousetrap = Mousetrap(document);
+        for (var method in documentMousetrap) {
+            if (method.charAt(0) !== '_') {
+                Mousetrap[method] = (function(method) {
+                    return function() {
+                        return documentMousetrap[method].apply(documentMousetrap, arguments);
+                    };
+                } (method));
+            }
+        }
+    };
+
+    Mousetrap.init();
+
+    // expose mousetrap to the global object
+    window.Mousetrap = Mousetrap;
+
+    // expose as a common js module
+    if (typeof module !== 'undefined' && module.exports) {
+        module.exports = Mousetrap;
+    }
+
+    // expose mousetrap as an AMD module
+    if (typeof define === 'function' && define.amd) {
+        define(function() {
+            return Mousetrap;
+        });
+    }
+}) (window, document);
+
 /*!
  * mustache.js - Logic-less {{mustache}} templates with JavaScript
  * http://github.com/janl/mustache.js
index 3d81e669878f3beb8aa9d68fc1fd5cf9a60ee379..83ad047d9bfc960d96554de415651a45e4750b0f 100644 (file)
@@ -377,10 +377,172 @@ var FlowyDocModel = Backbone.Collection.extend({
     comparator: 'id',
 });
 
+// TODO: None of this code has ever been run and doesn't work
+// TODO: More documentation
+
+// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
+// firstShortcut.rebind("Control + m");
+// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
+// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
+// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
+
+var Shortcut = (function(document, _) {
+    var globalMousetrap = new Mousetrap();
+    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
+    var Shortcut = {
+        globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
+        globalMousetrap: globalMousetrap,
+        boundObjects: [globalObject],
+        shortcuts: [],
+        registerShortcut: function(shortcut) {
+            if (arguments.length > 1) {
+                shortcut = {
+                    keybinding: arguments[0],
+                    action: arguments[1],
+                    object: arguments[2],
+                };
+            }
+            var manager = this;
+            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
+            _.defaults(shortcut, {
+                register: function() { manager.registerShortcut(this); },
+                unregister: function() { manager.unregisterShortcut(this); },
+                bind: function(object) { manager.bindShortcut(this, object); },
+                unbind: function(object) { manager.unbindShortcut(this, object); },
+                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
+                object: "global",
+                keybinding: "",
+                action: function() {},
+                boundObjects: [],
+                ownedByUs: true
+            });
+            this.shortcuts.push(shortcut);
+            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
+            return shortcut;
+        },
+        unregisterShortcut: function(shortcut) {
+            _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method
+        },
+        bindShortcut: function(shortcut, object) { // Do not call directly
+            object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
+            shortcut.boundObjects.push(object);
+        },
+        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
+            if (!object){
+                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
+            }
+            object.mousetrap.unbind(shortcut.keybinding);
+            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
+        },
+        rebindShortcut: function(shortcut, keybinding) {
+            // TODO: figure out a way to do this with 'shorcut.keybinding=' binding
+            if (keybinding === shortcut.keybinding) return;
+            var oldKeybinding = shortcut.keybinding;
+            var objects = shortcut.boundObjects;
+            shortcut.unbind();
+            shorcut.keybinding = keybinding;
+            _.each(objects, shortcut.bind, shortcut);
+            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
+        },
+        userRebindShortcut: function(shortcut, options) {
+            // Cancels on focus loss or if the user presses the escape key
+            options = _.defaults({}, options, {
+                element: undefined, // If the user clicks outside this element, cancel
+                onCancel: function() {},
+            });
+            var manager = this;
+            var cancelled = false;
+            var cancel = function() {
+                if (!cancelled) {
+                    cancelled = true;
+                    if (options.onCancel) options.onCancel();
+                }
+            };
+            if (element) element.addEventListener("blur", cancel);
+            Mousetrap.record(function (keybinding) { // TODO: requires plugin
+                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
+                    cancel();
+                } else {
+                    manager.rebindShortcut(shortcut, keybinding);
+                    if (element) element.removeEventListener("blur", cancel);
+                }
+            });
+        },
+        shortcutPressed: function(shortcut, object) {
+            if (shortcut.action) shortcut.action(object, object, shortcut);
+        },
+        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
+            // NOTE: You may want to hook into this if you want to save/load user preferences
+            // shortcut.id is probably what you want and not the whole shortcut
+        },
+        addObject: function(element, type) {
+            var manager = this;
+            object = {
+                element:element,
+                type:type,
+                mousetrap: new Mousetrap(element),
+                remove: function() { manager.removeObject(this); }
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return object;
+        },
+        removeObject: function(object) {
+            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
+            this.boundObjects = _.without(this.boundObjects, object);
+        },
+        changeGlobalObject: function(newObject) {
+            // TODO: Figure out how to do this with 'this.globalObject=' binding
+            this.removeObject(this.globalObject, "global");
+            this.globalObject = {
+                element: element,
+                type: type,
+                isGlobal: true,
+                mousetrap: this.globalMousetrap,
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return this.globalObject;
+        },
+        pause: function() { // TODO: These require plugin, test
+            this.boundObjects.each(function(object) {
+                object.mousetrap.pause();
+            });
+        },
+        unpause: function() {
+            this.boundObjects.each(function(object) {
+                object.mousetrap.unpause();
+            });
+        },
+        displayShortcuts: function(shortcuts, options) {
+            options = _.defaults({}, options, {
+                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
+                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
+                allowRebind: false, // Insert javascript to allow rebinding shortcuts
+                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
+            });
+            if (!shortcuts) shortcuts = this.shortcuts;
+            if (options.objectGrouping === "default") {
+                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
+                options.objectGrouping = multipleObjectTypes;
+            }
+            // TODO: Return HTML
+            return "";
+        },
+        _displayKeybinding: function(keybinding, options) {
+            options = _.defaults({}, options, { displayMultiple: true });
+            // TODO: Return HTML
+            return "";
+        },
+    };
+    return Shortcut;
+})(document, _);
+
 /**
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
+ * @depend ../library/shortcut.js
  */
 
 var testTodos = [
@@ -502,6 +664,11 @@ var AppView = Backbone.View.extend({
     },
 });
 
+// DEBUGGING
+window.Backbone = Backbone;
+window.Shortcut = Shortcut;
+window.AppView = AppView;
+
 /**
  * @depend views/app.js
  */
index 1d2033a637993512d5419b35debe2d9802486ecd..1c1923078554e34547d07ed4c3f9511f6e51c0eb 100644 (file)
@@ -376,10 +376,172 @@ var FlowyDocModel = Backbone.Collection.extend({
     comparator: 'id',
 });
 
+// TODO: None of this code has ever been run and doesn't work
+// TODO: More documentation
+
+// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
+// firstShortcut.rebind("Control + m");
+// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
+// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
+// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
+
+var Shortcut = (function(document, _) {
+    var globalMousetrap = new Mousetrap();
+    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
+    var Shortcut = {
+        globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
+        globalMousetrap: globalMousetrap,
+        boundObjects: [globalObject],
+        shortcuts: [],
+        registerShortcut: function(shortcut) {
+            if (arguments.length > 1) {
+                shortcut = {
+                    keybinding: arguments[0],
+                    action: arguments[1],
+                    object: arguments[2],
+                };
+            }
+            var manager = this;
+            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
+            _.defaults(shortcut, {
+                register: function() { manager.registerShortcut(this); },
+                unregister: function() { manager.unregisterShortcut(this); },
+                bind: function(object) { manager.bindShortcut(this, object); },
+                unbind: function(object) { manager.unbindShortcut(this, object); },
+                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
+                object: "global",
+                keybinding: "",
+                action: function() {},
+                boundObjects: [],
+                ownedByUs: true
+            });
+            this.shortcuts.push(shortcut);
+            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
+            return shortcut;
+        },
+        unregisterShortcut: function(shortcut) {
+            _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method
+        },
+        bindShortcut: function(shortcut, object) { // Do not call directly
+            object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
+            shortcut.boundObjects.push(object);
+        },
+        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
+            if (!object){
+                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
+            }
+            object.mousetrap.unbind(shortcut.keybinding);
+            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
+        },
+        rebindShortcut: function(shortcut, keybinding) {
+            // TODO: figure out a way to do this with 'shorcut.keybinding=' binding
+            if (keybinding === shortcut.keybinding) return;
+            var oldKeybinding = shortcut.keybinding;
+            var objects = shortcut.boundObjects;
+            shortcut.unbind();
+            shorcut.keybinding = keybinding;
+            _.each(objects, shortcut.bind, shortcut);
+            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
+        },
+        userRebindShortcut: function(shortcut, options) {
+            // Cancels on focus loss or if the user presses the escape key
+            options = _.defaults({}, options, {
+                element: undefined, // If the user clicks outside this element, cancel
+                onCancel: function() {},
+            });
+            var manager = this;
+            var cancelled = false;
+            var cancel = function() {
+                if (!cancelled) {
+                    cancelled = true;
+                    if (options.onCancel) options.onCancel();
+                }
+            };
+            if (element) element.addEventListener("blur", cancel);
+            Mousetrap.record(function (keybinding) { // TODO: requires plugin
+                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
+                    cancel();
+                } else {
+                    manager.rebindShortcut(shortcut, keybinding);
+                    if (element) element.removeEventListener("blur", cancel);
+                }
+            });
+        },
+        shortcutPressed: function(shortcut, object) {
+            if (shortcut.action) shortcut.action(object, object, shortcut);
+        },
+        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
+            // NOTE: You may want to hook into this if you want to save/load user preferences
+            // shortcut.id is probably what you want and not the whole shortcut
+        },
+        addObject: function(element, type) {
+            var manager = this;
+            object = {
+                element:element,
+                type:type,
+                mousetrap: new Mousetrap(element),
+                remove: function() { manager.removeObject(this); }
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return object;
+        },
+        removeObject: function(object) {
+            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
+            this.boundObjects = _.without(this.boundObjects, object);
+        },
+        changeGlobalObject: function(newObject) {
+            // TODO: Figure out how to do this with 'this.globalObject=' binding
+            this.removeObject(this.globalObject, "global");
+            this.globalObject = {
+                element: element,
+                type: type,
+                isGlobal: true,
+                mousetrap: this.globalMousetrap,
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return this.globalObject;
+        },
+        pause: function() { // TODO: These require plugin, test
+            this.boundObjects.each(function(object) {
+                object.mousetrap.pause();
+            });
+        },
+        unpause: function() {
+            this.boundObjects.each(function(object) {
+                object.mousetrap.unpause();
+            });
+        },
+        displayShortcuts: function(shortcuts, options) {
+            options = _.defaults({}, options, {
+                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
+                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
+                allowRebind: false, // Insert javascript to allow rebinding shortcuts
+                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
+            });
+            if (!shortcuts) shortcuts = this.shortcuts;
+            if (options.objectGrouping === "default") {
+                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
+                options.objectGrouping = multipleObjectTypes;
+            }
+            // TODO: Return HTML
+            return "";
+        },
+        _displayKeybinding: function(keybinding, options) {
+            options = _.defaults({}, options, { displayMultiple: true });
+            // TODO: Return HTML
+            return "";
+        },
+    };
+    return Shortcut;
+})(document, _);
+
 /**
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
+ * @depend ../library/shortcut.js
  */
 
 var testTodos = [
@@ -501,6 +663,11 @@ var AppView = Backbone.View.extend({
     },
 });
 
+// DEBUGGING
+window.Backbone = Backbone;
+window.Shortcut = Shortcut;
+window.AppView = AppView;
+
 /**
  * @depend views/app.js
  */
diff --git a/src/library/shortcut.js b/src/library/shortcut.js
new file mode 100644 (file)
index 0000000..6bd0860
--- /dev/null
@@ -0,0 +1,160 @@
+// TODO: None of this code has ever been run and doesn't work
+// TODO: More documentation
+
+// firstShortcut = Shortcut.registerShortcut("Control + k", function() { console.log("Shortcut pressed."); });
+// firstShortcut.rebind("Control + m");
+// Shortcut.registerShortcut({keybinding: "L", objectType: "textbox", action: function() { console.log("L pressed while inside the textbox."); }});
+// o = Shortcut.addObject(aTextbox, "textbox"); // Can come before or after keyboard binding
+// o.remove() // Once we lose textbox focus, stop keeping track of it, for example
+
+var Shortcut = (function(document, _) {
+    var globalMousetrap = new Mousetrap();
+    var globalObject = {element: document, type: "global", isGlobal: true, mousetrap: globalMousetrap};
+    var Shortcut = {
+        globalObject: globalObject, // The object passed to callbacks when a global keybinding is invoked.
+        globalMousetrap: globalMousetrap,
+        boundObjects: [globalObject],
+        shortcuts: [],
+        registerShortcut: function(shortcut) {
+            if (arguments.length > 1) {
+                shortcut = {
+                    keybinding: arguments[0],
+                    action: arguments[1],
+                    object: arguments[2],
+                };
+            }
+            var manager = this;
+            if (!shortcut.ownedByUs) shortcut = _.clone(shortcut);
+            _.defaults(shortcut, {
+                register: function() { manager.registerShortcut(this); },
+                unregister: function() { manager.unregisterShortcut(this); },
+                bind: function(object) { manager.bindShortcut(this, object); },
+                unbind: function(object) { manager.unbindShortcut(this, object); },
+                rebind: function(keybinding) { manager.rebindShortcut(this, keybinding); },
+                object: "global",
+                keybinding: "",
+                action: function() {},
+                boundObjects: [],
+                ownedByUs: true
+            });
+            this.shortcuts.push(shortcut);
+            _.chain(this.boundObjects).where({'type':shortcut.object}).each(_.partial(this.bindShortcut, shortcut, _), this).value();
+            return shortcut;
+        },
+        unregisterShortcut: function(shortcut) {
+            _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this); // TODO: This will die under mutation, find a safer 'each' method
+        },
+        bindShortcut: function(shortcut, object) { // Do not call directly
+            object.mousetrap.bind(shortcut.keybinding, _.bind(this.shortcutPressed, this, shortcut, object));
+            shortcut.boundObjects.push(object);
+        },
+        unbindShortcut: function(shortcut, object) { // Do not call directly with an object
+            if (!object){
+                _.each(shortcut.boundObjects, _.partial(this.unbindShortcut, shortcut, _), this);
+            }
+            object.mousetrap.unbind(shortcut.keybinding);
+            shortcut.boundObjects = _.without(shortcut.boundObjects, object);
+        },
+        rebindShortcut: function(shortcut, keybinding) {
+            // TODO: figure out a way to do this with 'shorcut.keybinding=' binding
+            if (keybinding === shortcut.keybinding) return;
+            var oldKeybinding = shortcut.keybinding;
+            var objects = shortcut.boundObjects;
+            shortcut.unbind();
+            shorcut.keybinding = keybinding;
+            _.each(objects, shortcut.bind, shortcut);
+            if (this.onRebindShortcut) this.onRebindShortcut(shortcut, keybinding, oldKeybinding);
+        },
+        userRebindShortcut: function(shortcut, options) {
+            // Cancels on focus loss or if the user presses the escape key
+            options = _.defaults({}, options, {
+                element: undefined, // If the user clicks outside this element, cancel
+                onCancel: function() {},
+            });
+            var manager = this;
+            var cancelled = false;
+            var cancel = function() {
+                if (!cancelled) {
+                    cancelled = true;
+                    if (options.onCancel) options.onCancel();
+                }
+            };
+            if (element) element.addEventListener("blur", cancel);
+            Mousetrap.record(function (keybinding) { // TODO: requires plugin
+                if (_.equal(keybinding, ["ESCAPE"])) { // TODO: Test
+                    cancel();
+                } else {
+                    manager.rebindShortcut(shortcut, keybinding);
+                    if (element) element.removeEventListener("blur", cancel);
+                }
+            });
+        },
+        shortcutPressed: function(shortcut, object) {
+            if (shortcut.action) shortcut.action(object, object, shortcut);
+        },
+        onRebindShortcut: function(shortcut, keybinding, oldKeybinding) {
+            // NOTE: You may want to hook into this if you want to save/load user preferences
+            // shortcut.id is probably what you want and not the whole shortcut
+        },
+        addObject: function(element, type) {
+            var manager = this;
+            object = {
+                element:element,
+                type:type,
+                mousetrap: new Mousetrap(element),
+                remove: function() { manager.removeObject(this); }
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(_.partial(this.bindShortcut, _, object), this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return object;
+        },
+        removeObject: function(object) {
+            _.each(this.shortcuts, _.partial(this.unbindShortcut, _, object), this);
+            this.boundObjects = _.without(this.boundObjects, object);
+        },
+        changeGlobalObject: function(newObject) {
+            // TODO: Figure out how to do this with 'this.globalObject=' binding
+            this.removeObject(this.globalObject, "global");
+            this.globalObject = {
+                element: element,
+                type: type,
+                isGlobal: true,
+                mousetrap: this.globalMousetrap,
+            };
+            _.chain(this.shortcuts).where({'object': type}).each(function(shortcut) { this.bindShortcut(shortcut, object); }, this).value();
+            if (!_.contains(this.boundObjects)) this.boundObjects.push(object);
+            return this.globalObject;
+        },
+        pause: function() { // TODO: These require plugin, test
+            this.boundObjects.each(function(object) {
+                object.mousetrap.pause();
+            });
+        },
+        unpause: function() {
+            this.boundObjects.each(function(object) {
+                object.mousetrap.unpause();
+            });
+        },
+        displayShortcuts: function(shortcuts, options) {
+            options = _.defaults({}, options, {
+                objectGrouping: "default", // Display one list, or group under headers by object type? "true" "false", or "default"
+                displayMultiple: true, // Display multiple shortcuts or just the canonical one?
+                allowRebind: false, // Insert javascript to allow rebinding shortcuts
+                highlightRepeats: true, // Warn about shortcuts which are bound to multiple actions (if user can remap shortcuts)
+            });
+            if (!shortcuts) shortcuts = this.shortcuts;
+            if (options.objectGrouping === "default") {
+                var multipleObjectTypes = _.chain(shortcuts).pluck('object').unique().count().value() > 1;
+                options.objectGrouping = multipleObjectTypes;
+            }
+            // TODO: Return HTML
+            return "";
+        },
+        _displayKeybinding: function(keybinding, options) {
+            options = _.defaults({}, options, { displayMultiple: true });
+            // TODO: Return HTML
+            return "";
+        },
+    };
+    return Shortcut;
+})(document, _);
index d8d900b796028cbc3bb37b863a9ee5c47aca8670..fba645158f21da9f4d3a43a3d181441d94fb6849 100644 (file)
@@ -2,6 +2,7 @@
  * @depend ../views/todo.js
  * @depend ../models/flowyDoc.js
  * @depend ../models/todo.js
+ * @depend ../library/shortcut.js
  */
 
 var testTodos = [
@@ -122,3 +123,8 @@ var AppView = Backbone.View.extend({
         this.list.each(this.addOne, this);
     },
 });
+
+// DEBUGGING
+window.Backbone = Backbone;
+window.Shortcut = Shortcut;
+window.AppView = AppView;