]>
git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
2 * import KeyTable from "./keysym";
3 * import keysyms from "./keysymdef";
11 function substituteCodepoint(cp
) {
12 // Any Unicode code points which do not have corresponding keysym entries
13 // can be swapped out for another code point by adding them to this table
15 // {S,s} with comma below -> {S,s} with cedilla
18 // {T,t} with comma below -> {T,t} with cedilla
23 var sub
= substitutions
[cp
];
24 return sub
? sub
: cp
;
28 return navigator
&& !!(/mac/i).exec(navigator
.platform
);
30 function isWindows() {
31 return navigator
&& !!(/win/i).exec(navigator
.platform
);
34 return navigator
&& !!(/linux/i).exec(navigator
.platform
);
37 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
38 function hasShortcutModifier(charModifier
, currentModifiers
) {
40 for (var key
in currentModifiers
) {
41 if (parseInt(key
) !== KeyTable
.XK_Shift_L
) {
42 mods
[key
] = currentModifiers
[key
];
47 for (var k
in currentModifiers
) {
52 if (hasCharModifier(charModifier
, mods
)) {
53 return sum
> charModifier
.length
;
60 // Return true if the specified char modifier is currently down
61 function hasCharModifier(charModifier
, currentModifiers
) {
62 if (charModifier
.length
=== 0) { return false; }
64 for (var i
= 0; i
< charModifier
.length
; ++i
) {
65 if (!currentModifiers
[charModifier
[i
]]) {
72 // Helper object tracking modifier key state
73 // and generates fake key events to compensate if it gets out of sync
74 function ModifierSync(charModifier
) {
77 // on Mac, Option (AKA Alt) is used as a char modifier
78 charModifier
= [KeyTable
.XK_Alt_L
];
80 else if (isWindows()) {
81 // on Windows, Ctrl+Alt is used as a char modifier
82 charModifier
= [KeyTable
.XK_Alt_L
, KeyTable
.XK_Control_L
];
85 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
86 charModifier
= [KeyTable
.XK_ISO_Level3_Shift
];
94 state
[KeyTable
.XK_Control_L
] = false;
95 state
[KeyTable
.XK_Alt_L
] = false;
96 state
[KeyTable
.XK_ISO_Level3_Shift
] = false;
97 state
[KeyTable
.XK_Shift_L
] = false;
98 state
[KeyTable
.XK_Meta_L
] = false;
100 function sync(evt
, keysym
) {
102 function syncKey(keysym
) {
103 return {keysym
: keysyms
.lookup(keysym
), type
: state
[keysym
] ? 'keydown' : 'keyup'};
106 if (evt
.ctrlKey
!== undefined &&
107 evt
.ctrlKey
!== state
[KeyTable
.XK_Control_L
] && keysym
!== KeyTable
.XK_Control_L
) {
108 state
[KeyTable
.XK_Control_L
] = evt
.ctrlKey
;
109 result
.push(syncKey(KeyTable
.XK_Control_L
));
111 if (evt
.altKey
!== undefined &&
112 evt
.altKey
!== state
[KeyTable
.XK_Alt_L
] && keysym
!== KeyTable
.XK_Alt_L
) {
113 state
[KeyTable
.XK_Alt_L
] = evt
.altKey
;
114 result
.push(syncKey(KeyTable
.XK_Alt_L
));
116 if (evt
.altGraphKey
!== undefined &&
117 evt
.altGraphKey
!== state
[KeyTable
.XK_ISO_Level3_Shift
] && keysym
!== KeyTable
.XK_ISO_Level3_Shift
) {
118 state
[KeyTable
.XK_ISO_Level3_Shift
] = evt
.altGraphKey
;
119 result
.push(syncKey(KeyTable
.XK_ISO_Level3_Shift
));
121 if (evt
.shiftKey
!== undefined &&
122 evt
.shiftKey
!== state
[KeyTable
.XK_Shift_L
] && keysym
!== KeyTable
.XK_Shift_L
) {
123 state
[KeyTable
.XK_Shift_L
] = evt
.shiftKey
;
124 result
.push(syncKey(KeyTable
.XK_Shift_L
));
126 if (evt
.metaKey
!== undefined &&
127 evt
.metaKey
!== state
[KeyTable
.XK_Meta_L
] && keysym
!== KeyTable
.XK_Meta_L
) {
128 state
[KeyTable
.XK_Meta_L
] = evt
.metaKey
;
129 result
.push(syncKey(KeyTable
.XK_Meta_L
));
133 function syncKeyEvent(evt
, down
) {
134 var obj
= getKeysym(evt
);
135 var keysym
= obj
? obj
.keysym
: null;
137 // first, apply the event itself, if relevant
138 if (keysym
!== null && state
[keysym
] !== undefined) {
139 state
[keysym
] = down
;
141 return sync(evt
, keysym
);
145 // sync on the appropriate keyboard event
146 keydown: function(evt
) { return syncKeyEvent(evt
, true);},
147 keyup: function(evt
) { return syncKeyEvent(evt
, false);},
148 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
149 syncAny: function(evt
) { return sync(evt
);},
151 // is a shortcut modifier down?
152 hasShortcutModifier: function() { return hasShortcutModifier(charModifier
, state
); },
153 // if a char modifier is down, return the keys it consists of, otherwise return null
154 activeCharModifier: function() { return hasCharModifier(charModifier
, state
) ? charModifier
: null; }
158 // Get a key ID from a keyboard event
159 // May be a string or an integer depending on the available properties
160 function getKey(evt
){
161 if ('keyCode' in evt
&& 'key' in evt
) {
162 return evt
.key
+ ':' + evt
.keyCode
;
164 else if ('keyCode' in evt
) {
172 // Get the most reliable keysym value we can get from a key event
173 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
174 function getKeysym(evt
){
176 if (evt
.char && evt
.char.length
=== 1) {
177 codepoint
= evt
.char.charCodeAt();
179 else if (evt
.charCode
) {
180 codepoint
= evt
.charCode
;
182 else if (evt
.keyCode
&& evt
.type
=== 'keypress') {
183 // IE10 stores the char code as keyCode, and has no other useful properties
184 codepoint
= evt
.keyCode
;
187 var res
= keysyms
.fromUnicode(substituteCodepoint(codepoint
));
192 // we could check evt.key here.
193 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
194 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
195 // so we don't *need* it yet
197 return keysyms
.lookup(keysymFromKeyCode(evt
.keyCode
, evt
.shiftKey
));
200 return keysyms
.lookup(keysymFromKeyCode(evt
.which
, evt
.shiftKey
));
205 // Given a keycode, try to predict which keysym it might be.
206 // If the keycode is unknown, null is returned.
207 function keysymFromKeyCode(keycode
, shiftPressed
) {
208 if (typeof(keycode
) !== 'number') {
211 // won't be accurate for azerty
212 if (keycode
>= 0x30 && keycode
<= 0x39) {
213 return keycode
; // digit
215 if (keycode
>= 0x41 && keycode
<= 0x5a) {
216 // remap to lowercase unless shift is down
217 return shiftPressed
? keycode
: keycode
+ 32; // A-Z
219 if (keycode
>= 0x60 && keycode
<= 0x69) {
220 return KeyTable
.XK_KP_0
+ (keycode
- 0x60); // numpad 0-9
224 case 0x20: return KeyTable
.XK_space
;
225 case 0x6a: return KeyTable
.XK_KP_Multiply
;
226 case 0x6b: return KeyTable
.XK_KP_Add
;
227 case 0x6c: return KeyTable
.XK_KP_Separator
;
228 case 0x6d: return KeyTable
.XK_KP_Subtract
;
229 case 0x6e: return KeyTable
.XK_KP_Decimal
;
230 case 0x6f: return KeyTable
.XK_KP_Divide
;
231 case 0xbb: return KeyTable
.XK_plus
;
232 case 0xbc: return KeyTable
.XK_comma
;
233 case 0xbd: return KeyTable
.XK_minus
;
234 case 0xbe: return KeyTable
.XK_period
;
237 return nonCharacterKey({keyCode
: keycode
});
240 // if the key is a known non-character key (any key which doesn't generate character data)
241 // return its keysym value. Otherwise return null
242 function nonCharacterKey(evt
) {
243 // evt.key not implemented yet
244 if (!evt
.keyCode
) { return null; }
245 var keycode
= evt
.keyCode
;
247 if (keycode
>= 0x70 && keycode
<= 0x87) {
248 return KeyTable
.XK_F1
+ keycode
- 0x70; // F1-F24
252 case 8 : return KeyTable
.XK_BackSpace
;
253 case 13 : return KeyTable
.XK_Return
;
255 case 9 : return KeyTable
.XK_Tab
;
257 case 27 : return KeyTable
.XK_Escape
;
258 case 46 : return KeyTable
.XK_Delete
;
260 case 36 : return KeyTable
.XK_Home
;
261 case 35 : return KeyTable
.XK_End
;
262 case 33 : return KeyTable
.XK_Page_Up
;
263 case 34 : return KeyTable
.XK_Page_Down
;
264 case 45 : return KeyTable
.XK_Insert
;
266 case 37 : return KeyTable
.XK_Left
;
267 case 38 : return KeyTable
.XK_Up
;
268 case 39 : return KeyTable
.XK_Right
;
269 case 40 : return KeyTable
.XK_Down
;
271 case 16 : return KeyTable
.XK_Shift_L
;
272 case 17 : return KeyTable
.XK_Control_L
;
273 case 18 : return KeyTable
.XK_Alt_L
; // also: Option-key on Mac
275 case 224 : return KeyTable
.XK_Meta_L
;
276 case 225 : return KeyTable
.XK_ISO_Level3_Shift
; // AltGr
277 case 91 : return KeyTable
.XK_Super_L
; // also: Windows-key
278 case 92 : return KeyTable
.XK_Super_R
; // also: Windows-key
279 case 93 : return KeyTable
.XK_Menu
; // also: Windows-Menu, Command on Mac
280 default: return null;
284 KeyboardUtil
.hasShortcutModifier
= hasShortcutModifier
;
285 KeyboardUtil
.hasCharModifier
= hasCharModifier
;
286 KeyboardUtil
.ModifierSync
= ModifierSync
;
287 KeyboardUtil
.getKey
= getKey
;
288 KeyboardUtil
.getKeysym
= getKeysym
;
289 KeyboardUtil
.keysymFromKeyCode
= keysymFromKeyCode
;
290 KeyboardUtil
.nonCharacterKey
= nonCharacterKey
;
291 KeyboardUtil
.substituteCodepoint
= substituteCodepoint
;
294 KeyboardUtil
.QEMUKeyEventDecoder = function(modifierState
, next
) {
297 function sendAll(evts
) {
298 for (var i
= 0; i
< evts
.length
; ++i
) {
303 var numPadCodes
= ["Numpad0", "Numpad1", "Numpad2",
304 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
305 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
307 var numLockOnKeySyms
= {
308 "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
309 "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
310 "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
311 "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
314 var numLockOnKeyCodes
= [96, 97, 98, 99, 100, 101, 102,
315 103, 104, 105, 108, 110];
317 function isNumPadMultiKey(evt
) {
318 return (numPadCodes
.indexOf(evt
.code
) !== -1);
321 function getNumPadKeySym(evt
) {
322 if (numLockOnKeyCodes
.indexOf(evt
.keyCode
) !== -1) {
323 return numLockOnKeySyms
[evt
.code
];
328 function process(evt
, type
) {
329 var result
= {type
: type
};
330 result
.code
= evt
.code
;
333 if (isNumPadMultiKey(evt
)) {
334 result
.keysym
= getNumPadKeySym(evt
);
337 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
338 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
340 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!KeyboardUtil
.nonCharacterKey(evt
));
346 keydown: function(evt
) {
347 sendAll(modifierState
.keydown(evt
));
348 return process(evt
, 'keydown');
350 keypress: function(evt
) {
353 keyup: function(evt
) {
354 sendAll(modifierState
.keyup(evt
));
355 return process(evt
, 'keyup');
357 syncModifiers: function(evt
) {
358 sendAll(modifierState
.syncAny(evt
));
360 releaseAll: function() { next({type
: 'releaseall'}); }
364 KeyboardUtil
.TrackQEMUKeyState = function(next
) {
368 return function (evt
) {
369 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
374 if (!last
|| last
.code
!== evt
.code
) {
375 last
= {code
: evt
.code
};
377 if (state
.length
> 0 && state
[state
.length
-1].code
== 'ControlLeft') {
378 if (evt
.code
!== 'AltRight') {
379 next({code
: 'ControlLeft', type
: 'keydown', keysym
: 0});
386 if (evt
.code
!== 'ControlLeft') {
392 if (state
.length
=== 0) {
396 // do we have a matching key tracked as being down?
397 for (var i
= 0; i
!== state
.length
; ++i
) {
398 if (state
[i
].code
=== evt
.code
) {
403 // if we couldn't find a match (it happens), assume it was the last key pressed
405 if (evt
.code
=== 'ControlLeft') {
408 idx
= state
.length
- 1;
411 state
.splice(idx
, 1);
415 /* jshint shadow: true */
416 for (var i
= 0; i
< state
.length
; ++i
) {
417 next({code
: state
[i
].code
, keysym
: 0, type
: 'keyup'});
419 /* jshint shadow: false */
425 // Takes a DOM keyboard event and:
426 // - determines which keysym it represents
427 // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
428 // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
429 // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
430 // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
431 // This information is collected into an object which is passed to the next() function. (one call per event)
432 KeyboardUtil
.KeyEventDecoder = function(modifierState
, next
) {
434 function sendAll(evts
) {
435 for (var i
= 0; i
< evts
.length
; ++i
) {
439 function process(evt
, type
) {
440 var result
= {type
: type
};
441 var keyId
= KeyboardUtil
.getKey(evt
);
443 result
.keyId
= keyId
;
446 var keysym
= KeyboardUtil
.getKeysym(evt
);
448 var hasModifier
= modifierState
.hasShortcutModifier() || !!modifierState
.activeCharModifier();
449 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
450 // "special" keys like enter, tab or backspace don't send keypress events,
451 // and some browsers don't send keypresses at all if a modifier is down
452 if (keysym
&& (type
!== 'keydown' || KeyboardUtil
.nonCharacterKey(evt
) || hasModifier
)) {
453 result
.keysym
= keysym
;
456 var isShift
= evt
.keyCode
=== 0x10 || evt
.key
=== 'Shift';
458 // Should we prevent the browser from handling the event?
459 // Doing so on a keydown (in most browsers) prevents keypress from being generated
460 // so only do that if we have to.
461 var suppress
= !isShift
&& (type
!== 'keydown' || modifierState
.hasShortcutModifier() || !!KeyboardUtil
.nonCharacterKey(evt
));
463 // If a char modifier is down on a keydown, we need to insert a stall,
464 // so VerifyCharModifier knows to wait and see if a keypress is comnig
465 var stall
= type
=== 'keydown' && modifierState
.activeCharModifier() && !KeyboardUtil
.nonCharacterKey(evt
);
467 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
468 var active
= modifierState
.activeCharModifier();
470 // If we have a char modifier down, and we're able to determine a keysym reliably
471 // then (a) we know to treat the modifier as a char modifier,
472 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
473 if (active
&& keysym
) {
474 var isCharModifier
= false;
475 for (var i
= 0; i
< active
.length
; ++i
) {
476 if (active
[i
] === keysym
.keysym
) {
477 isCharModifier
= true;
480 if (type
=== 'keypress' && !isCharModifier
) {
481 result
.escape
= modifierState
.activeCharModifier();
486 // insert a fake "stall" event
487 next({type
: 'stall'});
495 keydown: function(evt
) {
496 sendAll(modifierState
.keydown(evt
));
497 return process(evt
, 'keydown');
499 keypress: function(evt
) {
500 return process(evt
, 'keypress');
502 keyup: function(evt
) {
503 sendAll(modifierState
.keyup(evt
));
504 return process(evt
, 'keyup');
506 syncModifiers: function(evt
) {
507 sendAll(modifierState
.syncAny(evt
));
509 releaseAll: function() { next({type
: 'releaseall'}); }
513 // Combines keydown and keypress events where necessary to handle char modifiers.
514 // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
515 // For example, on Windows, AltGr is synonymous with Ctrl-Alt. On a Danish keyboard layout, AltGr-2 yields a @, but Ctrl-Alt-D does nothing
516 // so when used with the '2' key, Ctrl-Alt counts as a char modifier (and should be escaped), but when used with 'D', it does not.
517 // The only way we can distinguish these cases is to wait and see if a keypress event arrives
518 // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
519 KeyboardUtil
.VerifyCharModifier = function(next
) {
528 var delayProcess = function () {
534 while (queue
.length
!== 0) {
536 queue
= queue
.splice(1);
539 // insert a delay before processing available events.
540 /* jshint loopfunc: true */
541 timer
= setTimeout(delayProcess
, 5);
542 /* jshint loopfunc: false */
545 // is the next element a keypress? Then we should merge the two
546 if (queue
.length
!== 0 && queue
[0].type
=== 'keypress') {
547 // Firefox sends keypress even when no char is generated.
548 // so, if keypress keysym is the same as we'd have guessed from keydown,
549 // the modifier didn't have any effect, and should not be escaped
550 if (queue
[0].escape
&& (!cur
.keysym
|| cur
.keysym
.keysym
!== queue
[0].keysym
.keysym
)) {
551 cur
.escape
= queue
[0].escape
;
553 cur
.keysym
= queue
[0].keysym
;
554 queue
= queue
.splice(1);
559 // swallow stall events, and pass all others to the next stage
560 if (cur
.type
!== 'stall') {
565 return function(evt
) {
571 // Keeps track of which keys we (and the server) believe are down
572 // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
573 // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
574 // key repeat events should be merged into a single entry.
575 // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
576 KeyboardUtil
.TrackKeyState = function(next
) {
580 return function (evt
) {
581 var last
= state
.length
!== 0 ? state
[state
.length
-1] : null;
585 // insert a new entry if last seen key was different.
586 if (!last
|| !evt
.keyId
|| last
.keyId
!== evt
.keyId
) {
587 last
= {keyId
: evt
.keyId
, keysyms
: {}};
591 // make sure last event contains this keysym (a single "logical" keyevent
592 // can cause multiple key events to be sent to the VNC server)
593 last
.keysyms
[evt
.keysym
.keysym
] = evt
.keysym
;
594 last
.ignoreKeyPress
= true;
600 last
= {keyId
: evt
.keyId
, keysyms
: {}};
604 console
.log('keypress with no keysym:', evt
);
607 // If we didn't expect a keypress, and already sent a keydown to the VNC server
608 // based on the keydown, make sure to skip this event.
609 if (evt
.keysym
&& !last
.ignoreKeyPress
) {
610 last
.keysyms
[evt
.keysym
.keysym
] = evt
.keysym
;
611 evt
.type
= 'keydown';
616 if (state
.length
=== 0) {
620 // do we have a matching key tracked as being down?
621 for (var i
= 0; i
!== state
.length
; ++i
) {
622 if (state
[i
].keyId
=== evt
.keyId
) {
627 // if we couldn't find a match (it happens), assume it was the last key pressed
629 idx
= state
.length
- 1;
632 var item
= state
.splice(idx
, 1)[0];
633 // for each keysym tracked by this key entry, clone the current event and override the keysym
634 var clone
= (function(){
636 return function (obj
) { Clone
.prototype=obj
; return new Clone(); };
638 for (var key
in item
.keysyms
) {
639 var out
= clone(evt
);
640 out
.keysym
= item
.keysyms
[key
];
645 /* jshint shadow: true */
646 for (var i
= 0; i
< state
.length
; ++i
) {
647 for (var key
in state
[i
].keysyms
) {
648 var keysym
= state
[i
].keysyms
[key
];
649 next({keyId
: 0, keysym
: keysym
, type
: 'keyup'});
652 /* jshint shadow: false */
658 // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
659 // then the modifier must be "undone" before sending the @, and "redone" afterwards.
660 KeyboardUtil
.EscapeModifiers = function(next
) {
662 return function(evt
) {
663 if (evt
.type
!== 'keydown' || evt
.escape
=== undefined) {
668 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
669 next({type
: 'keyup', keyId
: 0, keysym
: keysyms
.lookup(evt
.escape
[i
])});
671 // send the character event
674 /* jshint shadow: true */
675 for (var i
= 0; i
< evt
.escape
.length
; ++i
) {
676 next({type
: 'keydown', keyId
: 0, keysym
: keysyms
.lookup(evt
.escape
[i
])});
678 /* jshint shadow: false */
682 /* [module] export default KeyboardUtil; */