2 /* ***** BEGIN LICENSE BLOCK *****
3 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
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/
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
15 * The Original Code is guacamole-common-js.
17 * The Initial Developer of the Original Code is
19 * Portions created by the Initial Developer are Copyright (C) 2010
20 * the Initial Developer. All Rights Reserved.
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.
36 * ***** END LICENSE BLOCK ***** */
38 // Guacamole namespace
39 var Guacamole = Guacamole || {};
42 * Provides cross-browser mouse events for a given element. The events of
43 * the given element are automatically populated with handlers that translate
44 * mouse events into a non-browser-specific event provided by the
45 * Guacamole.Mouse instance.
48 * @param {Element} element The Element to use to provide mouse events.
50 Guacamole.Mouse = function(element) {
53 * Reference to this Guacamole.Mouse.
56 var guac_mouse = this;
59 * The current mouse state. The properties of this state are updated when
60 * mouse events fire. This state object is also passed in as a parameter to
61 * the handler of any mouse events.
63 * @type Guacamole.Mouse.State
65 this.currentState = new Guacamole.Mouse.State(
67 false, false, false, false, false
71 * Fired whenever the user presses a mouse button down over the element
72 * associated with this Guacamole.Mouse.
75 * @param {Guacamole.Mouse.State} state The current mouse state.
77 this.onmousedown = null;
80 * Fired whenever the user releases a mouse button down over the element
81 * associated with this Guacamole.Mouse.
84 * @param {Guacamole.Mouse.State} state The current mouse state.
86 this.onmouseup = null;
89 * Fired whenever the user moves the mouse over the element associated with
90 * this Guacamole.Mouse.
93 * @param {Guacamole.Mouse.State} state The current mouse state.
95 this.onmousemove = null;
98 * Zero-delay timeout set when mouse events are fired, and canceled when
99 * touch events are detected, in order to prevent touch events registering
100 * as mouse events (some browsers will do this).
102 var deferred_mouse_event = null;
105 * Flag which, when set to true, will cause all mouse events to be
106 * ignored. Used to temporarily ignore events when generated by
107 * touch events, and not by a mouse.
109 var ignore_mouse = false;
112 * Forces all mouse events to be ignored until the event queue is flushed.
114 function ignorePendingMouseEvents() {
116 // Cancel deferred event
117 if (deferred_mouse_event) {
118 window.clearTimeout(deferred_mouse_event);
119 deferred_mouse_event = null;
122 // Ignore all other events until end of event loop
124 window.setTimeout(function() {
125 ignore_mouse = false;
130 function cancelEvent(e) {
132 if (e.preventDefault) e.preventDefault();
133 e.returnValue = false;
136 function moveMouse(clientX, clientY) {
138 guac_mouse.currentState.x = clientX - element.offsetLeft;
139 guac_mouse.currentState.y = clientY - element.offsetTop;
141 // This is all JUST so we can get the mouse position within the element
142 var parent = element.offsetParent;
143 while (parent && !(parent === document.body)) {
144 guac_mouse.currentState.x -= parent.offsetLeft - parent.scrollLeft;
145 guac_mouse.currentState.y -= parent.offsetTop - parent.scrollTop;
147 parent = parent.offsetParent;
150 // Offset by document scroll amount
151 var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
152 var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
154 guac_mouse.currentState.x -= parent.offsetLeft - documentScrollLeft;
155 guac_mouse.currentState.y -= parent.offsetTop - documentScrollTop;
157 if (guac_mouse.onmousemove)
158 deferred_mouse_event = window.setTimeout(function() {
159 guac_mouse.onmousemove(guac_mouse.currentState);
160 deferred_mouse_event = null;
166 // Block context menu so right-click gets sent properly
167 element.addEventListener("contextmenu", function(e) {
171 element.addEventListener("mousemove", function(e) {
175 // If artificial event detected, ignore currently pending events
176 if (deferred_mouse_event)
177 ignorePendingMouseEvents();
182 moveMouse(e.clientX, e.clientY);
186 element.addEventListener("mousedown", function(e) {
190 // If artificial event detected, ignore currently pending events
191 if (deferred_mouse_event)
192 ignorePendingMouseEvents();
199 guac_mouse.currentState.left = true;
202 guac_mouse.currentState.middle = true;
205 guac_mouse.currentState.right = true;
209 if (guac_mouse.onmousedown)
210 deferred_mouse_event = window.setTimeout(function() {
211 guac_mouse.onmousedown(guac_mouse.currentState);
212 deferred_mouse_event = null;
217 element.addEventListener("mouseup", function(e) {
221 // If artificial event detected, ignore currently pending events
222 if (deferred_mouse_event)
223 ignorePendingMouseEvents();
230 guac_mouse.currentState.left = false;
233 guac_mouse.currentState.middle = false;
236 guac_mouse.currentState.right = false;
240 if (guac_mouse.onmouseup)
241 deferred_mouse_event = window.setTimeout(function() {
242 guac_mouse.onmouseup(guac_mouse.currentState);
243 deferred_mouse_event = null;
248 element.addEventListener("mouseout", function(e) {
250 // Get parent of the element the mouse pointer is leaving
251 if (!e) e = window.event;
253 // Check that mouseout is due to actually LEAVING the element
254 var target = e.relatedTarget || e.toElement;
255 while (target != null) {
256 if (target === element)
258 target = target.parentNode;
263 // Release all buttons
264 if (guac_mouse.currentState.left
265 || guac_mouse.currentState.middle
266 || guac_mouse.currentState.right) {
268 guac_mouse.currentState.left = false;
269 guac_mouse.currentState.middle = false;
270 guac_mouse.currentState.right = false;
272 if (guac_mouse.onmouseup)
273 guac_mouse.onmouseup(guac_mouse.currentState);
278 // Override selection on mouse event element.
279 element.addEventListener("selectstart", function(e) {
284 // Ignore all pending mouse events when touch events are the apparent source
285 element.addEventListener("touchmove", ignorePendingMouseEvents, false);
286 element.addEventListener("touchstart", ignorePendingMouseEvents, false);
287 element.addEventListener("touchend", ignorePendingMouseEvents, false);
289 // Scroll wheel support
290 function mousewheel_handler(e) {
295 else if (e.wheelDelta)
296 delta = -event.wheelDelta;
300 if (guac_mouse.onmousedown) {
301 guac_mouse.currentState.up = true;
302 guac_mouse.onmousedown(guac_mouse.currentState);
305 if (guac_mouse.onmouseup) {
306 guac_mouse.currentState.up = false;
307 guac_mouse.onmouseup(guac_mouse.currentState);
313 if (guac_mouse.onmousedown) {
314 guac_mouse.currentState.down = true;
315 guac_mouse.onmousedown(guac_mouse.currentState);
318 if (guac_mouse.onmouseup) {
319 guac_mouse.currentState.down = false;
320 guac_mouse.onmouseup(guac_mouse.currentState);
327 element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
328 element.addEventListener('mousewheel', mousewheel_handler, false);
334 * Provides cross-browser relative touch event translation for a given element.
336 * Touch events are translated into mouse events as if the touches occurred
337 * on a touchpad (drag to push the mouse pointer, tap to click).
340 * @param {Element} element The Element to use to provide touch events.
342 Guacamole.Mouse.Touchpad = function(element) {
345 * Reference to this Guacamole.Mouse.Touchpad.
348 var guac_touchpad = this;
351 * The distance a two-finger touch must move per scrollwheel event, in
354 this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
357 * The maximum number of milliseconds to wait for a touch to end for the
358 * gesture to be considered a click.
360 this.clickTimingThreshold = 250;
363 * The maximum number of pixels to allow a touch to move for the gesture to
364 * be considered a click.
366 this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
369 * The current mouse state. The properties of this state are updated when
370 * mouse events fire. This state object is also passed in as a parameter to
371 * the handler of any mouse events.
373 * @type Guacamole.Mouse.State
375 this.currentState = new Guacamole.Mouse.State(
377 false, false, false, false, false
381 * Fired whenever a mouse button is effectively pressed. This can happen
382 * as part of a "click" gesture initiated by the user by tapping one
383 * or more fingers over the touchpad element, as part of a "scroll"
384 * gesture initiated by dragging two fingers up or down, etc.
387 * @param {Guacamole.Mouse.State} state The current mouse state.
389 this.onmousedown = null;
392 * Fired whenever a mouse button is effectively released. This can happen
393 * as part of a "click" gesture initiated by the user by tapping one
394 * or more fingers over the touchpad element, as part of a "scroll"
395 * gesture initiated by dragging two fingers up or down, etc.
398 * @param {Guacamole.Mouse.State} state The current mouse state.
400 this.onmouseup = null;
403 * Fired whenever the user moves the mouse by dragging their finger over
404 * the touchpad element.
407 * @param {Guacamole.Mouse.State} state The current mouse state.
409 this.onmousemove = null;
412 var last_touch_x = 0;
413 var last_touch_y = 0;
414 var last_touch_time = 0;
415 var pixels_moved = 0;
417 var touch_buttons = {
423 var gesture_in_progress = false;
424 var click_release_timeout = null;
426 element.addEventListener("touchend", function(e) {
431 // If we're handling a gesture AND this is the last touch
432 if (gesture_in_progress && e.touches.length == 0) {
434 var time = new Date().getTime();
436 // Get corresponding mouse button
437 var button = touch_buttons[touch_count];
439 // If mouse already down, release anad clear timeout
440 if (guac_touchpad.currentState[button]) {
442 // Fire button up event
443 guac_touchpad.currentState[button] = false;
444 if (guac_touchpad.onmouseup)
445 guac_touchpad.onmouseup(guac_touchpad.currentState);
447 // Clear timeout, if set
448 if (click_release_timeout) {
449 window.clearTimeout(click_release_timeout);
450 click_release_timeout = null;
455 // If single tap detected (based on time and distance)
456 if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
457 && pixels_moved < guac_touchpad.clickMoveThreshold) {
459 // Fire button down event
460 guac_touchpad.currentState[button] = true;
461 if (guac_touchpad.onmousedown)
462 guac_touchpad.onmousedown(guac_touchpad.currentState);
464 // Delay mouse up - mouse up should be canceled if
465 // touchstart within timeout.
466 click_release_timeout = window.setTimeout(function() {
468 // Fire button up event
469 guac_touchpad.currentState[button] = false;
470 if (guac_touchpad.onmouseup)
471 guac_touchpad.onmouseup(guac_touchpad.currentState);
474 gesture_in_progress = false;
476 }, guac_touchpad.clickTimingThreshold);
480 // If we're not waiting to see if this is a click, stop gesture
481 if (!click_release_timeout)
482 gesture_in_progress = false;
488 element.addEventListener("touchstart", function(e) {
493 // Track number of touches, but no more than three
494 touch_count = Math.min(e.touches.length, 3);
496 // Clear timeout, if set
497 if (click_release_timeout) {
498 window.clearTimeout(click_release_timeout);
499 click_release_timeout = null;
502 // Record initial touch location and time for touch movement
504 if (!gesture_in_progress) {
506 // Stop mouse events while touching
507 gesture_in_progress = true;
509 // Record touch location and time
510 var starting_touch = e.touches[0];
511 last_touch_x = starting_touch.clientX;
512 last_touch_y = starting_touch.clientY;
513 last_touch_time = new Date().getTime();
520 element.addEventListener("touchmove", function(e) {
525 // Get change in touch location
526 var touch = e.touches[0];
527 var delta_x = touch.clientX - last_touch_x;
528 var delta_y = touch.clientY - last_touch_y;
530 // Track pixels moved
531 pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
533 // If only one touch involved, this is mouse move
534 if (touch_count == 1) {
536 // Calculate average velocity in Manhatten pixels per millisecond
537 var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
539 // Scale mouse movement relative to velocity
540 var scale = 1 + velocity;
542 // Update mouse location
543 guac_touchpad.currentState.x += delta_x*scale;
544 guac_touchpad.currentState.y += delta_y*scale;
546 // Prevent mouse from leaving screen
548 if (guac_touchpad.currentState.x < 0)
549 guac_touchpad.currentState.x = 0;
550 else if (guac_touchpad.currentState.x >= element.offsetWidth)
551 guac_touchpad.currentState.x = element.offsetWidth - 1;
553 if (guac_touchpad.currentState.y < 0)
554 guac_touchpad.currentState.y = 0;
555 else if (guac_touchpad.currentState.y >= element.offsetHeight)
556 guac_touchpad.currentState.y = element.offsetHeight - 1;
558 // Fire movement event, if defined
559 if (guac_touchpad.onmousemove)
560 guac_touchpad.onmousemove(guac_touchpad.currentState);
562 // Update touch location
563 last_touch_x = touch.clientX;
564 last_touch_y = touch.clientY;
568 // Interpret two-finger swipe as scrollwheel
569 else if (touch_count == 2) {
571 // If change in location passes threshold for scroll
572 if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
574 // Decide button based on Y movement direction
576 if (delta_y > 0) button = "down";
579 // Fire button down event
580 guac_touchpad.currentState[button] = true;
581 if (guac_touchpad.onmousedown)
582 guac_touchpad.onmousedown(guac_touchpad.currentState);
584 // Fire button up event
585 guac_touchpad.currentState[button] = false;
586 if (guac_touchpad.onmouseup)
587 guac_touchpad.onmouseup(guac_touchpad.currentState);
589 // Only update touch location after a scroll has been
591 last_touch_x = touch.clientX;
592 last_touch_y = touch.clientY;
603 * Provides cross-browser absolute touch event translation for a given element.
605 * Touch events are translated into mouse events as if the touches occurred
606 * on a touchscreen (tapping anywhere on the screen clicks at that point,
607 * long-press to right-click).
610 * @param {Element} element The Element to use to provide touch events.
612 Guacamole.Mouse.Touchscreen = function(element) {
615 * Reference to this Guacamole.Mouse.Touchscreen.
618 var guac_touchscreen = this;
621 * The distance a two-finger touch must move per scrollwheel event, in
624 this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
627 * The current mouse state. The properties of this state are updated when
628 * mouse events fire. This state object is also passed in as a parameter to
629 * the handler of any mouse events.
631 * @type Guacamole.Mouse.State
633 this.currentState = new Guacamole.Mouse.State(
635 false, false, false, false, false
639 * Fired whenever a mouse button is effectively pressed. This can happen
640 * as part of a "mousedown" gesture initiated by the user by pressing one
641 * finger over the touchscreen element, as part of a "scroll" gesture
642 * initiated by dragging two fingers up or down, etc.
645 * @param {Guacamole.Mouse.State} state The current mouse state.
647 this.onmousedown = null;
650 * Fired whenever a mouse button is effectively released. This can happen
651 * as part of a "mouseup" gesture initiated by the user by removing the
652 * finger pressed against the touchscreen element, or as part of a "scroll"
653 * gesture initiated by dragging two fingers up or down, etc.
656 * @param {Guacamole.Mouse.State} state The current mouse state.
658 this.onmouseup = null;
661 * Fired whenever the user moves the mouse by dragging their finger over
662 * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad,
663 * dragging a finger over the touchscreen element will always cause
664 * the mouse button to be effectively down, as if clicking-and-dragging.
667 * @param {Guacamole.Mouse.State} state The current mouse state.
669 this.onmousemove = null;
671 element.addEventListener("touchend", function(e) {
677 guac_touchscreen.currentState.left = false;
679 // Fire release event when the last touch is released, if event defined
680 if (e.touches.length == 0 && guac_touchscreen.onmouseup)
681 guac_touchscreen.onmouseup(guac_touchscreen.currentState);
685 element.addEventListener("touchstart", function(e) {
691 var touch = e.touches[0];
694 guac_touchscreen.currentState.left = true;
695 guac_touchscreen.currentState.x = touch.clientX;
696 guac_touchscreen.currentState.y = touch.clientY;
698 // Fire press event, if defined
699 if (guac_touchscreen.onmousedown)
700 guac_touchscreen.onmousedown(guac_touchscreen.currentState);
705 element.addEventListener("touchmove", function(e) {
711 var touch = e.touches[0];
714 guac_touchscreen.currentState.x = touch.clientX;
715 guac_touchscreen.currentState.y = touch.clientY;
717 // Fire movement event, if defined
718 if (guac_touchscreen.onmousemove)
719 guac_touchscreen.onmousemove(guac_touchscreen.currentState);
726 * Simple container for properties describing the state of a mouse.
729 * @param {Number} x The X position of the mouse pointer in pixels.
730 * @param {Number} y The Y position of the mouse pointer in pixels.
731 * @param {Boolean} left Whether the left mouse button is pressed.
732 * @param {Boolean} middle Whether the middle mouse button is pressed.
733 * @param {Boolean} right Whether the right mouse button is pressed.
734 * @param {Boolean} up Whether the up mouse button is pressed (the fourth
735 * button, usually part of a scroll wheel).
736 * @param {Boolean} down Whether the down mouse button is pressed (the fifth
737 * button, usually part of a scroll wheel).
739 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
742 * The current X position of the mouse pointer.
748 * The current Y position of the mouse pointer.
754 * Whether the left mouse button is currently pressed.
760 * Whether the middle mouse button is currently pressed.
766 * Whether the right mouse button is currently pressed.
772 * Whether the up mouse button is currently pressed. This is the fourth
773 * mouse button, associated with upward scrolling of the mouse scroll
780 * Whether the down mouse button is currently pressed. This is the fifth
781 * mouse button, associated with downward scrolling of the mouse scroll