]> git.proxmox.com Git - mirror_novnc.git/blob - core/input/keyboard.js
fa4a5ae4f0ff1c2ac3a6583dda89106edc02de44
[mirror_novnc.git] / core / input / keyboard.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 or any later version (see LICENSE.txt)
6 */
7
8 /*jslint browser: true, white: false */
9 /*global window, Util */
10
11 import * as Log from '../util/logging.js';
12 import { stopEvent } from '../util/events.js';
13 import { set_defaults, make_properties } from '../util/properties.js';
14 import * as KeyboardUtil from "./util.js";
15 import KeyTable from "./keysym.js";
16
17 //
18 // Keyboard event handler
19 //
20
21 export default function Keyboard(defaults) {
22 this._keyDownList = {}; // List of depressed keys
23 // (even if they are happy)
24 this._pendingKey = null; // Key waiting for keypress
25
26 set_defaults(this, defaults, {
27 'target': null,
28 });
29
30 // keep these here so we can refer to them later
31 this._eventHandlers = {
32 'keyup': this._handleKeyUp.bind(this),
33 'keydown': this._handleKeyDown.bind(this),
34 'keypress': this._handleKeyPress.bind(this),
35 'blur': this._allKeysUp.bind(this)
36 };
37 };
38
39 function isMac() {
40 return navigator && !!(/mac/i).exec(navigator.platform);
41 }
42 function isWindows() {
43 return navigator && !!(/win/i).exec(navigator.platform);
44 }
45 function isIOS() {
46 return navigator &&
47 (!!(/ipad/i).exec(navigator.platform) ||
48 !!(/iphone/i).exec(navigator.platform) ||
49 !!(/ipod/i).exec(navigator.platform));
50 }
51 function isIE() {
52 return navigator && !!(/trident/i).exec(navigator.userAgent);
53 }
54 function isEdge() {
55 return navigator && !!(/edge/i).exec(navigator.userAgent);
56 }
57
58 Keyboard.prototype = {
59 // private methods
60
61 _sendKeyEvent: function (keysym, code, down) {
62 if (!this._onKeyEvent) {
63 return;
64 }
65
66 Log.Debug("onKeyEvent " + (down ? "down" : "up") +
67 ", keysym: " + keysym, ", code: " + code);
68
69 // Windows sends CtrlLeft+AltRight when you press
70 // AltGraph, which tends to confuse the hell out of
71 // remote systems. Fake a release of these keys until
72 // there is a way to detect AltGraph properly.
73 var fakeAltGraph = false;
74 if (down && isWindows()) {
75 if ((code !== 'ControlLeft') &&
76 (code !== 'AltRight') &&
77 ('ControlLeft' in this._keyDownList) &&
78 ('AltRight' in this._keyDownList)) {
79 fakeAltGraph = true;
80 this._onKeyEvent(this._keyDownList['AltRight'],
81 'AltRight', false);
82 this._onKeyEvent(this._keyDownList['ControlLeft'],
83 'ControlLeft', false);
84 }
85 }
86
87 this._onKeyEvent(keysym, code, down);
88
89 if (fakeAltGraph) {
90 this._onKeyEvent(this._keyDownList['ControlLeft'],
91 'ControlLeft', true);
92 this._onKeyEvent(this._keyDownList['AltRight'],
93 'AltRight', true);
94 }
95 },
96
97 _getKeyCode: function (e) {
98 var code = KeyboardUtil.getKeycode(e);
99 if (code !== 'Unidentified') {
100 return code;
101 }
102
103 // Unstable, but we don't have anything else to go on
104 // (don't use it for 'keypress' events thought since
105 // WebKit sets it to the same as charCode)
106 if (e.keyCode && (e.type !== 'keypress')) {
107 // 229 is used for composition events
108 if (e.keyCode !== 229) {
109 return 'Platform' + e.keyCode;
110 }
111 }
112
113 // A precursor to the final DOM3 standard. Unfortunately it
114 // is not layout independent, so it is as bad as using keyCode
115 if (e.keyIdentifier) {
116 // Non-character key?
117 if (e.keyIdentifier.substr(0, 2) !== 'U+') {
118 return e.keyIdentifier;
119 }
120
121 var codepoint = parseInt(e.keyIdentifier.substr(2), 16);
122 var char = String.fromCharCode(codepoint);
123 // Some implementations fail to uppercase the symbols
124 char = char.toUpperCase();
125
126 return 'Platform' + char.charCodeAt();
127 }
128
129 return 'Unidentified';
130 },
131
132 _handleKeyDown: function (e) {
133 var code = this._getKeyCode(e);
134 var keysym = KeyboardUtil.getKeysym(e);
135
136 // We cannot handle keys we cannot track, but we also need
137 // to deal with virtual keyboards which omit key info
138 // (iOS omits tracking info on keyup events, which forces us to
139 // special treat that platform here)
140 if ((code === 'Unidentified') || isIOS()) {
141 if (keysym) {
142 // If it's a virtual keyboard then it should be
143 // sufficient to just send press and release right
144 // after each other
145 this._sendKeyEvent(keysym, code, true);
146 this._sendKeyEvent(keysym, code, false);
147 }
148
149 stopEvent(e);
150 return;
151 }
152
153 // Alt behaves more like AltGraph on macOS, so shuffle the
154 // keys around a bit to make things more sane for the remote
155 // server. This method is used by RealVNC and TigerVNC (and
156 // possibly others).
157 if (isMac()) {
158 switch (keysym) {
159 case KeyTable.XK_Super_L:
160 keysym = KeyTable.XK_Alt_L;
161 break;
162 case KeyTable.XK_Super_R:
163 keysym = KeyTable.XK_Super_L;
164 break;
165 case KeyTable.XK_Alt_L:
166 keysym = KeyTable.XK_Mode_switch;
167 break;
168 case KeyTable.XK_Alt_R:
169 keysym = KeyTable.XK_ISO_Level3_Shift;
170 break;
171 }
172 }
173
174 // Is this key already pressed? If so, then we must use the
175 // same keysym or we'll confuse the server
176 if (code in this._keyDownList) {
177 keysym = this._keyDownList[code];
178 }
179
180 // macOS doesn't send proper key events for modifiers, only
181 // state change events. That gets extra confusing for CapsLock
182 // which toggles on each press, but not on release. So pretend
183 // it was a quick press and release of the button.
184 if (isMac() && (code === 'CapsLock')) {
185 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
186 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
187 stopEvent(e);
188 return;
189 }
190
191 // If this is a legacy browser then we'll need to wait for
192 // a keypress event as well
193 // (IE and Edge has a broken KeyboardEvent.key, so we can't
194 // just check for the presence of that field)
195 if (!keysym && (!e.key || isIE() || isEdge())) {
196 this._pendingKey = code;
197 // However we might not get a keypress event if the key
198 // is non-printable, which needs some special fallback
199 // handling
200 setTimeout(this._handleKeyPressTimeout.bind(this), 10, e);
201 return;
202 }
203
204 this._pendingKey = null;
205 stopEvent(e);
206
207 this._keyDownList[code] = keysym;
208
209 this._sendKeyEvent(keysym, code, true);
210 },
211
212 // Legacy event for browsers without code/key
213 _handleKeyPress: function (e) {
214 stopEvent(e);
215
216 // Are we expecting a keypress?
217 if (this._pendingKey === null) {
218 return;
219 }
220
221 var code = this._getKeyCode(e);
222 var keysym = KeyboardUtil.getKeysym(e);
223
224 // The key we were waiting for?
225 if ((code !== 'Unidentified') && (code != this._pendingKey)) {
226 return;
227 }
228
229 code = this._pendingKey;
230 this._pendingKey = null;
231
232 if (!keysym) {
233 console.log('keypress with no keysym:', e);
234 return;
235 }
236
237 this._keyDownList[code] = keysym;
238
239 this._sendKeyEvent(keysym, code, true);
240 },
241 _handleKeyPressTimeout: function (e) {
242 // Did someone manage to sort out the key already?
243 if (this._pendingKey === null) {
244 return;
245 }
246
247 var code, keysym;
248
249 code = this._pendingKey;
250 this._pendingKey = null;
251
252 // We have no way of knowing the proper keysym with the
253 // information given, but the following are true for most
254 // layouts
255 if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) {
256 // Digit
257 keysym = e.keyCode;
258 } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) {
259 // Character (A-Z)
260 var char = String.fromCharCode(e.keyCode);
261 // A feeble attempt at the correct case
262 if (e.shiftKey)
263 char = char.toUpperCase();
264 else
265 char = char.toLowerCase();
266 keysym = char.charCodeAt();
267 } else {
268 // Unknown, give up
269 keysym = 0;
270 }
271
272 this._keyDownList[code] = keysym;
273
274 this._sendKeyEvent(keysym, code, true);
275 },
276
277 _handleKeyUp: function (e) {
278 stopEvent(e);
279
280 var code = this._getKeyCode(e);
281
282 // See comment in _handleKeyDown()
283 if (isMac() && (code === 'CapsLock')) {
284 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
285 this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
286 return;
287 }
288
289 // Do we really think this key is down?
290 if (!(code in this._keyDownList)) {
291 return;
292 }
293
294 this._sendKeyEvent(this._keyDownList[code], code, false);
295
296 delete this._keyDownList[code];
297 },
298
299 _allKeysUp: function () {
300 Log.Debug(">> Keyboard.allKeysUp");
301 for (var code in this._keyDownList) {
302 this._sendKeyEvent(this._keyDownList[code], code, false);
303 };
304 this._keyDownList = {};
305 Log.Debug("<< Keyboard.allKeysUp");
306 },
307
308 // Public methods
309
310 grab: function () {
311 //Log.Debug(">> Keyboard.grab");
312 var c = this._target;
313
314 c.addEventListener('keydown', this._eventHandlers.keydown);
315 c.addEventListener('keyup', this._eventHandlers.keyup);
316 c.addEventListener('keypress', this._eventHandlers.keypress);
317
318 // Release (key up) if window loses focus
319 window.addEventListener('blur', this._eventHandlers.blur);
320
321 //Log.Debug("<< Keyboard.grab");
322 },
323
324 ungrab: function () {
325 //Log.Debug(">> Keyboard.ungrab");
326 var c = this._target;
327
328 c.removeEventListener('keydown', this._eventHandlers.keydown);
329 c.removeEventListener('keyup', this._eventHandlers.keyup);
330 c.removeEventListener('keypress', this._eventHandlers.keypress);
331 window.removeEventListener('blur', this._eventHandlers.blur);
332
333 // Release (key up) all keys that are in a down state
334 this._allKeysUp();
335
336 //Log.Debug(">> Keyboard.ungrab");
337 },
338 };
339
340 make_properties(Keyboard, [
341 ['target', 'wo', 'dom'], // DOM element that captures keyboard input
342
343 ['onKeyEvent', 'rw', 'func'] // Handler for key press/release
344 ]);