]> git.proxmox.com Git - mirror_novnc.git/blame - core/input/devices.js
Simplify handling of keypress
[mirror_novnc.git] / core / input / devices.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
d6e281ba 8/*jslint browser: true, white: false */
d3796c14
JM
9/*global window, Util */
10
6d6f0db0
SR
11import * as Log from '../util/logging.js';
12import { isTouchDevice } from '../util/browsers.js'
13import { setCapture, releaseCapture, stopEvent, getPointerEvent } from '../util/events.js';
14import { set_defaults, make_properties } from '../util/properties.js';
15import * as KeyboardUtil from "./util.js";
16
17//
18// Keyboard event handler
19//
20
21const Keyboard = function (defaults) {
22 this._keyDownList = []; // List of depressed keys
23 // (even if they are happy)
9fce233d 24 this._pendingKey = null; // Key waiting for keypress
6d6f0db0 25
f7363fd2
PO
26 this._modifierState = KeyboardUtil.ModifierSync();
27
6d6f0db0
SR
28 set_defaults(this, defaults, {
29 'target': document,
30 'focused': true
31 });
32
6d6f0db0
SR
33 // keep these here so we can refer to them later
34 this._eventHandlers = {
35 'keyup': this._handleKeyUp.bind(this),
36 'keydown': this._handleKeyDown.bind(this),
37 'keypress': this._handleKeyPress.bind(this),
38 'blur': this._allKeysUp.bind(this)
39 };
40};
d6e281ba 41
6d6f0db0
SR
42Keyboard.prototype = {
43 // private methods
d6e281ba 44
f7363fd2
PO
45 _sendKeyEvent: function (keysym, code, down) {
46 if (!this._onKeyEvent) {
47 return;
6d6f0db0 48 }
f7363fd2
PO
49
50 Log.Debug("onKeyEvent " + (down ? "down" : "up") +
51 ", keysym: " + keysym, ", code: " + code);
52
53 this._onKeyEvent(keysym, code, down);
54 },
55
56 _getKeyCode: function (e) {
57 var code = KeyboardUtil.getKeycode(e);
58 if (code === 'Unidentified') {
59 // Unstable, but we don't have anything else to go on
60 // (don't use it for 'keypress' events thought since
61 // WebKit sets it to the same as charCode)
62 if (e.keyCode && (e.type !== 'keypress')) {
63 code = 'Platform' + e.keyCode;
64 }
65 }
66
67 return code;
6d6f0db0 68 },
d6e281ba 69
6d6f0db0
SR
70 _handleKeyDown: function (e) {
71 if (!this._focused) { return; }
72
f7363fd2
PO
73 this._modifierState.keydown(e);
74
75 var code = this._getKeyCode(e);
76 var keysym = KeyboardUtil.getKeysym(e);
77
78 // If this is a legacy browser then we'll need to wait for
9fce233d
PO
79 // a keypress event as well
80 if (!keysym) {
81 this._pendingKey = code;
82 return;
f7363fd2
PO
83 }
84
9fce233d
PO
85 this._pendingKey = null;
86 stopEvent(e);
87
f7363fd2
PO
88 // if a char modifier is pressed, get the keys it consists
89 // of (on Windows, AltGr is equivalent to Ctrl+Alt)
90 var active = this._modifierState.activeCharModifier();
91
92 // If we have a char modifier down, and we're able to
93 // determine a keysym reliably then (a) we know to treat
94 // the modifier as a char modifier, and (b) we'll have to
95 // "escape" the modifier to undo the modifier when sending
96 // the char.
9fce233d 97 if (active) {
f7363fd2
PO
98 var isCharModifier = false;
99 for (var i = 0; i < active.length; ++i) {
100 if (active[i] === keysym) {
101 isCharModifier = true;
102 }
103 }
104 if (!isCharModifier) {
105 var escape = this._modifierState.activeCharModifier();
106 }
107 }
108
109 var last;
110 if (this._keyDownList.length === 0) {
111 last = null;
6d6f0db0 112 } else {
f7363fd2
PO
113 last = this._keyDownList[this._keyDownList.length-1];
114 }
115
116 // insert a new entry if last seen key was different.
117 if (!last || code === 'Unidentified' || last.code !== code) {
118 last = {code: code, keysyms: {}};
119 this._keyDownList.push(last);
120 }
121
f7363fd2
PO
122 // make sure last event contains this keysym (a single "logical" keyevent
123 // can cause multiple key events to be sent to the VNC server)
124 last.keysyms[keysym] = keysym;
f7363fd2
PO
125
126 // undo modifiers
127 if (escape) {
128 for (var i = 0; i < escape.length; ++i) {
129 this._sendKeyEvent(escape[i], 'Unidentified', false);
130 }
131 }
132
133 // send the character event
134 this._sendKeyEvent(keysym, code, true);
135
136 // redo modifiers
137 if (escape) {
138 for (i = 0; i < escape.length; ++i) {
139 this._sendKeyEvent(escape[i], 'Unidentified', true);
140 }
6d6f0db0
SR
141 }
142 },
d6e281ba 143
f7363fd2 144 // Legacy event for browsers without code/key
6d6f0db0
SR
145 _handleKeyPress: function (e) {
146 if (!this._focused) { return; }
d6e281ba 147
f7363fd2
PO
148 stopEvent(e);
149
9fce233d
PO
150 // Are we expecting a keypress?
151 if (this._pendingKey === null) {
152 return;
153 }
154
f7363fd2
PO
155 var code = this._getKeyCode(e);
156 var keysym = KeyboardUtil.getKeysym(e);
157
9fce233d
PO
158 // The key we were waiting for?
159 if ((code !== 'Unidentified') && (code != this._pendingKey)) {
160 return;
161 }
162
163 code = this._pendingKey;
164 this._pendingKey = null;
165
f7363fd2
PO
166 // if a char modifier is pressed, get the keys it consists
167 // of (on Windows, AltGr is equivalent to Ctrl+Alt)
168 var active = this._modifierState.activeCharModifier();
169
170 // If we have a char modifier down, and we're able to
171 // determine a keysym reliably then (a) we know to treat
172 // the modifier as a char modifier, and (b) we'll have to
173 // "escape" the modifier to undo the modifier when sending
174 // the char.
175 if (active && keysym) {
176 var isCharModifier = false;
177 for (var i = 0; i < active.length; ++i) {
178 if (active[i] === keysym) {
179 isCharModifier = true;
180 }
181 }
182 if (!isCharModifier) {
183 var escape = this._modifierState.activeCharModifier();
184 }
185 }
186
187 var last;
188 if (this._keyDownList.length === 0) {
189 last = null;
190 } else {
191 last = this._keyDownList[this._keyDownList.length-1];
192 }
193
194 if (!last) {
195 last = {code: code, keysyms: {}};
196 this._keyDownList.push(last);
197 }
198 if (!keysym) {
199 console.log('keypress with no keysym:', e);
200 return;
201 }
202
f7363fd2
PO
203 last.keysyms[keysym] = keysym;
204
205 // undo modifiers
206 if (escape) {
207 for (var i = 0; i < escape.length; ++i) {
208 this._sendKeyEvent(escape[i], 'Unidentified', false);
209 }
210 }
211
212 // send the character event
213 this._sendKeyEvent(keysym, code, true);
214
215 // redo modifiers
216 if (escape) {
217 for (i = 0; i < escape.length; ++i) {
218 this._sendKeyEvent(escape[i], 'Unidentified', true);
219 }
6d6f0db0
SR
220 }
221 },
d3796c14 222
6d6f0db0
SR
223 _handleKeyUp: function (e) {
224 if (!this._focused) { return; }
c96f9003 225
f7363fd2
PO
226 stopEvent(e);
227
228 this._modifierState.keyup(e);
229
230 var code = this._getKeyCode(e);
231
232 if (this._keyDownList.length === 0) {
233 return;
234 }
235 var idx = null;
236 // do we have a matching key tracked as being down?
237 for (var i = 0; i !== this._keyDownList.length; ++i) {
238 if (this._keyDownList[i].code === code) {
239 idx = i;
240 break;
241 }
242 }
243 // if we couldn't find a match (it happens), assume it was the last key pressed
244 if (idx === null) {
245 idx = this._keyDownList.length - 1;
246 }
247
248 var item = this._keyDownList.splice(idx, 1)[0];
249 for (var key in item.keysyms) {
250 this._sendKeyEvent(item.keysyms[key], code, false);
6d6f0db0
SR
251 }
252 },
d3796c14 253
6d6f0db0
SR
254 _allKeysUp: function () {
255 Log.Debug(">> Keyboard.allKeysUp");
f7363fd2
PO
256 for (var i = 0; i < this._keyDownList.length; i++) {
257 var item = this._keyDownList[i];
258 for (var key in item.keysyms) {
259 this._sendKeyEvent(item.keysyms[key], 'Unidentified', false);
260 }
261 };
262 this._keyDownList = [];
6d6f0db0
SR
263 Log.Debug("<< Keyboard.allKeysUp");
264 },
d6e281ba 265
6d6f0db0 266 // Public methods
d6e281ba 267
6d6f0db0
SR
268 grab: function () {
269 //Log.Debug(">> Keyboard.grab");
270 var c = this._target;
d6e281ba 271
6d6f0db0
SR
272 c.addEventListener('keydown', this._eventHandlers.keydown);
273 c.addEventListener('keyup', this._eventHandlers.keyup);
274 c.addEventListener('keypress', this._eventHandlers.keypress);
d6e281ba 275
6d6f0db0
SR
276 // Release (key up) if window loses focus
277 window.addEventListener('blur', this._eventHandlers.blur);
d6e281ba 278
6d6f0db0
SR
279 //Log.Debug("<< Keyboard.grab");
280 },
d6e281ba 281
6d6f0db0
SR
282 ungrab: function () {
283 //Log.Debug(">> Keyboard.ungrab");
284 var c = this._target;
d6e281ba 285
6d6f0db0
SR
286 c.removeEventListener('keydown', this._eventHandlers.keydown);
287 c.removeEventListener('keyup', this._eventHandlers.keyup);
288 c.removeEventListener('keypress', this._eventHandlers.keypress);
289 window.removeEventListener('blur', this._eventHandlers.blur);
d6e281ba 290
6d6f0db0
SR
291 // Release (key up) all keys that are in a down state
292 this._allKeysUp();
d3796c14 293
6d6f0db0
SR
294 //Log.Debug(">> Keyboard.ungrab");
295 },
6d6f0db0 296};
d6e281ba 297
6d6f0db0
SR
298make_properties(Keyboard, [
299 ['target', 'wo', 'dom'], // DOM element that captures keyboard input
300 ['focused', 'rw', 'bool'], // Capture and send key events
d6e281ba 301
d0703d1b 302 ['onKeyEvent', 'rw', 'func'] // Handler for key press/release
6d6f0db0 303]);
d6e281ba 304
6d6f0db0
SR
305const Mouse = function (defaults) {
306 this._mouseCaptured = false;
d3796c14 307
6d6f0db0
SR
308 this._doubleClickTimer = null;
309 this._lastTouchPos = null;
d6e281ba 310
6d6f0db0
SR
311 // Configuration attributes
312 set_defaults(this, defaults, {
313 'target': document,
314 'focused': true,
315 'touchButton': 1
316 });
d6e281ba 317
6d6f0db0
SR
318 this._eventHandlers = {
319 'mousedown': this._handleMouseDown.bind(this),
320 'mouseup': this._handleMouseUp.bind(this),
321 'mousemove': this._handleMouseMove.bind(this),
322 'mousewheel': this._handleMouseWheel.bind(this),
323 'mousedisable': this._handleMouseDisable.bind(this)
d6e281ba 324 };
6d6f0db0 325};
d6e281ba 326
6d6f0db0
SR
327Mouse.prototype = {
328 // private methods
329 _captureMouse: function () {
330 // capturing the mouse ensures we get the mouseup event
331 setCapture(this._target);
d6e281ba 332
6d6f0db0
SR
333 // some browsers give us mouseup events regardless,
334 // so if we never captured the mouse, we can disregard the event
335 this._mouseCaptured = true;
336 },
d6e281ba 337
6d6f0db0
SR
338 _releaseMouse: function () {
339 releaseCapture();
340 this._mouseCaptured = false;
341 },
d6e281ba 342
6d6f0db0
SR
343 _resetDoubleClickTimer: function () {
344 this._doubleClickTimer = null;
345 },
b2f1961a 346
6d6f0db0
SR
347 _handleMouseButton: function (e, down) {
348 if (!this._focused) { return; }
cf19ad37 349
6d6f0db0
SR
350 var pos = this._getMousePosition(e);
351
352 var bmask;
353 if (e.touches || e.changedTouches) {
354 // Touch device
355
356 // When two touches occur within 500 ms of each other and are
357 // close enough together a double click is triggered.
358 if (down == 1) {
359 if (this._doubleClickTimer === null) {
360 this._lastTouchPos = pos;
361 } else {
362 clearTimeout(this._doubleClickTimer);
363
364 // When the distance between the two touches is small enough
365 // force the position of the latter touch to the position of
366 // the first.
367
368 var xs = this._lastTouchPos.x - pos.x;
369 var ys = this._lastTouchPos.y - pos.y;
370 var d = Math.sqrt((xs * xs) + (ys * ys));
371
372 // The goal is to trigger on a certain physical width, the
373 // devicePixelRatio brings us a bit closer but is not optimal.
374 var threshold = 20 * (window.devicePixelRatio || 1);
375 if (d < threshold) {
376 pos = this._lastTouchPos;
d6e281ba 377 }
a4ec2f5c 378 }
6d6f0db0 379 this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
b2f1961a 380 }
6d6f0db0
SR
381 bmask = this._touchButton;
382 // If bmask is set
383 } else if (e.which) {
384 /* everything except IE */
385 bmask = 1 << e.button;
386 } else {
387 /* IE including 9 */
388 bmask = (e.button & 0x1) + // Left
389 (e.button & 0x2) * 2 + // Right
390 (e.button & 0x4) / 2; // Middle
391 }
d6e281ba 392
6d6f0db0
SR
393 if (this._onMouseButton) {
394 Log.Debug("onMouseButton " + (down ? "down" : "up") +
395 ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
396 this._onMouseButton(pos.x, pos.y, down, bmask);
397 }
398 stopEvent(e);
399 },
d6e281ba 400
6d6f0db0
SR
401 _handleMouseDown: function (e) {
402 this._captureMouse();
403 this._handleMouseButton(e, 1);
404 },
d6e281ba 405
6d6f0db0
SR
406 _handleMouseUp: function (e) {
407 if (!this._mouseCaptured) { return; }
d6e281ba 408
6d6f0db0
SR
409 this._handleMouseButton(e, 0);
410 this._releaseMouse();
411 },
d6e281ba 412
6d6f0db0
SR
413 _handleMouseWheel: function (e) {
414 if (!this._focused) { return; }
d6e281ba 415
6d6f0db0 416 var pos = this._getMousePosition(e);
ebb9086a 417
6d6f0db0
SR
418 if (this._onMouseButton) {
419 if (e.deltaX < 0) {
420 this._onMouseButton(pos.x, pos.y, 1, 1 << 5);
421 this._onMouseButton(pos.x, pos.y, 0, 1 << 5);
422 } else if (e.deltaX > 0) {
423 this._onMouseButton(pos.x, pos.y, 1, 1 << 6);
424 this._onMouseButton(pos.x, pos.y, 0, 1 << 6);
d6e281ba 425 }
ebb9086a 426
6d6f0db0
SR
427 if (e.deltaY < 0) {
428 this._onMouseButton(pos.x, pos.y, 1, 1 << 3);
429 this._onMouseButton(pos.x, pos.y, 0, 1 << 3);
430 } else if (e.deltaY > 0) {
431 this._onMouseButton(pos.x, pos.y, 1, 1 << 4);
432 this._onMouseButton(pos.x, pos.y, 0, 1 << 4);
d6e281ba 433 }
6d6f0db0 434 }
d6e281ba 435
6d6f0db0
SR
436 stopEvent(e);
437 },
af1b2ae1 438
6d6f0db0
SR
439 _handleMouseMove: function (e) {
440 if (! this._focused) { return; }
d6e281ba 441
6d6f0db0
SR
442 var pos = this._getMousePosition(e);
443 if (this._onMouseMove) {
444 this._onMouseMove(pos.x, pos.y);
445 }
446 stopEvent(e);
447 },
448
449 _handleMouseDisable: function (e) {
450 if (!this._focused) { return; }
451
452 /*
453 * Stop propagation if inside canvas area
454 * Note: This is only needed for the 'click' event as it fails
455 * to fire properly for the target element so we have
456 * to listen on the document element instead.
457 */
458 if (e.target == this._target) {
459 stopEvent(e);
460 }
461 },
462
463 // Return coordinates relative to target
464 _getMousePosition: function(e) {
465 e = getPointerEvent(e);
466 var bounds = this._target.getBoundingClientRect();
467 var x, y;
468 // Clip to target bounds
469 if (e.clientX < bounds.left) {
470 x = 0;
471 } else if (e.clientX >= bounds.right) {
472 x = bounds.width - 1;
473 } else {
474 x = e.clientX - bounds.left;
475 }
476 if (e.clientY < bounds.top) {
477 y = 0;
478 } else if (e.clientY >= bounds.bottom) {
479 y = bounds.height - 1;
480 } else {
481 y = e.clientY - bounds.top;
482 }
483 return {x:x, y:y};
484 },
485
486 // Public methods
487 grab: function () {
488 var c = this._target;
489
490 if (isTouchDevice) {
491 c.addEventListener('touchstart', this._eventHandlers.mousedown);
492 window.addEventListener('touchend', this._eventHandlers.mouseup);
493 c.addEventListener('touchend', this._eventHandlers.mouseup);
494 c.addEventListener('touchmove', this._eventHandlers.mousemove);
495 }
496 c.addEventListener('mousedown', this._eventHandlers.mousedown);
497 window.addEventListener('mouseup', this._eventHandlers.mouseup);
498 c.addEventListener('mouseup', this._eventHandlers.mouseup);
499 c.addEventListener('mousemove', this._eventHandlers.mousemove);
500 c.addEventListener('wheel', this._eventHandlers.mousewheel);
501
502 /* Prevent middle-click pasting (see above for why we bind to document) */
503 document.addEventListener('click', this._eventHandlers.mousedisable);
504
505 /* preventDefault() on mousedown doesn't stop this event for some
506 reason so we have to explicitly block it */
507 c.addEventListener('contextmenu', this._eventHandlers.mousedisable);
508 },
509
510 ungrab: function () {
511 var c = this._target;
512
513 if (isTouchDevice) {
514 c.removeEventListener('touchstart', this._eventHandlers.mousedown);
515 window.removeEventListener('touchend', this._eventHandlers.mouseup);
516 c.removeEventListener('touchend', this._eventHandlers.mouseup);
517 c.removeEventListener('touchmove', this._eventHandlers.mousemove);
518 }
519 c.removeEventListener('mousedown', this._eventHandlers.mousedown);
520 window.removeEventListener('mouseup', this._eventHandlers.mouseup);
521 c.removeEventListener('mouseup', this._eventHandlers.mouseup);
522 c.removeEventListener('mousemove', this._eventHandlers.mousemove);
523 c.removeEventListener('wheel', this._eventHandlers.mousewheel);
d6e281ba 524
6d6f0db0 525 document.removeEventListener('click', this._eventHandlers.mousedisable);
d6e281ba 526
6d6f0db0
SR
527 c.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
528 }
529};
530
531make_properties(Mouse, [
532 ['target', 'ro', 'dom'], // DOM element that captures mouse input
6d6f0db0 533 ['focused', 'rw', 'bool'], // Capture and send mouse clicks/movement
d6e281ba 534
6d6f0db0
SR
535 ['onMouseButton', 'rw', 'func'], // Handler for mouse button click/release
536 ['onMouseMove', 'rw', 'func'], // Handler for mouse movement
537 ['touchButton', 'rw', 'int'] // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
538]);
d6e281ba 539
6d6f0db0 540export { Keyboard, Mouse };