From da2907e9596660ead9cf9d71f1e5169b8ed0eb1b Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Mon, 30 Mar 2015 18:26:40 -0700 Subject: [PATCH] use keyboardjs instead of custom version --- public/js/keyboard.js | 961 +++++++++++++++++++++++++++++++ views/layout.erb | 1 + views/site_files/text_editor.erb | 10 + 3 files changed, 972 insertions(+) create mode 100644 public/js/keyboard.js diff --git a/public/js/keyboard.js b/public/js/keyboard.js new file mode 100644 index 00000000..5a518d7d --- /dev/null +++ b/public/js/keyboard.js @@ -0,0 +1,961 @@ +/** + * 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"]], + ['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; + } +}); diff --git a/views/layout.erb b/views/layout.erb index 6163e22c..1ea6d224 100644 --- a/views/layout.erb +++ b/views/layout.erb @@ -47,6 +47,7 @@ +