1 /* 2 * Copyright (C) 2013 Glyptodon LLC 3 * 4 * Permission is hereby granted, free of charge, to any person obtaining a copy 5 * of this software and associated documentation files (the "Software"), to deal 6 * in the Software without restriction, including without limitation the rights 7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 * copies of the Software, and to permit persons to whom the Software is 9 * furnished to do so, subject to the following conditions: 10 * 11 * The above copyright notice and this permission notice shall be included in 12 * all copies or substantial portions of the Software. 13 * 14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 * THE SOFTWARE. 21 */ 22 23 var Guacamole = Guacamole || {}; 24 25 /** 26 * Provides cross-browser mouse events for a given element. The events of 27 * the given element are automatically populated with handlers that translate 28 * mouse events into a non-browser-specific event provided by the 29 * Guacamole.Mouse instance. 30 * 31 * @constructor 32 * @param {Element} element The Element to use to provide mouse events. 33 */ 34 Guacamole.Mouse = function(element) { 35 36 /** 37 * Reference to this Guacamole.Mouse. 38 * @private 39 */ 40 var guac_mouse = this; 41 42 /** 43 * The number of mousemove events to require before re-enabling mouse 44 * event handling after receiving a touch event. 45 */ 46 this.touchMouseThreshold = 3; 47 48 /** 49 * The minimum amount of pixels scrolled required for a single scroll button 50 * click. 51 */ 52 this.scrollThreshold = 120; 53 54 /** 55 * The number of pixels to scroll per line. 56 */ 57 this.PIXELS_PER_LINE = 40; 58 59 /** 60 * The number of pixels to scroll per page. 61 */ 62 this.PIXELS_PER_PAGE = 640; 63 64 /** 65 * The current mouse state. The properties of this state are updated when 66 * mouse events fire. This state object is also passed in as a parameter to 67 * the handler of any mouse events. 68 * 69 * @type Guacamole.Mouse.State 70 */ 71 this.currentState = new Guacamole.Mouse.State( 72 0, 0, 73 false, false, false, false, false 74 ); 75 76 /** 77 * Fired whenever the user presses a mouse button down over the element 78 * associated with this Guacamole.Mouse. 79 * 80 * @event 81 * @param {Guacamole.Mouse.State} state The current mouse state. 82 */ 83 this.onmousedown = null; 84 85 /** 86 * Fired whenever the user releases a mouse button down over the element 87 * associated with this Guacamole.Mouse. 88 * 89 * @event 90 * @param {Guacamole.Mouse.State} state The current mouse state. 91 */ 92 this.onmouseup = null; 93 94 /** 95 * Fired whenever the user moves the mouse over the element associated with 96 * this Guacamole.Mouse. 97 * 98 * @event 99 * @param {Guacamole.Mouse.State} state The current mouse state. 100 */ 101 this.onmousemove = null; 102 103 /** 104 * Counter of mouse events to ignore. This decremented by mousemove, and 105 * while non-zero, mouse events will have no effect. 106 * @private 107 */ 108 var ignore_mouse = 0; 109 110 /** 111 * Cumulative scroll delta amount. This value is accumulated through scroll 112 * events and results in scroll button clicks if it exceeds a certain 113 * threshold. 114 */ 115 var scroll_delta = 0; 116 117 function cancelEvent(e) { 118 e.stopPropagation(); 119 if (e.preventDefault) e.preventDefault(); 120 e.returnValue = false; 121 } 122 123 // Block context menu so right-click gets sent properly 124 element.addEventListener("contextmenu", function(e) { 125 cancelEvent(e); 126 }, false); 127 128 element.addEventListener("mousemove", function(e) { 129 130 cancelEvent(e); 131 132 // If ignoring events, decrement counter 133 if (ignore_mouse) { 134 ignore_mouse--; 135 return; 136 } 137 138 guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY); 139 140 if (guac_mouse.onmousemove) 141 guac_mouse.onmousemove(guac_mouse.currentState); 142 143 }, false); 144 145 element.addEventListener("mousedown", function(e) { 146 147 cancelEvent(e); 148 149 // Do not handle if ignoring events 150 if (ignore_mouse) 151 return; 152 153 switch (e.button) { 154 case 0: 155 guac_mouse.currentState.left = true; 156 break; 157 case 1: 158 guac_mouse.currentState.middle = true; 159 break; 160 case 2: 161 guac_mouse.currentState.right = true; 162 break; 163 } 164 165 if (guac_mouse.onmousedown) 166 guac_mouse.onmousedown(guac_mouse.currentState); 167 168 }, false); 169 170 element.addEventListener("mouseup", function(e) { 171 172 cancelEvent(e); 173 174 // Do not handle if ignoring events 175 if (ignore_mouse) 176 return; 177 178 switch (e.button) { 179 case 0: 180 guac_mouse.currentState.left = false; 181 break; 182 case 1: 183 guac_mouse.currentState.middle = false; 184 break; 185 case 2: 186 guac_mouse.currentState.right = false; 187 break; 188 } 189 190 if (guac_mouse.onmouseup) 191 guac_mouse.onmouseup(guac_mouse.currentState); 192 193 }, false); 194 195 element.addEventListener("mouseout", function(e) { 196 197 // Get parent of the element the mouse pointer is leaving 198 if (!e) e = window.event; 199 200 // Check that mouseout is due to actually LEAVING the element 201 var target = e.relatedTarget || e.toElement; 202 while (target !== null) { 203 if (target === element) 204 return; 205 target = target.parentNode; 206 } 207 208 cancelEvent(e); 209 210 // Release all buttons 211 if (guac_mouse.currentState.left 212 || guac_mouse.currentState.middle 213 || guac_mouse.currentState.right) { 214 215 guac_mouse.currentState.left = false; 216 guac_mouse.currentState.middle = false; 217 guac_mouse.currentState.right = false; 218 219 if (guac_mouse.onmouseup) 220 guac_mouse.onmouseup(guac_mouse.currentState); 221 } 222 223 }, false); 224 225 // Override selection on mouse event element. 226 element.addEventListener("selectstart", function(e) { 227 cancelEvent(e); 228 }, false); 229 230 // Ignore all pending mouse events when touch events are the apparent source 231 function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } 232 233 element.addEventListener("touchmove", ignorePendingMouseEvents, false); 234 element.addEventListener("touchstart", ignorePendingMouseEvents, false); 235 element.addEventListener("touchend", ignorePendingMouseEvents, false); 236 237 // Scroll wheel support 238 function mousewheel_handler(e) { 239 240 // Determine approximate scroll amount (in pixels) 241 var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta; 242 243 // If successfully retrieved scroll amount, convert to pixels if not 244 // already in pixels 245 if (delta) { 246 247 // Convert to pixels if delta was lines 248 if (e.deltaMode === 1) 249 delta = e.deltaY * guac_mouse.PIXELS_PER_LINE; 250 251 // Convert to pixels if delta was pages 252 else if (e.deltaMode === 2) 253 delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE; 254 255 } 256 257 // Otherwise, assume legacy mousewheel event and line scrolling 258 else 259 delta = e.detail * guac_mouse.PIXELS_PER_LINE; 260 261 // Update overall delta 262 scroll_delta += delta; 263 264 // Up 265 while (scroll_delta <= -guac_mouse.scrollThreshold) { 266 267 if (guac_mouse.onmousedown) { 268 guac_mouse.currentState.up = true; 269 guac_mouse.onmousedown(guac_mouse.currentState); 270 } 271 272 if (guac_mouse.onmouseup) { 273 guac_mouse.currentState.up = false; 274 guac_mouse.onmouseup(guac_mouse.currentState); 275 } 276 277 scroll_delta += guac_mouse.scrollThreshold; 278 279 } 280 281 // Down 282 while (scroll_delta >= guac_mouse.scrollThreshold) { 283 284 if (guac_mouse.onmousedown) { 285 guac_mouse.currentState.down = true; 286 guac_mouse.onmousedown(guac_mouse.currentState); 287 } 288 289 if (guac_mouse.onmouseup) { 290 guac_mouse.currentState.down = false; 291 guac_mouse.onmouseup(guac_mouse.currentState); 292 } 293 294 scroll_delta -= guac_mouse.scrollThreshold; 295 296 } 297 298 cancelEvent(e); 299 300 } 301 302 element.addEventListener('DOMMouseScroll', mousewheel_handler, false); 303 element.addEventListener('mousewheel', mousewheel_handler, false); 304 element.addEventListener('wheel', mousewheel_handler, false); 305 306 }; 307 308 /** 309 * Simple container for properties describing the state of a mouse. 310 * 311 * @constructor 312 * @param {Number} x The X position of the mouse pointer in pixels. 313 * @param {Number} y The Y position of the mouse pointer in pixels. 314 * @param {Boolean} left Whether the left mouse button is pressed. 315 * @param {Boolean} middle Whether the middle mouse button is pressed. 316 * @param {Boolean} right Whether the right mouse button is pressed. 317 * @param {Boolean} up Whether the up mouse button is pressed (the fourth 318 * button, usually part of a scroll wheel). 319 * @param {Boolean} down Whether the down mouse button is pressed (the fifth 320 * button, usually part of a scroll wheel). 321 */ 322 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { 323 324 /** 325 * Reference to this Guacamole.Mouse.State. 326 * @private 327 */ 328 var guac_state = this; 329 330 /** 331 * The current X position of the mouse pointer. 332 * @type Number 333 */ 334 this.x = x; 335 336 /** 337 * The current Y position of the mouse pointer. 338 * @type Number 339 */ 340 this.y = y; 341 342 /** 343 * Whether the left mouse button is currently pressed. 344 * @type Boolean 345 */ 346 this.left = left; 347 348 /** 349 * Whether the middle mouse button is currently pressed. 350 * @type Boolean 351 */ 352 this.middle = middle; 353 354 /** 355 * Whether the right mouse button is currently pressed. 356 * @type Boolean 357 */ 358 this.right = right; 359 360 /** 361 * Whether the up mouse button is currently pressed. This is the fourth 362 * mouse button, associated with upward scrolling of the mouse scroll 363 * wheel. 364 * @type Boolean 365 */ 366 this.up = up; 367 368 /** 369 * Whether the down mouse button is currently pressed. This is the fifth 370 * mouse button, associated with downward scrolling of the mouse scroll 371 * wheel. 372 * @type Boolean 373 */ 374 this.down = down; 375 376 /** 377 * Updates the position represented within this state object by the given 378 * element and clientX/clientY coordinates (commonly available within event 379 * objects). Position is translated from clientX/clientY (relative to 380 * viewport) to element-relative coordinates. 381 * 382 * @param {Element} element The element the coordinates should be relative 383 * to. 384 * @param {Number} clientX The X coordinate to translate, viewport-relative. 385 * @param {Number} clientY The Y coordinate to translate, viewport-relative. 386 */ 387 this.fromClientPosition = function(element, clientX, clientY) { 388 389 guac_state.x = clientX - element.offsetLeft; 390 guac_state.y = clientY - element.offsetTop; 391 392 // This is all JUST so we can get the mouse position within the element 393 var parent = element.offsetParent; 394 while (parent && !(parent === document.body)) { 395 guac_state.x -= parent.offsetLeft - parent.scrollLeft; 396 guac_state.y -= parent.offsetTop - parent.scrollTop; 397 398 parent = parent.offsetParent; 399 } 400 401 // Element ultimately depends on positioning within document body, 402 // take document scroll into account. 403 if (parent) { 404 var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; 405 var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; 406 407 guac_state.x -= parent.offsetLeft - documentScrollLeft; 408 guac_state.y -= parent.offsetTop - documentScrollTop; 409 } 410 411 }; 412 413 }; 414 415 /** 416 * Provides cross-browser relative touch event translation for a given element. 417 * 418 * Touch events are translated into mouse events as if the touches occurred 419 * on a touchpad (drag to push the mouse pointer, tap to click). 420 * 421 * @constructor 422 * @param {Element} element The Element to use to provide touch events. 423 */ 424 Guacamole.Mouse.Touchpad = function(element) { 425 426 /** 427 * Reference to this Guacamole.Mouse.Touchpad. 428 * @private 429 */ 430 var guac_touchpad = this; 431 432 /** 433 * The distance a two-finger touch must move per scrollwheel event, in 434 * pixels. 435 */ 436 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 437 438 /** 439 * The maximum number of milliseconds to wait for a touch to end for the 440 * gesture to be considered a click. 441 */ 442 this.clickTimingThreshold = 250; 443 444 /** 445 * The maximum number of pixels to allow a touch to move for the gesture to 446 * be considered a click. 447 */ 448 this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); 449 450 /** 451 * The current mouse state. The properties of this state are updated when 452 * mouse events fire. This state object is also passed in as a parameter to 453 * the handler of any mouse events. 454 * 455 * @type Guacamole.Mouse.State 456 */ 457 this.currentState = new Guacamole.Mouse.State( 458 0, 0, 459 false, false, false, false, false 460 ); 461 462 /** 463 * Fired whenever a mouse button is effectively pressed. This can happen 464 * as part of a "click" gesture initiated by the user by tapping one 465 * or more fingers over the touchpad element, as part of a "scroll" 466 * gesture initiated by dragging two fingers up or down, etc. 467 * 468 * @event 469 * @param {Guacamole.Mouse.State} state The current mouse state. 470 */ 471 this.onmousedown = null; 472 473 /** 474 * Fired whenever a mouse button is effectively released. This can happen 475 * as part of a "click" gesture initiated by the user by tapping one 476 * or more fingers over the touchpad element, as part of a "scroll" 477 * gesture initiated by dragging two fingers up or down, etc. 478 * 479 * @event 480 * @param {Guacamole.Mouse.State} state The current mouse state. 481 */ 482 this.onmouseup = null; 483 484 /** 485 * Fired whenever the user moves the mouse by dragging their finger over 486 * the touchpad element. 487 * 488 * @event 489 * @param {Guacamole.Mouse.State} state The current mouse state. 490 */ 491 this.onmousemove = null; 492 493 var touch_count = 0; 494 var last_touch_x = 0; 495 var last_touch_y = 0; 496 var last_touch_time = 0; 497 var pixels_moved = 0; 498 499 var touch_buttons = { 500 1: "left", 501 2: "right", 502 3: "middle" 503 }; 504 505 var gesture_in_progress = false; 506 var click_release_timeout = null; 507 508 element.addEventListener("touchend", function(e) { 509 510 e.stopPropagation(); 511 e.preventDefault(); 512 513 // If we're handling a gesture AND this is the last touch 514 if (gesture_in_progress && e.touches.length === 0) { 515 516 var time = new Date().getTime(); 517 518 // Get corresponding mouse button 519 var button = touch_buttons[touch_count]; 520 521 // If mouse already down, release anad clear timeout 522 if (guac_touchpad.currentState[button]) { 523 524 // Fire button up event 525 guac_touchpad.currentState[button] = false; 526 if (guac_touchpad.onmouseup) 527 guac_touchpad.onmouseup(guac_touchpad.currentState); 528 529 // Clear timeout, if set 530 if (click_release_timeout) { 531 window.clearTimeout(click_release_timeout); 532 click_release_timeout = null; 533 } 534 535 } 536 537 // If single tap detected (based on time and distance) 538 if (time - last_touch_time <= guac_touchpad.clickTimingThreshold 539 && pixels_moved < guac_touchpad.clickMoveThreshold) { 540 541 // Fire button down event 542 guac_touchpad.currentState[button] = true; 543 if (guac_touchpad.onmousedown) 544 guac_touchpad.onmousedown(guac_touchpad.currentState); 545 546 // Delay mouse up - mouse up should be canceled if 547 // touchstart within timeout. 548 click_release_timeout = window.setTimeout(function() { 549 550 // Fire button up event 551 guac_touchpad.currentState[button] = false; 552 if (guac_touchpad.onmouseup) 553 guac_touchpad.onmouseup(guac_touchpad.currentState); 554 555 // Gesture now over 556 gesture_in_progress = false; 557 558 }, guac_touchpad.clickTimingThreshold); 559 560 } 561 562 // If we're not waiting to see if this is a click, stop gesture 563 if (!click_release_timeout) 564 gesture_in_progress = false; 565 566 } 567 568 }, false); 569 570 element.addEventListener("touchstart", function(e) { 571 572 e.stopPropagation(); 573 e.preventDefault(); 574 575 // Track number of touches, but no more than three 576 touch_count = Math.min(e.touches.length, 3); 577 578 // Clear timeout, if set 579 if (click_release_timeout) { 580 window.clearTimeout(click_release_timeout); 581 click_release_timeout = null; 582 } 583 584 // Record initial touch location and time for touch movement 585 // and tap gestures 586 if (!gesture_in_progress) { 587 588 // Stop mouse events while touching 589 gesture_in_progress = true; 590 591 // Record touch location and time 592 var starting_touch = e.touches[0]; 593 last_touch_x = starting_touch.clientX; 594 last_touch_y = starting_touch.clientY; 595 last_touch_time = new Date().getTime(); 596 pixels_moved = 0; 597 598 } 599 600 }, false); 601 602 element.addEventListener("touchmove", function(e) { 603 604 e.stopPropagation(); 605 e.preventDefault(); 606 607 // Get change in touch location 608 var touch = e.touches[0]; 609 var delta_x = touch.clientX - last_touch_x; 610 var delta_y = touch.clientY - last_touch_y; 611 612 // Track pixels moved 613 pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); 614 615 // If only one touch involved, this is mouse move 616 if (touch_count === 1) { 617 618 // Calculate average velocity in Manhatten pixels per millisecond 619 var velocity = pixels_moved / (new Date().getTime() - last_touch_time); 620 621 // Scale mouse movement relative to velocity 622 var scale = 1 + velocity; 623 624 // Update mouse location 625 guac_touchpad.currentState.x += delta_x*scale; 626 guac_touchpad.currentState.y += delta_y*scale; 627 628 // Prevent mouse from leaving screen 629 630 if (guac_touchpad.currentState.x < 0) 631 guac_touchpad.currentState.x = 0; 632 else if (guac_touchpad.currentState.x >= element.offsetWidth) 633 guac_touchpad.currentState.x = element.offsetWidth - 1; 634 635 if (guac_touchpad.currentState.y < 0) 636 guac_touchpad.currentState.y = 0; 637 else if (guac_touchpad.currentState.y >= element.offsetHeight) 638 guac_touchpad.currentState.y = element.offsetHeight - 1; 639 640 // Fire movement event, if defined 641 if (guac_touchpad.onmousemove) 642 guac_touchpad.onmousemove(guac_touchpad.currentState); 643 644 // Update touch location 645 last_touch_x = touch.clientX; 646 last_touch_y = touch.clientY; 647 648 } 649 650 // Interpret two-finger swipe as scrollwheel 651 else if (touch_count === 2) { 652 653 // If change in location passes threshold for scroll 654 if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { 655 656 // Decide button based on Y movement direction 657 var button; 658 if (delta_y > 0) button = "down"; 659 else button = "up"; 660 661 // Fire button down event 662 guac_touchpad.currentState[button] = true; 663 if (guac_touchpad.onmousedown) 664 guac_touchpad.onmousedown(guac_touchpad.currentState); 665 666 // Fire button up event 667 guac_touchpad.currentState[button] = false; 668 if (guac_touchpad.onmouseup) 669 guac_touchpad.onmouseup(guac_touchpad.currentState); 670 671 // Only update touch location after a scroll has been 672 // detected 673 last_touch_x = touch.clientX; 674 last_touch_y = touch.clientY; 675 676 } 677 678 } 679 680 }, false); 681 682 }; 683 684 /** 685 * Provides cross-browser absolute touch event translation for a given element. 686 * 687 * Touch events are translated into mouse events as if the touches occurred 688 * on a touchscreen (tapping anywhere on the screen clicks at that point, 689 * long-press to right-click). 690 * 691 * @constructor 692 * @param {Element} element The Element to use to provide touch events. 693 */ 694 Guacamole.Mouse.Touchscreen = function(element) { 695 696 /** 697 * Reference to this Guacamole.Mouse.Touchscreen. 698 * @private 699 */ 700 var guac_touchscreen = this; 701 702 /** 703 * Whether a gesture is known to be in progress. If false, touch events 704 * will be ignored. 705 */ 706 var gesture_in_progress = false; 707 708 /** 709 * The start X location of a gesture. 710 * @private 711 */ 712 var gesture_start_x = null; 713 714 /** 715 * The start Y location of a gesture. 716 * @private 717 */ 718 var gesture_start_y = null; 719 720 /** 721 * The timeout associated with the delayed, cancellable click release. 722 */ 723 var click_release_timeout = null; 724 725 /** 726 * The timeout associated with long-press for right click. 727 */ 728 var long_press_timeout = null; 729 730 /** 731 * The distance a two-finger touch must move per scrollwheel event, in 732 * pixels. 733 */ 734 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 735 736 /** 737 * The maximum number of milliseconds to wait for a touch to end for the 738 * gesture to be considered a click. 739 */ 740 this.clickTimingThreshold = 250; 741 742 /** 743 * The maximum number of pixels to allow a touch to move for the gesture to 744 * be considered a click. 745 */ 746 this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); 747 748 /** 749 * The amount of time a press must be held for long press to be 750 * detected. 751 */ 752 this.longPressThreshold = 500; 753 754 /** 755 * The current mouse state. The properties of this state are updated when 756 * mouse events fire. This state object is also passed in as a parameter to 757 * the handler of any mouse events. 758 * 759 * @type Guacamole.Mouse.State 760 */ 761 this.currentState = new Guacamole.Mouse.State( 762 0, 0, 763 false, false, false, false, false 764 ); 765 766 /** 767 * Fired whenever a mouse button is effectively pressed. This can happen 768 * as part of a "mousedown" gesture initiated by the user by pressing one 769 * finger over the touchscreen element, as part of a "scroll" gesture 770 * initiated by dragging two fingers up or down, etc. 771 * 772 * @event 773 * @param {Guacamole.Mouse.State} state The current mouse state. 774 */ 775 this.onmousedown = null; 776 777 /** 778 * Fired whenever a mouse button is effectively released. This can happen 779 * as part of a "mouseup" gesture initiated by the user by removing the 780 * finger pressed against the touchscreen element, or as part of a "scroll" 781 * gesture initiated by dragging two fingers up or down, etc. 782 * 783 * @event 784 * @param {Guacamole.Mouse.State} state The current mouse state. 785 */ 786 this.onmouseup = null; 787 788 /** 789 * Fired whenever the user moves the mouse by dragging their finger over 790 * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, 791 * dragging a finger over the touchscreen element will always cause 792 * the mouse button to be effectively down, as if clicking-and-dragging. 793 * 794 * @event 795 * @param {Guacamole.Mouse.State} state The current mouse state. 796 */ 797 this.onmousemove = null; 798 799 /** 800 * Presses the given mouse button, if it isn't already pressed. Valid 801 * button values are "left", "middle", "right", "up", and "down". 802 * 803 * @private 804 * @param {String} button The mouse button to press. 805 */ 806 function press_button(button) { 807 if (!guac_touchscreen.currentState[button]) { 808 guac_touchscreen.currentState[button] = true; 809 if (guac_touchscreen.onmousedown) 810 guac_touchscreen.onmousedown(guac_touchscreen.currentState); 811 } 812 } 813 814 /** 815 * Releases the given mouse button, if it isn't already released. Valid 816 * button values are "left", "middle", "right", "up", and "down". 817 * 818 * @private 819 * @param {String} button The mouse button to release. 820 */ 821 function release_button(button) { 822 if (guac_touchscreen.currentState[button]) { 823 guac_touchscreen.currentState[button] = false; 824 if (guac_touchscreen.onmouseup) 825 guac_touchscreen.onmouseup(guac_touchscreen.currentState); 826 } 827 } 828 829 /** 830 * Clicks (presses and releases) the given mouse button. Valid button 831 * values are "left", "middle", "right", "up", and "down". 832 * 833 * @private 834 * @param {String} button The mouse button to click. 835 */ 836 function click_button(button) { 837 press_button(button); 838 release_button(button); 839 } 840 841 /** 842 * Moves the mouse to the given coordinates. These coordinates must be 843 * relative to the browser window, as they will be translated based on 844 * the touch event target's location within the browser window. 845 * 846 * @private 847 * @param {Number} x The X coordinate of the mouse pointer. 848 * @param {Number} y The Y coordinate of the mouse pointer. 849 */ 850 function move_mouse(x, y) { 851 guac_touchscreen.currentState.fromClientPosition(element, x, y); 852 if (guac_touchscreen.onmousemove) 853 guac_touchscreen.onmousemove(guac_touchscreen.currentState); 854 } 855 856 /** 857 * Returns whether the given touch event exceeds the movement threshold for 858 * clicking, based on where the touch gesture began. 859 * 860 * @private 861 * @param {TouchEvent} e The touch event to check. 862 * @return {Boolean} true if the movement threshold is exceeded, false 863 * otherwise. 864 */ 865 function finger_moved(e) { 866 var touch = e.touches[0] || e.changedTouches[0]; 867 var delta_x = touch.clientX - gesture_start_x; 868 var delta_y = touch.clientY - gesture_start_y; 869 return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; 870 } 871 872 /** 873 * Begins a new gesture at the location of the first touch in the given 874 * touch event. 875 * 876 * @private 877 * @param {TouchEvent} e The touch event beginning this new gesture. 878 */ 879 function begin_gesture(e) { 880 var touch = e.touches[0]; 881 gesture_in_progress = true; 882 gesture_start_x = touch.clientX; 883 gesture_start_y = touch.clientY; 884 } 885 886 /** 887 * End the current gesture entirely. Wait for all touches to be done before 888 * resuming gesture detection. 889 * 890 * @private 891 */ 892 function end_gesture() { 893 window.clearTimeout(click_release_timeout); 894 window.clearTimeout(long_press_timeout); 895 gesture_in_progress = false; 896 } 897 898 element.addEventListener("touchend", function(e) { 899 900 // Do not handle if no gesture 901 if (!gesture_in_progress) 902 return; 903 904 // Ignore if more than one touch 905 if (e.touches.length !== 0 || e.changedTouches.length !== 1) { 906 end_gesture(); 907 return; 908 } 909 910 // Long-press, if any, is over 911 window.clearTimeout(long_press_timeout); 912 913 // Always release mouse button if pressed 914 release_button("left"); 915 916 // If finger hasn't moved enough to cancel the click 917 if (!finger_moved(e)) { 918 919 e.preventDefault(); 920 921 // If not yet pressed, press and start delay release 922 if (!guac_touchscreen.currentState.left) { 923 924 var touch = e.changedTouches[0]; 925 move_mouse(touch.clientX, touch.clientY); 926 press_button("left"); 927 928 // Release button after a delay, if not canceled 929 click_release_timeout = window.setTimeout(function() { 930 release_button("left"); 931 end_gesture(); 932 }, guac_touchscreen.clickTimingThreshold); 933 934 } 935 936 } // end if finger not moved 937 938 }, false); 939 940 element.addEventListener("touchstart", function(e) { 941 942 // Ignore if more than one touch 943 if (e.touches.length !== 1) { 944 end_gesture(); 945 return; 946 } 947 948 e.preventDefault(); 949 950 // New touch begins a new gesture 951 begin_gesture(e); 952 953 // Keep button pressed if tap after left click 954 window.clearTimeout(click_release_timeout); 955 956 // Click right button if this turns into a long-press 957 long_press_timeout = window.setTimeout(function() { 958 var touch = e.touches[0]; 959 move_mouse(touch.clientX, touch.clientY); 960 click_button("right"); 961 end_gesture(); 962 }, guac_touchscreen.longPressThreshold); 963 964 }, false); 965 966 element.addEventListener("touchmove", function(e) { 967 968 // Do not handle if no gesture 969 if (!gesture_in_progress) 970 return; 971 972 // Cancel long press if finger moved 973 if (finger_moved(e)) 974 window.clearTimeout(long_press_timeout); 975 976 // Ignore if more than one touch 977 if (e.touches.length !== 1) { 978 end_gesture(); 979 return; 980 } 981 982 // Update mouse position if dragging 983 if (guac_touchscreen.currentState.left) { 984 985 e.preventDefault(); 986 e.stopPropagation(); 987 988 // Update state 989 var touch = e.touches[0]; 990 move_mouse(touch.clientX, touch.clientY); 991 992 } 993 994 }, false); 995 996 }; 997