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