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