1var kbdUtil = (function() { 2 "use strict"; 3 4 function substituteCodepoint(cp) { 5 // Any Unicode code points which do not have corresponding keysym entries 6 // can be swapped out for another code point by adding them to this table 7 var substitutions = { 8 // {S,s} with comma below -> {S,s} with cedilla 9 0x218 : 0x15e, 10 0x219 : 0x15f, 11 // {T,t} with comma below -> {T,t} with cedilla 12 0x21a : 0x162, 13 0x21b : 0x163 14 }; 15 16 var sub = substitutions[cp]; 17 return sub ? sub : cp; 18 } 19 20 function isMac() { 21 return navigator && !!(/mac/i).exec(navigator.platform); 22 } 23 function isWindows() { 24 return navigator && !!(/win/i).exec(navigator.platform); 25 } 26 function isLinux() { 27 return navigator && !!(/linux/i).exec(navigator.platform); 28 } 29 30 // Return true if a modifier which is not the specified char modifier (and is not shift) is down 31 function hasShortcutModifier(charModifier, currentModifiers) { 32 var mods = {}; 33 for (var key in currentModifiers) { 34 if (parseInt(key) !== 0xffe1) { 35 mods[key] = currentModifiers[key]; 36 } 37 } 38 39 var sum = 0; 40 for (var k in currentModifiers) { 41 if (mods[k]) { 42 ++sum; 43 } 44 } 45 if (hasCharModifier(charModifier, mods)) { 46 return sum > charModifier.length; 47 } 48 else { 49 return sum > 0; 50 } 51 } 52 53 // Return true if the specified char modifier is currently down 54 function hasCharModifier(charModifier, currentModifiers) { 55 if (charModifier.length === 0) { return false; } 56 57 for (var i = 0; i < charModifier.length; ++i) { 58 if (!currentModifiers[charModifier[i]]) { 59 return false; 60 } 61 } 62 return true; 63 } 64 65 // Helper object tracking modifier key state 66 // and generates fake key events to compensate if it gets out of sync 67 function ModifierSync(charModifier) { 68 var ctrl = 0xffe3; 69 var alt = 0xffe9; 70 var altGr = 0xfe03; 71 var shift = 0xffe1; 72 var meta = 0xffe7; 73 74 if (!charModifier) { 75 if (isMac()) { 76 // on Mac, Option (AKA Alt) is used as a char modifier 77 charModifier = [alt]; 78 } 79 else if (isWindows()) { 80 // on Windows, Ctrl+Alt is used as a char modifier 81 charModifier = [alt, ctrl]; 82 } 83 else if (isLinux()) { 84 // on Linux, AltGr is used as a char modifier 85 charModifier = [altGr]; 86 } 87 else { 88 charModifier = []; 89 } 90 } 91 92 var state = {}; 93 state[ctrl] = false; 94 state[alt] = false; 95 state[altGr] = false; 96 state[shift] = false; 97 state[meta] = false; 98 99 function sync(evt, keysym) { 100 var result = []; 101 function syncKey(keysym) { 102 return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'}; 103 } 104 105 if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) { 106 state[ctrl] = evt.ctrlKey; 107 result.push(syncKey(ctrl)); 108 } 109 if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) { 110 state[alt] = evt.altKey; 111 result.push(syncKey(alt)); 112 } 113 if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) { 114 state[altGr] = evt.altGraphKey; 115 result.push(syncKey(altGr)); 116 } 117 if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) { 118 state[shift] = evt.shiftKey; 119 result.push(syncKey(shift)); 120 } 121 if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) { 122 state[meta] = evt.metaKey; 123 result.push(syncKey(meta)); 124 } 125 return result; 126 } 127 function syncKeyEvent(evt, down) { 128 var obj = getKeysym(evt); 129 var keysym = obj ? obj.keysym : null; 130 131 // first, apply the event itself, if relevant 132 if (keysym !== null && state[keysym] !== undefined) { 133 state[keysym] = down; 134 } 135 return sync(evt, keysym); 136 } 137 138 return { 139 // sync on the appropriate keyboard event 140 keydown: function(evt) { return syncKeyEvent(evt, true);}, 141 keyup: function(evt) { return syncKeyEvent(evt, false);}, 142 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway 143 syncAny: function(evt) { return sync(evt);}, 144 145 // is a shortcut modifier down? 146 hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); }, 147 // if a char modifier is down, return the keys it consists of, otherwise return null 148 activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; } 149 }; 150 } 151 152 // Get a key ID from a keyboard event 153 // May be a string or an integer depending on the available properties 154 function getKey(evt){ 155 if ('keyCode' in evt && 'key' in evt) { 156 return evt.key + ':' + evt.keyCode; 157 } 158 else if ('keyCode' in evt) { 159 return evt.keyCode; 160 } 161 else { 162 return evt.key; 163 } 164 } 165 166 // Get the most reliable keysym value we can get from a key event 167 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which 168 function getKeysym(evt){ 169 var codepoint; 170 if (evt.char && evt.char.length === 1) { 171 codepoint = evt.char.charCodeAt(); 172 } 173 else if (evt.charCode) { 174 codepoint = evt.charCode; 175 } 176 else if (evt.keyCode && evt.type === 'keypress') { 177 // IE10 stores the char code as keyCode, and has no other useful properties 178 codepoint = evt.keyCode; 179 } 180 if (codepoint) { 181 var res = keysyms.fromUnicode(substituteCodepoint(codepoint)); 182 if (res) { 183 return res; 184 } 185 } 186 // we could check evt.key here. 187 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list, 188 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key 189 // so we don't *need* it yet 190 if (evt.keyCode) { 191 return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey)); 192 } 193 if (evt.which) { 194 return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey)); 195 } 196 return null; 197 } 198 199 // Given a keycode, try to predict which keysym it might be. 200 // If the keycode is unknown, null is returned. 201 function keysymFromKeyCode(keycode, shiftPressed) { 202 if (typeof(keycode) !== 'number') { 203 return null; 204 } 205 // won't be accurate for azerty 206 if (keycode >= 0x30 && keycode <= 0x39) { 207 return keycode; // digit 208 } 209 if (keycode >= 0x41 && keycode <= 0x5a) { 210 // remap to lowercase unless shift is down 211 return shiftPressed ? keycode : keycode + 32; // A-Z 212 } 213 if (keycode >= 0x60 && keycode <= 0x69) { 214 return 0xffb0 + (keycode - 0x60); // numpad 0-9 215 } 216 217 switch(keycode) { 218 case 0x20: return 0x20; // space 219 case 0x6a: return 0xffaa; // multiply 220 case 0x6b: return 0xffab; // add 221 case 0x6c: return 0xffac; // separator 222 case 0x6d: return 0xffad; // subtract 223 case 0x6e: return 0xffae; // decimal 224 case 0x6f: return 0xffaf; // divide 225 case 0xbb: return 0x2b; // + 226 case 0xbc: return 0x2c; // , 227 case 0xbd: return 0x2d; // - 228 case 0xbe: return 0x2e; // . 229 } 230 231 return nonCharacterKey({keyCode: keycode}); 232 } 233 234 // if the key is a known non-character key (any key which doesn't generate character data) 235 // return its keysym value. Otherwise return null 236 function nonCharacterKey(evt) { 237 // evt.key not implemented yet 238 if (!evt.keyCode) { return null; } 239 var keycode = evt.keyCode; 240 241 if (keycode >= 0x70 && keycode <= 0x87) { 242 return 0xffbe + keycode - 0x70; // F1-F24 243 } 244 switch (keycode) { 245 246 case 8 : return 0xFF08; // BACKSPACE 247 case 13 : return 0xFF0D; // ENTER 248 249 case 9 : return 0xFF09; // TAB 250 251 case 27 : return 0xFF1B; // ESCAPE 252 case 46 : return 0xFFFF; // DELETE 253 254 case 36 : return 0xFF50; // HOME 255 case 35 : return 0xFF57; // END 256 case 33 : return 0xFF55; // PAGE_UP 257 case 34 : return 0xFF56; // PAGE_DOWN 258 case 45 : return 0xFF63; // INSERT 259 260 case 37 : return 0xFF51; // LEFT 261 case 38 : return 0xFF52; // UP 262 case 39 : return 0xFF53; // RIGHT 263 case 40 : return 0xFF54; // DOWN 264 case 16 : return 0xFFE1; // SHIFT 265 case 17 : return 0xFFE3; // CONTROL 266 case 18 : return 0xFFE9; // Left ALT (Mac Option) 267 268 case 224 : return 0xFE07; // Meta 269 case 225 : return 0xFE03; // AltGr 270 case 91 : return 0xFFEC; // Super_L (Win Key) 271 case 92 : return 0xFFED; // Super_R (Win Key) 272 case 93 : return 0xFF67; // Menu (Win Menu), Mac Command 273 default: return null; 274 } 275 } 276 return { 277 hasShortcutModifier : hasShortcutModifier, 278 hasCharModifier : hasCharModifier, 279 ModifierSync : ModifierSync, 280 getKey : getKey, 281 getKeysym : getKeysym, 282 keysymFromKeyCode : keysymFromKeyCode, 283 nonCharacterKey : nonCharacterKey, 284 substituteCodepoint : substituteCodepoint 285 }; 286})(); 287 288// Takes a DOM keyboard event and: 289// - determines which keysym it represents 290// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event) 291// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down 292// - marks each event with an 'escape' property if a modifier was down which should be "escaped" 293// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown 294// This information is collected into an object which is passed to the next() function. (one call per event) 295function KeyEventDecoder(modifierState, next) { 296 "use strict"; 297 function sendAll(evts) { 298 for (var i = 0; i < evts.length; ++i) { 299 next(evts[i]); 300 } 301 } 302 function process(evt, type) { 303 var result = {type: type}; 304 var keyId = kbdUtil.getKey(evt); 305 if (keyId) { 306 result.keyId = keyId; 307 } 308 309 var keysym = kbdUtil.getKeysym(evt); 310 311 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier(); 312 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress? 313 // "special" keys like enter, tab or backspace don't send keypress events, 314 // and some browsers don't send keypresses at all if a modifier is down 315 if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) { 316 result.keysym = keysym; 317 } 318 319 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift'; 320 321 // Should we prevent the browser from handling the event? 322 // Doing so on a keydown (in most browsers) prevents keypress from being generated 323 // so only do that if we have to. 324 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt)); 325 326 // If a char modifier is down on a keydown, we need to insert a stall, 327 // so VerifyCharModifier knows to wait and see if a keypress is comnig 328 var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt); 329 330 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt) 331 var active = modifierState.activeCharModifier(); 332 333 // If we have a char modifier down, and we're able to determine a keysym reliably 334 // then (a) we know to treat the modifier as a char modifier, 335 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char. 336 if (active && keysym) { 337 var isCharModifier = false; 338 for (var i = 0; i < active.length; ++i) { 339 if (active[i] === keysym.keysym) { 340 isCharModifier = true; 341 } 342 } 343 if (type === 'keypress' && !isCharModifier) { 344 result.escape = modifierState.activeCharModifier(); 345 } 346 } 347 348 if (stall) { 349 // insert a fake "stall" event 350 next({type: 'stall'}); 351 } 352 next(result); 353 354 return suppress; 355 } 356 357 return { 358 keydown: function(evt) { 359 sendAll(modifierState.keydown(evt)); 360 return process(evt, 'keydown'); 361 }, 362 keypress: function(evt) { 363 return process(evt, 'keypress'); 364 }, 365 keyup: function(evt) { 366 sendAll(modifierState.keyup(evt)); 367 return process(evt, 'keyup'); 368 }, 369 syncModifiers: function(evt) { 370 sendAll(modifierState.syncAny(evt)); 371 }, 372 releaseAll: function() { next({type: 'releaseall'}); } 373 }; 374} 375 376// Combines keydown and keypress events where necessary to handle char modifiers. 377// On some OS'es, a char modifier is sometimes used as a shortcut modifier. 378// 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 379// 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. 380// The only way we can distinguish these cases is to wait and see if a keypress event arrives 381// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two 382function VerifyCharModifier(next) { 383 "use strict"; 384 var queue = []; 385 var timer = null; 386 function process() { 387 if (timer) { 388 return; 389 } 390 391 var delayProcess = function () { 392 clearTimeout(timer); 393 timer = null; 394 process(); 395 }; 396 397 while (queue.length !== 0) { 398 var cur = queue[0]; 399 queue = queue.splice(1); 400 switch (cur.type) { 401 case 'stall': 402 // insert a delay before processing available events. 403 /* jshint loopfunc: true */ 404 timer = setTimeout(delayProcess, 5); 405 /* jshint loopfunc: false */ 406 return; 407 case 'keydown': 408 // is the next element a keypress? Then we should merge the two 409 if (queue.length !== 0 && queue[0].type === 'keypress') { 410 // Firefox sends keypress even when no char is generated. 411 // so, if keypress keysym is the same as we'd have guessed from keydown, 412 // the modifier didn't have any effect, and should not be escaped 413 if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) { 414 cur.escape = queue[0].escape; 415 } 416 cur.keysym = queue[0].keysym; 417 queue = queue.splice(1); 418 } 419 break; 420 } 421 422 // swallow stall events, and pass all others to the next stage 423 if (cur.type !== 'stall') { 424 next(cur); 425 } 426 } 427 } 428 return function(evt) { 429 queue.push(evt); 430 process(); 431 }; 432} 433 434// Keeps track of which keys we (and the server) believe are down 435// When a keyup is received, match it against this list, to determine the corresponding keysym(s) 436// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars 437// key repeat events should be merged into a single entry. 438// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess 439function TrackKeyState(next) { 440 "use strict"; 441 var state = []; 442 443 return function (evt) { 444 var last = state.length !== 0 ? state[state.length-1] : null; 445 446 switch (evt.type) { 447 case 'keydown': 448 // insert a new entry if last seen key was different. 449 if (!last || !evt.keyId || last.keyId !== evt.keyId) { 450 last = {keyId: evt.keyId, keysyms: {}}; 451 state.push(last); 452 } 453 if (evt.keysym) { 454 // make sure last event contains this keysym (a single "logical" keyevent 455 // can cause multiple key events to be sent to the VNC server) 456 last.keysyms[evt.keysym.keysym] = evt.keysym; 457 last.ignoreKeyPress = true; 458 next(evt); 459 } 460 break; 461 case 'keypress': 462 if (!last) { 463 last = {keyId: evt.keyId, keysyms: {}}; 464 state.push(last); 465 } 466 if (!evt.keysym) { 467 console.log('keypress with no keysym:', evt); 468 } 469 470 // If we didn't expect a keypress, and already sent a keydown to the VNC server 471 // based on the keydown, make sure to skip this event. 472 if (evt.keysym && !last.ignoreKeyPress) { 473 last.keysyms[evt.keysym.keysym] = evt.keysym; 474 evt.type = 'keydown'; 475 next(evt); 476 } 477 break; 478 case 'keyup': 479 if (state.length === 0) { 480 return; 481 } 482 var idx = null; 483 // do we have a matching key tracked as being down? 484 for (var i = 0; i !== state.length; ++i) { 485 if (state[i].keyId === evt.keyId) { 486 idx = i; 487 break; 488 } 489 } 490 // if we couldn't find a match (it happens), assume it was the last key pressed 491 if (idx === null) { 492 idx = state.length - 1; 493 } 494 495 var item = state.splice(idx, 1)[0]; 496 // for each keysym tracked by this key entry, clone the current event and override the keysym 497 var clone = (function(){ 498 function Clone(){} 499 return function (obj) { Clone.prototype=obj; return new Clone(); }; 500 }()); 501 for (var key in item.keysyms) { 502 var out = clone(evt); 503 out.keysym = item.keysyms[key]; 504 next(out); 505 } 506 break; 507 case 'releaseall': 508 /* jshint shadow: true */ 509 for (var i = 0; i < state.length; ++i) { 510 for (var key in state[i].keysyms) { 511 var keysym = state[i].keysyms[key]; 512 next({keyId: 0, keysym: keysym, type: 'keyup'}); 513 } 514 } 515 /* jshint shadow: false */ 516 state = []; 517 } 518 }; 519} 520 521// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @), 522// then the modifier must be "undone" before sending the @, and "redone" afterwards. 523function EscapeModifiers(next) { 524 "use strict"; 525 return function(evt) { 526 if (evt.type !== 'keydown' || evt.escape === undefined) { 527 next(evt); 528 return; 529 } 530 // undo modifiers 531 for (var i = 0; i < evt.escape.length; ++i) { 532 next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); 533 } 534 // send the character event 535 next(evt); 536 // redo modifiers 537 /* jshint shadow: true */ 538 for (var i = 0; i < evt.escape.length; ++i) { 539 next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])}); 540 } 541 /* jshint shadow: false */ 542 }; 543} 544