diff --git a/app/assets/javascripts/admin/views/admin-customize.js.es6 b/app/assets/javascripts/admin/views/admin-customize.js.es6
index faca47026f6..240ed0ac73f 100644
--- a/app/assets/javascripts/admin/views/admin-customize.js.es6
+++ b/app/assets/javascripts/admin/views/admin-customize.js.es6
@@ -1,18 +1,3 @@
-/*global Mousetrap:true */
-
export default Ember.View.extend({
- classNames: ['customize'],
-
- _init: function() {
- var controller = this.get('controller');
- Mousetrap.bindGlobal('mod+s', function() {
- controller.send("save");
- return false;
- });
- }.on("didInsertElement"),
-
- _cleanUp: function() {
- Mousetrap.unbindGlobal('mod+s');
- }.on("willDestroyElement")
-
+ classNames: ['customize']
});
diff --git a/app/assets/javascripts/discourse/components/d-editor.js.es6 b/app/assets/javascripts/discourse/components/d-editor.js.es6
index 54f07dcd922..736d21bb64b 100644
--- a/app/assets/javascripts/discourse/components/d-editor.js.es6
+++ b/app/assets/javascripts/discourse/components/d-editor.js.es6
@@ -1,3 +1,4 @@
+/*global Mousetrap:true */
import loadScript from 'discourse/lib/load-script';
import { default as property, on } from 'ember-addons/ember-computed-decorators';
import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
@@ -15,6 +16,8 @@ function getHead(head, prev) {
const _createCallbacks = [];
function Toolbar() {
+ this.shortcuts = {};
+
this.groups = [
{group: 'fontStyles', buttons: []},
{group: 'insertions', buttons: []},
@@ -24,27 +27,31 @@ function Toolbar() {
this.addButton({
id: 'bold',
group: 'fontStyles',
+ shortcut: 'B',
perform: e => e.applySurround('**', '**', 'bold_text')
});
this.addButton({
id: 'italic',
group: 'fontStyles',
+ shortcut: 'I',
perform: e => e.applySurround('*', '*', 'italic_text')
});
- this.addButton({group: 'insertions', id: 'link', action: 'showLinkModal'});
+ this.addButton({id: 'link', group: 'insertions', shortcut: 'K', action: 'showLinkModal'});
this.addButton({
id: 'quote',
group: 'insertions',
icon: 'quote-right',
+ shortcut: 'Shift+9',
perform: e => e.applySurround('> ', '', 'code_text')
});
this.addButton({
id: 'code',
group: 'insertions',
+ shortcut: 'Shift+C',
perform(e) {
if (e.selected.value.indexOf("\n") !== -1) {
e.applySurround(' ', '', 'code_text');
@@ -58,6 +65,7 @@ function Toolbar() {
id: 'bullet',
group: 'extras',
icon: 'list-ul',
+ shortcut: 'Shift+8',
title: 'composer.ulist_title',
perform: e => e.applyList('* ', 'list_item')
});
@@ -66,6 +74,7 @@ function Toolbar() {
id: 'list',
group: 'extras',
icon: 'list-ol',
+ shortcut: 'Shift+7',
title: 'composer.olist_title',
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
});
@@ -74,6 +83,7 @@ function Toolbar() {
id: 'heading',
group: 'extras',
icon: 'font',
+ shortcut: 'Alt+1',
perform: e => e.applyList('## ', 'heading_text')
});
@@ -81,6 +91,7 @@ function Toolbar() {
id: 'rule',
group: 'extras',
icon: 'minus',
+ shortcut: 'Alt+R',
title: 'composer.hr_title',
perform: e => e.addText("\n\n----------\n")
});
@@ -92,15 +103,34 @@ Toolbar.prototype.addButton = function(button) {
throw `Couldn't find toolbar group ${button.group}`;
}
- const title = button.title || `composer.${button.id}_title`;
- g.buttons.push({
+ const createdButton = {
id: button.id,
className: button.className || button.id,
icon: button.icon || button.id,
action: button.action || 'toolbarButton',
- perform: button.perform || Ember.k,
- title
- });
+ perform: button.perform || Ember.K
+ };
+
+ const title = I18n.t(button.title || `composer.${button.id}_title`);
+ if (button.shortcut) {
+ const mac = /Mac|iPod|iPhone|iPad/.test(navigator.platform);
+ const mod = mac ? 'Meta' : 'Ctrl';
+ createdButton.title = `${title} (${mod}+${button.shortcut})`;
+
+ // Mac users are used to glyphs for shortcut keys
+ if (mac) {
+ createdButton.title = createdButton.title.replace('Shift', "\u{21E7}")
+ .replace('Meta', "\u{2318}")
+ .replace('Alt', "\u{2325}")
+ .replace(/\+/g, '');
+ }
+
+ this.shortcuts[`${mod}+${button.shortcut}`.toLowerCase()] = createdButton;
+ } else {
+ createdButton.title = title;
+ }
+
+ g.buttons.push(createdButton);
};
export function onToolbarCreate(func) {
@@ -115,9 +145,24 @@ export default Ember.Component.extend({
lastSel: null,
@on('didInsertElement')
- _loadSanitizer() {
+ _startUp() {
this._applyEmojiAutocomplete();
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true));
+
+ const shortcuts = this.get('toolbar.shortcuts');
+ Ember.keys(shortcuts).forEach(sc => {
+ const button = shortcuts[sc];
+ Mousetrap(this.$('.d-editor-input')[0]).bind(sc, () => {
+ this.send(button.action, button);
+ });
+ });
+ },
+
+ @on('willDestroyElement')
+ _shutDown() {
+ Ember.keys(this.get('toolbar.shortcuts')).forEach(sc => {
+ Mousetrap(this.$('.d-editor-input')[0]).unbind(sc);
+ });
},
@property
diff --git a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6 b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6
index d5204d1729e..5f23126b776 100644
--- a/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6
+++ b/app/assets/javascripts/discourse/initializers/enable-emoji.js.es6
@@ -14,6 +14,7 @@ export default {
group: 'extras',
icon: 'smile-o',
action: 'emoji',
+ shortcut: 'Alt+E',
title: 'composer.emoji'
});
});
diff --git a/app/assets/javascripts/discourse/templates/components/d-editor.hbs b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
index ca71460686d..5375d94c188 100644
--- a/app/assets/javascripts/discourse/templates/components/d-editor.hbs
+++ b/app/assets/javascripts/discourse/templates/components/d-editor.hbs
@@ -10,7 +10,7 @@
{{#each toolbar.groups as |group|}}
{{#each group.buttons as |b|}}
- {{d-button action=b.action actionParam=b title=b.title icon=b.icon class=b.className}}
+ {{d-button action=b.action actionParam=b translatedTitle=b.title icon=b.icon class=b.className}}
{{/each}}
{{#unless group.lastGroup}}
diff --git a/vendor/assets/javascripts/mousetrap.js b/vendor/assets/javascripts/mousetrap.js
index 108216bd71e..48d2f625c23 100644
--- a/vendor/assets/javascripts/mousetrap.js
+++ b/vendor/assets/javascripts/mousetrap.js
@@ -1,6 +1,6 @@
/*global define:false */
/**
- * Copyright 2013 Craig Campbell
+ * 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.
@@ -17,7 +17,7 @@
* Mousetrap is a simple keyboard shortcut library for Javascript with
* no external dependencies
*
- * @version 1.4.6
+ * @version 1.5.3
* @url craig.is/killing/mice
*/
(function(window, document, undefined) {
@@ -32,162 +32,112 @@
* @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'
- },
+ 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}
- */
- _KEYCODE_MAP = {
- 106: '*',
- 107: '+',
- 109: '-',
- 110: '.',
- 111 : '/',
- 186: ';',
- 187: '=',
- 188: ',',
- 189: '-',
- 190: '.',
- 191: '/',
- 192: '`',
- 219: '[',
- 220: '\\',
- 221: ']',
- 222: '\''
- },
+ /**
+ * 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}
- */
- _SHIFT_MAP = {
- '~': '`',
- '!': '1',
- '@': '2',
- '#': '3',
- '$': '4',
- '%': '5',
- '^': '6',
- '&': '7',
- '*': '8',
- '(': '9',
- ')': '0',
- '_': '-',
- '+': '=',
- ':': ';',
- '\"': '\'',
- '<': ',',
- '>': '.',
- '?': '/',
- '|': '\\'
- },
+ /**
+ * 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}
- */
- _SPECIAL_ALIASES = {
- 'option': 'alt',
- 'command': 'meta',
- 'return': 'enter',
- 'escape': 'esc',
- 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
- },
+ /**
+ * 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}
- */
- _REVERSE_MAP,
-
- /**
- * a list of all the callbacks setup via Mousetrap.bind()
- *
- * @type {Object}
- */
- _callbacks = {},
-
- /**
- * direct map of string combinations to callbacks used for trigger()
- *
- * @type {Object}
- */
- _directMap = {},
-
- /**
- * keeps track of what level each sequence is at since multiple
- * sequences can start out with the same sequence
- *
- * @type {Object}
- */
- _sequenceLevels = {},
-
- /**
- * variable to store the setTimeout call
- *
- * @type {null|number}
- */
- _resetTimer,
-
- /**
- * temporary state where we will ignore the next keyup
- *
- * @type {boolean|string}
- */
- _ignoreNextKeyup = false,
-
- /**
- * temporary state where we will ignore the next keypress
- *
- * @type {boolean}
- */
- _ignoreNextKeypress = false,
-
- /**
- * are we currently inside of a sequence?
- * type of action ("keyup" or "keydown" or "keypress") or false
- *
- * @type {boolean|string}
- */
- _nextExpectedAction = false;
+ /**
+ * 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
@@ -277,103 +227,6 @@
return modifiers1.sort().join(',') === modifiers2.sort().join(',');
}
- /**
- * 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,
- callback,
- matches = [],
- action = e.type;
-
- // if there are no events related to this keycode
- if (!_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 < _callbacks[character].length; ++i) {
- callback = _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) {
- _callbacks[character].splice(i, 1);
- }
-
- matches.push(callback);
- }
- }
-
- return matches;
- }
-
/**
* takes a key event and figures out what the modifiers are
*
@@ -432,147 +285,6 @@
e.cancelBubble = true;
}
- /**
- * 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 (Mousetrap.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
- */
- function _handleKey(character, modifiers, e) {
- var callbacks = _getMatches(character, modifiers, e),
- i,
- doNotReset = {},
- maxLevel = 0,
- processedSequenceCallback = false;
-
- // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
- for (i = 0; i < callbacks.length; ++i) {
- if (callbacks[i].seq) {
- maxLevel = Math.max(maxLevel, callbacks[i].level);
- }
- }
-
- // loop through matching callbacks for this key event
- for (i = 0; i < callbacks.length; ++i) {
-
- // fire for all sequence callbacks
- // this is because if for example you have multiple sequences
- // bound such as "g i" and "g t" they both need to fire the
- // callback for matching g cause otherwise you can only ever
- // match the first one
- if (callbacks[i].seq) {
-
- // 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;
- }
-
- Mousetrap.handleKey(character, _eventModifiers(e), e);
- }
-
/**
* determines if the keycode specified is a modifier key or not
*
@@ -583,19 +295,6 @@
return key == 'shift' || key == 'ctrl' || key == 'alt' || key == 'meta';
}
- /**
- * 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);
- }
-
/**
* reverses the map lookup so that we can look for specific keys
* to see what can and can't use keypress
@@ -645,74 +344,6 @@
return action;
}
- /**
- * 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);
- }
- }
-
/**
* Converts from a string key combination to an array
*
@@ -724,6 +355,7 @@
return ['+'];
}
+ combination = combination.replace(/\+{2}/g, '+plus');
return combination.split('+');
}
@@ -735,10 +367,10 @@
* @returns {Object}
*/
function _getKeyInfo(combination, action) {
- var keys,
- key,
- i,
- modifiers = [];
+ var keys;
+ var key;
+ var i;
+ var modifiers = [];
// take the keys from this pattern and figure out what the actual
// pattern is all about
@@ -777,228 +409,613 @@
};
}
- /**
- * binds a single keyboard combination
- *
- * @param {string} combination
- * @param {Function} callback
- * @param {string=} action
- * @param {string=} sequenceName - name of sequence if part of sequence
- * @param {number=} level - what part of the sequence the command is
- * @returns void
- */
- function _bindSingle(combination, callback, action, sequenceName, level) {
-
- // store a direct mapped reference for use with Mousetrap.trigger
- _directMap[combination + ':' + action] = callback;
-
- // make sure multiple spaces in a row become a single space
- combination = combination.replace(/\s+/g, ' ');
-
- var sequence = combination.split(' '),
- info;
-
- // if this pattern is a sequence of keys then run through this method
- // to reprocess each pattern one key at a time
- if (sequence.length > 1) {
- _bindSequence(combination, sequence, callback, action);
- return;
+ function _belongsTo(element, ancestor) {
+ if (element === null || element === document) {
+ return false;
}
- info = _getKeyInfo(combination, action);
-
- // make sure to initialize array if this is the first time
- // a callback is added for this key
- _callbacks[info.key] = _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
- _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
- */
- function _bindMultiple(combinations, callback, action) {
- for (var i = 0; i < combinations.length; ++i) {
- _bindSingle(combinations[i], callback, action);
+ if (element === ancestor) {
+ return true;
}
+
+ return _belongsTo(element.parentNode, ancestor);
}
- // start!
- _addEvent(document, 'keypress', _handleKeyEvent);
- _addEvent(document, 'keydown', _handleKeyEvent);
- _addEvent(document, 'keyup', _handleKeyEvent);
+ function Mousetrap(targetElement) {
+ var self = this;
- var Mousetrap = {
+ targetElement = targetElement || document;
+
+ if (!(self instanceof Mousetrap)) {
+ return new Mousetrap(targetElement);
+ }
/**
- * binds an event to mousetrap
+ * element to attach key events to
*
- * can be a single key, a combination of keys separated with +,
- * an array of keys, or a sequence of keys separated by spaces
+ * @type {Element}
+ */
+ self.target = targetElement;
+
+ /**
+ * a list of all the callbacks setup via Mousetrap.bind()
*
- * 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)
+ * @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 {string|Array} keys
* @param {Function} callback
- * @param {string=} action - 'keypress', 'keydown', or 'keyup'
+ * @param {Event} e
* @returns void
*/
- bind: function(keys, callback, action) {
- keys = keys instanceof Array ? keys : [keys];
- _bindMultiple(keys, callback, action);
- return this;
- },
+ 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);
+ }
+ }
/**
- * unbinds an event to mousetrap
+ * handles a character key event
*
- * 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
+ * @param {string} character
+ * @param {Array} modifiers
+ * @param {Event} e
* @returns void
*/
- unbind: function(keys, action) {
- return Mousetrap.bind(keys, function() {}, action);
- },
+ 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';
+ };
/**
- * triggers an event that has already been bound
+ * handles a keydown event
*
- * @param {string} keys
+ * @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
*/
- trigger: function(keys, action) {
- if (_directMap[keys + ':' + action]) {
- _directMap[keys + ':' + action]({}, keys);
+ 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();
+ };
}
- return this;
- },
+
+ /**
+ * 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);
+ }
+ }
/**
- * 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
+ * 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
*/
- reset: function() {
- _callbacks = {};
- _directMap = {};
- return this;
- },
+ function _bindSingle(combination, callback, action, sequenceName, level) {
- /**
- * should we stop this event before firing off callbacks
- *
- * @param {Event} e
- * @param {Element} element
- * @return {boolean}
- */
- stopCallback: function(e, element) {
+ // store a direct mapped reference for use with Mousetrap.trigger
+ self._directMap[combination + ':' + action] = callback;
- // if the element has the class "mousetrap" then no need to stop
- if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
- return false;
+ // 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;
}
- // stop for input, select, and textarea
- return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
- },
+ 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
+ });
+ }
/**
- * exposes _handleKey publicly so it can be overwritten by extensions
+ * binds multiple combinations to the same callback
+ *
+ * @param {Array} combinations
+ * @param {Function} callback
+ * @param {string|undefined} action
+ * @returns void
*/
- handleKey: _handleKey
+ 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(Mousetrap);
+ define(function() {
+ return Mousetrap;
+ });
}
}) (window, document);
-
-/**
- * adds a bindGlobal method to Mousetrap that allows you to
- * bind specific keyboard shortcuts that will still work
- * inside a text input field
- *
- * usage:
- * Mousetrap.bindGlobal('ctrl+s', _saveChanges);
- * Mousetrap.unbindGlobal('ctrl+s');
- */
-/* global Mousetrap:true */
-Mousetrap = (function(Mousetrap) {
- var _globalCallbacks = {},
- _originalStopCallback = Mousetrap.stopCallback;
-
- Mousetrap.stopCallback = function(e, element, combo, sequence) {
- if (_globalCallbacks[combo] || _globalCallbacks[sequence]) {
- return false;
- }
-
- return _originalStopCallback(e, element, combo);
- };
-
- Mousetrap.bindGlobal = function(keys, callback, action) {
- Mousetrap.bind(keys, callback, action);
-
- if (keys instanceof Array) {
- for (var i = 0; i < keys.length; i++) {
- _globalCallbacks[keys[i]] = true;
- }
- return;
- }
-
- _globalCallbacks[keys] = true;
- };
-
- Mousetrap.unbindGlobal = function(keys, action) {
- Mousetrap.unbind(keys, action);
-
- if (keys instanceof Array) {
- for (var i = 0; i < keys.length; i++) {
- _globalCallbacks[keys[i]] = false;
- }
- return;
- }
-
- _globalCallbacks[keys] = false;
- };
-
- return Mousetrap;
-}) (Mousetrap);