]> git.proxmox.com Git - mirror_novnc.git/blame - core/input/mouse.js
Better cleanup in vnc_playback.html
[mirror_novnc.git] / core / input / mouse.js
CommitLineData
c1e2785f
SM
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
11import * as Log from '../util/logging.js';
12import { isTouchDevice } from '../util/browsers.js';
13import { setCapture, stopEvent, getPointerEvent } from '../util/events.js';
c1e2785f 14
28b004fd
SM
15var WHEEL_STEP = 10; // Delta threshold for a mouse wheel step
16var WHEEL_STEP_TIMEOUT = 50; // ms
17var WHEEL_LINE_HEIGHT = 19;
18
747b4623 19export default function Mouse(target) {
3d7bb020 20 this._target = target || document;
c1e2785f
SM
21
22 this._doubleClickTimer = null;
23 this._lastTouchPos = null;
24
28b004fd
SM
25 this._pos = null;
26 this._wheelStepXTimer = null;
27 this._wheelStepYTimer = null;
28 this._accumulatedWheelDeltaX = 0;
29 this._accumulatedWheelDeltaY = 0;
30
c1e2785f
SM
31 this._eventHandlers = {
32 'mousedown': this._handleMouseDown.bind(this),
33 'mouseup': this._handleMouseUp.bind(this),
34 'mousemove': this._handleMouseMove.bind(this),
35 'mousewheel': this._handleMouseWheel.bind(this),
36 'mousedisable': this._handleMouseDisable.bind(this)
37 };
38};
39
40Mouse.prototype = {
747b4623
PO
41 // ===== PROPERTIES =====
42
43 touchButton: 1, // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
44
45 // ===== EVENT HANDLERS =====
46
47 onmousebutton: function () {}, // Handler for mouse button click/release
48 onmousemove: function () {}, // Handler for mouse movement
49
50 // ===== PRIVATE METHODS =====
c1e2785f
SM
51
52 _resetDoubleClickTimer: function () {
53 this._doubleClickTimer = null;
54 },
55
56 _handleMouseButton: function (e, down) {
28b004fd
SM
57 this._updateMousePosition(e);
58 var pos = this._pos;
c1e2785f
SM
59
60 var bmask;
61 if (e.touches || e.changedTouches) {
62 // Touch device
63
64 // When two touches occur within 500 ms of each other and are
65 // close enough together a double click is triggered.
66 if (down == 1) {
67 if (this._doubleClickTimer === null) {
68 this._lastTouchPos = pos;
69 } else {
70 clearTimeout(this._doubleClickTimer);
71
72 // When the distance between the two touches is small enough
73 // force the position of the latter touch to the position of
74 // the first.
75
76 var xs = this._lastTouchPos.x - pos.x;
77 var ys = this._lastTouchPos.y - pos.y;
78 var d = Math.sqrt((xs * xs) + (ys * ys));
79
80 // The goal is to trigger on a certain physical width, the
81 // devicePixelRatio brings us a bit closer but is not optimal.
82 var threshold = 20 * (window.devicePixelRatio || 1);
83 if (d < threshold) {
84 pos = this._lastTouchPos;
85 }
86 }
87 this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500);
88 }
747b4623 89 bmask = this.touchButton;
c1e2785f
SM
90 // If bmask is set
91 } else if (e.which) {
92 /* everything except IE */
93 bmask = 1 << e.button;
94 } else {
95 /* IE including 9 */
96 bmask = (e.button & 0x1) + // Left
97 (e.button & 0x2) * 2 + // Right
98 (e.button & 0x4) / 2; // Middle
99 }
100
747b4623
PO
101 Log.Debug("onmousebutton " + (down ? "down" : "up") +
102 ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask);
103 this.onmousebutton(pos.x, pos.y, down, bmask);
104
c1e2785f
SM
105 stopEvent(e);
106 },
107
108 _handleMouseDown: function (e) {
109 // Touch events have implicit capture
110 if (e.type === "mousedown") {
111 setCapture(this._target);
112 }
113
114 this._handleMouseButton(e, 1);
115 },
116
117 _handleMouseUp: function (e) {
118 this._handleMouseButton(e, 0);
119 },
120
28b004fd
SM
121 // Mouse wheel events are sent in steps over VNC. This means that the VNC
122 // protocol can't handle a wheel event with specific distance or speed.
123 // Therefor, if we get a lot of small mouse wheel events we combine them.
124 _generateWheelStepX: function () {
125
126 if (this._accumulatedWheelDeltaX < 0) {
747b4623
PO
127 this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5);
128 this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5);
28b004fd 129 } else if (this._accumulatedWheelDeltaX > 0) {
747b4623
PO
130 this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6);
131 this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6);
28b004fd
SM
132 }
133
134 this._accumulatedWheelDeltaX = 0;
135 },
136
137 _generateWheelStepY: function () {
138
139 if (this._accumulatedWheelDeltaY < 0) {
747b4623
PO
140 this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3);
141 this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3);
28b004fd 142 } else if (this._accumulatedWheelDeltaY > 0) {
747b4623
PO
143 this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4);
144 this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4);
28b004fd
SM
145 }
146
147 this._accumulatedWheelDeltaY = 0;
148 },
149
150 _resetWheelStepTimers: function () {
151 window.clearTimeout(this._wheelStepXTimer);
152 window.clearTimeout(this._wheelStepYTimer);
153 this._wheelStepXTimer = null;
154 this._wheelStepYTimer = null;
155 },
156
c1e2785f 157 _handleMouseWheel: function (e) {
28b004fd 158 this._resetWheelStepTimers();
c1e2785f 159
28b004fd 160 this._updateMousePosition(e);
c1e2785f 161
28b004fd
SM
162 var dX = e.deltaX;
163 var dY = e.deltaY;
164
165 // Pixel units unless it's non-zero.
166 // Note that if deltamode is line or page won't matter since we aren't
167 // sending the mouse wheel delta to the server anyway.
168 // The difference between pixel and line can be important however since
169 // we have a threshold that can be smaller than the line height.
170 if (e.deltaMode !== 0) {
171 dX *= WHEEL_LINE_HEIGHT;
172 dY *= WHEEL_LINE_HEIGHT;
173 }
174
175 this._accumulatedWheelDeltaX += dX;
176 this._accumulatedWheelDeltaY += dY;
177
178 // Generate a mouse wheel step event when the accumulated delta
179 // for one of the axes is large enough.
180 // Small delta events that do not pass the threshold get sent
181 // after a timeout.
182 if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) {
183 this._generateWheelStepX();
184 } else {
185 this._wheelStepXTimer =
186 window.setTimeout(this._generateWheelStepX.bind(this),
187 WHEEL_STEP_TIMEOUT);
188 }
189 if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) {
190 this._generateWheelStepY();
191 } else {
192 this._wheelStepYTimer =
193 window.setTimeout(this._generateWheelStepY.bind(this),
194 WHEEL_STEP_TIMEOUT);
c1e2785f
SM
195 }
196
197 stopEvent(e);
198 },
199
200 _handleMouseMove: function (e) {
28b004fd 201 this._updateMousePosition(e);
747b4623 202 this.onmousemove(this._pos.x, this._pos.y);
c1e2785f
SM
203 stopEvent(e);
204 },
205
206 _handleMouseDisable: function (e) {
c1e2785f
SM
207 /*
208 * Stop propagation if inside canvas area
209 * Note: This is only needed for the 'click' event as it fails
210 * to fire properly for the target element so we have
211 * to listen on the document element instead.
212 */
213 if (e.target == this._target) {
214 stopEvent(e);
215 }
216 },
217
28b004fd
SM
218 // Update coordinates relative to target
219 _updateMousePosition: function(e) {
c1e2785f
SM
220 e = getPointerEvent(e);
221 var bounds = this._target.getBoundingClientRect();
222 var x, y;
223 // Clip to target bounds
224 if (e.clientX < bounds.left) {
225 x = 0;
226 } else if (e.clientX >= bounds.right) {
227 x = bounds.width - 1;
228 } else {
229 x = e.clientX - bounds.left;
230 }
231 if (e.clientY < bounds.top) {
232 y = 0;
233 } else if (e.clientY >= bounds.bottom) {
234 y = bounds.height - 1;
235 } else {
236 y = e.clientY - bounds.top;
237 }
28b004fd 238 this._pos = {x:x, y:y};
c1e2785f
SM
239 },
240
747b4623
PO
241 // ===== PUBLIC METHODS =====
242
c1e2785f
SM
243 grab: function () {
244 var c = this._target;
245
246 if (isTouchDevice) {
247 c.addEventListener('touchstart', this._eventHandlers.mousedown);
248 c.addEventListener('touchend', this._eventHandlers.mouseup);
249 c.addEventListener('touchmove', this._eventHandlers.mousemove);
250 }
251 c.addEventListener('mousedown', this._eventHandlers.mousedown);
252 c.addEventListener('mouseup', this._eventHandlers.mouseup);
253 c.addEventListener('mousemove', this._eventHandlers.mousemove);
254 c.addEventListener('wheel', this._eventHandlers.mousewheel);
255
256 /* Prevent middle-click pasting (see above for why we bind to document) */
257 document.addEventListener('click', this._eventHandlers.mousedisable);
258
259 /* preventDefault() on mousedown doesn't stop this event for some
260 reason so we have to explicitly block it */
261 c.addEventListener('contextmenu', this._eventHandlers.mousedisable);
262 },
263
264 ungrab: function () {
265 var c = this._target;
266
28b004fd
SM
267 this._resetWheelStepTimers();
268
c1e2785f
SM
269 if (isTouchDevice) {
270 c.removeEventListener('touchstart', this._eventHandlers.mousedown);
271 c.removeEventListener('touchend', this._eventHandlers.mouseup);
272 c.removeEventListener('touchmove', this._eventHandlers.mousemove);
273 }
274 c.removeEventListener('mousedown', this._eventHandlers.mousedown);
275 c.removeEventListener('mouseup', this._eventHandlers.mouseup);
276 c.removeEventListener('mousemove', this._eventHandlers.mousemove);
277 c.removeEventListener('wheel', this._eventHandlers.mousewheel);
278
279 document.removeEventListener('click', this._eventHandlers.mousedisable);
280
281 c.removeEventListener('contextmenu', this._eventHandlers.mousedisable);
282 }
283};