/** * Title: KeyboardJS * Version: v0.4.1 * Description: KeyboardJS is a flexible and easy to use keyboard binding * library. * Author: Robert Hurst. * * Copyright 2011, Robert William Hurst * Licenced under the BSD License. * See https://raw.github.com/RobertWHurst/KeyboardJS/master/license.txt */ (function(context, factory) { //INDEXOF POLLYFILL [].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;}); //AMD if(typeof define === 'function' && define.amd) { define(constructAMD); } //CommonJS else if(typeof module !== 'undefined') {constructCommonJS()} //GLOBAL else { 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); return; //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('KeyboardJS', 'k'); return library; //sets library namespaces function noConflict( ) { var args, 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) { var KeyboardJS = {}, locales = {}, locale, map, macros, activeKeys = [], bindings = [], activeBindings = [], activeMacros = [], aI, usLocale; targetWindow = targetWindow || window; /////////////////////// // DEFAULT US LOCALE // /////////////////////// //define US locale //If you create a new locale please submit it as a pull request or post // it in the issue tracker at // http://github.com/RobertWhurst/KeyboardJS/issues/ usLocale = { "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) { usLocale.map[aI] = String.fromCharCode(aI + 32); usLocale.macros.push(['shift + ' + String.fromCharCode(aI + 32) + ', capslock + ' + String.fromCharCode(aI + 32), [String.fromCharCode(aI)]]); } registerLocale('us', usLocale); getSetLocale('us'); ////////// // INIT // ////////// //enable the library enable(); ///////// // API // ///////// //assemble the library and return it KeyboardJS.enable = enable; KeyboardJS.disable = disable; KeyboardJS.activeKeys = getActiveKeys; KeyboardJS.releaseKey = removeActiveKey; KeyboardJS.pressKey = addActiveKey; KeyboardJS.on = createBinding; KeyboardJS.clear = removeBindingByKeyCombo; KeyboardJS.clear.key = removeBindingByKeyName; KeyboardJS.locale = getSetLocale; KeyboardJS.locale.register = registerLocale; KeyboardJS.macro = createMacro; KeyboardJS.macro.remove = removeMacro; KeyboardJS.key = {}; KeyboardJS.key.name = getKeyName; KeyboardJS.key.code = getKeyCode; KeyboardJS.combo = {}; KeyboardJS.combo.active = isSatisfiedCombo; KeyboardJS.combo.parse = parseKeyCombo; KeyboardJS.combo.stringify = stringifyKeyCombo; return KeyboardJS; ////////////////////// // INSTANCE METHODS // ////////////////////// /** * Enables KeyboardJS */ 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); } } /** * Exits all active bindings and disables KeyboardJS */ 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 [Optional] */ function reset(event) { activeKeys = []; pruneMacros(); pruneBindings(event); } /** * Key down event handler. * @param {KeyboardEvent} event */ 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 (getActiveKeys().indexOf(keyName) != -1) event.isRepeat = true; addActiveKey(keyName); } executeMacros(); executeBindings(event); } /** * Key up event handler. * @param {KeyboardEvent} event */ 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); } /** * Accepts a key code and returns the key names defined by the current * locale. * @param {Number} keyCode * @return {Array} keyNames An array of key names defined for the key * code as defined by the current locale. */ function getKeyName(keyCode) { return map[keyCode] || []; } /** * Accepts a key name and returns the key code defined by the current * locale. * @param {Number} keyName * @return {Number|false} */ 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 // //////////// /** * Accepts a key combo and an array of key names to inject once the key * combo is satisfied. * @param {String} combo * @param {Array} injectedKeys */ 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]); } /** * Accepts a key combo and clears any and all macros bound to that key * combo. * @param {String} combo */ 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(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 to 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 // ////////////// /** * Creates a binding object, and, if provided, binds a key down hander and * a key up handler. Returns a binding object that emits keyup and * keydown events. * @param {String} keyCombo * @param {Function} keyDownCallback [Optional] * @param {Function} keyUpCallback [Optional] * @return {Object} binding */ function createBinding(keyCombo, keyDownCallback, keyUpCallback) { var api = {}, binding, subBindings = [], bindingApi = {}, kI, subCombo; //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 = []; //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); } } /** * Accepts an event name. and any number of callbacks. When the event is * emitted, all callbacks are executed. Available events are key up and * key down. * @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 = clear; return api; /** * Clears the binding */ function clear() { 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); } } } } } } } /** * Clears all binding attached to a given key combo. Key name order does not * matter as long as the key combos equate. * @param {String} keyCombo */ function removeBindingByKeyCombo(keyCombo) { var bI, binding, keyName; for(bI = 0; bI < bindings.length; bI += 1) { binding = bindings[bI]; if(compareCombos(keyCombo, binding.keyCombo)) { bindings.splice(bI, 1); bI -= 1; } } } /** * Clears all binding attached to key combos containing a given key name. * @param {String} keyName */ 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); 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)) { 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.keyDownCallback[cI](event, getActiveKeys(), binding.keyCombo) === false) { killEventBubble = true; } } if(killEventBubble === 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, getActiveKeys(), binding.keyCombo) === false) { killEventBubble = true; } } if(killEventBubble === true) { event.preventDefault(); event.stopPropagation(); } activeBindings.splice(bI, 1); bI -= 1; } } } /////////////////// // COMBO STRINGS // /////////////////// /** * Compares two key combos returning true when they are functionally * equivalent. * @param {String} keyComboArrayA keyCombo A key combo string or array. * @param {String} 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; } /** * 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} keyCombo A key combo string or array. * @return {Boolean} */ 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} keyCombo A key combo string or array. * @return {Array} */ function extractComboKeys(keyCombo) { var cI, sI, kI, 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; } /** * 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} */ 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; } /** * Stringifys a key combo. * @param {Array|String} keyComboArray A key combo array. If a key * combo string is given it will be returned. * @return {String} */ 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 // ///////////////// /** * Returns the a copy of the active keys array. * @return {Array} */ function getActiveKeys() { return [].concat(activeKeys); } /** * Adds a key to the active keys array, but only if it has not already been * added. * @param {String} keyName The key name string. */ 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); } /** * Removes a key from the active keys array. * @param {String} keyNames The key name string. */ 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); } } ///////////// // LOCALES // ///////////// /** * Registers a new locale. 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 locale for your key mappings. Instead of key 65 mapped * to 'a' you could map it to 'jump'. * @param {String} localeName The name of the new locale. * @param {Object} localeMap The locale map. */ function registerLocale(localeName, localeMap) { //validate arguments if(typeof localeName !== 'string') { throw new Error('Cannot register new locale. The locale name must be a string.'); } if(typeof localeMap !== 'object') { throw new Error('Cannot register ' + localeName + ' locale. The locale map must be an object.'); } if(typeof localeMap.map !== 'object') { throw new Error('Cannot register ' + localeName + ' locale. The locale map is invalid.'); } //stash the locale if(!localeMap.macros) { localeMap.macros = []; } locales[localeName] = localeMap; } /** * Swaps the current locale. * @param {String} localeName The locale to activate. * @return {Object} */ function getSetLocale(localeName) { //if a new locale is given then set it if(localeName) { if(typeof localeName !== 'string') { throw new Error('Cannot set locale. The locale name must be a string.'); } if(!locales[localeName]) { throw new Error('Cannot set locale to ' + localeName + ' because it does not exist. If you would like to submit a ' + localeName + ' locale map for KeyboardJS please submit it at https://github.com/RobertWHurst/KeyboardJS/issues.'); } //set the current map and macros map = locales[localeName].map; macros = locales[localeName].macros; //set the current locale locale = localeName; } //return the current locale return locale; } });