Use addEventListener(), only ignore key event if truly unknown (might have keyIdentif...
[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 // Guacamole namespace
39 var Guacamole = Guacamole || {};
40
41 /**
42  * Provides cross-browser and cross-keyboard keyboard for a specific element.
43  * Browser and keyboard layout variation is abstracted away, providing events
44  * which represent keys as their corresponding X11 keysym.
45  * 
46  * @constructor
47  * @param {Element} element The Element to use to provide keyboard events.
48  */
49 Guacamole.Keyboard = function(element) {
50
51     /**
52      * Reference to this Guacamole.Keyboard.
53      * @private
54      */
55     var guac_keyboard = this;
56
57     /**
58      * Fired whenever the user presses a key with the element associated
59      * with this Guacamole.Keyboard in focus.
60      * 
61      * @event
62      * @param {Number} keysym The keysym of the key being pressed.
63      * @returns {Boolean} true if the originating event of this keypress should
64      *                    be allowed through to the browser, false or undefined
65      *                    otherwise.
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      * @returns {Boolean} true if the originating event of this key release 
76      *                    should be allowed through to the browser, false or
77      *                    undefined otherwise.
78      */
79     this.onkeyup = null;
80
81     /**
82      * Map of known JavaScript keycodes which do not map to typable characters
83      * to their unshifted X11 keysym equivalents.
84      * @private
85      */
86     var unshiftedKeySym = {
87         8:   0xFF08, // backspace
88         9:   0xFF09, // tab
89         13:  0xFF0D, // enter
90         16:  0xFFE1, // shift
91         17:  0xFFE3, // ctrl
92         18:  0xFFE9, // alt
93         19:  0xFF13, // pause/break
94         20:  0xFFE5, // caps lock
95         27:  0xFF1B, // escape
96         32:  0x0020, // space
97         33:  0xFF55, // page up
98         34:  0xFF56, // page down
99         35:  0xFF57, // end
100         36:  0xFF50, // home
101         37:  0xFF51, // left arrow
102         38:  0xFF52, // up arrow
103         39:  0xFF53, // right arrow
104         40:  0xFF54, // down arrow
105         45:  0xFF63, // insert
106         46:  0xFFFF, // delete
107         91:  0xFFEB, // left window key (super_l)
108         92:  0xFF67, // right window key (menu key?)
109         93:  null,   // select key
110         112: 0xFFBE, // f1
111         113: 0xFFBF, // f2
112         114: 0xFFC0, // f3
113         115: 0xFFC1, // f4
114         116: 0xFFC2, // f5
115         117: 0xFFC3, // f6
116         118: 0xFFC4, // f7
117         119: 0xFFC5, // f8
118         120: 0xFFC6, // f9
119         121: 0xFFC7, // f10
120         122: 0xFFC8, // f11
121         123: 0xFFC9, // f12
122         144: 0xFF7F, // num lock
123         145: 0xFF14  // scroll lock
124     };
125
126     /**
127      * Map of known JavaScript keyidentifiers which do not map to typable
128      * characters to their unshifted X11 keysym equivalents.
129      * @private
130      */
131     var keyidentifier_keysym = {
132         "AllCandidates": 0xFF3D,
133         "Alphanumeric": 0xFF30,
134         "Alt": 0xFFE9,
135         "Attn": 0xFD0E,
136         "AltGraph": 0xFFEA,
137         "CapsLock": 0xFFE5,
138         "Clear": 0xFF0B,
139         "Convert": 0xFF21,
140         "Copy": 0xFD15,
141         "Crsel": 0xFD1C,
142         "CodeInput": 0xFF37,
143         "Control": 0xFFE3,
144         "Down": 0xFF54,
145         "End": 0xFF57,
146         "Enter": 0xFF0D,
147         "EraseEof": 0xFD06,
148         "Execute": 0xFF62,
149         "Exsel": 0xFD1D,
150         "F1": 0xFFBE,
151         "F2": 0xFFBF,
152         "F3": 0xFFC0,
153         "F4": 0xFFC1,
154         "F5": 0xFFC2,
155         "F6": 0xFFC3,
156         "F7": 0xFFC4,
157         "F8": 0xFFC5,
158         "F9": 0xFFC6,
159         "F10": 0xFFC7,
160         "F11": 0xFFC8,
161         "F12": 0xFFC9,
162         "F13": 0xFFCA,
163         "F14": 0xFFCB,
164         "F15": 0xFFCC,
165         "F16": 0xFFCD,
166         "F17": 0xFFCE,
167         "F18": 0xFFCF,
168         "F19": 0xFFD0,
169         "F20": 0xFFD1,
170         "F21": 0xFFD2,
171         "F22": 0xFFD3,
172         "F23": 0xFFD4,
173         "F24": 0xFFD5,
174         "Find": 0xFF68,
175         "FullWidth": null,
176         "HalfWidth": null,
177         "HangulMode": 0xFF31,
178         "HanjaMode": 0xFF34,
179         "Help": 0xFF6A,
180         "Hiragana": 0xFF25,
181         "Home": 0xFF50,
182         "Insert": 0xFF63,
183         "JapaneseHiragana": 0xFF25,
184         "JapaneseKatakana": 0xFF26,
185         "JapaneseRomaji": 0xFF24,
186         "JunjaMode": 0xFF38,
187         "KanaMode": 0xFF2D,
188         "KanjiMode": 0xFF21,
189         "Katakana": 0xFF26,
190         "Left": 0xFF51,
191         "Meta": 0xFFE7,
192         "NumLock": 0xFF7F,
193         "PageDown": 0xFF55,
194         "PageUp": 0xFF56,
195         "Pause": 0xFF13,
196         "PreviousCandidate": 0xFF3E,
197         "PrintScreen": 0xFD1D,
198         "Right": 0xFF53,
199         "RomanCharacters": null,
200         "Scroll": 0xFF14,
201         "Select": 0xFF60,
202         "Shift": 0xFFE1,
203         "Up": 0xFF52,
204         "Undo": 0xFF65,
205         "Win": 0xFFEB
206     };
207
208     /**
209      * Map of known JavaScript keycodes which do not map to typable characters
210      * to their shifted X11 keysym equivalents. Keycodes must only be listed
211      * here if their shifted X11 keysym equivalents differ from their unshifted
212      * equivalents.
213      * @private
214      */
215     var shiftedKeySym = {
216         18:  0xFFE7  // alt
217     };
218
219     /**
220      * All modifiers and their states.
221      */
222     this.modifiers = {
223         
224         /**
225          * Whether shift is currently pressed.
226          */
227         "shift": false,
228         
229         /**
230          * Whether ctrl is currently pressed.
231          */
232         "ctrl" : false,
233         
234         /**
235          * Whether alt is currently pressed.
236          */
237         "alt"  : false
238
239     };
240
241     /**
242      * The state of every key, indexed by keysym. If a particular key is
243      * pressed, the value of pressed for that keysym will be true. If a key
244      * is not currently pressed, the value for that keysym may be false or
245      * undefined.
246      */
247     this.pressed = [];
248
249     var keydownChar = new Array();
250
251     // ID of routine repeating keystrokes. -1 = not repeating.
252     var repeatKeyTimeoutId = -1;
253     var repeatKeyIntervalId = -1;
254
255     // Starts repeating keystrokes
256     function startRepeat(keySym) {
257         repeatKeyIntervalId = setInterval(function() {
258             sendKeyReleased(keySym);
259             sendKeyPressed(keySym);
260         }, 50);
261     }
262
263     // Stops repeating keystrokes
264     function stopRepeat() {
265         if (repeatKeyTimeoutId != -1) clearTimeout(repeatKeyTimeoutId);
266         if (repeatKeyIntervalId != -1) clearInterval(repeatKeyIntervalId);
267     }
268
269
270     function getKeySymFromKeyIdentifier(shifted, keyIdentifier) {
271
272         var unicodePrefixLocation = keyIdentifier.indexOf("U+");
273         if (unicodePrefixLocation >= 0) {
274
275             var hex = keyIdentifier.substring(unicodePrefixLocation+2);
276             var codepoint = parseInt(hex, 16);
277             var typedCharacter;
278
279             // Convert case if shifted
280             if (shifted == 0)
281                 typedCharacter = String.fromCharCode(codepoint).toLowerCase();
282             else
283                 typedCharacter = String.fromCharCode(codepoint).toUpperCase();
284
285             // Get codepoint
286             codepoint = typedCharacter.charCodeAt(0);
287
288             return getKeySymFromCharCode(codepoint);
289
290         }
291
292         return keyidentifier_keysym[keyIdentifier];
293
294     }
295
296     function isControlCharacter(codepoint) {
297         return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F);
298     }
299
300     function getKeySymFromCharCode(codepoint) {
301
302         // Keysyms for control characters
303         if (isControlCharacter(codepoint)) return 0xFF00 | codepoint;
304
305         // Keysyms for ASCII chars
306         if (codepoint >= 0x0000 && codepoint <= 0x00FF)
307             return codepoint;
308
309         // Keysyms for Unicode
310         if (codepoint >= 0x0100 && codepoint <= 0x10FFFF)
311             return 0x01000000 | codepoint;
312
313         return null;
314
315     }
316
317     function getKeySymFromKeyCode(keyCode) {
318
319         var keysym = null;
320         if (!guac_keyboard.modifiers.shift) keysym = unshiftedKeySym[keyCode];
321         else {
322             keysym = shiftedKeySym[keyCode];
323             if (keysym == null) keysym = unshiftedKeySym[keyCode];
324         }
325
326         return keysym;
327
328     }
329
330
331     // Sends a single keystroke over the network
332     function sendKeyPressed(keysym) {
333
334         // Mark key as pressed
335         guac_keyboard.pressed[keysym] = true;
336
337         // Send key event
338         if (keysym != null && guac_keyboard.onkeydown)
339             return guac_keyboard.onkeydown(keysym) != false;
340         
341         return true;
342
343     }
344
345     // Sends a single keystroke over the network
346     function sendKeyReleased(keysym) {
347
348         // Mark key as released
349         guac_keyboard.pressed[keysym] = false;
350
351         // Send key event
352         if (keysym != null && guac_keyboard.onkeyup)
353             return guac_keyboard.onkeyup(keysym) != false;
354
355         return true;
356
357     }
358
359
360     var expect_keypress = true;
361     var keydown_code = null;
362
363     var deferred_keypress = null;
364     var keydown_keysym = null;
365     var keypress_keysym = null;
366
367     function handleKeyEvents() {
368
369         // Prefer keysym from keypress
370         var keysym = keypress_keysym || keydown_keysym;
371         var keynum = keydown_code;
372
373         if (keydownChar[keynum] != keysym) {
374
375             // If this button is already pressed, release first
376             var lastKeyDownChar = keydownChar[keydown_code];
377             if (lastKeyDownChar)
378                 sendKeyReleased(lastKeyDownChar);
379
380             // Send event
381             keydownChar[keynum] = keysym;
382             sendKeyPressed(keysym);
383
384             // Clear old key repeat, if any.
385             stopRepeat();
386
387             // Start repeating (if not a modifier key) after a short delay
388             if (keynum != 16 && keynum != 17 && keynum != 18)
389                 repeatKeyTimeoutId = setTimeout(function() { startRepeat(keysym); }, 500);
390
391         }
392
393         // Done with deferred key event
394         deferred_keypress = null;
395         keypress_keysym   = null;
396         keydown_keysym    = null;
397         keydown_code      = null;
398
399     }
400
401     function isTypable(keyIdentifier) {
402
403         // Find unicode prefix
404         var unicodePrefixLocation = keyIdentifier.indexOf("U+");
405         if (unicodePrefixLocation == -1)
406             return false;
407
408         // Parse codepoint value
409         var hex = keyIdentifier.substring(unicodePrefixLocation+2);
410         var codepoint = parseInt(hex, 16);
411
412         // If control character, not typable
413         if (isControlCharacter(codepoint)) return false;
414
415         // Otherwise, typable
416         return true;
417
418     }
419
420     // When key pressed
421     element.addEventListener("keydown", function(e) {
422
423         // Only intercept if handler set
424         if (!guac_keyboard.onkeydown) return;
425
426         var keynum;
427         if (window.event) keynum = window.event.keyCode;
428         else if (e.which) keynum = e.which;
429
430         // Ignore any unknown key events
431         if (keynum == 0 && !e.keyIdentifier) {
432             e.preventDefault();
433             return;
434         }
435
436         expect_keypress = true;
437
438         // Ctrl/Alt/Shift
439         if (keynum == 16)      guac_keyboard.modifiers.shift = true;
440         else if (keynum == 17) guac_keyboard.modifiers.ctrl  = true;
441         else if (keynum == 18) guac_keyboard.modifiers.alt   = true;
442
443         // Try to get keysym from keycode
444         keydown_keysym = getKeySymFromKeyCode(keynum);
445
446         // If key is known from keycode, prevent default
447         if (keydown_keysym)
448             expect_keypress = false;
449         
450         // Also try to get get keysym from keyIdentifier
451         if (e.keyIdentifier) {
452
453             keydown_keysym = keydown_keysym ||
454                 getKeySymFromKeyIdentifier(guac_keyboard.modifiers.shift, e.keyIdentifier);
455
456             // Prevent default if non-typable character or if modifier combination
457             // likely to be eaten by browser otherwise (NOTE: We must not prevent
458             // default for Ctrl+Alt, as that combination is commonly used for
459             // AltGr. If we receive AltGr, we need to handle keypress, which
460             // means we cannot cancel keydown).
461             if (!isTypable(e.keyIdentifier)
462                     || ( guac_keyboard.modifiers.ctrl && !guac_keyboard.modifiers.alt)
463                     || (!guac_keyboard.modifiers.ctrl &&  guac_keyboard.modifiers.alt))
464                 expect_keypress = false;
465             
466         }
467
468         // Set keycode which will be associated with any future keypress
469         keydown_code = keynum;
470
471         // If we expect to handle via keypress, set failsafe timeout and
472         // wait for keypress.
473         if (expect_keypress) {
474             if (!deferred_keypress)
475                 deferred_keypress = window.setTimeout(handleKeyEvents, 0);
476         }
477
478         // Otherwise, handle now
479         else {
480             e.preventDefault();
481             handleKeyEvents();
482         }
483
484     }, true);
485
486     // When key pressed
487     element.addEventListener("keypress", function(e) {
488
489         // Only intercept if handler set
490         if (!guac_keyboard.onkeydown) return;
491
492         e.preventDefault();
493
494         // Do not handle if we weren't expecting this event (will have already
495         // been handled by keydown)
496         if (!expect_keypress) return;
497
498         var keynum;
499         if (window.event) keynum = window.event.keyCode;
500         else if (e.which) keynum = e.which;
501
502         keypress_keysym = getKeySymFromCharCode(keynum);
503
504         // If event identified as a typable character, and we're holding Ctrl+Alt,
505         // assume Ctrl+Alt is actually AltGr, and release both.
506         if (!isControlCharacter(keynum) && guac_keyboard.modifiers.ctrl && guac_keyboard.modifiers.alt) {
507             sendKeyReleased(0xFFE3);
508             sendKeyReleased(0xFFE9);
509         }
510
511         // Clear timeout, if any
512         if (deferred_keypress)
513             window.clearTimeout(deferred_keypress);
514
515         // Handle event with all aggregated data
516         handleKeyEvents();
517
518     }, true);
519
520     // When key released
521     element.addEventListener("keyup", function(e) {
522
523         // Only intercept if handler set
524         if (!guac_keyboard.onkeyup) return;
525
526         e.preventDefault();
527
528         var keynum;
529         if (window.event) keynum = window.event.keyCode;
530         else if (e.which) keynum = e.which;
531         
532         // Ctrl/Alt/Shift
533         if (keynum == 16)      guac_keyboard.modifiers.shift = false;
534         else if (keynum == 17) guac_keyboard.modifiers.ctrl  = false;
535         else if (keynum == 18) guac_keyboard.modifiers.alt   = false;
536         else
537             stopRepeat();
538
539         // Get corresponding character
540         var lastKeyDownChar = keydownChar[keynum];
541
542         // Clear character record
543         keydownChar[keynum] = null;
544
545         // Send release event
546         sendKeyReleased(lastKeyDownChar);
547
548     }, true);
549
550     // When focus is lost, clear modifiers.
551     element.addEventListener("blur", function() {
552         guac_keyboard.modifiers.alt = false;
553         guac_keyboard.modifiers.ctrl = false;
554         guac_keyboard.modifiers.shift = false;
555     }, true);
556
557 };