• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1var kbdUtil = (function() {
2    "use strict";
3
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
20    function isMac() {
21        return navigator && !!(/mac/i).exec(navigator.platform);
22    }
23    function isWindows() {
24        return navigator && !!(/win/i).exec(navigator.platform);
25    }
26    function isLinux() {
27        return navigator && !!(/linux/i).exec(navigator.platform);
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) {
34            if (parseInt(key) !== 0xffe1) {
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){
155        if ('keyCode' in evt && 'key' in evt) {
156            return evt.key + ':' + evt.keyCode;
157        }
158        else if ('keyCode' in evt) {
159            return evt.keyCode;
160        }
161        else {
162            return evt.key;
163        }
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        }
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        }
180        if (codepoint) {
181            var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
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,
283        nonCharacterKey : nonCharacterKey,
284        substituteCodepoint : substituteCodepoint
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
391        var delayProcess = function () {
392            clearTimeout(timer);
393            timer = null;
394            process();
395        };
396
397        while (queue.length !== 0) {
398            var cur = queue[0];
399            queue = queue.splice(1);
400            switch (cur.type) {
401            case 'stall':
402                // insert a delay before processing available events.
403                /* jshint loopfunc: true */
404                timer = setTimeout(delayProcess, 5);
405                /* jshint loopfunc: false */
406                return;
407            case 'keydown':
408                // is the next element a keypress? Then we should merge the two
409                if (queue.length !== 0 && queue[0].type === 'keypress') {
410                    // Firefox sends keypress even when no char is generated.
411                    // so, if keypress keysym is the same as we'd have guessed from keydown,
412                    // the modifier didn't have any effect, and should not be escaped
413                    if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
414                        cur.escape = queue[0].escape;
415                    }
416                    cur.keysym = queue[0].keysym;
417                    queue = queue.splice(1);
418                }
419                break;
420            }
421
422            // swallow stall events, and pass all others to the next stage
423            if (cur.type !== 'stall') {
424                next(cur);
425            }
426        }
427    }
428    return function(evt) {
429        queue.push(evt);
430        process();
431    };
432}
433
434// Keeps track of which keys we (and the server) believe are down
435// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
436// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
437// key repeat events should be merged into a single entry.
438// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
439function TrackKeyState(next) {
440    "use strict";
441    var state = [];
442
443    return function (evt) {
444        var last = state.length !== 0 ? state[state.length-1] : null;
445
446        switch (evt.type) {
447        case 'keydown':
448            // insert a new entry if last seen key was different.
449            if (!last || !evt.keyId || last.keyId !== evt.keyId) {
450                last = {keyId: evt.keyId, keysyms: {}};
451                state.push(last);
452            }
453            if (evt.keysym) {
454                // make sure last event contains this keysym (a single "logical" keyevent
455                // can cause multiple key events to be sent to the VNC server)
456                last.keysyms[evt.keysym.keysym] = evt.keysym;
457                last.ignoreKeyPress = true;
458                next(evt);
459            }
460            break;
461        case 'keypress':
462            if (!last) {
463                last = {keyId: evt.keyId, keysyms: {}};
464                state.push(last);
465            }
466            if (!evt.keysym) {
467                console.log('keypress with no keysym:', evt);
468            }
469
470            // If we didn't expect a keypress, and already sent a keydown to the VNC server
471            // based on the keydown, make sure to skip this event.
472            if (evt.keysym && !last.ignoreKeyPress) {
473                last.keysyms[evt.keysym.keysym] = evt.keysym;
474                evt.type = 'keydown';
475                next(evt);
476            }
477            break;
478        case 'keyup':
479            if (state.length === 0) {
480                return;
481            }
482            var idx = null;
483            // do we have a matching key tracked as being down?
484            for (var i = 0; i !== state.length; ++i) {
485                if (state[i].keyId === evt.keyId) {
486                    idx = i;
487                    break;
488                }
489            }
490            // if we couldn't find a match (it happens), assume it was the last key pressed
491            if (idx === null) {
492                idx = state.length - 1;
493            }
494
495            var item = state.splice(idx, 1)[0];
496            // for each keysym tracked by this key entry, clone the current event and override the keysym
497            var clone = (function(){
498                function Clone(){}
499                return function (obj) { Clone.prototype=obj; return new Clone(); };
500            }());
501            for (var key in item.keysyms) {
502                var out = clone(evt);
503                out.keysym = item.keysyms[key];
504                next(out);
505            }
506            break;
507        case 'releaseall':
508            /* jshint shadow: true */
509            for (var i = 0; i < state.length; ++i) {
510                for (var key in state[i].keysyms) {
511                    var keysym = state[i].keysyms[key];
512                    next({keyId: 0, keysym: keysym, type: 'keyup'});
513                }
514            }
515            /* jshint shadow: false */
516            state = [];
517        }
518    };
519}
520
521// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
522// then the modifier must be "undone" before sending the @, and "redone" afterwards.
523function EscapeModifiers(next) {
524    "use strict";
525    return function(evt) {
526        if (evt.type !== 'keydown' || evt.escape === undefined) {
527            next(evt);
528            return;
529        }
530        // undo modifiers
531        for (var i = 0; i < evt.escape.length; ++i) {
532            next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
533        }
534        // send the character event
535        next(evt);
536        // redo modifiers
537        /* jshint shadow: true */
538        for (var i = 0; i < evt.escape.length; ++i) {
539            next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
540        }
541        /* jshint shadow: false */
542    };
543}
544