Fix jsdoc, add missing documentation.
[guacamole-common-js.git] / src / main / resources / mouse.js
index 8613ce9..9dc2f22 100644 (file)
  *
  * ***** 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.
+     * The number of mousemove events to require before re-enabling mouse
+     * event handling after receiving a touch event.
      */
-    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);
+    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) {
+
+        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) {
 
-    // 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 = 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("mousemove", function(e) {
+    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;
+
+        // 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];
@@ -284,24 +484,24 @@ Guacamole.Mouse = function(element) {
             var scale = 1 + velocity;
 
             // Update mouse location
-            guac_mouse.currentState.x += delta_x*scale;
-            guac_mouse.currentState.y += delta_y*scale;
+            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;
@@ -313,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;
@@ -321,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
@@ -341,137 +541,138 @@ Guacamole.Mouse = function(element) {
 
     }, false);
 
+};
 
-    element.addEventListener("mousedown", function(e) {
-
-        // Don't handle if we aren't supposed to
-        if (gesture_in_progress) return;
+/**
+ * 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) {
 
-        cancelEvent(e);
+    /**
+     * Reference to this Guacamole.Mouse.Touchscreen.
+     * @private
+     */
+    var guac_touchscreen = this;
 
-        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 distance a two-finger touch must move per scrollwheel event, in
+     * pixels.
+     */
+    this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
 
-        if (guac_mouse.onmousedown)
-            guac_mouse.onmousedown(guac_mouse.currentState);
+    /**
+     * 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
+    );
 
-    }, 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;
 
-    element.addEventListener("mouseup", function(e) {
+    /**
+     * 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;
 
-        // Don't handle if we aren't supposed to
-        if (gesture_in_progress) return;
+    element.addEventListener("touchend", function(e) {
+        
+        // Ignore if more than one touch
+        if (e.touches.length + e.changedTouches.length != 1)
+            return;
 
-        cancelEvent(e);
+        e.stopPropagation();
+        e.preventDefault();
 
-        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;
-        }
+        // Release button
+        guac_touchscreen.currentState.left = false;
 
-        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);
 
 };
 
@@ -492,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
      */
@@ -536,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;
+        }
+
+    };
+
 };