Fix jsdoc, add missing documentation.
[guacamole-common-js.git] / src / main / resources / mouse.js
1
2 /* ***** BEGIN LICENSE BLOCK *****
3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4  *
5  * The contents of this file are subject to the Mozilla Public License Version
6  * 1.1 (the "License"); you may not use this file except in compliance with
7  * the License. You may obtain a copy of the License at
8  * http://www.mozilla.org/MPL/
9  *
10  * Software distributed under the License is distributed on an "AS IS" basis,
11  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
12  * for the specific language governing rights and limitations under the
13  * License.
14  *
15  * The Original Code is guacamole-common-js.
16  *
17  * The Initial Developer of the Original Code is
18  * Michael Jumper.
19  * Portions created by the Initial Developer are Copyright (C) 2010
20  * the Initial Developer. All Rights Reserved.
21  *
22  * Contributor(s):
23  *
24  * Alternatively, the contents of this file may be used under the terms of
25  * either the GNU General Public License Version 2 or later (the "GPL"), or
26  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
27  * in which case the provisions of the GPL or the LGPL are applicable instead
28  * of those above. If you wish to allow use of your version of this file only
29  * under the terms of either the GPL or the LGPL, and not to allow others to
30  * use your version of this file under the terms of the MPL, indicate your
31  * decision by deleting the provisions above and replace them with the notice
32  * and other provisions required by the GPL or the LGPL. If you do not delete
33  * the provisions above, a recipient may use your version of this file under
34  * the terms of any one of the MPL, the GPL or the LGPL.
35  *
36  * ***** END LICENSE BLOCK ***** */
37
38 /**
39  * Namespace for all Guacamole JavaScript objects.
40  * @namespace
41  */
42 var Guacamole = Guacamole || {};
43
44 /**
45  * Provides cross-browser mouse events for a given element. The events of
46  * the given element are automatically populated with handlers that translate
47  * mouse events into a non-browser-specific event provided by the
48  * Guacamole.Mouse instance.
49  * 
50  * @constructor
51  * @param {Element} element The Element to use to provide mouse events.
52  */
53 Guacamole.Mouse = function(element) {
54
55     /**
56      * Reference to this Guacamole.Mouse.
57      * @private
58      */
59     var guac_mouse = this;
60
61     /**
62      * The number of mousemove events to require before re-enabling mouse
63      * event handling after receiving a touch event.
64      */
65     this.touchMouseThreshold = 3;
66
67     /**
68      * The current mouse state. The properties of this state are updated when
69      * mouse events fire. This state object is also passed in as a parameter to
70      * the handler of any mouse events.
71      * 
72      * @type Guacamole.Mouse.State
73      */
74     this.currentState = new Guacamole.Mouse.State(
75         0, 0, 
76         false, false, false, false, false
77     );
78
79     /**
80      * Fired whenever the user presses a mouse button down over the element
81      * associated with this Guacamole.Mouse.
82      * 
83      * @event
84      * @param {Guacamole.Mouse.State} state The current mouse state.
85      */
86         this.onmousedown = null;
87
88     /**
89      * Fired whenever the user releases a mouse button down over the element
90      * associated with this Guacamole.Mouse.
91      * 
92      * @event
93      * @param {Guacamole.Mouse.State} state The current mouse state.
94      */
95         this.onmouseup = null;
96
97     /**
98      * Fired whenever the user moves the mouse over the element associated with
99      * this Guacamole.Mouse.
100      * 
101      * @event
102      * @param {Guacamole.Mouse.State} state The current mouse state.
103      */
104         this.onmousemove = null;
105
106     /**
107      * Counter of mouse events to ignore. This decremented by mousemove, and
108      * while non-zero, mouse events will have no effect.
109      * @private
110      */
111     var ignore_mouse = 0;
112
113     function cancelEvent(e) {
114         e.stopPropagation();
115         if (e.preventDefault) e.preventDefault();
116         e.returnValue = false;
117     }
118
119     // Block context menu so right-click gets sent properly
120     element.addEventListener("contextmenu", function(e) {
121         cancelEvent(e);
122     }, false);
123
124     element.addEventListener("mousemove", function(e) {
125
126         cancelEvent(e);
127
128         // If ignoring events, decrement counter
129         if (ignore_mouse) {
130             ignore_mouse--;
131             return;
132         }
133
134         guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY);
135
136         if (guac_mouse.onmousemove)
137             guac_mouse.onmousemove(guac_mouse.currentState);
138
139     }, false);
140
141     element.addEventListener("mousedown", function(e) {
142
143         cancelEvent(e);
144
145         // Do not handle if ignoring events
146         if (ignore_mouse)
147             return;
148
149         switch (e.button) {
150             case 0:
151                 guac_mouse.currentState.left = true;
152                 break;
153             case 1:
154                 guac_mouse.currentState.middle = true;
155                 break;
156             case 2:
157                 guac_mouse.currentState.right = true;
158                 break;
159         }
160
161         if (guac_mouse.onmousedown)
162             guac_mouse.onmousedown(guac_mouse.currentState);
163
164     }, false);
165
166     element.addEventListener("mouseup", function(e) {
167
168         cancelEvent(e);
169
170         // Do not handle if ignoring events
171         if (ignore_mouse)
172             return;
173
174         switch (e.button) {
175             case 0:
176                 guac_mouse.currentState.left = false;
177                 break;
178             case 1:
179                 guac_mouse.currentState.middle = false;
180                 break;
181             case 2:
182                 guac_mouse.currentState.right = false;
183                 break;
184         }
185
186         if (guac_mouse.onmouseup)
187             guac_mouse.onmouseup(guac_mouse.currentState);
188
189     }, false);
190
191     element.addEventListener("mouseout", function(e) {
192
193         // Get parent of the element the mouse pointer is leaving
194         if (!e) e = window.event;
195
196         // Check that mouseout is due to actually LEAVING the element
197         var target = e.relatedTarget || e.toElement;
198         while (target != null) {
199             if (target === element)
200                 return;
201             target = target.parentNode;
202         }
203
204         cancelEvent(e);
205
206         // Release all buttons
207         if (guac_mouse.currentState.left
208             || guac_mouse.currentState.middle
209             || guac_mouse.currentState.right) {
210
211             guac_mouse.currentState.left = false;
212             guac_mouse.currentState.middle = false;
213             guac_mouse.currentState.right = false;
214
215             if (guac_mouse.onmouseup)
216                 guac_mouse.onmouseup(guac_mouse.currentState);
217         }
218
219     }, false);
220
221     // Override selection on mouse event element.
222     element.addEventListener("selectstart", function(e) {
223         cancelEvent(e);
224     }, false);
225
226     // Ignore all pending mouse events when touch events are the apparent source
227     function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
228
229     element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
230     element.addEventListener("touchstart", ignorePendingMouseEvents, false);
231     element.addEventListener("touchend",   ignorePendingMouseEvents, false);
232
233     // Scroll wheel support
234     function mousewheel_handler(e) {
235
236         var delta = 0;
237         if (e.detail)
238             delta = e.detail;
239         else if (e.wheelDelta)
240             delta = -event.wheelDelta;
241
242         // Up
243         if (delta < 0) {
244             if (guac_mouse.onmousedown) {
245                 guac_mouse.currentState.up = true;
246                 guac_mouse.onmousedown(guac_mouse.currentState);
247             }
248
249             if (guac_mouse.onmouseup) {
250                 guac_mouse.currentState.up = false;
251                 guac_mouse.onmouseup(guac_mouse.currentState);
252             }
253         }
254
255         // Down
256         if (delta > 0) {
257             if (guac_mouse.onmousedown) {
258                 guac_mouse.currentState.down = true;
259                 guac_mouse.onmousedown(guac_mouse.currentState);
260             }
261
262             if (guac_mouse.onmouseup) {
263                 guac_mouse.currentState.down = false;
264                 guac_mouse.onmouseup(guac_mouse.currentState);
265             }
266         }
267
268         cancelEvent(e);
269
270     }
271     element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
272     element.addEventListener('mousewheel',     mousewheel_handler, false);
273
274 };
275
276
277 /**
278  * Provides cross-browser relative touch event translation for a given element.
279  * 
280  * Touch events are translated into mouse events as if the touches occurred
281  * on a touchpad (drag to push the mouse pointer, tap to click).
282  * 
283  * @constructor
284  * @param {Element} element The Element to use to provide touch events.
285  */
286 Guacamole.Mouse.Touchpad = function(element) {
287
288     /**
289      * Reference to this Guacamole.Mouse.Touchpad.
290      * @private
291      */
292     var guac_touchpad = this;
293
294     /**
295      * The distance a two-finger touch must move per scrollwheel event, in
296      * pixels.
297      */
298     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
299
300     /**
301      * The maximum number of milliseconds to wait for a touch to end for the
302      * gesture to be considered a click.
303      */
304     this.clickTimingThreshold = 250;
305
306     /**
307      * The maximum number of pixels to allow a touch to move for the gesture to
308      * be considered a click.
309      */
310     this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
311
312     /**
313      * The current mouse state. The properties of this state are updated when
314      * mouse events fire. This state object is also passed in as a parameter to
315      * the handler of any mouse events.
316      * 
317      * @type Guacamole.Mouse.State
318      */
319     this.currentState = new Guacamole.Mouse.State(
320         0, 0, 
321         false, false, false, false, false
322     );
323
324     /**
325      * Fired whenever a mouse button is effectively pressed. This can happen
326      * as part of a "click" gesture initiated by the user by tapping one
327      * or more fingers over the touchpad element, as part of a "scroll"
328      * gesture initiated by dragging two fingers up or down, etc.
329      * 
330      * @event
331      * @param {Guacamole.Mouse.State} state The current mouse state.
332      */
333         this.onmousedown = null;
334
335     /**
336      * Fired whenever a mouse button is effectively released. This can happen
337      * as part of a "click" gesture initiated by the user by tapping one
338      * or more fingers over the touchpad element, as part of a "scroll"
339      * gesture initiated by dragging two fingers up or down, etc.
340      * 
341      * @event
342      * @param {Guacamole.Mouse.State} state The current mouse state.
343      */
344         this.onmouseup = null;
345
346     /**
347      * Fired whenever the user moves the mouse by dragging their finger over
348      * the touchpad element.
349      * 
350      * @event
351      * @param {Guacamole.Mouse.State} state The current mouse state.
352      */
353         this.onmousemove = null;
354
355     var touch_count = 0;
356     var last_touch_x = 0;
357     var last_touch_y = 0;
358     var last_touch_time = 0;
359     var pixels_moved = 0;
360
361     var touch_buttons = {
362         1: "left",
363         2: "right",
364         3: "middle"
365     };
366
367     var gesture_in_progress = false;
368     var click_release_timeout = null;
369
370     element.addEventListener("touchend", function(e) {
371         
372         e.stopPropagation();
373         e.preventDefault();
374             
375         // If we're handling a gesture AND this is the last touch
376         if (gesture_in_progress && e.touches.length == 0) {
377             
378             var time = new Date().getTime();
379
380             // Get corresponding mouse button
381             var button = touch_buttons[touch_count];
382
383             // If mouse already down, release anad clear timeout
384             if (guac_touchpad.currentState[button]) {
385
386                 // Fire button up event
387                 guac_touchpad.currentState[button] = false;
388                 if (guac_touchpad.onmouseup)
389                     guac_touchpad.onmouseup(guac_touchpad.currentState);
390
391                 // Clear timeout, if set
392                 if (click_release_timeout) {
393                     window.clearTimeout(click_release_timeout);
394                     click_release_timeout = null;
395                 }
396
397             }
398
399             // If single tap detected (based on time and distance)
400             if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
401                     && pixels_moved < guac_touchpad.clickMoveThreshold) {
402
403                 // Fire button down event
404                 guac_touchpad.currentState[button] = true;
405                 if (guac_touchpad.onmousedown)
406                     guac_touchpad.onmousedown(guac_touchpad.currentState);
407
408                 // Delay mouse up - mouse up should be canceled if
409                 // touchstart within timeout.
410                 click_release_timeout = window.setTimeout(function() {
411                     
412                     // Fire button up event
413                     guac_touchpad.currentState[button] = false;
414                     if (guac_touchpad.onmouseup)
415                         guac_touchpad.onmouseup(guac_touchpad.currentState);
416                     
417                     // Gesture now over
418                     gesture_in_progress = false;
419
420                 }, guac_touchpad.clickTimingThreshold);
421
422             }
423
424             // If we're not waiting to see if this is a click, stop gesture
425             if (!click_release_timeout)
426                 gesture_in_progress = false;
427
428         }
429
430     }, false);
431
432     element.addEventListener("touchstart", function(e) {
433
434         e.stopPropagation();
435         e.preventDefault();
436
437         // Track number of touches, but no more than three
438         touch_count = Math.min(e.touches.length, 3);
439
440         // Clear timeout, if set
441         if (click_release_timeout) {
442             window.clearTimeout(click_release_timeout);
443             click_release_timeout = null;
444         }
445
446         // Record initial touch location and time for touch movement
447         // and tap gestures
448         if (!gesture_in_progress) {
449
450             // Stop mouse events while touching
451             gesture_in_progress = true;
452
453             // Record touch location and time
454             var starting_touch = e.touches[0];
455             last_touch_x = starting_touch.clientX;
456             last_touch_y = starting_touch.clientY;
457             last_touch_time = new Date().getTime();
458             pixels_moved = 0;
459
460         }
461
462     }, false);
463
464     element.addEventListener("touchmove", function(e) {
465
466         e.stopPropagation();
467         e.preventDefault();
468
469         // Get change in touch location
470         var touch = e.touches[0];
471         var delta_x = touch.clientX - last_touch_x;
472         var delta_y = touch.clientY - last_touch_y;
473
474         // Track pixels moved
475         pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
476
477         // If only one touch involved, this is mouse move
478         if (touch_count == 1) {
479
480             // Calculate average velocity in Manhatten pixels per millisecond
481             var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
482
483             // Scale mouse movement relative to velocity
484             var scale = 1 + velocity;
485
486             // Update mouse location
487             guac_touchpad.currentState.x += delta_x*scale;
488             guac_touchpad.currentState.y += delta_y*scale;
489
490             // Prevent mouse from leaving screen
491
492             if (guac_touchpad.currentState.x < 0)
493                 guac_touchpad.currentState.x = 0;
494             else if (guac_touchpad.currentState.x >= element.offsetWidth)
495                 guac_touchpad.currentState.x = element.offsetWidth - 1;
496
497             if (guac_touchpad.currentState.y < 0)
498                 guac_touchpad.currentState.y = 0;
499             else if (guac_touchpad.currentState.y >= element.offsetHeight)
500                 guac_touchpad.currentState.y = element.offsetHeight - 1;
501
502             // Fire movement event, if defined
503             if (guac_touchpad.onmousemove)
504                 guac_touchpad.onmousemove(guac_touchpad.currentState);
505
506             // Update touch location
507             last_touch_x = touch.clientX;
508             last_touch_y = touch.clientY;
509
510         }
511
512         // Interpret two-finger swipe as scrollwheel
513         else if (touch_count == 2) {
514
515             // If change in location passes threshold for scroll
516             if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
517
518                 // Decide button based on Y movement direction
519                 var button;
520                 if (delta_y > 0) button = "down";
521                 else             button = "up";
522
523                 // Fire button down event
524                 guac_touchpad.currentState[button] = true;
525                 if (guac_touchpad.onmousedown)
526                     guac_touchpad.onmousedown(guac_touchpad.currentState);
527
528                 // Fire button up event
529                 guac_touchpad.currentState[button] = false;
530                 if (guac_touchpad.onmouseup)
531                     guac_touchpad.onmouseup(guac_touchpad.currentState);
532
533                 // Only update touch location after a scroll has been
534                 // detected
535                 last_touch_x = touch.clientX;
536                 last_touch_y = touch.clientY;
537
538             }
539
540         }
541
542     }, false);
543
544 };
545
546 /**
547  * Provides cross-browser absolute touch event translation for a given element.
548  * 
549  * Touch events are translated into mouse events as if the touches occurred
550  * on a touchscreen (tapping anywhere on the screen clicks at that point,
551  * long-press to right-click).
552  * 
553  * @constructor
554  * @param {Element} element The Element to use to provide touch events.
555  */
556 Guacamole.Mouse.Touchscreen = function(element) {
557
558     /**
559      * Reference to this Guacamole.Mouse.Touchscreen.
560      * @private
561      */
562     var guac_touchscreen = this;
563
564     /**
565      * The distance a two-finger touch must move per scrollwheel event, in
566      * pixels.
567      */
568     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
569
570     /**
571      * The current mouse state. The properties of this state are updated when
572      * mouse events fire. This state object is also passed in as a parameter to
573      * the handler of any mouse events.
574      * 
575      * @type Guacamole.Mouse.State
576      */
577     this.currentState = new Guacamole.Mouse.State(
578         0, 0, 
579         false, false, false, false, false
580     );
581
582     /**
583      * Fired whenever a mouse button is effectively pressed. This can happen
584      * as part of a "mousedown" gesture initiated by the user by pressing one
585      * finger over the touchscreen element, as part of a "scroll" gesture
586      * initiated by dragging two fingers up or down, etc.
587      * 
588      * @event
589      * @param {Guacamole.Mouse.State} state The current mouse state.
590      */
591         this.onmousedown = null;
592
593     /**
594      * Fired whenever a mouse button is effectively released. This can happen
595      * as part of a "mouseup" gesture initiated by the user by removing the
596      * finger pressed against the touchscreen element, or as part of a "scroll"
597      * gesture initiated by dragging two fingers up or down, etc.
598      * 
599      * @event
600      * @param {Guacamole.Mouse.State} state The current mouse state.
601      */
602         this.onmouseup = null;
603
604     /**
605      * Fired whenever the user moves the mouse by dragging their finger over
606      * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad,
607      * dragging a finger over the touchscreen element will always cause
608      * the mouse button to be effectively down, as if clicking-and-dragging.
609      * 
610      * @event
611      * @param {Guacamole.Mouse.State} state The current mouse state.
612      */
613         this.onmousemove = null;
614
615     element.addEventListener("touchend", function(e) {
616         
617         // Ignore if more than one touch
618         if (e.touches.length + e.changedTouches.length != 1)
619             return;
620
621         e.stopPropagation();
622         e.preventDefault();
623
624         // Release button
625         guac_touchscreen.currentState.left = false;
626
627         // Fire release event when the last touch is released, if event defined
628         if (e.touches.length == 0 && guac_touchscreen.onmouseup)
629             guac_touchscreen.onmouseup(guac_touchscreen.currentState);
630
631     }, false);
632
633     element.addEventListener("touchstart", function(e) {
634
635         // Ignore if more than one touch
636         if (e.touches.length != 1)
637             return;
638
639         e.stopPropagation();
640         e.preventDefault();
641
642         // Get touch
643         var touch = e.touches[0];
644
645         // Update state
646         guac_touchscreen.currentState.left = true;
647         guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY);
648
649         // Fire press event, if defined
650         if (guac_touchscreen.onmousedown)
651             guac_touchscreen.onmousedown(guac_touchscreen.currentState);
652
653
654     }, false);
655
656     element.addEventListener("touchmove", function(e) {
657
658         // Ignore if more than one touch
659         if (e.touches.length != 1)
660             return;
661
662         e.stopPropagation();
663         e.preventDefault();
664
665         // Get touch
666         var touch = e.touches[0];
667
668         // Update state
669         guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY);
670
671         // Fire movement event, if defined
672         if (guac_touchscreen.onmousemove)
673             guac_touchscreen.onmousemove(guac_touchscreen.currentState);
674
675     }, false);
676
677 };
678
679 /**
680  * Simple container for properties describing the state of a mouse.
681  * 
682  * @constructor
683  * @param {Number} x The X position of the mouse pointer in pixels.
684  * @param {Number} y The Y position of the mouse pointer in pixels.
685  * @param {Boolean} left Whether the left mouse button is pressed. 
686  * @param {Boolean} middle Whether the middle mouse button is pressed. 
687  * @param {Boolean} right Whether the right mouse button is pressed. 
688  * @param {Boolean} up Whether the up mouse button is pressed (the fourth
689  *                     button, usually part of a scroll wheel). 
690  * @param {Boolean} down Whether the down mouse button is pressed (the fifth
691  *                       button, usually part of a scroll wheel). 
692  */
693 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
694
695     /**
696      * Reference to this Guacamole.Mouse.State.
697      * @private
698      */
699     var guac_state = this;
700
701     /**
702      * The current X position of the mouse pointer.
703      * @type Number
704      */
705     this.x = x;
706
707     /**
708      * The current Y position of the mouse pointer.
709      * @type Number
710      */
711     this.y = y;
712
713     /**
714      * Whether the left mouse button is currently pressed.
715      * @type Boolean
716      */
717     this.left = left;
718
719     /**
720      * Whether the middle mouse button is currently pressed.
721      * @type Boolean
722      */
723     this.middle = middle
724
725     /**
726      * Whether the right mouse button is currently pressed.
727      * @type Boolean
728      */
729     this.right = right;
730
731     /**
732      * Whether the up mouse button is currently pressed. This is the fourth
733      * mouse button, associated with upward scrolling of the mouse scroll
734      * wheel.
735      * @type Boolean
736      */
737     this.up = up;
738
739     /**
740      * Whether the down mouse button is currently pressed. This is the fifth 
741      * mouse button, associated with downward scrolling of the mouse scroll
742      * wheel.
743      * @type Boolean
744      */
745     this.down = down;
746
747     /**
748      * Updates the position represented within this state object by the given
749      * element and clientX/clientY coordinates (commonly available within event
750      * objects). Position is translated from clientX/clientY (relative to
751      * viewport) to element-relative coordinates.
752      * 
753      * @param {Element} element The element the coordinates should be relative
754      *                          to.
755      * @param {Number} clientX The X coordinate to translate, viewport-relative.
756      * @param {Number} clientY The Y coordinate to translate, viewport-relative.
757      */
758     this.fromClientPosition = function(element, clientX, clientY) {
759     
760         guac_state.x = clientX - element.offsetLeft;
761         guac_state.y = clientY - element.offsetTop;
762
763         // This is all JUST so we can get the mouse position within the element
764         var parent = element.offsetParent;
765         while (parent && !(parent === document.body)) {
766             guac_state.x -= parent.offsetLeft - parent.scrollLeft;
767             guac_state.y -= parent.offsetTop  - parent.scrollTop;
768
769             parent = parent.offsetParent;
770         }
771
772         // Element ultimately depends on positioning within document body,
773         // take document scroll into account. 
774         if (parent) {
775             var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
776             var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
777
778             guac_state.x -= parent.offsetLeft - documentScrollLeft;
779             guac_state.y -= parent.offsetTop  - documentScrollTop;
780         }
781
782     };
783
784 };
785