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.preventDefault(); 511 512 // If we're handling a gesture AND this is the last touch 513 if (gesture_in_progress && e.touches.length === 0) { 514 515 var time = new Date().getTime(); 516 517 // Get corresponding mouse button 518 var button = touch_buttons[touch_count]; 519 520 // If mouse already down, release anad clear timeout 521 if (guac_touchpad.currentState[button]) { 522 523 // Fire button up event 524 guac_touchpad.currentState[button] = false; 525 if (guac_touchpad.onmouseup) 526 guac_touchpad.onmouseup(guac_touchpad.currentState); 527 528 // Clear timeout, if set 529 if (click_release_timeout) { 530 window.clearTimeout(click_release_timeout); 531 click_release_timeout = null; 532 } 533 534 } 535 536 // If single tap detected (based on time and distance) 537 if (time - last_touch_time <= guac_touchpad.clickTimingThreshold 538 && pixels_moved < guac_touchpad.clickMoveThreshold) { 539 540 // Fire button down event 541 guac_touchpad.currentState[button] = true; 542 if (guac_touchpad.onmousedown) 543 guac_touchpad.onmousedown(guac_touchpad.currentState); 544 545 // Delay mouse up - mouse up should be canceled if 546 // touchstart within timeout. 547 click_release_timeout = window.setTimeout(function() { 548 549 // Fire button up event 550 guac_touchpad.currentState[button] = false; 551 if (guac_touchpad.onmouseup) 552 guac_touchpad.onmouseup(guac_touchpad.currentState); 553 554 // Gesture now over 555 gesture_in_progress = false; 556 557 }, guac_touchpad.clickTimingThreshold); 558 559 } 560 561 // If we're not waiting to see if this is a click, stop gesture 562 if (!click_release_timeout) 563 gesture_in_progress = false; 564 565 } 566 567 }, false); 568 569 element.addEventListener("touchstart", function(e) { 570 571 e.preventDefault(); 572 573 // Track number of touches, but no more than three 574 touch_count = Math.min(e.touches.length, 3); 575 576 // Clear timeout, if set 577 if (click_release_timeout) { 578 window.clearTimeout(click_release_timeout); 579 click_release_timeout = null; 580 } 581 582 // Record initial touch location and time for touch movement 583 // and tap gestures 584 if (!gesture_in_progress) { 585 586 // Stop mouse events while touching 587 gesture_in_progress = true; 588 589 // Record touch location and time 590 var starting_touch = e.touches[0]; 591 last_touch_x = starting_touch.clientX; 592 last_touch_y = starting_touch.clientY; 593 last_touch_time = new Date().getTime(); 594 pixels_moved = 0; 595 596 } 597 598 }, false); 599 600 element.addEventListener("touchmove", function(e) { 601 602 e.preventDefault(); 603 604 // Get change in touch location 605 var touch = e.touches[0]; 606 var delta_x = touch.clientX - last_touch_x; 607 var delta_y = touch.clientY - last_touch_y; 608 609 // Track pixels moved 610 pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); 611 612 // If only one touch involved, this is mouse move 613 if (touch_count === 1) { 614 615 // Calculate average velocity in Manhatten pixels per millisecond 616 var velocity = pixels_moved / (new Date().getTime() - last_touch_time); 617 618 // Scale mouse movement relative to velocity 619 var scale = 1 + velocity; 620 621 // Update mouse location 622 guac_touchpad.currentState.x += delta_x*scale; 623 guac_touchpad.currentState.y += delta_y*scale; 624 625 // Prevent mouse from leaving screen 626 627 if (guac_touchpad.currentState.x < 0) 628 guac_touchpad.currentState.x = 0; 629 else if (guac_touchpad.currentState.x >= element.offsetWidth) 630 guac_touchpad.currentState.x = element.offsetWidth - 1; 631 632 if (guac_touchpad.currentState.y < 0) 633 guac_touchpad.currentState.y = 0; 634 else if (guac_touchpad.currentState.y >= element.offsetHeight) 635 guac_touchpad.currentState.y = element.offsetHeight - 1; 636 637 // Fire movement event, if defined 638 if (guac_touchpad.onmousemove) 639 guac_touchpad.onmousemove(guac_touchpad.currentState); 640 641 // Update touch location 642 last_touch_x = touch.clientX; 643 last_touch_y = touch.clientY; 644 645 } 646 647 // Interpret two-finger swipe as scrollwheel 648 else if (touch_count === 2) { 649 650 // If change in location passes threshold for scroll 651 if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { 652 653 // Decide button based on Y movement direction 654 var button; 655 if (delta_y > 0) button = "down"; 656 else button = "up"; 657 658 // Fire button down event 659 guac_touchpad.currentState[button] = true; 660 if (guac_touchpad.onmousedown) 661 guac_touchpad.onmousedown(guac_touchpad.currentState); 662 663 // Fire button up event 664 guac_touchpad.currentState[button] = false; 665 if (guac_touchpad.onmouseup) 666 guac_touchpad.onmouseup(guac_touchpad.currentState); 667 668 // Only update touch location after a scroll has been 669 // detected 670 last_touch_x = touch.clientX; 671 last_touch_y = touch.clientY; 672 673 } 674 675 } 676 677 }, false); 678 679 }; 680 681 /** 682 * Provides cross-browser absolute touch event translation for a given element. 683 * 684 * Touch events are translated into mouse events as if the touches occurred 685 * on a touchscreen (tapping anywhere on the screen clicks at that point, 686 * long-press to right-click). 687 * 688 * @constructor 689 * @param {Element} element The Element to use to provide touch events. 690 */ 691 Guacamole.Mouse.Touchscreen = function(element) { 692 693 /** 694 * Reference to this Guacamole.Mouse.Touchscreen. 695 * @private 696 */ 697 var guac_touchscreen = this; 698 699 /** 700 * Whether a gesture is known to be in progress. If false, touch events 701 * will be ignored. 702 */ 703 var gesture_in_progress = false; 704 705 /** 706 * The start X location of a gesture. 707 * @private 708 */ 709 var gesture_start_x = null; 710 711 /** 712 * The start Y location of a gesture. 713 * @private 714 */ 715 var gesture_start_y = null; 716 717 /** 718 * The timeout associated with the delayed, cancellable click release. 719 */ 720 var click_release_timeout = null; 721 722 /** 723 * The timeout associated with long-press for right click. 724 */ 725 var long_press_timeout = null; 726 727 /** 728 * The distance a two-finger touch must move per scrollwheel event, in 729 * pixels. 730 */ 731 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 732 733 /** 734 * The maximum number of milliseconds to wait for a touch to end for the 735 * gesture to be considered a click. 736 */ 737 this.clickTimingThreshold = 250; 738 739 /** 740 * The maximum number of pixels to allow a touch to move for the gesture to 741 * be considered a click. 742 */ 743 this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); 744 745 /** 746 * The amount of time a press must be held for long press to be 747 * detected. 748 */ 749 this.longPressThreshold = 500; 750 751 /** 752 * The current mouse state. The properties of this state are updated when 753 * mouse events fire. This state object is also passed in as a parameter to 754 * the handler of any mouse events. 755 * 756 * @type Guacamole.Mouse.State 757 */ 758 this.currentState = new Guacamole.Mouse.State( 759 0, 0, 760 false, false, false, false, false 761 ); 762 763 /** 764 * Fired whenever a mouse button is effectively pressed. This can happen 765 * as part of a "mousedown" gesture initiated by the user by pressing one 766 * finger over the touchscreen element, as part of a "scroll" gesture 767 * initiated by dragging two fingers up or down, etc. 768 * 769 * @event 770 * @param {Guacamole.Mouse.State} state The current mouse state. 771 */ 772 this.onmousedown = null; 773 774 /** 775 * Fired whenever a mouse button is effectively released. This can happen 776 * as part of a "mouseup" gesture initiated by the user by removing the 777 * finger pressed against the touchscreen element, or as part of a "scroll" 778 * gesture initiated by dragging two fingers up or down, etc. 779 * 780 * @event 781 * @param {Guacamole.Mouse.State} state The current mouse state. 782 */ 783 this.onmouseup = null; 784 785 /** 786 * Fired whenever the user moves the mouse by dragging their finger over 787 * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, 788 * dragging a finger over the touchscreen element will always cause 789 * the mouse button to be effectively down, as if clicking-and-dragging. 790 * 791 * @event 792 * @param {Guacamole.Mouse.State} state The current mouse state. 793 */ 794 this.onmousemove = null; 795 796 /** 797 * Presses the given mouse button, if it isn't already pressed. Valid 798 * button values are "left", "middle", "right", "up", and "down". 799 * 800 * @private 801 * @param {String} button The mouse button to press. 802 */ 803 function press_button(button) { 804 if (!guac_touchscreen.currentState[button]) { 805 guac_touchscreen.currentState[button] = true; 806 if (guac_touchscreen.onmousedown) 807 guac_touchscreen.onmousedown(guac_touchscreen.currentState); 808 } 809 } 810 811 /** 812 * Releases the given mouse button, if it isn't already released. Valid 813 * button values are "left", "middle", "right", "up", and "down". 814 * 815 * @private 816 * @param {String} button The mouse button to release. 817 */ 818 function release_button(button) { 819 if (guac_touchscreen.currentState[button]) { 820 guac_touchscreen.currentState[button] = false; 821 if (guac_touchscreen.onmouseup) 822 guac_touchscreen.onmouseup(guac_touchscreen.currentState); 823 } 824 } 825 826 /** 827 * Clicks (presses and releases) the given mouse button. Valid button 828 * values are "left", "middle", "right", "up", and "down". 829 * 830 * @private 831 * @param {String} button The mouse button to click. 832 */ 833 function click_button(button) { 834 press_button(button); 835 release_button(button); 836 } 837 838 /** 839 * Moves the mouse to the given coordinates. These coordinates must be 840 * relative to the browser window, as they will be translated based on 841 * the touch event target's location within the browser window. 842 * 843 * @private 844 * @param {Number} x The X coordinate of the mouse pointer. 845 * @param {Number} y The Y coordinate of the mouse pointer. 846 */ 847 function move_mouse(x, y) { 848 guac_touchscreen.currentState.fromClientPosition(element, x, y); 849 if (guac_touchscreen.onmousemove) 850 guac_touchscreen.onmousemove(guac_touchscreen.currentState); 851 } 852 853 /** 854 * Returns whether the given touch event exceeds the movement threshold for 855 * clicking, based on where the touch gesture began. 856 * 857 * @private 858 * @param {TouchEvent} e The touch event to check. 859 * @return {Boolean} true if the movement threshold is exceeded, false 860 * otherwise. 861 */ 862 function finger_moved(e) { 863 var touch = e.touches[0] || e.changedTouches[0]; 864 var delta_x = touch.clientX - gesture_start_x; 865 var delta_y = touch.clientY - gesture_start_y; 866 return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; 867 } 868 869 /** 870 * Begins a new gesture at the location of the first touch in the given 871 * touch event. 872 * 873 * @private 874 * @param {TouchEvent} e The touch event beginning this new gesture. 875 */ 876 function begin_gesture(e) { 877 var touch = e.touches[0]; 878 gesture_in_progress = true; 879 gesture_start_x = touch.clientX; 880 gesture_start_y = touch.clientY; 881 } 882 883 /** 884 * End the current gesture entirely. Wait for all touches to be done before 885 * resuming gesture detection. 886 * 887 * @private 888 */ 889 function end_gesture() { 890 window.clearTimeout(click_release_timeout); 891 window.clearTimeout(long_press_timeout); 892 gesture_in_progress = false; 893 } 894 895 element.addEventListener("touchend", function(e) { 896 897 // Do not handle if no gesture 898 if (!gesture_in_progress) 899 return; 900 901 // Ignore if more than one touch 902 if (e.touches.length !== 0 || e.changedTouches.length !== 1) { 903 end_gesture(); 904 return; 905 } 906 907 // Long-press, if any, is over 908 window.clearTimeout(long_press_timeout); 909 910 // Always release mouse button if pressed 911 release_button("left"); 912 913 // If finger hasn't moved enough to cancel the click 914 if (!finger_moved(e)) { 915 916 e.preventDefault(); 917 918 // If not yet pressed, press and start delay release 919 if (!guac_touchscreen.currentState.left) { 920 921 var touch = e.changedTouches[0]; 922 move_mouse(touch.clientX, touch.clientY); 923 press_button("left"); 924 925 // Release button after a delay, if not canceled 926 click_release_timeout = window.setTimeout(function() { 927 release_button("left"); 928 end_gesture(); 929 }, guac_touchscreen.clickTimingThreshold); 930 931 } 932 933 } // end if finger not moved 934 935 }, false); 936 937 element.addEventListener("touchstart", function(e) { 938 939 // Ignore if more than one touch 940 if (e.touches.length !== 1) { 941 end_gesture(); 942 return; 943 } 944 945 e.preventDefault(); 946 947 // New touch begins a new gesture 948 begin_gesture(e); 949 950 // Keep button pressed if tap after left click 951 window.clearTimeout(click_release_timeout); 952 953 // Click right button if this turns into a long-press 954 long_press_timeout = window.setTimeout(function() { 955 var touch = e.touches[0]; 956 move_mouse(touch.clientX, touch.clientY); 957 click_button("right"); 958 end_gesture(); 959 }, guac_touchscreen.longPressThreshold); 960 961 }, false); 962 963 element.addEventListener("touchmove", function(e) { 964 965 // Do not handle if no gesture 966 if (!gesture_in_progress) 967 return; 968 969 // Cancel long press if finger moved 970 if (finger_moved(e)) 971 window.clearTimeout(long_press_timeout); 972 973 // Ignore if more than one touch 974 if (e.touches.length !== 1) { 975 end_gesture(); 976 return; 977 } 978 979 // Update mouse position if dragging 980 if (guac_touchscreen.currentState.left) { 981 982 e.preventDefault(); 983 984 // Update state 985 var touch = e.touches[0]; 986 move_mouse(touch.clientX, touch.clientY); 987 988 } 989 990 }, false); 991 992 }; 993