├── .editorconfig ├── .gitignore ├── .travis.yml ├── .zuul.yml ├── COPYRIGHT ├── Combokeys ├── index.js ├── prototype │ ├── addEvents.js │ ├── bind.js │ ├── bindMultiple.js │ ├── bindSequence.js │ ├── bindSingle.js │ ├── detach.js │ ├── dom-event.js │ ├── fireCallback.js │ ├── getKeyInfo.js │ ├── getMatches.js │ ├── getReverseMap.js │ ├── handleKey.js │ ├── handleKeyEvent.js │ ├── modifiersMatch.js │ ├── pickBestAction.js │ ├── reset.js │ ├── resetSequenceTimer.js │ ├── resetSequences.js │ ├── stopCallback.js │ ├── trigger.js │ └── unbind.js └── reset.js ├── LICENSE.txt ├── README.md ├── helpers ├── characterFromEvent.js ├── eventModifiers.js ├── isModifier.js ├── keysFromString.js ├── preventDefault.js ├── shift-map.js ├── special-aliases.js ├── special-characters-map.js ├── special-keys-map.js └── stopPropagation.js ├── package-lock.json ├── package.json ├── plugins ├── bind-dictionary │ ├── README.md │ └── index.js ├── global-bind │ ├── README.md │ └── index.js ├── pause │ ├── README.md │ └── index.js └── record │ ├── README.md │ └── index.js └── test ├── bind.js ├── constructor.js ├── detach.js ├── helpers └── make-element.js ├── index.js ├── initialization.js ├── lib └── key-event.js ├── plugins ├── bind-dictionary.js ├── global-bind.js ├── pause.js └── record.js └── unbind.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = space 9 | 10 | [*.{js,json}] 11 | indent_size = 2 12 | 13 | [*.{yml}] 14 | indent_size = 2 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | dist/**/* 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | -------------------------------------------------------------------------------- /.zuul.yml: -------------------------------------------------------------------------------- 1 | ui: mocha-bdd 2 | 3 | browsers: 4 | - name: chrome 5 | version: latest 6 | - name: safari: 7 | version: latest 8 | - name: firefox 9 | version: latest 10 | - name: ie 11 | version: 6..latest 12 | - name: opera 13 | version: latest 14 | - name: iphone 15 | version: latest 16 | - name: ipad 17 | version: latest 18 | -------------------------------------------------------------------------------- /COPYRIGHT: -------------------------------------------------------------------------------- 1 | Copyright 2013 Craig Campbell 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /Combokeys/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | module.exports = function (element, options) { 5 | var self = this 6 | var Combokeys = self.constructor 7 | 8 | /** 9 | * an object of passed options 10 | * 11 | * @type { storeInstancesGlobally?: true } 12 | */ 13 | 14 | self.options = Object.assign({ storeInstancesGlobally: true }, options || {}) 15 | 16 | /** 17 | * a list of all the callbacks setup via Combokeys.bind() 18 | * 19 | * @type {Object} 20 | */ 21 | self.callbacks = {} 22 | 23 | /** 24 | * direct map of string combinations to callbacks used for trigger() 25 | * 26 | * @type {Object} 27 | */ 28 | self.directMap = {} 29 | 30 | /** 31 | * keeps track of what level each sequence is at since multiple 32 | * sequences can start out with the same sequence 33 | * 34 | * @type {Object} 35 | */ 36 | self.sequenceLevels = {} 37 | 38 | /** 39 | * variable to store the setTimeout call 40 | * 41 | * @type {null|number} 42 | */ 43 | self.resetTimer = null 44 | 45 | /** 46 | * temporary state where we will ignore the next keyup 47 | * 48 | * @type {boolean|string} 49 | */ 50 | self.ignoreNextKeyup = false 51 | 52 | /** 53 | * temporary state where we will ignore the next keypress 54 | * 55 | * @type {boolean} 56 | */ 57 | self.ignoreNextKeypress = false 58 | 59 | /** 60 | * are we currently inside of a sequence? 61 | * type of action ("keyup" or "keydown" or "keypress") or false 62 | * 63 | * @type {boolean|string} 64 | */ 65 | self.nextExpectedAction = false 66 | 67 | self.element = element 68 | 69 | self.addEvents() 70 | 71 | if (self.options.storeInstancesGlobally) { 72 | Combokeys.instances.push(self) 73 | } 74 | 75 | return self 76 | } 77 | 78 | module.exports.prototype.bind = require('./prototype/bind') 79 | module.exports.prototype.bindMultiple = require('./prototype/bindMultiple') 80 | module.exports.prototype.unbind = require('./prototype/unbind') 81 | module.exports.prototype.trigger = require('./prototype/trigger') 82 | module.exports.prototype.reset = require('./prototype/reset.js') 83 | module.exports.prototype.stopCallback = require('./prototype/stopCallback') 84 | module.exports.prototype.handleKey = require('./prototype/handleKey') 85 | module.exports.prototype.addEvents = require('./prototype/addEvents') 86 | module.exports.prototype.bindSingle = require('./prototype/bindSingle') 87 | module.exports.prototype.getKeyInfo = require('./prototype/getKeyInfo') 88 | module.exports.prototype.pickBestAction = require('./prototype/pickBestAction') 89 | module.exports.prototype.getReverseMap = require('./prototype/getReverseMap') 90 | module.exports.prototype.getMatches = require('./prototype/getMatches') 91 | module.exports.prototype.resetSequences = require('./prototype/resetSequences') 92 | module.exports.prototype.fireCallback = require('./prototype/fireCallback') 93 | module.exports.prototype.bindSequence = require('./prototype/bindSequence') 94 | module.exports.prototype.resetSequenceTimer = require('./prototype/resetSequenceTimer') 95 | module.exports.prototype.detach = require('./prototype/detach') 96 | 97 | module.exports.instances = [] 98 | module.exports.reset = require('./reset') 99 | 100 | /** 101 | * variable to store the flipped version of MAP from above 102 | * needed to check if we should use keypress or not when no action 103 | * is specified 104 | * 105 | * @type {Object|undefined} 106 | */ 107 | module.exports.REVERSE_MAP = null 108 | -------------------------------------------------------------------------------- /Combokeys/prototype/addEvents.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | module.exports = function () { 4 | var self = this 5 | var on = require('./dom-event') 6 | var element = self.element 7 | 8 | self.eventHandler = require('./handleKeyEvent').bind(self) 9 | 10 | on(element, 'keypress', self.eventHandler) 11 | on(element, 'keydown', self.eventHandler) 12 | on(element, 'keyup', self.eventHandler) 13 | } 14 | -------------------------------------------------------------------------------- /Combokeys/prototype/bind.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | /** 4 | * binds an event to Combokeys 5 | * 6 | * can be a single key, a combination of keys separated with +, 7 | * an array of keys, or a sequence of keys separated by spaces 8 | * 9 | * be sure to list the modifier keys first to make sure that the 10 | * correct key ends up getting bound (the last key in the pattern) 11 | * 12 | * @param {string|Array} keys 13 | * @param {Function} callback 14 | * @param {string=} action - "keypress", "keydown", or "keyup" 15 | * @returns void 16 | */ 17 | module.exports = function (keys, callback, action) { 18 | var self = this 19 | 20 | keys = keys instanceof Array ? keys : [keys] 21 | self.bindMultiple(keys, callback, action) 22 | return self 23 | } 24 | -------------------------------------------------------------------------------- /Combokeys/prototype/bindMultiple.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * binds multiple combinations to the same callback 6 | * 7 | * @param {Array} combinations 8 | * @param {Function} callback 9 | * @param {string|undefined} action 10 | * @returns void 11 | */ 12 | module.exports = function (combinations, callback, action) { 13 | var self = this 14 | 15 | for (var j = 0; j < combinations.length; ++j) { 16 | self.bindSingle(combinations[j], callback, action) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Combokeys/prototype/bindSequence.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * binds a key sequence to an event 6 | * 7 | * @param {string} combo - combo specified in bind call 8 | * @param {Array} keys 9 | * @param {Function} callback 10 | * @param {string=} action 11 | * @returns void 12 | */ 13 | module.exports = function (combo, keys, callback, action) { 14 | var self = this 15 | 16 | // start off by adding a sequence level record for this combination 17 | // and setting the level to 0 18 | self.sequenceLevels[combo] = 0 19 | 20 | /** 21 | * callback to increase the sequence level for this sequence and reset 22 | * all other sequences that were active 23 | * 24 | * @param {string} nextAction 25 | * @returns {Function} 26 | */ 27 | function increaseSequence (nextAction) { 28 | return function () { 29 | self.nextExpectedAction = nextAction 30 | ++self.sequenceLevels[combo] 31 | self.resetSequenceTimer() 32 | } 33 | } 34 | 35 | /** 36 | * wraps the specified callback inside of another function in order 37 | * to reset all sequence counters as soon as this sequence is done 38 | * 39 | * @param {Event} e 40 | * @returns void 41 | */ 42 | function callbackAndReset (e) { 43 | var characterFromEvent 44 | self.fireCallback(callback, e, combo) 45 | 46 | // we should ignore the next key up if the action is key down 47 | // or keypress. this is so if you finish a sequence and 48 | // release the key the final key will not trigger a keyup 49 | if (action !== 'keyup') { 50 | characterFromEvent = require('../../helpers/characterFromEvent') 51 | self.ignoreNextKeyup = characterFromEvent(e) 52 | } 53 | 54 | // weird race condition if a sequence ends with the key 55 | // another sequence begins with 56 | setTimeout( 57 | function () { 58 | self.resetSequences() 59 | }, 60 | 10 61 | ) 62 | } 63 | 64 | // loop through keys one at a time and bind the appropriate callback 65 | // function. for any key leading up to the final one it should 66 | // increase the sequence. after the final, it should reset all sequences 67 | // 68 | // if an action is specified in the original bind call then that will 69 | // be used throughout. otherwise we will pass the action that the 70 | // next key in the sequence should match. this allows a sequence 71 | // to mix and match keypress and keydown events depending on which 72 | // ones are better suited to the key provided 73 | for (var j = 0; j < keys.length; ++j) { 74 | var isFinal = j + 1 === keys.length 75 | var wrappedCallback = isFinal ? callbackAndReset : increaseSequence(action || self.getKeyInfo(keys[j + 1]).action) 76 | self.bindSingle(keys[j], wrappedCallback, action, combo, j) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Combokeys/prototype/bindSingle.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * binds a single keyboard combination 6 | * 7 | * @param {string} combination 8 | * @param {Function} callback 9 | * @param {string=} action 10 | * @param {string=} sequenceName - name of sequence if part of sequence 11 | * @param {number=} level - what part of the sequence the command is 12 | * @returns void 13 | */ 14 | module.exports = function (combination, callback, action, sequenceName, level) { 15 | var self = this 16 | 17 | // store a direct mapped reference for use with Combokeys.trigger 18 | self.directMap[combination + ':' + action] = callback 19 | 20 | // make sure multiple spaces in a row become a single space 21 | combination = combination.replace(/\s+/g, ' ') 22 | 23 | var sequence = combination.split(' ') 24 | var info 25 | 26 | // if this pattern is a sequence of keys then run through this method 27 | // to reprocess each pattern one key at a time 28 | if (sequence.length > 1) { 29 | self.bindSequence(combination, sequence, callback, action) 30 | return 31 | } 32 | 33 | info = self.getKeyInfo(combination, action) 34 | 35 | // make sure to initialize array if this is the first time 36 | // a callback is added for this key 37 | self.callbacks[info.key] = self.callbacks[info.key] || [] 38 | 39 | // remove an existing match if there is one 40 | self.getMatches(info.key, info.modifiers, {type: info.action}, sequenceName, combination, level) 41 | 42 | // add this call back to the array 43 | // if it is a sequence put it at the beginning 44 | // if not put it at the end 45 | // 46 | // this is important because the way these are processed expects 47 | // the sequence ones to come first 48 | self.callbacks[info.key][sequenceName ? 'unshift' : 'push']({ 49 | callback: callback, 50 | modifiers: info.modifiers, 51 | action: info.action, 52 | seq: sequenceName, 53 | level: level, 54 | combo: combination 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /Combokeys/prototype/detach.js: -------------------------------------------------------------------------------- 1 | var off = require('./dom-event').off 2 | module.exports = function () { 3 | var self = this 4 | var element = self.element 5 | 6 | off(element, 'keypress', self.eventHandler) 7 | off(element, 'keydown', self.eventHandler) 8 | off(element, 'keyup', self.eventHandler) 9 | } 10 | -------------------------------------------------------------------------------- /Combokeys/prototype/dom-event.js: -------------------------------------------------------------------------------- 1 | module.exports = on 2 | module.exports.on = on 3 | module.exports.off = off 4 | 5 | function on (element, event, callback, capture) { 6 | !element.addEventListener && (event = 'on' + event) 7 | ;(element.addEventListener || element.attachEvent).call(element, event, callback, capture) 8 | return callback 9 | } 10 | 11 | function off (element, event, callback, capture) { 12 | !element.removeEventListener && (event = 'on' + event) 13 | ;(element.removeEventListener || element.detachEvent).call(element, event, callback, capture) 14 | return callback 15 | } 16 | -------------------------------------------------------------------------------- /Combokeys/prototype/fireCallback.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * actually calls the callback function 6 | * 7 | * if your callback function returns false this will use the jquery 8 | * convention - prevent default and stop propogation on the event 9 | * 10 | * @param {Function} callback 11 | * @param {Event} e 12 | * @returns void 13 | */ 14 | module.exports = function (callback, e, combo, sequence) { 15 | var self = this 16 | var preventDefault 17 | var stopPropagation 18 | 19 | // if this event should not happen stop here 20 | if (self.stopCallback(e, e.target || e.srcElement, combo, sequence)) { 21 | return 22 | } 23 | 24 | if (callback(e, combo) === false) { 25 | preventDefault = require('../../helpers/preventDefault') 26 | preventDefault(e) 27 | stopPropagation = require('../../helpers/stopPropagation') 28 | stopPropagation(e) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Combokeys/prototype/getKeyInfo.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * Gets info for a specific key combination 6 | * 7 | * @param {string} combination key combination ("command+s" or "a" or "*") 8 | * @param {string=} action 9 | * @returns {Object} 10 | */ 11 | module.exports = function (combination, action) { 12 | var self = this 13 | var keysFromString 14 | var keys 15 | var key 16 | var j 17 | var modifiers = [] 18 | var SPECIAL_ALIASES 19 | var SHIFT_MAP 20 | var isModifier 21 | 22 | keysFromString = require('../../helpers/keysFromString') 23 | // take the keys from this pattern and figure out what the actual 24 | // pattern is all about 25 | keys = keysFromString(combination) 26 | 27 | SPECIAL_ALIASES = require('../../helpers/special-aliases') 28 | SHIFT_MAP = require('../../helpers/shift-map') 29 | isModifier = require('../../helpers/isModifier') 30 | for (j = 0; j < keys.length; ++j) { 31 | key = keys[j] 32 | 33 | // normalize key names 34 | if (SPECIAL_ALIASES[key]) { 35 | key = SPECIAL_ALIASES[key] 36 | } 37 | 38 | // if this is not a keypress event then we should 39 | // be smart about using shift keys 40 | // this will only work for US keyboards however 41 | if (action && action !== 'keypress' && SHIFT_MAP[key]) { 42 | key = SHIFT_MAP[key] 43 | modifiers.push('shift') 44 | } 45 | 46 | // if this key is a modifier then add it to the list of modifiers 47 | if (isModifier(key)) { 48 | modifiers.push(key) 49 | } 50 | } 51 | 52 | // depending on what the key combination is 53 | // we will try to pick the best event for it 54 | action = self.pickBestAction(key, modifiers, action) 55 | 56 | return { 57 | key: key, 58 | modifiers: modifiers, 59 | action: action 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Combokeys/prototype/getMatches.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * finds all callbacks that match based on the keycode, modifiers, 6 | * and action 7 | * 8 | * @param {string} character 9 | * @param {Array} modifiers 10 | * @param {Event|Object} e 11 | * @param {string=} sequenceName - name of the sequence we are looking for 12 | * @param {string=} combination 13 | * @param {number=} level 14 | * @returns {Array} 15 | */ 16 | module.exports = function (character, modifiers, e, sequenceName, combination, level) { 17 | var self = this 18 | var j 19 | var callback 20 | var matches = [] 21 | var action = e.type 22 | var isModifier 23 | var modifiersMatch 24 | 25 | if ( 26 | action === 'keypress' && 27 | // Firefox fires keypress for arrows 28 | !(e.code && e.code.slice(0, 5) === 'Arrow') 29 | ) { 30 | // 'any-character' callbacks are only on `keypress` 31 | var anyCharCallbacks = self.callbacks['any-character'] || [] 32 | anyCharCallbacks.forEach(function (callback) { 33 | matches.push(callback) 34 | }) 35 | } 36 | 37 | if (!self.callbacks[character]) { return matches } 38 | 39 | isModifier = require('../../helpers/isModifier') 40 | // if a modifier key is coming up on its own we should allow it 41 | if (action === 'keyup' && isModifier(character)) { 42 | modifiers = [character] 43 | } 44 | 45 | // loop through all callbacks for the key that was pressed 46 | // and see if any of them match 47 | for (j = 0; j < self.callbacks[character].length; ++j) { 48 | callback = self.callbacks[character][j] 49 | 50 | // if a sequence name is not specified, but this is a sequence at 51 | // the wrong level then move onto the next match 52 | if (!sequenceName && callback.seq && self.sequenceLevels[callback.seq] !== callback.level) { 53 | continue 54 | } 55 | 56 | // if the action we are looking for doesn't match the action we got 57 | // then we should keep going 58 | if (action !== callback.action) { 59 | continue 60 | } 61 | 62 | // if this is a keypress event and the meta key and control key 63 | // are not pressed that means that we need to only look at the 64 | // character, otherwise check the modifiers as well 65 | // 66 | // chrome will not fire a keypress if meta or control is down 67 | // safari will fire a keypress if meta or meta+shift is down 68 | // firefox will fire a keypress if meta or control is down 69 | modifiersMatch = require('./modifiersMatch') 70 | if ((action === 'keypress' && !e.metaKey && !e.ctrlKey) || modifiersMatch(modifiers, callback.modifiers)) { 71 | // when you bind a combination or sequence a second time it 72 | // should overwrite the first one. if a sequenceName or 73 | // combination is specified in this call it does just that 74 | // 75 | // @todo make deleting its own method? 76 | var deleteCombo = !sequenceName && callback.combo === combination 77 | var deleteSequence = sequenceName && callback.seq === sequenceName && callback.level === level 78 | if (deleteCombo || deleteSequence) { 79 | self.callbacks[character].splice(j, 1) 80 | } 81 | 82 | matches.push(callback) 83 | } 84 | } 85 | 86 | return matches 87 | } 88 | -------------------------------------------------------------------------------- /Combokeys/prototype/getReverseMap.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * reverses the map lookup so that we can look for specific keys 6 | * to see what can and can't use keypress 7 | * 8 | * @return {Object} 9 | */ 10 | module.exports = function () { 11 | var self = this 12 | var constructor = self.constructor 13 | var SPECIAL_KEYS_MAP 14 | 15 | if (!constructor.REVERSE_MAP) { 16 | constructor.REVERSE_MAP = {} 17 | SPECIAL_KEYS_MAP = require('../../helpers/special-keys-map') 18 | for (var key in SPECIAL_KEYS_MAP) { 19 | // pull out the numeric keypad from here cause keypress should 20 | // be able to detect the keys from the character 21 | if (key > 95 && key < 112) { 22 | continue 23 | } 24 | 25 | if (SPECIAL_KEYS_MAP.hasOwnProperty(key)) { 26 | constructor.REVERSE_MAP[SPECIAL_KEYS_MAP[key]] = key 27 | } 28 | } 29 | } 30 | return constructor.REVERSE_MAP 31 | } 32 | -------------------------------------------------------------------------------- /Combokeys/prototype/handleKey.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * handles a character key event 6 | * 7 | * @param {string} character 8 | * @param {Array} modifiers 9 | * @param {Event} e 10 | * @returns void 11 | */ 12 | module.exports = function (character, modifiers, e) { 13 | var self = this 14 | var callbacks 15 | var j 16 | var doNotReset = {} 17 | var maxLevel = 0 18 | var processedSequenceCallback = false 19 | var isModifier 20 | var ignoreThisKeypress 21 | 22 | callbacks = self.getMatches(character, modifiers, e) 23 | // Calculate the maxLevel for sequences so we can only execute the longest callback sequence 24 | for (j = 0; j < callbacks.length; ++j) { 25 | if (callbacks[j].seq) { 26 | maxLevel = Math.max(maxLevel, callbacks[j].level) 27 | } 28 | } 29 | 30 | // loop through matching callbacks for this key event 31 | for (j = 0; j < callbacks.length; ++j) { 32 | // fire for all sequence callbacks 33 | // this is because if for example you have multiple sequences 34 | // bound such as "g i" and "g t" they both need to fire the 35 | // callback for matching g cause otherwise you can only ever 36 | // match the first one 37 | if (callbacks[j].seq) { 38 | // only fire callbacks for the maxLevel to prevent 39 | // subsequences from also firing 40 | // 41 | // for example 'a option b' should not cause 'option b' to fire 42 | // even though 'option b' is part of the other sequence 43 | // 44 | // any sequences that do not match here will be discarded 45 | // below by the resetSequences call 46 | if (callbacks[j].level !== maxLevel) { 47 | continue 48 | } 49 | 50 | processedSequenceCallback = true 51 | 52 | // keep a list of which sequences were matches for later 53 | doNotReset[callbacks[j].seq] = 1 54 | self.fireCallback(callbacks[j].callback, e, callbacks[j].combo, callbacks[j].seq) 55 | continue 56 | } 57 | 58 | // if there were no sequence matches but we are still here 59 | // that means this is a regular match so we should fire that 60 | if (!processedSequenceCallback) { 61 | self.fireCallback(callbacks[j].callback, e, callbacks[j].combo) 62 | } 63 | } 64 | 65 | // if the key you pressed matches the type of sequence without 66 | // being a modifier (ie "keyup" or "keypress") then we should 67 | // reset all sequences that were not matched by this event 68 | // 69 | // this is so, for example, if you have the sequence "h a t" and you 70 | // type "h e a r t" it does not match. in this case the "e" will 71 | // cause the sequence to reset 72 | // 73 | // modifier keys are ignored because you can have a sequence 74 | // that contains modifiers such as "enter ctrl+space" and in most 75 | // cases the modifier key will be pressed before the next key 76 | // 77 | // also if you have a sequence such as "ctrl+b a" then pressing the 78 | // "b" key will trigger a "keypress" and a "keydown" 79 | // 80 | // the "keydown" is expected when there is a modifier, but the 81 | // "keypress" ends up matching the nextExpectedAction since it occurs 82 | // after and that causes the sequence to reset 83 | // 84 | // we ignore keypresses in a sequence that directly follow a keydown 85 | // for the same character 86 | ignoreThisKeypress = e.type === 'keypress' && self.ignoreNextKeypress 87 | isModifier = require('../../helpers/isModifier') 88 | if (e.type === self.nextExpectedAction && !isModifier(character) && !ignoreThisKeypress) { 89 | self.resetSequences(doNotReset) 90 | } 91 | 92 | self.ignoreNextKeypress = processedSequenceCallback && e.type === 'keydown' 93 | } 94 | -------------------------------------------------------------------------------- /Combokeys/prototype/handleKeyEvent.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * handles a keydown event 6 | * 7 | * @param {Event} e 8 | * @returns void 9 | */ 10 | module.exports = function (e) { 11 | var self = this 12 | var characterFromEvent 13 | var eventModifiers 14 | 15 | // normalize e.which for key events 16 | // @see http://stackoverflow.com/questions/4285627/javascript-keycode-vs-charcode-utter-confusion 17 | if (typeof e.which !== 'number') { 18 | e.which = e.keyCode 19 | } 20 | characterFromEvent = require('../../helpers/characterFromEvent') 21 | var character = characterFromEvent(e) 22 | 23 | // no character found then stop 24 | if (character === undefined) { 25 | return 26 | } 27 | 28 | // need to use === for the character check because the character can be 0 29 | if (e.type === 'keyup' && self.ignoreNextKeyup === character) { 30 | self.ignoreNextKeyup = false 31 | return 32 | } 33 | 34 | eventModifiers = require('../../helpers/eventModifiers') 35 | self.handleKey(character, eventModifiers(e), e) 36 | } 37 | -------------------------------------------------------------------------------- /Combokeys/prototype/modifiersMatch.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * checks if two arrays are equal 6 | * 7 | * @param {Array} modifiers1 8 | * @param {Array} modifiers2 9 | * @returns {boolean} 10 | */ 11 | module.exports = function (modifiers1, modifiers2) { 12 | return modifiers1.sort().join(',') === modifiers2.sort().join(',') 13 | } 14 | -------------------------------------------------------------------------------- /Combokeys/prototype/pickBestAction.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * picks the best action based on the key combination 6 | * 7 | * @param {string} key - character for key 8 | * @param {Array} modifiers 9 | * @param {string=} action passed in 10 | */ 11 | module.exports = function (key, modifiers, action) { 12 | var self = this 13 | 14 | // if no action was picked in we should try to pick the one 15 | // that we think would work best for this key 16 | if (!action) { 17 | action = self.getReverseMap()[key] ? 'keydown' : 'keypress' 18 | } 19 | 20 | // modifier keys don't work as expected with keypress, 21 | // switch to keydown 22 | if (action === 'keypress' && modifiers.length) { 23 | action = 'keydown' 24 | } 25 | 26 | return action 27 | } 28 | -------------------------------------------------------------------------------- /Combokeys/prototype/reset.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * resets the library back to its initial state. This is useful 6 | * if you want to clear out the current keyboard shortcuts and bind 7 | * new ones - for example if you switch to another page 8 | * 9 | * @returns void 10 | */ 11 | module.exports = function () { 12 | var self = this 13 | self.callbacks = {} 14 | self.directMap = {} 15 | return this 16 | } 17 | -------------------------------------------------------------------------------- /Combokeys/prototype/resetSequenceTimer.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | /** 4 | * called to set a 1 second timeout on the specified sequence 5 | * 6 | * this is so after each key press in the sequence you have 1 second 7 | * to press the next key before you have to start over 8 | * 9 | * @returns void 10 | */ 11 | module.exports = function () { 12 | var self = this 13 | 14 | clearTimeout(self.resetTimer) 15 | self.resetTimer = setTimeout( 16 | function () { 17 | self.resetSequences() 18 | }, 19 | 1000 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /Combokeys/prototype/resetSequences.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * resets all sequence counters except for the ones passed in 6 | * 7 | * @param {Object} doNotReset 8 | * @returns void 9 | */ 10 | module.exports = function (doNotReset) { 11 | var self = this 12 | 13 | doNotReset = doNotReset || {} 14 | 15 | var activeSequences = false 16 | var key 17 | 18 | for (key in self.sequenceLevels) { 19 | if (doNotReset[key]) { 20 | activeSequences = true 21 | continue 22 | } 23 | self.sequenceLevels[key] = 0 24 | } 25 | 26 | if (!activeSequences) { 27 | self.nextExpectedAction = false 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Combokeys/prototype/stopCallback.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | /** 5 | * should we stop this event before firing off callbacks 6 | * 7 | * @param {Event} e 8 | * @param {Element} element 9 | * @return {boolean} 10 | */ 11 | module.exports = function (e, element) { 12 | // if the element has the class "combokeys" then no need to stop 13 | if ((' ' + element.className + ' ').indexOf(' combokeys ') > -1) { 14 | return false 15 | } 16 | 17 | var tagName = element.tagName.toLowerCase() 18 | 19 | // stop for input, select, and textarea 20 | return tagName === 'input' || tagName === 'select' || tagName === 'textarea' || element.isContentEditable 21 | } 22 | -------------------------------------------------------------------------------- /Combokeys/prototype/trigger.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | /** 4 | * triggers an event that has already been bound 5 | * 6 | * @param {string} keys 7 | * @param {string=} action 8 | * @returns void 9 | */ 10 | module.exports = function (keys, action) { 11 | var self = this 12 | if (self.directMap[keys + ':' + action]) { 13 | self.directMap[keys + ':' + action]({}, keys) 14 | } 15 | return this 16 | } 17 | -------------------------------------------------------------------------------- /Combokeys/prototype/unbind.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | /** 4 | * unbinds an event to Combokeys 5 | * 6 | * the unbinding sets the callback function of the specified key combo 7 | * to an empty function and deletes the corresponding key in the 8 | * directMap dict. 9 | * 10 | * TODO: actually remove this from the callbacks dictionary instead 11 | * of binding an empty function 12 | * 13 | * the keycombo+action has to be exactly the same as 14 | * it was defined in the bind method 15 | * 16 | * @param {string|Array} keys 17 | * @param {string} action 18 | * @returns void 19 | */ 20 | module.exports = function (keys, action) { 21 | var self = this 22 | 23 | return self.bind(keys, function () {}, action) 24 | } 25 | -------------------------------------------------------------------------------- /Combokeys/reset.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | 4 | module.exports = function () { 5 | var self = this 6 | 7 | self.instances.forEach(function (combokeys) { 8 | combokeys.reset() 9 | }) 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Combokeys [![Build Status](https://travis-ci.org/avocode/combokeys.svg?branch=master)](https://travis-ci.org/avocode/combokeys) [![js-standard-style](https://raw.githubusercontent.com/feross/standard/master/badge.png)](https://github.com/feross/standard) 2 | 3 | Combokeys is a JavaScript library for handling keyboard shortcuts in the browser. 4 | 5 | It is licensed under the Apache 2.0 license. 6 | 7 | It is around **3.3kb** minified and gzipped and **9.9kb** minified, has no external dependencies, and has been tested in the following browsers: 8 | 9 | - Internet Explorer 6+ (test suite works in IE9+) 10 | - Safari 11 | - Firefox 12 | - Chrome 13 | 14 | It has support for ``keypress``, ``keydown``, and ``keyup`` events on specific keys, keyboard combinations, or key sequences. 15 | 16 | ## Fork notice 17 | 18 | This project was forked from [ccampbell/mousetrap](https://github.com/ccampbell/mousetrap). 19 | 20 | It was forked because pull–requests were not being reviewed. 21 | 22 | This fork's author intends to review pull–requests. 23 | 24 | Main changes are 25 | 26 | 1. Refactored as CommonJS 27 | 2. Doesn't automatically listen on the `document`. Instead, it is now a constructor and the element on which to listen must be provided on instantiation. Multiple instances possible. 28 | 29 | ## Getting started 30 | 31 | Get it on your page: 32 | 33 | ```js 34 | var Combokeys; 35 | Combokeys = require("combokeys"); 36 | ``` 37 | 38 | Instantiate it for the entire page: 39 | 40 | ```js 41 | var combokeys = new Combokeys(document.documentElement); 42 | ``` 43 | 44 | Or, instantiate it for one or more specific elements: 45 | 46 | ```js 47 | var firstCombokeys = new Combokeys(document.getElementById("first")); 48 | var secondCombokeys = new Combokeys(document.getElementById("second")); 49 | ``` 50 | 51 | Add some combos! 52 | 53 | ```js 54 | // single keys 55 | combokeys.bind('4', function() { console.log('4'); }); 56 | firstCombokeys.bind("?", function() { console.log('show shortcuts!'); }); 57 | secondCombokeys.bind('esc', function() { console.log('escape'); }, 'keyup'); 58 | 59 | // combinations 60 | combokeys.bind('command+shift+k', function() { console.log('command shift k'); }); 61 | 62 | // map multiple combinations to the same callback 63 | combokeys.bind(['command+k', 'ctrl+k'], function() { 64 | console.log('command k or control k'); 65 | // return false to prevent default browser behavior 66 | // and stop event from bubbling 67 | return false; 68 | }); 69 | 70 | // gmail style sequences 71 | combokeys.bind('g i', function() { console.log('go to inbox'); }); 72 | combokeys.bind('* a', function() { console.log('select all'); }); 73 | 74 | // any character (actual character inserted—triggered by the `keypress` event) 75 | combokeys.bind('any-character', function () { console.log('some visual feedback') }); 76 | 77 | // konami code! 78 | combokeys.bind('up up down down left right left right b a enter', function() { 79 | console.log('konami code'); 80 | }); 81 | ``` 82 | 83 | When you’re done with it, detach: 84 | 85 | ```js 86 | combokeys.detach() 87 | // and it will not listen on the element any more 88 | ``` 89 | 90 | You can also bind the plus and minus keys conveniently: 91 | 92 | ```js 93 | combokeys.bind(['mod+plus', 'mod+minus'], function(e) { 94 | e.preventDefault(); 95 | console.log("Override browser zoom!"); 96 | }); 97 | ``` 98 | 99 | ## Why Combokeys? 100 | 101 | There are a number of other similar libraries out there so what makes this one different? 102 | 103 | - CommonJS, [NPM](https://www.npmjs.org/package/combokeys). 104 | - You can listen on multiple, specified elements simultaneously. 105 | - You are not limited to ``keydown`` events (You can specify ``keypress``, ``keydown``, or ``keyup`` or let Combokeys choose for you). 106 | - You can bind key events directly to special keys such as ``?`` or ``*`` without having to specify ``shift+/`` or ``shift+8`` which are not consistent across all keyboards 107 | - It works with international keyboard layouts 108 | - You can bind Gmail like key sequences in addition to regular keys and key combinations 109 | - You can programatically trigger key events with the ``trigger()`` method 110 | - It works with the numeric keypad on your keyboard 111 | - The code is well documented/commented 112 | 113 | ### AMD usage 114 | 115 | You can also build an AMD-compatible version by running `npm run build`. This creates a universally compatible ```dist/combokeys.js``` which, you can use via RequireJS, or include directly in a ``` 20 | ``` 21 | -------------------------------------------------------------------------------- /plugins/record/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | 'use strict' 3 | /** 4 | * This extension allows you to record a sequence using Combokeys. 5 | * 6 | * @author Dan Tao 7 | */ 8 | module.exports = function (Combokeys) { 9 | /** 10 | * the sequence currently being recorded 11 | * 12 | * @type {Array} 13 | */ 14 | var recordedSequence = [] 15 | 16 | /** 17 | * a callback to invoke after recording a sequence 18 | * 19 | * @type {Function|null} 20 | */ 21 | var recordedSequenceCallback = null 22 | 23 | /** 24 | * a list of all of the keys currently held down 25 | * 26 | * @type {Array} 27 | */ 28 | var currentRecordedKeys = [] 29 | 30 | /** 31 | * temporary state where we remember if we've already captured a 32 | * character key in the current combo 33 | * 34 | * @type {boolean} 35 | */ 36 | var recordedCharacterKey = false 37 | 38 | /** 39 | * a handle for the timer of the current recording 40 | * 41 | * @type {null|number} 42 | */ 43 | var recordTimer = null 44 | 45 | /** 46 | * the original handleKey method to override when Combokeys.record() is 47 | * called 48 | * 49 | * @type {Function} 50 | */ 51 | var origHandleKey = Combokeys.handleKey 52 | 53 | /** 54 | * orders key combos 55 | * @param {Array} x 56 | * @param {Array} y 57 | * @return {boolean} 58 | */ 59 | function sortKeyCombo (x, y) { 60 | // modifier keys always come first, in alphabetical order 61 | if (x.length > 1 && y.length === 1) { 62 | return -1 63 | } else if (x.length === 1 && y.length > 1) { 64 | return 1 65 | } 66 | 67 | // character keys come next (list should contain no duplicates, 68 | // so no need for equality check) 69 | return x > y ? 1 : -1 70 | } 71 | 72 | /** 73 | * ensures each combo in a sequence is in a predictable order and formats 74 | * key combos to be "+"-delimited 75 | * 76 | * modifies the sequence in-place 77 | * 78 | * @param {Array} sequence 79 | * @returns void 80 | */ 81 | function normalizeSequence (sequence) { 82 | var i 83 | 84 | for (i = 0; i < sequence.length; ++i) { 85 | sequence[i].sort(sortKeyCombo) 86 | sequence[i] = sequence[i].join('+') 87 | } 88 | } 89 | 90 | /** 91 | * finishes the current recording, passes the recorded sequence to the stored 92 | * callback, and sets Combokeys.handleKey back to its original function 93 | * 94 | * @returns void 95 | */ 96 | function finishRecording () { 97 | if (recordedSequenceCallback) { 98 | normalizeSequence(recordedSequence) 99 | recordedSequenceCallback(recordedSequence) 100 | } 101 | 102 | // reset all recorded state 103 | recordedSequence = [] 104 | recordedSequenceCallback = null 105 | currentRecordedKeys = [] 106 | 107 | Combokeys.handleKey = origHandleKey 108 | } 109 | 110 | /** 111 | * called to set a 1 second timeout on the current recording 112 | * 113 | * this is so after each key press in the sequence the recording will wait for 114 | * 1 more second before executing the callback 115 | * 116 | * @returns void 117 | */ 118 | function restartRecordTimer () { 119 | clearTimeout(recordTimer) 120 | recordTimer = setTimeout(finishRecording, 1000) 121 | } 122 | 123 | /** 124 | * marks whatever key combination that's been recorded so far as finished 125 | * and gets ready for the next combo 126 | * 127 | * @returns void 128 | */ 129 | function recordCurrentCombo () { 130 | recordedSequence.push(currentRecordedKeys) 131 | currentRecordedKeys = [] 132 | recordedCharacterKey = false 133 | restartRecordTimer() 134 | } 135 | 136 | /** 137 | * marks a character key as held down while recording a sequence 138 | * 139 | * @param {string} key 140 | * @returns void 141 | */ 142 | function recordKey (key) { 143 | var i 144 | 145 | // one-off implementation of Array.indexOf, since IE6-9 don't support it 146 | for (i = 0; i < currentRecordedKeys.length; ++i) { 147 | if (currentRecordedKeys[i] === key) { 148 | return 149 | } 150 | } 151 | 152 | currentRecordedKeys.push(key) 153 | 154 | if (key.length === 1) { 155 | recordedCharacterKey = true 156 | } 157 | } 158 | 159 | /** 160 | * handles a character key event 161 | * 162 | * @param {string} character 163 | * @param {Array} modifiers 164 | * @param {Event} e 165 | * @returns void 166 | */ 167 | function handleKey (character, modifiers, e) { 168 | var i 169 | // remember this character if we're currently recording a sequence 170 | if (e.type === 'keydown') { 171 | if (character.length === 1 && recordedCharacterKey) { 172 | recordCurrentCombo() 173 | } 174 | 175 | for (i = 0; i < modifiers.length; ++i) { 176 | recordKey(modifiers[i]) 177 | } 178 | recordKey(character) 179 | 180 | // once a key is released, all keys that were held down at the time 181 | // count as a keypress 182 | } else if (e.type === 'keyup' && currentRecordedKeys.length > 0) { 183 | recordCurrentCombo() 184 | } 185 | } 186 | 187 | /** 188 | * records the next sequence and passes it to a callback once it's 189 | * completed 190 | * 191 | * @param {Function} callback 192 | * @returns void 193 | */ 194 | Combokeys.record = function (callback) { 195 | Combokeys.handleKey = handleKey 196 | recordedSequenceCallback = callback 197 | } 198 | 199 | return Combokeys 200 | } 201 | -------------------------------------------------------------------------------- /test/bind.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha, browser */ 2 | var Combokeys = require('..') 3 | var assert = require('proclaim') 4 | var makeElement = require('./helpers/make-element') 5 | var sinon = require('sinon') 6 | var KeyEvent = require('./lib/key-event') 7 | 8 | describe('combokeys.bind', function () { 9 | it('should work', function () { 10 | var combokeys = new Combokeys(document.documentElement) 11 | var spy = sinon.spy() 12 | combokeys.bind('z', spy) 13 | KeyEvent.simulate('Z'.charCodeAt(0), 90, null, document.documentElement) 14 | assert.strictEqual(spy.callCount, 1) 15 | combokeys.detach() 16 | }) 17 | describe('basic', function () { 18 | it('z key fires when pressing z', function () { 19 | var element = makeElement() 20 | var spy = sinon.spy() 21 | 22 | var combokeys = new Combokeys(element) 23 | combokeys.bind('z', spy) 24 | 25 | KeyEvent.simulate('Z'.charCodeAt(0), 90, null, element) 26 | 27 | // really slow for some reason 28 | // assert(spy).to.have.been.calledOnce 29 | assert.strictEqual(spy.callCount, 1, 'callback should fire once') 30 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 31 | assert.strictEqual(spy.args[0][1], 'z', 'second argument should be key combo') 32 | }) 33 | 34 | it('z key fires from keydown', function () { 35 | var element = makeElement() 36 | var spy = sinon.spy() 37 | 38 | var combokeys = new Combokeys(element) 39 | combokeys.bind('z', spy, 'keydown') 40 | 41 | KeyEvent.simulate('Z'.charCodeAt(0), 90, null, element) 42 | 43 | // really slow for some reason 44 | // assert(spy).to.have.been.calledOnce 45 | assert.strictEqual(spy.callCount, 1, 'callback should fire once') 46 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 47 | assert.strictEqual(spy.args[0][1], 'z', 'second argument should be key combo') 48 | }) 49 | 50 | it('z key does not fire when pressing b', function () { 51 | var element = makeElement() 52 | var spy = sinon.spy() 53 | 54 | var combokeys = new Combokeys(element) 55 | combokeys.bind('z', spy) 56 | 57 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 58 | 59 | assert.strictEqual(spy.callCount, 0) 60 | }) 61 | 62 | it('z key does not fire when holding a modifier key', function () { 63 | var element = makeElement() 64 | var spy = sinon.spy() 65 | var modifiers = ['ctrl', 'alt', 'meta', 'shift'] 66 | var charCode 67 | var modifier 68 | 69 | var combokeys = new Combokeys(element) 70 | combokeys.bind('z', spy) 71 | 72 | for (var i = 0; i < 4; i++) { 73 | modifier = modifiers[i] 74 | charCode = 'Z'.charCodeAt(0) 75 | 76 | // character code is different when alt is pressed 77 | if (modifier === 'alt') { 78 | charCode = 'Ω'.charCodeAt(0) 79 | } 80 | 81 | spy.reset() 82 | 83 | KeyEvent.simulate(charCode, 90, [modifier], element) 84 | 85 | assert.strictEqual(spy.callCount, 0) 86 | } 87 | }) 88 | 89 | it('keyup events should fire', function () { 90 | var element = makeElement() 91 | var spy = sinon.spy() 92 | 93 | var combokeys = new Combokeys(element) 94 | combokeys.bind('z', spy, 'keyup') 95 | 96 | KeyEvent.simulate('Z'.charCodeAt(0), 90, null, element) 97 | 98 | assert.strictEqual(spy.callCount, 1, 'keyup event for `z` should fire') 99 | 100 | // for key held down we should only get one key up 101 | KeyEvent.simulate('Z'.charCodeAt(0), 90, [], element, 10) 102 | assert.strictEqual(spy.callCount, 2, 'keyup event for `z` should fire once for held down key') 103 | }) 104 | 105 | it('keyup event for 0 should fire', function () { 106 | var element = makeElement() 107 | var spy = sinon.spy() 108 | 109 | var combokeys = new Combokeys(element) 110 | combokeys.bind('0', spy, 'keyup') 111 | 112 | KeyEvent.simulate(0, 48, null, element) 113 | 114 | assert.strictEqual(spy.callCount, 1, 'keyup event for `0` should fire') 115 | }) 116 | 117 | it('rebinding a key overwrites the callback for that key', function () { 118 | var element = makeElement() 119 | var spy1 = sinon.spy() 120 | var spy2 = sinon.spy() 121 | 122 | var combokeys = new Combokeys(element) 123 | combokeys.bind('x', spy1) 124 | combokeys.bind('x', spy2) 125 | 126 | KeyEvent.simulate('X'.charCodeAt(0), 88, null, element) 127 | 128 | assert.strictEqual(spy1.callCount, 0, 'original callback should not fire') 129 | assert.strictEqual(spy2.callCount, 1, 'new callback should fire') 130 | }) 131 | 132 | it('binding of `any-character` works', function () { 133 | var element = makeElement() 134 | var spy = sinon.spy() 135 | 136 | var combokeys = new Combokeys(element) 137 | combokeys.bind('any-character', spy) 138 | 139 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 140 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 141 | KeyEvent.simulate('C'.charCodeAt(0), 67, null, element) 142 | assert.strictEqual(spy.callCount, 3, 'gets called on any character') 143 | }) 144 | 145 | it('binding an array of keys', function () { 146 | var element = makeElement() 147 | var spy = sinon.spy() 148 | 149 | var combokeys = new Combokeys(element) 150 | combokeys.bind(['a', 'b', 'c'], spy) 151 | 152 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 153 | assert.strictEqual(spy.callCount, 1, 'new callback was called') 154 | assert.strictEqual(spy.args[0][1], 'a', 'callback should match `a`') 155 | 156 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 157 | assert.strictEqual(spy.callCount, 2, 'new callback was called twice') 158 | assert.strictEqual(spy.args[1][1], 'b', 'callback should match `b`') 159 | 160 | KeyEvent.simulate('C'.charCodeAt(0), 67, null, element) 161 | assert.strictEqual(spy.callCount, 3, 'new callback was called three times') 162 | assert.strictEqual(spy.args[2][1], 'c', 'callback should match `c`') 163 | }) 164 | 165 | it('return false should prevent default and stop propagation', function () { 166 | var element = makeElement() 167 | var spy = sinon.spy(function () { 168 | return false 169 | }) 170 | 171 | var combokeys = new Combokeys(element) 172 | combokeys.bind('command+s', spy) 173 | 174 | if (Event.prototype.preventDefault) { 175 | var preventDefaultSpy = sinon.spy(Event.prototype, 'preventDefault') 176 | } 177 | if (Event.prototype.stopPropagation) { 178 | var stopPropagationSpy = sinon.spy(Event.prototype, 'stopPropagation') 179 | } 180 | KeyEvent.simulate('S'.charCodeAt(0), 83, ['meta'], element) 181 | 182 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 183 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 184 | var event = spy.args[0][0] 185 | if (event.preventDefault) { 186 | assert.isTrue(preventDefaultSpy.calledOnce, 'default action was cancelled') 187 | } else { 188 | assert.isFalse(event.returnValue, 'default is prevented') 189 | } 190 | if (event.stopPropagation) { 191 | assert.isTrue(stopPropagationSpy.calledOnce, 'propagation was cancelled') 192 | } else { 193 | assert.isTrue(event.cancelBubble, 'propagation is cancelled') 194 | } 195 | 196 | // try without return false 197 | spy = sinon.spy() 198 | combokeys.bind('command+s', spy) 199 | KeyEvent.simulate('S'.charCodeAt(0), 83, ['meta'], element) 200 | 201 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 202 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 203 | event = spy.args[0][0] 204 | if (event.preventDefault) { 205 | assert.isTrue(preventDefaultSpy.calledOnce, 'default action was not cancelled') 206 | } else { 207 | assert.isTrue(event.returnValue !== false, 'default is not prevented') 208 | } 209 | if (event.stopPropagation) { 210 | assert.isTrue(stopPropagationSpy.calledOnce, 'propagation was not cancelled') 211 | } else { 212 | assert.isFalse(event.cancelBubble, 'propagation is not cancelled') 213 | } 214 | if (preventDefaultSpy) preventDefaultSpy.restore() 215 | if (stopPropagationSpy) stopPropagationSpy.restore() 216 | }) 217 | 218 | it('capslock key is ignored', function () { 219 | var element = makeElement() 220 | var spy = sinon.spy() 221 | 222 | var combokeys = new Combokeys(element) 223 | combokeys.bind('a', spy) 224 | 225 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 226 | assert.strictEqual(spy.callCount, 1, 'callback should fire for lowercase a') 227 | 228 | spy.reset() 229 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 230 | assert.strictEqual(spy.callCount, 1, 'callback should fire for capslock A') 231 | 232 | spy.reset() 233 | KeyEvent.simulate('A'.charCodeAt(0), 65, ['shift'], element) 234 | assert.strictEqual(spy.callCount, 0, 'callback should not fire fort shift+a') 235 | }) 236 | }) 237 | 238 | describe('special characters', function () { 239 | it('binding special characters', function () { 240 | var element = makeElement() 241 | var spy = sinon.spy() 242 | 243 | var combokeys = new Combokeys(element) 244 | combokeys.bind('*', spy) 245 | 246 | KeyEvent.simulate('*'.charCodeAt(0), 56, ['shift'], element) 247 | 248 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 249 | assert.strictEqual(spy.args[0][1], '*', 'callback should match *') 250 | }) 251 | 252 | it('binding special characters keyup', function () { 253 | var element = makeElement() 254 | var spy = sinon.spy() 255 | 256 | var combokeys = new Combokeys(element) 257 | combokeys.bind('*', spy, 'keyup') 258 | 259 | KeyEvent.simulate('*'.charCodeAt(0), 56, ['shift'], element) 260 | 261 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 262 | assert.strictEqual(spy.args[0][1], '*', 'callback should match *') 263 | }) 264 | 265 | it('binding keys with no associated charCode', function () { 266 | var element = makeElement() 267 | var spy = sinon.spy() 268 | 269 | var combokeys = new Combokeys(element) 270 | combokeys.bind('left', spy) 271 | 272 | KeyEvent.simulate(0, 37, null, element) 273 | 274 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 275 | assert.strictEqual(spy.args[0][1], 'left', 'callback should match `left`') 276 | }) 277 | 278 | it('able to bind plus and minus', function () { 279 | var element = makeElement() 280 | var spy1 = sinon.spy() 281 | var spy2 = sinon.spy() 282 | 283 | var combokeys = new Combokeys(element) 284 | combokeys.bind('ctrl+minus', spy1) 285 | combokeys.bind('ctrl+plus', spy2) 286 | 287 | KeyEvent.simulate('-'.charCodeAt(0), 189, ['ctrl'], element) 288 | assert.strictEqual(spy1.callCount, 1, '`ctrl+minus` should fire') 289 | 290 | KeyEvent.simulate('+'.charCodeAt(0), 187, ['ctrl'], element) 291 | assert.strictEqual(spy2.callCount, 1, '`ctrl+plus` should fire') 292 | }) 293 | 294 | it('able to bind plus and minus from numeric keypad', function () { 295 | var element = makeElement() 296 | var spy1 = sinon.spy() 297 | var spy2 = sinon.spy() 298 | 299 | var combokeys = new Combokeys(element) 300 | combokeys.bind('ctrl+minus', spy1) 301 | combokeys.bind('ctrl+plus', spy2) 302 | 303 | KeyEvent.simulate('-'.charCodeAt(0), 109, ['ctrl'], element) 304 | assert.strictEqual(spy1.callCount, 1, '`ctrl+minus` should fire') 305 | 306 | KeyEvent.simulate('+'.charCodeAt(0), 107, ['ctrl'], element) 307 | assert.strictEqual(spy2.callCount, 1, '`ctrl+plus` should fire') 308 | }) 309 | 310 | it('able to bind 0 from numeric keypad', function () { 311 | var element = makeElement() 312 | var spy = sinon.spy() 313 | 314 | var combokeys = new Combokeys(element) 315 | combokeys.bind('ctrl+0', spy) 316 | 317 | KeyEvent.simulate('0'.charCodeAt(0), 96, ['ctrl'], element) 318 | assert.strictEqual(spy.callCount, 1, '`ctrl+0` should fire') 319 | }) 320 | }) 321 | 322 | describe('combos with modifiers', function () { 323 | it('binding key combinations', function () { 324 | var element = makeElement() 325 | var spy = sinon.spy() 326 | 327 | var combokeys = new Combokeys(element) 328 | combokeys.bind('command+o', spy) 329 | 330 | KeyEvent.simulate('O'.charCodeAt(0), 79, ['meta'], element) 331 | 332 | assert.strictEqual(spy.callCount, 1, 'command+o callback should fire') 333 | assert.strictEqual(spy.args[0][1], 'command+o', 'keyboard string returned is correct') 334 | }) 335 | 336 | it('binding key combos with multiple modifiers', function () { 337 | var element = makeElement() 338 | var spy = sinon.spy() 339 | 340 | var combokeys = new Combokeys(element) 341 | combokeys.bind('command+shift+o', spy) 342 | KeyEvent.simulate('O'.charCodeAt(0), 79, ['meta'], element) 343 | assert.strictEqual(spy.callCount, 0, 'command+o callback should not fire') 344 | 345 | KeyEvent.simulate('O'.charCodeAt(0), 79, ['meta', 'shift'], element) 346 | assert.strictEqual(spy.callCount, 1, 'command+o callback should fire') 347 | }) 348 | }) 349 | 350 | describe('sequences', function () { 351 | it('binding sequences', function () { 352 | var element = makeElement() 353 | var spy = sinon.spy() 354 | 355 | var combokeys = new Combokeys(element) 356 | combokeys.bind('g i', spy) 357 | 358 | KeyEvent.simulate('G'.charCodeAt(0), 71, null, element) 359 | assert.strictEqual(spy.callCount, 0, 'callback should not fire') 360 | 361 | KeyEvent.simulate('I'.charCodeAt(0), 73, null, element) 362 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 363 | }) 364 | 365 | it('binding sequences with mixed types', function () { 366 | var element = makeElement() 367 | var spy = sinon.spy() 368 | 369 | var combokeys = new Combokeys(element) 370 | combokeys.bind('g o enter', spy) 371 | 372 | KeyEvent.simulate('G'.charCodeAt(0), 71, null, element) 373 | assert.strictEqual(spy.callCount, 0, 'callback should not fire') 374 | 375 | KeyEvent.simulate('O'.charCodeAt(0), 79, null, element) 376 | assert.strictEqual(spy.callCount, 0, 'callback should not fire') 377 | 378 | KeyEvent.simulate(0, 13, null, element) 379 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 380 | }) 381 | 382 | it('binding sequences starting with modifier keys', function () { 383 | var element = makeElement() 384 | var spy = sinon.spy() 385 | 386 | var combokeys = new Combokeys(element) 387 | combokeys.bind('option enter', spy) 388 | KeyEvent.simulate(0, 18, ['alt'], element) 389 | KeyEvent.simulate(0, 13, null, element) 390 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 391 | 392 | spy = sinon.spy() 393 | combokeys.bind('command enter', spy) 394 | KeyEvent.simulate(0, 91, ['meta'], element) 395 | KeyEvent.simulate(0, 13, null, element) 396 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 397 | 398 | spy = sinon.spy() 399 | combokeys.bind('escape enter', spy) 400 | KeyEvent.simulate(0, 27, null, element) 401 | KeyEvent.simulate(0, 13, null, element) 402 | assert.strictEqual(spy.callCount, 1, 'callback should fire') 403 | }) 404 | 405 | it('key within sequence should not fire', function () { 406 | var element = makeElement() 407 | var spy1 = sinon.spy() 408 | var spy2 = sinon.spy() 409 | 410 | var combokeys = new Combokeys(element) 411 | combokeys.bind('a', spy1) 412 | combokeys.bind('c a t', spy2) 413 | 414 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 415 | assert.strictEqual(spy1.callCount, 1, 'callback 1 should fire') 416 | spy1.reset() 417 | 418 | KeyEvent.simulate('C'.charCodeAt(0), 67, null, element) 419 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 420 | KeyEvent.simulate('T'.charCodeAt(0), 84, null, element) 421 | assert.strictEqual(spy1.callCount, 0, 'callback for `a` key should not fire') 422 | assert.strictEqual(spy2.callCount, 1, 'callback for `c a t` sequence should fire') 423 | }) 424 | 425 | it('keyup at end of sequence should not fire', function () { 426 | var element = makeElement() 427 | var spy1 = sinon.spy() 428 | var spy2 = sinon.spy() 429 | 430 | var combokeys = new Combokeys(element) 431 | combokeys.bind('t', spy1, 'keyup') 432 | combokeys.bind('b a t', spy2) 433 | 434 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 435 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 436 | KeyEvent.simulate('T'.charCodeAt(0), 84, null, element) 437 | 438 | assert.strictEqual(spy1.callCount, 0, 'callback for `t` keyup should not fire') 439 | assert.strictEqual(spy2.callCount, 1, 'callback for `b a t` sequence should fire') 440 | }) 441 | 442 | it('keyup sequences should work', function () { 443 | var element = makeElement() 444 | var spy = sinon.spy() 445 | var combokeys = new Combokeys(element) 446 | combokeys.bind('b a t', spy, 'keyup') 447 | 448 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 449 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 450 | 451 | // hold the last key down for a while 452 | KeyEvent.simulate('t'.charCodeAt(0), 84, [], element, 10) 453 | 454 | assert.strictEqual(spy.callCount, 1, 'callback for `b a t` sequence should fire on keyup') 455 | }) 456 | 457 | it('extra spaces in sequences should be ignored', function () { 458 | var element = makeElement() 459 | var spy = sinon.spy() 460 | var combokeys = new Combokeys(element) 461 | combokeys.bind('b a t', spy) 462 | 463 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 464 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 465 | KeyEvent.simulate('t'.charCodeAt(0), 84, null, element) 466 | 467 | assert.strictEqual(spy.callCount, 1, 'callback for `b a t` sequence should fire') 468 | }) 469 | 470 | it('modifiers and sequences play nicely', function () { 471 | var element = makeElement() 472 | var spy1 = sinon.spy() 473 | var spy2 = sinon.spy() 474 | 475 | var combokeys = new Combokeys(element) 476 | combokeys.bind('ctrl a', spy1) 477 | combokeys.bind('ctrl+b', spy2) 478 | 479 | KeyEvent.simulate(0, 17, ['ctrl'], element) 480 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 481 | assert.strictEqual(spy1.callCount, 1, '`ctrl a` should fire') 482 | 483 | KeyEvent.simulate('B'.charCodeAt(0), 66, ['ctrl'], element) 484 | assert.strictEqual(spy2.callCount, 1, '`ctrl+b` should fire') 485 | }) 486 | 487 | it('sequences that start the same work', function () { 488 | var element = makeElement() 489 | var spy1 = sinon.spy() 490 | var spy2 = sinon.spy() 491 | 492 | var combokeys = new Combokeys(element) 493 | combokeys.bind('g g l', spy2) 494 | combokeys.bind('g g o', spy1) 495 | 496 | KeyEvent.simulate('g'.charCodeAt(0), 71, null, element) 497 | KeyEvent.simulate('g'.charCodeAt(0), 71, null, element) 498 | KeyEvent.simulate('o'.charCodeAt(0), 79, null, element) 499 | assert.strictEqual(spy1.callCount, 1, '`g g o` should fire') 500 | assert.strictEqual(spy2.callCount, 0, '`g g l` should not fire') 501 | 502 | spy1.reset() 503 | spy2.reset() 504 | KeyEvent.simulate('g'.charCodeAt(0), 71, null, element) 505 | KeyEvent.simulate('g'.charCodeAt(0), 71, null, element) 506 | KeyEvent.simulate('l'.charCodeAt(0), 76, null, element) 507 | assert.strictEqual(spy1.callCount, 0, '`g g o` should not fire') 508 | assert.strictEqual(spy2.callCount, 1, '`g g l` should fire') 509 | }) 510 | 511 | it('sequences should not fire subsequences', function () { 512 | var element = makeElement() 513 | var spy1 = sinon.spy() 514 | var spy2 = sinon.spy() 515 | 516 | var combokeys = new Combokeys(element) 517 | combokeys.bind('a b c', spy1) 518 | combokeys.bind('b c', spy2) 519 | 520 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 521 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 522 | KeyEvent.simulate('C'.charCodeAt(0), 67, null, element) 523 | 524 | assert.strictEqual(spy1.callCount, 1, '`a b c` should fire') 525 | assert.strictEqual(spy2.callCount, 0, '`b c` should not fire') 526 | 527 | spy1.reset() 528 | spy2.reset() 529 | combokeys.bind('option b', spy1) 530 | combokeys.bind('a option b', spy2) 531 | 532 | KeyEvent.simulate('A'.charCodeAt(0), 65, null, element) 533 | KeyEvent.simulate(0, 18, ['alt'], element) 534 | KeyEvent.simulate('B'.charCodeAt(0), 66, null, element) 535 | 536 | assert.strictEqual(spy1.callCount, 0, '`option b` should not fire') 537 | assert.strictEqual(spy2.callCount, 1, '`a option b` should fire') 538 | }) 539 | 540 | it('rebinding same sequence should override previous', function () { 541 | var element = makeElement() 542 | var spy1 = sinon.spy() 543 | var spy2 = sinon.spy() 544 | var combokeys = new Combokeys(element) 545 | combokeys.bind('a b c', spy1) 546 | combokeys.bind('a b c', spy2) 547 | 548 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 549 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 550 | KeyEvent.simulate('c'.charCodeAt(0), 67, null, element) 551 | 552 | assert.strictEqual(spy1.callCount, 0, 'first callback should not fire') 553 | assert.strictEqual(spy2.callCount, 1, 'second callback should fire') 554 | }) 555 | 556 | it('broken sequences', function () { 557 | var element = makeElement() 558 | var spy = sinon.spy() 559 | var combokeys = new Combokeys(element) 560 | combokeys.bind('h a t', spy) 561 | 562 | KeyEvent.simulate('h'.charCodeAt(0), 72, null, element) 563 | KeyEvent.simulate('e'.charCodeAt(0), 69, null, element) 564 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 565 | KeyEvent.simulate('r'.charCodeAt(0), 82, null, element) 566 | KeyEvent.simulate('t'.charCodeAt(0), 84, null, element) 567 | 568 | assert.strictEqual(spy.callCount, 0, 'sequence for `h a t` should not fire for `h e a r t`') 569 | }) 570 | 571 | it('sequences containing combos should work', function () { 572 | var element = makeElement() 573 | var spy = sinon.spy() 574 | var combokeys = new Combokeys(element) 575 | combokeys.bind('a ctrl+b', spy) 576 | 577 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 578 | KeyEvent.simulate('B'.charCodeAt(0), 66, ['ctrl'], element) 579 | 580 | assert.strictEqual(spy.callCount, 1, '`a ctrl+b` should fire') 581 | 582 | combokeys.unbind('a ctrl+b') 583 | 584 | spy = sinon.spy() 585 | combokeys.bind('ctrl+b a', spy) 586 | 587 | KeyEvent.simulate('b'.charCodeAt(0), 66, ['ctrl'], element) 588 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 589 | 590 | assert.strictEqual(spy.callCount, 1, '`ctrl+b a` should fire') 591 | }) 592 | 593 | it('sequences starting with spacebar should work', function () { 594 | var element = makeElement() 595 | var spy = sinon.spy() 596 | var combokeys = new Combokeys(element) 597 | combokeys.bind('a space b c', spy) 598 | 599 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 600 | KeyEvent.simulate(32, 32, null, element) 601 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 602 | KeyEvent.simulate('c'.charCodeAt(0), 67, null, element) 603 | 604 | assert.strictEqual(spy.callCount, 1, '`a space b c` should fire') 605 | }) 606 | 607 | it('konami code', function () { 608 | var element = makeElement() 609 | var spy = sinon.spy() 610 | var combokeys = new Combokeys(element) 611 | combokeys.bind('up up down down left right left right b a enter', spy) 612 | 613 | KeyEvent.simulate(0, 38, null, element) 614 | KeyEvent.simulate(0, 38, null, element) 615 | KeyEvent.simulate(0, 40, null, element) 616 | KeyEvent.simulate(0, 40, null, element) 617 | KeyEvent.simulate(0, 37, null, element) 618 | KeyEvent.simulate(0, 39, null, element) 619 | KeyEvent.simulate(0, 37, null, element) 620 | KeyEvent.simulate(0, 39, null, element) 621 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 622 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 623 | KeyEvent.simulate(0, 13, null, element) 624 | 625 | assert.strictEqual(spy.callCount, 1, 'konami code should fire') 626 | }) 627 | 628 | it('sequence timer resets', function () { 629 | var element = makeElement() 630 | var spy = sinon.spy() 631 | var clock = sinon.useFakeTimers() 632 | 633 | var combokeys = new Combokeys(element) 634 | combokeys.bind('h a t', spy) 635 | 636 | KeyEvent.simulate('h'.charCodeAt(0), 72, null, element) 637 | clock.tick(600) 638 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 639 | clock.tick(900) 640 | KeyEvent.simulate('t'.charCodeAt(0), 84, null, element) 641 | 642 | assert.strictEqual(spy.callCount, 1, 'sequence should fire after waiting') 643 | clock.restore() 644 | }) 645 | 646 | it('sequences timeout', function () { 647 | var element = makeElement() 648 | var spy = sinon.spy() 649 | var clock = sinon.useFakeTimers() 650 | 651 | var combokeys = new Combokeys(element) 652 | combokeys.bind('g t', spy) 653 | KeyEvent.simulate('g'.charCodeAt(0), 71, null, element) 654 | clock.tick(1000) 655 | KeyEvent.simulate('t'.charCodeAt(0), 84, null, element) 656 | 657 | assert.strictEqual(spy.callCount, 0, 'sequence callback should not fire') 658 | clock.restore() 659 | }) 660 | }) 661 | 662 | describe('default actions', function () { 663 | var keys = { 664 | keypress: [ 665 | ['a', 65], 666 | ['A', 65, ['shift']], 667 | ['7', 55], 668 | ['?', 191], 669 | ['*', 56], 670 | ['+', 187], 671 | ['$', 52], 672 | ['[', 219], 673 | ['.', 190] 674 | ], 675 | keydown: [ 676 | ["shift+'", 222, ['shift']], 677 | ['shift+a', 65, ['shift']], 678 | ['shift+5', 53, ['shift']], 679 | ['command+shift+p', 80, ['meta', 'shift']], 680 | ['space', 32], 681 | ['left', 37] 682 | ] 683 | } 684 | 685 | function getCallback (key, keyCode, type, modifiers) { 686 | return function () { 687 | var element = makeElement() 688 | var spy = sinon.spy() 689 | 690 | var combokeys = new Combokeys(element) 691 | combokeys.bind(key, spy) 692 | 693 | KeyEvent.simulate(key.charCodeAt(0), keyCode, modifiers, element) 694 | assert.strictEqual(spy.callCount, 1) 695 | assert.strictEqual(spy.args[0][0].type, type) 696 | } 697 | } 698 | 699 | for (var type in keys) { 700 | for (var i = 0; i < keys[type].length; i++) { 701 | var key = keys[type][i][0] 702 | var keyCode = keys[type][i][1] 703 | var modifiers = keys[type][i][2] || [] 704 | it('"' + key + '" uses "' + type + '"', getCallback(key, keyCode, type, modifiers)) 705 | } 706 | } 707 | }) 708 | }) 709 | -------------------------------------------------------------------------------- /test/constructor.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var Combokeys = require('..') 3 | var assert = require('proclaim') 4 | var makeElement = require('./helpers/make-element') 5 | var sinon = require('sinon') 6 | var KeyEvent = require('./lib/key-event') 7 | 8 | describe('combokeys.constructor', function () { 9 | it('should store instances globally', function () { 10 | var element = makeElement() 11 | var spy = sinon.spy() 12 | var combokeys = new Combokeys(element) 13 | assert.strictEqual(Combokeys.instances.length, 1) 14 | }) 15 | 16 | it('should not store instances globally', function () { 17 | var element = makeElement() 18 | var spy = sinon.spy() 19 | var combokeys = new Combokeys(element, { storeInstancesGlobally: false }) 20 | assert.strictEqual(Combokeys.instances.length, 0) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/detach.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var Combokeys = require('..') 3 | var assert = require('proclaim') 4 | var makeElement = require('./helpers/make-element') 5 | var sinon = require('sinon') 6 | var KeyEvent = require('./lib/key-event') 7 | 8 | describe('combokeys.detach', function () { 9 | it('detaches', function () { 10 | var element = makeElement() 11 | var spy = sinon.spy() 12 | var combokeys = new Combokeys(element) 13 | combokeys.bind('a', spy) 14 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 15 | assert.strictEqual(spy.callCount, 1, 'calls back normally') 16 | combokeys.detach() 17 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 18 | assert.strictEqual(spy.callCount, 1, 'does not call back because detached') 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/helpers/make-element.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | var element = document.createElement('div') 3 | document.body.appendChild(element) 4 | return element 5 | } 6 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('es5-shim/es5-shim') 3 | require('es5-shim/es5-sham') 4 | 5 | // them tests 6 | require('./initialization') 7 | require('./bind') 8 | require('./unbind') 9 | require('./detach') 10 | require('./plugins/bind-dictionary') 11 | require('./plugins/global-bind') 12 | require('./plugins/pause') 13 | require('./plugins/record') 14 | -------------------------------------------------------------------------------- /test/initialization.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var Combokeys = require('..') 3 | var assert = require('proclaim') 4 | var makeElement = require('./helpers/make-element') 5 | 6 | describe('initialization', function () { 7 | it('initializes on the document', function () { 8 | var combokeys = new Combokeys(document.documentElement) 9 | assert.instanceOf(combokeys, Combokeys) 10 | assert.strictEqual(combokeys.element, document.documentElement) 11 | combokeys.detach() 12 | }) 13 | it('can initialize multipe instances', function () { 14 | var first = makeElement() 15 | var second = makeElement() 16 | 17 | var firstCombokeys = new Combokeys(first) 18 | var secondCombokeys = new Combokeys(second) 19 | 20 | assert.instanceOf(secondCombokeys, Combokeys) 21 | assert.notEqual(firstCombokeys, secondCombokeys) 22 | assert.strictEqual(firstCombokeys.element, first) 23 | assert.strictEqual(secondCombokeys.element, second) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/lib/key-event.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node, browser */ 2 | var KeyEvent = function (data, type) { 3 | 'use strict' 4 | this.keyCode = 'keyCode' in data ? data.keyCode : 0 5 | this.charCode = 'charCode' in data ? data.charCode : 0 6 | 7 | var modifiers = 'modifiers' in data ? data.modifiers : [] 8 | 9 | this.ctrlKey = false 10 | this.metaKey = false 11 | this.altKey = false 12 | this.shiftKey = false 13 | 14 | for (var i = 0; i < modifiers.length; i++) { 15 | this[modifiers[i] + 'Key'] = true 16 | } 17 | 18 | this.type = type || 'keypress' 19 | } 20 | 21 | KeyEvent.prototype.toNative = function () { 22 | 'use strict' 23 | var event = document.createEvent ? document.createEvent('Event') : document.createEventObject() 24 | 25 | if (event.initEvent) { 26 | event.initEvent(this.type, true, true) 27 | } 28 | 29 | event.keyCode = this.keyCode 30 | event.which = this.charCode || this.keyCode 31 | event.shiftKey = this.shiftKey 32 | event.metaKey = this.metaKey 33 | event.altKey = this.altKey 34 | event.ctrlKey = this.ctrlKey 35 | 36 | return event 37 | } 38 | 39 | KeyEvent.prototype.fire = function (element) { 40 | 'use strict' 41 | var event = this.toNative() 42 | if (element.dispatchEvent) { 43 | element.dispatchEvent(event) 44 | return 45 | } 46 | 47 | element.fireEvent('on' + this.type, event) 48 | } 49 | 50 | // simulates complete key event as if the user pressed the key in the browser 51 | // triggers a keydown, then a keypress, then a keyup 52 | KeyEvent.simulate = function (charCode, keyCode, modifiers, element, repeat) { 53 | 'use strict' 54 | modifiers = modifiers || [] 55 | 56 | if (element === undefined) { 57 | element = document.documentElement 58 | } 59 | 60 | if (repeat === undefined) { 61 | repeat = 1 62 | } 63 | 64 | var modifierToKeyCode = { 65 | 'shift': 16, 66 | 'ctrl': 17, 67 | 'alt': 18, 68 | 'meta': 91 69 | } 70 | 71 | // if the key is a modifier then take it out of the regular 72 | // keypress/keydown 73 | if (keyCode === 16 || keyCode === 17 || keyCode === 18 || keyCode === 91) { 74 | repeat = 0 75 | } 76 | 77 | var modifiersToInclude = [] 78 | var keyEvents = [] 79 | 80 | // modifiers would go down first 81 | for (var i = 0; i < modifiers.length; i++) { 82 | modifiersToInclude.push(modifiers[i]) 83 | keyEvents.push(new KeyEvent({ 84 | charCode: 0, 85 | keyCode: modifierToKeyCode[modifiers[i]], 86 | modifiers: modifiersToInclude 87 | }, 'keydown')) 88 | } 89 | 90 | // @todo factor in duration for these 91 | while (repeat > 0) { 92 | keyEvents.push(new KeyEvent({ 93 | charCode: 0, 94 | keyCode: keyCode, 95 | modifiers: modifiersToInclude 96 | }, 'keydown')) 97 | 98 | keyEvents.push(new KeyEvent({ 99 | charCode: charCode, 100 | keyCode: charCode, 101 | modifiers: modifiersToInclude 102 | }, 'keypress')) 103 | 104 | repeat-- 105 | } 106 | 107 | keyEvents.push(new KeyEvent({ 108 | charCode: 0, 109 | keyCode: keyCode, 110 | modifiers: modifiersToInclude 111 | }, 'keyup')) 112 | 113 | // now lift up the modifier keys 114 | for (i = 0; i < modifiersToInclude.length; i++) { 115 | var modifierKeyCode = modifierToKeyCode[modifiersToInclude[i]] 116 | modifiersToInclude.splice(i, 1) 117 | keyEvents.push(new KeyEvent({ 118 | charCode: 0, 119 | keyCode: modifierKeyCode, 120 | modifiers: modifiersToInclude 121 | }, 'keyup')) 122 | } 123 | 124 | for (i = 0; i < keyEvents.length; i++) { 125 | // console.log("firing", keyEvents[i].type, keyEvents[i].keyCode, keyEvents[i].charCode) 126 | keyEvents[i].fire(element) 127 | } 128 | } 129 | 130 | module.exports = KeyEvent 131 | -------------------------------------------------------------------------------- /test/plugins/bind-dictionary.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, mocha */ 2 | var assert = require('proclaim') 3 | var sinon = require('sinon') 4 | var Combokeys = require('../..') 5 | var KeyEvent = require('.././lib/key-event') 6 | 7 | describe('combokeys.bind', function () { 8 | it('bind multiple keys', function () { 9 | var spy = sinon.spy() 10 | 11 | var combokeys = new Combokeys(document) 12 | require('../../plugins/bind-dictionary')(combokeys) 13 | combokeys.bind({ 14 | 'a': spy, 15 | 'b': spy, 16 | 'c': spy 17 | }) 18 | 19 | KeyEvent.simulate('A'.charCodeAt(0), 65) 20 | KeyEvent.simulate('B'.charCodeAt(0), 66) 21 | KeyEvent.simulate('C'.charCodeAt(0), 67) 22 | KeyEvent.simulate('Z'.charCodeAt(0), 90) 23 | 24 | assert.equal(spy.callCount, 3, 'callback should fire three times') 25 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 26 | assert.equal(spy.args[0][1], 'a', 'second argument should be key combo') 27 | }) 28 | }) 29 | 30 | describe('combokeys.unbind', function () { 31 | it('unbind works', function () { 32 | var spy = sinon.spy() 33 | var combokeys = new Combokeys(document) 34 | require('../../plugins/bind-dictionary')(combokeys) 35 | combokeys.bind({ 36 | 'a': spy 37 | }) 38 | KeyEvent.simulate('a'.charCodeAt(0), 65) 39 | assert.equal(spy.callCount, 1, 'callback for a should fire') 40 | 41 | combokeys.unbind('a') 42 | KeyEvent.simulate('a'.charCodeAt(0), 65) 43 | assert.equal(spy.callCount, 1, 'callback for a should not fire after unbind') 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /test/plugins/global-bind.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, mocha */ 2 | var assert = require('proclaim') 3 | var sinon = require('sinon') 4 | var Combokeys = require('../..') 5 | var KeyEvent = require('.././lib/key-event') 6 | 7 | describe('combokeys.bindGlobal', function () { 8 | it('z key fires when pressing z', function () { 9 | var spy = sinon.spy() 10 | 11 | var combokeys = new Combokeys(document) 12 | require('../../plugins/global-bind')(combokeys) 13 | combokeys.bindGlobal('z', spy) 14 | 15 | KeyEvent.simulate('Z'.charCodeAt(0), 90) 16 | 17 | assert.equal(spy.callCount, 1, 'callback should fire once') 18 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 19 | assert.equal(spy.args[0][1], 'z', 'second argument should be key combo') 20 | }) 21 | 22 | it('z key fires when pressing z in input', function () { 23 | var spy = sinon.spy() 24 | 25 | var combokeys = new Combokeys(document) 26 | require('../../plugins/global-bind')(combokeys) 27 | combokeys.bindGlobal('z', spy) 28 | 29 | var el = document.createElement('input') 30 | document.body.appendChild(el) 31 | 32 | KeyEvent.simulate('Z'.charCodeAt(0), 90, undefined, el) 33 | 34 | assert.equal(spy.callCount, 1, 'callback should fire once') 35 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 36 | assert.equal(spy.args[0][1], 'z', 'second argument should be key combo') 37 | }) 38 | 39 | it('z key fires when pressing z in textarea', function () { 40 | var spy = sinon.spy() 41 | 42 | var combokeys = new Combokeys(document) 43 | require('../../plugins/global-bind')(combokeys) 44 | combokeys.bindGlobal('z', spy) 45 | 46 | var el = document.createElement('textarea') 47 | document.body.appendChild(el) 48 | 49 | KeyEvent.simulate('Z'.charCodeAt(0), 90, undefined, el) 50 | 51 | assert.equal(spy.callCount, 1, 'callback should fire once') 52 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 53 | assert.equal(spy.args[0][1], 'z', 'second argument should be key combo') 54 | }) 55 | 56 | it('z key fires when pressing z in select', function () { 57 | var spy = sinon.spy() 58 | 59 | var combokeys = new Combokeys(document) 60 | require('../../plugins/global-bind')(combokeys) 61 | combokeys.bindGlobal('z', spy) 62 | 63 | var el = document.createElement('select') 64 | document.body.appendChild(el) 65 | 66 | KeyEvent.simulate('Z'.charCodeAt(0), 90, undefined, el) 67 | 68 | assert.equal(spy.callCount, 1, 'callback should fire once') 69 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 70 | assert.equal(spy.args[0][1], 'z', 'second argument should be key combo') 71 | }) 72 | 73 | it('z key fires when pressing z in contenteditable', function () { 74 | var spy = sinon.spy() 75 | 76 | var combokeys = new Combokeys(document) 77 | require('../../plugins/global-bind')(combokeys) 78 | combokeys.bindGlobal('z', spy) 79 | 80 | var el = document.createElement('div') 81 | el.contentEditable = 'true' 82 | document.body.appendChild(el) 83 | 84 | KeyEvent.simulate('Z'.charCodeAt(0), 90, undefined, el) 85 | 86 | assert.equal(spy.callCount, 1, 'callback should fire once') 87 | assert.instanceOf(spy.args[0][0], Event, 'first argument should be Event') 88 | assert.equal(spy.args[0][1], 'z', 'second argument should be key combo') 89 | }) 90 | }) 91 | 92 | describe('combokeys.unbind', function () { 93 | it('unbind works', function () { 94 | var spy = sinon.spy() 95 | var combokeys = new Combokeys(document) 96 | var el = document.createElement('input') 97 | document.body.appendChild(el) 98 | require('../../plugins/global-bind')(combokeys) 99 | combokeys.bindGlobal('a', spy) 100 | KeyEvent.simulate('a'.charCodeAt(0), 65, undefined, el) 101 | assert.equal(spy.callCount, 1, 'callback for a should fire') 102 | 103 | combokeys.unbind('a') 104 | KeyEvent.simulate('a'.charCodeAt(0), 65, undefined, el) 105 | assert.equal(spy.callCount, 1, 'callback for a should not fire after unbind') 106 | 107 | // If we now bind the same key without bindGlobal, 108 | // it's not bound globally 109 | combokeys.bind('a', spy) 110 | KeyEvent.simulate('a'.charCodeAt(0), 65, undefined, el) 111 | assert.equal(spy.callCount, 1, 'callback for a should not fire in an input after bind') 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /test/plugins/pause.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var assert = require('proclaim') 3 | var sinon = require('sinon') 4 | var Combokeys = require('../..') 5 | var KeyEvent = require('.././lib/key-event') 6 | 7 | describe('combokeys.pause', function () { 8 | it('pause and unpause works', function () { 9 | var spy = sinon.spy() 10 | 11 | var combokeys = new Combokeys(document) 12 | require('../../plugins/pause')(combokeys) 13 | combokeys.bind('a', spy) 14 | 15 | KeyEvent.simulate('A'.charCodeAt(0), 65) 16 | combokeys.pause() 17 | KeyEvent.simulate('A'.charCodeAt(0), 65) 18 | combokeys.unpause() 19 | KeyEvent.simulate('A'.charCodeAt(0), 65) 20 | 21 | assert.equal(spy.callCount, 2, 'callback should fire twice') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /test/plugins/record.js: -------------------------------------------------------------------------------- 1 | /* eslint-env browser, mocha */ 2 | var assert = require('proclaim') 3 | var sinon = require('sinon') 4 | var Combokeys = require('../..') 5 | var KeyEvent = require('.././lib/key-event') 6 | 7 | describe('combokeys.record', function () { 8 | it('recording keys works', function (done) { 9 | var spy = sinon.spy() 10 | 11 | var combokeys = new Combokeys(document) 12 | require('../../plugins/record')(combokeys) 13 | combokeys.record(spy) 14 | 15 | KeyEvent.simulate('A'.charCodeAt(0), 65) 16 | KeyEvent.simulate('B'.charCodeAt(0), 66) 17 | KeyEvent.simulate('C'.charCodeAt(0), 67) 18 | KeyEvent.simulate('O'.charCodeAt(0), 79, ['meta', 'shift']) 19 | 20 | setTimeout(function () { 21 | assert.equal(spy.callCount, 1, 'callback should fire once') 22 | assert.deepEqual( 23 | spy.args[0][0], 24 | ['a', 'b', 'c', 'meta+shift+o'], 25 | 'all key presses should be recorded' 26 | ) 27 | done() 28 | }, 1000) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/unbind.js: -------------------------------------------------------------------------------- 1 | /* eslint-env mocha */ 2 | var Combokeys = require('..') 3 | var assert = require('proclaim') 4 | var makeElement = require('./helpers/make-element') 5 | var sinon = require('sinon') 6 | var KeyEvent = require('./lib/key-event') 7 | 8 | describe('combokeys.unbind', function () { 9 | it('unbind works', function () { 10 | var element = makeElement() 11 | var spy = sinon.spy() 12 | var combokeys = new Combokeys(element) 13 | combokeys.bind('a', spy) 14 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 15 | assert.strictEqual(spy.callCount, 1, 'callback for a should fire') 16 | 17 | combokeys.unbind('a') 18 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 19 | assert.strictEqual(spy.callCount, 1, 'callback for a should not fire after unbind') 20 | }) 21 | 22 | it('unbinds \'any-character\'', function () { 23 | var element = makeElement() 24 | var spy = sinon.spy() 25 | var combokeys = new Combokeys(element) 26 | combokeys.bind('any-character', spy) 27 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 28 | assert.strictEqual(spy.callCount, 1, 'just checking the callback') 29 | combokeys.unbind('any-character') 30 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 31 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 32 | assert.strictEqual(spy.callCount, 1, 'unbound') 33 | }) 34 | 35 | it('unbind accepts an array', function () { 36 | var element = makeElement() 37 | var spy = sinon.spy() 38 | var combokeys = new Combokeys(element) 39 | combokeys.bind(['a', 'b', 'c'], spy) 40 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 41 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 42 | KeyEvent.simulate('c'.charCodeAt(0), 67, null, element) 43 | assert.strictEqual(spy.callCount, 3, 'callback should have fired 3 times') 44 | 45 | combokeys.unbind(['a', 'b', 'c']) 46 | KeyEvent.simulate('a'.charCodeAt(0), 65, null, element) 47 | KeyEvent.simulate('b'.charCodeAt(0), 66, null, element) 48 | KeyEvent.simulate('c'.charCodeAt(0), 67, null, element) 49 | assert.strictEqual(spy.callCount, 3, 'callback should not fire after unbind') 50 | }) 51 | }) 52 | --------------------------------------------------------------------------------