1 2 /* ***** BEGIN LICENSE BLOCK ***** 3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1 4 * 5 * The contents of this file are subject to the Mozilla Public License Version 6 * 1.1 (the "License"); you may not use this file except in compliance with 7 * the License. You may obtain a copy of the License at 8 * http://www.mozilla.org/MPL/ 9 * 10 * Software distributed under the License is distributed on an "AS IS" basis, 11 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License 12 * for the specific language governing rights and limitations under the 13 * License. 14 * 15 * The Original Code is guacamole-common-js. 16 * 17 * The Initial Developer of the Original Code is 18 * Michael Jumper. 19 * Portions created by the Initial Developer are Copyright (C) 2010 20 * the Initial Developer. All Rights Reserved. 21 * 22 * Contributor(s): 23 * 24 * Alternatively, the contents of this file may be used under the terms of 25 * either the GNU General Public License Version 2 or later (the "GPL"), or 26 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), 27 * in which case the provisions of the GPL or the LGPL are applicable instead 28 * of those above. If you wish to allow use of your version of this file only 29 * under the terms of either the GPL or the LGPL, and not to allow others to 30 * use your version of this file under the terms of the MPL, indicate your 31 * decision by deleting the provisions above and replace them with the notice 32 * and other provisions required by the GPL or the LGPL. If you do not delete 33 * the provisions above, a recipient may use your version of this file under 34 * the terms of any one of the MPL, the GPL or the LGPL. 35 * 36 * ***** END LICENSE BLOCK ***** */ 37 38 /** 39 * Namespace for all Guacamole JavaScript objects. 40 * @namespace 41 */ 42 var Guacamole = Guacamole || {}; 43 44 /** 45 * Provides cross-browser and cross-keyboard keyboard for a specific element. 46 * Browser and keyboard layout variation is abstracted away, providing events 47 * which represent keys as their corresponding X11 keysym. 48 * 49 * @constructor 50 * @param {Element} element The Element to use to provide keyboard events. 51 */ 52 Guacamole.Keyboard = function(element) { 53 54 /** 55 * Reference to this Guacamole.Keyboard. 56 * @private 57 */ 58 var guac_keyboard = this; 59 60 /** 61 * Fired whenever the user presses a key with the element associated 62 * with this Guacamole.Keyboard in focus. 63 * 64 * @event 65 * @param {Number} keysym The keysym of the key being pressed. 66 */ 67 this.onkeydown = null; 68 69 /** 70 * Fired whenever the user releases a key with the element associated 71 * with this Guacamole.Keyboard in focus. 72 * 73 * @event 74 * @param {Number} keysym The keysym of the key being released. 75 */ 76 this.onkeyup = null; 77 78 /** 79 * Map of known JavaScript keycodes which do not map to typable characters 80 * to their unshifted X11 keysym equivalents. 81 * @private 82 */ 83 var unshiftedKeysym = { 84 8: [0xFF08], // backspace 85 9: [0xFF09], // tab 86 13: [0xFF0D], // enter 87 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 88 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl 89 18: [0xFFE9, 0xFFE9, 0xFFEA], // alt 90 19: [0xFF13], // pause/break 91 20: [0xFFE5], // caps lock 92 27: [0xFF1B], // escape 93 32: [0x0020], // space 94 33: [0xFF55], // page up 95 34: [0xFF56], // page down 96 35: [0xFF57], // end 97 36: [0xFF50], // home 98 37: [0xFF51], // left arrow 99 38: [0xFF52], // up arrow 100 39: [0xFF53], // right arrow 101 40: [0xFF54], // down arrow 102 45: [0xFF63], // insert 103 46: [0xFFFF], // delete 104 91: [0xFFEB], // left window key (super_l) 105 92: [0xFF67], // right window key (menu key?) 106 93: null, // select key 107 112: [0xFFBE], // f1 108 113: [0xFFBF], // f2 109 114: [0xFFC0], // f3 110 115: [0xFFC1], // f4 111 116: [0xFFC2], // f5 112 117: [0xFFC3], // f6 113 118: [0xFFC4], // f7 114 119: [0xFFC5], // f8 115 120: [0xFFC6], // f9 116 121: [0xFFC7], // f10 117 122: [0xFFC8], // f11 118 123: [0xFFC9], // f12 119 144: [0xFF7F], // num lock 120 145: [0xFF14] // scroll lock 121 }; 122 123 /** 124 * Map of known JavaScript keyidentifiers which do not map to typable 125 * characters to their unshifted X11 keysym equivalents. 126 * @private 127 */ 128 var keyidentifier_keysym = { 129 "AllCandidates": [0xFF3D], 130 "Alphanumeric": [0xFF30], 131 "Alt": [0xFFE9, 0xFFE9, 0xFFEA], 132 "Attn": [0xFD0E], 133 "AltGraph": [0xFFEA], 134 "CapsLock": [0xFFE5], 135 "Clear": [0xFF0B], 136 "Convert": [0xFF21], 137 "Copy": [0xFD15], 138 "Crsel": [0xFD1C], 139 "CodeInput": [0xFF37], 140 "Control": [0xFFE3, 0xFFE3, 0xFFE4], 141 "Down": [0xFF54], 142 "End": [0xFF57], 143 "Enter": [0xFF0D], 144 "EraseEof": [0xFD06], 145 "Execute": [0xFF62], 146 "Exsel": [0xFD1D], 147 "F1": [0xFFBE], 148 "F2": [0xFFBF], 149 "F3": [0xFFC0], 150 "F4": [0xFFC1], 151 "F5": [0xFFC2], 152 "F6": [0xFFC3], 153 "F7": [0xFFC4], 154 "F8": [0xFFC5], 155 "F9": [0xFFC6], 156 "F10": [0xFFC7], 157 "F11": [0xFFC8], 158 "F12": [0xFFC9], 159 "F13": [0xFFCA], 160 "F14": [0xFFCB], 161 "F15": [0xFFCC], 162 "F16": [0xFFCD], 163 "F17": [0xFFCE], 164 "F18": [0xFFCF], 165 "F19": [0xFFD0], 166 "F20": [0xFFD1], 167 "F21": [0xFFD2], 168 "F22": [0xFFD3], 169 "F23": [0xFFD4], 170 "F24": [0xFFD5], 171 "Find": [0xFF68], 172 "FullWidth": null, 173 "HalfWidth": null, 174 "HangulMode": [0xFF31], 175 "HanjaMode": [0xFF34], 176 "Help": [0xFF6A], 177 "Hiragana": [0xFF25], 178 "Home": [0xFF50], 179 "Insert": [0xFF63], 180 "JapaneseHiragana": [0xFF25], 181 "JapaneseKatakana": [0xFF26], 182 "JapaneseRomaji": [0xFF24], 183 "JunjaMode": [0xFF38], 184 "KanaMode": [0xFF2D], 185 "KanjiMode": [0xFF21], 186 "Katakana": [0xFF26], 187 "Left": [0xFF51], 188 "Meta": [0xFFE7], 189 "NumLock": [0xFF7F], 190 "PageDown": [0xFF55], 191 "PageUp": [0xFF56], 192 "Pause": [0xFF13], 193 "PreviousCandidate": [0xFF3E], 194 "PrintScreen": [0xFD1D], 195 "Right": [0xFF53], 196 "RomanCharacters": null, 197 "Scroll": [0xFF14], 198 "Select": [0xFF60], 199 "Shift": [0xFFE1, 0xFFE1, 0xFFE2], 200 "Up": [0xFF52], 201 "Undo": [0xFF65], 202 "Win": [0xFFEB] 203 }; 204 205 /** 206 * Map of known JavaScript keycodes which do not map to typable characters 207 * to their shifted X11 keysym equivalents. Keycodes must only be listed 208 * here if their shifted X11 keysym equivalents differ from their unshifted 209 * equivalents. 210 * @private 211 */ 212 var shiftedKeysym = { 213 18: [0xFFE7, 0xFFE7, 0xFFEA] // alt 214 }; 215 216 /** 217 * All keysyms which should not repeat when held down. 218 * @private 219 */ 220 var no_repeat = { 221 0xFFE1: true, // Left shift 222 0xFFE2: true, // Right shift 223 0xFFE3: true, // Left ctrl 224 0xFFE4: true, // Right ctrl 225 0xFFE9: true, // Left alt 226 0xFFEA: true // Right alt (or AltGr) 227 }; 228 229 /** 230 * All modifiers and their states. 231 */ 232 this.modifiers = { 233 234 /** 235 * Whether shift is currently pressed. 236 */ 237 "shift": false, 238 239 /** 240 * Whether ctrl is currently pressed. 241 */ 242 "ctrl" : false, 243 244 /** 245 * Whether alt is currently pressed. 246 */ 247 "alt" : false, 248 249 /** 250 * Whether meta (apple key) is currently pressed. 251 */ 252 "meta" : false 253 254 }; 255 256 /** 257 * The state of every key, indexed by keysym. If a particular key is 258 * pressed, the value of pressed for that keysym will be true. If a key 259 * is not currently pressed, it will not be defined. 260 */ 261 this.pressed = {}; 262 263 /** 264 * The keysym associated with a given keycode when keydown fired. 265 * @private 266 */ 267 var keydownChar = []; 268 269 /** 270 * Timeout before key repeat starts. 271 * @private 272 */ 273 var key_repeat_timeout = null; 274 275 /** 276 * Interval which presses and releases the last key pressed while that 277 * key is still being held down. 278 * @private 279 */ 280 var key_repeat_interval = null; 281 282 /** 283 * Given an array of keysyms indexed by location, returns the keysym 284 * for the given location, or the keysym for the standard location if 285 * undefined. 286 * 287 * @param {Array} keysyms An array of keysyms, where the index of the 288 * keysym in the array is the location value. 289 * @param {Number} location The location on the keyboard corresponding to 290 * the key pressed, as defined at: 291 * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent 292 */ 293 function get_keysym(keysyms, location) { 294 295 if (!keysyms) 296 return null; 297 298 return keysyms[location] || keysyms[0]; 299 } 300 301 function keysym_from_key_identifier(shifted, keyIdentifier, location) { 302 303 var unicodePrefixLocation = keyIdentifier.indexOf("U+"); 304 if (unicodePrefixLocation >= 0) { 305 306 var hex = keyIdentifier.substring(unicodePrefixLocation+2); 307 var codepoint = parseInt(hex, 16); 308 var typedCharacter; 309 310 // Convert case if shifted 311 if (shifted == 0) 312 typedCharacter = String.fromCharCode(codepoint).toLowerCase(); 313 else 314 typedCharacter = String.fromCharCode(codepoint).toUpperCase(); 315 316 // Get codepoint 317 codepoint = typedCharacter.charCodeAt(0); 318 319 return keysym_from_charcode(codepoint); 320 321 } 322 323 return get_keysym(keyidentifier_keysym[keyIdentifier], location); 324 325 } 326 327 function isControlCharacter(codepoint) { 328 return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); 329 } 330 331 function keysym_from_charcode(codepoint) { 332 333 // Keysyms for control characters 334 if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; 335 336 // Keysyms for ASCII chars 337 if (codepoint >= 0x0000 && codepoint <= 0x00FF) 338 return codepoint; 339 340 // Keysyms for Unicode 341 if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) 342 return 0x01000000 | codepoint; 343 344 return null; 345 346 } 347 348 function keysym_from_keycode(keyCode, location) { 349 350 var keysyms; 351 352 // If not shifted, just return unshifted keysym 353 if (!guac_keyboard.modifiers.shift) 354 keysyms = unshiftedKeysym[keyCode]; 355 356 // Otherwise, return shifted keysym, if defined 357 else 358 keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode]; 359 360 return get_keysym(keysyms, location); 361 362 } 363 364 365 /** 366 * Marks a key as pressed, firing the keydown event if registered. Key 367 * repeat for the pressed key will start after a delay if that key is 368 * not a modifier. 369 * @private 370 */ 371 function press_key(keysym) { 372 373 // Don't bother with pressing the key if the key is unknown 374 if (keysym == null) return; 375 376 // Only press if released 377 if (!guac_keyboard.pressed[keysym]) { 378 379 // Mark key as pressed 380 guac_keyboard.pressed[keysym] = true; 381 382 // Send key event 383 if (guac_keyboard.onkeydown) { 384 guac_keyboard.onkeydown(keysym); 385 386 // Stop any current repeat 387 window.clearTimeout(key_repeat_timeout); 388 window.clearInterval(key_repeat_interval); 389 390 // Repeat after a delay as long as pressed 391 if (!no_repeat[keysym]) 392 key_repeat_timeout = window.setTimeout(function() { 393 key_repeat_interval = window.setInterval(function() { 394 guac_keyboard.onkeyup(keysym); 395 guac_keyboard.onkeydown(keysym); 396 }, 50); 397 }, 500); 398 399 400 } 401 } 402 403 } 404 405 /** 406 * Marks a key as released, firing the keyup event if registered. 407 * @private 408 */ 409 function release_key(keysym) { 410 411 // Only release if pressed 412 if (guac_keyboard.pressed[keysym]) { 413 414 // Mark key as released 415 delete guac_keyboard.pressed[keysym]; 416 417 // Stop repeat 418 window.clearTimeout(key_repeat_timeout); 419 window.clearInterval(key_repeat_interval); 420 421 // Send key event 422 if (keysym != null && guac_keyboard.onkeyup) 423 guac_keyboard.onkeyup(keysym); 424 425 } 426 427 } 428 429 function isTypable(keyIdentifier) { 430 431 // Find unicode prefix 432 var unicodePrefixLocation = keyIdentifier.indexOf("U+"); 433 if (unicodePrefixLocation == -1) 434 return false; 435 436 // Parse codepoint value 437 var hex = keyIdentifier.substring(unicodePrefixLocation+2); 438 var codepoint = parseInt(hex, 16); 439 440 // If control character, not typable 441 if (isControlCharacter(codepoint)) return false; 442 443 // Otherwise, typable 444 return true; 445 446 } 447 448 /** 449 * Given a keyboard event, updates the local modifier state and remote 450 * key state based on the modifier flags within the event. This function 451 * pays no attention to keycodes. 452 * 453 * @param {KeyboardEvent} e The keyboard event containing the flags to update. 454 */ 455 function update_modifier_state(e) { 456 457 // Release alt if implicitly released 458 if (guac_keyboard.modifiers.alt && e.altKey === false) { 459 release_key(0xFFE9); // Left alt 460 release_key(0xFFEA); // Right alt (or AltGr) 461 guac_keyboard.modifiers.alt = false; 462 } 463 464 // Release shift if implicitly released 465 if (guac_keyboard.modifiers.shift && e.shiftKey === false) { 466 release_key(0xFFE1); // Left shift 467 release_key(0xFFE2); // Right shift 468 guac_keyboard.modifiers.shift = false; 469 } 470 471 // Release ctrl if implicitly released 472 if (guac_keyboard.modifiers.ctrl && e.ctrlKey === false) { 473 release_key(0xFFE3); // Left ctrl 474 release_key(0xFFE4); // Right ctrl 475 guac_keyboard.modifiers.ctrl = false; 476 } 477 478 } 479 480 // When key pressed 481 element.addEventListener("keydown", function(e) { 482 483 // Only intercept if handler set 484 if (!guac_keyboard.onkeydown) return; 485 486 var keynum; 487 if (window.event) keynum = window.event.keyCode; 488 else if (e.which) keynum = e.which; 489 490 // Get key location 491 var location = e.location || e.keyLocation || 0; 492 493 // Ignore any unknown key events 494 if (keynum == 0 && !e.keyIdentifier) { 495 e.preventDefault(); 496 return; 497 } 498 499 // Fix modifier states 500 update_modifier_state(e); 501 502 // Ctrl/Alt/Shift/Meta 503 if (keynum == 16) guac_keyboard.modifiers.shift = true; 504 else if (keynum == 17) guac_keyboard.modifiers.ctrl = true; 505 else if (keynum == 18) guac_keyboard.modifiers.alt = true; 506 else if (keynum == 91) guac_keyboard.modifiers.meta = true; 507 508 // Try to get keysym from keycode 509 var keysym = keysym_from_keycode(keynum, location); 510 511 // By default, we expect a corresponding keypress event 512 var expect_keypress = true; 513 514 // If key is known from keycode, prevent default 515 if (keysym) 516 expect_keypress = false; 517 518 // Also try to get get keysym from keyIdentifier 519 if (e.keyIdentifier) { 520 521 keysym = keysym || 522 keysym_from_key_identifier(guac_keyboard.modifiers.shift, 523 e.keyIdentifier, location); 524 525 // Prevent default if non-typable character or if modifier combination 526 // likely to be eaten by browser otherwise (NOTE: We must not prevent 527 // default for Ctrl+Alt, as that combination is commonly used for 528 // AltGr. If we receive AltGr, we need to handle keypress, which 529 // means we cannot cancel keydown). 530 if (!isTypable(e.keyIdentifier) 531 || ( guac_keyboard.modifiers.ctrl && !guac_keyboard.modifiers.alt) 532 || (!guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) 533 || (guac_keyboard.modifiers.meta)) 534 expect_keypress = false; 535 536 } 537 538 // If we do not expect to handle via keypress, handle now 539 if (!expect_keypress) { 540 e.preventDefault(); 541 542 // Press key if known 543 if (keysym != null) { 544 keydownChar[keynum] = keysym; 545 press_key(keysym); 546 547 // If a key is pressed while meta is held down, the keyup will never be sent in Chrome, so send it now. (bug #108404) 548 if(guac_keyboard.modifiers.meta) { 549 release_key(keysym); 550 } 551 } 552 553 } 554 555 }, true); 556 557 // When key pressed 558 element.addEventListener("keypress", function(e) { 559 560 // Only intercept if handler set 561 if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; 562 563 e.preventDefault(); 564 565 var keynum; 566 if (window.event) keynum = window.event.keyCode; 567 else if (e.which) keynum = e.which; 568 569 var keysym = keysym_from_charcode(keynum); 570 571 // Fix modifier states 572 update_modifier_state(e); 573 574 // If event identified as a typable character, and we're holding Ctrl+Alt, 575 // assume Ctrl+Alt is actually AltGr, and release both. 576 if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) { 577 release_key(0xFFE3); // Left ctrl 578 release_key(0xFFE4); // Right ctrl 579 release_key(0xFFE9); // Left alt 580 release_key(0xFFEA); // Right alt 581 } 582 583 // Send press + release if keysym known 584 if (keysym != null) { 585 press_key(keysym); 586 release_key(keysym); 587 } 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 // Ctrl/Alt/Shift/Meta 607 if (keynum == 16) guac_keyboard.modifiers.shift = false; 608 else if (keynum == 17) guac_keyboard.modifiers.ctrl = false; 609 else if (keynum == 18) guac_keyboard.modifiers.alt = false; 610 else if (keynum == 91) guac_keyboard.modifiers.meta = false; 611 612 // Send release event if original key known 613 var keydown_keysym = keydownChar[keynum]; 614 if (keydown_keysym != null) 615 release_key(keydown_keysym); 616 617 // Clear character record 618 keydownChar[keynum] = null; 619 620 }, true); 621 622 }; 623