]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
Uncomment ES6 module syntax
[mirror_novnc.git] / core / input / util.js
1 import KeyTable from "./keysym.js";
2 import keysyms from "./keysymdef.js";
3
4
5 var KeyboardUtil = {};
6
7 (function() {
8 "use strict";
9
10 function substituteCodepoint(cp) {
11 // Any Unicode code points which do not have corresponding keysym entries
12 // can be swapped out for another code point by adding them to this table
13 var substitutions = {
14 // {S,s} with comma below -> {S,s} with cedilla
15 0x218 : 0x15e,
16 0x219 : 0x15f,
17 // {T,t} with comma below -> {T,t} with cedilla
18 0x21a : 0x162,
19 0x21b : 0x163
20 };
21
22 var sub = substitutions[cp];
23 return sub ? sub : cp;
24 }
25
26 function isMac() {
27 return navigator && !!(/mac/i).exec(navigator.platform);
28 }
29 function isWindows() {
30 return navigator && !!(/win/i).exec(navigator.platform);
31 }
32 function isLinux() {
33 return navigator && !!(/linux/i).exec(navigator.platform);
34 }
35
36 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
37 function hasShortcutModifier(charModifier, currentModifiers) {
38 var mods = {};
39 for (var key in currentModifiers) {
40 if (parseInt(key) !== KeyTable.XK_Shift_L) {
41 mods[key] = currentModifiers[key];
42 }
43 }
44
45 var sum = 0;
46 for (var k in currentModifiers) {
47 if (mods[k]) {
48 ++sum;
49 }
50 }
51 if (hasCharModifier(charModifier, mods)) {
52 return sum > charModifier.length;
53 }
54 else {
55 return sum > 0;
56 }
57 }
58
59 // Return true if the specified char modifier is currently down
60 function hasCharModifier(charModifier, currentModifiers) {
61 if (charModifier.length === 0) { return false; }
62
63 for (var i = 0; i < charModifier.length; ++i) {
64 if (!currentModifiers[charModifier[i]]) {
65 return false;
66 }
67 }
68 return true;
69 }
70
71 // Helper object tracking modifier key state
72 // and generates fake key events to compensate if it gets out of sync
73 function ModifierSync(charModifier) {
74 if (!charModifier) {
75 if (isMac()) {
76 // on Mac, Option (AKA Alt) is used as a char modifier
77 charModifier = [KeyTable.XK_Alt_L];
78 }
79 else if (isWindows()) {
80 // on Windows, Ctrl+Alt is used as a char modifier
81 charModifier = [KeyTable.XK_Alt_L, KeyTable.XK_Control_L];
82 }
83 else if (isLinux()) {
84 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
85 charModifier = [KeyTable.XK_ISO_Level3_Shift];
86 }
87 else {
88 charModifier = [];
89 }
90 }
91
92 var state = {};
93 state[KeyTable.XK_Control_L] = false;
94 state[KeyTable.XK_Alt_L] = false;
95 state[KeyTable.XK_ISO_Level3_Shift] = false;
96 state[KeyTable.XK_Shift_L] = false;
97 state[KeyTable.XK_Meta_L] = 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 &&
106 evt.ctrlKey !== state[KeyTable.XK_Control_L] && keysym !== KeyTable.XK_Control_L) {
107 state[KeyTable.XK_Control_L] = evt.ctrlKey;
108 result.push(syncKey(KeyTable.XK_Control_L));
109 }
110 if (evt.altKey !== undefined &&
111 evt.altKey !== state[KeyTable.XK_Alt_L] && keysym !== KeyTable.XK_Alt_L) {
112 state[KeyTable.XK_Alt_L] = evt.altKey;
113 result.push(syncKey(KeyTable.XK_Alt_L));
114 }
115 if (evt.altGraphKey !== undefined &&
116 evt.altGraphKey !== state[KeyTable.XK_ISO_Level3_Shift] && keysym !== KeyTable.XK_ISO_Level3_Shift) {
117 state[KeyTable.XK_ISO_Level3_Shift] = evt.altGraphKey;
118 result.push(syncKey(KeyTable.XK_ISO_Level3_Shift));
119 }
120 if (evt.shiftKey !== undefined &&
121 evt.shiftKey !== state[KeyTable.XK_Shift_L] && keysym !== KeyTable.XK_Shift_L) {
122 state[KeyTable.XK_Shift_L] = evt.shiftKey;
123 result.push(syncKey(KeyTable.XK_Shift_L));
124 }
125 if (evt.metaKey !== undefined &&
126 evt.metaKey !== state[KeyTable.XK_Meta_L] && keysym !== KeyTable.XK_Meta_L) {
127 state[KeyTable.XK_Meta_L] = evt.metaKey;
128 result.push(syncKey(KeyTable.XK_Meta_L));
129 }
130 return result;
131 }
132 function syncKeyEvent(evt, down) {
133 var obj = getKeysym(evt);
134 var keysym = obj ? obj.keysym : null;
135
136 // first, apply the event itself, if relevant
137 if (keysym !== null && state[keysym] !== undefined) {
138 state[keysym] = down;
139 }
140 return sync(evt, keysym);
141 }
142
143 return {
144 // sync on the appropriate keyboard event
145 keydown: function(evt) { return syncKeyEvent(evt, true);},
146 keyup: function(evt) { return syncKeyEvent(evt, false);},
147 // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
148 syncAny: function(evt) { return sync(evt);},
149
150 // is a shortcut modifier down?
151 hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
152 // if a char modifier is down, return the keys it consists of, otherwise return null
153 activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
154 };
155 }
156
157 // Get a key ID from a keyboard event
158 // May be a string or an integer depending on the available properties
159 function getKey(evt){
160 if ('keyCode' in evt && 'key' in evt) {
161 return evt.key + ':' + evt.keyCode;
162 }
163 else if ('keyCode' in evt) {
164 return evt.keyCode;
165 }
166 else {
167 return evt.key;
168 }
169 }
170
171 // Get the most reliable keysym value we can get from a key event
172 // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
173 function getKeysym(evt){
174 var codepoint;
175 if (evt.char && evt.char.length === 1) {
176 codepoint = evt.char.charCodeAt();
177 }
178 else if (evt.charCode) {
179 codepoint = evt.charCode;
180 }
181 else if (evt.keyCode && evt.type === 'keypress') {
182 // IE10 stores the char code as keyCode, and has no other useful properties
183 codepoint = evt.keyCode;
184 }
185 if (codepoint) {
186 return keysyms.fromUnicode(substituteCodepoint(codepoint));
187 }
188 // we could check evt.key here.
189 // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
190 // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
191 // so we don't *need* it yet
192 if (evt.keyCode) {
193 return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
194 }
195 if (evt.which) {
196 return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
197 }
198 return null;
199 }
200
201 // Given a keycode, try to predict which keysym it might be.
202 // If the keycode is unknown, null is returned.
203 function keysymFromKeyCode(keycode, shiftPressed) {
204 if (typeof(keycode) !== 'number') {
205 return null;
206 }
207 // won't be accurate for azerty
208 if (keycode >= 0x30 && keycode <= 0x39) {
209 return keycode; // digit
210 }
211 if (keycode >= 0x41 && keycode <= 0x5a) {
212 // remap to lowercase unless shift is down
213 return shiftPressed ? keycode : keycode + 32; // A-Z
214 }
215 if (keycode >= 0x60 && keycode <= 0x69) {
216 return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9
217 }
218
219 switch(keycode) {
220 case 0x20: return KeyTable.XK_space;
221 case 0x6a: return KeyTable.XK_KP_Multiply;
222 case 0x6b: return KeyTable.XK_KP_Add;
223 case 0x6c: return KeyTable.XK_KP_Separator;
224 case 0x6d: return KeyTable.XK_KP_Subtract;
225 case 0x6e: return KeyTable.XK_KP_Decimal;
226 case 0x6f: return KeyTable.XK_KP_Divide;
227 case 0xbb: return KeyTable.XK_plus;
228 case 0xbc: return KeyTable.XK_comma;
229 case 0xbd: return KeyTable.XK_minus;
230 case 0xbe: return KeyTable.XK_period;
231 }
232
233 return nonCharacterKey({keyCode: keycode});
234 }
235
236 // if the key is a known non-character key (any key which doesn't generate character data)
237 // return its keysym value. Otherwise return null
238 function nonCharacterKey(evt) {
239 // evt.key not implemented yet
240 if (!evt.keyCode) { return null; }
241 var keycode = evt.keyCode;
242
243 if (keycode >= 0x70 && keycode <= 0x87) {
244 return KeyTable.XK_F1 + keycode - 0x70; // F1-F24
245 }
246 switch (keycode) {
247
248 case 8 : return KeyTable.XK_BackSpace;
249 case 13 : return KeyTable.XK_Return;
250
251 case 9 : return KeyTable.XK_Tab;
252
253 case 27 : return KeyTable.XK_Escape;
254 case 46 : return KeyTable.XK_Delete;
255
256 case 36 : return KeyTable.XK_Home;
257 case 35 : return KeyTable.XK_End;
258 case 33 : return KeyTable.XK_Page_Up;
259 case 34 : return KeyTable.XK_Page_Down;
260 case 45 : return KeyTable.XK_Insert;
261
262 case 37 : return KeyTable.XK_Left;
263 case 38 : return KeyTable.XK_Up;
264 case 39 : return KeyTable.XK_Right;
265 case 40 : return KeyTable.XK_Down;
266
267 case 16 : return KeyTable.XK_Shift_L;
268 case 17 : return KeyTable.XK_Control_L;
269 case 18 : return KeyTable.XK_Alt_L; // also: Option-key on Mac
270
271 case 224 : return KeyTable.XK_Meta_L;
272 case 225 : return KeyTable.XK_ISO_Level3_Shift; // AltGr
273 case 91 : return KeyTable.XK_Super_L; // also: Windows-key
274 case 92 : return KeyTable.XK_Super_R; // also: Windows-key
275 case 93 : return KeyTable.XK_Menu; // also: Windows-Menu, Command on Mac
276 default: return null;
277 }
278 }
279
280 KeyboardUtil.hasShortcutModifier = hasShortcutModifier;
281 KeyboardUtil.hasCharModifier = hasCharModifier;
282 KeyboardUtil.ModifierSync = ModifierSync;
283 KeyboardUtil.getKey = getKey;
284 KeyboardUtil.getKeysym = getKeysym;
285 KeyboardUtil.keysymFromKeyCode = keysymFromKeyCode;
286 KeyboardUtil.nonCharacterKey = nonCharacterKey;
287 KeyboardUtil.substituteCodepoint = substituteCodepoint;
288 })();
289
290 KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) {
291 "use strict";
292
293 function sendAll(evts) {
294 for (var i = 0; i < evts.length; ++i) {
295 next(evts[i]);
296 }
297 }
298
299 var numPadCodes = ["Numpad0", "Numpad1", "Numpad2",
300 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
301 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
302
303 var numLockOnKeySyms = {
304 "Numpad0": 0xffb0, "Numpad1": 0xffb1, "Numpad2": 0xffb2,
305 "Numpad3": 0xffb3, "Numpad4": 0xffb4, "Numpad5": 0xffb5,
306 "Numpad6": 0xffb6, "Numpad7": 0xffb7, "Numpad8": 0xffb8,
307 "Numpad9": 0xffb9, "NumpadDecimal": 0xffac
308 };
309
310 var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102,
311 103, 104, 105, 108, 110];
312
313 function isNumPadMultiKey(evt) {
314 return (numPadCodes.indexOf(evt.code) !== -1);
315 }
316
317 function getNumPadKeySym(evt) {
318 if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
319 return numLockOnKeySyms[evt.code];
320 }
321 return 0;
322 }
323
324 function process(evt, type) {
325 var result = {type: type};
326 result.code = evt.code;
327 result.keysym = 0;
328
329 if (isNumPadMultiKey(evt)) {
330 result.keysym = getNumPadKeySym(evt);
331 }
332
333 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
334 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
335
336 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt));
337
338 next(result);
339 return suppress;
340 }
341 return {
342 keydown: function(evt) {
343 sendAll(modifierState.keydown(evt));
344 return process(evt, 'keydown');
345 },
346 keypress: function(evt) {
347 return true;
348 },
349 keyup: function(evt) {
350 sendAll(modifierState.keyup(evt));
351 return process(evt, 'keyup');
352 },
353 syncModifiers: function(evt) {
354 sendAll(modifierState.syncAny(evt));
355 },
356 releaseAll: function() { next({type: 'releaseall'}); }
357 };
358 };
359
360 KeyboardUtil.TrackQEMUKeyState = function(next) {
361 "use strict";
362 var state = [];
363
364 return function (evt) {
365 var last = state.length !== 0 ? state[state.length-1] : null;
366
367 switch (evt.type) {
368 case 'keydown':
369
370 if (!last || last.code !== evt.code) {
371 last = {code: evt.code};
372
373 if (state.length > 0 && state[state.length-1].code == 'ControlLeft') {
374 if (evt.code !== 'AltRight') {
375 next({code: 'ControlLeft', type: 'keydown', keysym: 0});
376 } else {
377 state.pop();
378 }
379 }
380 state.push(last);
381 }
382 if (evt.code !== 'ControlLeft') {
383 next(evt);
384 }
385 break;
386
387 case 'keyup':
388 if (state.length === 0) {
389 return;
390 }
391 var idx = null;
392 // do we have a matching key tracked as being down?
393 for (var i = 0; i !== state.length; ++i) {
394 if (state[i].code === evt.code) {
395 idx = i;
396 break;
397 }
398 }
399 // if we couldn't find a match (it happens), assume it was the last key pressed
400 if (idx === null) {
401 if (evt.code === 'ControlLeft') {
402 return;
403 }
404 idx = state.length - 1;
405 }
406
407 state.splice(idx, 1);
408 next(evt);
409 break;
410 case 'releaseall':
411 /* jshint shadow: true */
412 for (var i = 0; i < state.length; ++i) {
413 next({code: state[i].code, keysym: 0, type: 'keyup'});
414 }
415 /* jshint shadow: false */
416 state = [];
417 }
418 };
419 };
420
421 // Takes a DOM keyboard event and:
422 // - determines which keysym it represents
423 // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
424 // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
425 // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
426 // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
427 // This information is collected into an object which is passed to the next() function. (one call per event)
428 KeyboardUtil.KeyEventDecoder = function(modifierState, next) {
429 "use strict";
430 function sendAll(evts) {
431 for (var i = 0; i < evts.length; ++i) {
432 next(evts[i]);
433 }
434 }
435 function process(evt, type) {
436 var result = {type: type};
437 var keyId = KeyboardUtil.getKey(evt);
438 if (keyId) {
439 result.keyId = keyId;
440 }
441
442 var keysym = KeyboardUtil.getKeysym(evt);
443
444 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
445 // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
446 // "special" keys like enter, tab or backspace don't send keypress events,
447 // and some browsers don't send keypresses at all if a modifier is down
448 if (keysym && (type !== 'keydown' || KeyboardUtil.nonCharacterKey(evt) || hasModifier)) {
449 result.keysym = keysym;
450 }
451
452 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
453
454 // Should we prevent the browser from handling the event?
455 // Doing so on a keydown (in most browsers) prevents keypress from being generated
456 // so only do that if we have to.
457 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt));
458
459 // If a char modifier is down on a keydown, we need to insert a stall,
460 // so VerifyCharModifier knows to wait and see if a keypress is comnig
461 var stall = type === 'keydown' && modifierState.activeCharModifier() && !KeyboardUtil.nonCharacterKey(evt);
462
463 // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
464 var active = modifierState.activeCharModifier();
465
466 // If we have a char modifier down, and we're able to determine a keysym reliably
467 // then (a) we know to treat the modifier as a char modifier,
468 // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
469 if (active && keysym) {
470 var isCharModifier = false;
471 for (var i = 0; i < active.length; ++i) {
472 if (active[i] === keysym.keysym) {
473 isCharModifier = true;
474 }
475 }
476 if (type === 'keypress' && !isCharModifier) {
477 result.escape = modifierState.activeCharModifier();
478 }
479 }
480
481 if (stall) {
482 // insert a fake "stall" event
483 next({type: 'stall'});
484 }
485 next(result);
486
487 return suppress;
488 }
489
490 return {
491 keydown: function(evt) {
492 sendAll(modifierState.keydown(evt));
493 return process(evt, 'keydown');
494 },
495 keypress: function(evt) {
496 return process(evt, 'keypress');
497 },
498 keyup: function(evt) {
499 sendAll(modifierState.keyup(evt));
500 return process(evt, 'keyup');
501 },
502 syncModifiers: function(evt) {
503 sendAll(modifierState.syncAny(evt));
504 },
505 releaseAll: function() { next({type: 'releaseall'}); }
506 };
507 };
508
509 // Combines keydown and keypress events where necessary to handle char modifiers.
510 // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
511 // 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
512 // 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.
513 // The only way we can distinguish these cases is to wait and see if a keypress event arrives
514 // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
515 KeyboardUtil.VerifyCharModifier = function(next) {
516 "use strict";
517 var queue = [];
518 var timer = null;
519 function process() {
520 if (timer) {
521 return;
522 }
523
524 var delayProcess = function () {
525 clearTimeout(timer);
526 timer = null;
527 process();
528 };
529
530 while (queue.length !== 0) {
531 var cur = queue[0];
532 queue = queue.splice(1);
533 switch (cur.type) {
534 case 'stall':
535 // insert a delay before processing available events.
536 /* jshint loopfunc: true */
537 timer = setTimeout(delayProcess, 5);
538 /* jshint loopfunc: false */
539 return;
540 case 'keydown':
541 // is the next element a keypress? Then we should merge the two
542 if (queue.length !== 0 && queue[0].type === 'keypress') {
543 // Firefox sends keypress even when no char is generated.
544 // so, if keypress keysym is the same as we'd have guessed from keydown,
545 // the modifier didn't have any effect, and should not be escaped
546 if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
547 cur.escape = queue[0].escape;
548 }
549 cur.keysym = queue[0].keysym;
550 queue = queue.splice(1);
551 }
552 break;
553 }
554
555 // swallow stall events, and pass all others to the next stage
556 if (cur.type !== 'stall') {
557 next(cur);
558 }
559 }
560 }
561 return function(evt) {
562 queue.push(evt);
563 process();
564 };
565 };
566
567 // Keeps track of which keys we (and the server) believe are down
568 // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
569 // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
570 // key repeat events should be merged into a single entry.
571 // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
572 KeyboardUtil.TrackKeyState = function(next) {
573 "use strict";
574 var state = [];
575
576 return function (evt) {
577 var last = state.length !== 0 ? state[state.length-1] : null;
578
579 switch (evt.type) {
580 case 'keydown':
581 // insert a new entry if last seen key was different.
582 if (!last || !evt.keyId || last.keyId !== evt.keyId) {
583 last = {keyId: evt.keyId, keysyms: {}};
584 state.push(last);
585 }
586 if (evt.keysym) {
587 // make sure last event contains this keysym (a single "logical" keyevent
588 // can cause multiple key events to be sent to the VNC server)
589 last.keysyms[evt.keysym.keysym] = evt.keysym;
590 last.ignoreKeyPress = true;
591 next(evt);
592 }
593 break;
594 case 'keypress':
595 if (!last) {
596 last = {keyId: evt.keyId, keysyms: {}};
597 state.push(last);
598 }
599 if (!evt.keysym) {
600 console.log('keypress with no keysym:', evt);
601 }
602
603 // If we didn't expect a keypress, and already sent a keydown to the VNC server
604 // based on the keydown, make sure to skip this event.
605 if (evt.keysym && !last.ignoreKeyPress) {
606 last.keysyms[evt.keysym.keysym] = evt.keysym;
607 evt.type = 'keydown';
608 next(evt);
609 }
610 break;
611 case 'keyup':
612 if (state.length === 0) {
613 return;
614 }
615 var idx = null;
616 // do we have a matching key tracked as being down?
617 for (var i = 0; i !== state.length; ++i) {
618 if (state[i].keyId === evt.keyId) {
619 idx = i;
620 break;
621 }
622 }
623 // if we couldn't find a match (it happens), assume it was the last key pressed
624 if (idx === null) {
625 idx = state.length - 1;
626 }
627
628 var item = state.splice(idx, 1)[0];
629 // for each keysym tracked by this key entry, clone the current event and override the keysym
630 var clone = (function(){
631 function Clone(){}
632 return function (obj) { Clone.prototype=obj; return new Clone(); };
633 }());
634 for (var key in item.keysyms) {
635 var out = clone(evt);
636 out.keysym = item.keysyms[key];
637 next(out);
638 }
639 break;
640 case 'releaseall':
641 /* jshint shadow: true */
642 for (var i = 0; i < state.length; ++i) {
643 for (var key in state[i].keysyms) {
644 var keysym = state[i].keysyms[key];
645 next({keyId: 0, keysym: keysym, type: 'keyup'});
646 }
647 }
648 /* jshint shadow: false */
649 state = [];
650 }
651 };
652 };
653
654 // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
655 // then the modifier must be "undone" before sending the @, and "redone" afterwards.
656 KeyboardUtil.EscapeModifiers = function(next) {
657 "use strict";
658 return function(evt) {
659 if (evt.type !== 'keydown' || evt.escape === undefined) {
660 next(evt);
661 return;
662 }
663 // undo modifiers
664 for (var i = 0; i < evt.escape.length; ++i) {
665 next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
666 }
667 // send the character event
668 next(evt);
669 // redo modifiers
670 /* jshint shadow: true */
671 for (var i = 0; i < evt.escape.length; ++i) {
672 next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
673 }
674 /* jshint shadow: false */
675 };
676 };
677
678 export default KeyboardUtil;