/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is guacamole-common-js. * * The Initial Developer of the Original Code is * Michael Jumper. * Portions created by the Initial Developer are Copyright (C) 2010 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ /** * Namespace for all Guacamole JavaScript objects. * @namespace */ var Guacamole = Guacamole || {}; /** * Provides cross-browser and cross-keyboard keyboard for a specific element. * Browser and keyboard layout variation is abstracted away, providing events * which represent keys as their corresponding X11 keysym. * * @constructor * @param {Element} element The Element to use to provide keyboard events. */ Guacamole.Keyboard = function(element) { /** * Reference to this Guacamole.Keyboard. * @private */ var guac_keyboard = this; /** * Fired whenever the user presses a key with the element associated * with this Guacamole.Keyboard in focus. * * @event * @param {Number} keysym The keysym of the key being pressed. */ this.onkeydown = null; /** * Fired whenever the user releases a key with the element associated * with this Guacamole.Keyboard in focus. * * @event * @param {Number} keysym The keysym of the key being released. */ this.onkeyup = null; /** * Map of known JavaScript keycodes which do not map to typable characters * to their unshifted X11 keysym equivalents. * @private */ var unshiftedKeysym = { 8: [0xFF08], // backspace 9: [0xFF09], // tab 13: [0xFF0D], // enter 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl 18: [0xFFE9, 0xFFE9, 0xFFEA], // alt 19: [0xFF13], // pause/break 20: [0xFFE5], // caps lock 27: [0xFF1B], // escape 32: [0x0020], // space 33: [0xFF55], // page up 34: [0xFF56], // page down 35: [0xFF57], // end 36: [0xFF50], // home 37: [0xFF51], // left arrow 38: [0xFF52], // up arrow 39: [0xFF53], // right arrow 40: [0xFF54], // down arrow 45: [0xFF63], // insert 46: [0xFFFF], // delete 91: [0xFFEB], // left window key (super_l) 92: [0xFF67], // right window key (menu key?) 93: null, // select key 112: [0xFFBE], // f1 113: [0xFFBF], // f2 114: [0xFFC0], // f3 115: [0xFFC1], // f4 116: [0xFFC2], // f5 117: [0xFFC3], // f6 118: [0xFFC4], // f7 119: [0xFFC5], // f8 120: [0xFFC6], // f9 121: [0xFFC7], // f10 122: [0xFFC8], // f11 123: [0xFFC9], // f12 144: [0xFF7F], // num lock 145: [0xFF14] // scroll lock }; /** * Map of known JavaScript keyidentifiers which do not map to typable * characters to their unshifted X11 keysym equivalents. * @private */ var keyidentifier_keysym = { "AllCandidates": [0xFF3D], "Alphanumeric": [0xFF30], "Alt": [0xFFE9, 0xFFE9, 0xFFEA], "Attn": [0xFD0E], "AltGraph": [0xFFEA], "CapsLock": [0xFFE5], "Clear": [0xFF0B], "Convert": [0xFF21], "Copy": [0xFD15], "Crsel": [0xFD1C], "CodeInput": [0xFF37], "Control": [0xFFE3, 0xFFE3, 0xFFE4], "Down": [0xFF54], "End": [0xFF57], "Enter": [0xFF0D], "EraseEof": [0xFD06], "Execute": [0xFF62], "Exsel": [0xFD1D], "F1": [0xFFBE], "F2": [0xFFBF], "F3": [0xFFC0], "F4": [0xFFC1], "F5": [0xFFC2], "F6": [0xFFC3], "F7": [0xFFC4], "F8": [0xFFC5], "F9": [0xFFC6], "F10": [0xFFC7], "F11": [0xFFC8], "F12": [0xFFC9], "F13": [0xFFCA], "F14": [0xFFCB], "F15": [0xFFCC], "F16": [0xFFCD], "F17": [0xFFCE], "F18": [0xFFCF], "F19": [0xFFD0], "F20": [0xFFD1], "F21": [0xFFD2], "F22": [0xFFD3], "F23": [0xFFD4], "F24": [0xFFD5], "Find": [0xFF68], "FullWidth": null, "HalfWidth": null, "HangulMode": [0xFF31], "HanjaMode": [0xFF34], "Help": [0xFF6A], "Hiragana": [0xFF25], "Home": [0xFF50], "Insert": [0xFF63], "JapaneseHiragana": [0xFF25], "JapaneseKatakana": [0xFF26], "JapaneseRomaji": [0xFF24], "JunjaMode": [0xFF38], "KanaMode": [0xFF2D], "KanjiMode": [0xFF21], "Katakana": [0xFF26], "Left": [0xFF51], "Meta": [0xFFE7], "NumLock": [0xFF7F], "PageDown": [0xFF55], "PageUp": [0xFF56], "Pause": [0xFF13], "PreviousCandidate": [0xFF3E], "PrintScreen": [0xFD1D], "Right": [0xFF53], "RomanCharacters": null, "Scroll": [0xFF14], "Select": [0xFF60], "Shift": [0xFFE1, 0xFFE1, 0xFFE2], "Up": [0xFF52], "Undo": [0xFF65], "Win": [0xFFEB] }; /** * Map of known JavaScript keycodes which do not map to typable characters * to their shifted X11 keysym equivalents. Keycodes must only be listed * here if their shifted X11 keysym equivalents differ from their unshifted * equivalents. * @private */ var shiftedKeysym = { 18: [0xFFE7, 0xFFE7, 0xFFEA] // alt }; /** * All keysyms which should not repeat when held down. * @private */ var no_repeat = { 0xFFE1: true, // Left shift 0xFFE2: true, // Right shift 0xFFE3: true, // Left ctrl 0xFFE4: true, // Right ctrl 0xFFE9: true, // Left alt 0xFFEA: true // Right alt (or AltGr) }; /** * All modifiers and their states. */ this.modifiers = { /** * Whether shift is currently pressed. */ "shift": false, /** * Whether ctrl is currently pressed. */ "ctrl" : false, /** * Whether alt is currently pressed. */ "alt" : false, /** * Whether meta (apple key) is currently pressed. */ "meta" : false }; /** * The state of every key, indexed by keysym. If a particular key is * pressed, the value of pressed for that keysym will be true. If a key * is not currently pressed, it will not be defined. */ this.pressed = {}; /** * The keysym associated with a given keycode when keydown fired. * @private */ var keydownChar = []; /** * Timeout before key repeat starts. * @private */ var key_repeat_timeout = null; /** * Interval which presses and releases the last key pressed while that * key is still being held down. * @private */ var key_repeat_interval = null; /** * Given an array of keysyms indexed by location, returns the keysym * for the given location, or the keysym for the standard location if * undefined. * * @param {Array} keysyms An array of keysyms, where the index of the * keysym in the array is the location value. * @param {Number} location The location on the keyboard corresponding to * the key pressed, as defined at: * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent */ function get_keysym(keysyms, location) { if (!keysyms) return null; return keysyms[location] || keysyms[0]; } function keysym_from_key_identifier(shifted, keyIdentifier, location) { var unicodePrefixLocation = keyIdentifier.indexOf("U+"); if (unicodePrefixLocation >= 0) { var hex = keyIdentifier.substring(unicodePrefixLocation+2); var codepoint = parseInt(hex, 16); var typedCharacter; // Convert case if shifted if (shifted == 0) typedCharacter = String.fromCharCode(codepoint).toLowerCase(); else typedCharacter = String.fromCharCode(codepoint).toUpperCase(); // Get codepoint codepoint = typedCharacter.charCodeAt(0); return keysym_from_charcode(codepoint); } return get_keysym(keyidentifier_keysym[keyIdentifier], location); } function isControlCharacter(codepoint) { return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); } function keysym_from_charcode(codepoint) { // Keysyms for control characters if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; // Keysyms for ASCII chars if (codepoint >= 0x0000 && codepoint <= 0x00FF) return codepoint; // Keysyms for Unicode if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) return 0x01000000 | codepoint; return null; } function keysym_from_keycode(keyCode, location) { var keysyms; // If not shifted, just return unshifted keysym if (!guac_keyboard.modifiers.shift) keysyms = unshiftedKeysym[keyCode]; // Otherwise, return shifted keysym, if defined else keysyms = shiftedKeysym[keyCode] || unshiftedKeysym[keyCode]; return get_keysym(keysyms, location); } /** * Marks a key as pressed, firing the keydown event if registered. Key * repeat for the pressed key will start after a delay if that key is * not a modifier. * @private */ function press_key(keysym) { // Don't bother with pressing the key if the key is unknown if (keysym == null) return; // Only press if released if (!guac_keyboard.pressed[keysym]) { // Mark key as pressed guac_keyboard.pressed[keysym] = true; // Send key event if (guac_keyboard.onkeydown) { guac_keyboard.onkeydown(keysym); // Stop any current repeat window.clearTimeout(key_repeat_timeout); window.clearInterval(key_repeat_interval); // Repeat after a delay as long as pressed if (!no_repeat[keysym]) key_repeat_timeout = window.setTimeout(function() { key_repeat_interval = window.setInterval(function() { guac_keyboard.onkeyup(keysym); guac_keyboard.onkeydown(keysym); }, 50); }, 500); } } } /** * Marks a key as released, firing the keyup event if registered. * @private */ function release_key(keysym) { // Only release if pressed if (guac_keyboard.pressed[keysym]) { // Mark key as released delete guac_keyboard.pressed[keysym]; // Stop repeat window.clearTimeout(key_repeat_timeout); window.clearInterval(key_repeat_interval); // Send key event if (keysym != null && guac_keyboard.onkeyup) guac_keyboard.onkeyup(keysym); } } function isTypable(keyIdentifier) { // Find unicode prefix var unicodePrefixLocation = keyIdentifier.indexOf("U+"); if (unicodePrefixLocation == -1) return false; // Parse codepoint value var hex = keyIdentifier.substring(unicodePrefixLocation+2); var codepoint = parseInt(hex, 16); // If control character, not typable if (isControlCharacter(codepoint)) return false; // Otherwise, typable return true; } /** * Given a keyboard event, updates the local modifier state and remote * key state based on the modifier flags within the event. This function * pays no attention to keycodes. * * @param {KeyboardEvent} e The keyboard event containing the flags to update. */ function update_modifier_state(e) { // Release alt if implicitly released if (guac_keyboard.modifiers.alt && e.altKey === false) { release_key(0xFFE9); // Left alt release_key(0xFFEA); // Right alt (or AltGr) guac_keyboard.modifiers.alt = false; } // Release shift if implicitly released if (guac_keyboard.modifiers.shift && e.shiftKey === false) { release_key(0xFFE1); // Left shift release_key(0xFFE2); // Right shift guac_keyboard.modifiers.shift = false; } // Release ctrl if implicitly released if (guac_keyboard.modifiers.ctrl && e.ctrlKey === false) { release_key(0xFFE3); // Left ctrl release_key(0xFFE4); // Right ctrl guac_keyboard.modifiers.ctrl = false; } } // When key pressed element.addEventListener("keydown", function(e) { // Only intercept if handler set if (!guac_keyboard.onkeydown) return; var keynum; if (window.event) keynum = window.event.keyCode; else if (e.which) keynum = e.which; // Get key location var location = e.location || e.keyLocation || 0; // Ignore any unknown key events if (keynum == 0 && !e.keyIdentifier) { e.preventDefault(); return; } // Fix modifier states update_modifier_state(e); // Ctrl/Alt/Shift/Meta if (keynum == 16) guac_keyboard.modifiers.shift = true; else if (keynum == 17) guac_keyboard.modifiers.ctrl = true; else if (keynum == 18) guac_keyboard.modifiers.alt = true; else if (keynum == 91) guac_keyboard.modifiers.meta = true; // Try to get keysym from keycode var keysym = keysym_from_keycode(keynum, location); // By default, we expect a corresponding keypress event var expect_keypress = true; // If key is known from keycode, prevent default if (keysym) expect_keypress = false; // Also try to get get keysym from keyIdentifier if (e.keyIdentifier) { keysym = keysym || keysym_from_key_identifier(guac_keyboard.modifiers.shift, e.keyIdentifier, location); // Prevent default if non-typable character or if modifier combination // likely to be eaten by browser otherwise (NOTE: We must not prevent // default for Ctrl+Alt, as that combination is commonly used for // AltGr. If we receive AltGr, we need to handle keypress, which // means we cannot cancel keydown). if (!isTypable(e.keyIdentifier) || ( guac_keyboard.modifiers.ctrl && !guac_keyboard.modifiers.alt) || (!guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) || (guac_keyboard.modifiers.meta)) expect_keypress = false; } // If we do not expect to handle via keypress, handle now if (!expect_keypress) { e.preventDefault(); // Press key if known if (keysym != null) { keydownChar[keynum] = keysym; press_key(keysym); // If a key is pressed while meta is held down, the keyup will never be sent in Chrome, so send it now. (bug #108404) if(guac_keyboard.modifiers.meta) { release_key(keysym); } } } }, true); // When key pressed element.addEventListener("keypress", function(e) { // Only intercept if handler set if (!guac_keyboard.onkeydown && !guac_keyboard.onkeyup) return; e.preventDefault(); var keynum; if (window.event) keynum = window.event.keyCode; else if (e.which) keynum = e.which; var keysym = keysym_from_charcode(keynum); // Fix modifier states update_modifier_state(e); // If event identified as a typable character, and we're holding Ctrl+Alt, // assume Ctrl+Alt is actually AltGr, and release both. if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) { release_key(0xFFE3); // Left ctrl release_key(0xFFE4); // Right ctrl release_key(0xFFE9); // Left alt release_key(0xFFEA); // Right alt } // Send press + release if keysym known if (keysym != null) { press_key(keysym); release_key(keysym); } }, true); // When key released element.addEventListener("keyup", function(e) { // Only intercept if handler set if (!guac_keyboard.onkeyup) return; e.preventDefault(); var keynum; if (window.event) keynum = window.event.keyCode; else if (e.which) keynum = e.which; // Fix modifier states update_modifier_state(e); // Ctrl/Alt/Shift/Meta if (keynum == 16) guac_keyboard.modifiers.shift = false; else if (keynum == 17) guac_keyboard.modifiers.ctrl = false; else if (keynum == 18) guac_keyboard.modifiers.alt = false; else if (keynum == 91) guac_keyboard.modifiers.meta = false; // Send release event if original key known var keydown_keysym = keydownChar[keynum]; if (keydown_keysym != null) release_key(keydown_keysym); // Clear character record keydownChar[keynum] = null; }, true); };