Add keyboard shortcuts back to d-editor

This commit is contained in:
Robin Ward 2015-11-03 14:01:07 -05:00
parent 4aa601414d
commit bb21902954
5 changed files with 727 additions and 679 deletions

View file

@ -1,18 +1,3 @@
/*global Mousetrap:true */
export default Ember.View.extend({ export default Ember.View.extend({
classNames: ['customize'], 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")
}); });

View file

@ -1,3 +1,4 @@
/*global Mousetrap:true */
import loadScript from 'discourse/lib/load-script'; import loadScript from 'discourse/lib/load-script';
import { default as property, on } from 'ember-addons/ember-computed-decorators'; import { default as property, on } from 'ember-addons/ember-computed-decorators';
import { showSelector } from "discourse/lib/emoji/emoji-toolbar"; import { showSelector } from "discourse/lib/emoji/emoji-toolbar";
@ -15,6 +16,8 @@ function getHead(head, prev) {
const _createCallbacks = []; const _createCallbacks = [];
function Toolbar() { function Toolbar() {
this.shortcuts = {};
this.groups = [ this.groups = [
{group: 'fontStyles', buttons: []}, {group: 'fontStyles', buttons: []},
{group: 'insertions', buttons: []}, {group: 'insertions', buttons: []},
@ -24,27 +27,31 @@ function Toolbar() {
this.addButton({ this.addButton({
id: 'bold', id: 'bold',
group: 'fontStyles', group: 'fontStyles',
shortcut: 'B',
perform: e => e.applySurround('**', '**', 'bold_text') perform: e => e.applySurround('**', '**', 'bold_text')
}); });
this.addButton({ this.addButton({
id: 'italic', id: 'italic',
group: 'fontStyles', group: 'fontStyles',
shortcut: 'I',
perform: e => e.applySurround('*', '*', 'italic_text') 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({ this.addButton({
id: 'quote', id: 'quote',
group: 'insertions', group: 'insertions',
icon: 'quote-right', icon: 'quote-right',
shortcut: 'Shift+9',
perform: e => e.applySurround('> ', '', 'code_text') perform: e => e.applySurround('> ', '', 'code_text')
}); });
this.addButton({ this.addButton({
id: 'code', id: 'code',
group: 'insertions', group: 'insertions',
shortcut: 'Shift+C',
perform(e) { perform(e) {
if (e.selected.value.indexOf("\n") !== -1) { if (e.selected.value.indexOf("\n") !== -1) {
e.applySurround(' ', '', 'code_text'); e.applySurround(' ', '', 'code_text');
@ -58,6 +65,7 @@ function Toolbar() {
id: 'bullet', id: 'bullet',
group: 'extras', group: 'extras',
icon: 'list-ul', icon: 'list-ul',
shortcut: 'Shift+8',
title: 'composer.ulist_title', title: 'composer.ulist_title',
perform: e => e.applyList('* ', 'list_item') perform: e => e.applyList('* ', 'list_item')
}); });
@ -66,6 +74,7 @@ function Toolbar() {
id: 'list', id: 'list',
group: 'extras', group: 'extras',
icon: 'list-ol', icon: 'list-ol',
shortcut: 'Shift+7',
title: 'composer.olist_title', title: 'composer.olist_title',
perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item') perform: e => e.applyList(i => !i ? "1. " : `${parseInt(i) + 1}. `, 'list_item')
}); });
@ -74,6 +83,7 @@ function Toolbar() {
id: 'heading', id: 'heading',
group: 'extras', group: 'extras',
icon: 'font', icon: 'font',
shortcut: 'Alt+1',
perform: e => e.applyList('## ', 'heading_text') perform: e => e.applyList('## ', 'heading_text')
}); });
@ -81,6 +91,7 @@ function Toolbar() {
id: 'rule', id: 'rule',
group: 'extras', group: 'extras',
icon: 'minus', icon: 'minus',
shortcut: 'Alt+R',
title: 'composer.hr_title', title: 'composer.hr_title',
perform: e => e.addText("\n\n----------\n") perform: e => e.addText("\n\n----------\n")
}); });
@ -92,15 +103,34 @@ Toolbar.prototype.addButton = function(button) {
throw `Couldn't find toolbar group ${button.group}`; throw `Couldn't find toolbar group ${button.group}`;
} }
const title = button.title || `composer.${button.id}_title`; const createdButton = {
g.buttons.push({
id: button.id, id: button.id,
className: button.className || button.id, className: button.className || button.id,
icon: button.icon || button.id, icon: button.icon || button.id,
action: button.action || 'toolbarButton', action: button.action || 'toolbarButton',
perform: button.perform || Ember.k, perform: button.perform || Ember.K
title };
});
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) { export function onToolbarCreate(func) {
@ -115,9 +145,24 @@ export default Ember.Component.extend({
lastSel: null, lastSel: null,
@on('didInsertElement') @on('didInsertElement')
_loadSanitizer() { _startUp() {
this._applyEmojiAutocomplete(); this._applyEmojiAutocomplete();
loadScript('defer/html-sanitizer-bundle').then(() => this.set('ready', true)); 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 @property

View file

@ -14,6 +14,7 @@ export default {
group: 'extras', group: 'extras',
icon: 'smile-o', icon: 'smile-o',
action: 'emoji', action: 'emoji',
shortcut: 'Alt+E',
title: 'composer.emoji' title: 'composer.emoji'
}); });
}); });

View file

@ -10,7 +10,7 @@
<div class='d-editor-button-bar'> <div class='d-editor-button-bar'>
{{#each toolbar.groups as |group|}} {{#each toolbar.groups as |group|}}
{{#each group.buttons as |b|}} {{#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}} {{/each}}
{{#unless group.lastGroup}} {{#unless group.lastGroup}}
<div class='d-editor-spacer'></div> <div class='d-editor-spacer'></div>

View file

@ -1,6 +1,6 @@
/*global define:false */ /*global define:false */
/** /**
* Copyright 2013 Craig Campbell * Copyright 2015 Craig Campbell
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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 * Mousetrap is a simple keyboard shortcut library for Javascript with
* no external dependencies * no external dependencies
* *
* @version 1.4.6 * @version 1.5.3
* @url craig.is/killing/mice * @url craig.is/killing/mice
*/ */
(function(window, document, undefined) { (function(window, document, undefined) {
@ -54,7 +54,7 @@
91: 'meta', 91: 'meta',
93: 'meta', 93: 'meta',
224: 'meta' 224: 'meta'
}, };
/** /**
* mapping for special characters so they can support * mapping for special characters so they can support
@ -64,7 +64,7 @@
* *
* @type {Object} * @type {Object}
*/ */
_KEYCODE_MAP = { var _KEYCODE_MAP = {
106: '*', 106: '*',
107: '+', 107: '+',
109: '-', 109: '-',
@ -81,7 +81,7 @@
220: '\\', 220: '\\',
221: ']', 221: ']',
222: '\'' 222: '\''
}, };
/** /**
* this is a mapping of keys that require shift on a US keypad * this is a mapping of keys that require shift on a US keypad
@ -93,7 +93,7 @@
* *
* @type {Object} * @type {Object}
*/ */
_SHIFT_MAP = { var _SHIFT_MAP = {
'~': '`', '~': '`',
'!': '1', '!': '1',
'@': '2', '@': '2',
@ -113,7 +113,7 @@
'>': '.', '>': '.',
'?': '/', '?': '/',
'|': '\\' '|': '\\'
}, };
/** /**
* this is a list of special strings you can use to map * this is a list of special strings you can use to map
@ -121,13 +121,14 @@
* *
* @type {Object} * @type {Object}
*/ */
_SPECIAL_ALIASES = { var _SPECIAL_ALIASES = {
'option': 'alt', 'option': 'alt',
'command': 'meta', 'command': 'meta',
'return': 'enter', 'return': 'enter',
'escape': 'esc', 'escape': 'esc',
'plus': '+',
'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl' 'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'
}, };
/** /**
* variable to store the flipped version of _MAP from above * variable to store the flipped version of _MAP from above
@ -136,58 +137,7 @@
* *
* @type {Object|undefined} * @type {Object|undefined}
*/ */
_REVERSE_MAP, var _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;
/** /**
* loop through the f keys, f1 to f19 and add them to the 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(','); 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 * takes a key event and figures out what the modifiers are
* *
@ -432,6 +285,306 @@
e.cancelBubble = true; 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 === null || 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 * actually calls the callback function
* *
@ -445,7 +598,7 @@
function _fireCallback(callback, e, combo, sequence) { function _fireCallback(callback, e, combo, sequence) {
// if this event should not happen stop here // if this event should not happen stop here
if (Mousetrap.stopCallback(e, e.target || e.srcElement, combo, sequence)) { if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) {
return; return;
} }
@ -463,12 +616,12 @@
* @param {Event} e * @param {Event} e
* @returns void * @returns void
*/ */
function _handleKey(character, modifiers, e) { self._handleKey = function(character, modifiers, e) {
var callbacks = _getMatches(character, modifiers, e), var callbacks = _getMatches(character, modifiers, e);
i, var i;
doNotReset = {}, var doNotReset = {};
maxLevel = 0, var maxLevel = 0;
processedSequenceCallback = false; var processedSequenceCallback = false;
// Calculate the maxLevel for sequences so we can only execute the longest callback sequence // Calculate the maxLevel for sequences so we can only execute the longest callback sequence
for (i = 0; i < callbacks.length; ++i) { for (i = 0; i < callbacks.length; ++i) {
@ -541,7 +694,7 @@
} }
_ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown'; _ignoreNextKeypress = processedSequenceCallback && e.type == 'keydown';
} };
/** /**
* handles a keydown event * handles a keydown event
@ -570,17 +723,7 @@
return; return;
} }
Mousetrap.handleKey(character, _eventModifiers(e), e); self.handleKey(character, _eventModifiers(e), e);
}
/**
* 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';
} }
/** /**
@ -596,55 +739,6 @@
_resetTimer = setTimeout(_resetSequences, 1000); _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
*
* @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;
}
/** /**
* binds a key sequence to an event * binds a key sequence to an event
* *
@ -713,70 +807,6 @@
} }
} }
/**
* Converts from a string key combination to an array
*
* @param {string} combination like "command+shift+l"
* @return {Array}
*/
function _keysFromString(combination) {
if (combination === '+') {
return ['+'];
}
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,
key,
i,
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
};
}
/** /**
* binds a single keyboard combination * binds a single keyboard combination
* *
@ -790,13 +820,13 @@
function _bindSingle(combination, callback, action, sequenceName, level) { function _bindSingle(combination, callback, action, sequenceName, level) {
// store a direct mapped reference for use with Mousetrap.trigger // store a direct mapped reference for use with Mousetrap.trigger
_directMap[combination + ':' + action] = callback; self._directMap[combination + ':' + action] = callback;
// make sure multiple spaces in a row become a single space // make sure multiple spaces in a row become a single space
combination = combination.replace(/\s+/g, ' '); combination = combination.replace(/\s+/g, ' ');
var sequence = combination.split(' '), var sequence = combination.split(' ');
info; var info;
// if this pattern is a sequence of keys then run through this method // if this pattern is a sequence of keys then run through this method
// to reprocess each pattern one key at a time // to reprocess each pattern one key at a time
@ -809,7 +839,7 @@
// make sure to initialize array if this is the first time // make sure to initialize array if this is the first time
// a callback is added for this key // a callback is added for this key
_callbacks[info.key] = _callbacks[info.key] || []; self._callbacks[info.key] = self._callbacks[info.key] || [];
// remove an existing match if there is one // remove an existing match if there is one
_getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level); _getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level);
@ -820,7 +850,7 @@
// //
// this is important because the way these are processed expects // this is important because the way these are processed expects
// the sequence ones to come first // the sequence ones to come first
_callbacks[info.key][sequenceName ? 'unshift' : 'push']({ self._callbacks[info.key][sequenceName ? 'unshift' : 'push']({
callback: callback, callback: callback,
modifiers: info.modifiers, modifiers: info.modifiers,
action: info.action, action: info.action,
@ -838,18 +868,17 @@
* @param {string|undefined} action * @param {string|undefined} action
* @returns void * @returns void
*/ */
function _bindMultiple(combinations, callback, action) { self._bindMultiple = function(combinations, callback, action) {
for (var i = 0; i < combinations.length; ++i) { for (var i = 0; i < combinations.length; ++i) {
_bindSingle(combinations[i], callback, action); _bindSingle(combinations[i], callback, action);
} }
} };
// start! // start!
_addEvent(document, 'keypress', _handleKeyEvent); _addEvent(targetElement, 'keypress', _handleKeyEvent);
_addEvent(document, 'keydown', _handleKeyEvent); _addEvent(targetElement, 'keydown', _handleKeyEvent);
_addEvent(document, 'keyup', _handleKeyEvent); _addEvent(targetElement, 'keyup', _handleKeyEvent);
}
var Mousetrap = {
/** /**
* binds an event to mousetrap * binds an event to mousetrap
@ -865,11 +894,12 @@
* @param {string=} action - 'keypress', 'keydown', or 'keyup' * @param {string=} action - 'keypress', 'keydown', or 'keyup'
* @returns void * @returns void
*/ */
bind: function(keys, callback, action) { Mousetrap.prototype.bind = function(keys, callback, action) {
var self = this;
keys = keys instanceof Array ? keys : [keys]; keys = keys instanceof Array ? keys : [keys];
_bindMultiple(keys, callback, action); self._bindMultiple.call(self, keys, callback, action);
return this; return self;
}, };
/** /**
* unbinds an event to mousetrap * unbinds an event to mousetrap
@ -888,9 +918,10 @@
* @param {string} action * @param {string} action
* @returns void * @returns void
*/ */
unbind: function(keys, action) { Mousetrap.prototype.unbind = function(keys, action) {
return Mousetrap.bind(keys, function() {}, action); var self = this;
}, return self.bind.call(self, keys, function() {}, action);
};
/** /**
* triggers an event that has already been bound * triggers an event that has already been bound
@ -899,12 +930,13 @@
* @param {string=} action * @param {string=} action
* @returns void * @returns void
*/ */
trigger: function(keys, action) { Mousetrap.prototype.trigger = function(keys, action) {
if (_directMap[keys + ':' + action]) { var self = this;
_directMap[keys + ':' + action]({}, keys); if (self._directMap[keys + ':' + action]) {
self._directMap[keys + ':' + action]({}, keys);
} }
return this; return self;
}, };
/** /**
* resets the library back to its initial state. this is useful * resets the library back to its initial state. this is useful
@ -913,11 +945,12 @@
* *
* @returns void * @returns void
*/ */
reset: function() { Mousetrap.prototype.reset = function() {
_callbacks = {}; var self = this;
_directMap = {}; self._callbacks = {};
return this; self._directMap = {};
}, return self;
};
/** /**
* should we stop this event before firing off callbacks * should we stop this event before firing off callbacks
@ -926,79 +959,63 @@
* @param {Element} element * @param {Element} element
* @return {boolean} * @return {boolean}
*/ */
stopCallback: function(e, element) { Mousetrap.prototype.stopCallback = function(e, element) {
var self = this;
// if the element has the class "mousetrap" then no need to stop // if the element has the class "mousetrap" then no need to stop
if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) { if ((' ' + element.className + ' ').indexOf(' mousetrap ') > -1) {
return false; return false;
} }
if (_belongsTo(element, self.target)) {
return false;
}
// stop for input, select, and textarea // stop for input, select, and textarea
return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable; return element.tagName == 'INPUT' || element.tagName == 'SELECT' || element.tagName == 'TEXTAREA' || element.isContentEditable;
}, };
/** /**
* exposes _handleKey publicly so it can be overwritten by extensions * exposes _handleKey publicly so it can be overwritten by extensions
*/ */
handleKey: _handleKey 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 // expose mousetrap to the global object
window.Mousetrap = Mousetrap; window.Mousetrap = Mousetrap;
// expose as a common js module
if (typeof module !== 'undefined' && module.exports) {
module.exports = Mousetrap;
}
// expose mousetrap as an AMD module // expose mousetrap as an AMD module
if (typeof define === 'function' && define.amd) { if (typeof define === 'function' && define.amd) {
define(Mousetrap); define(function() {
return Mousetrap;
});
} }
}) (window, document); }) (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);