Home | History | Annotate | Download | only in include
      1 var 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)
    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 
    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
    439 function 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.
    523 function 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