keyboard.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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 && !!(/mac/i).exec(navigator.platform);
  19. }
  20. function isWindows() {
  21. return navigator && !!(/win/i).exec(navigator.platform);
  22. }
  23. function isLinux() {
  24. return navigator && !!(/linux/i).exec(navigator.platform);
  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 (parseInt(key) !== XK_Shift_L) {
  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. if (!charModifier) {
  61. if (isMac()) {
  62. // on Mac, Option (AKA Alt) is used as a char modifier
  63. charModifier = [XK_Alt_L];
  64. }
  65. else if (isWindows()) {
  66. // on Windows, Ctrl+Alt is used as a char modifier
  67. charModifier = [XK_Alt_L, XK_Control_L];
  68. }
  69. else if (isLinux()) {
  70. // on Linux, ISO Level 3 Shift (AltGr) is used as a char modifier
  71. charModifier = [XK_ISO_Level3_Shift];
  72. }
  73. else {
  74. charModifier = [];
  75. }
  76. }
  77. var state = {};
  78. state[XK_Control_L] = false;
  79. state[XK_Alt_L] = false;
  80. state[XK_ISO_Level3_Shift] = false;
  81. state[XK_Shift_L] = false;
  82. state[XK_Meta_L] = false;
  83. function sync(evt, keysym) {
  84. var result = [];
  85. function syncKey(keysym) {
  86. return {keysym: keysyms.lookup(keysym), type: state[keysym] ? 'keydown' : 'keyup'};
  87. }
  88. if (evt.ctrlKey !== undefined &&
  89. evt.ctrlKey !== state[XK_Control_L] && keysym !== XK_Control_L) {
  90. state[XK_Control_L] = evt.ctrlKey;
  91. result.push(syncKey(XK_Control_L));
  92. }
  93. if (evt.altKey !== undefined &&
  94. evt.altKey !== state[XK_Alt_L] && keysym !== XK_Alt_L) {
  95. state[XK_Alt_L] = evt.altKey;
  96. result.push(syncKey(XK_Alt_L));
  97. }
  98. if (evt.altGraphKey !== undefined &&
  99. evt.altGraphKey !== state[XK_ISO_Level3_Shift] && keysym !== XK_ISO_Level3_Shift) {
  100. state[XK_ISO_Level3_Shift] = evt.altGraphKey;
  101. result.push(syncKey(XK_ISO_Level3_Shift));
  102. }
  103. if (evt.shiftKey !== undefined &&
  104. evt.shiftKey !== state[XK_Shift_L] && keysym !== XK_Shift_L) {
  105. state[XK_Shift_L] = evt.shiftKey;
  106. result.push(syncKey(XK_Shift_L));
  107. }
  108. if (evt.metaKey !== undefined &&
  109. evt.metaKey !== state[XK_Meta_L] && keysym !== XK_Meta_L) {
  110. state[XK_Meta_L] = evt.metaKey;
  111. result.push(syncKey(XK_Meta_L));
  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 ('keyCode' in evt && 'key' in evt) {
  140. return evt.key + ':' + evt.keyCode;
  141. }
  142. else if ('keyCode' in evt) {
  143. return evt.keyCode;
  144. }
  145. else {
  146. return evt.key;
  147. }
  148. }
  149. // Get the most reliable keysym value we can get from a key event
  150. // if char/charCode is available, prefer those, otherwise fall back to key/keyCode/which
  151. function getKeysym(evt){
  152. var codepoint;
  153. if (evt.char && evt.char.length === 1) {
  154. codepoint = evt.char.charCodeAt();
  155. }
  156. else if (evt.charCode) {
  157. codepoint = evt.charCode;
  158. }
  159. else if (evt.keyCode && evt.type === 'keypress') {
  160. // IE10 stores the char code as keyCode, and has no other useful properties
  161. codepoint = evt.keyCode;
  162. }
  163. if (codepoint) {
  164. var res = keysyms.fromUnicode(substituteCodepoint(codepoint));
  165. if (res) {
  166. return res;
  167. }
  168. }
  169. // we could check evt.key here.
  170. // Legal values are defined in http://www.w3.org/TR/DOM-Level-3-Events/#key-values-list,
  171. // so we "just" need to map them to keysym, but AFAIK this is only available in IE10, which also provides evt.key
  172. // so we don't *need* it yet
  173. if (evt.keyCode) {
  174. return keysyms.lookup(keysymFromKeyCode(evt.keyCode, evt.shiftKey));
  175. }
  176. if (evt.which) {
  177. return keysyms.lookup(keysymFromKeyCode(evt.which, evt.shiftKey));
  178. }
  179. return null;
  180. }
  181. // Given a keycode, try to predict which keysym it might be.
  182. // If the keycode is unknown, null is returned.
  183. function keysymFromKeyCode(keycode, shiftPressed) {
  184. if (typeof(keycode) !== 'number') {
  185. return null;
  186. }
  187. // won't be accurate for azerty
  188. if (keycode >= 0x30 && keycode <= 0x39) {
  189. return keycode; // digit
  190. }
  191. if (keycode >= 0x41 && keycode <= 0x5a) {
  192. // remap to lowercase unless shift is down
  193. return shiftPressed ? keycode : keycode + 32; // A-Z
  194. }
  195. if (keycode >= 0x60 && keycode <= 0x69) {
  196. return XK_KP_0 + (keycode - 0x60); // numpad 0-9
  197. }
  198. switch(keycode) {
  199. case 0x20: return XK_space;
  200. case 0x6a: return XK_KP_Multiply;
  201. case 0x6b: return XK_KP_Add;
  202. case 0x6c: return XK_KP_Separator;
  203. case 0x6d: return XK_KP_Subtract;
  204. case 0x6e: return XK_KP_Decimal;
  205. case 0x6f: return XK_KP_Divide;
  206. case 0xbb: return XK_plus;
  207. case 0xbc: return XK_comma;
  208. case 0xbd: return XK_minus;
  209. case 0xbe: return XK_period;
  210. }
  211. return nonCharacterKey({keyCode: keycode});
  212. }
  213. // if the key is a known non-character key (any key which doesn't generate character data)
  214. // return its keysym value. Otherwise return null
  215. function nonCharacterKey(evt) {
  216. // evt.key not implemented yet
  217. if (!evt.keyCode) { return null; }
  218. var keycode = evt.keyCode;
  219. if (keycode >= 0x70 && keycode <= 0x87) {
  220. return XK_F1 + keycode - 0x70; // F1-F24
  221. }
  222. switch (keycode) {
  223. case 8 : return XK_BackSpace;
  224. case 13 : return XK_Return;
  225. case 9 : return XK_Tab;
  226. case 27 : return XK_Escape;
  227. case 46 : return XK_Delete;
  228. case 36 : return XK_Home;
  229. case 35 : return XK_End;
  230. case 33 : return XK_Page_Up;
  231. case 34 : return XK_Page_Down;
  232. case 45 : return XK_Insert;
  233. case 37 : return XK_Left;
  234. case 38 : return XK_Up;
  235. case 39 : return XK_Right;
  236. case 40 : return XK_Down;
  237. case 16 : return XK_Shift_L;
  238. case 17 : return XK_Control_L;
  239. case 18 : return XK_Alt_L; // also: Option-key on Mac
  240. case 224 : return XK_Meta_L;
  241. case 225 : return XK_ISO_Level3_Shift; // AltGr
  242. case 91 : return XK_Super_L; // also: Windows-key
  243. case 92 : return XK_Super_R; // also: Windows-key
  244. case 93 : return XK_Menu; // also: Windows-Menu, Command on Mac
  245. default: return null;
  246. }
  247. }
  248. return {
  249. hasShortcutModifier : hasShortcutModifier,
  250. hasCharModifier : hasCharModifier,
  251. ModifierSync : ModifierSync,
  252. getKey : getKey,
  253. getKeysym : getKeysym,
  254. keysymFromKeyCode : keysymFromKeyCode,
  255. nonCharacterKey : nonCharacterKey,
  256. substituteCodepoint : substituteCodepoint
  257. };
  258. })();
  259. // Takes a DOM keyboard event and:
  260. // - determines which keysym it represents
  261. // - determines a keyId identifying the key that was pressed (corresponding to the key/keyCode properties on the DOM event)
  262. // - synthesizes events to synchronize modifier key state between which modifiers are actually down, and which we thought were down
  263. // - marks each event with an 'escape' property if a modifier was down which should be "escaped"
  264. // - generates a "stall" event in cases where it might be necessary to wait and see if a keypress event follows a keydown
  265. // This information is collected into an object which is passed to the next() function. (one call per event)
  266. function KeyEventDecoder(modifierState, next) {
  267. "use strict";
  268. function sendAll(evts) {
  269. for (var i = 0; i < evts.length; ++i) {
  270. next(evts[i]);
  271. }
  272. }
  273. function process(evt, type) {
  274. var result = {type: type};
  275. var keyId = kbdUtil.getKey(evt);
  276. if (keyId) {
  277. result.keyId = keyId;
  278. }
  279. var keysym = kbdUtil.getKeysym(evt);
  280. var hasModifier = modifierState.hasShortcutModifier() || !!modifierState.activeCharModifier();
  281. // Is this a case where we have to decide on the keysym right away, rather than waiting for the keypress?
  282. // "special" keys like enter, tab or backspace don't send keypress events,
  283. // and some browsers don't send keypresses at all if a modifier is down
  284. if (keysym && (type !== 'keydown' || kbdUtil.nonCharacterKey(evt) || hasModifier)) {
  285. result.keysym = keysym;
  286. }
  287. var isShift = evt.keyCode === 0x10 || evt.key === 'Shift';
  288. // Should we prevent the browser from handling the event?
  289. // Doing so on a keydown (in most browsers) prevents keypress from being generated
  290. // so only do that if we have to.
  291. var suppress = !isShift && (type !== 'keydown' || modifierState.hasShortcutModifier() || !!kbdUtil.nonCharacterKey(evt));
  292. // If a char modifier is down on a keydown, we need to insert a stall,
  293. // so VerifyCharModifier knows to wait and see if a keypress is comnig
  294. var stall = type === 'keydown' && modifierState.activeCharModifier() && !kbdUtil.nonCharacterKey(evt);
  295. // if a char modifier is pressed, get the keys it consists of (on Windows, AltGr is equivalent to Ctrl+Alt)
  296. var active = modifierState.activeCharModifier();
  297. // If we have a char modifier down, and we're able to determine a keysym reliably
  298. // then (a) we know to treat the modifier as a char modifier,
  299. // and (b) we'll have to "escape" the modifier to undo the modifier when sending the char.
  300. if (active && keysym) {
  301. var isCharModifier = false;
  302. for (var i = 0; i < active.length; ++i) {
  303. if (active[i] === keysym.keysym) {
  304. isCharModifier = true;
  305. }
  306. }
  307. if (type === 'keypress' && !isCharModifier) {
  308. result.escape = modifierState.activeCharModifier();
  309. }
  310. }
  311. if (stall) {
  312. // insert a fake "stall" event
  313. next({type: 'stall'});
  314. }
  315. next(result);
  316. return suppress;
  317. }
  318. return {
  319. keydown: function(evt) {
  320. sendAll(modifierState.keydown(evt));
  321. return process(evt, 'keydown');
  322. },
  323. keypress: function(evt) {
  324. return process(evt, 'keypress');
  325. },
  326. keyup: function(evt) {
  327. sendAll(modifierState.keyup(evt));
  328. return process(evt, 'keyup');
  329. },
  330. syncModifiers: function(evt) {
  331. sendAll(modifierState.syncAny(evt));
  332. },
  333. releaseAll: function() { next({type: 'releaseall'}); }
  334. };
  335. }
  336. // Combines keydown and keypress events where necessary to handle char modifiers.
  337. // On some OS'es, a char modifier is sometimes used as a shortcut modifier.
  338. // 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
  339. // 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.
  340. // The only way we can distinguish these cases is to wait and see if a keypress event arrives
  341. // When we receive a "stall" event, wait a few ms before processing the next keydown. If a keypress has also arrived, merge the two
  342. function VerifyCharModifier(next) {
  343. "use strict";
  344. var queue = [];
  345. var timer = null;
  346. function process() {
  347. if (timer) {
  348. return;
  349. }
  350. var delayProcess = function () {
  351. clearTimeout(timer);
  352. timer = null;
  353. process();
  354. };
  355. while (queue.length !== 0) {
  356. var cur = queue[0];
  357. queue = queue.splice(1);
  358. switch (cur.type) {
  359. case 'stall':
  360. // insert a delay before processing available events.
  361. /* jshint loopfunc: true */
  362. timer = setTimeout(delayProcess, 5);
  363. /* jshint loopfunc: false */
  364. return;
  365. case 'keydown':
  366. // is the next element a keypress? Then we should merge the two
  367. if (queue.length !== 0 && queue[0].type === 'keypress') {
  368. // Firefox sends keypress even when no char is generated.
  369. // so, if keypress keysym is the same as we'd have guessed from keydown,
  370. // the modifier didn't have any effect, and should not be escaped
  371. if (queue[0].escape && (!cur.keysym || cur.keysym.keysym !== queue[0].keysym.keysym)) {
  372. cur.escape = queue[0].escape;
  373. }
  374. cur.keysym = queue[0].keysym;
  375. queue = queue.splice(1);
  376. }
  377. break;
  378. }
  379. // swallow stall events, and pass all others to the next stage
  380. if (cur.type !== 'stall') {
  381. next(cur);
  382. }
  383. }
  384. }
  385. return function(evt) {
  386. queue.push(evt);
  387. process();
  388. };
  389. }
  390. // Keeps track of which keys we (and the server) believe are down
  391. // When a keyup is received, match it against this list, to determine the corresponding keysym(s)
  392. // in some cases, a single key may produce multiple keysyms, so the corresponding keyup event must release all of these chars
  393. // key repeat events should be merged into a single entry.
  394. // Because we can't always identify which entry a keydown or keyup event corresponds to, we sometimes have to guess
  395. function TrackKeyState(next) {
  396. "use strict";
  397. var state = [];
  398. return function (evt) {
  399. var last = state.length !== 0 ? state[state.length-1] : null;
  400. switch (evt.type) {
  401. case 'keydown':
  402. // insert a new entry if last seen key was different.
  403. if (!last || !evt.keyId || last.keyId !== evt.keyId) {
  404. last = {keyId: evt.keyId, keysyms: {}};
  405. state.push(last);
  406. }
  407. if (evt.keysym) {
  408. // make sure last event contains this keysym (a single "logical" keyevent
  409. // can cause multiple key events to be sent to the VNC server)
  410. last.keysyms[evt.keysym.keysym] = evt.keysym;
  411. last.ignoreKeyPress = true;
  412. next(evt);
  413. }
  414. break;
  415. case 'keypress':
  416. if (!last) {
  417. last = {keyId: evt.keyId, keysyms: {}};
  418. state.push(last);
  419. }
  420. if (!evt.keysym) {
  421. console.log('keypress with no keysym:', evt);
  422. }
  423. // If we didn't expect a keypress, and already sent a keydown to the VNC server
  424. // based on the keydown, make sure to skip this event.
  425. if (evt.keysym && !last.ignoreKeyPress) {
  426. last.keysyms[evt.keysym.keysym] = evt.keysym;
  427. evt.type = 'keydown';
  428. next(evt);
  429. }
  430. break;
  431. case 'keyup':
  432. if (state.length === 0) {
  433. return;
  434. }
  435. var idx = null;
  436. // do we have a matching key tracked as being down?
  437. for (var i = 0; i !== state.length; ++i) {
  438. if (state[i].keyId === evt.keyId) {
  439. idx = i;
  440. break;
  441. }
  442. }
  443. // if we couldn't find a match (it happens), assume it was the last key pressed
  444. if (idx === null) {
  445. idx = state.length - 1;
  446. }
  447. var item = state.splice(idx, 1)[0];
  448. // for each keysym tracked by this key entry, clone the current event and override the keysym
  449. var clone = (function(){
  450. function Clone(){}
  451. return function (obj) { Clone.prototype=obj; return new Clone(); };
  452. }());
  453. for (var key in item.keysyms) {
  454. var out = clone(evt);
  455. out.keysym = item.keysyms[key];
  456. next(out);
  457. }
  458. break;
  459. case 'releaseall':
  460. /* jshint shadow: true */
  461. for (var i = 0; i < state.length; ++i) {
  462. for (var key in state[i].keysyms) {
  463. var keysym = state[i].keysyms[key];
  464. next({keyId: 0, keysym: keysym, type: 'keyup'});
  465. }
  466. }
  467. /* jshint shadow: false */
  468. state = [];
  469. }
  470. };
  471. }
  472. // Handles "escaping" of modifiers: if a char modifier is used to produce a keysym (such as AltGr-2 to generate an @),
  473. // then the modifier must be "undone" before sending the @, and "redone" afterwards.
  474. function EscapeModifiers(next) {
  475. "use strict";
  476. return function(evt) {
  477. if (evt.type !== 'keydown' || evt.escape === undefined) {
  478. next(evt);
  479. return;
  480. }
  481. // undo modifiers
  482. for (var i = 0; i < evt.escape.length; ++i) {
  483. next({type: 'keyup', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
  484. }
  485. // send the character event
  486. next(evt);
  487. // redo modifiers
  488. /* jshint shadow: true */
  489. for (var i = 0; i < evt.escape.length; ++i) {
  490. next({type: 'keydown', keyId: 0, keysym: keysyms.lookup(evt.escape[i])});
  491. }
  492. /* jshint shadow: false */
  493. };
  494. }