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