]> git.proxmox.com Git - mirror_novnc.git/blame - core/input/keyboard.js
Use fat arrow functions `const foo = () => { ... };` for callbacks
[mirror_novnc.git] / core / input / keyboard.js
CommitLineData
d3796c14
JM
1/*
2 * noVNC: HTML5 VNC client
1d728ace 3 * Copyright (C) 2012 Joel Martin
b2f1961a 4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
1d728ace 5 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
d3796c14
JM
6 */
7
6d6f0db0 8import * as Log from '../util/logging.js';
c1e2785f 9import { stopEvent } from '../util/events.js';
6d6f0db0 10import * as KeyboardUtil from "./util.js";
bf43c263 11import KeyTable from "./keysym.js";
59ef2916 12import * as browser from "../util/browser.js";
6d6f0db0
SR
13
14//
15// Keyboard event handler
16//
17
0e4808bf
JD
18export default class Keyboard {
19 constructor(target) {
20 this._target = target || null;
21
22 this._keyDownList = {}; // List of depressed keys
23 // (even if they are happy)
24 this._pendingKey = null; // Key waiting for keypress
25 this._altGrArmed = false; // Windows AltGr detection
26
27 // keep these here so we can refer to them later
28 this._eventHandlers = {
29 'keyup': this._handleKeyUp.bind(this),
30 'keydown': this._handleKeyDown.bind(this),
31 'keypress': this._handleKeyPress.bind(this),
32 'blur': this._allKeysUp.bind(this),
33 'checkalt': this._checkAlt.bind(this),
34 };
d6e281ba 35
0e4808bf 36 // ===== EVENT HANDLERS =====
d6e281ba 37
0e4808bf
JD
38 this.onkeyevent = () => {}; // Handler for key press/release
39 }
f7363fd2 40
747b4623
PO
41 // ===== PRIVATE METHODS =====
42
0e4808bf 43 _sendKeyEvent(keysym, code, down) {
d6ae4457
PO
44 if (down) {
45 this._keyDownList[code] = keysym;
46 } else {
47 // Do we really think this key is down?
48 if (!(code in this._keyDownList)) {
49 return;
bf43c263 50 }
d6ae4457 51 delete this._keyDownList[code];
bf43c263
PO
52 }
53
747b4623 54 Log.Debug("onkeyevent " + (down ? "down" : "up") +
f7363fd2 55 ", keysym: " + keysym, ", code: " + code);
747b4623 56 this.onkeyevent(keysym, code, down);
0e4808bf 57 }
f7363fd2 58
0e4808bf 59 _getKeyCode(e) {
2b5f94fa 60 const code = KeyboardUtil.getKeycode(e);
7e79dfe4
PO
61 if (code !== 'Unidentified') {
62 return code;
63 }
64
65 // Unstable, but we don't have anything else to go on
66 // (don't use it for 'keypress' events thought since
67 // WebKit sets it to the same as charCode)
68 if (e.keyCode && (e.type !== 'keypress')) {
4093c37f
PO
69 // 229 is used for composition events
70 if (e.keyCode !== 229) {
71 return 'Platform' + e.keyCode;
72 }
7e79dfe4
PO
73 }
74
75 // A precursor to the final DOM3 standard. Unfortunately it
76 // is not layout independent, so it is as bad as using keyCode
77 if (e.keyIdentifier) {
78 // Non-character key?
79 if (e.keyIdentifier.substr(0, 2) !== 'U+') {
80 return e.keyIdentifier;
f7363fd2 81 }
7e79dfe4 82
2b5f94fa
JD
83 const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
84 const char = String.fromCharCode(codepoint).toUpperCase();
7e79dfe4
PO
85
86 return 'Platform' + char.charCodeAt();
f7363fd2
PO
87 }
88
7e79dfe4 89 return 'Unidentified';
0e4808bf 90 }
d6e281ba 91
0e4808bf 92 _handleKeyDown(e) {
2b5f94fa
JD
93 const code = this._getKeyCode(e);
94 let keysym = KeyboardUtil.getKeysym(e);
f7363fd2 95
b22c9ef9
PO
96 // Windows doesn't have a proper AltGr, but handles it using
97 // fake Ctrl+Alt. However the remote end might not be Windows,
98 // so we need to merge those in to a single AltGr event. We
99 // detect this case by seeing the two key events directly after
100 // each other with a very short time between them (<50ms).
101 if (this._altGrArmed) {
102 this._altGrArmed = false;
103 clearTimeout(this._altGrTimeout);
104
105 if ((code === "AltRight") &&
106 ((e.timeStamp - this._altGrCtrlTime) < 50)) {
107 // FIXME: We fail to detect this if either Ctrl key is
108 // first manually pressed as Windows then no
109 // longer sends the fake Ctrl down event. It
110 // does however happily send real Ctrl events
111 // even when AltGr is already down. Some
112 // browsers detect this for us though and set the
113 // key to "AltGraph".
114 keysym = KeyTable.XK_ISO_Level3_Shift;
115 } else {
116 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
117 }
118 }
119
ae820533
PO
120 // We cannot handle keys we cannot track, but we also need
121 // to deal with virtual keyboards which omit key info
9e99ce12
PO
122 // (iOS omits tracking info on keyup events, which forces us to
123 // special treat that platform here)
59ef2916 124 if ((code === 'Unidentified') || browser.isIOS()) {
ae820533
PO
125 if (keysym) {
126 // If it's a virtual keyboard then it should be
127 // sufficient to just send press and release right
128 // after each other
9e99ce12
PO
129 this._sendKeyEvent(keysym, code, true);
130 this._sendKeyEvent(keysym, code, false);
ae820533
PO
131 }
132
133 stopEvent(e);
134 return;
135 }
136
bf43c263
PO
137 // Alt behaves more like AltGraph on macOS, so shuffle the
138 // keys around a bit to make things more sane for the remote
139 // server. This method is used by RealVNC and TigerVNC (and
140 // possibly others).
59ef2916 141 if (browser.isMac()) {
bf43c263
PO
142 switch (keysym) {
143 case KeyTable.XK_Super_L:
144 keysym = KeyTable.XK_Alt_L;
145 break;
146 case KeyTable.XK_Super_R:
147 keysym = KeyTable.XK_Super_L;
148 break;
149 case KeyTable.XK_Alt_L:
150 keysym = KeyTable.XK_Mode_switch;
151 break;
152 case KeyTable.XK_Alt_R:
153 keysym = KeyTable.XK_ISO_Level3_Shift;
154 break;
155 }
156 }
157
ae820533
PO
158 // Is this key already pressed? If so, then we must use the
159 // same keysym or we'll confuse the server
160 if (code in this._keyDownList) {
161 keysym = this._keyDownList[code];
162 }
163
634cc1ba
PO
164 // macOS doesn't send proper key events for modifiers, only
165 // state change events. That gets extra confusing for CapsLock
166 // which toggles on each press, but not on release. So pretend
167 // it was a quick press and release of the button.
59ef2916 168 if (browser.isMac() && (code === 'CapsLock')) {
634cc1ba
PO
169 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
170 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
171 stopEvent(e);
172 return;
173 }
174
f7363fd2 175 // If this is a legacy browser then we'll need to wait for
9fce233d 176 // a keypress event as well
844e9839
PO
177 // (IE and Edge has a broken KeyboardEvent.key, so we can't
178 // just check for the presence of that field)
59ef2916 179 if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) {
9fce233d 180 this._pendingKey = code;
7cac5c8e
PO
181 // However we might not get a keypress event if the key
182 // is non-printable, which needs some special fallback
183 // handling
184 setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
9fce233d 185 return;
f7363fd2
PO
186 }
187
9fce233d
PO
188 this._pendingKey = null;
189 stopEvent(e);
190
b22c9ef9
PO
191 // Possible start of AltGr sequence? (see above)
192 if ((code === "ControlLeft") && browser.isWindows() &&
193 !("ControlLeft" in this._keyDownList)) {
194 this._altGrArmed = true;
195 this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
196 this._altGrCtrlTime = e.timeStamp;
197 return;
198 }
f7363fd2 199
f7363fd2 200 this._sendKeyEvent(keysym, code, true);
0e4808bf 201 }
d6e281ba 202
f7363fd2 203 // Legacy event for browsers without code/key
0e4808bf 204 _handleKeyPress(e) {
f7363fd2
PO
205 stopEvent(e);
206
9fce233d
PO
207 // Are we expecting a keypress?
208 if (this._pendingKey === null) {
209 return;
210 }
211
2b5f94fa
JD
212 let code = this._getKeyCode(e);
213 const keysym = KeyboardUtil.getKeysym(e);
f7363fd2 214
9fce233d
PO
215 // The key we were waiting for?
216 if ((code !== 'Unidentified') && (code != this._pendingKey)) {
217 return;
218 }
219
220 code = this._pendingKey;
221 this._pendingKey = null;
222
f7363fd2 223 if (!keysym) {
a0035359 224 Log.Info('keypress with no keysym:', e);
f7363fd2
PO
225 return;
226 }
227
f7363fd2 228 this._sendKeyEvent(keysym, code, true);
0e4808bf
JD
229 }
230
231 _handleKeyPressTimeout(e) {
7cac5c8e
PO
232 // Did someone manage to sort out the key already?
233 if (this._pendingKey === null) {
234 return;
235 }
236
2b5f94fa 237 let keysym;
7cac5c8e 238
2b5f94fa 239 const code = this._pendingKey;
7cac5c8e
PO
240 this._pendingKey = null;
241
242 // We have no way of knowing the proper keysym with the
243 // information given, but the following are true for most
244 // layouts
245 if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
246 // Digit
247 keysym = e.keyCode;
248 } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
249 // Character (A-Z)
2b5f94fa 250 let char = String.fromCharCode(e.keyCode);
7cac5c8e
PO
251 // A feeble attempt at the correct case
252 if (e.shiftKey)
253 char = char.toUpperCase();
254 else
255 char = char.toLowerCase();
256 keysym = char.charCodeAt();
257 } else {
258 // Unknown, give up
259 keysym = 0;
260 }
261
7cac5c8e 262 this._sendKeyEvent(keysym, code, true);
0e4808bf 263 }
d3796c14 264
0e4808bf 265 _handleKeyUp(e) {
f7363fd2
PO
266 stopEvent(e);
267
2b5f94fa 268 const code = this._getKeyCode(e);
f7363fd2 269
b22c9ef9
PO
270 // We can't get a release in the middle of an AltGr sequence, so
271 // abort that detection
272 if (this._altGrArmed) {
273 this._altGrArmed = false;
274 clearTimeout(this._altGrTimeout);
275 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
276 }
277
634cc1ba 278 // See comment in _handleKeyDown()
59ef2916 279 if (browser.isMac() && (code === 'CapsLock')) {
634cc1ba
PO
280 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
281 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
282 return;
283 }
284
ae820533 285 this._sendKeyEvent(this._keyDownList[code], code, false);
0e4808bf 286 }
ae820533 287
0e4808bf 288 _handleAltGrTimeout() {
b22c9ef9
PO
289 this._altGrArmed = false;
290 clearTimeout(this._altGrTimeout);
291 this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
0e4808bf 292 }
d3796c14 293
0e4808bf 294 _allKeysUp() {
6d6f0db0 295 Log.Debug(">> Keyboard.allKeysUp");
2b5f94fa 296 for (let code in this._keyDownList) {
ae820533 297 this._sendKeyEvent(this._keyDownList[code], code, false);
8727f598 298 }
6d6f0db0 299 Log.Debug("<< Keyboard.allKeysUp");
0e4808bf 300 }
d6e281ba 301
3a7c0c67 302 // Firefox Alt workaround, see below
0e4808bf 303 _checkAlt(e) {
3a7c0c67
PO
304 if (e.altKey) {
305 return;
306 }
307
2b5f94fa
JD
308 const target = this._target;
309 const downList = this._keyDownList;
651c23ec 310 ['AltLeft', 'AltRight'].forEach((code) => {
3a7c0c67
PO
311 if (!(code in downList)) {
312 return;
313 }
314
2b5f94fa 315 const event = new KeyboardEvent('keyup',
3a7c0c67
PO
316 { key: downList[code],
317 code: code });
318 target.dispatchEvent(event);
319 });
0e4808bf 320 }
3a7c0c67 321
747b4623 322 // ===== PUBLIC METHODS =====
d6e281ba 323
0e4808bf 324 grab() {
6d6f0db0 325 //Log.Debug(">> Keyboard.grab");
d6e281ba 326
2b5f94fa
JD
327 this._target.addEventListener('keydown', this._eventHandlers.keydown);
328 this._target.addEventListener('keyup', this._eventHandlers.keyup);
329 this._target.addEventListener('keypress', this._eventHandlers.keypress);
d6e281ba 330
6d6f0db0
SR
331 // Release (key up) if window loses focus
332 window.addEventListener('blur', this._eventHandlers.blur);
d6e281ba 333
3a7c0c67
PO
334 // Firefox has broken handling of Alt, so we need to poll as
335 // best we can for releases (still doesn't prevent the menu
336 // from popping up though as we can't call preventDefault())
337 if (browser.isWindows() && browser.isFirefox()) {
2b5f94fa 338 const handler = this._eventHandlers.checkalt;
3a7c0c67
PO
339 ['mousedown', 'mouseup', 'mousemove', 'wheel',
340 'touchstart', 'touchend', 'touchmove',
651c23ec 341 'keydown', 'keyup'].forEach(type =>
3a7c0c67
PO
342 document.addEventListener(type, handler,
343 { capture: true,
651c23ec 344 passive: true }));
3a7c0c67
PO
345 }
346
6d6f0db0 347 //Log.Debug("<< Keyboard.grab");
0e4808bf 348 }
d6e281ba 349
0e4808bf 350 ungrab() {
6d6f0db0 351 //Log.Debug(">> Keyboard.ungrab");
d6e281ba 352
3a7c0c67 353 if (browser.isWindows() && browser.isFirefox()) {
2b5f94fa 354 const handler = this._eventHandlers.checkalt;
3a7c0c67
PO
355 ['mousedown', 'mouseup', 'mousemove', 'wheel',
356 'touchstart', 'touchend', 'touchmove',
651c23ec 357 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler));
3a7c0c67
PO
358 }
359
2b5f94fa
JD
360 this._target.removeEventListener('keydown', this._eventHandlers.keydown);
361 this._target.removeEventListener('keyup', this._eventHandlers.keyup);
362 this._target.removeEventListener('keypress', this._eventHandlers.keypress);
6d6f0db0 363 window.removeEventListener('blur', this._eventHandlers.blur);
d6e281ba 364
6d6f0db0
SR
365 // Release (key up) all keys that are in a down state
366 this._allKeysUp();
d3796c14 367
6d6f0db0 368 //Log.Debug(">> Keyboard.ungrab");
0e4808bf
JD
369 }
370}