]> git.proxmox.com Git - mirror_novnc.git/blame - include/keyboard.js
Cleanup: Input code
[mirror_novnc.git] / include / keyboard.js
CommitLineData
4ef7566b 1var kbdUtil = (function() {
2 "use strict";
3
466a09f0 4 function substituteCodepoint(cp) {
5 // Any Unicode code points which do not have corresponding keysym entries
6 // can be swapped out for another code point by adding them to this table
7 var substitutions = {
8 // {S,s} with comma below -> {S,s} with cedilla
9 0x218 : 0x15e,
10 0x219 : 0x15f,
11 // {T,t} with comma below -> {T,t} with cedilla
12 0x21a : 0x162,
13 0x21b : 0x163
14 };
15
16 var sub = substitutions[cp];
17 return sub ? sub : cp;
18 };
19
4ef7566b 20 function isMac() {
23078406 21 return navigator && !!(/mac/i).exec(navigator.platform);
4ef7566b 22 }
23 function isWindows() {
23078406 24 return navigator && !!(/win/i).exec(navigator.platform);
4ef7566b 25 }
26 function isLinux() {
23078406 27 return navigator && !!(/linux/i).exec(navigator.platform);
4ef7566b 28 }
29
30 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
31 function hasShortcutModifier(charModifier, currentModifiers) {
32 var mods = {};
33 for (var key in currentModifiers) {
f6a1d98a 34 if (parseInt(key) !== 0xffe1) {
4ef7566b 35 mods[key] = currentModifiers[key];
36 }
37 }
38
39 var sum = 0;
40 for (var k in currentModifiers) {
41 if (mods[k]) {
42 ++sum;
43 }
44 }
45 if (hasCharModifier(charModifier, mods)) {
46 return sum > charModifier.length;
47 }
48 else {
49 return sum > 0;
50 }
51 }
52
53 // Return true if the specified char modifier is currently down
54 function hasCharModifier(charModifier, currentModifiers) {
55 if (charModifier.length === 0) { return false; }
56
57 for (var i = 0; i < charModifier.length; ++i) {
58 if (!currentModifiers[charModifier[i]]) {
59 return false;
60 }
61 }
62 return true;
63 }
64
65 // Helper object tracking modifier key state
66 // and generates fake key events to compensate if it gets out of sync
67 function ModifierSync(charModifier) {
68 var ctrl = 0xffe3;
69 var alt = 0xffe9;
70 var altGr = 0xfe03;
71 var shift = 0xffe1;
72 var meta = 0xffe7;
73
74 if (!charModifier) {
75 if (isMac()) {
76 // on Mac, Option (AKA Alt) is used as a char modifier
77 charModifier = [alt];
78 }
79 else if (isWindows()) {
80 // on Windows, Ctrl+Alt is used as a char modifier
81 charModifier = [alt, ctrl];
82 }
83 else if (isLinux()) {
84 // on Linux, AltGr is used as a char modifier
85 charModifier = [altGr];
86 }
87 else {
88 charModifier = [];
89 }
90 }
91
92 var state = {};
93 state[ctrl] = false;
94 state[alt] = false;
95 state[altGr] = false;
96 state[shift] = false;
97 state[meta] = false;
98
99 function sync(evt, keysym) {
100 var result = [];
101 function syncKey(keysym) {
102 return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
103 }
104
105 if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) {
106 state[ctrl] = evt.ctrlKey;
107 result.push(syncKey(ctrl));
108 }
109 if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) {
110 state[alt] = evt.altKey;
111 result.push(syncKey(alt));
112 }
113 if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) {
114 state[altGr] = evt.altGraphKey;
115 result.push(syncKey(altGr));
116 }
117 if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) {
118 state[shift] = evt.shiftKey;
119 result.push(syncKey(shift));
120 }
121 if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) {
122 state[meta] = evt.metaKey;
123 result.push(syncKey(meta));
124 }
125 return result;
126 }
127 function syncKeyEvent(evt, down) {
128 var obj = getKeysym(evt);
129 var keysym = obj ? obj.keysym : null;
130
131 // first, apply the event itself, if relevant
132 if (keysym !== null && state[keysym] !== undefined) {
133 state[keysym] = down;
134 }
135 return sync(evt, keysym);
136 }
137
138 return {
139 // sync on the appropriate keyboard event
140 keydown: function(evt) { return syncKeyEvent(evt, true);},
141 keyup: function(evt) { return syncKeyEvent(evt, false);},
142 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
143 syncAny: function(evt) { return sync(evt);},
144
145 // is a shortcut modifier down?
146 hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
147 // if a char modifier is down, return the keys it consists of, otherwise return null
148 activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
149 };
150 }
151
152 // Get a key ID from a keyboard event
153 // May be a string or an integer depending on the available properties
154 function getKey(evt){
c3f60524
JD
155 if ('keyCode' in evt && 'key' in evt) {
156 return evt.key + ':' + evt.keyCode;
4ef7566b 157 }
c3f60524 158 else if ('keyCode' in evt) {
4ef7566b 159 return evt.keyCode;
160 }
c3f60524
JD
161 else {
162 return evt.key;
163 }
4ef7566b 164 }
165
166 // Get the most reliable keysym value we can get from a key event
167 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
168 function getKeysym(evt){
169 var codepoint;
170 if (evt.char && evt.char.length === 1) {
171 codepoint = evt.char.charCodeAt();
172 }
173 else if (evt.charCode) {
174 codepoint = evt.charCode;
175 }
60a415ae 176 else if (evt.keyCode && evt.type === 'keypress') {
177 // IE10 stores the char code as keyCode, and has no other useful properties
178 codepoint = evt.keyCode;
179 }
4ef7566b 180 if (codepoint) {
466a09f0 181 var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
4ef7566b 182 if (res) {
183 return res;
184 }
185 }
186 // we could check evt.key here.
187 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
188 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
189 // so we don't *need* it yet
190 if (evt.keyCode) {
191 return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
192 }
193 if (evt.which) {
194 return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
195 }
196 return null;
197 }
198
199 // Given a keycode, try to predict which keysym it might be.
200 // If the keycode is unknown, null is returned.
201 function keysymFromKeyCode(keycode, shiftPressed) {
202 if (typeof(keycode) !== 'number') {
203 return null;
204 }
205 // won't be accurate for azerty
206 if (keycode >= 0x30 && keycode <= 0x39) {
207 return keycode; // digit
208 }
209 if (keycode >= 0x41 && keycode <= 0x5a) {
210 // remap to lowercase unless shift is down
211 return shiftPressed ? keycode : keycode + 32; // A-Z
212 }
213 if (keycode >= 0x60 && keycode <= 0x69) {
214 return 0xffb0 + (keycode - 0x60); // numpad 0-9
215 }
216
217 switch(keycode) {
218 case 0x20: return 0x20; // space
219 case 0x6a: return 0xffaa; // multiply
220 case 0x6b: return 0xffab; // add
221 case 0x6c: return 0xffac; // separator
222 case 0x6d: return 0xffad; // subtract
223 case 0x6e: return 0xffae; // decimal
224 case 0x6f: return 0xffaf; // divide
225 case 0xbb: return 0x2b; // +
226 case 0xbc: return 0x2c; // ,
227 case 0xbd: return 0x2d; // -
228 case 0xbe: return 0x2e; // .
229 }
230
231 return nonCharacterKey({keyCode: keycode});
232 }
233
234 // if the key is a known non-character key (any key which doesn't generate character data)
235 // return its keysym value. Otherwise return null
236 function nonCharacterKey(evt) {
237 // evt.key not implemented yet
238 if (!evt.keyCode) { return null; }
239 var keycode = evt.keyCode;
240
241 if (keycode >= 0x70 && keycode <= 0x87) {
242 return 0xffbe + keycode - 0x70; // F1-F24
243 }
244 switch (keycode) {
245
246 case 8 : return 0xFF08; // BACKSPACE
247 case 13 : return 0xFF0D; // ENTER
248
249 case 9 : return 0xFF09; // TAB
250
251 case 27 : return 0xFF1B; // ESCAPE
252 case 46 : return 0xFFFF; // DELETE
253
254 case 36 : return 0xFF50; // HOME
255 case 35 : return 0xFF57; // END
256 case 33 : return 0xFF55; // PAGE_UP
257 case 34 : return 0xFF56; // PAGE_DOWN
258 case 45 : return 0xFF63; // INSERT
259
260 case 37 : return 0xFF51; // LEFT
261 case 38 : return 0xFF52; // UP
262 case 39 : return 0xFF53; // RIGHT
263 case 40 : return 0xFF54; // DOWN
264 case 16 : return 0xFFE1; // SHIFT
265 case 17 : return 0xFFE3; // CONTROL
266 case 18 : return 0xFFE9; // Left ALT (Mac Option)
267
268 case 224 : return 0xFE07; // Meta
269 case 225 : return 0xFE03; // AltGr
270 case 91 : return 0xFFEC; // Super_L (Win Key)
271 case 92 : return 0xFFED; // Super_R (Win Key)
272 case 93 : return 0xFF67; // Menu (Win Menu), Mac Command
273 default: return null;
274 }
275 }
276 return {
277 hasShortcutModifier : hasShortcutModifier,
278 hasCharModifier : hasCharModifier,
279 ModifierSync : ModifierSync,
280 getKey : getKey,
281 getKeysym : getKeysym,
282 keysymFromKeyCode : keysymFromKeyCode,
466a09f0 283 nonCharacterKey : nonCharacterKey,
284 substituteCodepoint : substituteCodepoint
4ef7566b 285 };
286})();
287
288// Takes a DOM keyboard event and:
289// - determines which keysym it represents
290// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
291// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
292// - marks each event with an 'escape' property if a modifier was down which should be "escaped"
293// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
294// This information is collected into an object which is passed to the next() function. (one call per event)
295function KeyEventDecoder(modifierState, next) {
296 "use strict";
297 function sendAll(evts) {
298 for (var i = 0; i < evts.length; ++i) {
299 next(evts[i]);
300 }
301 }
302 function process(evt, type) {
303 var result = {type: type};
304 var keyId = kbdUtil.getKey(evt);
305 if (keyId) {
306 result.keyId = keyId;
307 }
308
309 var keysym = kbdUtil.getKeysym(evt);
310
311 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
312 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
313 // "special" keys like enter, tab or backspace don't send keypress events,
314 // and some browsers don't send keypresses at all if a modifier is down
315 if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) {
316 result.keysym = keysym;
317 }
318
319 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
320
321 // Should we prevent the browser from handling the event?
322 // Doing so on a keydown (in most browsers) prevents keypress from being generated
323 // so only do that if we have to.
324 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt));
325
326 // If a char modifier is down on a keydown, we need to insert a stall,
327 // so VerifyCharModifier knows to wait and see if a keypress is comnig
328 var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt);
329
330 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
331 var active = modifierState.activeCharModifier();
332
333 // If we have a char modifier down, and we're able to determine a keysym reliably
334 // then (a) we know to treat the modifier as a char modifier,
335 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
336 if (active && keysym) {
337 var isCharModifier = false;
338 for (var i = 0; i < active.length; ++i) {
339 if (active[i] === keysym.keysym) {
340 isCharModifier = true;
341 }
342 }
343 if (type === 'keypress' && !isCharModifier) {
344 result.escape = modifierState.activeCharModifier();
345 }
346 }
347
348 if (stall) {
349 // insert a fake "stall" event
350 next({type: 'stall'});
351 }
352 next(result);
353
354 return suppress;
355 }
356
357 return {
358 keydown: function(evt) {
359 sendAll(modifierState.keydown(evt));
360 return process(evt, 'keydown');
361 },
362 keypress: function(evt) {
363 return process(evt, 'keypress');
364 },
365 keyup: function(evt) {
366 sendAll(modifierState.keyup(evt));
367 return process(evt, 'keyup');
368 },
369 syncModifiers: function(evt) {
370 sendAll(modifierState.syncAny(evt));
371 },
372 releaseAll: function() { next({type: 'releaseall'}); }
373 };
374}
375
376// Combines keydown and keypress events where necessary to handle char modifiers.
377// On some OS'es, a char modifier is sometimes used as a shortcut modifier.
378// 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
379// 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.
380// The only way we can distinguish these cases is to wait and see if a keypress event arrives
381// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
382function VerifyCharModifier(next) {
383 "use strict";
384 var queue = [];
385 var timer = null;
386 function process() {
387 if (timer) {
388 return;
389 }
390 while (queue.length !== 0) {
391 var cur = queue[0];
392 queue = queue.splice(1);
393 switch (cur.type) {
394 case 'stall':
395 // insert a delay before processing available events.
396 timer = setTimeout(function() {
397 clearTimeout(timer);
398 timer = null;
399 process();
400 }, 5);
401 return;
402 case 'keydown':
403 // is the next element a keypress? Then we should merge the two
404 if (queue.length !== 0 && queue[0].type === 'keypress') {
405 // Firefox sends keypress even when no char is generated.
406 // so, if keypress keysym is the same as we'd have guessed from keydown,
407 // the modifier didn't have any effect, and should not be escaped
408 if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
409 cur.escape = queue[0].escape;
410 }
411 cur.keysym = queue[0].keysym;
412 queue = queue.splice(1);
413 }
414 break;
415 }
416
417 // swallow stall events, and pass all others to the next stage
418 if (cur.type !== 'stall') {
419 next(cur);
420 }
421 }
422 }
423 return function(evt) {
424 queue.push(evt);
425 process();
426 };
427}
428
429// Keeps track of which keys we (and the server) believe are down
430// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
431// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
432// key repeat events should be merged into a single entry.
433// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
434function TrackKeyState(next) {
435 "use strict";
436 var state = [];
437
438 return function (evt) {
439 var last = state.length !== 0 ? state[state.length-1] : null;
440
441 switch (evt.type) {
442 case 'keydown':
443 // insert a new entry if last seen key was different.
444 if (!last || !evt.keyId || last.keyId !== evt.keyId) {
445 last = {keyId: evt.keyId, keysyms: {}};
446 state.push(last);
447 }
448 if (evt.keysym) {
449 // make sure last event contains this keysym (a single "logical" keyevent
450 // can cause multiple key events to be sent to the VNC server)
451 last.keysyms[evt.keysym.keysym] = evt.keysym;
452 last.ignoreKeyPress = true;
453 next(evt);
454 }
455 break;
456 case 'keypress':
457 if (!last) {
458 last = {keyId: evt.keyId, keysyms: {}};
459 state.push(last);
460 }
461 if (!evt.keysym) {
462 console.log('keypress with no keysym:', evt);
463 }
464
465 // If we didn't expect a keypress, and already sent a keydown to the VNC server
466 // based on the keydown, make sure to skip this event.
467 if (evt.keysym && !last.ignoreKeyPress) {
468 last.keysyms[evt.keysym.keysym] = evt.keysym;
469 evt.type = 'keydown';
470 next(evt);
471 }
472 break;
473 case 'keyup':
474 if (state.length === 0) {
475 return;
476 }
477 var idx = null;
478 // do we have a matching key tracked as being down?
479 for (var i = 0; i !== state.length; ++i) {
480 if (state[i].keyId === evt.keyId) {
481 idx = i;
482 break;
483 }
484 }
485 // if we couldn't find a match (it happens), assume it was the last key pressed
486 if (idx === null) {
487 idx = state.length - 1;
488 }
489
490 var item = state.splice(idx, 1)[0];
491 // for each keysym tracked by this key entry, clone the current event and override the keysym
492 for (var key in item.keysyms) {
493 var clone = (function(){
494 function Clone(){}
495 return function (obj) { Clone.prototype=obj; return new Clone(); };
496 }());
497 var out = clone(evt);
498 out.keysym = item.keysyms[key];
499 next(out);
500 }
501 break;
502 case 'releaseall':
503 for (var i = 0; i < state.length; ++i) {
504 for (var key in state[i].keysyms) {
505 var keysym = state[i].keysyms[key];
506 next({keyId: 0, keysym: keysym, type: 'keyup'});
507 }
508 }
509 state = [];
510 }
511 };
512}
513
514// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
515// then the modifier must be "undone" before sending the @, and "redone" afterwards.
516function EscapeModifiers(next) {
517 "use strict";
518 return function(evt) {
519 if (evt.type !== 'keydown' || evt.escape === undefined) {
520 next(evt);
521 return;
522 }
523 // undo modifiers
524 for (var i = 0; i < evt.escape.length; ++i) {
525 next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
526 }
527 // send the character event
528 next(evt);
529 // redo modifiers
530 for (var i = 0; i < evt.escape.length; ++i) {
531 next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
532 }
533 };
534}