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 keysym associated with a given keycode when keydown fired. 259 * @private 260 */ 261 var keydownChar = []; 262 263 /** 264 * Timeout before key repeat starts. 265 * @private 266 */ 267 var key_repeat_timeout = null; 268 269 /** 270 * Interval which presses and releases the last key pressed while that 271 * key is still being held down. 272 * @private 273 */ 274 var key_repeat_interval = null; 275 276 /** 277 * Given an array of keysyms indexed by location, returns the keysym 278 * for the given location, or the keysym for the standard location if 279 * undefined. 280 * 281 * @param {Array} keysyms An array of keysyms, where the index of the 282 * keysym in the array is the location value. 283 * @param {Number} location The location on the keyboard corresponding to 284 * the key pressed, as defined at: 285 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 286 */ 287 function get_keysym(keysyms, location) { 288 289 if (!keysyms) 290 return null; 291 292 return keysyms[location] || keysyms[0]; 293 } 294 295 function keysym_from_key_identifier(shifted, identifier, location) { 296 297 var typedCharacter; 298 299 // If identifier is U+xxxx, decode Unicode character 300 var unicodePrefixLocation = identifier.indexOf("U+"); 301 if (unicodePrefixLocation >= 0) { 302 var hex = identifier.substring(unicodePrefixLocation+2); 303 typedCharacter = String.fromCharCode(parseInt(hex, 16)); 304 } 305 306 // If single character, use that as typed character 307 else if (identifier.length === 1) 308 typedCharacter = identifier; 309 310 // Otherwise, look up corresponding keysym 311 else 312 return get_keysym(keyidentifier_keysym[identifier], location); 313 314 // Convert case if shifted 315 if (shifted) 316 typedCharacter = typedCharacter.toUpperCase(); 317 else 318 typedCharacter = typedCharacter.toLowerCase(); 319 320 // Get codepoint 321 var codepoint = typedCharacter.charCodeAt(0); 322 return keysym_from_charcode(codepoint); 323 324 } 325 326 function isControlCharacter(codepoint) { 327 return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); 328 } 329 330 function keysym_from_charcode(codepoint) { 331 332 // Keysyms for control characters 333 if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; 334 335 // Keysyms for ASCII chars 336 if (codepoint >= 0x0000 && codepoint <= 0x00FF) 337 return codepoint; 338 339 // Keysyms for Unicode 340 if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) 341 return 0x01000000 | codepoint; 342 343 return null; 344 345 } 346 347 function keysym_from_keycode(keyCode, location) { 348 349 var keysyms; 350 351 // If not shifted, just return unshifted keysym 352 if (!guac_keyboard.modifiers.shift) 353 keysyms = unshiftedKeysym[keyCode]; 354 355 // Otherwise, return shifted keysym, if defined 356 else 357 keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode]; 358 359 return get_keysym(keysyms, location); 360 361 } 362 363 /** 364 * Marks a key as pressed, firing the keydown event if registered. Key 365 * repeat for the pressed key will start after a delay if that key is 366 * not a modifier. 367 * 368 * @private 369 * @param keysym The keysym of the key to press. 370 * @return {Boolean} true if event should NOT be canceled, false otherwise. 371 */ 372 function press_key(keysym) { 373 374 // Don't bother with pressing the key if the key is unknown 375 if (keysym === null) return; 376 377 // Only press if released 378 if (!guac_keyboard.pressed[keysym]) { 379 380 // Mark key as pressed 381 guac_keyboard.pressed[keysym] = true; 382 383 // Send key event 384 if (guac_keyboard.onkeydown) { 385 var result = guac_keyboard.onkeydown(keysym); 386 387 // Stop any current repeat 388 window.clearTimeout(key_repeat_timeout); 389 window.clearInterval(key_repeat_interval); 390 391 // Repeat after a delay as long as pressed 392 if (!no_repeat[keysym]) 393 key_repeat_timeout = window.setTimeout(function() { 394 key_repeat_interval = window.setInterval(function() { 395 guac_keyboard.onkeyup(keysym); 396 guac_keyboard.onkeydown(keysym); 397 }, 50); 398 }, 500); 399 400 return result; 401 } 402 } 403 404 return false; 405 406 } 407 408 /** 409 * Marks a key as released, firing the keyup event if registered. 410 * 411 * @private 412 * @param keysym The keysym of the key to release. 413 */ 414 function release_key(keysym) { 415 416 // Only release if pressed 417 if (guac_keyboard.pressed[keysym]) { 418 419 // Mark key as released 420 delete guac_keyboard.pressed[keysym]; 421 422 // Stop repeat 423 window.clearTimeout(key_repeat_timeout); 424 window.clearInterval(key_repeat_interval); 425 426 // Send key event 427 if (keysym !== null && guac_keyboard.onkeyup) 428 guac_keyboard.onkeyup(keysym); 429 430 } 431 432 } 433 434 /** 435 * Given a keyboard event, updates the local modifier state and remote 436 * key state based on the modifier flags within the event. This function 437 * pays no attention to keycodes. 438 * 439 * @param {KeyboardEvent} e The keyboard event containing the flags to update. 440 */ 441 function update_modifier_state(e) { 442 443 // Get state 444 var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e); 445 446 // Release alt if implicitly released 447 if (guac_keyboard.modifiers.alt && state.alt === false) { 448 release_key(0xFFE9); // Left alt 449 release_key(0xFFEA); // Right alt (or AltGr) 450 } 451 452 // Release shift if implicitly released 453 if (guac_keyboard.modifiers.shift && state.shift === false) { 454 release_key(0xFFE1); // Left shift 455 release_key(0xFFE2); // Right shift 456 } 457 458 // Release ctrl if implicitly released 459 if (guac_keyboard.modifiers.ctrl && state.ctrl === false) { 460 release_key(0xFFE3); // Left ctrl 461 release_key(0xFFE4); // Right ctrl 462 } 463 464 // Release meta if implicitly released 465 if (guac_keyboard.modifiers.meta && state.meta === false) { 466 release_key(0xFFE7); // Left meta 467 release_key(0xFFE8); // Right meta 468 } 469 470 // Release hyper if implicitly released 471 if (guac_keyboard.modifiers.hyper && state.hyper === false) { 472 release_key(0xFFEB); // Left hyper 473 release_key(0xFFEC); // Right hyper 474 } 475 476 // Update state 477 guac_keyboard.modifiers = state; 478 479 } 480 481 // When key pressed 482 element.addEventListener("keydown", function(e) { 483 484 // Only intercept if handler set 485 if (!guac_keyboard.onkeydown) return; 486 487 var keynum; 488 if (window.event) keynum = window.event.keyCode; 489 else if (e.which) keynum = e.which; 490 491 // Get key location 492 var location = e.location || e.keyLocation || 0; 493 494 // Ignore any unknown key events 495 if (!keynum && !identifier) { 496 e.preventDefault(); 497 return; 498 } 499 500 // Fix modifier states 501 update_modifier_state(e); 502 503 // Try to get keysym from keycode 504 var keysym = keysym_from_keycode(keynum, location); 505 506 // Also try to get get keysym from e.key 507 if (e.key) 508 keysym = keysym || keysym_from_key_identifier( 509 guac_keyboard.modifiers.shift, e.key, location); 510 511 // If no e.key, use e.keyIdentifier if absolutely necessary (can be buggy) 512 else { 513 514 var keypress_unlikely = guac_keyboard.modifiers.ctrl 515 || guac_keyboard.modifiers.alt 516 || guac_keyboard.modifiers.meta 517 || guac_keyboard.modifiers.hyper; 518 519 if (keypress_unlikely && e.keyIdentifier) 520 keysym = keysym || keysym_from_key_identifier( 521 guac_keyboard.modifiers.shift, e.keyIdentifier, location); 522 523 } 524 525 // Press key if known 526 if (keysym !== null) { 527 528 keydownChar[keynum] = keysym; 529 if (!press_key(keysym)) 530 e.preventDefault(); 531 532 // If a key is pressed while meta is held down, the keyup will 533 // never be sent in Chrome, so send it now. (bug #108404) 534 if (guac_keyboard.modifiers.meta && keysym !== 0xFFE7 && keysym !== 0xFFE8) 535 release_key(keysym); 536 537 } 538 539 }, true); 540 541 // When key pressed 542 element.addEventListener("keypress", function(e) { 543 544 // Only intercept if handler set 545 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 546 547 var keynum; 548 if (window.event) keynum = window.event.keyCode; 549 else if (e.which) keynum = e.which; 550 551 var keysym = keysym_from_charcode(keynum); 552 553 // Fix modifier states 554 update_modifier_state(e); 555 556 // If event identified as a typable character, and we're holding Ctrl+Alt, 557 // assume Ctrl+Alt is actually AltGr, and release both. 558 if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) { 559 release_key(0xFFE3); // Left ctrl 560 release_key(0xFFE4); // Right ctrl 561 release_key(0xFFE9); // Left alt 562 release_key(0xFFEA); // Right alt 563 } 564 565 // Send press + release if keysym known 566 if (keysym !== null) { 567 if (!press_key(keysym)) 568 e.preventDefault(); 569 release_key(keysym); 570 } 571 else 572 e.preventDefault(); 573 574 }, true); 575 576 // When key released 577 element.addEventListener("keyup", function(e) { 578 579 // Only intercept if handler set 580 if (!guac_keyboard.onkeyup) return; 581 582 e.preventDefault(); 583 584 var keynum; 585 if (window.event) keynum = window.event.keyCode; 586 else if (e.which) keynum = e.which; 587 588 // Fix modifier states 589 update_modifier_state(e); 590 591 // Send release event if original key known 592 var keysym = keydownChar[keynum]; 593 if (keysym !== null) 594 release_key(keysym); 595 596 // Clear character record 597 keydownChar[keynum] = null; 598 599 }, true); 600 601 }; 602 603 /** 604 * The state of all supported keyboard modifiers. 605 * @constructor 606 */ 607 Guacamole.Keyboard.ModifierState = function() { 608 609 /** 610 * Whether shift is currently pressed. 611 * @type Boolean 612 */ 613 this.shift = false; 614 615 /** 616 * Whether ctrl is currently pressed. 617 * @type Boolean 618 */ 619 this.ctrl = false; 620 621 /** 622 * Whether alt is currently pressed. 623 * @type Boolean 624 */ 625 this.alt = false; 626 627 /** 628 * Whether meta (apple key) is currently pressed. 629 * @type Boolean 630 */ 631 this.meta = false; 632 633 /** 634 * Whether hyper (windows key) is currently pressed. 635 * @type Boolean 636 */ 637 this.hyper = false; 638 639 }; 640 641 /** 642 * Returns the modifier state applicable to the keyboard event given. 643 * 644 * @param {KeyboardEvent} e The keyboard event to read. 645 * @returns {Guacamole.Keyboard.ModifierState} The current state of keyboard 646 * modifiers. 647 */ 648 Guacamole.Keyboard.ModifierState.fromKeyboardEvent = function(e) { 649 650 var state = new Guacamole.Keyboard.ModifierState(); 651 652 // Assign states from old flags 653 state.shift = e.shiftKey; 654 state.ctrl = e.ctrlKey; 655 state.alt = e.altKey; 656 state.meta = e.metaKey; 657 658 // Use DOM3 getModifierState() for others 659 if (e.getModifierState) { 660 state.hyper = e.getModifierState("OS") 661 || e.getModifierState("Super") 662 || e.getModifierState("Hyper") 663 || e.getModifierState("Win"); 664 } 665 666 return state; 667 668 }; 669