X-Git-Url: http://git.alex.org.uk diff --git a/src/main/resources/mouse.js b/src/main/resources/mouse.js index 86672c9..9dc2f22 100644 --- a/src/main/resources/mouse.js +++ b/src/main/resources/mouse.js @@ -35,7 +35,10 @@ * * ***** END LICENSE BLOCK ***** */ -// Guacamole namespace +/** + * Namespace for all Guacamole JavaScript objects. + * @namespace + */ var Guacamole = Guacamole || {}; /** @@ -44,9 +47,6 @@ var Guacamole = Guacamole || {}; * mouse events into a non-browser-specific event provided by the * Guacamole.Mouse instance. * - * 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 mouse events. */ @@ -59,22 +59,10 @@ Guacamole.Mouse = function(element) { var guac_mouse = 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. + * The number of mousemove events to require before re-enabling mouse + * event handling after receiving a touch event. */ - this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); + this.touchMouseThreshold = 3; /** * The current mouse state. The properties of this state are updated when @@ -115,55 +103,260 @@ Guacamole.Mouse = function(element) { */ this.onmousemove = 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; + function cancelEvent(e) { e.stopPropagation(); if (e.preventDefault) e.preventDefault(); e.returnValue = false; } - function moveMouse(clientX, clientY) { - - guac_mouse.currentState.x = clientX - element.offsetLeft; - guac_mouse.currentState.y = clientY - element.offsetTop; + // Block context menu so right-click gets sent properly + element.addEventListener("contextmenu", function(e) { + cancelEvent(e); + }, false); - // This is all JUST so we can get the mouse position within the element - var parent = element.offsetParent; - while (parent) { + element.addEventListener("mousemove", function(e) { - guac_mouse.currentState.x -= parent.offsetLeft - parent.scrollLeft; - guac_mouse.currentState.y -= parent.offsetTop - parent.scrollTop; + cancelEvent(e); - parent = parent.offsetParent; + // 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) { - // Block context menu so right-click gets sent properly - element.addEventListener("contextmenu", 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("mousemove", function(e) { + 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); - // Don't handle if we aren't supposed to - if (gesture_in_progress) return; + 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 != null) { + if (target === element) + return; + target = target.parentNode; + } cancelEvent(e); - moveMouse(e.clientX, e.clientY); + // 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); + } + + }, 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) { + + var delta = 0; + if (e.detail) + delta = e.detail; + else if (e.wheelDelta) + delta = -event.wheelDelta; + + // Up + if (delta < 0) { + 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); + } + } + + // Down + if (delta > 0) { + 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); + } + } + + cancelEvent(e); + + } + element.addEventListener('DOMMouseScroll', mousewheel_handler, false); + element.addEventListener('mousewheel', mousewheel_handler, false); + +}; + + +/** + * 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_distance = 0; var touch_buttons = { 1: "left", @@ -176,23 +369,24 @@ Guacamole.Mouse = function(element) { element.addEventListener("touchend", function(e) { + e.stopPropagation(); + e.preventDefault(); + // If we're handling a gesture AND this is the last touch if (gesture_in_progress && e.touches.length == 0) { - cancelEvent(e); - 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_mouse.currentState[button]) { + if (guac_touchpad.currentState[button]) { // Fire button up event - guac_mouse.currentState[button] = false; - if (guac_mouse.onmouseup) - guac_mouse.onmouseup(guac_mouse.currentState); + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); // Clear timeout, if set if (click_release_timeout) { @@ -203,54 +397,59 @@ Guacamole.Mouse = function(element) { } // If single tap detected (based on time and distance) - if (time - last_touch_time <= guac_mouse.clickTimingThreshold - && pixels_moved < guac_mouse.clickMoveThreshold) { + if (time - last_touch_time <= guac_touchpad.clickTimingThreshold + && pixels_moved < guac_touchpad.clickMoveThreshold) { // Fire button down event - guac_mouse.currentState[button] = true; - if (guac_mouse.onmousedown) - guac_mouse.onmousedown(guac_mouse.currentState); + 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_mouse.currentState[button] = false; - if (guac_mouse.onmouseup) - guac_mouse.onmouseup(guac_mouse.currentState); + guac_touchpad.currentState[button] = false; + if (guac_touchpad.onmouseup) + guac_touchpad.onmouseup(guac_touchpad.currentState); - // Allow mouse events now that touching is over + // Gesture now over gesture_in_progress = false; - - }, guac_mouse.clickTimingThreshold); + + }, 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.stopPropagation(); + 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 (e.touches.length == 1) { - - cancelEvent(e); + if (!gesture_in_progress) { // Stop mouse events while touching gesture_in_progress = true; - // Clear timeout, if set - if (click_release_timeout) { - window.clearTimeout(click_release_timeout); - click_release_timeout = null; - } - // Record touch location and time var starting_touch = e.touches[0]; last_touch_x = starting_touch.clientX; @@ -264,7 +463,8 @@ Guacamole.Mouse = function(element) { element.addEventListener("touchmove", function(e) { - cancelEvent(e); + e.stopPropagation(); + e.preventDefault(); // Get change in touch location var touch = e.touches[0]; @@ -277,25 +477,31 @@ Guacamole.Mouse = function(element) { // 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_mouse.currentState.x += delta_x; - guac_mouse.currentState.y += delta_y; + guac_touchpad.currentState.x += delta_x*scale; + guac_touchpad.currentState.y += delta_y*scale; // Prevent mouse from leaving screen - if (guac_mouse.currentState.x < 0) - guac_mouse.currentState.x = 0; - else if (guac_mouse.currentState.x >= element.offsetWidth) - guac_mouse.currentState.x = element.offsetWidth - 1; + 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_mouse.currentState.y < 0) - guac_mouse.currentState.y = 0; - else if (guac_mouse.currentState.y >= element.offsetHeight) - guac_mouse.currentState.y = element.offsetHeight - 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_mouse.onmousemove) - guac_mouse.onmousemove(guac_mouse.currentState); + if (guac_touchpad.onmousemove) + guac_touchpad.onmousemove(guac_touchpad.currentState); // Update touch location last_touch_x = touch.clientX; @@ -307,7 +513,7 @@ Guacamole.Mouse = function(element) { else if (touch_count == 2) { // If change in location passes threshold for scroll - if (Math.abs(delta_y) >= guac_mouse.scrollThreshold) { + if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { // Decide button based on Y movement direction var button; @@ -315,14 +521,14 @@ Guacamole.Mouse = function(element) { else button = "up"; // Fire button down event - guac_mouse.currentState[button] = true; - if (guac_mouse.onmousedown) - guac_mouse.onmousedown(guac_mouse.currentState); + guac_touchpad.currentState[button] = true; + if (guac_touchpad.onmousedown) + guac_touchpad.onmousedown(guac_touchpad.currentState); // Fire button up event - guac_mouse.currentState[button] = false; - if (guac_mouse.onmouseup) - guac_mouse.onmouseup(guac_mouse.currentState); + 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 @@ -335,137 +541,138 @@ Guacamole.Mouse = function(element) { }, false); +}; - element.addEventListener("mousedown", function(e) { +/** + * 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) { - // Don't handle if we aren't supposed to - if (gesture_in_progress) return; + /** + * Reference to this Guacamole.Mouse.Touchscreen. + * @private + */ + var guac_touchscreen = this; - cancelEvent(e); + /** + * The distance a two-finger touch must move per scrollwheel event, in + * pixels. + */ + this.scrollThreshold = 20 * (window.devicePixelRatio || 1); - 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; - } + /** + * 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 + ); - if (guac_mouse.onmousedown) - guac_mouse.onmousedown(guac_mouse.currentState); + /** + * 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; - }, false); + /** + * 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; - element.addEventListener("mouseup", function(e) { + element.addEventListener("touchend", function(e) { + + // Ignore if more than one touch + if (e.touches.length + e.changedTouches.length != 1) + return; - // Don't handle if we aren't supposed to - if (gesture_in_progress) return; + e.stopPropagation(); + e.preventDefault(); - cancelEvent(e); + // Release button + guac_touchscreen.currentState.left = false; - 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); + // Fire release event when the last touch is released, if event defined + if (e.touches.length == 0 && guac_touchscreen.onmouseup) + guac_touchscreen.onmouseup(guac_touchscreen.currentState); }, false); - element.addEventListener("mouseout", function(e) { - - // Don't handle if we aren't supposed to - if (gesture_in_progress) return; - - // Get parent of the element the mouse pointer is leaving - if (!e) e = window.event; + element.addEventListener("touchstart", function(e) { - // Check that mouseout is due to actually LEAVING the element - var target = e.relatedTarget || e.toElement; - while (target != null) { - if (target === element) - return; - target = target.parentNode; - } + // Ignore if more than one touch + if (e.touches.length != 1) + return; - cancelEvent(e); + e.stopPropagation(); + e.preventDefault(); - // Release all buttons - if (guac_mouse.currentState.left - || guac_mouse.currentState.middle - || guac_mouse.currentState.right) { + // Get touch + var touch = e.touches[0]; - guac_mouse.currentState.left = false; - guac_mouse.currentState.middle = false; - guac_mouse.currentState.right = false; + // Update state + guac_touchscreen.currentState.left = true; + guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY); - if (guac_mouse.onmouseup) - guac_mouse.onmouseup(guac_mouse.currentState); - } + // Fire press event, if defined + if (guac_touchscreen.onmousedown) + guac_touchscreen.onmousedown(guac_touchscreen.currentState); - }, false); - // Override selection on mouse event element. - element.addEventListener("selectstart", function(e) { - cancelEvent(e); }, false); - // Scroll wheel support - function mousewheel_handler(e) { - - // Don't handle if we aren't supposed to - if (gesture_in_progress) return; - - var delta = 0; - if (e.detail) - delta = e.detail; - else if (e.wheelDelta) - delta = -event.wheelDelta; + element.addEventListener("touchmove", function(e) { - // Up - if (delta < 0) { - if (guac_mouse.onmousedown) { - guac_mouse.currentState.up = true; - guac_mouse.onmousedown(guac_mouse.currentState); - } + // Ignore if more than one touch + if (e.touches.length != 1) + return; - if (guac_mouse.onmouseup) { - guac_mouse.currentState.up = false; - guac_mouse.onmouseup(guac_mouse.currentState); - } - } + e.stopPropagation(); + e.preventDefault(); - // Down - if (delta > 0) { - if (guac_mouse.onmousedown) { - guac_mouse.currentState.down = true; - guac_mouse.onmousedown(guac_mouse.currentState); - } + // Get touch + var touch = e.touches[0]; - if (guac_mouse.onmouseup) { - guac_mouse.currentState.down = false; - guac_mouse.onmouseup(guac_mouse.currentState); - } - } + // Update state + guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY); - cancelEvent(e); + // Fire movement event, if defined + if (guac_touchscreen.onmousemove) + guac_touchscreen.onmousemove(guac_touchscreen.currentState); - } - element.addEventListener('DOMMouseScroll', mousewheel_handler, false); - element.addEventListener('mousewheel', mousewheel_handler, false); + }, false); }; @@ -486,6 +693,12 @@ Guacamole.Mouse = function(element) { 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 */ @@ -530,6 +743,43 @@ Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { * @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; + } + + }; + };