Fix jsdoc, add missing documentation.
[guacamole-common-js.git] / src / main / resources / keyboard.js
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, // shift
88         17:  0xFFE3, // ctrl
89         18:  0xFFE9, // 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,
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,
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,
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  // alt
214     };
215
216     /**
217      * All modifiers and their states.
218      */
219     this.modifiers = {
220         
221         /**
222          * Whether shift is currently pressed.
223          */
224         "shift": false,
225         
226         /**
227          * Whether ctrl is currently pressed.
228          */
229         "ctrl" : false,
230         
231         /**
232          * Whether alt is currently pressed.
233          */
234         "alt"  : false
235
236     };
237
238     /**
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
242      * undefined.
243      */
244     this.pressed = [];
245
246     var keydownChar = new Array();
247
248     // ID of routine repeating keystrokes. -1 = not repeating.
249     var repeatKeyTimeoutId = -1;
250     var repeatKeyIntervalId = -1;
251
252     // Starts repeating keystrokes
253     function startRepeat(keySym) {
254         repeatKeyIntervalId = setInterval(function() {
255             sendKeyReleased(keySym);
256             sendKeyPressed(keySym);
257         }, 50);
258     }
259
260     // Stops repeating keystrokes
261     function stopRepeat() {
262         if (repeatKeyTimeoutId != -1) clearTimeout(repeatKeyTimeoutId);
263         if (repeatKeyIntervalId != -1) clearInterval(repeatKeyIntervalId);
264     }
265
266
267     function getKeySymFromKeyIdentifier(shifted, keyIdentifier) {
268
269         var unicodePrefixLocation = keyIdentifier.indexOf("U+");
270         if (unicodePrefixLocation >= 0) {
271
272             var hex = keyIdentifier.substring(unicodePrefixLocation+2);
273             var codepoint = parseInt(hex, 16);
274             var typedCharacter;
275
276             // Convert case if shifted
277             if (shifted == 0)
278                 typedCharacter = String.fromCharCode(codepoint).toLowerCase();
279             else
280                 typedCharacter = String.fromCharCode(codepoint).toUpperCase();
281
282             // Get codepoint
283             codepoint = typedCharacter.charCodeAt(0);
284
285             return getKeySymFromCharCode(codepoint);
286
287         }
288
289         return keyidentifier_keysym[keyIdentifier];
290
291     }
292
293     function isControlCharacter(codepoint) {
294         return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
295     }
296
297     function getKeySymFromCharCode(codepoint) {
298
299         // Keysyms for control characters
300         if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
301
302         // Keysyms for ASCII chars
303         if (codepoint >= 0x0000 && codepoint <= 0x00FF)
304             return codepoint;
305
306         // Keysyms for Unicode
307         if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
308             return 0x01000000 | codepoint;
309
310         return null;
311
312     }
313
314     function getKeySymFromKeyCode(keyCode) {
315
316         var keysym = null;
317         if (!guac_keyboard.modifiers.shift) keysym = unshiftedKeySym[keyCode];
318         else {
319             keysym = shiftedKeySym[keyCode];
320             if (keysym == null) keysym = unshiftedKeySym[keyCode];
321         }
322
323         return keysym;
324
325     }
326
327
328     // Sends a single keystroke over the network
329     function sendKeyPressed(keysym) {
330
331         // Mark key as pressed
332         guac_keyboard.pressed[keysym] = true;
333
334         // Send key event
335         if (keysym != null && guac_keyboard.onkeydown)
336             guac_keyboard.onkeydown(keysym);
337
338     }
339
340     // Sends a single keystroke over the network
341     function sendKeyReleased(keysym) {
342
343         // Mark key as released
344         guac_keyboard.pressed[keysym] = false;
345
346         // Send key event
347         if (keysym != null && guac_keyboard.onkeyup)
348             guac_keyboard.onkeyup(keysym);
349
350     }
351
352
353     var expect_keypress = true;
354     var keydown_code = null;
355
356     var deferred_keypress = null;
357     var keydown_keysym = null;
358     var keypress_keysym = null;
359
360     function handleKeyEvents() {
361
362         // Prefer keysym from keypress
363         var keysym = keypress_keysym || keydown_keysym;
364         var keynum = keydown_code;
365
366         if (keydownChar[keynum] != keysym) {
367
368             // If this button is already pressed, release first
369             var lastKeyDownChar = keydownChar[keydown_code];
370             if (lastKeyDownChar)
371                 sendKeyReleased(lastKeyDownChar);
372
373             // Send event
374             keydownChar[keynum] = keysym;
375             sendKeyPressed(keysym);
376
377             // Clear old key repeat, if any.
378             stopRepeat();
379
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);
383
384         }
385
386         // Done with deferred key event
387         deferred_keypress = null;
388         keypress_keysym   = null;
389         keydown_keysym    = null;
390         keydown_code      = null;
391
392     }
393
394     function isTypable(keyIdentifier) {
395
396         // Find unicode prefix
397         var unicodePrefixLocation = keyIdentifier.indexOf("U+");
398         if (unicodePrefixLocation == -1)
399             return false;
400
401         // Parse codepoint value
402         var hex = keyIdentifier.substring(unicodePrefixLocation+2);
403         var codepoint = parseInt(hex, 16);
404
405         // If control character, not typable
406         if (isControlCharacter(codepoint)) return false;
407
408         // Otherwise, typable
409         return true;
410
411     }
412
413     // When key pressed
414     element.addEventListener("keydown", function(e) {
415
416         // Only intercept if handler set
417         if (!guac_keyboard.onkeydown) return;
418
419         var keynum;
420         if (window.event) keynum = window.event.keyCode;
421         else if (e.which) keynum = e.which;
422
423         // Ignore any unknown key events
424         if (keynum == 0 && !e.keyIdentifier) {
425             e.preventDefault();
426             return;
427         }
428
429         expect_keypress = true;
430
431         // Ctrl/Alt/Shift
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;
435
436         // Try to get keysym from keycode
437         keydown_keysym = getKeySymFromKeyCode(keynum);
438
439         // If key is known from keycode, prevent default
440         if (keydown_keysym)
441             expect_keypress = false;
442         
443         // Also try to get get keysym from keyIdentifier
444         if (e.keyIdentifier) {
445
446             keydown_keysym = keydown_keysym ||
447                 getKeySymFromKeyIdentifier(guac_keyboard.modifiers.shift, e.keyIdentifier);
448
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;
458             
459         }
460
461         // Set keycode which will be associated with any future keypress
462         keydown_code = keynum;
463
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);
469         }
470
471         // Otherwise, handle now
472         else {
473             e.preventDefault();
474             handleKeyEvents();
475         }
476
477     }, true);
478
479     // When key pressed
480     element.addEventListener("keypress", function(e) {
481
482         // Only intercept if handler set
483         if (!guac_keyboard.onkeydown) return;
484
485         e.preventDefault();
486
487         // Do not handle if we weren't expecting this event (will have already
488         // been handled by keydown)
489         if (!expect_keypress) return;
490
491         var keynum;
492         if (window.event) keynum = window.event.keyCode;
493         else if (e.which) keynum = e.which;
494
495         keypress_keysym = getKeySymFromCharCode(keynum);
496
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);
502         }
503
504         // Clear timeout, if any
505         if (deferred_keypress)
506             window.clearTimeout(deferred_keypress);
507
508         // Handle event with all aggregated data
509         handleKeyEvents();
510
511     }, true);
512
513     // When key released
514     element.addEventListener("keyup", function(e) {
515
516         // Only intercept if handler set
517         if (!guac_keyboard.onkeyup) return;
518
519         e.preventDefault();
520
521         var keynum;
522         if (window.event) keynum = window.event.keyCode;
523         else if (e.which) keynum = e.which;
524         
525         // Ctrl/Alt/Shift
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;
529         else
530             stopRepeat();
531
532         // Get corresponding character
533         var lastKeyDownChar = keydownChar[keynum];
534
535         // Clear character record
536         keydownChar[keynum] = null;
537
538         // Send release event
539         sendKeyReleased(lastKeyDownChar);
540
541     }, true);
542
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;
548     }, true);
549
550 };