]>
Commit | Line | Data |
---|---|---|
4ef7566b | 1 | var 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) | |
295 | function 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 | |
382 | function 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 | |
434 | function 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. | |
516 | function 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 | } |