2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
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/
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
15 * The Original Code is guacamole-common-js.
17 * The Initial Developer of the Original Code is
19 * Portions created by the Initial Developer are Copyright (C) 2010
20 * the Initial Developer. All Rights Reserved.
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.
36 * ***** END LICENSE BLOCK ***** */
39 * Namespace for all Guacamole JavaScript objects.
42 var Guacamole = Guacamole || {};
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.
50 * @param {Element} element The Element to use to provide keyboard events.
52 Guacamole.Keyboard = function(element) {
55 * Reference to this Guacamole.Keyboard.
58 var guac_keyboard = this;
61 * Fired whenever the user presses a key with the element associated
62 * with this Guacamole.Keyboard in focus.
65 * @param {Number} keysym The keysym of the key being pressed.
67 this.onkeydown = null;
70 * Fired whenever the user releases a key with the element associated
71 * with this Guacamole.Keyboard in focus.
74 * @param {Number} keysym The keysym of the key being released.
79 * Map of known JavaScript keycodes which do not map to typable characters
80 * to their unshifted X11 keysym equivalents.
83 var unshiftedKeySym = {
84 8: 0xFF08, // backspace
90 19: 0xFF13, // pause/break
91 20: 0xFFE5, // caps lock
94 33: 0xFF55, // page up
95 34: 0xFF56, // page down
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
119 144: 0xFF7F, // num lock
120 145: 0xFF14 // scroll lock
124 * Map of known JavaScript keyidentifiers which do not map to typable
125 * characters to their unshifted X11 keysym equivalents.
128 var keyidentifier_keysym = {
129 "AllCandidates": 0xFF3D,
130 "Alphanumeric": 0xFF30,
174 "HangulMode": 0xFF31,
180 "JapaneseHiragana": 0xFF25,
181 "JapaneseKatakana": 0xFF26,
182 "JapaneseRomaji": 0xFF24,
193 "PreviousCandidate": 0xFF3E,
194 "PrintScreen": 0xFD1D,
196 "RomanCharacters": null,
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
212 var shiftedKeySym = {
217 * All modifiers and their states.
222 * Whether shift is currently pressed.
227 * Whether ctrl is currently pressed.
232 * Whether alt is currently pressed.
239 * The state of every key, indexed by keysym. If a particular key is
240 * pressed, the value of pressed for that keysym will be true. If a key
241 * is not currently pressed, the value for that keysym may be false or
246 var keydownChar = new Array();
248 // ID of routine repeating keystrokes. -1 = not repeating.
249 var repeatKeyTimeoutId = -1;
250 var repeatKeyIntervalId = -1;
252 // Starts repeating keystrokes
253 function startRepeat(keySym) {
254 repeatKeyIntervalId = setInterval(function() {
255 sendKeyReleased(keySym);
256 sendKeyPressed(keySym);
260 // Stops repeating keystrokes
261 function stopRepeat() {
262 if (repeatKeyTimeoutId != -1) clearTimeout(repeatKeyTimeoutId);
263 if (repeatKeyIntervalId != -1) clearInterval(repeatKeyIntervalId);
267 function getKeySymFromKeyIdentifier(shifted, keyIdentifier) {
269 var unicodePrefixLocation = keyIdentifier.indexOf("U+");
270 if (unicodePrefixLocation >= 0) {
272 var hex = keyIdentifier.substring(unicodePrefixLocation+2);
273 var codepoint = parseInt(hex, 16);
276 // Convert case if shifted
278 typedCharacter = String.fromCharCode(codepoint).toLowerCase();
280 typedCharacter = String.fromCharCode(codepoint).toUpperCase();
283 codepoint = typedCharacter.charCodeAt(0);
285 return getKeySymFromCharCode(codepoint);
289 return keyidentifier_keysym[keyIdentifier];
293 function isControlCharacter(codepoint) {
294 return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
297 function getKeySymFromCharCode(codepoint) {
299 // Keysyms for control characters
300 if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
302 // Keysyms for ASCII chars
303 if (codepoint >= 0x0000 && codepoint <= 0x00FF)
306 // Keysyms for Unicode
307 if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
308 return 0x01000000 | codepoint;
314 function getKeySymFromKeyCode(keyCode) {
317 if (!guac_keyboard.modifiers.shift) keysym = unshiftedKeySym[keyCode];
319 keysym = shiftedKeySym[keyCode];
320 if (keysym == null) keysym = unshiftedKeySym[keyCode];
328 // Sends a single keystroke over the network
329 function sendKeyPressed(keysym) {
331 // Mark key as pressed
332 guac_keyboard.pressed[keysym] = true;
335 if (keysym != null && guac_keyboard.onkeydown)
336 guac_keyboard.onkeydown(keysym);
340 // Sends a single keystroke over the network
341 function sendKeyReleased(keysym) {
343 // Mark key as released
344 guac_keyboard.pressed[keysym] = false;
347 if (keysym != null && guac_keyboard.onkeyup)
348 guac_keyboard.onkeyup(keysym);
353 var expect_keypress = true;
354 var keydown_code = null;
356 var deferred_keypress = null;
357 var keydown_keysym = null;
358 var keypress_keysym = null;
360 function handleKeyEvents() {
362 // Prefer keysym from keypress
363 var keysym = keypress_keysym || keydown_keysym;
364 var keynum = keydown_code;
366 if (keydownChar[keynum] != keysym) {
368 // If this button is already pressed, release first
369 var lastKeyDownChar = keydownChar[keydown_code];
371 sendKeyReleased(lastKeyDownChar);
374 keydownChar[keynum] = keysym;
375 sendKeyPressed(keysym);
377 // Clear old key repeat, if any.
380 // Start repeating (if not a modifier key) after a short delay
381 if (keynum != 16 && keynum != 17 && keynum != 18)
382 repeatKeyTimeoutId = setTimeout(function() { startRepeat(keysym); }, 500);
386 // Done with deferred key event
387 deferred_keypress = null;
388 keypress_keysym = null;
389 keydown_keysym = null;
394 function isTypable(keyIdentifier) {
396 // Find unicode prefix
397 var unicodePrefixLocation = keyIdentifier.indexOf("U+");
398 if (unicodePrefixLocation == -1)
401 // Parse codepoint value
402 var hex = keyIdentifier.substring(unicodePrefixLocation+2);
403 var codepoint = parseInt(hex, 16);
405 // If control character, not typable
406 if (isControlCharacter(codepoint)) return false;
408 // Otherwise, typable
414 element.addEventListener("keydown", function(e) {
416 // Only intercept if handler set
417 if (!guac_keyboard.onkeydown) return;
420 if (window.event) keynum = window.event.keyCode;
421 else if (e.which) keynum = e.which;
423 // Ignore any unknown key events
424 if (keynum == 0 && !e.keyIdentifier) {
429 expect_keypress = true;
432 if (keynum == 16) guac_keyboard.modifiers.shift = true;
433 else if (keynum == 17) guac_keyboard.modifiers.ctrl = true;
434 else if (keynum == 18) guac_keyboard.modifiers.alt = true;
436 // Try to get keysym from keycode
437 keydown_keysym = getKeySymFromKeyCode(keynum);
439 // If key is known from keycode, prevent default
441 expect_keypress = false;
443 // Also try to get get keysym from keyIdentifier
444 if (e.keyIdentifier) {
446 keydown_keysym = keydown_keysym ||
447 getKeySymFromKeyIdentifier(guac_keyboard.modifiers.shift, e.keyIdentifier);
449 // Prevent default if non-typable character or if modifier combination
450 // likely to be eaten by browser otherwise (NOTE: We must not prevent
451 // default for Ctrl+Alt, as that combination is commonly used for
452 // AltGr. If we receive AltGr, we need to handle keypress, which
453 // means we cannot cancel keydown).
454 if (!isTypable(e.keyIdentifier)
455 || ( guac_keyboard.modifiers.ctrl && !guac_keyboard.modifiers.alt)
456 || (!guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt))
457 expect_keypress = false;
461 // Set keycode which will be associated with any future keypress
462 keydown_code = keynum;
464 // If we expect to handle via keypress, set failsafe timeout and
465 // wait for keypress.
466 if (expect_keypress) {
467 if (!deferred_keypress)
468 deferred_keypress = window.setTimeout(handleKeyEvents, 0);
471 // Otherwise, handle now
480 element.addEventListener("keypress", function(e) {
482 // Only intercept if handler set
483 if (!guac_keyboard.onkeydown) return;
487 // Do not handle if we weren't expecting this event (will have already
488 // been handled by keydown)
489 if (!expect_keypress) return;
492 if (window.event) keynum = window.event.keyCode;
493 else if (e.which) keynum = e.which;
495 keypress_keysym = getKeySymFromCharCode(keynum);
497 // If event identified as a typable character, and we're holding Ctrl+Alt,
498 // assume Ctrl+Alt is actually AltGr, and release both.
499 if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) {
500 sendKeyReleased(0xFFE3);
501 sendKeyReleased(0xFFE9);
504 // Clear timeout, if any
505 if (deferred_keypress)
506 window.clearTimeout(deferred_keypress);
508 // Handle event with all aggregated data
514 element.addEventListener("keyup", function(e) {
516 // Only intercept if handler set
517 if (!guac_keyboard.onkeyup) return;
522 if (window.event) keynum = window.event.keyCode;
523 else if (e.which) keynum = e.which;
526 if (keynum == 16) guac_keyboard.modifiers.shift = false;
527 else if (keynum == 17) guac_keyboard.modifiers.ctrl = false;
528 else if (keynum == 18) guac_keyboard.modifiers.alt = false;
532 // Get corresponding character
533 var lastKeyDownChar = keydownChar[keynum];
535 // Clear character record
536 keydownChar[keynum] = null;
538 // Send release event
539 sendKeyReleased(lastKeyDownChar);
543 // When focus is lost, clear modifiers.
544 element.addEventListener("blur", function() {
545 guac_keyboard.modifiers.alt = false;
546 guac_keyboard.modifiers.ctrl = false;
547 guac_keyboard.modifiers.shift = false;