keyboard.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528
  1. var kbdUtil = (function() {
  2. "use strict";
  3. function substituteCodepoint(cp) {
  4. // Any Unicode code points which do not have corresponding keysym entries
  5. // can be swapped out for another code point by adding them to this table
  6. var substitutions = {
  7. // {S,s} with comma below -> {S,s} with cedilla
  8. 0x218 : 0x15e,
  9. 0x219 : 0x15f,
  10. // {T,t} with comma below -> {T,t} with cedilla
  11. 0x21a : 0x162,
  12. 0x21b : 0x163
  13. };
  14. var sub = substitutions[cp];
  15. return sub ? sub : cp;
  16. };
  17. function isMac() {
  18. return navigator && !!(/macintosh/i).exec(navigator.appVersion);
  19. }
  20. function isWindows() {
  21. return navigator && !!(/windows/i).exec(navigator.appVersion);
  22. }
  23. function isLinux() {
  24. return navigator && !!(/linux/i).exec(navigator.appVersion);
  25. }
  26. // Return true if a modifier which is not the specified char modifier (and is not shift) is down
  27. function hasShortcutModifier(charModifier, currentModifiers) {
  28. var mods = {};
  29. for (var key in currentModifiers) {
  30. if (key !== 0xffe1) {
  31. mods[key] = currentModifiers[key];
  32. }
  33. }
  34. var sum = 0;
  35. for (var k in currentModifiers) {
  36. if (mods[k]) {
  37. ++sum;
  38. }
  39. }
  40. if (hasCharModifier(charModifier, mods)) {
  41. return sum > charModifier.length;
  42. }
  43. else {
  44. return sum > 0;
  45. }
  46. }
  47. // Return true if the specified char modifier is currently down
  48. function hasCharModifier(charModifier, currentModifiers) {
  49. if (charModifier.length === 0) { return false; }
  50. for (var i = 0; i < charModifier.length; ++i) {
  51. if (!currentModifiers[charModifier[i]]) {
  52. return false;
  53. }
  54. }
  55. return true;
  56. }
  57. // Helper object tracking modifier key state
  58. // and generates fake key events to compensate if it gets out of sync
  59. function ModifierSync(charModifier) {
  60. var ctrl = 0xffe3;
  61. var alt = 0xffe9;
  62. var altGr = 0xfe03;
  63. var shift = 0xffe1;
  64. var meta = 0xffe7;
  65. if (!charModifier) {
  66. if (isMac()) {
  67. // on Mac, Option (AKA Alt) is used as a char modifier
  68. charModifier = [alt];
  69. }
  70. else if (isWindows()) {
  71. // on Windows, Ctrl+Alt is used as a char modifier
  72. charModifier = [alt, ctrl];
  73. }
  74. else if (isLinux()) {
  75. // on Linux, AltGr is used as a char modifier
  76. charModifier = [altGr];
  77. }
  78. else {
  79. charModifier = [];
  80. }
  81. }
  82. var state = {};
  83. state[ctrl] = false;
  84. state[alt] = false;
  85. state[altGr] = false;
  86. state[shift] = false;
  87. state[meta] = false;
  88. function sync(evt, keysym) {
  89. var result = [];
  90. function syncKey(keysym) {
  91. return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
  92. }
  93. if (evt.ctrlKey !== undefined && evt.ctrlKey !== state[ctrl] && keysym !== ctrl) {
  94. state[ctrl] = evt.ctrlKey;
  95. result.push(syncKey(ctrl));
  96. }
  97. if (evt.altKey !== undefined && evt.altKey !== state[alt] && keysym !== alt) {
  98. state[alt] = evt.altKey;
  99. result.push(syncKey(alt));
  100. }
  101. if (evt.altGraphKey !== undefined && evt.altGraphKey !== state[altGr] && keysym !== altGr) {
  102. state[altGr] = evt.altGraphKey;
  103. result.push(syncKey(altGr));
  104. }
  105. if (evt.shiftKey !== undefined && evt.shiftKey !== state[shift] && keysym !== shift) {
  106. state[shift] = evt.shiftKey;
  107. result.push(syncKey(shift));
  108. }
  109. if (evt.metaKey !== undefined && evt.metaKey !== state[meta] && keysym !== meta) {
  110. state[meta] = evt.metaKey;
  111. result.push(syncKey(meta));
  112. }
  113. return result;
  114. }
  115. function syncKeyEvent(evt, down) {
  116. var obj = getKeysym(evt);
  117. var keysym = obj ? obj.keysym : null;
  118. // first, apply the event itself, if relevant
  119. if (keysym !== null && state[keysym] !== undefined) {
  120. state[keysym] = down;
  121. }
  122. return sync(evt, keysym);
  123. }
  124. return {
  125. // sync on the appropriate keyboard event
  126. keydown: function(evt) { return syncKeyEvent(evt, true);},
  127. keyup: function(evt) { return syncKeyEvent(evt, false);},
  128. // Call this with a non-keyboard event (such as mouse events) to use its modifier state to synchronize anyway
  129. syncAny: function(evt) { return sync(evt);},
  130. // is a shortcut modifier down?
  131. hasShortcutModifier: function() { return hasShortcutModifier(charModifier, state); },
  132. // if a char modifier is down, return the keys it consists of, otherwise return null
  133. activeCharModifier: function() { return hasCharModifier(charModifier, state) ? charModifier : null; }
  134. };
  135. }
  136. // Get a key ID from a keyboard event
  137. // May be a string or an integer depending on the available properties
  138. function getKey(evt){
  139. if (evt.key) {
  140. return evt.key;
  141. }
  142. else {
  143. return evt.keyCode;
  144. }
  145. }
  146. // Get the most reliable keysym value we can get from a key event
  147. // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
  148. function getKeysym(evt){
  149. var codepoint;
  150. if (evt.char && evt.char.length === 1) {
  151. codepoint = evt.char.charCodeAt();
  152. }
  153. else if (evt.charCode) {
  154. codepoint = evt.charCode;
  155. }
  156. if (codepoint) {
  157. var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
  158. if (res) {
  159. return res;
  160. }
  161. }
  162. // we could check evt.key here.
  163. // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
  164. // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
  165. // so we don't *need* it yet
  166. if (evt.keyCode) {
  167. return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
  168. }
  169. if (evt.which) {
  170. return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
  171. }
  172. return null;
  173. }
  174. // Given a keycode, try to predict which keysym it might be.
  175. // If the keycode is unknown, null is returned.
  176. function keysymFromKeyCode(keycode, shiftPressed) {
  177. if (typeof(keycode) !== 'number') {
  178. return null;
  179. }
  180. // won't be accurate for azerty
  181. if (keycode >= 0x30 && keycode <= 0x39) {
  182. return keycode; // digit
  183. }
  184. if (keycode >= 0x41 && keycode <= 0x5a) {
  185. // remap to lowercase unless shift is down
  186. return shiftPressed ? keycode : keycode + 32; // A-Z
  187. }
  188. if (keycode >= 0x60 && keycode <= 0x69) {
  189. return 0xffb0 + (keycode - 0x60); // numpad 0-9
  190. }
  191. switch(keycode) {
  192. case 0x20: return 0x20; // space
  193. case 0x6a: return 0xffaa; // multiply
  194. case 0x6b: return 0xffab; // add
  195. case 0x6c: return 0xffac; // separator
  196. case 0x6d: return 0xffad; // subtract
  197. case 0x6e: return 0xffae; // decimal
  198. case 0x6f: return 0xffaf; // divide
  199. case 0xbb: return 0x2b; // +
  200. case 0xbc: return 0x2c; // ,
  201. case 0xbd: return 0x2d; // -
  202. case 0xbe: return 0x2e; // .
  203. }
  204. return nonCharacterKey({keyCode: keycode});
  205. }
  206. // if the key is a known non-character key (any key which doesn't generate character data)
  207. // return its keysym value. Otherwise return null
  208. function nonCharacterKey(evt) {
  209. // evt.key not implemented yet
  210. if (!evt.keyCode) { return null; }
  211. var keycode = evt.keyCode;
  212. if (keycode >= 0x70 && keycode <= 0x87) {
  213. return 0xffbe + keycode - 0x70; // F1-F24
  214. }
  215. switch (keycode) {
  216. case 8 : return 0xFF08; // BACKSPACE
  217. case 13 : return 0xFF0D; // ENTER
  218. case 9 : return 0xFF09; // TAB
  219. case 27 : return 0xFF1B; // ESCAPE
  220. case 46 : return 0xFFFF; // DELETE
  221. case 36 : return 0xFF50; // HOME
  222. case 35 : return 0xFF57; // END
  223. case 33 : return 0xFF55; // PAGE_UP
  224. case 34 : return 0xFF56; // PAGE_DOWN
  225. case 45 : return 0xFF63; // INSERT
  226. case 37 : return 0xFF51; // LEFT
  227. case 38 : return 0xFF52; // UP
  228. case 39 : return 0xFF53; // RIGHT
  229. case 40 : return 0xFF54; // DOWN
  230. case 16 : return 0xFFE1; // SHIFT
  231. case 17 : return 0xFFE3; // CONTROL
  232. case 18 : return 0xFFE9; // Left ALT (Mac Option)
  233. case 224 : return 0xFE07; // Meta
  234. case 225 : return 0xFE03; // AltGr
  235. case 91 : return 0xFFEC; // Super_L (Win Key)
  236. case 92 : return 0xFFED; // Super_R (Win Key)
  237. case 93 : return 0xFF67; // Menu (Win Menu), Mac Command
  238. default: return null;
  239. }
  240. }
  241. return {
  242. hasShortcutModifier : hasShortcutModifier,
  243. hasCharModifier : hasCharModifier,
  244. ModifierSync : ModifierSync,
  245. getKey : getKey,
  246. getKeysym : getKeysym,
  247. keysymFromKeyCode : keysymFromKeyCode,
  248. nonCharacterKey : nonCharacterKey,
  249. substituteCodepoint : substituteCodepoint
  250. };
  251. })();
  252. // Takes a DOM keyboard event and:
  253. // - determines which keysym it represents
  254. // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
  255. // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
  256. // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
  257. // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
  258. // This information is collected into an object which is passed to the next() function. (one call per event)
  259. function KeyEventDecoder(modifierState, next) {
  260. "use strict";
  261. function sendAll(evts) {
  262. for (var i = 0; i < evts.length; ++i) {
  263. next(evts[i]);
  264. }
  265. }
  266. function process(evt, type) {
  267. var result = {type: type};
  268. var keyId = kbdUtil.getKey(evt);
  269. if (keyId) {
  270. result.keyId = keyId;
  271. }
  272. var keysym = kbdUtil.getKeysym(evt);
  273. var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
  274. // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
  275. // "special" keys like enter, tab or backspace don't send keypress events,
  276. // and some browsers don't send keypresses at all if a modifier is down
  277. if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) {
  278. result.keysym = keysym;
  279. }
  280. var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
  281. // Should we prevent the browser from handling the event?
  282. // Doing so on a keydown (in most browsers) prevents keypress from being generated
  283. // so only do that if we have to.
  284. var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt));
  285. // If a char modifier is down on a keydown, we need to insert a stall,
  286. // so VerifyCharModifier knows to wait and see if a keypress is comnig
  287. var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt);
  288. // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
  289. var active = modifierState.activeCharModifier();
  290. // If we have a char modifier down, and we're able to determine a keysym reliably
  291. // then (a) we know to treat the modifier as a char modifier,
  292. // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
  293. if (active && keysym) {
  294. var isCharModifier = false;
  295. for (var i = 0; i < active.length; ++i) {
  296. if (active[i] === keysym.keysym) {
  297. isCharModifier = true;
  298. }
  299. }
  300. if (type === 'keypress' && !isCharModifier) {
  301. result.escape = modifierState.activeCharModifier();
  302. }
  303. }
  304. if (stall) {
  305. // insert a fake "stall" event
  306. next({type: 'stall'});
  307. }
  308. next(result);
  309. return suppress;
  310. }
  311. return {
  312. keydown: function(evt) {
  313. sendAll(modifierState.keydown(evt));
  314. return process(evt, 'keydown');
  315. },
  316. keypress: function(evt) {
  317. return process(evt, 'keypress');
  318. },
  319. keyup: function(evt) {
  320. sendAll(modifierState.keyup(evt));
  321. return process(evt, 'keyup');
  322. },
  323. syncModifiers: function(evt) {
  324. sendAll(modifierState.syncAny(evt));
  325. },
  326. releaseAll: function() { next({type: 'releaseall'}); }
  327. };
  328. }
  329. // Combines keydown and keypress events where necessary to handle char modifiers.
  330. // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
  331. // 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
  332. // 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.
  333. // The only way we can distinguish these cases is to wait and see if a keypress event arrives
  334. // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
  335. function VerifyCharModifier(next) {
  336. "use strict";
  337. var queue = [];
  338. var timer = null;
  339. function process() {
  340. if (timer) {
  341. return;
  342. }
  343. while (queue.length !== 0) {
  344. var cur = queue[0];
  345. queue = queue.splice(1);
  346. switch (cur.type) {
  347. case 'stall':
  348. // insert a delay before processing available events.
  349. timer = setTimeout(function() {
  350. clearTimeout(timer);
  351. timer = null;
  352. process();
  353. }, 5);
  354. return;
  355. case 'keydown':
  356. // is the next element a keypress? Then we should merge the two
  357. if (queue.length !== 0 && queue[0].type === 'keypress') {
  358. // Firefox sends keypress even when no char is generated.
  359. // so, if keypress keysym is the same as we'd have guessed from keydown,
  360. // the modifier didn't have any effect, and should not be escaped
  361. if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
  362. cur.escape = queue[0].escape;
  363. }
  364. cur.keysym = queue[0].keysym;
  365. queue = queue.splice(1);
  366. }
  367. break;
  368. }
  369. // swallow stall events, and pass all others to the next stage
  370. if (cur.type !== 'stall') {
  371. next(cur);
  372. }
  373. }
  374. }
  375. return function(evt) {
  376. queue.push(evt);
  377. process();
  378. };
  379. }
  380. // Keeps track of which keys we (and the server) believe are down
  381. // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
  382. // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
  383. // key repeat events should be merged into a single entry.
  384. // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
  385. function TrackKeyState(next) {
  386. "use strict";
  387. var state = [];
  388. return function (evt) {
  389. var last = state.length !== 0 ? state[state.length-1] : null;
  390. switch (evt.type) {
  391. case 'keydown':
  392. // insert a new entry if last seen key was different.
  393. if (!last || !evt.keyId || last.keyId !== evt.keyId) {
  394. last = {keyId: evt.keyId, keysyms: {}};
  395. state.push(last);
  396. }
  397. if (evt.keysym) {
  398. // make sure last event contains this keysym (a single "logical" keyevent
  399. // can cause multiple key events to be sent to the VNC server)
  400. last.keysyms[evt.keysym.keysym] = evt.keysym;
  401. last.ignoreKeyPress = true;
  402. next(evt);
  403. }
  404. break;
  405. case 'keypress':
  406. if (!last) {
  407. last = {keyId: evt.keyId, keysyms: {}};
  408. state.push(last);
  409. }
  410. if (!evt.keysym) {
  411. console.log('keypress with no keysym:', evt);
  412. }
  413. // If we didn't expect a keypress, and already sent a keydown to the VNC server
  414. // based on the keydown, make sure to skip this event.
  415. if (evt.keysym && !last.ignoreKeyPress) {
  416. last.keysyms[evt.keysym.keysym] = evt.keysym;
  417. evt.type = 'keydown';
  418. next(evt);
  419. }
  420. break;
  421. case 'keyup':
  422. if (state.length === 0) {
  423. return;
  424. }
  425. var idx = null;
  426. // do we have a matching key tracked as being down?
  427. for (var i = 0; i !== state.length; ++i) {
  428. if (state[i].keyId === evt.keyId) {
  429. idx = i;
  430. break;
  431. }
  432. }
  433. // if we couldn't find a match (it happens), assume it was the last key pressed
  434. if (idx === null) {
  435. idx = state.length - 1;
  436. }
  437. var item = state.splice(idx, 1)[0];
  438. // for each keysym tracked by this key entry, clone the current event and override the keysym
  439. for (var key in item.keysyms) {
  440. var clone = (function(){
  441. function Clone(){}
  442. return function (obj) { Clone.prototype=obj; return new Clone(); };
  443. }());
  444. var out = clone(evt);
  445. out.keysym = item.keysyms[key];
  446. next(out);
  447. }
  448. break;
  449. case 'releaseall':
  450. for (var i = 0; i < state.length; ++i) {
  451. for (var key in state[i].keysyms) {
  452. var keysym = state[i].keysyms[key];
  453. next({keyId: 0, keysym: keysym, type: 'keyup'});
  454. }
  455. }
  456. state = [];
  457. }
  458. };
  459. }
  460. // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
  461. // then the modifier must be "undone" before sending the @, and "redone" afterwards.
  462. function EscapeModifiers(next) {
  463. "use strict";
  464. return function(evt) {
  465. if (evt.type !== 'keydown' || evt.escape === undefined) {
  466. next(evt);
  467. return;
  468. }
  469. // undo modifiers
  470. for (var i = 0; i < evt.escape.length; ++i) {
  471. next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
  472. }
  473. // send the character event
  474. next(evt);
  475. // redo modifiers
  476. for (var i = 0; i < evt.escape.length; ++i) {
  477. next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
  478. }
  479. };
  480. }