Standalone implementation of new keyboard handling
To run unit tests: $ cd noVNC/tests $ npm install mocha $ npm install chai <open keyboard-tests.html in a browser> To manually test: <open keytest.html in a browser> Design overview: The big complex browser-dependent keyboard input events are replaced by custom objects containing only the data we put there. These objects always have a 'keyId' property, which is mainly used to compare events, to determine if they represent the same key (in order to match keydown/keypress/keyup events for the same key together), and do not have a 'keysym' property until we know that the specified keysym should be sent to the VNC server. (In other words, even if we know which keysym a keypress represents, a 'keysym' property is not added to the object until we also know that the keysym should be sent as-is to the VNC server. Modifiers are divided into two groups: char modifiers and shortcut modifiers. The latter never have an effect in themselves, but an application may react to them to implement keyboard shortcuts (CTRL-C for example). The former *may* be used to transform the character they are combined with, and in those cases, the modifier press itself should not be sent to the VNC server. For example, AltGr on European keyboards can be combined with many, but not all, keys, to produce different characters. The new keyboard handling is effectively a pipeline where each stage performs a single well-defined transformation to clean up the input and eliminate browser quirks. It can easily be extended with new stages, but currently consists of the following: 1. KeyEventDecoder: takes the raw keydown/keyup/keypress event sent by the browser, and converts it to our custom event wrapper, adding some hints for later stages. If we get a keydown event, and we can't rely on getting a subsequent keypress with more accurate information, we also try to determine the keysym here. Also uses a ModifierSync object to keep track of the state of modifier keys, and insert fake events to compensate when the browser forgets to generate modifier key events. 2. VerifyCharModifier: When a char modifier is combined with another key, we need to know if this creates a new keypress (for example AltGr + 2 on a DK keyboard yields @), or if it has no effect. This stage handles this by "stalling" the pipeline in cases where this is uncertain, giving us time to see if a keypress event is coming. 3. TrackKeyState: since some browsers forget to send keyup events, we need to keep track of which keys are currently down. This also means we need to "collapse" keydown and keypress events into one: only one of them should result in an event sent to the VNC server. 4. EscapeModifiers: when a char modifier is used to generate a character (say, AltGr creating a @), we need to "undo" the modifier itself while sending the character - the VNC server should see the sequence keyup(AltGr), keydown(@), keydown(AltGr), keyup(@). the commenting!
This commit is contained in:
parent
b66ffcddcf
commit
24a6d8bbf6
|
|
@ -2,3 +2,4 @@
|
|||
*.o
|
||||
tests/data_*.js
|
||||
utils/rebind.so
|
||||
node_modules
|
||||
|
|
|
|||
|
|
@ -0,0 +1,253 @@
|
|||
function isMac() {
|
||||
return navigator && !!(/macintosh/i).exec(navigator.appVersion);
|
||||
}
|
||||
function isWindows() {
|
||||
return navigator && !!(/windows/i).exec(navigator.appVersion);
|
||||
}
|
||||
function isLinux() {
|
||||
return navigator && !!(/linux/i).exec(navigator.appVersion);
|
||||
}
|
||||
|
||||
// on Mac, Option (AKA Alt) is used as a char modifier
|
||||
var charModifierListMac = [0xffe9];
|
||||
// on Windows, Ctrl+Alt is used as a char modifier
|
||||
var charModifierListWin = [0xffe9, 0xffe3];
|
||||
// on Linux, AltGr is used as a char modifier
|
||||
var charModifierListLinux = [0xfe03]
|
||||
|
||||
// Return true if a modifier which is not the specified char modifier (and is not shift) is down
|
||||
function hasShortcutModifier(charModifier, currentModifiers) {
|
||||
var mods = {};
|
||||
for (var key in currentModifiers) {
|
||||
if (key != 0xffe1) {
|
||||
mods[key] = currentModifiers[key];
|
||||
}
|
||||
}
|
||||
|
||||
var sum = 0;
|
||||
for (var k in currentModifiers) {
|
||||
if (mods[k]) {
|
||||
++sum;
|
||||
}
|
||||
}
|
||||
if (hasCharModifier(charModifier, mods)) {
|
||||
return sum > charModifier.length;
|
||||
}
|
||||
else {
|
||||
return sum > 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Return true if the specified char modifier is currently down
|
||||
function hasCharModifier(charModifier, currentModifiers) {
|
||||
if (charModifier == []) { return false; }
|
||||
|
||||
for (var i = 0; i < charModifier.length; ++i) {
|
||||
if (!currentModifiers[charModifier[i]]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper object tracking modifier key state
|
||||
// and generates fake key events to compensate if it gets out of sync
|
||||
function ModifierSync(charModifier) {
|
||||
if (!charModifier) {
|
||||
if (isMac()) {
|
||||
charModifier = charModifierListMac;
|
||||
}
|
||||
else if (isWindows()) {
|
||||
charModifier = charModifierListWin;
|
||||
}
|
||||
else if (isLinux()) {
|
||||
charModifier = charModifierListLinux;
|
||||
}
|
||||
else {
|
||||
charModifier = [];
|
||||
}
|
||||
}
|
||||
var ctrl = 0xffe3;
|
||||
var alt = 0xffe9;
|
||||
var altGr = 0xfe03;
|
||||
var shift = 0xffe1;
|
||||
var meta = 0xffe7;
|
||||
|
||||
var state = {};
|
||||
state[ctrl] = false;
|
||||
state[alt] = false;
|
||||
state[altGr] = false;
|
||||
state[shift] = false;
|
||||
state[meta] = false;
|
||||
|
||||
|
||||
function sync(evt, keysym) {
|
||||
var result = [];
|
||||
function syncKey(keysym) {
|
||||
return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
|
||||
}
|
||||
|
||||
if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) {
|
||||
state[ctrl] = evt.ctrlKey;
|
||||
result.push(syncKey(ctrl));
|
||||
}
|
||||
if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) {
|
||||
state[alt] = evt.altKey;
|
||||
result.push(syncKey(alt));
|
||||
}
|
||||
if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) {
|
||||
state[altGr] = evt.altGraphKey;
|
||||
result.push(syncKey(altGr));
|
||||
}
|
||||
if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) {
|
||||
state[shift] = evt.shiftKey;
|
||||
result.push(syncKey(shift));
|
||||
}
|
||||
if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) {
|
||||
state[meta] = evt.metaKey;
|
||||
result.push(syncKey(meta));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function syncKeyEvent(evt, down) {
|
||||
var obj = getKeysym(evt);
|
||||
var keysym = obj ? obj.keysym : null;
|
||||
|
||||
// first, apply the event itself, if relevant
|
||||
if (keysym != null && state[keysym] !== undefined) {
|
||||
state[keysym] = down;
|
||||
}
|
||||
return sync(evt, keysym);
|
||||
}
|
||||
|
||||
return {
|
||||
// sync on the appropriate keyboard event
|
||||
keydown: function(evt) { return syncKeyEvent(evt, true);},
|
||||
keyup: function(evt) { return syncKeyEvent(evt, false);},
|
||||
// Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
|
||||
syncAny: function(evt) { return sync(evt);},
|
||||
|
||||
// is a shortcut modifier down?
|
||||
hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
|
||||
// if a char modifier is down, return the keys it consists of, otherwise return null
|
||||
activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
|
||||
};
|
||||
}
|
||||
|
||||
// Get a key ID from a keyboard event
|
||||
// May be a string or an integer depending on the available properties
|
||||
function getKey(evt){
|
||||
if (evt.key) {
|
||||
return evt.key;
|
||||
}
|
||||
else {
|
||||
return evt.keyCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Get the most reliable keysym value we can get from a key event
|
||||
// if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
|
||||
function getKeysym(evt){
|
||||
if (evt.char && evt.char.length === 1) {
|
||||
var codepoint = evt.char.charCodeAt();
|
||||
var res = keysyms.fromUnicode(codepoint);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
if (evt.charCode) {
|
||||
var res = keysyms.fromUnicode(evt.charCode);
|
||||
if (res) {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
// we could check evt.key here.
|
||||
// Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
|
||||
// so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
|
||||
// so we don't *need* it yet
|
||||
if (evt.keyCode) {
|
||||
return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
|
||||
}
|
||||
if (evt.which) {
|
||||
return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Given a keycode, try to predict which keysym it might be.
|
||||
// If the keycode is unknown, null is returned.
|
||||
function keysymFromKeyCode(keycode, shiftPressed) {
|
||||
if (typeof(keycode) !== 'number') {
|
||||
return null;
|
||||
}
|
||||
// won't be accurate for azerty
|
||||
if (keycode >= 0x30 && keycode <= 0x39) {
|
||||
return keycode; // digit
|
||||
}
|
||||
if (keycode >= 0x41 && keycode <= 0x5a) {
|
||||
// remap to lowercase unless shift is down
|
||||
return shiftPressed ? keycode : keycode + 32; // A-Z
|
||||
}
|
||||
if (keycode >= 0x60 && keycode <= 0x69) {
|
||||
return 0xffb0 + (keycode - 0x60); // numpad 0-9
|
||||
}
|
||||
|
||||
switch(keycode) {
|
||||
case 0x20: return 0x20; // space
|
||||
case 0x6a: return 0xffaa; // multiply
|
||||
case 0x6b: return 0xffab; // add
|
||||
case 0x6c: return 0xffac; // separator
|
||||
case 0x6d: return 0xffad; // subtract
|
||||
case 0x6e: return 0xffae; // decimal
|
||||
case 0x6f: return 0xffaf; // divide
|
||||
case 0xbb: return 0x2b; // +
|
||||
case 0xbc: return 0x2c; // ,
|
||||
case 0xbd: return 0x2d; // -
|
||||
case 0xbe: return 0x2e; // .
|
||||
}
|
||||
|
||||
return nonCharacterKey({keyCode: keycode});
|
||||
}
|
||||
|
||||
// if the key is a known non-character key (any key which doesn't generate character data)
|
||||
// return its keysym value. Otherwise return null
|
||||
function nonCharacterKey(evt) {
|
||||
// evt.key not implemented yet
|
||||
if (!evt.keyCode) { return null; }
|
||||
var keycode = evt.keyCode;
|
||||
|
||||
if (keycode >= 0x70 && keycode <= 0x87) {
|
||||
return 0xffbe + keycode - 0x70; // F1-F24
|
||||
}
|
||||
switch (keycode) {
|
||||
|
||||
case 8 : return 0xFF08; // BACKSPACE
|
||||
case 13 : return 0xFF0D; // ENTER
|
||||
|
||||
case 9 : return 0xFF09; // TAB
|
||||
|
||||
case 27 : return 0xFF1B; // ESCAPE
|
||||
case 46 : return 0xFFFF; // DELETE
|
||||
|
||||
case 36 : return 0xFF50; // HOME
|
||||
case 35 : return 0xFF57; // END
|
||||
case 33 : return 0xFF55; // PAGE_UP
|
||||
case 34 : return 0xFF56; // PAGE_DOWN
|
||||
case 45 : return 0xFF63; // INSERT
|
||||
|
||||
case 37 : return 0xFF51; // LEFT
|
||||
case 38 : return 0xFF52; // UP
|
||||
case 39 : return 0xFF53; // RIGHT
|
||||
case 40 : return 0xFF54; // DOWN
|
||||
case 16 : return 0xFFE1; // SHIFT
|
||||
case 17 : return 0xFFE3; // CONTROL
|
||||
case 18 : return 0xFFE9; // Left ALT (Mac Option)
|
||||
|
||||
case 224 : return 0xFE07; // Meta
|
||||
case 225 : return 0xFE03; // AltGr
|
||||
case 91 : return 0xFFEC; // Super_L (Win Key)
|
||||
case 92 : return 0xFFED; // Super_R (Win Key)
|
||||
case 93 : return 0xFF67; // Menu (Win Menu), Mac Command
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
// Takes a DOM keyboard event and:
|
||||
// - determines which keysym it represents
|
||||
// - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
|
||||
// - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
|
||||
// - marks each event with an 'escape' property if a modifier was down which should be "escaped"
|
||||
// - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
|
||||
// This information is collected into an object which is passed to the next() function. (one call per event)
|
||||
function KeyEventDecoder(modifierState, next) {
|
||||
function sendAll(evts) {
|
||||
for (var i = 0; i < evts.length; ++i) {
|
||||
next(evts[i]);
|
||||
}
|
||||
}
|
||||
function process(evt, type) {
|
||||
var result = {type: type};
|
||||
var keyId = getKey(evt);
|
||||
if (keyId) {
|
||||
result['keyId'] = keyId;
|
||||
}
|
||||
|
||||
var keysym = getKeysym(evt);
|
||||
|
||||
var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
|
||||
// Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
|
||||
// "special" keys like enter, tab or backspace don't send keypress events,
|
||||
// and some browsers don't send keypresses at all if a modifier is down
|
||||
if (keysym && (type !== 'keydown' || nonCharacterKey(evt) || hasModifier)) {
|
||||
result['keysym'] = keysym;
|
||||
}
|
||||
|
||||
var isShift = evt.keyCode == 0x10 || evt.key == 'Shift';
|
||||
|
||||
// Should we prevent the browser from handling the event?
|
||||
// Doing so on a keydown (in most browsers) prevents keypress from being generated
|
||||
// so only do that if we have to.
|
||||
var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!nonCharacterKey(evt));
|
||||
|
||||
// If a char modifier is down on a keydown, we need to insert a stall,
|
||||
// so VerifyCharModifier knows to wait and see if a keypress is comnig
|
||||
var stall = type === 'keydown' && modifierState.activeCharModifier() && !nonCharacterKey(evt);
|
||||
|
||||
// if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
|
||||
var active = modifierState.activeCharModifier();
|
||||
|
||||
// If we have a char modifier down, and we're able to determine a keysym reliably
|
||||
// then (a) we know to treat the modifier as a char modifier,
|
||||
// and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
|
||||
if (active && keysym) {
|
||||
var isCharModifier = false;
|
||||
for (var i = 0; i < active.length; ++i) {
|
||||
if (active[i] == keysym.keysym) {
|
||||
isCharModifier = true;
|
||||
}
|
||||
}
|
||||
if (type == 'keypress' && !isCharModifier) {
|
||||
result.escape = modifierState.activeCharModifier();
|
||||
}
|
||||
}
|
||||
|
||||
if (stall) {
|
||||
// insert a fake "stall" event
|
||||
next({type: 'stall'});
|
||||
}
|
||||
next(result);
|
||||
|
||||
return suppress;
|
||||
}
|
||||
|
||||
return {
|
||||
keydown: function(evt) {
|
||||
sendAll(modifierState.keydown(evt));
|
||||
return process(evt, 'keydown');
|
||||
},
|
||||
keypress: function(evt) {
|
||||
return process(evt, 'keypress');
|
||||
},
|
||||
keyup: function(evt) {
|
||||
sendAll(modifierState.keyup(evt));
|
||||
return process(evt, 'keyup');
|
||||
},
|
||||
syncModifiers: function(evt) {
|
||||
sendAll(modifierState.syncAny(evt));
|
||||
},
|
||||
releaseAll: function() { next({type: 'releaseall'}); }
|
||||
};
|
||||
}
|
||||
|
||||
// Combines keydown and keypress events where necessary to handle char modifiers.
|
||||
// On some OS'es, a char modifier is sometimes used as a shortcut modifier.
|
||||
// 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
|
||||
// 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.
|
||||
// The only way we can distinguish these cases is to wait and see if a keypress event arrives
|
||||
// When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
|
||||
function VerifyCharModifier(next) {
|
||||
var queue = [];
|
||||
var timer = null;
|
||||
function process() {
|
||||
if (timer) {
|
||||
return;
|
||||
}
|
||||
while (queue.length != 0) {
|
||||
var cur = queue[0];
|
||||
queue = queue.splice(1);
|
||||
switch (cur.type) {
|
||||
case 'stall':
|
||||
// insert a delay before processing available events.
|
||||
timer = setTimeout(function() {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
process();
|
||||
}, 5);
|
||||
return;
|
||||
case 'keydown':
|
||||
// is the next element a keypress? Then we should merge the two
|
||||
if (queue.length != 0 && queue[0].type == 'keypress') {
|
||||
// Firefox sends keypress even when no char is generated.
|
||||
// so, if keypress keysym is the same as we'd have guessed from keydown,
|
||||
// the modifier didn't have any effect, and should not be escaped
|
||||
if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
|
||||
cur.escape = queue[0].escape;
|
||||
}
|
||||
cur.keysym = queue[0].keysym;
|
||||
queue = queue.splice(1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// swallow stall events, and pass all others to the next stage
|
||||
if (cur.type !== 'stall') {
|
||||
next(cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
return function(evt) {
|
||||
queue.push(evt);
|
||||
process();
|
||||
};
|
||||
}
|
||||
|
||||
// Keeps track of which keys we (and the server) believe are down
|
||||
// When a keyup is received, match it against this list, to determine the corresponding keysym(s)
|
||||
// in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
|
||||
// key repeat events should be merged into a single entry.
|
||||
// Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
|
||||
function TrackKeyState(next) {
|
||||
var state = [];
|
||||
|
||||
return function (evt) {
|
||||
var last = state.length != 0 ? state[state.length-1] : null;
|
||||
|
||||
switch (evt.type) {
|
||||
case 'keydown':
|
||||
// insert a new entry if last seen key was different.
|
||||
if (!last || !evt.keyId || last.keyId != evt.keyId) {
|
||||
last = {keyId: evt.keyId, keysyms: {}};
|
||||
state.push(last);
|
||||
}
|
||||
if (evt.keysym) {
|
||||
// make sure last event contains this keysym (a single "logical" keyevent
|
||||
// can cause multiple key events to be sent to the VNC server)
|
||||
last.keysyms[evt.keysym.keysym] = evt.keysym;
|
||||
last.ignoreKeyPress = true;
|
||||
next(evt);
|
||||
}
|
||||
break;
|
||||
case 'keypress':
|
||||
if (!last) {
|
||||
last = {keyId: evt.keyId, keysyms: {}};
|
||||
state.push(last);
|
||||
}
|
||||
if (!evt.keysym) {
|
||||
console.log('keypress with no keysym:', evt);
|
||||
}
|
||||
|
||||
// If we didn't expect a keypress, and already sent a keydown to the VNC server
|
||||
// based on the keydown, make sure to skip this event.
|
||||
if (evt.keysym && !last.ignoreKeyPress) {
|
||||
last.keysyms[evt.keysym.keysym] = evt.keysym;
|
||||
evt.type = 'keydown';
|
||||
next(evt);
|
||||
}
|
||||
break;
|
||||
case 'keyup':
|
||||
if (state.length == 0) {
|
||||
return;
|
||||
}
|
||||
var idx = null;
|
||||
// do we have a matching key tracked as being down?
|
||||
for (var i = 0; i != state.length; ++i) {
|
||||
if (state[i].keyId === evt.keyId) {
|
||||
idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// if we couldn't find a match (it happens), assume it was the last key pressed
|
||||
if (idx === null) {
|
||||
idx = state.length - 1;
|
||||
}
|
||||
|
||||
var item = state.splice(idx, 1)[0];
|
||||
// for each keysym tracked by this key entry, clone the current event and override the keysym
|
||||
for (var key in item.keysyms) {
|
||||
var clone = (function(){
|
||||
return function (obj) { Clone.prototype=obj; return new Clone() };
|
||||
function Clone(){}
|
||||
}());
|
||||
var out = clone(evt);
|
||||
out.keysym = item.keysyms[key];
|
||||
next(out);
|
||||
}
|
||||
break;
|
||||
case 'releaseall':
|
||||
for (var i = 0; i < state.length; ++i) {
|
||||
for (var key in state[i].keysyms) {
|
||||
var keysym = state[i].keysyms[key];
|
||||
next({keyId: 0, keysym: keysym, type: 'keyup'});
|
||||
}
|
||||
}
|
||||
state = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
|
||||
// then the modifier must be "undone" before sending the @, and "redone" afterwards.
|
||||
function EscapeModifiers(next) {
|
||||
return function(evt) {
|
||||
if (evt.type != 'keydown' || evt.escape === undefined) {
|
||||
next(evt);
|
||||
return;
|
||||
}
|
||||
// undo modifiers
|
||||
for (var i = 0; i < evt.escape.length; ++i) {
|
||||
next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
|
||||
}
|
||||
// send the character event
|
||||
next(evt);
|
||||
// redo modifiers
|
||||
for (var i = 0; i < evt.escape.length; ++i) {
|
||||
next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,61 @@
|
|||
fs = require('fs');
|
||||
|
||||
// Set this to false to omit key names from the generated keysymdef.js
|
||||
// This reduces the file size by around 40kb, but may hinder debugging
|
||||
var USE_KEYNAMES=true;
|
||||
|
||||
var buf = fs.readFileSync("keysymdef.h");
|
||||
var str = buf.toString('utf8');
|
||||
|
||||
var re = /^\#define XK_([a-zA-Z_0-9]+)\s+0x([0-9a-fA-F]+)\s*(\/\*\s*(.*)\s*\*\/)?\s*$/m;
|
||||
|
||||
var arr = str.split('\n');
|
||||
|
||||
var keysyms = {}
|
||||
var codepoints = {}
|
||||
|
||||
for (var i = 0; i < arr.length; ++i) {
|
||||
//console.log(arr[i]);
|
||||
var result = re.exec(arr[i]);
|
||||
if (result){
|
||||
var keyname = result[1];
|
||||
var keysym = parseInt(result[2], 16);
|
||||
var remainder = result[3]
|
||||
|
||||
var val = {keyname: keyname, keysym: keysym};
|
||||
keysyms[keysym] = keyname;
|
||||
|
||||
var unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder);
|
||||
if (unicodeRes) {
|
||||
var unicode = parseInt(unicodeRes[1], 16);
|
||||
if (!codepoints[unicode]){
|
||||
codepoints[unicode] = keysym;
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log("no unicode codepoint found:", arr[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.log("line is not a keysym:", arr[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var out = "// This file describes mappings from Unicode codepoints to the keysym values\n" +
|
||||
"// (and optionally, key names) expected by the RFB protocol\n" +
|
||||
"// How this file was generated:\n" +
|
||||
"// Install node.js and run the command 'node parse.js'\n" +
|
||||
"var keysyms = (function(){\n" +
|
||||
" var keynames = {keysyms};\n" +
|
||||
" var codepoints = {codepoints};\n" +
|
||||
"\n" +
|
||||
" function lookup(k) { return k ? {keysym: k, keyname: keynames ? keynames[k] : k} : undefined; }\n" +
|
||||
" return {\n" +
|
||||
" fromUnicode : function(u) { return lookup(codepoints[u]); },\n" +
|
||||
" lookup : lookup\n" +
|
||||
" };\n" +
|
||||
"})();\n";
|
||||
out = out.replace('{keysyms}', USE_KEYNAMES ? JSON.stringify(keysyms) : "null");
|
||||
out = out.replace('{codepoints}', JSON.stringify(codepoints));
|
||||
|
||||
fs.writeFileSync("keysymdef.js", out);
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Mocha Tests</title>
|
||||
<link rel="stylesheet" href="node_modules/mocha/mocha.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!--
|
||||
Keyboard handling unit tests
|
||||
To run, I used npm (from node.js) to install the libraries chai and mocha in $(PWD)
|
||||
Installing the libraries in any other old way should work as well, provided you update the script paths below
|
||||
-->
|
||||
<div id="mocha"></div>
|
||||
<script src="node_modules/chai/chai.js"></script>
|
||||
<script src="node_modules/mocha/mocha.js"></script>
|
||||
<script>mocha.setup('bdd')</script>
|
||||
<script src="../src/keysymdef.js"></script>
|
||||
<script src="../src/keyboard.js"></script>
|
||||
<script src="../src/helper.js"></script>
|
||||
<script src="test.keyboard.js"></script>
|
||||
<script src="test.helper.js"></script>
|
||||
<script>
|
||||
mocha.checkLeaks();
|
||||
mocha.globals(['navigator']);
|
||||
mocha.run();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Keytest</title>
|
||||
</head>
|
||||
<body>
|
||||
<script src="../src/keysymdef.js"></script>
|
||||
<script src="../src/keyboard.js"></script>
|
||||
<script src="../src/helper.js"></script>
|
||||
<div><em>Type some stuff and see what happens</em></div>
|
||||
<input id="clear" type="button" value="Clear" />
|
||||
<div style="width: 100%; height: 600px; border-style:solid; border-width: 1px;"><pre id="log" /></div>
|
||||
|
||||
<script>
|
||||
var elem = document.getElementById('log');
|
||||
var textProp = (elem.textContent !== undefined ? "textContent" : "innerText");
|
||||
function write() {
|
||||
//console.log(arguments);
|
||||
// for each in args, jsonify and print
|
||||
for (var i = 0; i < arguments.length; ++i) {
|
||||
var val = typeof(arguments[i]) === 'string' ? arguments[i] : JSON.stringify(arguments[i]);
|
||||
elem[textProp] += val + ", ";
|
||||
}
|
||||
if (arguments.length != 0) {
|
||||
elem[textProp] += '\n';
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('clear').onclick = function() { elem[textProp] = "";}
|
||||
|
||||
|
||||
var k = KeyEventDecoder(ModifierSync(),
|
||||
VerifyCharModifier(
|
||||
TrackKeyState(
|
||||
EscapeModifiers(function(evt) {
|
||||
write(evt.type, evt.keysym.keyname);
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
function suppress(evt) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
if (window.event) {
|
||||
window.event.cancelBubble = true;
|
||||
}
|
||||
//return false;
|
||||
}
|
||||
|
||||
window.onkeydown = function(evt) {
|
||||
if (k.keydown(evt)) { suppress(evt); }
|
||||
};
|
||||
window.onkeypress = function(evt) {
|
||||
if (k.keypress(evt)) { suppress(evt); }
|
||||
};
|
||||
window.onkeyup = function(evt) {
|
||||
if (k.keyup(evt)) { suppress(evt); }
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,236 @@
|
|||
var assert = chai.assert;
|
||||
var expect = chai.expect;
|
||||
|
||||
describe('Helpers', function() {
|
||||
describe('keysymFromKeyCode', function() {
|
||||
it('should map known keycodes to keysyms', function() {
|
||||
expect(keysymFromKeyCode(0x41, false), 'a').to.be.equal(0x61);
|
||||
expect(keysymFromKeyCode(0x41, true), 'A').to.be.equal(0x41);
|
||||
expect(keysymFromKeyCode(0xd, false), 'enter').to.be.equal(0xFF0D);
|
||||
expect(keysymFromKeyCode(0x11, false), 'ctrl').to.be.equal(0xFFE3);
|
||||
expect(keysymFromKeyCode(0x12, false), 'alt').to.be.equal(0xFFE9);
|
||||
expect(keysymFromKeyCode(0xe1, false), 'altgr').to.be.equal(0xFE03);
|
||||
expect(keysymFromKeyCode(0x1b, false), 'esc').to.be.equal(0xFF1B);
|
||||
expect(keysymFromKeyCode(0x26, false), 'up').to.be.equal(0xFF52);
|
||||
});
|
||||
it('should return null for unknown keycodes', function() {
|
||||
expect(keysymFromKeyCode(0xc0, false), 'DK æ').to.be.null;
|
||||
expect(keysymFromKeyCode(0xde, false), 'DK ø').to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('keysyms.fromUnicode', function() {
|
||||
it('should map ASCII characters to keysyms', function() {
|
||||
expect(keysyms.fromUnicode('a'.charCodeAt())).to.have.property('keysym', 0x61);
|
||||
expect(keysyms.fromUnicode('A'.charCodeAt())).to.have.property('keysym', 0x41);
|
||||
});
|
||||
it('should map Latin-1 characters to keysyms', function() {
|
||||
expect(keysyms.fromUnicode('ø'.charCodeAt())).to.have.property('keysym', 0xf8);
|
||||
|
||||
expect(keysyms.fromUnicode('é'.charCodeAt())).to.have.property('keysym', 0xe9);
|
||||
});
|
||||
it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function() {
|
||||
expect(keysyms.fromUnicode('Š'.charCodeAt())).to.have.property('keysym', 0x01a9);
|
||||
});
|
||||
it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function() {
|
||||
expect(keysyms.fromUnicode('ŵ'.charCodeAt())).to.have.property('keysym', 0x1000175);
|
||||
});
|
||||
it('should return undefined for unknown codepoints', function() {
|
||||
expect(keysyms.fromUnicode('\n'.charCodeAt())).to.be.undefined;
|
||||
expect(keysyms.fromUnicode('\u1F686'.charCodeAt())).to.be.undefined;
|
||||
});
|
||||
});
|
||||
|
||||
describe('nonCharacterKey', function() {
|
||||
it('should recognize the right keys', function() {
|
||||
expect(nonCharacterKey({keyCode: 0xd}), 'enter').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0x08}), 'backspace').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0x09}), 'tab').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0x10}), 'shift').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0x11}), 'ctrl').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0x12}), 'alt').to.be.defined;
|
||||
expect(nonCharacterKey({keyCode: 0xe0}), 'meta').to.be.defined;
|
||||
});
|
||||
it('should not recognize character keys', function() {
|
||||
expect(nonCharacterKey({keyCode: 'A'}), 'A').to.be.null;
|
||||
expect(nonCharacterKey({keyCode: '1'}), '1').to.be.null;
|
||||
expect(nonCharacterKey({keyCode: '.'}), '.').to.be.null;
|
||||
expect(nonCharacterKey({keyCode: ' '}), 'space').to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getKeysym', function() {
|
||||
it('should prefer char', function() {
|
||||
expect(getKeysym({char : 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x61);
|
||||
});
|
||||
it('should use charCode if no char', function() {
|
||||
expect(getKeysym({char : '', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||
expect(getKeysym({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||
expect(getKeysym({char : 'hello', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.have.property('keysym', 0x01a9);
|
||||
});
|
||||
it('should use keyCode if no charCode', function() {
|
||||
expect(getKeysym({keyCode: 0x42, which: 0x43, shiftKey: false})).to.have.property('keysym', 0x62);
|
||||
expect(getKeysym({keyCode: 0x42, which: 0x43, shiftKey: true})).to.have.property('keysym', 0x42);
|
||||
});
|
||||
it('should use which if no keyCode', function() {
|
||||
expect(getKeysym({which: 0x43, shiftKey: false})).to.have.property('keysym', 0x63);
|
||||
expect(getKeysym({which: 0x43, shiftKey: true})).to.have.property('keysym', 0x43);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Modifier Sync', function() { // return a list of fake events necessary to fix modifier state
|
||||
describe('Toggle all modifiers', function() {
|
||||
var sync = ModifierSync();
|
||||
it ('should do nothing if all modifiers are up as expected', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: false,
|
||||
altKey: false,
|
||||
altGraphKey: false,
|
||||
shiftKey: false,
|
||||
metaKey: false})
|
||||
).to.have.lengthOf(0);
|
||||
});
|
||||
it ('should synthesize events if all keys are unexpectedly down', function() {
|
||||
var result = sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
altGraphKey: true,
|
||||
shiftKey: true,
|
||||
metaKey: true
|
||||
});
|
||||
expect(result).to.have.lengthOf(5);
|
||||
var keysyms = {};
|
||||
for (var i = 0; i < result.length; ++i) {
|
||||
keysyms[result[i].keysym] = (result[i].type == 'keydown');
|
||||
}
|
||||
expect(keysyms[0xffe3]);
|
||||
expect(keysyms[0xffe9]);
|
||||
expect(keysyms[0xfe03]);
|
||||
expect(keysyms[0xffe1]);
|
||||
expect(keysyms[0xffe7]);
|
||||
});
|
||||
it ('should do nothing if all modifiers are down as expected', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: true,
|
||||
altKey: true,
|
||||
altGraphKey: true,
|
||||
shiftKey: true,
|
||||
metaKey: true
|
||||
})).to.have.lengthOf(0);
|
||||
});
|
||||
});
|
||||
describe('Toggle Ctrl', function() {
|
||||
var sync = ModifierSync();
|
||||
it('should sync if modifier is suddenly down', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: true,
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keydown'}]);
|
||||
});
|
||||
it('should sync if modifier is suddenly up', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: false
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe3), type: 'keyup'}]);
|
||||
});
|
||||
});
|
||||
describe('Toggle Alt', function() {
|
||||
var sync = ModifierSync();
|
||||
it('should sync if modifier is suddenly down', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
altKey: true,
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||
});
|
||||
it('should sync if modifier is suddenly up', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
altKey: false
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keyup'}]);
|
||||
});
|
||||
});
|
||||
describe('Toggle AltGr', function() {
|
||||
var sync = ModifierSync();
|
||||
it('should sync if modifier is suddenly down', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
altGraphKey: true,
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keydown'}]);
|
||||
});
|
||||
it('should sync if modifier is suddenly up', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
altGraphKey: false
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xfe03), type: 'keyup'}]);
|
||||
});
|
||||
});
|
||||
describe('Toggle Shift', function() {
|
||||
var sync = ModifierSync();
|
||||
it('should sync if modifier is suddenly down', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
shiftKey: true,
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keydown'}]);
|
||||
});
|
||||
it('should sync if modifier is suddenly up', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
shiftKey: false
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe1), type: 'keyup'}]);
|
||||
});
|
||||
});
|
||||
describe('Toggle Meta', function() {
|
||||
var sync = ModifierSync();
|
||||
it('should sync if modifier is suddenly down', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
metaKey: true,
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keydown'}]);
|
||||
});
|
||||
it('should sync if modifier is suddenly up', function() {
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
metaKey: false
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe7), type: 'keyup'}]);
|
||||
});
|
||||
});
|
||||
describe('Modifier keyevents', function() {
|
||||
it('should not sync a modifier on its own events', function() {
|
||||
expect(ModifierSync().keydown({
|
||||
keyCode: 0x11,
|
||||
ctrlKey: false
|
||||
})).to.be.deep.equal([]);
|
||||
expect(ModifierSync().keydown({
|
||||
keyCode: 0x11,
|
||||
ctrlKey: true
|
||||
}), 'B').to.be.deep.equal([]);
|
||||
})
|
||||
it('should update state on modifier keyevents', function() {
|
||||
var sync = ModifierSync();
|
||||
sync.keydown({
|
||||
keyCode: 0x11,
|
||||
});
|
||||
expect(sync.keydown({
|
||||
keyCode: 0x41,
|
||||
ctrlKey: true,
|
||||
})).to.be.deep.equal([]);
|
||||
});
|
||||
it('should sync other modifiers on ctrl events', function() {
|
||||
expect(ModifierSync().keydown({
|
||||
keyCode: 0x11,
|
||||
altKey: true
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||
})
|
||||
});
|
||||
describe('Sync on non-key events', function() {
|
||||
it('should generate sync events when receiving non-keyboard events', function() {
|
||||
expect(ModifierSync().syncAny({
|
||||
altKey: true
|
||||
})).to.be.deep.equal([{keysym: keysyms.lookup(0xffe9), type: 'keydown'}]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,840 @@
|
|||
var assert = chai.assert;
|
||||
var expect = chai.expect;
|
||||
|
||||
|
||||
describe('Key Event Pipeline Stages', function() {
|
||||
describe('Decode Keyboard Events', function() {
|
||||
it('should pass events to the next stage', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.an.object;
|
||||
done();
|
||||
}).keydown({keyCode: 0x41});
|
||||
});
|
||||
it('should pass the right keysym through', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt.keysym).to.be.deep.equal(keysyms.lookup(0x61));
|
||||
done();
|
||||
}).keypress({keyCode: 0x41});
|
||||
});
|
||||
it('should pass the right keyid through', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.have.property('keyId', 0x41);
|
||||
done();
|
||||
}).keydown({keyCode: 0x41});
|
||||
});
|
||||
it('should not sync modifiers on a keypress', function() {
|
||||
// Firefox provides unreliable modifier state on keypress events
|
||||
var count = 0;
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
++count;
|
||||
}).keypress({keyCode: 0x41, ctrlKey: true});
|
||||
expect(count).to.be.equal(1);
|
||||
});
|
||||
it('should sync modifiers if necessary', function(done) {
|
||||
var count = 0;
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
switch (count) {
|
||||
case 0: // fake a ctrl keydown
|
||||
expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xffe3), type: 'keydown'});
|
||||
++count;
|
||||
break;
|
||||
case 1:
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown', keysym: keysyms.lookup(0x61)});
|
||||
done();
|
||||
break;
|
||||
}
|
||||
}).keydown({keyCode: 0x41, ctrlKey: true});
|
||||
});
|
||||
it('should forward keydown events with the right type', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
|
||||
done();
|
||||
}).keydown({keyCode: 0x41})
|
||||
});
|
||||
it('should forward keyup events with the right type', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
|
||||
done();
|
||||
}).keyup({keyCode: 0x41});
|
||||
});
|
||||
it('should forward keypress events with the right type', function(done) {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
|
||||
done();
|
||||
}).keypress({keyCode: 0x41});
|
||||
});
|
||||
it('should generate stalls if a char modifier is down while a key is pressed', function(done) {
|
||||
var count = 0;
|
||||
KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||
switch (count) {
|
||||
case 0: // fake altgr
|
||||
expect(evt).to.be.deep.equal({keysym: keysyms.lookup(0xfe03), type: 'keydown'});
|
||||
++count;
|
||||
break;
|
||||
case 1: // stall before processing the 'a' keydown
|
||||
expect(evt).to.be.deep.equal({type: 'stall'});
|
||||
++count;
|
||||
break;
|
||||
case 2: // 'a'
|
||||
expect(evt).to.be.deep.equal({
|
||||
type: 'keydown',
|
||||
keyId: 0x41,
|
||||
keysym: keysyms.lookup(0x61)
|
||||
});
|
||||
|
||||
done();
|
||||
break;
|
||||
}
|
||||
}).keydown({keyCode: 0x41, altGraphKey: true});
|
||||
|
||||
});
|
||||
describe('suppress the right events at the right time', function() {
|
||||
it('should suppress anything while a shortcut modifier is down', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||
|
||||
obj.keydown({keyCode: 0x11}); // press ctrl
|
||||
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.true;
|
||||
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.true;
|
||||
expect(obj.keydown({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||
expect(obj.keydown({keyCode: 0xde})).to.be.true; // Ø key on DK
|
||||
});
|
||||
it('should suppress non-character keys', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||
|
||||
expect(obj.keydown({keyCode: 0x08}), 'a').to.be.true;
|
||||
expect(obj.keydown({keyCode: 0x09}), 'b').to.be.true;
|
||||
expect(obj.keydown({keyCode: 0x11}), 'd').to.be.true;
|
||||
expect(obj.keydown({keyCode: 0x12}), 'e').to.be.true;
|
||||
});
|
||||
it('should not suppress shift', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||
|
||||
expect(obj.keydown({keyCode: 0x10}), 'd').to.be.false;
|
||||
});
|
||||
it('should generate event for shift keydown', function() {
|
||||
var called = false;
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.have.property('keysym');
|
||||
called = true;
|
||||
}).keydown({keyCode: 0x10});
|
||||
expect(called).to.be.true;
|
||||
});
|
||||
it('should not suppress character keys', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||
|
||||
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
|
||||
expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
|
||||
});
|
||||
it('should not suppress if a char modifier is down', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {});
|
||||
|
||||
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||
expect(obj.keydown({keyCode: 'A'.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: ' '.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: '1'.charCodeAt()})).to.be.false;
|
||||
expect(obj.keydown({keyCode: 0x3c})).to.be.false; // < key on DK Windows
|
||||
expect(obj.keydown({keyCode: 0xde})).to.be.false; // Ø key on DK
|
||||
});
|
||||
});
|
||||
describe('Keypress and keyup events', function() {
|
||||
it('should always suppress event propagation', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {});
|
||||
|
||||
expect(obj.keypress({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||
expect(obj.keypress({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||
expect(obj.keypress({keyCode: 0x11})).to.be.true;
|
||||
|
||||
expect(obj.keyup({keyCode: 'A'.charCodeAt()})).to.be.true;
|
||||
expect(obj.keyup({keyCode: 0x3c})).to.be.true; // < key on DK Windows
|
||||
expect(obj.keyup({keyCode: 0x11})).to.be.true;
|
||||
});
|
||||
it('should never generate stalls', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt.type).to.not.be.equal('stall');
|
||||
});
|
||||
|
||||
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||
obj.keypress({keyCode: 0x3c});
|
||||
obj.keypress({keyCode: 0x11});
|
||||
|
||||
obj.keyup({keyCode: 'A'.charCodeAt()});
|
||||
obj.keyup({keyCode: 0x3c});
|
||||
obj.keyup({keyCode: 0x11});
|
||||
});
|
||||
});
|
||||
describe('mark events if a char modifier is down', function() {
|
||||
it('should not mark modifiers on a keydown event', function() {
|
||||
var times_called = 0;
|
||||
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0: //altgr
|
||||
break;
|
||||
case 1: // 'a'
|
||||
expect(evt).to.not.have.property('escape');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||
obj.keydown({keyCode: 'A'.charCodeAt()});
|
||||
});
|
||||
|
||||
it('should indicate on events if a single-key char modifier is down', function(done) {
|
||||
var times_called = 0;
|
||||
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0: //altgr
|
||||
break;
|
||||
case 1: // 'a'
|
||||
expect(evt).to.be.deep.equal({
|
||||
type: 'keypress',
|
||||
keyId: 'A'.charCodeAt(),
|
||||
keysym: keysyms.lookup('a'.charCodeAt()),
|
||||
escape: [0xfe03]
|
||||
});
|
||||
done();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||
});
|
||||
it('should indicate on events if a multi-key char modifier is down', function(done) {
|
||||
var times_called = 0;
|
||||
var obj = KeyEventDecoder(ModifierSync([0xffe9, 0xffe3]), function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0: //ctrl
|
||||
break;
|
||||
case 1: //alt
|
||||
break;
|
||||
case 2: // 'a'
|
||||
expect(evt).to.be.deep.equal({
|
||||
type: 'keypress',
|
||||
keyId: 'A'.charCodeAt(),
|
||||
keysym: keysyms.lookup('a'.charCodeAt()),
|
||||
escape: [0xffe9, 0xffe3]
|
||||
});
|
||||
done();
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
obj.keydown({keyCode: 0x11}); // press ctrl
|
||||
obj.keydown({keyCode: 0x12}); // press alt
|
||||
obj.keypress({keyCode: 'A'.charCodeAt()});
|
||||
});
|
||||
it('should not consider a char modifier to be down on the modifier key itself', function() {
|
||||
var obj = KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||
expect(evt).to.not.have.property('escape');
|
||||
});
|
||||
|
||||
obj.keydown({keyCode: 0xe1}); // press altgr
|
||||
|
||||
});
|
||||
});
|
||||
describe('add/remove keysym', function() {
|
||||
it('should remove keysym from keydown if a char key and no modifier', function() {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, type: 'keydown'});
|
||||
}).keydown({keyCode: 0x41});
|
||||
});
|
||||
it('should not remove keysym from keydown if a shortcut modifier is down', function() {
|
||||
var times_called = 0;
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
switch (times_called++) {
|
||||
case 1:
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
|
||||
break;
|
||||
}
|
||||
}).keydown({keyCode: 0x41, ctrlKey: true});
|
||||
expect(times_called).to.be.equal(2);
|
||||
});
|
||||
it('should not remove keysym from keydown if a char modifier is down', function() {
|
||||
var times_called = 0;
|
||||
KeyEventDecoder(ModifierSync([0xfe03]), function(evt) {
|
||||
switch (times_called++) {
|
||||
case 2:
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keydown'});
|
||||
break;
|
||||
}
|
||||
}).keydown({keyCode: 0x41, altGraphKey: true});
|
||||
expect(times_called).to.be.equal(3);
|
||||
});
|
||||
it('should not remove keysym from keydown if key is noncharacter', function() {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt, 'bacobjpace').to.be.deep.equal({keyId: 0x09, keysym: keysyms.lookup(0xff09), type: 'keydown'});
|
||||
}).keydown({keyCode: 0x09});
|
||||
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt, 'ctrl').to.be.deep.equal({keyId: 0x11, keysym: keysyms.lookup(0xffe3), type: 'keydown'});
|
||||
}).keydown({keyCode: 0x11});
|
||||
});
|
||||
it('should never remove keysym from keypress', function() {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keypress'});
|
||||
}).keypress({keyCode: 0x41});
|
||||
});
|
||||
it('should never remove keysym from keyup', function() {
|
||||
KeyEventDecoder(ModifierSync(), function(evt) {
|
||||
expect(evt).to.be.deep.equal({keyId: 0x41, keysym: keysyms.lookup(0x61), type: 'keyup'});
|
||||
}).keyup({keyCode: 0x41});
|
||||
});
|
||||
});
|
||||
// on keypress, keyup(?), always set keysym
|
||||
// on keydown, only do it if we don't expect a keypress: if noncharacter OR modifier is down
|
||||
});
|
||||
|
||||
describe('Verify that char modifiers are active', function() {
|
||||
it('should pass keydown events through if there is no stall', function(done) {
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
done();
|
||||
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
});
|
||||
it('should pass keyup events through if there is no stall', function(done) {
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
expect(evt).to.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
done();
|
||||
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
});
|
||||
it('should pass keypress events through if there is no stall', function(done) {
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
expect(evt).to.deep.equal({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
done();
|
||||
})({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
});
|
||||
it('should not pass stall events through', function(done){
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
// should only be called once, for the keydown
|
||||
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
done();
|
||||
});
|
||||
|
||||
obj({type: 'stall'});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
});
|
||||
it('should merge keydown and keypress events if they come after a stall', function(done) {
|
||||
var next_called = false;
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
// should only be called once, for the keydown
|
||||
expect(next_called).to.be.false;
|
||||
next_called = true;
|
||||
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44)});
|
||||
done();
|
||||
});
|
||||
|
||||
obj({type: 'stall'});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||
expect(next_called).to.be.false;
|
||||
});
|
||||
it('should preserve modifier attribute when merging if keysyms differ', function(done) {
|
||||
var next_called = false;
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
// should only be called once, for the keydown
|
||||
expect(next_called).to.be.false;
|
||||
next_called = true;
|
||||
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
|
||||
done();
|
||||
});
|
||||
|
||||
obj({type: 'stall'});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44), escape: [0xffe3]});
|
||||
expect(next_called).to.be.false;
|
||||
});
|
||||
it('should not preserve modifier attribute when merging if keysyms are the same', function() {
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
expect(evt).to.not.have.property('escape');
|
||||
});
|
||||
|
||||
obj({type: 'stall'});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x42), escape: [0xffe3]});
|
||||
});
|
||||
it('should not merge keydown and keypress events if there is no stall', function(done) {
|
||||
var times_called = 0;
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
switch(times_called) {
|
||||
case 0:
|
||||
expect(evt).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
break;
|
||||
case 1:
|
||||
expect(evt).to.deep.equal({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||
done();
|
||||
break;
|
||||
}
|
||||
|
||||
++times_called;
|
||||
});
|
||||
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
obj({type: 'keypress', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||
});
|
||||
it('should not merge keydown and keypress events if separated by another event', function(done) {
|
||||
var times_called = 0;
|
||||
var obj = VerifyCharModifier(function(evt){
|
||||
switch(times_called) {
|
||||
case 0:
|
||||
expect(evt,1).to.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
break;
|
||||
case 1:
|
||||
expect(evt,2).to.deep.equal({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||
break;
|
||||
case 2:
|
||||
expect(evt,3).to.deep.equal({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
|
||||
done();
|
||||
break;
|
||||
}
|
||||
|
||||
++times_called;
|
||||
});
|
||||
|
||||
obj({type: 'stall'});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
obj({type: 'keyup', keyId: 0x43, keysym: keysyms.lookup(0x44)});
|
||||
obj({type: 'keypress', keyId: 0x45, keysym: keysyms.lookup(0x46)});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Track Key State', function() {
|
||||
it('should do nothing on keyup events if no keys are down', function() {
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
expect(true).to.be.false;
|
||||
});
|
||||
obj({type: 'keyup', keyId: 0x41});
|
||||
});
|
||||
it('should insert into the queue on keydown if no keys are down', function() {
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
}
|
||||
elem = null;
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0x41};
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
expect(times_called).to.be.equal(2);
|
||||
});
|
||||
it('should insert into the queue on keypress if no keys are down', function() {
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
}
|
||||
elem = null;
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0x41};
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
expect(times_called).to.be.equal(2);
|
||||
});
|
||||
it('should add keysym to last key entry if keyId matches', function() {
|
||||
// this implies that a single keyup will release both keysyms
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)};
|
||||
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0x41};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
it('should create new key entry if keyId matches and keysym does not', function() {
|
||||
// this implies that a single keyup will release both keysyms
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
|
||||
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(2);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(3);
|
||||
elem = {type: 'keyup', keyId: 0};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
it('should merge key entry if keyIds are zero and keysyms match', function() {
|
||||
// this implies that a single keyup will release both keysyms
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(2);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(3);
|
||||
});
|
||||
it('should add keysym as separate entry if keyId does not match last event', function() {
|
||||
// this implies that separate keyups are required
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keypress', keyId: 0x42, keysym: keysyms.lookup(0x43)};
|
||||
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0x41};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
elem = {type: 'keyup', keyId: 0x42};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
it('should add keysym as separate entry if keyId does not match last event and first is zero', function() {
|
||||
// this implies that separate keyups are required
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x43)};
|
||||
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
expect(times_called).to.be.equal(2);
|
||||
elem = {type: 'keyup', keyId: 0};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(3);
|
||||
elem = {type: 'keyup', keyId: 0x42};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
it('should add keysym as separate entry if keyId does not match last event and second is zero', function() {
|
||||
// this implies that a separate keyups are required
|
||||
var times_called = 0;
|
||||
var elem = null;
|
||||
var keysymsdown = {};
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
if (elem.type == 'keyup') {
|
||||
expect(evt).to.have.property('keysym');
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
delete keysymsdown[evt.keysym.keysym];
|
||||
}
|
||||
else {
|
||||
expect(evt).to.be.deep.equal(elem);
|
||||
expect (keysymsdown[evt.keysym.keysym]).to.not.be.undefined;
|
||||
elem = null;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)};
|
||||
keysymsdown[keysyms.lookup(0x42).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x43)};
|
||||
keysymsdown[keysyms.lookup(0x43).keysym] = true;
|
||||
obj(elem);
|
||||
expect(elem).to.be.null;
|
||||
elem = {type: 'keyup', keyId: 0x41};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(3);
|
||||
elem = {type: 'keyup', keyId: 0};
|
||||
obj(elem);
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
it('should pop matching key event on keyup', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
expect(evt.type).to.be.equal('keydown');
|
||||
break;
|
||||
case 3:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
|
||||
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||
obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
|
||||
obj({type: 'keyup', keyId: 0x42});
|
||||
expect(times_called).to.equal(4);
|
||||
});
|
||||
it('should pop the first zero keyevent on keyup with zero keyId', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
expect(evt.type).to.be.equal('keydown');
|
||||
break;
|
||||
case 3:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x61)});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x61)});
|
||||
obj({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0x62)});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x63)});
|
||||
obj({type: 'keyup', keyId: 0x0});
|
||||
expect(times_called).to.equal(4);
|
||||
});
|
||||
it('should pop the last keyevents keysym if no match is found for keyId', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
switch (times_called++) {
|
||||
case 0:
|
||||
case 1:
|
||||
case 2:
|
||||
expect(evt.type).to.be.equal('keydown');
|
||||
break;
|
||||
case 3:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x44, keysym: keysyms.lookup(0x63)});
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x61)});
|
||||
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x62)});
|
||||
obj({type: 'keydown', keyId: 0x43, keysym: keysyms.lookup(0x63)});
|
||||
obj({type: 'keyup', keyId: 0x44});
|
||||
expect(times_called).to.equal(4);
|
||||
});
|
||||
describe('Firefox sends keypress even when keydown is suppressed', function() {
|
||||
it('should discard the keypress', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
expect(times_called).to.be.equal(0);
|
||||
++times_called;
|
||||
});
|
||||
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
expect(times_called).to.be.equal(1);
|
||||
obj({type: 'keypress', keyId: 0x41, keysym: keysyms.lookup(0x43)});
|
||||
});
|
||||
});
|
||||
describe('releaseAll', function() {
|
||||
it('should do nothing if no keys have been pressed', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
++times_called;
|
||||
});
|
||||
obj({type: 'releaseall'});
|
||||
expect(times_called).to.be.equal(0);
|
||||
});
|
||||
it('should release the keys that have been pressed', function() {
|
||||
var times_called = 0;
|
||||
var obj = TrackKeyState(function(evt) {
|
||||
switch (times_called++) {
|
||||
case 2:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x41)});
|
||||
break;
|
||||
case 3:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0x42)});
|
||||
break;
|
||||
}
|
||||
});
|
||||
obj({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x41)});
|
||||
obj({type: 'keydown', keyId: 0x42, keysym: keysyms.lookup(0x42)});
|
||||
expect(times_called).to.be.equal(2);
|
||||
obj({type: 'releaseall'});
|
||||
expect(times_called).to.be.equal(4);
|
||||
obj({type: 'releaseall'});
|
||||
expect(times_called).to.be.equal(4);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('Escape Modifiers', function() {
|
||||
describe('Keydown', function() {
|
||||
it('should pass through when a char modifier is not down', function() {
|
||||
var times_called = 0;
|
||||
EscapeModifiers(function(evt) {
|
||||
expect(times_called).to.be.equal(0);
|
||||
++times_called;
|
||||
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
expect(times_called).to.be.equal(1);
|
||||
});
|
||||
it('should generate fake undo/redo events when a char modifier is down', function() {
|
||||
var times_called = 0;
|
||||
EscapeModifiers(function(evt) {
|
||||
switch(times_called++) {
|
||||
case 0:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe9)});
|
||||
break;
|
||||
case 1:
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0, keysym: keysyms.lookup(0xffe3)});
|
||||
break;
|
||||
case 2:
|
||||
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]});
|
||||
break;
|
||||
case 3:
|
||||
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe9)});
|
||||
break;
|
||||
case 4:
|
||||
expect(evt).to.be.deep.equal({type: 'keydown', keyId: 0, keysym: keysyms.lookup(0xffe3)});
|
||||
break;
|
||||
}
|
||||
})({type: 'keydown', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xffe9, 0xffe3]});
|
||||
expect(times_called).to.be.equal(5);
|
||||
});
|
||||
});
|
||||
describe('Keyup', function() {
|
||||
it('should pass through when a char modifier is down', function() {
|
||||
var times_called = 0;
|
||||
EscapeModifiers(function(evt) {
|
||||
expect(times_called).to.be.equal(0);
|
||||
++times_called;
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]});
|
||||
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42), escape: [0xfe03]});
|
||||
expect(times_called).to.be.equal(1);
|
||||
});
|
||||
it('should pass through when a char modifier is not down', function() {
|
||||
var times_called = 0;
|
||||
EscapeModifiers(function(evt) {
|
||||
expect(times_called).to.be.equal(0);
|
||||
++times_called;
|
||||
expect(evt).to.be.deep.equal({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
})({type: 'keyup', keyId: 0x41, keysym: keysyms.lookup(0x42)});
|
||||
expect(times_called).to.be.equal(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue