]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/util.js
Merge pull request #603 from WardF/master
[mirror_novnc.git] / core / input / util.js
1 /* [module]
2 * import KeyTable from "./keysym";
3 * import keysyms from "./keysymdef";
4 */
5
6 var KeyboardUtil = {};
7
8 (function() {
9 "use strict";
10
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
14 var substitutions = {
15 // {S,s} with comma below -> {S,s} with cedilla
16 0x218 : 0x15e,
17 0x219 : 0x15f,
18 // {T,t} with comma below -> {T,t} with cedilla
19 0x21a : 0x162,
20 0x21b : 0x163
21 };
22
23 var sub = substitutions[cp];
24 return sub ? sub : cp;
25 }
26
27 function isMac() {
28 return navigator && !!(/mac/i).exec(navigator.platform);
29 }
30 function isWindows() {
31 return navigator && !!(/win/i).exec(navigator.platform);
32 }
33 function isLinux() {
34 return navigator && !!(/linux/i).exec(navigator.platform);
35 }
36
37 // Return true if a modifier which is not the specified char modifier (and is not shift) is down
38 function hasShortcutModifier(charModifier, currentModifiers) {
39 var mods = {};
40 for (var key in currentModifiers) {
41 if (parseInt(key) !== KeyTable.XK_Shift_L) {
42 mods[key] = currentModifiers[key];
43 }
44 }
45
46 var sum = 0;
47 for (var k in currentModifiers) {
48 if (mods[k]) {
49 ++sum;
50 }
51 }
52 if (hasCharModifier(charModifier, mods)) {
53 return sum > charModifier.length;
54 }
55 else {
56 return sum > 0;
57 }
58 }
59
60 // Return true if the specified char modifier is currently down
61 function hasCharModifier(charModifier, currentModifiers) {
62 if (charModifier.length === 0) { return false; }
63
64 for (var i = 0; i < charModifier.length; ++i) {
65 if (!currentModifiers[charModifier[i]]) {
66 return false;
67 }
68 }
69 return true;
70 }
71
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) {
75 if (!charModifier) {
76 if (isMac()) {
77 // on Mac, Option (AKA Alt) is used as a char modifier
78 charModifier = [KeyTable.XK_Alt_L];
79 }
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];
83 }
84 else if (isLinux()) {
85 // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
86 charModifier = [KeyTable.XK_ISO_Level3_Shift];
87 }
88 else {
89 charModifier = [];
90 }
91 }
92
93 var state = {};
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;
99
100 function sync(evt, keysym) {
101 var result = [];
102 function syncKey(keysym) {
103 return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
104 }
105
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));
110 }
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));
115 }
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));
120 }
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));
125 }
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));
130 }
131 return result;
132 }
133 function syncKeyEvent(evt, down) {
134 var obj = getKeysym(evt);
135 var keysym = obj ? obj.keysym : null;
136
137 // first, apply the event itself, if relevant
138 if (keysym !== null && state[keysym] !== undefined) {
139 state[keysym] = down;
140 }
141 return sync(evt, keysym);
142 }
143
144 return {
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);},
150
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; }
155 };
156 }
157
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;
163 }
164 else if ('keyCode' in evt) {
165 return evt.keyCode;
166 }
167 else {
168 return evt.key;
169 }
170 }
171
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){
175 var codepoint;
176 if (evt.char && evt.char.length === 1) {
177 codepoint = evt.char.charCodeAt();
178 }
179 else if (evt.charCode) {
180 codepoint = evt.charCode;
181 }
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;
185 }
186 if (codepoint) {
187 var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
188 if (res) {
189 return res;
190 }
191 }
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
196 if (evt.keyCode) {
197 return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
198 }
199 if (evt.which) {
200 return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
201 }
202 return null;
203 }
204
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') {
209 return null;
210 }
211 // won't be accurate for azerty
212 if (keycode >= 0x30 && keycode <= 0x39) {
213 return keycode; // digit
214 }
215 if (keycode >= 0x41 && keycode <= 0x5a) {
216 // remap to lowercase unless shift is down
217 return shiftPressed ? keycode : keycode + 32; // A-Z
218 }
219 if (keycode >= 0x60 && keycode <= 0x69) {
220 return KeyTable.XK_KP_0 + (keycode - 0x60); // numpad 0-9
221 }
222
223 switch(keycode) {
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;
235 }
236
237 return nonCharacterKey({keyCode: keycode});
238 }
239
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;
246
247 if (keycode >= 0x70 && keycode <= 0x87) {
248 return KeyTable.XK_F1 + keycode - 0x70; // F1-F24
249 }
250 switch (keycode) {
251
252 case 8 : return KeyTable.XK_BackSpace;
253 case 13 : return KeyTable.XK_Return;
254
255 case 9 : return KeyTable.XK_Tab;
256
257 case 27 : return KeyTable.XK_Escape;
258 case 46 : return KeyTable.XK_Delete;
259
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;
265
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;
270
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
274
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;
281 }
282 }
283
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;
292 })();
293
294 KeyboardUtil.QEMUKeyEventDecoder = function(modifierState, next) {
295 "use strict";
296
297 function sendAll(evts) {
298 for (var i = 0; i < evts.length; ++i) {
299 next(evts[i]);
300 }
301 }
302
303 var numPadCodes = ["Numpad0", "Numpad1", "Numpad2",
304 "Numpad3", "Numpad4", "Numpad5", "Numpad6",
305 "Numpad7", "Numpad8", "Numpad9", "NumpadDecimal"];
306
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
312 };
313
314 var numLockOnKeyCodes = [96, 97, 98, 99, 100, 101, 102,
315 103, 104, 105, 108, 110];
316
317 function isNumPadMultiKey(evt) {
318 return (numPadCodes.indexOf(evt.code) !== -1);
319 }
320
321 function getNumPadKeySym(evt) {
322 if (numLockOnKeyCodes.indexOf(evt.keyCode) !== -1) {
323 return numLockOnKeySyms[evt.code];
324 }
325 return 0;
326 }
327
328 function process(evt, type) {
329 var result = {type: type};
330 result.code = evt.code;
331 result.keysym = 0;
332
333 if (isNumPadMultiKey(evt)) {
334 result.keysym = getNumPadKeySym(evt);
335 }
336
337 var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
338 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
339
340 var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!KeyboardUtil.nonCharacterKey(evt));
341
342 next(result);
343 return suppress;
344 }
345 return {
346 keydown: function(evt) {
347 sendAll(modifierState.keydown(evt));
348 return process(evt, 'keydown');
349 },
350 keypress: function(evt) {
351 return true;
352 },
353 keyup: function(evt) {
354 sendAll(modifierState.keyup(evt));
355 return process(evt, 'keyup');
356 },
357 syncModifiers: function(evt) {
358 sendAll(modifierState.syncAny(evt));
359 },
360 releaseAll: function() { next({type: 'releaseall'}); }
361 };
362 };
363
364 KeyboardUtil.TrackQEMUKeyState = function(next) {
365 "use strict";
366 var state = [];
367
368 return function (evt) {
369 var last = state.length !== 0 ? state[state.length-1] : null;
370
371 switch (evt.type) {
372 case 'keydown':
373
374 if (!last || last.code !== evt.code) {
375 last = {code: evt.code};
376
377 if (state.length > 0 && state[state.length-1].code == 'ControlLeft') {
378 if (evt.code !== 'AltRight') {
379 next({code: 'ControlLeft', type: 'keydown', keysym: 0});
380 } else {
381 state.pop();
382 }
383 }
384 state.push(last);
385 }
386 if (evt.code !== 'ControlLeft') {
387 next(evt);
388 }
389 break;
390
391 case 'keyup':
392 if (state.length === 0) {
393 return;
394 }
395 var idx = null;
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) {
399 idx = i;
400 break;
401 }
402 }
403 // if we couldn't find a match (it happens), assume it was the last key pressed
404 if (idx === null) {
405 if (evt.code === 'ControlLeft') {
406 return;
407 }
408 idx = state.length - 1;
409 }
410
411 state.splice(idx, 1);
412 next(evt);
413 break;
414 case 'releaseall':
415 /* jshint shadow: true */
416 for (var i = 0; i < state.length; ++i) {
417 next({code: state[i].code, keysym: 0, type: 'keyup'});
418 }
419 /* jshint shadow: false */
420 state = [];
421 }
422 };
423 };
424
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) {
433 "use strict";
434 function sendAll(evts) {
435 for (var i = 0; i < evts.length; ++i) {
436 next(evts[i]);
437 }
438 }
439 function process(evt, type) {
440 var result = {type: type};
441 var keyId = KeyboardUtil.getKey(evt);
442 if (keyId) {
443 result.keyId = keyId;
444 }
445
446 var keysym = KeyboardUtil.getKeysym(evt);
447
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;
454 }
455
456 var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
457
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));
462
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);
466
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();
469
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;
478 }
479 }
480 if (type === 'keypress' && !isCharModifier) {
481 result.escape = modifierState.activeCharModifier();
482 }
483 }
484
485 if (stall) {
486 // insert a fake "stall" event
487 next({type: 'stall'});
488 }
489 next(result);
490
491 return suppress;
492 }
493
494 return {
495 keydown: function(evt) {
496 sendAll(modifierState.keydown(evt));
497 return process(evt, 'keydown');
498 },
499 keypress: function(evt) {
500 return process(evt, 'keypress');
501 },
502 keyup: function(evt) {
503 sendAll(modifierState.keyup(evt));
504 return process(evt, 'keyup');
505 },
506 syncModifiers: function(evt) {
507 sendAll(modifierState.syncAny(evt));
508 },
509 releaseAll: function() { next({type: 'releaseall'}); }
510 };
511 };
512
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) {
520 "use strict";
521 var queue = [];
522 var timer = null;
523 function process() {
524 if (timer) {
525 return;
526 }
527
528 var delayProcess = function () {
529 clearTimeout(timer);
530 timer = null;
531 process();
532 };
533
534 while (queue.length !== 0) {
535 var cur = queue[0];
536 queue = queue.splice(1);
537 switch (cur.type) {
538 case 'stall':
539 // insert a delay before processing available events.
540 /* jshint loopfunc: true */
541 timer = setTimeout(delayProcess, 5);
542 /* jshint loopfunc: false */
543 return;
544 case 'keydown':
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;
552 }
553 cur.keysym = queue[0].keysym;
554 queue = queue.splice(1);
555 }
556 break;
557 }
558
559 // swallow stall events, and pass all others to the next stage
560 if (cur.type !== 'stall') {
561 next(cur);
562 }
563 }
564 }
565 return function(evt) {
566 queue.push(evt);
567 process();
568 };
569 };
570
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) {
577 "use strict";
578 var state = [];
579
580 return function (evt) {
581 var last = state.length !== 0 ? state[state.length-1] : null;
582
583 switch (evt.type) {
584 case 'keydown':
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: {}};
588 state.push(last);
589 }
590 if (evt.keysym) {
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;
595 next(evt);
596 }
597 break;
598 case 'keypress':
599 if (!last) {
600 last = {keyId: evt.keyId, keysyms: {}};
601 state.push(last);
602 }
603 if (!evt.keysym) {
604 console.log('keypress with no keysym:', evt);
605 }
606
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';
612 next(evt);
613 }
614 break;
615 case 'keyup':
616 if (state.length === 0) {
617 return;
618 }
619 var idx = null;
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) {
623 idx = i;
624 break;
625 }
626 }
627 // if we couldn't find a match (it happens), assume it was the last key pressed
628 if (idx === null) {
629 idx = state.length - 1;
630 }
631
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(){
635 function Clone(){}
636 return function (obj) { Clone.prototype=obj; return new Clone(); };
637 }());
638 for (var key in item.keysyms) {
639 var out = clone(evt);
640 out.keysym = item.keysyms[key];
641 next(out);
642 }
643 break;
644 case 'releaseall':
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'});
650 }
651 }
652 /* jshint shadow: false */
653 state = [];
654 }
655 };
656 };
657
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) {
661 "use strict";
662 return function(evt) {
663 if (evt.type !== 'keydown' || evt.escape === undefined) {
664 next(evt);
665 return;
666 }
667 // undo modifiers
668 for (var i = 0; i < evt.escape.length; ++i) {
669 next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
670 }
671 // send the character event
672 next(evt);
673 // redo modifiers
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])});
677 }
678 /* jshint shadow: false */
679 };
680 };
681
682 /* [module] export default KeyboardUtil; */