1 /*
  2  * Copyright (C) 2013 Glyptodon LLC
  3  *
  4  * Permission is hereby granted, free of charge, to any person obtaining a copy
  5  * of this software and associated documentation files (the "Software"), to deal
  6  * in the Software without restriction, including without limitation the rights
  7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8  * copies of the Software, and to permit persons to whom the Software is
  9  * furnished to do so, subject to the following conditions:
 10  *
 11  * The above copyright notice and this permission notice shall be included in
 12  * all copies or substantial portions of the Software.
 13  *
 14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 20  * THE SOFTWARE.
 21  */
 22 
 23 var Guacamole = Guacamole || {};
 24 
 25 /**
 26  * Provides cross-browser mouse events for a given element. The events of
 27  * the given element are automatically populated with handlers that translate
 28  * mouse events into a non-browser-specific event provided by the
 29  * Guacamole.Mouse instance.
 30  * 
 31  * @constructor
 32  * @param {Element} element The Element to use to provide mouse events.
 33  */
 34 Guacamole.Mouse = function(element) {
 35 
 36     /**
 37      * Reference to this Guacamole.Mouse.
 38      * @private
 39      */
 40     var guac_mouse = this;
 41 
 42     /**
 43      * The number of mousemove events to require before re-enabling mouse
 44      * event handling after receiving a touch event.
 45      */
 46     this.touchMouseThreshold = 3;
 47 
 48     /**
 49      * The minimum amount of pixels scrolled required for a single scroll button
 50      * click.
 51      */
 52     this.scrollThreshold = 120;
 53 
 54     /**
 55      * The number of pixels to scroll per line.
 56      */
 57     this.PIXELS_PER_LINE = 40;
 58 
 59     /**
 60      * The number of pixels to scroll per page.
 61      */
 62     this.PIXELS_PER_PAGE = 640;
 63 
 64     /**
 65      * The current mouse state. The properties of this state are updated when
 66      * mouse events fire. This state object is also passed in as a parameter to
 67      * the handler of any mouse events.
 68      * 
 69      * @type Guacamole.Mouse.State
 70      */
 71     this.currentState = new Guacamole.Mouse.State(
 72         0, 0, 
 73         false, false, false, false, false
 74     );
 75 
 76     /**
 77      * Fired whenever the user presses a mouse button down over the element
 78      * associated with this Guacamole.Mouse.
 79      * 
 80      * @event
 81      * @param {Guacamole.Mouse.State} state The current mouse state.
 82      */
 83 	this.onmousedown = null;
 84 
 85     /**
 86      * Fired whenever the user releases a mouse button down over the element
 87      * associated with this Guacamole.Mouse.
 88      * 
 89      * @event
 90      * @param {Guacamole.Mouse.State} state The current mouse state.
 91      */
 92 	this.onmouseup = null;
 93 
 94     /**
 95      * Fired whenever the user moves the mouse over the element associated with
 96      * this Guacamole.Mouse.
 97      * 
 98      * @event
 99      * @param {Guacamole.Mouse.State} state The current mouse state.
100      */
101 	this.onmousemove = null;
102 
103     /**
104      * Counter of mouse events to ignore. This decremented by mousemove, and
105      * while non-zero, mouse events will have no effect.
106      * @private
107      */
108     var ignore_mouse = 0;
109 
110     /**
111      * Cumulative scroll delta amount. This value is accumulated through scroll
112      * events and results in scroll button clicks if it exceeds a certain
113      * threshold.
114      */
115     var scroll_delta = 0;
116 
117     function cancelEvent(e) {
118         e.stopPropagation();
119         if (e.preventDefault) e.preventDefault();
120         e.returnValue = false;
121     }
122 
123     // Block context menu so right-click gets sent properly
124     element.addEventListener("contextmenu", function(e) {
125         cancelEvent(e);
126     }, false);
127 
128     element.addEventListener("mousemove", function(e) {
129 
130         cancelEvent(e);
131 
132         // If ignoring events, decrement counter
133         if (ignore_mouse) {
134             ignore_mouse--;
135             return;
136         }
137 
138         guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY);
139 
140         if (guac_mouse.onmousemove)
141             guac_mouse.onmousemove(guac_mouse.currentState);
142 
143     }, false);
144 
145     element.addEventListener("mousedown", function(e) {
146 
147         cancelEvent(e);
148 
149         // Do not handle if ignoring events
150         if (ignore_mouse)
151             return;
152 
153         switch (e.button) {
154             case 0:
155                 guac_mouse.currentState.left = true;
156                 break;
157             case 1:
158                 guac_mouse.currentState.middle = true;
159                 break;
160             case 2:
161                 guac_mouse.currentState.right = true;
162                 break;
163         }
164 
165         if (guac_mouse.onmousedown)
166             guac_mouse.onmousedown(guac_mouse.currentState);
167 
168     }, false);
169 
170     element.addEventListener("mouseup", function(e) {
171 
172         cancelEvent(e);
173 
174         // Do not handle if ignoring events
175         if (ignore_mouse)
176             return;
177 
178         switch (e.button) {
179             case 0:
180                 guac_mouse.currentState.left = false;
181                 break;
182             case 1:
183                 guac_mouse.currentState.middle = false;
184                 break;
185             case 2:
186                 guac_mouse.currentState.right = false;
187                 break;
188         }
189 
190         if (guac_mouse.onmouseup)
191             guac_mouse.onmouseup(guac_mouse.currentState);
192 
193     }, false);
194 
195     element.addEventListener("mouseout", function(e) {
196 
197         // Get parent of the element the mouse pointer is leaving
198        	if (!e) e = window.event;
199 
200         // Check that mouseout is due to actually LEAVING the element
201         var target = e.relatedTarget || e.toElement;
202         while (target !== null) {
203             if (target === element)
204                 return;
205             target = target.parentNode;
206         }
207 
208         cancelEvent(e);
209 
210         // Release all buttons
211         if (guac_mouse.currentState.left
212             || guac_mouse.currentState.middle
213             || guac_mouse.currentState.right) {
214 
215             guac_mouse.currentState.left = false;
216             guac_mouse.currentState.middle = false;
217             guac_mouse.currentState.right = false;
218 
219             if (guac_mouse.onmouseup)
220                 guac_mouse.onmouseup(guac_mouse.currentState);
221         }
222 
223     }, false);
224 
225     // Override selection on mouse event element.
226     element.addEventListener("selectstart", function(e) {
227         cancelEvent(e);
228     }, false);
229 
230     // Ignore all pending mouse events when touch events are the apparent source
231     function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
232 
233     element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
234     element.addEventListener("touchstart", ignorePendingMouseEvents, false);
235     element.addEventListener("touchend",   ignorePendingMouseEvents, false);
236 
237     // Scroll wheel support
238     function mousewheel_handler(e) {
239 
240         // Determine approximate scroll amount (in pixels)
241         var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
242 
243         // If successfully retrieved scroll amount, convert to pixels if not
244         // already in pixels
245         if (delta) {
246 
247             // Convert to pixels if delta was lines
248             if (e.deltaMode === 1)
249                 delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
250 
251             // Convert to pixels if delta was pages
252             else if (e.deltaMode === 2)
253                 delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
254 
255         }
256 
257         // Otherwise, assume legacy mousewheel event and line scrolling
258         else
259             delta = e.detail * guac_mouse.PIXELS_PER_LINE;
260         
261         // Update overall delta
262         scroll_delta += delta;
263 
264         // Up
265         while (scroll_delta <= -guac_mouse.scrollThreshold) {
266 
267             if (guac_mouse.onmousedown) {
268                 guac_mouse.currentState.up = true;
269                 guac_mouse.onmousedown(guac_mouse.currentState);
270             }
271 
272             if (guac_mouse.onmouseup) {
273                 guac_mouse.currentState.up = false;
274                 guac_mouse.onmouseup(guac_mouse.currentState);
275             }
276 
277             scroll_delta += guac_mouse.scrollThreshold;
278 
279         }
280 
281         // Down
282         while (scroll_delta >= guac_mouse.scrollThreshold) {
283 
284             if (guac_mouse.onmousedown) {
285                 guac_mouse.currentState.down = true;
286                 guac_mouse.onmousedown(guac_mouse.currentState);
287             }
288 
289             if (guac_mouse.onmouseup) {
290                 guac_mouse.currentState.down = false;
291                 guac_mouse.onmouseup(guac_mouse.currentState);
292             }
293 
294             scroll_delta -= guac_mouse.scrollThreshold;
295 
296         }
297 
298         cancelEvent(e);
299 
300     }
301 
302     element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
303     element.addEventListener('mousewheel',     mousewheel_handler, false);
304     element.addEventListener('wheel',          mousewheel_handler, false);
305 
306 };
307 
308 /**
309  * Simple container for properties describing the state of a mouse.
310  * 
311  * @constructor
312  * @param {Number} x The X position of the mouse pointer in pixels.
313  * @param {Number} y The Y position of the mouse pointer in pixels.
314  * @param {Boolean} left Whether the left mouse button is pressed. 
315  * @param {Boolean} middle Whether the middle mouse button is pressed. 
316  * @param {Boolean} right Whether the right mouse button is pressed. 
317  * @param {Boolean} up Whether the up mouse button is pressed (the fourth
318  *                     button, usually part of a scroll wheel). 
319  * @param {Boolean} down Whether the down mouse button is pressed (the fifth
320  *                       button, usually part of a scroll wheel). 
321  */
322 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
323 
324     /**
325      * Reference to this Guacamole.Mouse.State.
326      * @private
327      */
328     var guac_state = this;
329 
330     /**
331      * The current X position of the mouse pointer.
332      * @type Number
333      */
334     this.x = x;
335 
336     /**
337      * The current Y position of the mouse pointer.
338      * @type Number
339      */
340     this.y = y;
341 
342     /**
343      * Whether the left mouse button is currently pressed.
344      * @type Boolean
345      */
346     this.left = left;
347 
348     /**
349      * Whether the middle mouse button is currently pressed.
350      * @type Boolean
351      */
352     this.middle = middle;
353 
354     /**
355      * Whether the right mouse button is currently pressed.
356      * @type Boolean
357      */
358     this.right = right;
359 
360     /**
361      * Whether the up mouse button is currently pressed. This is the fourth
362      * mouse button, associated with upward scrolling of the mouse scroll
363      * wheel.
364      * @type Boolean
365      */
366     this.up = up;
367 
368     /**
369      * Whether the down mouse button is currently pressed. This is the fifth 
370      * mouse button, associated with downward scrolling of the mouse scroll
371      * wheel.
372      * @type Boolean
373      */
374     this.down = down;
375 
376     /**
377      * Updates the position represented within this state object by the given
378      * element and clientX/clientY coordinates (commonly available within event
379      * objects). Position is translated from clientX/clientY (relative to
380      * viewport) to element-relative coordinates.
381      * 
382      * @param {Element} element The element the coordinates should be relative
383      *                          to.
384      * @param {Number} clientX The X coordinate to translate, viewport-relative.
385      * @param {Number} clientY The Y coordinate to translate, viewport-relative.
386      */
387     this.fromClientPosition = function(element, clientX, clientY) {
388     
389         guac_state.x = clientX - element.offsetLeft;
390         guac_state.y = clientY - element.offsetTop;
391 
392         // This is all JUST so we can get the mouse position within the element
393         var parent = element.offsetParent;
394         while (parent && !(parent === document.body)) {
395             guac_state.x -= parent.offsetLeft - parent.scrollLeft;
396             guac_state.y -= parent.offsetTop  - parent.scrollTop;
397 
398             parent = parent.offsetParent;
399         }
400 
401         // Element ultimately depends on positioning within document body,
402         // take document scroll into account. 
403         if (parent) {
404             var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
405             var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
406 
407             guac_state.x -= parent.offsetLeft - documentScrollLeft;
408             guac_state.y -= parent.offsetTop  - documentScrollTop;
409         }
410 
411     };
412 
413 };
414 
415 /**
416  * Provides cross-browser relative touch event translation for a given element.
417  * 
418  * Touch events are translated into mouse events as if the touches occurred
419  * on a touchpad (drag to push the mouse pointer, tap to click).
420  * 
421  * @constructor
422  * @param {Element} element The Element to use to provide touch events.
423  */
424 Guacamole.Mouse.Touchpad = function(element) {
425 
426     /**
427      * Reference to this Guacamole.Mouse.Touchpad.
428      * @private
429      */
430     var guac_touchpad = this;
431 
432     /**
433      * The distance a two-finger touch must move per scrollwheel event, in
434      * pixels.
435      */
436     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
437 
438     /**
439      * The maximum number of milliseconds to wait for a touch to end for the
440      * gesture to be considered a click.
441      */
442     this.clickTimingThreshold = 250;
443 
444     /**
445      * The maximum number of pixels to allow a touch to move for the gesture to
446      * be considered a click.
447      */
448     this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
449 
450     /**
451      * The current mouse state. The properties of this state are updated when
452      * mouse events fire. This state object is also passed in as a parameter to
453      * the handler of any mouse events.
454      * 
455      * @type Guacamole.Mouse.State
456      */
457     this.currentState = new Guacamole.Mouse.State(
458         0, 0, 
459         false, false, false, false, false
460     );
461 
462     /**
463      * Fired whenever a mouse button is effectively pressed. This can happen
464      * as part of a "click" gesture initiated by the user by tapping one
465      * or more fingers over the touchpad element, as part of a "scroll"
466      * gesture initiated by dragging two fingers up or down, etc.
467      * 
468      * @event
469      * @param {Guacamole.Mouse.State} state The current mouse state.
470      */
471 	this.onmousedown = null;
472 
473     /**
474      * Fired whenever a mouse button is effectively released. This can happen
475      * as part of a "click" gesture initiated by the user by tapping one
476      * or more fingers over the touchpad element, as part of a "scroll"
477      * gesture initiated by dragging two fingers up or down, etc.
478      * 
479      * @event
480      * @param {Guacamole.Mouse.State} state The current mouse state.
481      */
482 	this.onmouseup = null;
483 
484     /**
485      * Fired whenever the user moves the mouse by dragging their finger over
486      * the touchpad element.
487      * 
488      * @event
489      * @param {Guacamole.Mouse.State} state The current mouse state.
490      */
491 	this.onmousemove = null;
492 
493     var touch_count = 0;
494     var last_touch_x = 0;
495     var last_touch_y = 0;
496     var last_touch_time = 0;
497     var pixels_moved = 0;
498 
499     var touch_buttons = {
500         1: "left",
501         2: "right",
502         3: "middle"
503     };
504 
505     var gesture_in_progress = false;
506     var click_release_timeout = null;
507 
508     element.addEventListener("touchend", function(e) {
509         
510         e.stopPropagation();
511         e.preventDefault();
512             
513         // If we're handling a gesture AND this is the last touch
514         if (gesture_in_progress && e.touches.length === 0) {
515             
516             var time = new Date().getTime();
517 
518             // Get corresponding mouse button
519             var button = touch_buttons[touch_count];
520 
521             // If mouse already down, release anad clear timeout
522             if (guac_touchpad.currentState[button]) {
523 
524                 // Fire button up event
525                 guac_touchpad.currentState[button] = false;
526                 if (guac_touchpad.onmouseup)
527                     guac_touchpad.onmouseup(guac_touchpad.currentState);
528 
529                 // Clear timeout, if set
530                 if (click_release_timeout) {
531                     window.clearTimeout(click_release_timeout);
532                     click_release_timeout = null;
533                 }
534 
535             }
536 
537             // If single tap detected (based on time and distance)
538             if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
539                     && pixels_moved < guac_touchpad.clickMoveThreshold) {
540 
541                 // Fire button down event
542                 guac_touchpad.currentState[button] = true;
543                 if (guac_touchpad.onmousedown)
544                     guac_touchpad.onmousedown(guac_touchpad.currentState);
545 
546                 // Delay mouse up - mouse up should be canceled if
547                 // touchstart within timeout.
548                 click_release_timeout = window.setTimeout(function() {
549                     
550                     // Fire button up event
551                     guac_touchpad.currentState[button] = false;
552                     if (guac_touchpad.onmouseup)
553                         guac_touchpad.onmouseup(guac_touchpad.currentState);
554                     
555                     // Gesture now over
556                     gesture_in_progress = false;
557 
558                 }, guac_touchpad.clickTimingThreshold);
559 
560             }
561 
562             // If we're not waiting to see if this is a click, stop gesture
563             if (!click_release_timeout)
564                 gesture_in_progress = false;
565 
566         }
567 
568     }, false);
569 
570     element.addEventListener("touchstart", function(e) {
571 
572         e.stopPropagation();
573         e.preventDefault();
574 
575         // Track number of touches, but no more than three
576         touch_count = Math.min(e.touches.length, 3);
577 
578         // Clear timeout, if set
579         if (click_release_timeout) {
580             window.clearTimeout(click_release_timeout);
581             click_release_timeout = null;
582         }
583 
584         // Record initial touch location and time for touch movement
585         // and tap gestures
586         if (!gesture_in_progress) {
587 
588             // Stop mouse events while touching
589             gesture_in_progress = true;
590 
591             // Record touch location and time
592             var starting_touch = e.touches[0];
593             last_touch_x = starting_touch.clientX;
594             last_touch_y = starting_touch.clientY;
595             last_touch_time = new Date().getTime();
596             pixels_moved = 0;
597 
598         }
599 
600     }, false);
601 
602     element.addEventListener("touchmove", function(e) {
603 
604         e.stopPropagation();
605         e.preventDefault();
606 
607         // Get change in touch location
608         var touch = e.touches[0];
609         var delta_x = touch.clientX - last_touch_x;
610         var delta_y = touch.clientY - last_touch_y;
611 
612         // Track pixels moved
613         pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
614 
615         // If only one touch involved, this is mouse move
616         if (touch_count === 1) {
617 
618             // Calculate average velocity in Manhatten pixels per millisecond
619             var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
620 
621             // Scale mouse movement relative to velocity
622             var scale = 1 + velocity;
623 
624             // Update mouse location
625             guac_touchpad.currentState.x += delta_x*scale;
626             guac_touchpad.currentState.y += delta_y*scale;
627 
628             // Prevent mouse from leaving screen
629 
630             if (guac_touchpad.currentState.x < 0)
631                 guac_touchpad.currentState.x = 0;
632             else if (guac_touchpad.currentState.x >= element.offsetWidth)
633                 guac_touchpad.currentState.x = element.offsetWidth - 1;
634 
635             if (guac_touchpad.currentState.y < 0)
636                 guac_touchpad.currentState.y = 0;
637             else if (guac_touchpad.currentState.y >= element.offsetHeight)
638                 guac_touchpad.currentState.y = element.offsetHeight - 1;
639 
640             // Fire movement event, if defined
641             if (guac_touchpad.onmousemove)
642                 guac_touchpad.onmousemove(guac_touchpad.currentState);
643 
644             // Update touch location
645             last_touch_x = touch.clientX;
646             last_touch_y = touch.clientY;
647 
648         }
649 
650         // Interpret two-finger swipe as scrollwheel
651         else if (touch_count === 2) {
652 
653             // If change in location passes threshold for scroll
654             if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
655 
656                 // Decide button based on Y movement direction
657                 var button;
658                 if (delta_y > 0) button = "down";
659                 else             button = "up";
660 
661                 // Fire button down event
662                 guac_touchpad.currentState[button] = true;
663                 if (guac_touchpad.onmousedown)
664                     guac_touchpad.onmousedown(guac_touchpad.currentState);
665 
666                 // Fire button up event
667                 guac_touchpad.currentState[button] = false;
668                 if (guac_touchpad.onmouseup)
669                     guac_touchpad.onmouseup(guac_touchpad.currentState);
670 
671                 // Only update touch location after a scroll has been
672                 // detected
673                 last_touch_x = touch.clientX;
674                 last_touch_y = touch.clientY;
675 
676             }
677 
678         }
679 
680     }, false);
681 
682 };
683 
684 /**
685  * Provides cross-browser absolute touch event translation for a given element.
686  *
687  * Touch events are translated into mouse events as if the touches occurred
688  * on a touchscreen (tapping anywhere on the screen clicks at that point,
689  * long-press to right-click).
690  *
691  * @constructor
692  * @param {Element} element The Element to use to provide touch events.
693  */
694 Guacamole.Mouse.Touchscreen = function(element) {
695 
696     /**
697      * Reference to this Guacamole.Mouse.Touchscreen.
698      * @private
699      */
700     var guac_touchscreen = this;
701 
702     /**
703      * Whether a gesture is known to be in progress. If false, touch events
704      * will be ignored.
705      */
706     var gesture_in_progress = false;
707 
708     /**
709      * The start X location of a gesture.
710      * @private
711      */
712     var gesture_start_x = null;
713 
714     /**
715      * The start Y location of a gesture.
716      * @private
717      */
718     var gesture_start_y = null;
719 
720     /**
721      * The timeout associated with the delayed, cancellable click release.
722      */
723     var click_release_timeout = null;
724 
725     /**
726      * The timeout associated with long-press for right click.
727      */
728     var long_press_timeout = null;
729 
730     /**
731      * The distance a two-finger touch must move per scrollwheel event, in
732      * pixels.
733      */
734     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
735 
736     /**
737      * The maximum number of milliseconds to wait for a touch to end for the
738      * gesture to be considered a click.
739      */
740     this.clickTimingThreshold = 250;
741 
742     /**
743      * The maximum number of pixels to allow a touch to move for the gesture to
744      * be considered a click.
745      */
746     this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1);
747 
748     /**
749      * The amount of time a press must be held for long press to be
750      * detected.
751      */
752     this.longPressThreshold = 500;
753 
754     /**
755      * The current mouse state. The properties of this state are updated when
756      * mouse events fire. This state object is also passed in as a parameter to
757      * the handler of any mouse events.
758      *
759      * @type Guacamole.Mouse.State
760      */
761     this.currentState = new Guacamole.Mouse.State(
762         0, 0,
763         false, false, false, false, false
764     );
765 
766     /**
767      * Fired whenever a mouse button is effectively pressed. This can happen
768      * as part of a "mousedown" gesture initiated by the user by pressing one
769      * finger over the touchscreen element, as part of a "scroll" gesture
770      * initiated by dragging two fingers up or down, etc.
771      *
772      * @event
773      * @param {Guacamole.Mouse.State} state The current mouse state.
774      */
775 	this.onmousedown = null;
776 
777     /**
778      * Fired whenever a mouse button is effectively released. This can happen
779      * as part of a "mouseup" gesture initiated by the user by removing the
780      * finger pressed against the touchscreen element, or as part of a "scroll"
781      * gesture initiated by dragging two fingers up or down, etc.
782      *
783      * @event
784      * @param {Guacamole.Mouse.State} state The current mouse state.
785      */
786 	this.onmouseup = null;
787 
788     /**
789      * Fired whenever the user moves the mouse by dragging their finger over
790      * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad,
791      * dragging a finger over the touchscreen element will always cause
792      * the mouse button to be effectively down, as if clicking-and-dragging.
793      *
794      * @event
795      * @param {Guacamole.Mouse.State} state The current mouse state.
796      */
797 	this.onmousemove = null;
798 
799     /**
800      * Presses the given mouse button, if it isn't already pressed. Valid
801      * button values are "left", "middle", "right", "up", and "down".
802      *
803      * @private
804      * @param {String} button The mouse button to press.
805      */
806     function press_button(button) {
807         if (!guac_touchscreen.currentState[button]) {
808             guac_touchscreen.currentState[button] = true;
809             if (guac_touchscreen.onmousedown)
810                 guac_touchscreen.onmousedown(guac_touchscreen.currentState);
811         }
812     }
813 
814     /**
815      * Releases the given mouse button, if it isn't already released. Valid
816      * button values are "left", "middle", "right", "up", and "down".
817      *
818      * @private
819      * @param {String} button The mouse button to release.
820      */
821     function release_button(button) {
822         if (guac_touchscreen.currentState[button]) {
823             guac_touchscreen.currentState[button] = false;
824             if (guac_touchscreen.onmouseup)
825                 guac_touchscreen.onmouseup(guac_touchscreen.currentState);
826         }
827     }
828 
829     /**
830      * Clicks (presses and releases) the given mouse button. Valid button
831      * values are "left", "middle", "right", "up", and "down".
832      *
833      * @private
834      * @param {String} button The mouse button to click.
835      */
836     function click_button(button) {
837         press_button(button);
838         release_button(button);
839     }
840 
841     /**
842      * Moves the mouse to the given coordinates. These coordinates must be
843      * relative to the browser window, as they will be translated based on
844      * the touch event target's location within the browser window.
845      *
846      * @private
847      * @param {Number} x The X coordinate of the mouse pointer.
848      * @param {Number} y The Y coordinate of the mouse pointer.
849      */
850     function move_mouse(x, y) {
851         guac_touchscreen.currentState.fromClientPosition(element, x, y);
852         if (guac_touchscreen.onmousemove)
853             guac_touchscreen.onmousemove(guac_touchscreen.currentState);
854     }
855 
856     /**
857      * Returns whether the given touch event exceeds the movement threshold for
858      * clicking, based on where the touch gesture began.
859      *
860      * @private
861      * @param {TouchEvent} e The touch event to check.
862      * @return {Boolean} true if the movement threshold is exceeded, false
863      *                   otherwise.
864      */
865     function finger_moved(e) {
866         var touch = e.touches[0] || e.changedTouches[0];
867         var delta_x = touch.clientX - gesture_start_x;
868         var delta_y = touch.clientY - gesture_start_y;
869         return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold;
870     }
871 
872     /**
873      * Begins a new gesture at the location of the first touch in the given
874      * touch event.
875      * 
876      * @private
877      * @param {TouchEvent} e The touch event beginning this new gesture.
878      */
879     function begin_gesture(e) {
880         var touch = e.touches[0];
881         gesture_in_progress = true;
882         gesture_start_x = touch.clientX;
883         gesture_start_y = touch.clientY;
884     }
885 
886     /**
887      * End the current gesture entirely. Wait for all touches to be done before
888      * resuming gesture detection.
889      * 
890      * @private
891      */
892     function end_gesture() {
893         window.clearTimeout(click_release_timeout);
894         window.clearTimeout(long_press_timeout);
895         gesture_in_progress = false;
896     }
897 
898     element.addEventListener("touchend", function(e) {
899 
900         // Do not handle if no gesture
901         if (!gesture_in_progress)
902             return;
903 
904         // Ignore if more than one touch
905         if (e.touches.length !== 0 || e.changedTouches.length !== 1) {
906             end_gesture();
907             return;
908         }
909 
910         // Long-press, if any, is over
911         window.clearTimeout(long_press_timeout);
912 
913         // Always release mouse button if pressed
914         release_button("left");
915 
916         // If finger hasn't moved enough to cancel the click
917         if (!finger_moved(e)) {
918 
919             e.preventDefault();
920 
921             // If not yet pressed, press and start delay release
922             if (!guac_touchscreen.currentState.left) {
923 
924                 var touch = e.changedTouches[0];
925                 move_mouse(touch.clientX, touch.clientY);
926                 press_button("left");
927 
928                 // Release button after a delay, if not canceled
929                 click_release_timeout = window.setTimeout(function() {
930                     release_button("left");
931                     end_gesture();
932                 }, guac_touchscreen.clickTimingThreshold);
933 
934             }
935 
936         } // end if finger not moved
937 
938     }, false);
939 
940     element.addEventListener("touchstart", function(e) {
941 
942         // Ignore if more than one touch
943         if (e.touches.length !== 1) {
944             end_gesture();
945             return;
946         }
947 
948         e.preventDefault();
949 
950         // New touch begins a new gesture
951         begin_gesture(e);
952 
953         // Keep button pressed if tap after left click
954         window.clearTimeout(click_release_timeout);
955 
956         // Click right button if this turns into a long-press
957         long_press_timeout = window.setTimeout(function() {
958             var touch = e.touches[0];
959             move_mouse(touch.clientX, touch.clientY);
960             click_button("right");
961             end_gesture();
962         }, guac_touchscreen.longPressThreshold);
963 
964     }, false);
965 
966     element.addEventListener("touchmove", function(e) {
967 
968         // Do not handle if no gesture
969         if (!gesture_in_progress)
970             return;
971 
972         // Cancel long press if finger moved
973         if (finger_moved(e))
974             window.clearTimeout(long_press_timeout);
975 
976         // Ignore if more than one touch
977         if (e.touches.length !== 1) {
978             end_gesture();
979             return;
980         }
981 
982         // Update mouse position if dragging
983         if (guac_touchscreen.currentState.left) {
984 
985             e.preventDefault();
986             e.stopPropagation();
987 
988             // Update state
989             var touch = e.touches[0];
990             move_mouse(touch.clientX, touch.clientY);
991 
992         }
993 
994     }, false);
995 
996 };
997