├── .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 [](https://travis-ci.org/avocode/combokeys) [](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 |
--------------------------------------------------------------------------------