1 /* 2 * Copyright (C) 2013 Glyptodon LLC 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 var Guacamole = Guacamole || {}; 24 25 /** 26 * Provides cross-browser and cross-keyboard keyboard for a specific element. 27 * Browser and keyboard layout variation is abstracted away, providing events 28 * which represent keys as their corresponding X11 keysym. 29 * 30 * @constructor 31 * @param {Element} element The Element to use to provide keyboard events. 32 */ 33 Guacamole.Keyboard = function(element) { 34 35 /** 36 * Reference to this Guacamole.Keyboard. 37 * @private 38 */ 39 var guac_keyboard = this; 40 41 /** 42 * Fired whenever the user presses a key with the element associated 43 * with this Guacamole.Keyboard in focus. 44 * 45 * @event 46 * @param {Number} keysym The keysym of the key being pressed. 47 * @return {Boolean} true if the key event should be allowed through to the 48 * browser, false otherwise. 49 */ 50 this.onkeydown = null; 51 52 /** 53 * Fired whenever the user releases a key with the element associated 54 * with this Guacamole.Keyboard in focus. 55 * 56 * @event 57 * @param {Number} keysym The keysym of the key being released. 58 */ 59 this.onkeyup = null; 60 61 /** 62 * Map of known JavaScript keycodes which do not map to typable characters 63 * to their unshifted X11 keysym equivalents. 64 * @private 65 */ 66 var unshiftedKeysym = { 67 8: [0xFF08], // backspace 68 9: [0xFF09], // tab 69 13: [0xFF0D], // enter 70 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 71 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl 72 18: [0xFFE9, 0xFFE9, 0xFFEA], // alt 73 19: [0xFF13], // pause/break 74 20: [0xFFE5], // caps lock 75 27: [0xFF1B], // escape 76 32: [0x0020], // space 77 33: [0xFF55], // page up 78 34: [0xFF56], // page down 79 35: [0xFF57], // end 80 36: [0xFF50], // home 81 37: [0xFF51], // left arrow 82 38: [0xFF52], // up arrow 83 39: [0xFF53], // right arrow 84 40: [0xFF54], // down arrow 85 45: [0xFF63], // insert 86 46: [0xFFFF], // delete 87 91: [0xFFEB], // left window key (hyper_l) 88 92: [0xFF67], // right window key (menu key?) 89 93: null, // select key 90 112: [0xFFBE], // f1 91 113: [0xFFBF], // f2 92 114: [0xFFC0], // f3 93 115: [0xFFC1], // f4 94 116: [0xFFC2], // f5 95 117: [0xFFC3], // f6 96 118: [0xFFC4], // f7 97 119: [0xFFC5], // f8 98 120: [0xFFC6], // f9 99 121: [0xFFC7], // f10 100 122: [0xFFC8], // f11 101 123: [0xFFC9], // f12 102 144: [0xFF7F], // num lock 103 145: [0xFF14] // scroll lock 104 }; 105 106 /** 107 * Map of known JavaScript keyidentifiers which do not map to typable 108 * characters to their unshifted X11 keysym equivalents. 109 * @private 110 */ 111 var keyidentifier_keysym = { 112 "Again": [0xFF66], 113 "AllCandidates": [0xFF3D], 114 "Alphanumeric": [0xFF30], 115 "Alt": [0xFFE9, 0xFFE9, 0xFFEA], 116 "Attn": [0xFD0E], 117 "AltGraph": [0xFFEA], 118 "ArrowDown": [0xFF54], 119 "ArrowLeft": [0xFF51], 120 "ArrowRight": [0xFF53], 121 "ArrowUp": [0xFF52], 122 "Backspace": [0xFF08], 123 "CapsLock": [0xFFE5], 124 "Cancel": [0xFF69], 125 "Clear": [0xFF0B], 126 "Convert": [0xFF21], 127 "Copy": [0xFD15], 128 "Crsel": [0xFD1C], 129 "CrSel": [0xFD1C], 130 "CodeInput": [0xFF37], 131 "Compose": [0xFF20], 132 "Control": [0xFFE3, 0xFFE3, 0xFFE4], 133 "ContextMenu": [0xFF67], 134 "Delete": [0xFFFF], 135 "Down": [0xFF54], 136 "End": [0xFF57], 137 "Enter": [0xFF0D], 138 "EraseEof": [0xFD06], 139 "Escape": [0xFF1B], 140 "Execute": [0xFF62], 141 "Exsel": [0xFD1D], 142 "ExSel": [0xFD1D], 143 "F1": [0xFFBE], 144 "F2": [0xFFBF], 145 "F3": [0xFFC0], 146 "F4": [0xFFC1], 147 "F5": [0xFFC2], 148 "F6": [0xFFC3], 149 "F7": [0xFFC4], 150 "F8": [0xFFC5], 151 "F9": [0xFFC6], 152 "F10": [0xFFC7], 153 "F11": [0xFFC8], 154 "F12": [0xFFC9], 155 "F13": [0xFFCA], 156 "F14": [0xFFCB], 157 "F15": [0xFFCC], 158 "F16": [0xFFCD], 159 "F17": [0xFFCE], 160 "F18": [0xFFCF], 161 "F19": [0xFFD0], 162 "F20": [0xFFD1], 163 "F21": [0xFFD2], 164 "F22": [0xFFD3], 165 "F23": [0xFFD4], 166 "F24": [0xFFD5], 167 "Find": [0xFF68], 168 "GroupFirst": [0xFE0C], 169 "GroupLast": [0xFE0E], 170 "GroupNext": [0xFE08], 171 "GroupPrevious": [0xFE0A], 172 "FullWidth": null, 173 "HalfWidth": null, 174 "HangulMode": [0xFF31], 175 "Hankaku": [0xFF29], 176 "HanjaMode": [0xFF34], 177 "Help": [0xFF6A], 178 "Hiragana": [0xFF25], 179 "HiraganaKatakana": [0xFF27], 180 "Home": [0xFF50], 181 "Hyper": [0xFFED, 0xFFED, 0xFFEE], 182 "Insert": [0xFF63], 183 "JapaneseHiragana": [0xFF25], 184 "JapaneseKatakana": [0xFF26], 185 "JapaneseRomaji": [0xFF24], 186 "JunjaMode": [0xFF38], 187 "KanaMode": [0xFF2D], 188 "KanjiMode": [0xFF21], 189 "Katakana": [0xFF26], 190 "Left": [0xFF51], 191 "Meta": [0xFFE7], 192 "ModeChange": [0xFF7E], 193 "NumLock": [0xFF7F], 194 "PageDown": [0xFF55], 195 "PageUp": [0xFF56], 196 "Pause": [0xFF13], 197 "Play": [0xFD16], 198 "PreviousCandidate": [0xFF3E], 199 "PrintScreen": [0xFD1D], 200 "Redo": [0xFF66], 201 "Right": [0xFF53], 202 "RomanCharacters": null, 203 "Scroll": [0xFF14], 204 "Select": [0xFF60], 205 "Separator": [0xFFAC], 206 "Shift": [0xFFE1, 0xFFE1, 0xFFE2], 207 "SingleCandidate": [0xFF3C], 208 "Super": [0xFFEB, 0xFFEB, 0xFFEC], 209 "Tab": [0xFF09], 210 "Up": [0xFF52], 211 "Undo": [0xFF65], 212 "Win": [0xFFEB], 213 "Zenkaku": [0xFF28], 214 "ZenkakuHankaku": [0xFF2A] 215 }; 216 217 /** 218 * Map of known JavaScript keycodes which do not map to typable characters 219 * to their shifted X11 keysym equivalents. Keycodes must only be listed 220 * here if their shifted X11 keysym equivalents differ from their unshifted 221 * equivalents. 222 * @private 223 */ 224 var shiftedKeysym = { 225 18: [0xFFE7, 0xFFE7, 0xFFEA] // alt 226 }; 227 228 /** 229 * All keysyms which should not repeat when held down. 230 * @private 231 */ 232 var no_repeat = { 233 0xFFE1: true, // Left shift 234 0xFFE2: true, // Right shift 235 0xFFE3: true, // Left ctrl 236 0xFFE4: true, // Right ctrl 237 0xFFE7: true, // Left meta 238 0xFFE8: true, // Right meta 239 0xFFE9: true, // Left alt 240 0xFFEA: true, // Right alt (or AltGr) 241 0xFFEB: true, // Left hyper 242 0xFFEC: true // Right hyper 243 }; 244 245 /** 246 * All modifiers and their states. 247 */ 248 this.modifiers = new Guacamole.Keyboard.ModifierState(); 249 250 /** 251 * The state of every key, indexed by keysym. If a particular key is 252 * pressed, the value of pressed for that keysym will be true. If a key 253 * is not currently pressed, it will not be defined. 254 */ 255 this.pressed = {}; 256 257 /** 258 * The last result of calling the onkeydown handler for each key, indexed 259 * by keysym. This is used to prevent/allow default actions for key events, 260 * even when the onkeydown handler cannot be called again because the key 261 * is (theoretically) still pressed. 262 */ 263 var last_keydown_result = {}; 264 265 /** 266 * The keysym associated with a given keycode when keydown fired. 267 * @private 268 */ 269 var keydownChar = []; 270 271 /** 272 * Timeout before key repeat starts. 273 * @private 274 */ 275 var key_repeat_timeout = null; 276 277 /** 278 * Interval which presses and releases the last key pressed while that 279 * key is still being held down. 280 * @private 281 */ 282 var key_repeat_interval = null; 283 284 /** 285 * Given an array of keysyms indexed by location, returns the keysym 286 * for the given location, or the keysym for the standard location if 287 * undefined. 288 * 289 * @param {Array} keysyms An array of keysyms, where the index of the 290 * keysym in the array is the location value. 291 * @param {Number} location The location on the keyboard corresponding to 292 * the key pressed, as defined at: 293 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 294 */ 295 function get_keysym(keysyms, location) { 296 297 if (!keysyms) 298 return null; 299 300 return keysyms[location] || keysyms[0]; 301 } 302 303 function keysym_from_key_identifier(shifted, identifier, location) { 304 305 var typedCharacter; 306 307 // If identifier is U+xxxx, decode Unicode character 308 var unicodePrefixLocation = identifier.indexOf("U+"); 309 if (unicodePrefixLocation >= 0) { 310 var hex = identifier.substring(unicodePrefixLocation+2); 311 typedCharacter = String.fromCharCode(parseInt(hex, 16)); 312 } 313 314 // If single character, use that as typed character 315 else if (identifier.length === 1) 316 typedCharacter = identifier; 317 318 // Otherwise, look up corresponding keysym 319 else 320 return get_keysym(keyidentifier_keysym[identifier], location); 321 322 // Convert case if shifted 323 if (shifted) 324 typedCharacter = typedCharacter.toUpperCase(); 325 else 326 typedCharacter = typedCharacter.toLowerCase(); 327 328 // Get codepoint 329 var codepoint = typedCharacter.charCodeAt(0); 330 return keysym_from_charcode(codepoint); 331 332 } 333 334 function isControlCharacter(codepoint) { 335 return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); 336 } 337 338 function keysym_from_charcode(codepoint) { 339 340 // Keysyms for control characters 341 if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; 342 343 // Keysyms for ASCII chars 344 if (codepoint >= 0x0000 && codepoint <= 0x00FF) 345 return codepoint; 346 347 // Keysyms for Unicode 348 if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) 349 return 0x01000000 | codepoint; 350 351 return null; 352 353 } 354 355 function keysym_from_keycode(keyCode, location) { 356 357 var keysyms; 358 359 // If not shifted, just return unshifted keysym 360 if (!guac_keyboard.modifiers.shift) 361 keysyms = unshiftedKeysym[keyCode]; 362 363 // Otherwise, return shifted keysym, if defined 364 else 365 keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode]; 366 367 return get_keysym(keysyms, location); 368 369 } 370 371 /** 372 * Marks a key as pressed, firing the keydown event if registered. Key 373 * repeat for the pressed key will start after a delay if that key is 374 * not a modifier. 375 * 376 * @private 377 * @param keysym The keysym of the key to press. 378 * @return {Boolean} true if event should NOT be canceled, false otherwise. 379 */ 380 function press_key(keysym) { 381 382 // Don't bother with pressing the key if the key is unknown 383 if (keysym === null) return; 384 385 // Only press if released 386 if (!guac_keyboard.pressed[keysym]) { 387 388 // Mark key as pressed 389 guac_keyboard.pressed[keysym] = true; 390 391 // Send key event 392 if (guac_keyboard.onkeydown) { 393 var result = guac_keyboard.onkeydown(keysym); 394 last_keydown_result[keysym] = result; 395 396 // Stop any current repeat 397 window.clearTimeout(key_repeat_timeout); 398 window.clearInterval(key_repeat_interval); 399 400 // Repeat after a delay as long as pressed 401 if (!no_repeat[keysym]) 402 key_repeat_timeout = window.setTimeout(function() { 403 key_repeat_interval = window.setInterval(function() { 404 guac_keyboard.onkeyup(keysym); 405 guac_keyboard.onkeydown(keysym); 406 }, 50); 407 }, 500); 408 409 return result; 410 } 411 } 412 413 // Return the last keydown result by default, resort to false if unknown 414 return last_keydown_result[keysym] || false; 415 416 } 417 418 /** 419 * Marks a key as released, firing the keyup event if registered. 420 * 421 * @private 422 * @param keysym The keysym of the key to release. 423 */ 424 function release_key(keysym) { 425 426 // Only release if pressed 427 if (guac_keyboard.pressed[keysym]) { 428 429 // Mark key as released 430 delete guac_keyboard.pressed[keysym]; 431 432 // Stop repeat 433 window.clearTimeout(key_repeat_timeout); 434 window.clearInterval(key_repeat_interval); 435 436 // Send key event 437 if (keysym !== null && guac_keyboard.onkeyup) 438 guac_keyboard.onkeyup(keysym); 439 440 } 441 442 } 443 444 /** 445 * Given a keyboard event, updates the local modifier state and remote 446 * key state based on the modifier flags within the event. This function 447 * pays no attention to keycodes. 448 * 449 * @param {KeyboardEvent} e The keyboard event containing the flags to update. 450 */ 451 function update_modifier_state(e) { 452 453 // Get state 454 var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e); 455 456 // Release alt if implicitly released 457 if (guac_keyboard.modifiers.alt && state.alt === false) { 458 release_key(0xFFE9); // Left alt 459 release_key(0xFFEA); // Right alt (or AltGr) 460 } 461 462 // Release shift if implicitly released 463 if (guac_keyboard.modifiers.shift && state.shift === false) { 464 release_key(0xFFE1); // Left shift 465 release_key(0xFFE2); // Right shift 466 } 467 468 // Release ctrl if implicitly released 469 if (guac_keyboard.modifiers.ctrl && state.ctrl === false) { 470 release_key(0xFFE3); // Left ctrl 471 release_key(0xFFE4); // Right ctrl 472 } 473 474 // Release meta if implicitly released 475 if (guac_keyboard.modifiers.meta && state.meta === false) { 476 release_key(0xFFE7); // Left meta 477 release_key(0xFFE8); // Right meta 478 } 479 480 // Release hyper if implicitly released 481 if (guac_keyboard.modifiers.hyper && state.hyper === false) { 482 release_key(0xFFEB); // Left hyper 483 release_key(0xFFEC); // Right hyper 484 } 485 486 // Update state 487 guac_keyboard.modifiers = state; 488 489 } 490 491 // When key pressed 492 element.addEventListener("keydown", function(e) { 493 494 // Only intercept if handler set 495 if (!guac_keyboard.onkeydown) return; 496 497 var keynum; 498 if (window.event) keynum = window.event.keyCode; 499 else if (e.which) keynum = e.which; 500 501 // Get key location 502 var location = e.location || e.keyLocation || 0; 503 504 // Ignore any unknown key events 505 if (!keynum) { 506 e.preventDefault(); 507 return; 508 } 509 510 // Fix modifier states 511 update_modifier_state(e); 512 513 // Ignore (but do not prevent) the "composition" keycode sent by some 514 // browsers when an IME is in use (see: http://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html) 515 if (keynum === 229) 516 return; 517 518 // Try to get keysym from keycode 519 var keysym = keysym_from_keycode(keynum, location); 520 521 // Also try to get get keysym from e.key 522 if (e.key) 523 keysym = keysym || keysym_from_key_identifier( 524 guac_keyboard.modifiers.shift, e.key, location); 525 526 // If no e.key, use e.keyIdentifier if absolutely necessary (can be buggy) 527 else { 528 529 var keypress_unlikely = guac_keyboard.modifiers.ctrl 530 || guac_keyboard.modifiers.alt 531 || guac_keyboard.modifiers.meta 532 || guac_keyboard.modifiers.hyper; 533 534 if (keypress_unlikely && e.keyIdentifier) 535 keysym = keysym || keysym_from_key_identifier( 536 guac_keyboard.modifiers.shift, e.keyIdentifier, location); 537 538 } 539 540 // Press key if known 541 if (keysym !== null) { 542 543 keydownChar[keynum] = keysym; 544 if (!press_key(keysym)) 545 e.preventDefault(); 546 547 // If a key is pressed while meta is held down, the keyup will 548 // never be sent in Chrome, so send it now. (bug #108404) 549 if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8) 550 release_key(keysym); 551 552 } 553 554 }, true); 555 556 // When key pressed 557 element.addEventListener("keypress", function(e) { 558 559 // Only intercept if handler set 560 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 561 562 var keynum; 563 if (window.event) keynum = window.event.keyCode; 564 else if (e.which) keynum = e.which; 565 566 var keysym = keysym_from_charcode(keynum); 567 568 // Fix modifier states 569 update_modifier_state(e); 570 571 // If event identified as a typable character, and we're holding Ctrl+Alt, 572 // assume Ctrl+Alt is actually AltGr, and release both. 573 if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) { 574 release_key(0xFFE3); // Left ctrl 575 release_key(0xFFE4); // Right ctrl 576 release_key(0xFFE9); // Left alt 577 release_key(0xFFEA); // Right alt 578 } 579 580 // Send press + release if keysym known 581 if (keysym !== null) { 582 if (!press_key(keysym)) 583 e.preventDefault(); 584 release_key(keysym); 585 } 586 else 587 e.preventDefault(); 588 589 }, true); 590 591 // When key released 592 element.addEventListener("keyup", function(e) { 593 594 // Only intercept if handler set 595 if (!guac_keyboard.onkeyup) return; 596 597 e.preventDefault(); 598 599 var keynum; 600 if (window.event) keynum = window.event.keyCode; 601 else if (e.which) keynum = e.which; 602 603 // Fix modifier states 604 update_modifier_state(e); 605 606 // Send release event if original key known 607 var keysym = keydownChar[keynum]; 608 if (keysym !== null) 609 release_key(keysym); 610 611 // Clear character record 612 keydownChar[keynum] = null; 613 614 }, true); 615 616 }; 617 618 /** 619 * The state of all supported keyboard modifiers. 620 * @constructor 621 */ 622 Guacamole.Keyboard.ModifierState = function() { 623 624 /** 625 * Whether shift is currently pressed. 626 * @type Boolean 627 */ 628 this.shift = false; 629 630 /** 631 * Whether ctrl is currently pressed. 632 * @type Boolean 633 */ 634 this.ctrl = false; 635 636 /** 637 * Whether alt is currently pressed. 638 * @type Boolean 639 */ 640 this.alt = false; 641 642 /** 643 * Whether meta (apple key) is currently pressed. 644 * @type Boolean 645 */ 646 this.meta = false; 647 648 /** 649 * Whether hyper (windows key) is currently pressed. 650 * @type Boolean 651 */ 652 this.hyper = false; 653 654 }; 655 656 /** 657 * Returns the modifier state applicable to the keyboard event given. 658 * 659 * @param {KeyboardEvent} e The keyboard event to read. 660 * @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard 661 * modifiers. 662 */ 663 Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { 664 665 var state = new Guacamole.Keyboard.ModifierState(); 666 667 // Assign states from old flags 668 state.shift = e.shiftKey; 669 state.ctrl = e.ctrlKey; 670 state.alt = e.altKey; 671 state.meta = e.metaKey; 672 673 // Use DOM3 getModifierState() for others 674 if (e.getModifierState) { 675 state.hyper = e.getModifierState("OS") 676 || e.getModifierState("Super") 677 || e.getModifierState("Hyper") 678 || e.getModifierState("Win"); 679 } 680 681 return state; 682 683 }; 684