-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathKeyboard.js
1527 lines (1276 loc) · 47.8 KB
/
Keyboard.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
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|Document} [element]
* The Element to use to provide keyboard events. If omitted, at least one
* Element must be manually provided through the listenTo() function for
* the Guacamole.Keyboard instance to have any effect.
*/
Guacamole.Keyboard = function Keyboard(element) {
/**
* Reference to this Guacamole.Keyboard.
*
* @private
* @type {!Guacamole.Keyboard}
*/
var guac_keyboard = this;
/**
* An integer value which uniquely identifies this Guacamole.Keyboard
* instance with respect to other Guacamole.Keyboard instances.
*
* @private
* @type {!number}
*/
var guacKeyboardID = Guacamole.Keyboard._nextID++;
/**
* The name of the property which is added to event objects via markEvent()
* to note that they have already been handled by this Guacamole.Keyboard.
*
* @private
* @constant
* @type {!string}
*/
var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID;
/**
* 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.
*
* @return {!boolean}
* true if the key event should be allowed through to the browser,
* false otherwise.
*/
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;
/**
* Set of known platform-specific or browser-specific quirks which must be
* accounted for to properly interpret key events, even if the only way to
* reliably detect that quirk is to platform/browser-sniff.
*
* @private
* @type {!Object.<string, boolean>}
*/
var quirks = {
/**
* Whether keyup events are universally unreliable.
*
* @type {!boolean}
*/
keyupUnreliable: false,
/**
* Whether the Alt key is actually a modifier for typable keys and is
* thus never used for keyboard shortcuts.
*
* @type {!boolean}
*/
altIsTypableOnly: false,
/**
* Whether we can rely on receiving a keyup event for the Caps Lock
* key.
*
* @type {!boolean}
*/
capsLockKeyupUnreliable: false
};
// Set quirk flags depending on platform/browser, if such information is
// available
if (navigator && navigator.platform) {
// All keyup events are unreliable on iOS (sadly)
if (navigator.platform.match(/ipad|iphone|ipod/i))
quirks.keyupUnreliable = true;
// The Alt key on Mac is never used for keyboard shortcuts, and the
// Caps Lock key never dispatches keyup events
else if (navigator.platform.match(/^mac/i)) {
quirks.altIsTypableOnly = true;
quirks.capsLockKeyupUnreliable = true;
}
}
/**
* A key event having a corresponding timestamp. This event is non-specific.
* Its subclasses should be used instead when recording specific key
* events.
*
* @private
* @constructor
* @param {KeyboardEvent} [orig]
* The relevant DOM keyboard event.
*/
var KeyEvent = function KeyEvent(orig) {
/**
* Reference to this key event.
*
* @private
* @type {!KeyEvent}
*/
var key_event = this;
/**
* The JavaScript key code of the key pressed. For most events (keydown
* and keyup), this is a scancode-like value related to the position of
* the key on the US English "Qwerty" keyboard. For keypress events,
* this is the Unicode codepoint of the character that would be typed
* by the key pressed.
*
* @type {!number}
*/
this.keyCode = orig ? (orig.which || orig.keyCode) : 0;
/**
* The legacy DOM3 "keyIdentifier" of the key pressed, as defined at:
* http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent
*
* @type {!string}
*/
this.keyIdentifier = orig && orig.keyIdentifier;
/**
* The standard name of the key pressed, as defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {!string}
*/
this.key = orig && orig.key;
/**
* The location on the keyboard corresponding to the key pressed, as
* defined at:
* http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent
*
* @type {!number}
*/
this.location = orig ? getEventLocation(orig) : 0;
/**
* The state of all local keyboard modifiers at the time this event was
* received.
*
* @type {!Guacamole.Keyboard.ModifierState}
*/
this.modifiers = orig ? Guacamole.Keyboard.ModifierState.fromKeyboardEvent(orig) : new Guacamole.Keyboard.ModifierState();
/**
* An arbitrary timestamp in milliseconds, indicating this event's
* position in time relative to other events.
*
* @type {!number}
*/
this.timestamp = new Date().getTime();
/**
* Whether the default action of this key event should be prevented.
*
* @type {!boolean}
*/
this.defaultPrevented = false;
/**
* The keysym of the key associated with this key event, as determined
* by a best-effort guess using available event properties and keyboard
* state.
*
* @type {number}
*/
this.keysym = null;
/**
* Whether the keysym value of this key event is known to be reliable.
* If false, the keysym may still be valid, but it's only a best guess,
* and future key events may be a better source of information.
*
* @type {!boolean}
*/
this.reliable = false;
/**
* Returns the number of milliseconds elapsed since this event was
* received.
*
* @return {!number}
* The number of milliseconds elapsed since this event was
* received.
*/
this.getAge = function() {
return new Date().getTime() - key_event.timestamp;
};
};
/**
* Information related to the pressing of a key, which need not be a key
* associated with a printable character. The presence or absence of any
* information within this object is browser-dependent.
*
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {!KeyboardEvent} orig
* The relevant DOM "keydown" event.
*/
var KeydownEvent = function KeydownEvent(orig) {
// We extend KeyEvent
KeyEvent.call(this, orig);
// If key is known from keyCode or DOM3 alone, use that
this.keysym = keysym_from_key_identifier(this.key, this.location)
|| keysym_from_keycode(this.keyCode, this.location);
/**
* Whether the keyup following this keydown event is known to be
* reliable. If false, we cannot rely on the keyup event to occur.
*
* @type {!boolean}
*/
this.keyupReliable = !quirks.keyupUnreliable;
// DOM3 and keyCode are reliable sources if the corresponding key is
// not a printable key
if (this.keysym && !isPrintable(this.keysym))
this.reliable = true;
// Use legacy keyIdentifier as a last resort, if it looks sane
if (!this.keysym && key_identifier_sane(this.keyCode, this.keyIdentifier))
this.keysym = keysym_from_key_identifier(this.keyIdentifier, this.location, this.modifiers.shift);
// If a key is pressed while meta is held down, the keyup will
// never be sent in Chrome (bug #108404)
if (this.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8)
this.keyupReliable = false;
// We cannot rely on receiving keyup for Caps Lock on certain platforms
else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable)
this.keyupReliable = false;
// Determine whether default action for Alt+combinations must be prevented
var prevent_alt = !this.modifiers.ctrl && !quirks.altIsTypableOnly;
// If alt is typeable only, and this is actually an alt key event, treat as AltGr instead
if (quirks.altIsTypableOnly && (this.keysym === 0xFFE9 || this.keysym === 0xFFEA))
this.keysym = 0xFE03;
// Determine whether default action for Ctrl+combinations must be prevented
var prevent_ctrl = !this.modifiers.alt;
// We must rely on the (potentially buggy) keyIdentifier if preventing
// the default action is important
if ((prevent_ctrl && this.modifiers.ctrl)
|| (prevent_alt && this.modifiers.alt)
|| this.modifiers.meta
|| this.modifiers.hyper)
this.reliable = true;
// Record most recently known keysym by associated key code
recentKeysym[this.keyCode] = this.keysym;
};
KeydownEvent.prototype = new KeyEvent();
/**
* Information related to the pressing of a key, which MUST be
* associated with a printable character. The presence or absence of any
* information within this object is browser-dependent.
*
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {!KeyboardEvent} orig
* The relevant DOM "keypress" event.
*/
var KeypressEvent = function KeypressEvent(orig) {
// We extend KeyEvent
KeyEvent.call(this, orig);
// Pull keysym from char code
this.keysym = keysym_from_charcode(this.keyCode);
// Keypress is always reliable
this.reliable = true;
};
KeypressEvent.prototype = new KeyEvent();
/**
* Information related to the releasing of a key, which need not be a key
* associated with a printable character. The presence or absence of any
* information within this object is browser-dependent.
*
* @private
* @constructor
* @augments Guacamole.Keyboard.KeyEvent
* @param {!KeyboardEvent} orig
* The relevant DOM "keyup" event.
*/
var KeyupEvent = function KeyupEvent(orig) {
// We extend KeyEvent
KeyEvent.call(this, orig);
// If key is known from keyCode or DOM3 alone, use that (keyCode is
// still more reliable for keyup when dead keys are in use)
this.keysym = keysym_from_keycode(this.keyCode, this.location)
|| keysym_from_key_identifier(this.key, this.location);
// Fall back to the most recently pressed keysym associated with the
// keyCode if the inferred key doesn't seem to actually be pressed
if (!guac_keyboard.pressed[this.keysym])
this.keysym = recentKeysym[this.keyCode] || this.keysym;
// Keyup is as reliable as it will ever be
this.reliable = true;
};
KeyupEvent.prototype = new KeyEvent();
/**
* An array of recorded events, which can be instances of the private
* KeydownEvent, KeypressEvent, and KeyupEvent classes.
*
* @private
* @type {!KeyEvent[]}
*/
var eventLog = [];
/**
* Map of known JavaScript keycodes which do not map to typable characters
* to their X11 keysym equivalents.
*
* @private
* @type {!Object.<number, number[]>}
*/
var keycodeKeysyms = {
8: [0xFF08], // backspace
9: [0xFF09], // tab
12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5
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, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9
34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3
35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1
36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7
37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4
38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8
39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6
40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2
45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0
46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal
91: [0xFFE7], // left windows/command key (meta_l)
92: [0xFFE8], // right window/command key (meta_r)
93: [0xFF67], // menu key
96: [0xFFB0], // KP 0
97: [0xFFB1], // KP 1
98: [0xFFB2], // KP 2
99: [0xFFB3], // KP 3
100: [0xFFB4], // KP 4
101: [0xFFB5], // KP 5
102: [0xFFB6], // KP 6
103: [0xFFB7], // KP 7
104: [0xFFB8], // KP 8
105: [0xFFB9], // KP 9
106: [0xFFAA], // KP multiply
107: [0xFFAB], // KP add
109: [0xFFAD], // KP subtract
110: [0xFFAE], // KP decimal
111: [0xFFAF], // KP divide
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
225: [0xFE03] // altgraph (iso_level3_shift)
};
/**
* Map of known JavaScript keyidentifiers which do not map to typable
* characters to their unshifted X11 keysym equivalents.
*
* @private
* @type {!Object.<string, number[]>}
*/
var keyidentifier_keysym = {
"Again": [0xFF66],
"AllCandidates": [0xFF3D],
"Alphanumeric": [0xFF30],
"Alt": [0xFFE9, 0xFFE9, 0xFFEA],
"Attn": [0xFD0E],
"AltGraph": [0xFE03],
"ArrowDown": [0xFF54],
"ArrowLeft": [0xFF51],
"ArrowRight": [0xFF53],
"ArrowUp": [0xFF52],
"Backspace": [0xFF08],
"CapsLock": [0xFFE5],
"Cancel": [0xFF69],
"Clear": [0xFF0B],
"Convert": [0xFF23],
"Copy": [0xFD15],
"Crsel": [0xFD1C],
"CrSel": [0xFD1C],
"CodeInput": [0xFF37],
"Compose": [0xFF20],
"Control": [0xFFE3, 0xFFE3, 0xFFE4],
"ContextMenu": [0xFF67],
"Delete": [0xFFFF],
"Down": [0xFF54],
"End": [0xFF57],
"Enter": [0xFF0D],
"EraseEof": [0xFD06],
"Escape": [0xFF1B],
"Execute": [0xFF62],
"Exsel": [0xFD1D],
"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],
"GroupFirst": [0xFE0C],
"GroupLast": [0xFE0E],
"GroupNext": [0xFE08],
"GroupPrevious": [0xFE0A],
"FullWidth": null,
"HalfWidth": null,
"HangulMode": [0xFF31],
"Hankaku": [0xFF29],
"HanjaMode": [0xFF34],
"Help": [0xFF6A],
"Hiragana": [0xFF25],
"HiraganaKatakana": [0xFF27],
"Home": [0xFF50],
"Hyper": [0xFFED, 0xFFED, 0xFFEE],
"Insert": [0xFF63],
"JapaneseHiragana": [0xFF25],
"JapaneseKatakana": [0xFF26],
"JapaneseRomaji": [0xFF24],
"JunjaMode": [0xFF38],
"KanaMode": [0xFF2D],
"KanjiMode": [0xFF21],
"Katakana": [0xFF26],
"Left": [0xFF51],
"Meta": [0xFFE7, 0xFFE7, 0xFFE8],
"ModeChange": [0xFF7E],
"NonConvert": [0xFF22],
"NumLock": [0xFF7F],
"PageDown": [0xFF56],
"PageUp": [0xFF55],
"Pause": [0xFF13],
"Play": [0xFD16],
"PreviousCandidate": [0xFF3E],
"PrintScreen": [0xFF61],
"Redo": [0xFF66],
"Right": [0xFF53],
"Romaji": [0xFF24],
"RomanCharacters": null,
"Scroll": [0xFF14],
"Select": [0xFF60],
"Separator": [0xFFAC],
"Shift": [0xFFE1, 0xFFE1, 0xFFE2],
"SingleCandidate": [0xFF3C],
"Super": [0xFFEB, 0xFFEB, 0xFFEC],
"Tab": [0xFF09],
"UIKeyInputDownArrow": [0xFF54],
"UIKeyInputEscape": [0xFF1B],
"UIKeyInputLeftArrow": [0xFF51],
"UIKeyInputRightArrow": [0xFF53],
"UIKeyInputUpArrow": [0xFF52],
"Up": [0xFF52],
"Undo": [0xFF65],
"Win": [0xFFE7, 0xFFE7, 0xFFE8],
"Zenkaku": [0xFF28],
"ZenkakuHankaku": [0xFF2A]
};
/**
* All keysyms which should not repeat when held down.
*
* @private
* @type {!Object.<number, boolean>}
*/
var no_repeat = {
0xFE03: true, // ISO Level 3 Shift (AltGr)
0xFFE1: true, // Left shift
0xFFE2: true, // Right shift
0xFFE3: true, // Left ctrl
0xFFE4: true, // Right ctrl
0xFFE5: true, // Caps Lock
0xFFE7: true, // Left meta
0xFFE8: true, // Right meta
0xFFE9: true, // Left alt
0xFFEA: true, // Right alt
0xFFEB: true, // Left super/hyper
0xFFEC: true // Right super/hyper
};
/**
* All modifiers and their states.
*
* @type {!Guacamole.Keyboard.ModifierState}
*/
this.modifiers = new Guacamole.Keyboard.ModifierState();
/**
* 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.
*
* @type {!Object.<number, boolean>}
*/
this.pressed = {};
/**
* The state of every key, indexed by keysym, for strictly those keys whose
* status has been indirectly determined thorugh observation of other key
* events. If a particular key is implicitly pressed, the value of
* implicitlyPressed for that keysym will be true. If a key
* is not currently implicitly pressed (the key is not pressed OR the state
* of the key is explicitly known), it will not be defined.
*
* @private
* @type {!Object.<number, boolean>}
*/
var implicitlyPressed = {};
/**
* The last result of calling the onkeydown handler for each key, indexed
* by keysym. This is used to prevent/allow default actions for key events,
* even when the onkeydown handler cannot be called again because the key
* is (theoretically) still pressed.
*
* @private
* @type {!Object.<number, boolean>}
*/
var last_keydown_result = {};
/**
* The keysym most recently associated with a given keycode when keydown
* fired. This object maps keycodes to keysyms.
*
* @private
* @type {!Object.<number, number>}
*/
var recentKeysym = {};
/**
* Timeout before key repeat starts.
*
* @private
* @type {number}
*/
var key_repeat_timeout = null;
/**
* Interval which presses and releases the last key pressed while that
* key is still being held down.
*
* @private
* @type {number}
*/
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.
*
* @private
* @param {number[]} 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
*/
var get_keysym = function get_keysym(keysyms, location) {
if (!keysyms)
return null;
return keysyms[location] || keysyms[0];
};
/**
* Returns true if the given keysym corresponds to a printable character,
* false otherwise.
*
* @param {!number} keysym
* The keysym to check.
*
* @returns {!boolean}
* true if the given keysym corresponds to a printable character,
* false otherwise.
*/
var isPrintable = function isPrintable(keysym) {
// Keysyms with Unicode equivalents are printable
return (keysym >= 0x00 && keysym <= 0xFF)
|| (keysym & 0xFFFF0000) === 0x01000000;
};
function keysym_from_key_identifier(identifier, location, shifted) {
if (!identifier)
return null;
var typedCharacter;
// If identifier is U+xxxx, decode Unicode character
var unicodePrefixLocation = identifier.indexOf("U+");
if (unicodePrefixLocation >= 0) {
var hex = identifier.substring(unicodePrefixLocation+2);
typedCharacter = String.fromCharCode(parseInt(hex, 16));
}
// If single character and not keypad, use that as typed character
else if (identifier.length === 1 && location !== 3)
typedCharacter = identifier;
// Otherwise, look up corresponding keysym
else
return get_keysym(keyidentifier_keysym[identifier], location);
// Alter case if necessary
if (shifted === true)
typedCharacter = typedCharacter.toUpperCase();
else if (shifted === false)
typedCharacter = typedCharacter.toLowerCase();
// Get codepoint
var codepoint = typedCharacter.charCodeAt(0);
return keysym_from_charcode(codepoint);
}
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) {
return get_keysym(keycodeKeysyms[keyCode], location);
}
/**
* Heuristically detects if the legacy keyIdentifier property of
* a keydown/keyup event looks incorrectly derived. Chrome, and
* presumably others, will produce the keyIdentifier by assuming
* the keyCode is the Unicode codepoint for that key. This is not
* correct in all cases.
*
* @private
* @param {!number} keyCode
* The keyCode from a browser keydown/keyup event.
*
* @param {string} keyIdentifier
* The legacy keyIdentifier from a browser keydown/keyup event.
*
* @returns {!boolean}
* true if the keyIdentifier looks sane, false if the keyIdentifier
* appears incorrectly derived or is missing entirely.
*/
var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) {
// Missing identifier is not sane
if (!keyIdentifier)
return false;
// Assume non-Unicode keyIdentifier values are sane
var unicodePrefixLocation = keyIdentifier.indexOf("U+");
if (unicodePrefixLocation === -1)
return true;
// If the Unicode codepoint isn't identical to the keyCode,
// then the identifier is likely correct
var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16);
if (keyCode !== codepoint)
return true;
// The keyCodes for A-Z and 0-9 are actually identical to their
// Unicode codepoints
if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57))
return true;
// The keyIdentifier does NOT appear sane
return false;
};
/**
* 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. The return value of this function depends on the
* return value of the keydown event handler, if any.
*
* @param {number} keysym
* The keysym of the key to press.
*
* @return {boolean}
* true if event should NOT be canceled, false otherwise.
*/
this.press = function(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) {
var result = guac_keyboard.onkeydown(keysym);
last_keydown_result[keysym] = result;
// 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);
return result;
}
}
// Return the last keydown result by default, resort to false if unknown
return last_keydown_result[keysym] || false;
};
/**
* Marks a key as released, firing the keyup event if registered.
*
* @param {number} keysym
* The keysym of the key to release.
*/
this.release = function(keysym) {
// Only release if pressed
if (guac_keyboard.pressed[keysym]) {
// Mark key as released
delete guac_keyboard.pressed[keysym];
delete implicitlyPressed[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);
}
};
/**
* Presses and releases the keys necessary to type the given string of
* text.
*
* @param {!string} str
* The string to type.
*/
this.type = function type(str) {
// Press/release the key corresponding to each character in the string
for (var i = 0; i < str.length; i++) {
// Determine keysym of current character
var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i);
var keysym = keysym_from_charcode(codepoint);
// Press and release key for current character
guac_keyboard.press(keysym);
guac_keyboard.release(keysym);
}
};
/**
* Resets the state of this keyboard, releasing all keys, and firing keyup
* events for each released key.
*/
this.reset = function() {
// Release all pressed keys
for (var keysym in guac_keyboard.pressed)
guac_keyboard.release(parseInt(keysym));
// Clear event log
eventLog = [];
};
/**
* Resynchronizes the remote state of the given modifier with its
* corresponding local modifier state, as dictated by
* {@link KeyEvent#modifiers} within the given key event, by pressing or
* releasing keysyms.
*
* @private
* @param {!string} modifier
* The name of the {@link Guacamole.Keyboard.ModifierState} property
* being updated.
*
* @param {!number[]} keysyms
* The keysyms which represent the modifier being updated.
*
* @param {!KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being
* processed.
*/
var updateModifierState = function updateModifierState(modifier,
keysyms, keyEvent) {
var localState = keyEvent.modifiers[modifier];
var remoteState = guac_keyboard.modifiers[modifier];
var i;
// Do not trust changes in modifier state for events directly involving
// that modifier: (1) the flag may erroneously be cleared despite
// another version of the same key still being held and (2) the change
// in flag may be due to the current event being processed, thus
// updating things here is at best redundant and at worst incorrect
if (keysyms.indexOf(keyEvent.keysym) !== -1)
return;
// Release all related keys if modifier is implicitly released
if (remoteState && localState === false) {
for (i = 0; i < keysyms.length; i++) {
guac_keyboard.release(keysyms[i]);
}
}
// Press if modifier is implicitly pressed
else if (!remoteState && localState) {
// Verify that modifier flag isn't already pressed or already set
// due to another version of the same key being held down
for (i = 0; i < keysyms.length; i++) {
if (guac_keyboard.pressed[keysyms[i]])
return;
}
// Mark as implicitly pressed only if there is other information
// within the key event relating to a different key. Some
// platforms, such as iOS, will send essentially empty key events
// for modifier keys, using only the modifier flags to signal the
// identity of the key.
var keysym = keysyms[0];
if (keyEvent.keysym)
implicitlyPressed[keysym] = true;
guac_keyboard.press(keysym);
}
};
/**
* Given a keyboard event, updates the remote key state to match the local
* modifier state and remote based on the modifier flags within the event.
* This function pays no attention to keycodes.
*
* @private
* @param {!KeyEvent} keyEvent
* Guacamole's current best interpretation of the key event being
* processed.
*/
var syncModifierStates = function syncModifierStates(keyEvent) {
// Resync state of alt
updateModifierState('alt', [
0xFFE9, // Left alt