| var kbdUtil = (function() { |
| "use strict"; |
| |
| function substituteCodepoint(cp) { |
| // Any Unicode code points which do not have corresponding keysym entries |
| // can be swapped out for another code point by adding them to this table |
| var substitutions = { |
| // {S,s} with comma below -> {S,s} with cedilla |
| 0x218 : 0x15e, |
| 0x219 : 0x15f, |
| // {T,t} with comma below -> {T,t} with cedilla |
| 0x21a : 0x162, |
| 0x21b : 0x163 |
| }; |
| |
| var sub = substitutions[cp]; |
| return sub ? sub : cp; |
| } |
| |
| function isMac() { |
| return navigator && !!(/mac/i).exec(navigator.platform); |
| } |
| function isWindows() { |
| return navigator && !!(/win/i).exec(navigator.platform); |
| } |
| function isLinux() { |
| return navigator && !!(/linux/i).exec(navigator.platform); |
| } |
| |
| // Return true if a modifier which is not the specified char modifier (and is not shift) is down |
| function hasShortcutModifier(charModifier, currentModifiers) { |
| var mods = {}; |
| for (var key in currentModifiers) { |
| if (parseInt(key) !== 0xffe1) { |
| mods[key] = currentModifiers[key]; |
| } |
| } |
| |
| var sum = 0; |
| for (var k in currentModifiers) { |
| if (mods[k]) { |
| ++sum; |
| } |
| } |
| if (hasCharModifier(charModifier, mods)) { |
| return sum > charModifier.length; |
| } |
| else { |
| return sum > 0; |
| } |
| } |
| |
| // Return true if the specified char modifier is currently down |
| function hasCharModifier(charModifier, currentModifiers) { |
| if (charModifier.length === 0) { return false; } |
| |
| for (var i = 0; i < charModifier.length; ++i) { |
| if (!currentModifiers[charModifier[i]]) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Helper object tracking modifier key state |
| // and generates fake key events to compensate if it gets out of sync |
| function ModifierSync(charModifier) { |
| var ctrl = 0xffe3; |
| var alt = 0xffe9; |
| var altGr = 0xfe03; |
| var shift = 0xffe1; |
| var meta = 0xffe7; |
| |
| if (!charModifier) { |
| if (isMac()) { |
| // on Mac, Option (AKA Alt) is used as a char modifier |
| charModifier = [alt]; |
| } |
| else if (isWindows()) { |
| // on Windows, Ctrl+Alt is used as a char modifier |
| charModifier = [alt, ctrl]; |
| } |
| else if (isLinux()) { |
| // on Linux, AltGr is used as a char modifier |
| charModifier = [altGr]; |
| } |
| else { |
| charModifier = []; |
| } |
| } |
| |
| var state = {}; |
| state[ctrl] = false; |
| state[alt] = false; |
| state[altGr] = false; |
| state[shift] = false; |
| state[meta] = false; |
| |
| function sync(evt, keysym) { |
| var result = []; |
| function syncKey(keysym) { |
| return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; |
| } |
| |
| if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { |
| state[ctrl] = evt.ctrlKey; |
| result.push(syncKey(ctrl)); |
| } |
| if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { |
| state[alt] = evt.altKey; |
| result.push(syncKey(alt)); |
| } |
| if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { |
| state[altGr] = evt.altGraphKey; |
| result.push(syncKey(altGr)); |
| } |
| if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { |
| state[shift] = evt.shiftKey; |
| result.push(syncKey(shift)); |
| } |
| if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { |
| state[meta] = evt.metaKey; |
| result.push(syncKey(meta)); |
| } |
| return result; |
| } |
| function syncKeyEvent(evt, down) { |
| var obj = getKeysym(evt); |
| var keysym = obj ? obj.keysym : null; |
| |
| // first, apply the event itself, if relevant |
| if (keysym !== null && state[keysym] !== undefined) { |
| state[keysym] = down; |
| } |
| return sync(evt, keysym); |
| } |
| |
| return { |
| // sync on the appropriate keyboard event |
| keydown: function(evt) { return syncKeyEvent(evt, true);}, |
| keyup: function(evt) { return syncKeyEvent(evt, false);}, |
| // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway |
| syncAny: function(evt) { return sync(evt);}, |
| |
| // is a shortcut modifier down? |
| hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, |
| // if a char modifier is down, return the keys it consists of, otherwise return null |
| activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } |
| }; |
| } |
| |
| // Get a key ID from a keyboard event |
| // May be a string or an integer depending on the available properties |
| function getKey(evt){ |
| if ('keyCode' in evt && 'key' in evt) { |
| return evt.key + ':' + evt.keyCode; |
| } |
| else if ('keyCode' in evt) { |
| return evt.keyCode; |
| } |
| else { |
| return evt.key; |
| } |
| } |
| |
| // Get the most reliable keysym value we can get from a key event |
| // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which |
| function getKeysym(evt){ |
| var codepoint; |
| if (evt.char && evt.char.length === 1) { |
| codepoint = evt.char.charCodeAt(); |
| } |
| else if (evt.charCode) { |
| codepoint = evt.charCode; |
| } |
| else if (evt.keyCode && evt.type === 'keypress') { |
| // IE10 stores the char code as keyCode, and has no other useful properties |
| codepoint = evt.keyCode; |
| } |
| if (codepoint) { |
| var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); |
| if (res) { |
| return res; |
| } |
| } |
| // we could check evt.key here. |
| // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, |
| // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key |
| // so we don't *need* it yet |
| if (evt.keyCode) { |
| return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); |
| } |
| if (evt.which) { |
| return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); |
| } |
| return null; |
| } |
| |
| // Given a keycode, try to predict which keysym it might be. |
| // If the keycode is unknown, null is returned. |
| function keysymFromKeyCode(keycode, shiftPressed) { |
| if (typeof(keycode) !== 'number') { |
| return null; |
| } |
| // won't be accurate for azerty |
| if (keycode >= 0x30 && keycode <= 0x39) { |
| return keycode; // digit |
| } |
| if (keycode >= 0x41 && keycode <= 0x5a) { |
| // remap to lowercase unless shift is down |
| return shiftPressed ? keycode : keycode + 32; // A-Z |
| } |
| if (keycode >= 0x60 && keycode <= 0x69) { |
| return 0xffb0 + (keycode - 0x60); // numpad 0-9 |
| } |
| |
| switch(keycode) { |
| case 0x20: return 0x20; // space |
| case 0x6a: return 0xffaa; // multiply |
| case 0x6b: return 0xffab; // add |
| case 0x6c: return 0xffac; // separator |
| case 0x6d: return 0xffad; // subtract |
| case 0x6e: return 0xffae; // decimal |
| case 0x6f: return 0xffaf; // divide |
| case 0xbb: return 0x2b; // + |
| case 0xbc: return 0x2c; // , |
| case 0xbd: return 0x2d; // - |
| case 0xbe: return 0x2e; // . |
| } |
| |
| return nonCharacterKey({keyCode: keycode}); |
| } |
| |
| // if the key is a known non-character key (any key which doesn't generate character data) |
| // return its keysym value. Otherwise return null |
| function nonCharacterKey(evt) { |
| // evt.key not implemented yet |
| if (!evt.keyCode) { return null; } |
| var keycode = evt.keyCode; |
| |
| if (keycode >= 0x70 && keycode <= 0x87) { |
| return 0xffbe + keycode - 0x70; // F1-F24 |
| } |
| switch (keycode) { |
| |
| case 8 : return 0xFF08; // BACKSPACE |
| case 13 : return 0xFF0D; // ENTER |
| |
| case 9 : return 0xFF09; // TAB |
| |
| case 27 : return 0xFF1B; // ESCAPE |
| case 46 : return 0xFFFF; // DELETE |
| |
| case 36 : return 0xFF50; // HOME |
| case 35 : return 0xFF57; // END |
| case 33 : return 0xFF55; // PAGE_UP |
| case 34 : return 0xFF56; // PAGE_DOWN |
| case 45 : return 0xFF63; // INSERT |
| |
| case 37 : return 0xFF51; // LEFT |
| case 38 : return 0xFF52; // UP |
| case 39 : return 0xFF53; // RIGHT |
| case 40 : return 0xFF54; // DOWN |
| case 16 : return 0xFFE1; // SHIFT |
| case 17 : return 0xFFE3; // CONTROL |
| case 18 : return 0xFFE9; // Left ALT (Mac Option) |
| |
| case 224 : return 0xFE07; // Meta |
| case 225 : return 0xFE03; // AltGr |
| case 91 : return 0xFFEC; // Super_L (Win Key) |
| case 92 : return 0xFFED; // Super_R (Win Key) |
| case 93 : return 0xFF67; // Menu (Win Menu), Mac Command |
| default: return null; |
| } |
| } |
| return { |
| hasShortcutModifier : hasShortcutModifier, |
| hasCharModifier : hasCharModifier, |
| ModifierSync : ModifierSync, |
| getKey : getKey, |
| getKeysym : getKeysym, |
| keysymFromKeyCode : keysymFromKeyCode, |
| nonCharacterKey : nonCharacterKey, |
| substituteCodepoint : substituteCodepoint |
| }; |
| })(); |
| |
| // Takes a DOM keyboard event and: |
| // - determines which keysym it represents |
| // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) |
| // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down |
| // - marks each event with an 'escape' property if a modifier was down which should be "escaped" |
| // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown |
| // This information is collected into an object which is passed to the next() function. (one call per event) |
| function KeyEventDecoder(modifierState, next) { |
| "use strict"; |
| function sendAll(evts) { |
| for (var i = 0; i < evts.length; ++i) { |
| next(evts[i]); |
| } |
| } |
| function process(evt, type) { |
| var result = {type: type}; |
| var keyId = kbdUtil.getKey(evt); |
| if (keyId) { |
| result.keyId = keyId; |
| } |
| |
| var keysym = kbdUtil.getKeysym(evt); |
| |
| var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); |
| // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? |
| // "special" keys like enter, tab or backspace don't send keypress events, |
| // and some browsers don't send keypresses at all if a modifier is down |
| if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { |
| result.keysym = keysym; |
| } |
| |
| var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; |
| |
| // Should we prevent the browser from handling the event? |
| // Doing so on a keydown (in most browsers) prevents keypress from being generated |
| // so only do that if we have to. |
| var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); |
| |
| // If a char modifier is down on a keydown, we need to insert a stall, |
| // so VerifyCharModifier knows to wait and see if a keypress is comnig |
| var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); |
| |
| // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) |
| var active = modifierState.activeCharModifier(); |
| |
| // If we have a char modifier down, and we're able to determine a keysym reliably |
| // then (a) we know to treat the modifier as a char modifier, |
| // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. |
| if (active && keysym) { |
| var isCharModifier = false; |
| for (var i = 0; i < active.length; ++i) { |
| if (active[i] === keysym.keysym) { |
| isCharModifier = true; |
| } |
| } |
| if (type === 'keypress' && !isCharModifier) { |
| result.escape = modifierState.activeCharModifier(); |
| } |
| } |
| |
| if (stall) { |
| // insert a fake "stall" event |
| next({type: 'stall'}); |
| } |
| next(result); |
| |
| return suppress; |
| } |
| |
| return { |
| keydown: function(evt) { |
| sendAll(modifierState.keydown(evt)); |
| return process(evt, 'keydown'); |
| }, |
| keypress: function(evt) { |
| return process(evt, 'keypress'); |
| }, |
| keyup: function(evt) { |
| sendAll(modifierState.keyup(evt)); |
| return process(evt, 'keyup'); |
| }, |
| syncModifiers: function(evt) { |
| sendAll(modifierState.syncAny(evt)); |
| }, |
| releaseAll: function() { next({type: 'releaseall'}); } |
| }; |
| } |
| |
| // Combines keydown and keypress events where necessary to handle char modifiers. |
| // On some OS'es, a char modifier is sometimes used as a shortcut modifier. |
| // For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing |
| // so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not. |
| // The only way we can distinguish these cases is to wait and see if a keypress event arrives |
| // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two |
| function VerifyCharModifier(next) { |
| "use strict"; |
| var queue = []; |
| var timer = null; |
| function process() { |
| if (timer) { |
| return; |
| } |
| |
| var delayProcess = function () { |
| clearTimeout(timer); |
| timer = null; |
| process(); |
| }; |
| |
| while (queue.length !== 0) { |
| var cur = queue[0]; |
| queue = queue.splice(1); |
| switch (cur.type) { |
| case 'stall': |
| // insert a delay before processing available events. |
| /* jshint loopfunc: true */ |
| timer = setTimeout(delayProcess, 5); |
| /* jshint loopfunc: false */ |
| return; |
| case 'keydown': |
| // is the next element a keypress? Then we should merge the two |
| if (queue.length !== 0 && queue[0].type === 'keypress') { |
| // Firefox sends keypress even when no char is generated. |
| // so, if keypress keysym is the same as we'd have guessed from keydown, |
| // the modifier didn't have any effect, and should not be escaped |
| if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { |
| cur.escape = queue[0].escape; |
| } |
| cur.keysym = queue[0].keysym; |
| queue = queue.splice(1); |
| } |
| break; |
| } |
| |
| // swallow stall events, and pass all others to the next stage |
| if (cur.type !== 'stall') { |
| next(cur); |
| } |
| } |
| } |
| return function(evt) { |
| queue.push(evt); |
| process(); |
| }; |
| } |
| |
| // Keeps track of which keys we (and the server) believe are down |
| // When a keyup is received, match it against this list, to determine the corresponding keysym(s) |
| // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars |
| // key repeat events should be merged into a single entry. |
| // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess |
| function TrackKeyState(next) { |
| "use strict"; |
| var state = []; |
| |
| return function (evt) { |
| var last = state.length !== 0 ? state[state.length-1] : null; |
| |
| switch (evt.type) { |
| case 'keydown': |
| // insert a new entry if last seen key was different. |
| if (!last || !evt.keyId || last.keyId !== evt.keyId) { |
| last = {keyId: evt.keyId, keysyms: {}}; |
| state.push(last); |
| } |
| if (evt.keysym) { |
| // make sure last event contains this keysym (a single "logical" keyevent |
| // can cause multiple key events to be sent to the VNC server) |
| last.keysyms[evt.keysym.keysym] = evt.keysym; |
| last.ignoreKeyPress = true; |
| next(evt); |
| } |
| break; |
| case 'keypress': |
| if (!last) { |
| last = {keyId: evt.keyId, keysyms: {}}; |
| state.push(last); |
| } |
| if (!evt.keysym) { |
| console.log('keypress with no keysym:', evt); |
| } |
| |
| // If we didn't expect a keypress, and already sent a keydown to the VNC server |
| // based on the keydown, make sure to skip this event. |
| if (evt.keysym && !last.ignoreKeyPress) { |
| last.keysyms[evt.keysym.keysym] = evt.keysym; |
| evt.type = 'keydown'; |
| next(evt); |
| } |
| break; |
| case 'keyup': |
| if (state.length === 0) { |
| return; |
| } |
| var idx = null; |
| // do we have a matching key tracked as being down? |
| for (var i = 0; i !== state.length; ++i) { |
| if (state[i].keyId === evt.keyId) { |
| idx = i; |
| break; |
| } |
| } |
| // if we couldn't find a match (it happens), assume it was the last key pressed |
| if (idx === null) { |
| idx = state.length - 1; |
| } |
| |
| var item = state.splice(idx, 1)[0]; |
| // for each keysym tracked by this key entry, clone the current event and override the keysym |
| var clone = (function(){ |
| function Clone(){} |
| return function (obj) { Clone.prototype=obj; return new Clone(); }; |
| }()); |
| for (var key in item.keysyms) { |
| var out = clone(evt); |
| out.keysym = item.keysyms[key]; |
| next(out); |
| } |
| break; |
| case 'releaseall': |
| /* jshint shadow: true */ |
| for (var i = 0; i < state.length; ++i) { |
| for (var key in state[i].keysyms) { |
| var keysym = state[i].keysyms[key]; |
| next({keyId: 0, keysym: keysym, type: 'keyup'}); |
| } |
| } |
| /* jshint shadow: false */ |
| state = []; |
| } |
| }; |
| } |
| |
| // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), |
| // then the modifier must be "undone" before sending the @, and "redone" afterwards. |
| function EscapeModifiers(next) { |
| "use strict"; |
| return function(evt) { |
| if (evt.type !== 'keydown' || evt.escape === undefined) { |
| next(evt); |
| return; |
| } |
| // undo modifiers |
| for (var i = 0; i < evt.escape.length; ++i) { |
| next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); |
| } |
| // send the character event |
| next(evt); |
| // redo modifiers |
| /* jshint shadow: true */ |
| for (var i = 0; i < evt.escape.length; ++i) { |
| next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); |
| } |
| /* jshint shadow: false */ |
| }; |
| } |