/**
* #### Overview ####
*
* `TangibleKeyboard` is a a flexible and easy to use keyboard binding library allowing
* the user to attach events to key presses, key sequences and key combos. It is a
* spin-off of the nice `KeyboardJS` library created by
* <a href="https://github.com/RobertWHurst/KeyboardJS">Robert Hurst</a>.
*
* This particular version adds layouts for various keyboard emulators often used in
* physical computing to gather input from switches. It also adds explicit support for key
* repeat prevention. This means you can, if you so wish, ignore repeated `keydown` events
* sent by the OS when a key is being held down. This version also allows you to specify
* whether browser actions tied to certain key combinations should be triggered or not
* (`preventDefault`).
*
* #### Basic usage ####
*
* Using `TangibleKeyboard` is quite simple. Simply link to the JS file and use the `on()`
* method to attach a callback. Here's an example that will trigger a user function when
* the "a" key is pressed:
*
* <script src="tangiblekeyboard.js"></script>
*
* <script>
* TangibleKeyboard.on(
* 'a',
* {
* onKeyDown: function(e, keys, combo) { console.log(e); }
* }
* );
* </script>
*
* That's it.
*
* if you want to prevent default browser actions from being triggered, set
* `preventDefault` to `true`. If you want to prevent repeated events from being triggered
* when a key is being held down, set `preventRepeat` to `true`:
*
* <script>
* TangibleKeyboard.on(
* 'a',
* {
* onKeyDown: function(e, keys, combo) { console.log(e); },
* preventDefault: true,
* preventRepeat: true
* }
* );
* </script>
*
* #### Using a different keyboard layout ####
*
* `TangibleKeyboard` is, by default, configured to use the basic qwerty keyboard layout.
* If you want to use another one, such as the bundled IpacVE layout, you need to assign
* the layout:
*
* <script src="tangiblekeyboard.js"></script>
* <script src="layouts/tangiblekeyboard.layout.ipacve.js"></script>
*
* <script>
* TangibleKeyboard.setLayout('ipacve');
*
* TangibleKeyboard.on(
* '1RGHT',
* {
* onKeyDown: function(e, keys, combo) { console.log(e); }
* }
* );
* </script>
*
* #### Using it with AMD and CommonJS ####
*
* `TangibleKeyboard` is compatible with AMD and CommonJS. Here's an example of using an
* alternative keyboard layout with AMD:
*
* define(function (require) {
*
* var tk = require('tangiblekeyboard');
* var layout = require('layouts/tangiblekeyboard.layout.ipacve');
* tk.registerAndSetLayout("ipacve", layout);
*
* tk.on(
* '1RGHT',
* {
* onKeyDown: function(e, keys, combo) { console.log(e); }
* }
* );
*
* });
*
* @todo Check why a reset is issued upon fullscreenchange.
* @todo the issue for long-held keys that trigger a bunch of crap on release
*
* @class TangibleKeyboard
* @static
* @version @@version
* @author @@author
*/
/* global define, module */
/*jshint bitwise: false*/
(function(context, factory) {
'use strict';
// indexOf polyfill
[].indexOf||(Array.prototype.indexOf=function(a,b,c){for(c=this.length,b=(c+~~b)%c;b<c&&(!(b in this)||this[b]!==a);b++);return b^c?b:-1;});
if (typeof define === 'function' && define.amd) { // AMD/RequireJS
define(constructAMD);
} else if(typeof module !== 'undefined') { //CommonJS
constructCommonJS();
} else { // Global
constructGlobal();
}
// Construct AMD version of the library
function constructAMD() {
//create a library instance
return init(context);
//spawns a library instance
function init(context) {
var library;
library = factory(context, 'amd');
library.fork = init;
return library;
}
}
// Construct CommonJS version of the library
function constructCommonJS() {
//create a library instance
module.exports = init(context);
//spawns a library instance
function init(context) {
var library;
library = factory(context, 'CommonJS');
library.fork = init;
return library;
}
}
// Construct a global version of the library
function constructGlobal() {
var library;
//create a library instance
library = init(context);
//spawns a library instance
function init(context) {
var library, namespaces = [], previousValues = {};
library = factory(context, 'global');
library.fork = init;
library.noConflict = noConflict;
library.noConflict('TangibleKeyboard', 'k');
return library;
//sets library namespaces
function noConflict( ) {
var nI, newNamespaces;
newNamespaces = Array.prototype.slice.apply(arguments);
for(nI = 0; nI < namespaces.length; nI += 1) {
if(typeof previousValues[namespaces[nI]] === 'undefined') {
delete context[namespaces[nI]];
} else {
context[namespaces[nI]] = previousValues[namespaces[nI]];
}
}
previousValues = {};
for(nI = 0; nI < newNamespaces.length; nI += 1) {
if(typeof newNamespaces[nI] !== 'string') {
throw new Error(
'Cannot replace namespaces. All new namespaces must be ' +
'strings.'
);
}
previousValues[newNamespaces[nI]] = context[newNamespaces[nI]];
context[newNamespaces[nI]] = library;
}
namespaces = newNamespaces;
return namespaces;
}
}
}
})(this, function(targetWindow, env) {
'use strict';
var TangibleKeyboard = {},
layouts = {},
layout,
map,
macros,
activeKeys = [],
bindings = [],
activeBindings = [],
activeMacros = [],
aI,
qwertyLayout;
targetWindow = targetWindow || window;
///////////////////////
// DEFAULT US LAYOUT //
///////////////////////
qwertyLayout = {
"map": {
//general
"3": ["cancel"],
"8": ["backspace"],
"9": ["tab"],
"12": ["clear"],
"13": ["enter"],
"16": ["shift"],
"17": ["ctrl"],
"18": ["alt", "menu"],
"19": ["pause", "break"],
"20": ["capslock"],
"27": ["escape", "esc"],
"32": ["space", "spacebar"],
"33": ["pageup"],
"34": ["pagedown"],
"35": ["end"],
"36": ["home"],
"37": ["left"],
"38": ["up"],
"39": ["right"],
"40": ["down"],
"41": ["select"],
"42": ["printscreen"],
"43": ["execute"],
"44": ["snapshot"],
"45": ["insert", "ins"],
"46": ["delete", "del"],
"47": ["help"],
"91": ["command", "windows", "win", "super", "leftcommand", "leftwindows", "leftwin", "leftsuper"],
"92": ["command", "windows", "win", "super", "rightcommand", "rightwindows", "rightwin", "rightsuper"],
"145": ["scrolllock", "scroll"],
"186": ["semicolon", ";"],
"187": ["equal", "equalsign", "="],
"188": ["comma", ","],
"189": ["dash", "-"],
"190": ["period", "."],
"191": ["slash", "forwardslash", "/"],
"192": ["graveaccent", "`"],
"219": ["openbracket", "["],
"220": ["backslash", "\\"],
"221": ["closebracket", "]"],
"222": ["apostrophe", "'"],
//0-9
"48": ["zero", "0"],
"49": ["one", "1"],
"50": ["two", "2"],
"51": ["three", "3"],
"52": ["four", "4"],
"53": ["five", "5"],
"54": ["six", "6"],
"55": ["seven", "7"],
"56": ["eight", "8"],
"57": ["nine", "9"],
//numpad
"96": ["numzero", "num0"],
"97": ["numone", "num1"],
"98": ["numtwo", "num2"],
"99": ["numthree", "num3"],
"100": ["numfour", "num4"],
"101": ["numfive", "num5"],
"102": ["numsix", "num6"],
"103": ["numseven", "num7"],
"104": ["numeight", "num8"],
"105": ["numnine", "num9"],
"106": ["nummultiply", "num*"],
"107": ["numadd", "num+"],
"108": ["numenter"],
"109": ["numsubtract", "num-"],
"110": ["numdecimal", "num."],
"111": ["numdivide", "num/"],
"144": ["numlock", "num"],
//function keys
"112": ["f1"],
"113": ["f2"],
"114": ["f3"],
"115": ["f4"],
"116": ["f5"],
"117": ["f6"],
"118": ["f7"],
"119": ["f8"],
"120": ["f9"],
"121": ["f10"],
"122": ["f11"],
"123": ["f12"]
},
"macros": [
//secondary key symbols
['shift + `', ["tilde", "~"]],
['shift + 1', ["exclamation", "exclamationpoint", "!"]],
['shift + 2', ["at", "@"]],
['shift + 3', ["number", "#"]],
['shift + 4', ["dollar", "dollars", "dollarsign", "$"]],
['shift + 5', ["percent", "%"]],
['shift + 6', ["caret", "^"]],
['shift + 7', ["ampersand", "and", "&"]],
['shift + 8', ["asterisk", "*"]],
['shift + 9', ["openparen", "("]],
['shift + 0', ["closeparen", ")"]],
['shift + -', ["underscore", "_"]],
['shift + =', ["plus", "+"]],
['shift + (', ["opencurlybrace", "opencurlybracket", "{"]],
['shift + )', ["closecurlybrace", "closecurlybracket", "}"]],
['shift + \\', ["verticalbar", "|"]],
['shift + ;', ["colon", ":"]],
['shift + \'', ["quotationmark", "\""]],
['shift + !,', ["openanglebracket", "<"]],
['shift + .', ["closeanglebracket", ">"]],
['shift + /', ["questionmark", "?"]]
]
};
//a-z and A-Z
for (aI = 65; aI <= 90; aI += 1) {
qwertyLayout.map[aI] = String.fromCharCode(aI + 32);
qwertyLayout.macros.push(
[
'shift + ' + String.fromCharCode(aI + 32) + ', capslock + ' + String.fromCharCode(aI + 32),
[String.fromCharCode(aI)]
]
);
}
registerLayout('qwerty', qwertyLayout);
setLayout('qwerty');
//////////
// INIT //
//////////
//enable the library
enable();
/////////
// API //
/////////
/**
* Version of this library.
*
* @property version
* @static
* @type String
*/
TangibleKeyboard.version = '@@version';
/**
* [read-only] An array of the names of all the currently active keys (i.e. keydown
* state). This array will include all the names of all the keys that are currently
* pressed as long as they are defined in the currently-active layout.
*
* @property activeKeys
* @readOnly
* @type {Array}
*/
TangibleKeyboard.activeKeys = activeKeys;
/**
* Enables the library. It should be noted that the library is enabled by default.
* This method is only useful if the library has been manually disabled through the
* `disable()` method.
*
* Enabling the library basically means attaching the appropriate listeners to the
* host environment.
*
* @method enable
*/
TangibleKeyboard.enable = enable;
/**
* Removes all active user-defined bindings and completely disables the library. The
* library can be re-enabled afterwards with the `enable()` method.
*
* @method disable
*/
TangibleKeyboard.disable = disable;
/**
* Removes a key from the array of currently active keys.
*
* @method removeActiveKey
* @param {String} keyName The name of the key.
*/
TangibleKeyboard.removeActiveKey = removeActiveKey;
/**
* Adds a key to the array of active keys (if it not there already).
*
* @method addActiveKey
* @param {String} keyName The name of the key.
*/
TangibleKeyboard.addActiveKey = addActiveKey;
/**
* Binds keys or combinations of keys to user-defined callback functions. At least one
* callback function (keydown or keyup) must be specified. The keys are defined by
* using a key selector string. This string is simply a list of key names separated
* by one of the following operators:
*
* + <code> </code> (space)
* + <code>,</code>
* + <code>+</code>
* + <code>></code>
*
* Here are some examples of valid selectors:
*
* + `'a' ` : Pressing the 'a' key will trigger the user-defined callbacks.
* + `'a, b' ` : Pressing the 'a' key or the 'b' key will trigger the
* user-defined callbacks.
* + `'a + b' ` : Simultaneously pressing both the 'a' and 'b' keys will trigger
* the user-defined callbacks.
* + `'a > b' ` : Pressing the 'a' key, holding it down and then pressing the 'b'
* key will trigger the user-defined callbacks.
* + `'a + b, b + c'` : Simultaneously pressing the 'a' and 'b' keys or the 'b' and 'c'
* keys will trigger the user-defined callbacks.
*
* #### Example ####
*
* var binding = TangibleKeyboard.on(
* 'a',
* {
* keyDownCallback: function(e, keys, combo) { console.log(e); },
* keyUpCallback: function(e, keys, combo) { console.log(e); },
* preventRepeat: true,
* preventDefault: true
* }
* );
*
* The `onDownCallback` is fired once the key or combo becomes active. The
* `onUpCallback` is fired when the key or combo is no longer active (as soon as a
* single key is released).
*
* When triggered, the user-defined callbacks are passed three arguments:
*
* 1. the keydown or keyup `Event` object (as received from the host environment)
* 2. the array of currently active keys
* 3. the key selector string
*
* @method on
*
* @param keySelector {String} The key selector is a string defining the key(s) or key
* combination(s) that will trigger the callbacks. See the documentation for the
* `TangibleKeyboard.on()` method for full key selector syntax.
* @param [options] {Object}
* @param [options.onKeyDown] {Function} The function to execute when the key(s)
* or key combination(s) are engaged.
* @param [options.onKeyUp] {Function} The function to execute when the key(s)
* or key combination(s) are disengaged.
* @param [options.preventRepeat=false] {Boolean} Whether to prevent repeated events
* from firing when the key is being held down.
* @param [options.preventDefault=false] {Boolean} Whether to prevent default browser
* callbacks from being triggered.
*
* @return {Object} This object contains a `clear` and an `on` property which are both
* reference to functions. You can use the `clear()` function to remove the binding
* when you are done.
*
* You can use the `on()` function to add additional callbacks for keyup and keydown
* events. You simply pass the event name as the first parameter and any number of
* callbacks that should be fired for that event as the second parameter.
*/
TangibleKeyboard.on = createBinding;
/**
* Clears all bindings attached to a given key selector string. The key name order
* does not matter as long as the key selectors equate.
*
* @method removeBindingByKeySelector
* @param {String} keySelector
*/
TangibleKeyboard.removeBindingByKeySelector = removeBindingByKeySelector;
/**
* Clears all bindings attached to key selectors matching the supplied key name.
*
* @method removeBindingByKeyName
* @param {String} keyName
*/
TangibleKeyboard.removeBindingByKeyName = removeBindingByKeyName;
/**
* Assigns a new key layout. The new layout must have been previously registered with
* the `registerLayout()` function. By default, only the 'qwerty' layout is
* registered.
*
* @method setLayout
*
* @param {String} layoutName The new layout to use.
* @return {Object}
*/
TangibleKeyboard.setLayout = setLayout;
/**
* Returns the currently-assigned layout.
*
* @method getLayout
* @return {String} The layout identifier.
*/
TangibleKeyboard.getLayout = getLayout;
/**
* Registers a new layout. This is useful if you would like to add support for a new
* keyboard layout. It could also be useful for alternative key names. For example, if
* you program games you could create a layout for your key mappings. Instead of key
* 65 mapped to 'a' you could map it to 'jump'.
*
* @method registerLayout
* @param {String} layoutName The name of the new key layout.
* @param {Object} layoutMap The layout map.
*/
TangibleKeyboard.registerLayout = registerLayout;
/**
* Registers a new key layout and immediately assigns it as the currently-active
* layout.
*
* @method registerAndSetLayout
* @param {String} layoutName The name of the new key layout.
* @param {Object} layoutMap The layout map.
*/
TangibleKeyboard.registerAndSetLayout = function (layoutName, layoutMap) {
registerLayout(layoutName, layoutMap);
setLayout(layoutName);
};
/**
* Accepts a key code and returns the key names defined by the current key layout.
*
* @method getKeyName
* @param {Number} keyCode
* @return {Array} keyNames An array of key names defined for the key code as
* defined by the current layout.
*/
TangibleKeyboard.getKeyName = getKeyName;
/**
* Accepts a key name and returns the key code defined by the current key layout.
*
* @method getKeyCode
* @param {String} keyName
* @return {Number|Boolean}
*/
TangibleKeyboard.getKeyCode = getKeyCode;
/**
* Transforms an array of key selectors into a single key selection string. If a
* single selector is passed in, it will be returned as is.
*
* @param {Array|String} keySelectorArray An array of key selectors. If a key
* selector string is passed instead, it will simply be returned.
* @return {String}
*/
TangibleKeyboard.stringify = stringifyKeyCombo;
/**
* Accepts a key selector and an array of key names to inject once the key selector is
* satisfied. TO COMPLETE!!
*
* @method createMacro
*
* @param {String} combo
* @param {Array} injectedKeys
*/
TangibleKeyboard.createMacro = createMacro;
/**
* Clears all macros bound to the specified key selector. TO COMPLETE!!
*
* @method removeMacro
* @param {String} combo
*/
TangibleKeyboard.removeMacro = removeMacro;
/**
* Checks to see if a key combo string or key array is satisfied by the currently
* active keys. It does not take into account spent keys.
*
* @param {String|Array} keyCombo A key combo string or array.
* @return {Boolean}
*/
TangibleKeyboard.combo = {};
TangibleKeyboard.combo.active = isSatisfiedCombo;
/**
* Parses a key combo string into a 3 dimensional array.
* - Level 1 - sub combos.
* - Level 2 - combo stages. A stage is a set of key name pairs that must
* be satisfied in the order they are defined.
* - Level 3 - each key name to the stage.
* @param {String|Array} keyCombo A key combo string.
* @return {Array}
*/
TangibleKeyboard.combo.parse = parseKeyCombo;
return TangibleKeyboard;
//////////////////////
// INSTANCE METHODS //
//////////////////////
function enable() {
if(targetWindow.addEventListener) {
targetWindow.document.addEventListener('keydown', keydown, false);
targetWindow.document.addEventListener('keyup', keyup, false);
targetWindow.addEventListener('blur', reset, false);
targetWindow.addEventListener('webkitfullscreenchange', reset, false);
targetWindow.addEventListener('mozfullscreenchange', reset, false);
} else if(targetWindow.attachEvent) {
targetWindow.document.attachEvent('onkeydown', keydown);
targetWindow.document.attachEvent('onkeyup', keyup);
targetWindow.attachEvent('onblur', reset);
}
}
function disable() {
reset();
if(targetWindow.removeEventListener) {
targetWindow.document.removeEventListener('keydown', keydown, false);
targetWindow.document.removeEventListener('keyup', keyup, false);
targetWindow.removeEventListener('blur', reset, false);
targetWindow.removeEventListener('webkitfullscreenchange', reset, false);
targetWindow.removeEventListener('mozfullscreenchange', reset, false);
} else if(targetWindow.detachEvent) {
targetWindow.document.detachEvent('onkeydown', keydown);
targetWindow.document.detachEvent('onkeyup', keyup);
targetWindow.detachEvent('onblur', reset);
}
}
////////////////////
// EVENT HANDLERS //
////////////////////
/**
* Exits all active bindings. Optionally passes an event to all binding handlers.
*
* @param {KeyboardEvent} [event]
*/
function reset(event) {
activeKeys = [];
pruneMacros();
pruneBindings(event);
}
// Key down event handler.
function keydown(event) {
var keyNames,
keyName,
kI;
keyNames = getKeyName(event.keyCode);
if(keyNames.length < 1) { return; }
event.isRepeat = false;
for(kI = 0; kI < keyNames.length; kI += 1) {
keyName = keyNames[kI];
if (activeKeys.indexOf(keyName) !== -1) {
event.isRepeat = true;
}
addActiveKey(keyName);
}
executeMacros();
executeBindings(event);
}
// Key up event handler.
function keyup(event) {
var keyNames, kI;
keyNames = getKeyName(event.keyCode);
if(keyNames.length < 1) { return; }
for(kI = 0; kI < keyNames.length; kI += 1) {
removeActiveKey(keyNames[kI]);
}
pruneMacros();
pruneBindings(event);
}
function getKeyName(keyCode) {
return map[keyCode] || [];
}
function getKeyCode(keyName) {
var keyCode;
for(keyCode in map) {
if(!map.hasOwnProperty(keyCode)) { continue; }
if(map[keyCode].indexOf(keyName) > -1) { return keyCode; }
}
return false;
}
////////////
// MACROS //
////////////
function createMacro(combo, injectedKeys) {
if(
typeof combo !== 'string' &&
(typeof combo !== 'object' || typeof combo.push !== 'function')
) {
throw new Error("Cannot create macro. The combo must be a string or array.");
}
if(typeof injectedKeys !== 'object' || typeof injectedKeys.push !== 'function') {
throw new Error("Cannot create macro. The injectedKeys must be an array.");
}
macros.push([combo, injectedKeys]);
}
function removeMacro(combo) {
var macro;
if(
typeof combo !== 'string' &&
(typeof combo !== 'object' || typeof combo.push !== 'function')
) {
throw new Error("Cannot remove macro. The combo must be a string or array.");
}
for(var mI = 0; mI < macros.length; mI += 1) {
macro = macros[mI];
if(compareCombos(combo, macro[0])) {
removeActiveKey(macro[1]);
macros.splice(mI, 1);
break;
}
}
}
/**
* Executes macros against the active keys. Each macro's key combo is checked and if
* found to be satisfied, the macro's key names are injected into active keys.
*/
function executeMacros() {
var mI, combo, kI;
for(mI = 0; mI < macros.length; mI += 1) {
combo = parseKeyCombo(macros[mI][0]);
if(activeMacros.indexOf(macros[mI]) === -1 && isSatisfiedCombo(combo)) {
activeMacros.push(macros[mI]);
for(kI = 0; kI < macros[mI][1].length; kI += 1) {
addActiveKey(macros[mI][1][kI]);
}
}
}
}
/**
* Prunes active macros. Checks each active macro's key combo and if found to no
* longer be satisfied, each of the macro's key names are removed from active keys.
*/
function pruneMacros() {
var mI, combo, kI;
for(mI = 0; mI < activeMacros.length; mI += 1) {
combo = parseKeyCombo(activeMacros[mI][0]);
if(isSatisfiedCombo(combo) === false) {
for(kI = 0; kI < activeMacros[mI][1].length; kI += 1) {
removeActiveKey(activeMacros[mI][1][kI]);
}
activeMacros.splice(mI, 1);
mI -= 1;
}
}
}
//////////////
// BINDINGS //
//////////////
//function createBinding(keyCombo, keyDownCallback, keyUpCallback, options) {
function createBinding(keyCombo, options) {
var api = {},
binding,
subBindings = [],
kI,
subCombo,
keyDownCallback,
keyUpCallback;
options = options || {};
keyDownCallback = options.onKeyDown || undefined;
keyUpCallback = options.onKeyUp || undefined;
if (keyDownCallback === undefined && keyUpCallback === undefined) {
throw new Error('A keydown or keyup callback must be specified.');
}
if (keyDownCallback !== undefined && typeof(keyDownCallback) !== "function") {
throw new Error('Keydown callback must be a function.');
}
if (keyUpCallback !== undefined && typeof(keyUpCallback) !== "function") {
throw new Error('Keyup callback must be a function.');
}
//break the combo down into a combo array
if(typeof keyCombo === 'string') {
keyCombo = parseKeyCombo(keyCombo);
}
//bind each sub combo contained within the combo string
for(kI = 0; kI < keyCombo.length; kI += 1) {
binding = {};
//stringify the combo again
subCombo = stringifyKeyCombo([keyCombo[kI]]);
//validate the sub combo
if(typeof subCombo !== 'string') {
throw new Error(
'Failed to bind key combo. The key combo must be string.'
);
}
//create the binding
binding.keyCombo = subCombo;
binding.keyDownCallback = [];
binding.keyUpCallback = [];
binding.options = {
preventDefault: options.preventDefault || false,
preventRepeat: options.preventRepeat || false
};
//inject the key down and key up callbacks if given
if(keyDownCallback) { binding.keyDownCallback.push(keyDownCallback); }
if(keyUpCallback) { binding.keyUpCallback.push(keyUpCallback); }
//stash the new binding
bindings.push(binding);
subBindings.push(binding);
}
//build the binding api
api.clear = clear;
api.on = on;
return api;
/**
* Clears the binding
*/
function clear() {
var bI;
for(bI = 0; bI < subBindings.length; bI += 1) {
bindings.splice(bindings.indexOf(subBindings[bI]), 1);
}
}
/**
* Adds any number of callbacks to the event specified by name in the first
* parameter (`keyup` or `keydown`).
*
* @param {String} eventName
* @return {Object} subBinding
*/
function on(eventName ) {
var api = {}, callbacks, cI, bI;
//validate event name
if(typeof eventName !== 'string') {
throw new Error('Cannot bind callback. The event name must be a string.');
}
if(eventName !== 'keyup' && eventName !== 'keydown') {
throw new Error(
'Cannot bind callback. The event name must be a "keyup" or "keydown".'
);
}
//gather the callbacks
callbacks = Array.prototype.slice.apply(arguments, [1]);
//stash each the new binding
for(cI = 0; cI < callbacks.length; cI += 1) {
if(typeof callbacks[cI] === 'function') {
if(eventName === 'keyup') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyUpCallback.push(callbacks[cI]);
}
} else if(eventName === 'keydown') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyDownCallback.push(callbacks[cI]);
}
}
}
}
//construct and return the sub binding api
api.clear = function () {
var cI, bI;
for(cI = 0; cI < callbacks.length; cI += 1) {
if(typeof callbacks[cI] === 'function') {
if(eventName === 'keyup') {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyUpCallback.splice(
subBindings[bI].keyUpCallback.indexOf(callbacks[cI]),
1
);
}
} else {
for(bI = 0; bI < subBindings.length; bI += 1) {
subBindings[bI].keyDownCallback.splice(
subBindings[bI].keyDownCallback.indexOf(callbacks[cI]),
1
);
}
}
}
}
};
return api;
}
}
function removeBindingByKeySelector(keyCombo) {
var bI, binding;
for(bI = 0; bI < bindings.length; bI += 1) {
binding = bindings[bI];
if(compareCombos(keyCombo, binding.keyCombo)) {
bindings.splice(bI, 1); bI -= 1;
}
}
}
function removeBindingByKeyName(keyName) {
var bI, kI, binding;
if(keyName) {
for(bI = 0; bI < bindings.length; bI += 1) {
binding = bindings[bI];
for(kI = 0; kI < binding.keyCombo.length; kI += 1) {
if(binding.keyCombo[kI].indexOf(keyName) > -1) {
bindings.splice(bI, 1); bI -= 1;
break;
}
}
}
} else {
bindings = [];
}
}
/**
* Executes bindings that are active. Only allows the keys to be used once as to
* prevent binding overlap.
*
* @param {KeyboardEvent} event The keyboard event.
*/
function executeBindings(event) {
var bI,
sBI,
binding,
bindingKeys,
remainingKeys,
cI,
killEventBubble,
kI,
bindingKeysSatisfied,
index,
sortedBindings = [],
bindingWeight;
remainingKeys = [].concat(activeKeys);
// Sort bindings ?
for(bI = 0; bI < bindings.length; bI += 1) {
bindingWeight = extractComboKeys(bindings[bI].keyCombo).length;
if(!sortedBindings[bindingWeight]) { sortedBindings[bindingWeight] = []; }
sortedBindings[bindingWeight].push(bindings[bI]);
}
for(sBI = sortedBindings.length - 1; sBI >= 0; sBI -= 1) {
if(!sortedBindings[sBI]) { continue; }
for(bI = 0; bI < sortedBindings[sBI].length; bI += 1) {
binding = sortedBindings[sBI][bI];
bindingKeys = extractComboKeys(binding.keyCombo);
bindingKeysSatisfied = true;
for(kI = 0; kI < bindingKeys.length; kI += 1) {
if(remainingKeys.indexOf(bindingKeys[kI]) === -1) {
bindingKeysSatisfied = false;
break;
}
}
if(bindingKeysSatisfied && isSatisfiedCombo(binding.keyCombo)) {
if (event.isRepeat && binding.options.preventRepeat) {
// not a repeat or repeat prevention not enabled
} else if (event.isRepeat && event.type === "keydown") {
// is a repeat but is a keydown
} else {
activeBindings.push(binding);
}
for(kI = 0; kI < bindingKeys.length; kI += 1) {
index = remainingKeys.indexOf(bindingKeys[kI]);
if(index > -1) {
remainingKeys.splice(index, 1);
kI -= 1;
}
}
for(cI = 0; cI < binding.keyDownCallback.length; cI += 1) {
if (
binding.options.preventRepeat === true &&
event.isRepeat === true
) {
continue;
}
// Execute the callback
if (
binding.keyDownCallback[cI](
event, activeKeys, binding.keyCombo
) === false
) {
killEventBubble = true;
}
}
if (
killEventBubble === true ||
binding.options.preventDefault === true
) {
event.preventDefault();
event.stopPropagation();
}
}
}
}
}
/**
* Removes bindings that are no longer satisfied by the active keys. Also fires the
* key up callbacks.
*
* @param {KeyboardEvent} event
*/
function pruneBindings(event) {
var bI,
cI,
binding,
killEventBubble;
for (bI = 0; bI < activeBindings.length; bI += 1) {
binding = activeBindings[bI];
if(isSatisfiedCombo(binding.keyCombo) === false) {
for(cI = 0; cI < binding.keyUpCallback.length; cI += 1) {
if (
binding.keyUpCallback[cI](
event,
activeKeys,
binding.keyCombo
) === false) {
killEventBubble = true;
}
}
if(
killEventBubble === true ||
binding.options.preventDefault === true
) {
event.preventDefault();
event.stopPropagation();
}
activeBindings.splice(bI, 1);
bI -= 1;
}
}
}
///////////////////
// COMBO STRINGS //
///////////////////
/**
* Compares two key combos and return true when they are functionally equivalent.
*
* @param {String|Array} keyComboArrayA keyCombo A key combo string or array.
* @param {String|Array} keyComboArrayB keyCombo A key combo string or array.
* @return {Boolean}
*/
function compareCombos(keyComboArrayA, keyComboArrayB) {
var cI, sI, kI;
keyComboArrayA = parseKeyCombo(keyComboArrayA);
keyComboArrayB = parseKeyCombo(keyComboArrayB);
if(keyComboArrayA.length !== keyComboArrayB.length) { return false; }
for(cI = 0; cI < keyComboArrayA.length; cI += 1) {
if(keyComboArrayA[cI].length !== keyComboArrayB[cI].length) {
return false;
}
for(sI = 0; sI < keyComboArrayA[cI].length; sI += 1) {
if(keyComboArrayA[cI][sI].length !== keyComboArrayB[cI][sI].length) {
return false;
}
for(kI = 0; kI < keyComboArrayA[cI][sI].length; kI += 1) {
if(keyComboArrayB[cI][sI].indexOf(keyComboArrayA[cI][sI][kI]) === -1) {
return false;
}
}
}
}
return true;
}
function isSatisfiedCombo(keyCombo) {
var cI, sI, stage, kI, stageOffset = 0, index, comboMatches;
keyCombo = parseKeyCombo(keyCombo);
for(cI = 0; cI < keyCombo.length; cI += 1) {
comboMatches = true;
stageOffset = 0;
for(sI = 0; sI < keyCombo[cI].length; sI += 1) {
stage = [].concat(keyCombo[cI][sI]);
for(kI = stageOffset; kI < activeKeys.length; kI += 1) {
index = stage.indexOf(activeKeys[kI]);
if(index > -1) {
stage.splice(index, 1);
stageOffset = kI;
}
}
if(stage.length !== 0) { comboMatches = false; break; }
}
if(comboMatches) { return true; }
}
return false;
}
/**
* Accepts a key combo array or string and returns a flat array containing all keys
* referenced by the key combo.
*
* @param {String|Array} keyCombo A key combo string or array.
* @return {Array}
*/
function extractComboKeys(keyCombo) {
var cI, sI, keys = [];
keyCombo = parseKeyCombo(keyCombo);
for(cI = 0; cI < keyCombo.length; cI += 1) {
for(sI = 0; sI < keyCombo[cI].length; sI += 1) {
keys = keys.concat(keyCombo[cI][sI]);
}
}
return keys;
}
function parseKeyCombo(keyCombo) {
var s = keyCombo,
i = 0,
op = 0,
ws = false,
nc = false,
combos = [],
combo = [],
stage = [],
key = '';
if(typeof keyCombo === 'object' && typeof keyCombo.push === 'function') {
return keyCombo;
}
if(typeof keyCombo !== 'string') {
throw new Error(
'Cannot parse "keyCombo" because its type is "' + (typeof keyCombo) +
'". It must be a "string".'
);
}
//remove leading whitespace
while(s.charAt(i) === ' ') { i += 1; }
while(true) {
if(s.charAt(i) === ' ') {
//white space & next combo op
while(s.charAt(i) === ' ') { i += 1; }
ws = true;
} else if(s.charAt(i) === ',') {
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected , at character index ' + i + '.'); }
nc = true;
i += 1;
} else if(s.charAt(i) === '+') {
//next key
if(key.length) { stage.push(key); key = ''; }
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected + at character index ' + i + '.'); }
op = true;
i += 1;
} else if(s.charAt(i) === '>') {
//next stage op
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(op || nc) { throw new Error('Failed to parse key combo. Unexpected > at character index ' + i + '.'); }
op = true;
i += 1;
} else if(i < s.length - 1 && s.charAt(i) === '!' && (s.charAt(i + 1) === '>' || s.charAt(i + 1) === ',' || s.charAt(i + 1) === '+')) {
key += s.charAt(i + 1);
op = false;
ws = false;
nc = false;
i += 2;
} else if(i < s.length && s.charAt(i) !== '+' && s.charAt(i) !== '>' && s.charAt(i) !== ',' && s.charAt(i) !== ' ') {
//end combo
if(op === false && ws === true || nc === true) {
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(combo.length) { combos.push(combo); combo = []; }
}
op = false;
ws = false;
nc = false;
//key
while(i < s.length && s.charAt(i) !== '+' && s.charAt(i) !== '>' && s.charAt(i) !== ',' && s.charAt(i) !== ' ') {
key += s.charAt(i);
i += 1;
}
} else {
//unknown char
i += 1;
continue;
}
//end of combos string
if(i >= s.length) {
if(key.length) { stage.push(key); key = ''; }
if(stage.length) { combo.push(stage); stage = []; }
if(combo.length) { combos.push(combo); combo = []; }
break;
}
}
return combos;
}
function stringifyKeyCombo(keyComboArray) {
var cI, ccI, output = [];
if(typeof keyComboArray === 'string') { return keyComboArray; }
if(
typeof keyComboArray !== 'object' ||
typeof keyComboArray.push !== 'function') {
throw new Error('Cannot stringify key combo.');
}
for(cI = 0; cI < keyComboArray.length; cI += 1) {
output[cI] = [];
for(ccI = 0; ccI < keyComboArray[cI].length; ccI += 1) {
output[cI][ccI] = keyComboArray[cI][ccI].join(' + ');
}
output[cI] = output[cI].join(' > ');
}
return output.join(' ');
}
/////////////////
// ACTIVE KEYS //
/////////////////
function addActiveKey(keyName) {
if(keyName.match(/\s/)) {
throw new Error(
'Cannot add key name "' + keyName + '" to active keys because it ' +
'contains whitespace.'
);
}
if(activeKeys.indexOf(keyName) > -1) { return; }
activeKeys.push(keyName);
}
function removeActiveKey(keyName) {
var keyCode = getKeyCode(keyName);
if(keyCode === '91' || keyCode === '92') {
activeKeys = []; //remove all key on release of super.
} else {
activeKeys.splice(activeKeys.indexOf(keyName), 1);
}
}
/////////////
// LAYOUTS //
/////////////
function registerLayout(layoutName, layoutMap) {
// Validate arguments
if(typeof layoutName !== 'string') {
throw new Error(
'Cannot register new layout. The layout name must be a string.'
);
}
if(typeof layoutMap !== 'object') {
throw new Error(
'Cannot register "' + layoutName + '" layout. The layout map must be ' +
'an object.'
);
}
if(typeof layoutMap.map !== 'object') {
throw new Error(
'Cannot register "' + layoutName + '" layout. The layout map is invalid.'
);
}
// Stash the layout
if(!layoutMap.macros) { layoutMap.macros = []; }
layouts[layoutName] = layoutMap;
}
function setLayout(layoutName) {
if (typeof layoutName !== 'string') {
throw new Error('Cannot set layout. The layout name must be a string.');
}
if (!layouts[layoutName]) {
throw new Error(
'Cannot set layout to "' + layoutName + '" because no such layout ' +
'has been registered.');
}
// Set the requested map and macros
map = layouts[layoutName].map;
macros = layouts[layoutName].macros;
// Set the current layout
layout = layoutName;
}
function getLayout() {
return layout;
}
});