/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ var Guacamole = Guacamole || {}; /** * Provides cross-browser mouse events for a given element. The events of * the given element are automatically populated with handlers that translate * mouse events into a non-browser-specific event provided by the * Guacamole.Mouse instance. * * @constructor * @param {Element} element The Element to use to provide mouse events. */ Guacamole.Mouse = function(element) { /** * Reference to this Guacamole.Mouse. * @private */ var guac_mouse = this; /** * The number of mousemove events to require before re-enabling mouse * event handling after receiving a touch event. */ this.touchMouseThreshold = 3; /** * The minimum amount of pixels scrolled required for a single scroll button * click. */ this.scrollThreshold = 53; /** * The number of pixels to scroll per line. */ this.PIXELS_PER_LINE = 18; /** * The number of pixels to scroll per page. */ this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16; /** * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. * * @type {Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State( 0, 0, false, false, false, false, false ); /** * Fired whenever the user presses a mouse button down over the element * associated with this Guacamole.Mouse. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousedown = null; /** * Fired whenever the user releases a mouse button down over the element * associated with this Guacamole.Mouse. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmouseup = null; /** * Fired whenever the user moves the mouse over the element associated with * this Guacamole.Mouse. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousemove = null; /** * Fired whenever the mouse leaves the boundaries of the element associated * with this Guacamole.Mouse. * * @event */ this.onmouseout = null; /** * Counter of mouse events to ignore. This decremented by mousemove, and * while non-zero, mouse events will have no effect. * @private */ var ignore_mouse = 0; /** * Cumulative scroll delta amount. This value is accumulated through scroll * events and results in scroll button clicks if it exceeds a certain * threshold. * * @private */ var scroll_delta = 0; function cancelEvent(e) { e.stopPropagation(); if (e.preventDefault) e.preventDefault(); e.returnValue = false; } // Block context menu so right-click gets sent properly element.addEventListener("contextmenu", function(e) { cancelEvent(e); }, false); element.addEventListener("mousemove", function(e) { cancelEvent(e); // If ignoring events, decrement counter if (ignore_mouse) { ignore_mouse--; return; } guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY); if (guac_mouse.onmousemove) guac_mouse.onmousemove(guac_mouse.currentState); }, false); element.addEventListener("mousedown", function(e) { cancelEvent(e); // Do not handle if ignoring events if (ignore_mouse) return; switch (e.button) { case 0: guac_mouse.currentState.left = true; break; case 1: guac_mouse.currentState.middle = true; break; case 2: guac_mouse.currentState.right = true; break; } if (guac_mouse.onmousedown) guac_mouse.onmousedown(guac_mouse.currentState); }, false); element.addEventListener("mouseup", function(e) { cancelEvent(e); // Do not handle if ignoring events if (ignore_mouse) return; switch (e.button) { case 0: guac_mouse.currentState.left = false; break; case 1: guac_mouse.currentState.middle = false; break; case 2: guac_mouse.currentState.right = false; break; } if (guac_mouse.onmouseup) guac_mouse.onmouseup(guac_mouse.currentState); }, false); element.addEventListener("mouseout", function(e) { // Get parent of the element the mouse pointer is leaving if (!e) e = window.event; // Check that mouseout is due to actually LEAVING the element var target = e.relatedTarget || e.toElement; while (target) { if (target === element) return; target = target.parentNode; } cancelEvent(e); // Release all buttons if (guac_mouse.currentState.left || guac_mouse.currentState.middle || guac_mouse.currentState.right) { guac_mouse.currentState.left = false; guac_mouse.currentState.middle = false; guac_mouse.currentState.right = false; if (guac_mouse.onmouseup) guac_mouse.onmouseup(guac_mouse.currentState); } // Fire onmouseout event if (guac_mouse.onmouseout) guac_mouse.onmouseout(); }, false); // Override selection on mouse event element. element.addEventListener("selectstart", function(e) { cancelEvent(e); }, false); // Ignore all pending mouse events when touch events are the apparent source function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } element.addEventListener("touchmove", ignorePendingMouseEvents, false); element.addEventListener("touchstart", ignorePendingMouseEvents, false); element.addEventListener("touchend", ignorePendingMouseEvents, false); // Scroll wheel support function mousewheel_handler(e) { // Determine approximate scroll amount (in pixels) var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta; // If successfully retrieved scroll amount, convert to pixels if not // already in pixels if (delta) { // Convert to pixels if delta was lines if (e.deltaMode === 1) delta = e.deltaY * guac_mouse.PIXELS_PER_LINE; // Convert to pixels if delta was pages else if (e.deltaMode === 2) delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE; } // Otherwise, assume legacy mousewheel event and line scrolling else delta = e.detail * guac_mouse.PIXELS_PER_LINE; // Update overall delta scroll_delta += delta; // Up if (scroll_delta <= -guac_mouse.scrollThreshold) { // Repeatedly click the up button until insufficient delta remains do { if (guac_mouse.onmousedown) { guac_mouse.currentState.up = true; guac_mouse.onmousedown(guac_mouse.currentState); } if (guac_mouse.onmouseup) { guac_mouse.currentState.up = false; guac_mouse.onmouseup(guac_mouse.currentState); } scroll_delta += guac_mouse.scrollThreshold; } while (scroll_delta <= -guac_mouse.scrollThreshold); // Reset delta scroll_delta = 0; } // Down if (scroll_delta >= guac_mouse.scrollThreshold) { // Repeatedly click the down button until insufficient delta remains do { if (guac_mouse.onmousedown) { guac_mouse.currentState.down = true; guac_mouse.onmousedown(guac_mouse.currentState); } if (guac_mouse.onmouseup) { guac_mouse.currentState.down = false; guac_mouse.onmouseup(guac_mouse.currentState); } scroll_delta -= guac_mouse.scrollThreshold; } while (scroll_delta >= guac_mouse.scrollThreshold); // Reset delta scroll_delta = 0; } cancelEvent(e); } element.addEventListener('DOMMouseScroll', mousewheel_handler, false); element.addEventListener('mousewheel', mousewheel_handler, false); element.addEventListener('wheel', mousewheel_handler, false); /** * Whether the browser supports CSS3 cursor styling, including hotspot * coordinates. * * @private * @type {Boolean} */ var CSS3_CURSOR_SUPPORTED = (function() { var div = document.createElement("div"); // If no cursor property at all, then no support if (!("cursor" in div.style)) return false; try { // Apply simple 1x1 PNG div.style.cursor = "url(data:image/png;base64," + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI" + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA" + "AABJRU5ErkJggg==) 0 0, auto"; } catch (e) { return false; } // Verify cursor property is set to URL with hotspot return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || ""); })(); /** * Changes the local mouse cursor to the given canvas, having the given * hotspot coordinates. This affects styling of the element backing this * Guacamole.Mouse only, and may fail depending on browser support for * setting the mouse cursor. * * If setting the local cursor is desired, it is up to the implementation * to do something else, such as use the software cursor built into * Guacamole.Display, if the local cursor cannot be set. * * @param {HTMLCanvasElement} canvas The cursor image. * @param {Number} x The X-coordinate of the cursor hotspot. * @param {Number} y The Y-coordinate of the cursor hotspot. * @return {Boolean} true if the cursor was successfully set, false if the * cursor could not be set for any reason. */ this.setCursor = function(canvas, x, y) { // Attempt to set via CSS3 cursor styling if (CSS3_CURSOR_SUPPORTED) { var dataURL = canvas.toDataURL('image/png'); element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto"; return true; } // Otherwise, setting cursor failed return false; }; }; /** * Simple container for properties describing the state of a mouse. * * @constructor * @param {Number} x The X position of the mouse pointer in pixels. * @param {Number} y The Y position of the mouse pointer in pixels. * @param {Boolean} left Whether the left mouse button is pressed. * @param {Boolean} middle Whether the middle mouse button is pressed. * @param {Boolean} right Whether the right mouse button is pressed. * @param {Boolean} up Whether the up mouse button is pressed (the fourth * button, usually part of a scroll wheel). * @param {Boolean} down Whether the down mouse button is pressed (the fifth * button, usually part of a scroll wheel). */ Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { /** * Reference to this Guacamole.Mouse.State. * @private */ var guac_state = this; /** * The current X position of the mouse pointer. * @type {Number} */ this.x = x; /** * The current Y position of the mouse pointer. * @type {Number} */ this.y = y; /** * Whether the left mouse button is currently pressed. * @type {Boolean} */ this.left = left; /** * Whether the middle mouse button is currently pressed. * @type {Boolean} */ this.middle = middle; /** * Whether the right mouse button is currently pressed. * @type {Boolean} */ this.right = right; /** * Whether the up mouse button is currently pressed. This is the fourth * mouse button, associated with upward scrolling of the mouse scroll * wheel. * @type {Boolean} */ this.up = up; /** * Whether the down mouse button is currently pressed. This is the fifth * mouse button, associated with downward scrolling of the mouse scroll * wheel. * @type {Boolean} */ this.down = down; /** * Updates the position represented within this state object by the given * element and clientX/clientY coordinates (commonly available within event * objects). Position is translated from clientX/clientY (relative to * viewport) to element-relative coordinates. * * @param {Element} element The element the coordinates should be relative * to. * @param {Number} clientX The X coordinate to translate, viewport-relative. * @param {Number} clientY The Y coordinate to translate, viewport-relative. */ this.fromClientPosition = function(element, clientX, clientY) { guac_state.x = clientX - element.offsetLeft; guac_state.y = clientY - element.offsetTop; // This is all JUST so we can get the mouse position within the element var parent = element.offsetParent; while (parent && !(parent === document.body)) { guac_state.x -= parent.offsetLeft - parent.scrollLeft; guac_state.y -= parent.offsetTop - parent.scrollTop; parent = parent.offsetParent; } // Element ultimately depends on positioning within document body, // take document scroll into account. if (parent) { var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; guac_state.x -= parent.offsetLeft - documentScrollLeft; guac_state.y -= parent.offsetTop - documentScrollTop; } }; }; /** * Provides cross-browser relative touch event translation for a given element. * * Touch events are translated into mouse events as if the touches occurred * on a touchpad (drag to push the mouse pointer, tap to click). * * @constructor * @param {Element} element The Element to use to provide touch events. */ Guacamole.Mouse.Touchpad = function(element) { /** * Reference to this Guacamole.Mouse.Touchpad. * @private */ var guac_touchpad = this; /** * The distance a two-finger touch must move per scrollwheel event, in * pixels. */ this.scrollThreshold = 20 * (window.devicePixelRatio || 1); /** * The maximum number of milliseconds to wait for a touch to end for the * gesture to be considered a click. */ this.clickTimingThreshold = 250; /** * The maximum number of pixels to allow a touch to move for the gesture to * be considered a click. */ this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); /** * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. * * @type {Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State( 0, 0, false, false, false, false, false ); /** * Fired whenever a mouse button is effectively pressed. This can happen * as part of a "click" gesture initiated by the user by tapping one * or more fingers over the touchpad element, as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousedown = null; /** * Fired whenever a mouse button is effectively released. This can happen * as part of a "click" gesture initiated by the user by tapping one * or more fingers over the touchpad element, as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmouseup = null; /** * Fired whenever the user moves the mouse by dragging their finger over * the touchpad element. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousemove = null; var touch_count = 0; var last_touch_x = 0; var last_touch_y = 0; var last_touch_time = 0; var pixels_moved = 0; var touch_buttons = { 1: "left", 2: "right", 3: "middle" }; var gesture_in_progress = false; var click_release_timeout = null; element.addEventListener("touchend", function(e) { e.preventDefault(); // If we're handling a gesture AND this is the last touch if (gesture_in_progress && e.touches.length === 0) { var time = new Date().getTime(); // Get corresponding mouse button var button = touch_buttons[touch_count]; // If mouse already down, release anad clear timeout if (guac_touchpad.currentState[button]) { // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Clear timeout, if set if (click_release_timeout) { window.clearTimeout(click_release_timeout); click_release_timeout = null; } } // If single tap detected (based on time and distance) if (time - last_touch_time <= guac_touchpad.clickTimingThreshold && pixels_moved < guac_touchpad.clickMoveThreshold) { // Fire button down event guac_touchpad.currentState[button] = true; if (guac_touchpad.onmousedown) guac_touchpad.onmousedown(guac_touchpad.currentState); // Delay mouse up - mouse up should be canceled if // touchstart within timeout. click_release_timeout = window.setTimeout(function() { // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Gesture now over gesture_in_progress = false; }, guac_touchpad.clickTimingThreshold); } // If we're not waiting to see if this is a click, stop gesture if (!click_release_timeout) gesture_in_progress = false; } }, false); element.addEventListener("touchstart", function(e) { e.preventDefault(); // Track number of touches, but no more than three touch_count = Math.min(e.touches.length, 3); // Clear timeout, if set if (click_release_timeout) { window.clearTimeout(click_release_timeout); click_release_timeout = null; } // Record initial touch location and time for touch movement // and tap gestures if (!gesture_in_progress) { // Stop mouse events while touching gesture_in_progress = true; // Record touch location and time var starting_touch = e.touches[0]; last_touch_x = starting_touch.clientX; last_touch_y = starting_touch.clientY; last_touch_time = new Date().getTime(); pixels_moved = 0; } }, false); element.addEventListener("touchmove", function(e) { e.preventDefault(); // Get change in touch location var touch = e.touches[0]; var delta_x = touch.clientX - last_touch_x; var delta_y = touch.clientY - last_touch_y; // Track pixels moved pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); // If only one touch involved, this is mouse move if (touch_count === 1) { // Calculate average velocity in Manhatten pixels per millisecond var velocity = pixels_moved / (new Date().getTime() - last_touch_time); // Scale mouse movement relative to velocity var scale = 1 + velocity; // Update mouse location guac_touchpad.currentState.x += delta_x*scale; guac_touchpad.currentState.y += delta_y*scale; // Prevent mouse from leaving screen if (guac_touchpad.currentState.x < 0) guac_touchpad.currentState.x = 0; else if (guac_touchpad.currentState.x >= element.offsetWidth) guac_touchpad.currentState.x = element.offsetWidth - 1; if (guac_touchpad.currentState.y < 0) guac_touchpad.currentState.y = 0; else if (guac_touchpad.currentState.y >= element.offsetHeight) guac_touchpad.currentState.y = element.offsetHeight - 1; // Fire movement event, if defined if (guac_touchpad.onmousemove) guac_touchpad.onmousemove(guac_touchpad.currentState); // Update touch location last_touch_x = touch.clientX; last_touch_y = touch.clientY; } // Interpret two-finger swipe as scrollwheel else if (touch_count === 2) { // If change in location passes threshold for scroll if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { // Decide button based on Y movement direction var button; if (delta_y > 0) button = "down"; else button = "up"; // Fire button down event guac_touchpad.currentState[button] = true; if (guac_touchpad.onmousedown) guac_touchpad.onmousedown(guac_touchpad.currentState); // Fire button up event guac_touchpad.currentState[button] = false; if (guac_touchpad.onmouseup) guac_touchpad.onmouseup(guac_touchpad.currentState); // Only update touch location after a scroll has been // detected last_touch_x = touch.clientX; last_touch_y = touch.clientY; } } }, false); }; /** * Provides cross-browser absolute touch event translation for a given element. * * Touch events are translated into mouse events as if the touches occurred * on a touchscreen (tapping anywhere on the screen clicks at that point, * long-press to right-click). * * @constructor * @param {Element} element The Element to use to provide touch events. */ Guacamole.Mouse.Touchscreen = function(element) { /** * Reference to this Guacamole.Mouse.Touchscreen. * @private */ var guac_touchscreen = this; /** * Whether a gesture is known to be in progress. If false, touch events * will be ignored. * * @private */ var gesture_in_progress = false; /** * The start X location of a gesture. * @private */ var gesture_start_x = null; /** * The start Y location of a gesture. * @private */ var gesture_start_y = null; /** * The timeout associated with the delayed, cancellable click release. * * @private */ var click_release_timeout = null; /** * The timeout associated with long-press for right click. * * @private */ var long_press_timeout = null; /** * The distance a two-finger touch must move per scrollwheel event, in * pixels. */ this.scrollThreshold = 20 * (window.devicePixelRatio || 1); /** * The maximum number of milliseconds to wait for a touch to end for the * gesture to be considered a click. */ this.clickTimingThreshold = 250; /** * The maximum number of pixels to allow a touch to move for the gesture to * be considered a click. */ this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); /** * The amount of time a press must be held for long press to be * detected. */ this.longPressThreshold = 500; /** * The current mouse state. The properties of this state are updated when * mouse events fire. This state object is also passed in as a parameter to * the handler of any mouse events. * * @type {Guacamole.Mouse.State} */ this.currentState = new Guacamole.Mouse.State( 0, 0, false, false, false, false, false ); /** * Fired whenever a mouse button is effectively pressed. This can happen * as part of a "mousedown" gesture initiated by the user by pressing one * finger over the touchscreen element, as part of a "scroll" gesture * initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousedown = null; /** * Fired whenever a mouse button is effectively released. This can happen * as part of a "mouseup" gesture initiated by the user by removing the * finger pressed against the touchscreen element, or as part of a "scroll" * gesture initiated by dragging two fingers up or down, etc. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmouseup = null; /** * Fired whenever the user moves the mouse by dragging their finger over * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, * dragging a finger over the touchscreen element will always cause * the mouse button to be effectively down, as if clicking-and-dragging. * * @event * @param {Guacamole.Mouse.State} state The current mouse state. */ this.onmousemove = null; /** * Presses the given mouse button, if it isn't already pressed. Valid * button values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to press. */ function press_button(button) { if (!guac_touchscreen.currentState[button]) { guac_touchscreen.currentState[button] = true; if (guac_touchscreen.onmousedown) guac_touchscreen.onmousedown(guac_touchscreen.currentState); } } /** * Releases the given mouse button, if it isn't already released. Valid * button values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to release. */ function release_button(button) { if (guac_touchscreen.currentState[button]) { guac_touchscreen.currentState[button] = false; if (guac_touchscreen.onmouseup) guac_touchscreen.onmouseup(guac_touchscreen.currentState); } } /** * Clicks (presses and releases) the given mouse button. Valid button * values are "left", "middle", "right", "up", and "down". * * @private * @param {String} button The mouse button to click. */ function click_button(button) { press_button(button); release_button(button); } /** * Moves the mouse to the given coordinates. These coordinates must be * relative to the browser window, as they will be translated based on * the touch event target's location within the browser window. * * @private * @param {Number} x The X coordinate of the mouse pointer. * @param {Number} y The Y coordinate of the mouse pointer. */ function move_mouse(x, y) { guac_touchscreen.currentState.fromClientPosition(element, x, y); if (guac_touchscreen.onmousemove) guac_touchscreen.onmousemove(guac_touchscreen.currentState); } /** * Returns whether the given touch event exceeds the movement threshold for * clicking, based on where the touch gesture began. * * @private * @param {TouchEvent} e The touch event to check. * @return {Boolean} true if the movement threshold is exceeded, false * otherwise. */ function finger_moved(e) { var touch = e.touches[0] || e.changedTouches[0]; var delta_x = touch.clientX - gesture_start_x; var delta_y = touch.clientY - gesture_start_y; return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; } /** * Begins a new gesture at the location of the first touch in the given * touch event. * * @private * @param {TouchEvent} e The touch event beginning this new gesture. */ function begin_gesture(e) { var touch = e.touches[0]; gesture_in_progress = true; gesture_start_x = touch.clientX; gesture_start_y = touch.clientY; } /** * End the current gesture entirely. Wait for all touches to be done before * resuming gesture detection. * * @private */ function end_gesture() { window.clearTimeout(click_release_timeout); window.clearTimeout(long_press_timeout); gesture_in_progress = false; } element.addEventListener("touchend", function(e) { // Do not handle if no gesture if (!gesture_in_progress) return; // Ignore if more than one touch if (e.touches.length !== 0 || e.changedTouches.length !== 1) { end_gesture(); return; } // Long-press, if any, is over window.clearTimeout(long_press_timeout); // Always release mouse button if pressed release_button("left"); // If finger hasn't moved enough to cancel the click if (!finger_moved(e)) { e.preventDefault(); // If not yet pressed, press and start delay release if (!guac_touchscreen.currentState.left) { var touch = e.changedTouches[0]; move_mouse(touch.clientX, touch.clientY); press_button("left"); // Release button after a delay, if not canceled click_release_timeout = window.setTimeout(function() { release_button("left"); end_gesture(); }, guac_touchscreen.clickTimingThreshold); } } // end if finger not moved }, false); element.addEventListener("touchstart", function(e) { // Ignore if more than one touch if (e.touches.length !== 1) { end_gesture(); return; } e.preventDefault(); // New touch begins a new gesture begin_gesture(e); // Keep button pressed if tap after left click window.clearTimeout(click_release_timeout); // Click right button if this turns into a long-press long_press_timeout = window.setTimeout(function() { var touch = e.touches[0]; move_mouse(touch.clientX, touch.clientY); click_button("right"); end_gesture(); }, guac_touchscreen.longPressThreshold); }, false); element.addEventListener("touchmove", function(e) { // Do not handle if no gesture if (!gesture_in_progress) return; // Cancel long press if finger moved if (finger_moved(e)) window.clearTimeout(long_press_timeout); // Ignore if more than one touch if (e.touches.length !== 1) { end_gesture(); return; } // Update mouse position if dragging if (guac_touchscreen.currentState.left) { e.preventDefault(); // Update state var touch = e.touches[0]; move_mouse(touch.clientX, touch.clientY); } }, false); };