1 
  2 /* ***** BEGIN LICENSE BLOCK *****
  3  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  4  *
  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/
  9  *
 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
 13  * License.
 14  *
 15  * The Original Code is guacamole-common-js.
 16  *
 17  * The Initial Developer of the Original Code is
 18  * Michael Jumper.
 19  * Portions created by the Initial Developer are Copyright (C) 2010
 20  * the Initial Developer. All Rights Reserved.
 21  *
 22  * Contributor(s):
 23  *
 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.
 35  *
 36  * ***** END LICENSE BLOCK ***** */
 37 
 38 /**
 39  * Namespace for all Guacamole JavaScript objects.
 40  * @namespace
 41  */
 42 var Guacamole = Guacamole || {};
 43 
 44 /**
 45  * Provides cross-browser mouse events for a given element. The events of
 46  * the given element are automatically populated with handlers that translate
 47  * mouse events into a non-browser-specific event provided by the
 48  * Guacamole.Mouse instance.
 49  * 
 50  * @constructor
 51  * @param {Element} element The Element to use to provide mouse events.
 52  */
 53 Guacamole.Mouse = function(element) {
 54 
 55     /**
 56      * Reference to this Guacamole.Mouse.
 57      * @private
 58      */
 59     var guac_mouse = this;
 60 
 61     /**
 62      * The number of mousemove events to require before re-enabling mouse
 63      * event handling after receiving a touch event.
 64      */
 65     this.touchMouseThreshold = 3;
 66 
 67     /**
 68      * The minimum amount of pixels scrolled required for a single scroll button
 69      * click.
 70      */
 71     this.scrollThreshold = 120;
 72 
 73     /**
 74      * The number of pixels to scroll per line.
 75      */
 76     this.PIXELS_PER_LINE = 40;
 77 
 78     /**
 79      * The number of pixels to scroll per page.
 80      */
 81     this.PIXELS_PER_PAGE = 640;
 82 
 83     /**
 84      * The current mouse state. The properties of this state are updated when
 85      * mouse events fire. This state object is also passed in as a parameter to
 86      * the handler of any mouse events.
 87      * 
 88      * @type Guacamole.Mouse.State
 89      */
 90     this.currentState = new Guacamole.Mouse.State(
 91         0, 0, 
 92         false, false, false, false, false
 93     );
 94 
 95     /**
 96      * Fired whenever the user presses a mouse button down over the element
 97      * associated with this Guacamole.Mouse.
 98      * 
 99      * @event
100      * @param {Guacamole.Mouse.State} state The current mouse state.
101      */
102 	this.onmousedown = null;
103 
104     /**
105      * Fired whenever the user releases a mouse button down over the element
106      * associated with this Guacamole.Mouse.
107      * 
108      * @event
109      * @param {Guacamole.Mouse.State} state The current mouse state.
110      */
111 	this.onmouseup = null;
112 
113     /**
114      * Fired whenever the user moves the mouse over the element associated with
115      * this Guacamole.Mouse.
116      * 
117      * @event
118      * @param {Guacamole.Mouse.State} state The current mouse state.
119      */
120 	this.onmousemove = null;
121 
122     /**
123      * Counter of mouse events to ignore. This decremented by mousemove, and
124      * while non-zero, mouse events will have no effect.
125      * @private
126      */
127     var ignore_mouse = 0;
128 
129     /**
130      * Cumulative scroll delta amount. This value is accumulated through scroll
131      * events and results in scroll button clicks if it exceeds a certain
132      * threshold.
133      */
134     var scroll_delta = 0;
135 
136     function cancelEvent(e) {
137         e.stopPropagation();
138         if (e.preventDefault) e.preventDefault();
139         e.returnValue = false;
140     }
141 
142     // Block context menu so right-click gets sent properly
143     element.addEventListener("contextmenu", function(e) {
144         cancelEvent(e);
145     }, false);
146 
147     element.addEventListener("mousemove", function(e) {
148 
149         cancelEvent(e);
150 
151         // If ignoring events, decrement counter
152         if (ignore_mouse) {
153             ignore_mouse--;
154             return;
155         }
156 
157         guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY);
158 
159         if (guac_mouse.onmousemove)
160             guac_mouse.onmousemove(guac_mouse.currentState);
161 
162     }, false);
163 
164     element.addEventListener("mousedown", function(e) {
165 
166         cancelEvent(e);
167 
168         // Do not handle if ignoring events
169         if (ignore_mouse)
170             return;
171 
172         switch (e.button) {
173             case 0:
174                 guac_mouse.currentState.left = true;
175                 break;
176             case 1:
177                 guac_mouse.currentState.middle = true;
178                 break;
179             case 2:
180                 guac_mouse.currentState.right = true;
181                 break;
182         }
183 
184         if (guac_mouse.onmousedown)
185             guac_mouse.onmousedown(guac_mouse.currentState);
186 
187     }, false);
188 
189     element.addEventListener("mouseup", function(e) {
190 
191         cancelEvent(e);
192 
193         // Do not handle if ignoring events
194         if (ignore_mouse)
195             return;
196 
197         switch (e.button) {
198             case 0:
199                 guac_mouse.currentState.left = false;
200                 break;
201             case 1:
202                 guac_mouse.currentState.middle = false;
203                 break;
204             case 2:
205                 guac_mouse.currentState.right = false;
206                 break;
207         }
208 
209         if (guac_mouse.onmouseup)
210             guac_mouse.onmouseup(guac_mouse.currentState);
211 
212     }, false);
213 
214     element.addEventListener("mouseout", function(e) {
215 
216         // Get parent of the element the mouse pointer is leaving
217        	if (!e) e = window.event;
218 
219         // Check that mouseout is due to actually LEAVING the element
220         var target = e.relatedTarget || e.toElement;
221         while (target != null) {
222             if (target === element)
223                 return;
224             target = target.parentNode;
225         }
226 
227         cancelEvent(e);
228 
229         // Release all buttons
230         if (guac_mouse.currentState.left
231             || guac_mouse.currentState.middle
232             || guac_mouse.currentState.right) {
233 
234             guac_mouse.currentState.left = false;
235             guac_mouse.currentState.middle = false;
236             guac_mouse.currentState.right = false;
237 
238             if (guac_mouse.onmouseup)
239                 guac_mouse.onmouseup(guac_mouse.currentState);
240         }
241 
242     }, false);
243 
244     // Override selection on mouse event element.
245     element.addEventListener("selectstart", function(e) {
246         cancelEvent(e);
247     }, false);
248 
249     // Ignore all pending mouse events when touch events are the apparent source
250     function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; }
251 
252     element.addEventListener("touchmove",  ignorePendingMouseEvents, false);
253     element.addEventListener("touchstart", ignorePendingMouseEvents, false);
254     element.addEventListener("touchend",   ignorePendingMouseEvents, false);
255 
256     // Scroll wheel support
257     function mousewheel_handler(e) {
258 
259         // Determine approximate scroll amount (in pixels)
260         var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta;
261 
262         // If successfully retrieved scroll amount, convert to pixels if not
263         // already in pixels
264         if (delta) {
265 
266             // Convert to pixels if delta was lines
267             if (e.deltaMode === 1)
268                 delta = e.deltaY * guac_mouse.PIXELS_PER_LINE;
269 
270             // Convert to pixels if delta was pages
271             else if (e.deltaMode === 2)
272                 delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE;
273 
274         }
275 
276         // Otherwise, assume legacy mousewheel event and line scrolling
277         else
278             delta = e.detail * guac_mouse.PIXELS_PER_LINE;
279         
280         // Update overall delta
281         scroll_delta += delta;
282 
283         // Up
284         while (scroll_delta <= -guac_mouse.scrollThreshold) {
285 
286             if (guac_mouse.onmousedown) {
287                 guac_mouse.currentState.up = true;
288                 guac_mouse.onmousedown(guac_mouse.currentState);
289             }
290 
291             if (guac_mouse.onmouseup) {
292                 guac_mouse.currentState.up = false;
293                 guac_mouse.onmouseup(guac_mouse.currentState);
294             }
295 
296             scroll_delta += guac_mouse.scrollThreshold;
297 
298         }
299 
300         // Down
301         while (scroll_delta >= guac_mouse.scrollThreshold) {
302 
303             if (guac_mouse.onmousedown) {
304                 guac_mouse.currentState.down = true;
305                 guac_mouse.onmousedown(guac_mouse.currentState);
306             }
307 
308             if (guac_mouse.onmouseup) {
309                 guac_mouse.currentState.down = false;
310                 guac_mouse.onmouseup(guac_mouse.currentState);
311             }
312 
313             scroll_delta -= guac_mouse.scrollThreshold;
314 
315         }
316 
317         cancelEvent(e);
318 
319     }
320 
321     element.addEventListener('DOMMouseScroll', mousewheel_handler, false);
322     element.addEventListener('mousewheel',     mousewheel_handler, false);
323     element.addEventListener('wheel',          mousewheel_handler, false);
324 
325 };
326 
327 
328 /**
329  * Provides cross-browser relative touch event translation for a given element.
330  * 
331  * Touch events are translated into mouse events as if the touches occurred
332  * on a touchpad (drag to push the mouse pointer, tap to click).
333  * 
334  * @constructor
335  * @param {Element} element The Element to use to provide touch events.
336  */
337 Guacamole.Mouse.Touchpad = function(element) {
338 
339     /**
340      * Reference to this Guacamole.Mouse.Touchpad.
341      * @private
342      */
343     var guac_touchpad = this;
344 
345     /**
346      * The distance a two-finger touch must move per scrollwheel event, in
347      * pixels.
348      */
349     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
350 
351     /**
352      * The maximum number of milliseconds to wait for a touch to end for the
353      * gesture to be considered a click.
354      */
355     this.clickTimingThreshold = 250;
356 
357     /**
358      * The maximum number of pixels to allow a touch to move for the gesture to
359      * be considered a click.
360      */
361     this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1);
362 
363     /**
364      * The current mouse state. The properties of this state are updated when
365      * mouse events fire. This state object is also passed in as a parameter to
366      * the handler of any mouse events.
367      * 
368      * @type Guacamole.Mouse.State
369      */
370     this.currentState = new Guacamole.Mouse.State(
371         0, 0, 
372         false, false, false, false, false
373     );
374 
375     /**
376      * Fired whenever a mouse button is effectively pressed. This can happen
377      * as part of a "click" gesture initiated by the user by tapping one
378      * or more fingers over the touchpad element, as part of a "scroll"
379      * gesture initiated by dragging two fingers up or down, etc.
380      * 
381      * @event
382      * @param {Guacamole.Mouse.State} state The current mouse state.
383      */
384 	this.onmousedown = null;
385 
386     /**
387      * Fired whenever a mouse button is effectively released. This can happen
388      * as part of a "click" gesture initiated by the user by tapping one
389      * or more fingers over the touchpad element, as part of a "scroll"
390      * gesture initiated by dragging two fingers up or down, etc.
391      * 
392      * @event
393      * @param {Guacamole.Mouse.State} state The current mouse state.
394      */
395 	this.onmouseup = null;
396 
397     /**
398      * Fired whenever the user moves the mouse by dragging their finger over
399      * the touchpad element.
400      * 
401      * @event
402      * @param {Guacamole.Mouse.State} state The current mouse state.
403      */
404 	this.onmousemove = null;
405 
406     var touch_count = 0;
407     var last_touch_x = 0;
408     var last_touch_y = 0;
409     var last_touch_time = 0;
410     var pixels_moved = 0;
411 
412     var touch_buttons = {
413         1: "left",
414         2: "right",
415         3: "middle"
416     };
417 
418     var gesture_in_progress = false;
419     var click_release_timeout = null;
420 
421     element.addEventListener("touchend", function(e) {
422         
423         e.stopPropagation();
424         e.preventDefault();
425             
426         // If we're handling a gesture AND this is the last touch
427         if (gesture_in_progress && e.touches.length == 0) {
428             
429             var time = new Date().getTime();
430 
431             // Get corresponding mouse button
432             var button = touch_buttons[touch_count];
433 
434             // If mouse already down, release anad clear timeout
435             if (guac_touchpad.currentState[button]) {
436 
437                 // Fire button up event
438                 guac_touchpad.currentState[button] = false;
439                 if (guac_touchpad.onmouseup)
440                     guac_touchpad.onmouseup(guac_touchpad.currentState);
441 
442                 // Clear timeout, if set
443                 if (click_release_timeout) {
444                     window.clearTimeout(click_release_timeout);
445                     click_release_timeout = null;
446                 }
447 
448             }
449 
450             // If single tap detected (based on time and distance)
451             if (time - last_touch_time <= guac_touchpad.clickTimingThreshold
452                     && pixels_moved < guac_touchpad.clickMoveThreshold) {
453 
454                 // Fire button down event
455                 guac_touchpad.currentState[button] = true;
456                 if (guac_touchpad.onmousedown)
457                     guac_touchpad.onmousedown(guac_touchpad.currentState);
458 
459                 // Delay mouse up - mouse up should be canceled if
460                 // touchstart within timeout.
461                 click_release_timeout = window.setTimeout(function() {
462                     
463                     // Fire button up event
464                     guac_touchpad.currentState[button] = false;
465                     if (guac_touchpad.onmouseup)
466                         guac_touchpad.onmouseup(guac_touchpad.currentState);
467                     
468                     // Gesture now over
469                     gesture_in_progress = false;
470 
471                 }, guac_touchpad.clickTimingThreshold);
472 
473             }
474 
475             // If we're not waiting to see if this is a click, stop gesture
476             if (!click_release_timeout)
477                 gesture_in_progress = false;
478 
479         }
480 
481     }, false);
482 
483     element.addEventListener("touchstart", function(e) {
484 
485         e.stopPropagation();
486         e.preventDefault();
487 
488         // Track number of touches, but no more than three
489         touch_count = Math.min(e.touches.length, 3);
490 
491         // Clear timeout, if set
492         if (click_release_timeout) {
493             window.clearTimeout(click_release_timeout);
494             click_release_timeout = null;
495         }
496 
497         // Record initial touch location and time for touch movement
498         // and tap gestures
499         if (!gesture_in_progress) {
500 
501             // Stop mouse events while touching
502             gesture_in_progress = true;
503 
504             // Record touch location and time
505             var starting_touch = e.touches[0];
506             last_touch_x = starting_touch.clientX;
507             last_touch_y = starting_touch.clientY;
508             last_touch_time = new Date().getTime();
509             pixels_moved = 0;
510 
511         }
512 
513     }, false);
514 
515     element.addEventListener("touchmove", function(e) {
516 
517         e.stopPropagation();
518         e.preventDefault();
519 
520         // Get change in touch location
521         var touch = e.touches[0];
522         var delta_x = touch.clientX - last_touch_x;
523         var delta_y = touch.clientY - last_touch_y;
524 
525         // Track pixels moved
526         pixels_moved += Math.abs(delta_x) + Math.abs(delta_y);
527 
528         // If only one touch involved, this is mouse move
529         if (touch_count == 1) {
530 
531             // Calculate average velocity in Manhatten pixels per millisecond
532             var velocity = pixels_moved / (new Date().getTime() - last_touch_time);
533 
534             // Scale mouse movement relative to velocity
535             var scale = 1 + velocity;
536 
537             // Update mouse location
538             guac_touchpad.currentState.x += delta_x*scale;
539             guac_touchpad.currentState.y += delta_y*scale;
540 
541             // Prevent mouse from leaving screen
542 
543             if (guac_touchpad.currentState.x < 0)
544                 guac_touchpad.currentState.x = 0;
545             else if (guac_touchpad.currentState.x >= element.offsetWidth)
546                 guac_touchpad.currentState.x = element.offsetWidth - 1;
547 
548             if (guac_touchpad.currentState.y < 0)
549                 guac_touchpad.currentState.y = 0;
550             else if (guac_touchpad.currentState.y >= element.offsetHeight)
551                 guac_touchpad.currentState.y = element.offsetHeight - 1;
552 
553             // Fire movement event, if defined
554             if (guac_touchpad.onmousemove)
555                 guac_touchpad.onmousemove(guac_touchpad.currentState);
556 
557             // Update touch location
558             last_touch_x = touch.clientX;
559             last_touch_y = touch.clientY;
560 
561         }
562 
563         // Interpret two-finger swipe as scrollwheel
564         else if (touch_count == 2) {
565 
566             // If change in location passes threshold for scroll
567             if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) {
568 
569                 // Decide button based on Y movement direction
570                 var button;
571                 if (delta_y > 0) button = "down";
572                 else             button = "up";
573 
574                 // Fire button down event
575                 guac_touchpad.currentState[button] = true;
576                 if (guac_touchpad.onmousedown)
577                     guac_touchpad.onmousedown(guac_touchpad.currentState);
578 
579                 // Fire button up event
580                 guac_touchpad.currentState[button] = false;
581                 if (guac_touchpad.onmouseup)
582                     guac_touchpad.onmouseup(guac_touchpad.currentState);
583 
584                 // Only update touch location after a scroll has been
585                 // detected
586                 last_touch_x = touch.clientX;
587                 last_touch_y = touch.clientY;
588 
589             }
590 
591         }
592 
593     }, false);
594 
595 };
596 
597 /**
598  * Provides cross-browser absolute touch event translation for a given element.
599  * 
600  * Touch events are translated into mouse events as if the touches occurred
601  * on a touchscreen (tapping anywhere on the screen clicks at that point,
602  * long-press to right-click).
603  * 
604  * @constructor
605  * @param {Element} element The Element to use to provide touch events.
606  */
607 Guacamole.Mouse.Touchscreen = function(element) {
608 
609     /**
610      * Reference to this Guacamole.Mouse.Touchscreen.
611      * @private
612      */
613     var guac_touchscreen = this;
614 
615     /**
616      * The distance a two-finger touch must move per scrollwheel event, in
617      * pixels.
618      */
619     this.scrollThreshold = 20 * (window.devicePixelRatio || 1);
620 
621     /**
622      * The current mouse state. The properties of this state are updated when
623      * mouse events fire. This state object is also passed in as a parameter to
624      * the handler of any mouse events.
625      * 
626      * @type Guacamole.Mouse.State
627      */
628     this.currentState = new Guacamole.Mouse.State(
629         0, 0, 
630         false, false, false, false, false
631     );
632 
633     /**
634      * Fired whenever a mouse button is effectively pressed. This can happen
635      * as part of a "mousedown" gesture initiated by the user by pressing one
636      * finger over the touchscreen element, as part of a "scroll" gesture
637      * initiated by dragging two fingers up or down, etc.
638      * 
639      * @event
640      * @param {Guacamole.Mouse.State} state The current mouse state.
641      */
642 	this.onmousedown = null;
643 
644     /**
645      * Fired whenever a mouse button is effectively released. This can happen
646      * as part of a "mouseup" gesture initiated by the user by removing the
647      * finger pressed against the touchscreen element, or as part of a "scroll"
648      * gesture initiated by dragging two fingers up or down, etc.
649      * 
650      * @event
651      * @param {Guacamole.Mouse.State} state The current mouse state.
652      */
653 	this.onmouseup = null;
654 
655     /**
656      * Fired whenever the user moves the mouse by dragging their finger over
657      * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad,
658      * dragging a finger over the touchscreen element will always cause
659      * the mouse button to be effectively down, as if clicking-and-dragging.
660      * 
661      * @event
662      * @param {Guacamole.Mouse.State} state The current mouse state.
663      */
664 	this.onmousemove = null;
665 
666     element.addEventListener("touchend", function(e) {
667         
668         // Ignore if more than one touch
669         if (e.touches.length + e.changedTouches.length != 1)
670             return;
671 
672         e.stopPropagation();
673         e.preventDefault();
674 
675         // Release button
676         guac_touchscreen.currentState.left = false;
677 
678         // Fire release event when the last touch is released, if event defined
679         if (e.touches.length == 0 && guac_touchscreen.onmouseup)
680             guac_touchscreen.onmouseup(guac_touchscreen.currentState);
681 
682     }, false);
683 
684     element.addEventListener("touchstart", function(e) {
685 
686         // Ignore if more than one touch
687         if (e.touches.length != 1)
688             return;
689 
690         e.stopPropagation();
691         e.preventDefault();
692 
693         // Get touch
694         var touch = e.touches[0];
695 
696         // Update state
697         guac_touchscreen.currentState.left = true;
698         guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY);
699 
700         // Fire press event, if defined
701         if (guac_touchscreen.onmousedown)
702             guac_touchscreen.onmousedown(guac_touchscreen.currentState);
703 
704 
705     }, false);
706 
707     element.addEventListener("touchmove", function(e) {
708 
709         // Ignore if more than one touch
710         if (e.touches.length != 1)
711             return;
712 
713         e.stopPropagation();
714         e.preventDefault();
715 
716         // Get touch
717         var touch = e.touches[0];
718 
719         // Update state
720         guac_touchscreen.currentState.fromClientPosition(element, touch.clientX, touch.clientY);
721 
722         // Fire movement event, if defined
723         if (guac_touchscreen.onmousemove)
724             guac_touchscreen.onmousemove(guac_touchscreen.currentState);
725 
726     }, false);
727 
728 };
729 
730 /**
731  * Simple container for properties describing the state of a mouse.
732  * 
733  * @constructor
734  * @param {Number} x The X position of the mouse pointer in pixels.
735  * @param {Number} y The Y position of the mouse pointer in pixels.
736  * @param {Boolean} left Whether the left mouse button is pressed. 
737  * @param {Boolean} middle Whether the middle mouse button is pressed. 
738  * @param {Boolean} right Whether the right mouse button is pressed. 
739  * @param {Boolean} up Whether the up mouse button is pressed (the fourth
740  *                     button, usually part of a scroll wheel). 
741  * @param {Boolean} down Whether the down mouse button is pressed (the fifth
742  *                       button, usually part of a scroll wheel). 
743  */
744 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) {
745 
746     /**
747      * Reference to this Guacamole.Mouse.State.
748      * @private
749      */
750     var guac_state = this;
751 
752     /**
753      * The current X position of the mouse pointer.
754      * @type Number
755      */
756     this.x = x;
757 
758     /**
759      * The current Y position of the mouse pointer.
760      * @type Number
761      */
762     this.y = y;
763 
764     /**
765      * Whether the left mouse button is currently pressed.
766      * @type Boolean
767      */
768     this.left = left;
769 
770     /**
771      * Whether the middle mouse button is currently pressed.
772      * @type Boolean
773      */
774     this.middle = middle
775 
776     /**
777      * Whether the right mouse button is currently pressed.
778      * @type Boolean
779      */
780     this.right = right;
781 
782     /**
783      * Whether the up mouse button is currently pressed. This is the fourth
784      * mouse button, associated with upward scrolling of the mouse scroll
785      * wheel.
786      * @type Boolean
787      */
788     this.up = up;
789 
790     /**
791      * Whether the down mouse button is currently pressed. This is the fifth 
792      * mouse button, associated with downward scrolling of the mouse scroll
793      * wheel.
794      * @type Boolean
795      */
796     this.down = down;
797 
798     /**
799      * Updates the position represented within this state object by the given
800      * element and clientX/clientY coordinates (commonly available within event
801      * objects). Position is translated from clientX/clientY (relative to
802      * viewport) to element-relative coordinates.
803      * 
804      * @param {Element} element The element the coordinates should be relative
805      *                          to.
806      * @param {Number} clientX The X coordinate to translate, viewport-relative.
807      * @param {Number} clientY The Y coordinate to translate, viewport-relative.
808      */
809     this.fromClientPosition = function(element, clientX, clientY) {
810     
811         guac_state.x = clientX - element.offsetLeft;
812         guac_state.y = clientY - element.offsetTop;
813 
814         // This is all JUST so we can get the mouse position within the element
815         var parent = element.offsetParent;
816         while (parent && !(parent === document.body)) {
817             guac_state.x -= parent.offsetLeft - parent.scrollLeft;
818             guac_state.y -= parent.offsetTop  - parent.scrollTop;
819 
820             parent = parent.offsetParent;
821         }
822 
823         // Element ultimately depends on positioning within document body,
824         // take document scroll into account. 
825         if (parent) {
826             var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft;
827             var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop;
828 
829             guac_state.x -= parent.offsetLeft - documentScrollLeft;
830             guac_state.y -= parent.offsetTop  - documentScrollTop;
831         }
832 
833     };
834 
835 };
836 
837