211759d77613a9bb8dadf3b0f4060e241317f586
[guacamole-common-js.git] / src / main / resources / mouse.js
1
2 /*
3  *  Guacamole - Clientless Remote Desktop
4  *  Copyright (C) 2010  Michael Jumper
5  *
6  *  This program is free software: you can redistribute it and/or modify
7  *  it under the terms of the GNU Affero General Public License as published by
8  *  the Free Software Foundation, either version 3 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU Affero General Public License for more details.
15  *
16  *  You should have received a copy of the GNU Affero General Public License
17  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 // Guacamole namespace
21 var Guacamole = Guacamole || {};
22
23 /**
24  * Provides cross-browser mouse events for a given element. The events of
25  * the given element are automatically populated with handlers that translate
26  * mouse events into a non-browser-specific event provided by the
27  * Guacamole.Mouse instance.
28  * 
29  * Touch event support is planned, but currently only in testing (translate
30  * touch events into mouse events).
31  * 
32  * @constructor
33  * @param {Element} element The Element to use to provide mouse events.
34  */
35 Guacamole.Mouse = function(element) {
36
37     /**
38      * Reference to this Guacamole.Mouse.
39      * @private
40      */
41     var guac_mouse = this;
42
43     /**
44      * The current mouse state. The properties of this state are updated when
45      * mouse events fire. This state object is also passed in as a parameter to
46      * the handler of any mouse events.
47      * 
48      * @type Guacamole.Mouse.State
49      */
50     this.currentState = new Guacamole.Mouse.State(
51         0, 0, 
52         false, false, false, false, false
53     );
54
55     /**
56      * Fired whenever the user presses a mouse button down over the element
57      * associated with this Guacamole.Mouse.
58      * 
59      * @event
60      * @param {Guacamole.Mouse.State} state The current mouse state.
61      */
62         this.onmousedown = null;
63
64     /**
65      * Fired whenever the user releases a mouse button down over the element
66      * associated with this Guacamole.Mouse.
67      * 
68      * @event
69      * @param {Guacamole.Mouse.State} state The current mouse state.
70      */
71         this.onmouseup = null;
72
73     /**
74      * Fired whenever the user moves the mouse over the element associated with
75      * this Guacamole.Mouse.
76      * 
77      * @event
78      * @param {Guacamole.Mouse.State} state The current mouse state.
79      */
80         this.onmousemove = null;
81
82     function moveMouse(pageX, pageY) {
83
84         guac_mouse.currentState.x = pageX - element.offsetLeft;
85         guac_mouse.currentState.y = pageY - element.offsetTop;
86
87         // This is all JUST so we can get the mouse position within the element
88         var parent = element.offsetParent;
89         while (parent) {
90             if (parent.offsetLeft && parent.offsetTop) {
91                 guac_mouse.currentState.x -= parent.offsetLeft;
92                 guac_mouse.currentState.y -= parent.offsetTop;
93             }
94             parent = parent.offsetParent;
95         }
96
97         if (guac_mouse.onmousemove)
98             guac_mouse.onmousemove(guac_mouse.currentState);
99
100     }
101
102
103     // Block context menu so right-click gets sent properly
104     element.oncontextmenu = function(e) {
105         return false;
106     };
107
108     element.onmousemove = function(e) {
109
110         // Don't handle if we aren't supposed to
111         if (gesture_in_progress) return;
112
113         e.stopPropagation();
114
115         moveMouse(e.pageX, e.pageY);
116
117     };
118
119     var last_touch_x = 0;
120     var last_touch_y = 0;
121     var last_touch_time = 0;
122     var pixels_moved = 0;
123
124     var gesture_in_progress = false;
125     var click_release_timeout = null;
126
127     element.ontouchend = function(e) {
128         
129         // If we're handling a gesture
130         if (gesture_in_progress) {
131             
132             e.stopPropagation();
133             e.preventDefault();
134             
135             var time = new Date().getTime();
136
137             // If mouse already down, release anad clear timeout
138             if (guac_mouse.currentState.left) {
139
140                 // Fire left button up event
141                 guac_mouse.currentState.left = false;
142                 if (guac_mouse.onmouseup)
143                     guac_mouse.onmouseup(guac_mouse.currentState);
144
145                 // Clear timeout, if set
146                 if (click_release_timeout) {
147                     window.clearTimeout(click_release_timeout);
148                     click_release_timeout = null;
149                 }
150
151             }
152
153             // If single tap detected (based on time and distance)
154             if (time - last_touch_time <= 250 && pixels_moved < 10) {
155
156                 // Fire left button down event
157                 guac_mouse.currentState.left = true;
158                 if (guac_mouse.onmousedown)
159                     guac_mouse.onmousedown(guac_mouse.currentState);
160
161                 // Delay mouse up - mouse up should be canceled if
162                 // touchstart within timeout.
163                 click_release_timeout = window.setTimeout(function() {
164                     
165                     // Fire left button up event
166                     guac_mouse.currentState.left = false;
167                     if (guac_mouse.onmouseup)
168                         guac_mouse.onmouseup(guac_mouse.currentState);
169                     
170                     // Allow mouse events now that touching is over
171                     gesture_in_progress = false;
172             
173                 }, 250);
174
175             }
176
177         }
178
179     };
180
181     element.ontouchstart = function(e) {
182
183         // Record initial touch location and time for single-touch movement
184         // and tap gestures
185         if (e.touches.length == 1) {
186
187             e.stopPropagation();
188             e.preventDefault();
189
190             // Stop mouse events while touching
191             gesture_in_progress = true;
192
193             // Clear timeout, if set
194             if (click_release_timeout) {
195                 window.clearTimeout(click_release_timeout);
196                 click_release_timeout = null;
197             }
198
199             // Record touch location and time
200             var starting_touch = e.touches[0];
201             last_touch_x = starting_touch.pageX;
202             last_touch_y = starting_touch.pageY;
203             last_touch_time = new Date().getTime();
204             pixels_moved = 0;
205
206             // TODO: Handle different buttons
207
208         }
209
210     };
211
212     element.ontouchmove = function(e) {
213
214         // Handle single-touch movement gesture (touchpad mouse move)
215         if (e.touches.length == 1) {
216
217             e.stopPropagation();
218             e.preventDefault();
219
220             // Get change in touch location
221             var touch = e.touches[0];
222             var delta_x = touch.pageX - last_touch_x;
223             var delta_y = touch.pageY - last_touch_y;
224
225             // Track pixels moved
226             pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
227
228             // Update mouse location
229             guac_mouse.currentState.x += delta_x;
230             guac_mouse.currentState.y += delta_y;
231
232             // FIXME: Prevent mouse from leaving screen
233
234             // Fire movement event, if defined
235             if (guac_mouse.onmousemove)
236                 guac_mouse.onmousemove(guac_mouse.currentState);
237
238             // Update touch location
239             last_touch_x = touch.pageX;
240             last_touch_y = touch.pageY;
241
242         }
243
244     };
245
246
247     element.onmousedown = function(e) {
248
249         // Don't handle if we aren't supposed to
250         if (gesture_in_progress) return;
251
252         e.stopPropagation();
253
254         switch (e.button) {
255             case 0:
256                 guac_mouse.currentState.left = true;
257                 break;
258             case 1:
259                 guac_mouse.currentState.middle = true;
260                 break;
261             case 2:
262                 guac_mouse.currentState.right = true;
263                 break;
264         }
265
266         if (guac_mouse.onmousedown)
267             guac_mouse.onmousedown(guac_mouse.currentState);
268
269     };
270
271
272     element.onmouseup = function(e) {
273
274         // Don't handle if we aren't supposed to
275         if (gesture_in_progress) return;
276
277         e.stopPropagation();
278
279         switch (e.button) {
280             case 0:
281                 guac_mouse.currentState.left = false;
282                 break;
283             case 1:
284                 guac_mouse.currentState.middle = false;
285                 break;
286             case 2:
287                 guac_mouse.currentState.right = false;
288                 break;
289         }
290
291         if (guac_mouse.onmouseup)
292             guac_mouse.onmouseup(guac_mouse.currentState);
293
294     };
295
296     element.onmouseout = function(e) {
297
298         // Don't handle if we aren't supposed to
299         if (gesture_in_progress) return;
300
301         e.stopPropagation();
302
303         // Release all buttons
304         if (guac_mouse.currentState.left
305             || guac_mouse.currentState.middle
306             || guac_mouse.currentState.right) {
307
308             guac_mouse.currentState.left = false;
309             guac_mouse.currentState.middle = false;
310             guac_mouse.currentState.right = false;
311
312             if (guac_mouse.onmouseup)
313                 guac_mouse.onmouseup(guac_mouse.currentState);
314         }
315
316     };
317
318     // Override selection on mouse event element.
319     element.onselectstart = function() {
320         return false;
321     };
322
323     // Scroll wheel support
324     element.onmousewheel = function(e) {
325
326         // Don't handle if we aren't supposed to
327         if (gesture_in_progress) return;
328
329         var delta = 0;
330         if (e.detail)
331             delta = e.detail;
332         else if (e.wheelDelta)
333             delta = -event.wheelDelta;
334
335         // Up
336         if (delta < 0) {
337             if (guac_mouse.onmousedown) {
338                 guac_mouse.currentState.up = true;
339                 guac_mouse.onmousedown(guac_mouse.currentState);
340             }
341
342             if (guac_mouse.onmouseup) {
343                 guac_mouse.currentState.up = false;
344                 guac_mouse.onmouseup(guac_mouse.currentState);
345             }
346         }
347
348         // Down
349         if (delta > 0) {
350             if (guac_mouse.onmousedown) {
351                 guac_mouse.currentState.down = true;
352                 guac_mouse.onmousedown(guac_mouse.currentState);
353             }
354
355             if (guac_mouse.onmouseup) {
356                 guac_mouse.currentState.down = false;
357                 guac_mouse.onmouseup(guac_mouse.currentState);
358             }
359         }
360
361         if (e.preventDefault)
362             e.preventDefault();
363
364         e.returnValue = false;
365
366     };
367
368     element.addEventListener('DOMMouseScroll', element.onmousewheel, false);
369
370 };
371
372 /**
373  * Simple container for properties describing the state of a mouse.
374  * 
375  * @constructor
376  * @param {Number} x The X position of the mouse pointer in pixels.
377  * @param {Number} y The Y position of the mouse pointer in pixels.
378  * @param {Boolean} left Whether the left mouse button is pressed. 
379  * @param {Boolean} middle Whether the middle mouse button is pressed. 
380  * @param {Boolean} right Whether the right mouse button is pressed. 
381  * @param {Boolean} up Whether the up mouse button is pressed (the fourth
382  *                     button, usually part of a scroll wheel). 
383  * @param {Boolean} down Whether the down mouse button is pressed (the fifth
384  *                       button, usually part of a scroll wheel). 
385  */
386 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
387
388     /**
389      * The current X position of the mouse pointer.
390      * @type Number
391      */
392     this.x = x;
393
394     /**
395      * The current Y position of the mouse pointer.
396      * @type Number
397      */
398     this.y = y;
399
400     /**
401      * Whether the left mouse button is currently pressed.
402      * @type Boolean
403      */
404     this.left = left;
405
406     /**
407      * Whether the middle mouse button is currently pressed.
408      * @type Boolean
409      */
410     this.middle = middle
411
412     /**
413      * Whether the right mouse button is currently pressed.
414      * @type Boolean
415      */
416     this.right = right;
417
418     /**
419      * Whether the up mouse button is currently pressed. This is the fourth
420      * mouse button, associated with upward scrolling of the mouse scroll
421      * wheel.
422      * @type Boolean
423      */
424     this.up = up;
425
426     /**
427      * Whether the down mouse button is currently pressed. This is the fifth 
428      * mouse button, associated with downward scrolling of the mouse scroll
429      * wheel.
430      * @type Boolean
431      */
432     this.down = down;
433     
434 };
435