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 = 53; 53 54 /** 55 * The number of pixels to scroll per line. 56 */ 57 this.PIXELS_PER_LINE = 18; 58 59 /** 60 * The number of pixels to scroll per page. 61 */ 62 this.PIXELS_PER_PAGE = this.PIXELS_PER_LINE * 16; 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 * Fired whenever the mouse leaves the boundaries of the element associated 105 * with this Guacamole.Mouse. 106 * 107 * @event 108 */ 109 this.onmouseout = null; 110 111 /** 112 * Counter of mouse events to ignore. This decremented by mousemove, and 113 * while non-zero, mouse events will have no effect. 114 * @private 115 */ 116 var ignore_mouse = 0; 117 118 /** 119 * Cumulative scroll delta amount. This value is accumulated through scroll 120 * events and results in scroll button clicks if it exceeds a certain 121 * threshold. 122 */ 123 var scroll_delta = 0; 124 125 function cancelEvent(e) { 126 e.stopPropagation(); 127 if (e.preventDefault) e.preventDefault(); 128 e.returnValue = false; 129 } 130 131 // Block context menu so right-click gets sent properly 132 element.addEventListener("contextmenu", function(e) { 133 cancelEvent(e); 134 }, false); 135 136 element.addEventListener("mousemove", function(e) { 137 138 cancelEvent(e); 139 140 // If ignoring events, decrement counter 141 if (ignore_mouse) { 142 ignore_mouse--; 143 return; 144 } 145 146 guac_mouse.currentState.fromClientPosition(element, e.clientX, e.clientY); 147 148 if (guac_mouse.onmousemove) 149 guac_mouse.onmousemove(guac_mouse.currentState); 150 151 }, false); 152 153 element.addEventListener("mousedown", function(e) { 154 155 cancelEvent(e); 156 157 // Do not handle if ignoring events 158 if (ignore_mouse) 159 return; 160 161 switch (e.button) { 162 case 0: 163 guac_mouse.currentState.left = true; 164 break; 165 case 1: 166 guac_mouse.currentState.middle = true; 167 break; 168 case 2: 169 guac_mouse.currentState.right = true; 170 break; 171 } 172 173 if (guac_mouse.onmousedown) 174 guac_mouse.onmousedown(guac_mouse.currentState); 175 176 }, false); 177 178 element.addEventListener("mouseup", function(e) { 179 180 cancelEvent(e); 181 182 // Do not handle if ignoring events 183 if (ignore_mouse) 184 return; 185 186 switch (e.button) { 187 case 0: 188 guac_mouse.currentState.left = false; 189 break; 190 case 1: 191 guac_mouse.currentState.middle = false; 192 break; 193 case 2: 194 guac_mouse.currentState.right = false; 195 break; 196 } 197 198 if (guac_mouse.onmouseup) 199 guac_mouse.onmouseup(guac_mouse.currentState); 200 201 }, false); 202 203 element.addEventListener("mouseout", function(e) { 204 205 // Get parent of the element the mouse pointer is leaving 206 if (!e) e = window.event; 207 208 // Check that mouseout is due to actually LEAVING the element 209 var target = e.relatedTarget || e.toElement; 210 while (target) { 211 if (target === element) 212 return; 213 target = target.parentNode; 214 } 215 216 cancelEvent(e); 217 218 // Release all buttons 219 if (guac_mouse.currentState.left 220 || guac_mouse.currentState.middle 221 || guac_mouse.currentState.right) { 222 223 guac_mouse.currentState.left = false; 224 guac_mouse.currentState.middle = false; 225 guac_mouse.currentState.right = false; 226 227 if (guac_mouse.onmouseup) 228 guac_mouse.onmouseup(guac_mouse.currentState); 229 } 230 231 // Fire onmouseout event 232 if (guac_mouse.onmouseout) 233 guac_mouse.onmouseout(); 234 235 }, false); 236 237 // Override selection on mouse event element. 238 element.addEventListener("selectstart", function(e) { 239 cancelEvent(e); 240 }, false); 241 242 // Ignore all pending mouse events when touch events are the apparent source 243 function ignorePendingMouseEvents() { ignore_mouse = guac_mouse.touchMouseThreshold; } 244 245 element.addEventListener("touchmove", ignorePendingMouseEvents, false); 246 element.addEventListener("touchstart", ignorePendingMouseEvents, false); 247 element.addEventListener("touchend", ignorePendingMouseEvents, false); 248 249 // Scroll wheel support 250 function mousewheel_handler(e) { 251 252 // Determine approximate scroll amount (in pixels) 253 var delta = e.deltaY || -e.wheelDeltaY || -e.wheelDelta; 254 255 // If successfully retrieved scroll amount, convert to pixels if not 256 // already in pixels 257 if (delta) { 258 259 // Convert to pixels if delta was lines 260 if (e.deltaMode === 1) 261 delta = e.deltaY * guac_mouse.PIXELS_PER_LINE; 262 263 // Convert to pixels if delta was pages 264 else if (e.deltaMode === 2) 265 delta = e.deltaY * guac_mouse.PIXELS_PER_PAGE; 266 267 } 268 269 // Otherwise, assume legacy mousewheel event and line scrolling 270 else 271 delta = e.detail * guac_mouse.PIXELS_PER_LINE; 272 273 // Update overall delta 274 scroll_delta += delta; 275 276 // Up 277 if (scroll_delta <= -guac_mouse.scrollThreshold) { 278 279 // Repeatedly click the up button until insufficient delta remains 280 do { 281 282 if (guac_mouse.onmousedown) { 283 guac_mouse.currentState.up = true; 284 guac_mouse.onmousedown(guac_mouse.currentState); 285 } 286 287 if (guac_mouse.onmouseup) { 288 guac_mouse.currentState.up = false; 289 guac_mouse.onmouseup(guac_mouse.currentState); 290 } 291 292 scroll_delta += guac_mouse.scrollThreshold; 293 294 } while (scroll_delta <= -guac_mouse.scrollThreshold); 295 296 // Reset delta 297 scroll_delta = 0; 298 299 } 300 301 // Down 302 if (scroll_delta >= guac_mouse.scrollThreshold) { 303 304 // Repeatedly click the down button until insufficient delta remains 305 do { 306 307 if (guac_mouse.onmousedown) { 308 guac_mouse.currentState.down = true; 309 guac_mouse.onmousedown(guac_mouse.currentState); 310 } 311 312 if (guac_mouse.onmouseup) { 313 guac_mouse.currentState.down = false; 314 guac_mouse.onmouseup(guac_mouse.currentState); 315 } 316 317 scroll_delta -= guac_mouse.scrollThreshold; 318 319 } while (scroll_delta >= guac_mouse.scrollThreshold); 320 321 // Reset delta 322 scroll_delta = 0; 323 324 } 325 326 cancelEvent(e); 327 328 } 329 330 element.addEventListener('DOMMouseScroll', mousewheel_handler, false); 331 element.addEventListener('mousewheel', mousewheel_handler, false); 332 element.addEventListener('wheel', mousewheel_handler, false); 333 334 /** 335 * Whether the browser supports CSS3 cursor styling, including hotspot 336 * coordinates. 337 * 338 * @private 339 * @type Boolean 340 */ 341 var CSS3_CURSOR_SUPPORTED = (function() { 342 343 var div = document.createElement("div"); 344 345 // If no cursor property at all, then no support 346 if (!("cursor" in div.style)) 347 return false; 348 349 try { 350 // Apply simple 1x1 PNG 351 div.style.cursor = "url(data:image/png;base64," 352 + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB" 353 + "AQMAAAAl21bKAAAAA1BMVEX///+nxBvI" 354 + "AAAACklEQVQI12NgAAAAAgAB4iG8MwAA" 355 + "AABJRU5ErkJggg==) 0 0, auto"; 356 } 357 catch (e) { 358 return false; 359 } 360 361 // Verify cursor property is set to URL with hotspot 362 return /\burl\([^()]*\)\s+0\s+0\b/.test(div.style.cursor || ""); 363 364 })(); 365 366 /** 367 * Changes the local mouse cursor to the given canvas, having the given 368 * hotspot coordinates. This affects styling of the element backing this 369 * Guacamole.Mouse only, and may fail depending on browser support for 370 * setting the mouse cursor. 371 * 372 * If setting the local cursor is desired, it is up to the implementation 373 * to do something else, such as use the software cursor built into 374 * Guacamole.Display, if the local cursor cannot be set. 375 * 376 * @param {HTMLCanvasElement} canvas The cursor image. 377 * @param {Number} x The X-coordinate of the cursor hotspot. 378 * @param {Number} y The Y-coordinate of the cursor hotspot. 379 * @return {Boolean} true if the cursor was successfully set, false if the 380 * cursor could not be set for any reason. 381 */ 382 this.setCursor = function(canvas, x, y) { 383 384 // Attempt to set via CSS3 cursor styling 385 if (CSS3_CURSOR_SUPPORTED) { 386 var dataURL = canvas.toDataURL('image/png'); 387 element.style.cursor = "url(" + dataURL + ") " + x + " " + y + ", auto"; 388 return true; 389 } 390 391 // Otherwise, setting cursor failed 392 return false; 393 394 }; 395 396 }; 397 398 /** 399 * Simple container for properties describing the state of a mouse. 400 * 401 * @constructor 402 * @param {Number} x The X position of the mouse pointer in pixels. 403 * @param {Number} y The Y position of the mouse pointer in pixels. 404 * @param {Boolean} left Whether the left mouse button is pressed. 405 * @param {Boolean} middle Whether the middle mouse button is pressed. 406 * @param {Boolean} right Whether the right mouse button is pressed. 407 * @param {Boolean} up Whether the up mouse button is pressed (the fourth 408 * button, usually part of a scroll wheel). 409 * @param {Boolean} down Whether the down mouse button is pressed (the fifth 410 * button, usually part of a scroll wheel). 411 */ 412 Guacamole.Mouse.State = function(x, y, left, middle, right, up, down) { 413 414 /** 415 * Reference to this Guacamole.Mouse.State. 416 * @private 417 */ 418 var guac_state = this; 419 420 /** 421 * The current X position of the mouse pointer. 422 * @type Number 423 */ 424 this.x = x; 425 426 /** 427 * The current Y position of the mouse pointer. 428 * @type Number 429 */ 430 this.y = y; 431 432 /** 433 * Whether the left mouse button is currently pressed. 434 * @type Boolean 435 */ 436 this.left = left; 437 438 /** 439 * Whether the middle mouse button is currently pressed. 440 * @type Boolean 441 */ 442 this.middle = middle; 443 444 /** 445 * Whether the right mouse button is currently pressed. 446 * @type Boolean 447 */ 448 this.right = right; 449 450 /** 451 * Whether the up mouse button is currently pressed. This is the fourth 452 * mouse button, associated with upward scrolling of the mouse scroll 453 * wheel. 454 * @type Boolean 455 */ 456 this.up = up; 457 458 /** 459 * Whether the down mouse button is currently pressed. This is the fifth 460 * mouse button, associated with downward scrolling of the mouse scroll 461 * wheel. 462 * @type Boolean 463 */ 464 this.down = down; 465 466 /** 467 * Updates the position represented within this state object by the given 468 * element and clientX/clientY coordinates (commonly available within event 469 * objects). Position is translated from clientX/clientY (relative to 470 * viewport) to element-relative coordinates. 471 * 472 * @param {Element} element The element the coordinates should be relative 473 * to. 474 * @param {Number} clientX The X coordinate to translate, viewport-relative. 475 * @param {Number} clientY The Y coordinate to translate, viewport-relative. 476 */ 477 this.fromClientPosition = function(element, clientX, clientY) { 478 479 guac_state.x = clientX - element.offsetLeft; 480 guac_state.y = clientY - element.offsetTop; 481 482 // This is all JUST so we can get the mouse position within the element 483 var parent = element.offsetParent; 484 while (parent && !(parent === document.body)) { 485 guac_state.x -= parent.offsetLeft - parent.scrollLeft; 486 guac_state.y -= parent.offsetTop - parent.scrollTop; 487 488 parent = parent.offsetParent; 489 } 490 491 // Element ultimately depends on positioning within document body, 492 // take document scroll into account. 493 if (parent) { 494 var documentScrollLeft = document.body.scrollLeft || document.documentElement.scrollLeft; 495 var documentScrollTop = document.body.scrollTop || document.documentElement.scrollTop; 496 497 guac_state.x -= parent.offsetLeft - documentScrollLeft; 498 guac_state.y -= parent.offsetTop - documentScrollTop; 499 } 500 501 }; 502 503 }; 504 505 /** 506 * Provides cross-browser relative touch event translation for a given element. 507 * 508 * Touch events are translated into mouse events as if the touches occurred 509 * on a touchpad (drag to push the mouse pointer, tap to click). 510 * 511 * @constructor 512 * @param {Element} element The Element to use to provide touch events. 513 */ 514 Guacamole.Mouse.Touchpad = function(element) { 515 516 /** 517 * Reference to this Guacamole.Mouse.Touchpad. 518 * @private 519 */ 520 var guac_touchpad = this; 521 522 /** 523 * The distance a two-finger touch must move per scrollwheel event, in 524 * pixels. 525 */ 526 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 527 528 /** 529 * The maximum number of milliseconds to wait for a touch to end for the 530 * gesture to be considered a click. 531 */ 532 this.clickTimingThreshold = 250; 533 534 /** 535 * The maximum number of pixels to allow a touch to move for the gesture to 536 * be considered a click. 537 */ 538 this.clickMoveThreshold = 10 * (window.devicePixelRatio || 1); 539 540 /** 541 * The current mouse state. The properties of this state are updated when 542 * mouse events fire. This state object is also passed in as a parameter to 543 * the handler of any mouse events. 544 * 545 * @type Guacamole.Mouse.State 546 */ 547 this.currentState = new Guacamole.Mouse.State( 548 0, 0, 549 false, false, false, false, false 550 ); 551 552 /** 553 * Fired whenever a mouse button is effectively pressed. This can happen 554 * as part of a "click" gesture initiated by the user by tapping one 555 * or more fingers over the touchpad element, as part of a "scroll" 556 * gesture initiated by dragging two fingers up or down, etc. 557 * 558 * @event 559 * @param {Guacamole.Mouse.State} state The current mouse state. 560 */ 561 this.onmousedown = null; 562 563 /** 564 * Fired whenever a mouse button is effectively released. This can happen 565 * as part of a "click" gesture initiated by the user by tapping one 566 * or more fingers over the touchpad element, as part of a "scroll" 567 * gesture initiated by dragging two fingers up or down, etc. 568 * 569 * @event 570 * @param {Guacamole.Mouse.State} state The current mouse state. 571 */ 572 this.onmouseup = null; 573 574 /** 575 * Fired whenever the user moves the mouse by dragging their finger over 576 * the touchpad element. 577 * 578 * @event 579 * @param {Guacamole.Mouse.State} state The current mouse state. 580 */ 581 this.onmousemove = null; 582 583 var touch_count = 0; 584 var last_touch_x = 0; 585 var last_touch_y = 0; 586 var last_touch_time = 0; 587 var pixels_moved = 0; 588 589 var touch_buttons = { 590 1: "left", 591 2: "right", 592 3: "middle" 593 }; 594 595 var gesture_in_progress = false; 596 var click_release_timeout = null; 597 598 element.addEventListener("touchend", function(e) { 599 600 e.preventDefault(); 601 602 // If we're handling a gesture AND this is the last touch 603 if (gesture_in_progress && e.touches.length === 0) { 604 605 var time = new Date().getTime(); 606 607 // Get corresponding mouse button 608 var button = touch_buttons[touch_count]; 609 610 // If mouse already down, release anad clear timeout 611 if (guac_touchpad.currentState[button]) { 612 613 // Fire button up event 614 guac_touchpad.currentState[button] = false; 615 if (guac_touchpad.onmouseup) 616 guac_touchpad.onmouseup(guac_touchpad.currentState); 617 618 // Clear timeout, if set 619 if (click_release_timeout) { 620 window.clearTimeout(click_release_timeout); 621 click_release_timeout = null; 622 } 623 624 } 625 626 // If single tap detected (based on time and distance) 627 if (time - last_touch_time <= guac_touchpad.clickTimingThreshold 628 && pixels_moved < guac_touchpad.clickMoveThreshold) { 629 630 // Fire button down event 631 guac_touchpad.currentState[button] = true; 632 if (guac_touchpad.onmousedown) 633 guac_touchpad.onmousedown(guac_touchpad.currentState); 634 635 // Delay mouse up - mouse up should be canceled if 636 // touchstart within timeout. 637 click_release_timeout = window.setTimeout(function() { 638 639 // Fire button up event 640 guac_touchpad.currentState[button] = false; 641 if (guac_touchpad.onmouseup) 642 guac_touchpad.onmouseup(guac_touchpad.currentState); 643 644 // Gesture now over 645 gesture_in_progress = false; 646 647 }, guac_touchpad.clickTimingThreshold); 648 649 } 650 651 // If we're not waiting to see if this is a click, stop gesture 652 if (!click_release_timeout) 653 gesture_in_progress = false; 654 655 } 656 657 }, false); 658 659 element.addEventListener("touchstart", function(e) { 660 661 e.preventDefault(); 662 663 // Track number of touches, but no more than three 664 touch_count = Math.min(e.touches.length, 3); 665 666 // Clear timeout, if set 667 if (click_release_timeout) { 668 window.clearTimeout(click_release_timeout); 669 click_release_timeout = null; 670 } 671 672 // Record initial touch location and time for touch movement 673 // and tap gestures 674 if (!gesture_in_progress) { 675 676 // Stop mouse events while touching 677 gesture_in_progress = true; 678 679 // Record touch location and time 680 var starting_touch = e.touches[0]; 681 last_touch_x = starting_touch.clientX; 682 last_touch_y = starting_touch.clientY; 683 last_touch_time = new Date().getTime(); 684 pixels_moved = 0; 685 686 } 687 688 }, false); 689 690 element.addEventListener("touchmove", function(e) { 691 692 e.preventDefault(); 693 694 // Get change in touch location 695 var touch = e.touches[0]; 696 var delta_x = touch.clientX - last_touch_x; 697 var delta_y = touch.clientY - last_touch_y; 698 699 // Track pixels moved 700 pixels_moved += Math.abs(delta_x) + Math.abs(delta_y); 701 702 // If only one touch involved, this is mouse move 703 if (touch_count === 1) { 704 705 // Calculate average velocity in Manhatten pixels per millisecond 706 var velocity = pixels_moved / (new Date().getTime() - last_touch_time); 707 708 // Scale mouse movement relative to velocity 709 var scale = 1 + velocity; 710 711 // Update mouse location 712 guac_touchpad.currentState.x += delta_x*scale; 713 guac_touchpad.currentState.y += delta_y*scale; 714 715 // Prevent mouse from leaving screen 716 717 if (guac_touchpad.currentState.x < 0) 718 guac_touchpad.currentState.x = 0; 719 else if (guac_touchpad.currentState.x >= element.offsetWidth) 720 guac_touchpad.currentState.x = element.offsetWidth - 1; 721 722 if (guac_touchpad.currentState.y < 0) 723 guac_touchpad.currentState.y = 0; 724 else if (guac_touchpad.currentState.y >= element.offsetHeight) 725 guac_touchpad.currentState.y = element.offsetHeight - 1; 726 727 // Fire movement event, if defined 728 if (guac_touchpad.onmousemove) 729 guac_touchpad.onmousemove(guac_touchpad.currentState); 730 731 // Update touch location 732 last_touch_x = touch.clientX; 733 last_touch_y = touch.clientY; 734 735 } 736 737 // Interpret two-finger swipe as scrollwheel 738 else if (touch_count === 2) { 739 740 // If change in location passes threshold for scroll 741 if (Math.abs(delta_y) >= guac_touchpad.scrollThreshold) { 742 743 // Decide button based on Y movement direction 744 var button; 745 if (delta_y > 0) button = "down"; 746 else button = "up"; 747 748 // Fire button down event 749 guac_touchpad.currentState[button] = true; 750 if (guac_touchpad.onmousedown) 751 guac_touchpad.onmousedown(guac_touchpad.currentState); 752 753 // Fire button up event 754 guac_touchpad.currentState[button] = false; 755 if (guac_touchpad.onmouseup) 756 guac_touchpad.onmouseup(guac_touchpad.currentState); 757 758 // Only update touch location after a scroll has been 759 // detected 760 last_touch_x = touch.clientX; 761 last_touch_y = touch.clientY; 762 763 } 764 765 } 766 767 }, false); 768 769 }; 770 771 /** 772 * Provides cross-browser absolute touch event translation for a given element. 773 * 774 * Touch events are translated into mouse events as if the touches occurred 775 * on a touchscreen (tapping anywhere on the screen clicks at that point, 776 * long-press to right-click). 777 * 778 * @constructor 779 * @param {Element} element The Element to use to provide touch events. 780 */ 781 Guacamole.Mouse.Touchscreen = function(element) { 782 783 /** 784 * Reference to this Guacamole.Mouse.Touchscreen. 785 * @private 786 */ 787 var guac_touchscreen = this; 788 789 /** 790 * Whether a gesture is known to be in progress. If false, touch events 791 * will be ignored. 792 */ 793 var gesture_in_progress = false; 794 795 /** 796 * The start X location of a gesture. 797 * @private 798 */ 799 var gesture_start_x = null; 800 801 /** 802 * The start Y location of a gesture. 803 * @private 804 */ 805 var gesture_start_y = null; 806 807 /** 808 * The timeout associated with the delayed, cancellable click release. 809 */ 810 var click_release_timeout = null; 811 812 /** 813 * The timeout associated with long-press for right click. 814 */ 815 var long_press_timeout = null; 816 817 /** 818 * The distance a two-finger touch must move per scrollwheel event, in 819 * pixels. 820 */ 821 this.scrollThreshold = 20 * (window.devicePixelRatio || 1); 822 823 /** 824 * The maximum number of milliseconds to wait for a touch to end for the 825 * gesture to be considered a click. 826 */ 827 this.clickTimingThreshold = 250; 828 829 /** 830 * The maximum number of pixels to allow a touch to move for the gesture to 831 * be considered a click. 832 */ 833 this.clickMoveThreshold = 16 * (window.devicePixelRatio || 1); 834 835 /** 836 * The amount of time a press must be held for long press to be 837 * detected. 838 */ 839 this.longPressThreshold = 500; 840 841 /** 842 * The current mouse state. The properties of this state are updated when 843 * mouse events fire. This state object is also passed in as a parameter to 844 * the handler of any mouse events. 845 * 846 * @type Guacamole.Mouse.State 847 */ 848 this.currentState = new Guacamole.Mouse.State( 849 0, 0, 850 false, false, false, false, false 851 ); 852 853 /** 854 * Fired whenever a mouse button is effectively pressed. This can happen 855 * as part of a "mousedown" gesture initiated by the user by pressing one 856 * finger over the touchscreen element, as part of a "scroll" gesture 857 * initiated by dragging two fingers up or down, etc. 858 * 859 * @event 860 * @param {Guacamole.Mouse.State} state The current mouse state. 861 */ 862 this.onmousedown = null; 863 864 /** 865 * Fired whenever a mouse button is effectively released. This can happen 866 * as part of a "mouseup" gesture initiated by the user by removing the 867 * finger pressed against the touchscreen element, or as part of a "scroll" 868 * gesture initiated by dragging two fingers up or down, etc. 869 * 870 * @event 871 * @param {Guacamole.Mouse.State} state The current mouse state. 872 */ 873 this.onmouseup = null; 874 875 /** 876 * Fired whenever the user moves the mouse by dragging their finger over 877 * the touchscreen element. Note that unlike Guacamole.Mouse.Touchpad, 878 * dragging a finger over the touchscreen element will always cause 879 * the mouse button to be effectively down, as if clicking-and-dragging. 880 * 881 * @event 882 * @param {Guacamole.Mouse.State} state The current mouse state. 883 */ 884 this.onmousemove = null; 885 886 /** 887 * Presses the given mouse button, if it isn't already pressed. Valid 888 * button values are "left", "middle", "right", "up", and "down". 889 * 890 * @private 891 * @param {String} button The mouse button to press. 892 */ 893 function press_button(button) { 894 if (!guac_touchscreen.currentState[button]) { 895 guac_touchscreen.currentState[button] = true; 896 if (guac_touchscreen.onmousedown) 897 guac_touchscreen.onmousedown(guac_touchscreen.currentState); 898 } 899 } 900 901 /** 902 * Releases the given mouse button, if it isn't already released. Valid 903 * button values are "left", "middle", "right", "up", and "down". 904 * 905 * @private 906 * @param {String} button The mouse button to release. 907 */ 908 function release_button(button) { 909 if (guac_touchscreen.currentState[button]) { 910 guac_touchscreen.currentState[button] = false; 911 if (guac_touchscreen.onmouseup) 912 guac_touchscreen.onmouseup(guac_touchscreen.currentState); 913 } 914 } 915 916 /** 917 * Clicks (presses and releases) the given mouse button. Valid button 918 * values are "left", "middle", "right", "up", and "down". 919 * 920 * @private 921 * @param {String} button The mouse button to click. 922 */ 923 function click_button(button) { 924 press_button(button); 925 release_button(button); 926 } 927 928 /** 929 * Moves the mouse to the given coordinates. These coordinates must be 930 * relative to the browser window, as they will be translated based on 931 * the touch event target's location within the browser window. 932 * 933 * @private 934 * @param {Number} x The X coordinate of the mouse pointer. 935 * @param {Number} y The Y coordinate of the mouse pointer. 936 */ 937 function move_mouse(x, y) { 938 guac_touchscreen.currentState.fromClientPosition(element, x, y); 939 if (guac_touchscreen.onmousemove) 940 guac_touchscreen.onmousemove(guac_touchscreen.currentState); 941 } 942 943 /** 944 * Returns whether the given touch event exceeds the movement threshold for 945 * clicking, based on where the touch gesture began. 946 * 947 * @private 948 * @param {TouchEvent} e The touch event to check. 949 * @return {Boolean} true if the movement threshold is exceeded, false 950 * otherwise. 951 */ 952 function finger_moved(e) { 953 var touch = e.touches[0] || e.changedTouches[0]; 954 var delta_x = touch.clientX - gesture_start_x; 955 var delta_y = touch.clientY - gesture_start_y; 956 return Math.sqrt(delta_x*delta_x + delta_y*delta_y) >= guac_touchscreen.clickMoveThreshold; 957 } 958 959 /** 960 * Begins a new gesture at the location of the first touch in the given 961 * touch event. 962 * 963 * @private 964 * @param {TouchEvent} e The touch event beginning this new gesture. 965 */ 966 function begin_gesture(e) { 967 var touch = e.touches[0]; 968 gesture_in_progress = true; 969 gesture_start_x = touch.clientX; 970 gesture_start_y = touch.clientY; 971 } 972 973 /** 974 * End the current gesture entirely. Wait for all touches to be done before 975 * resuming gesture detection. 976 * 977 * @private 978 */ 979 function end_gesture() { 980 window.clearTimeout(click_release_timeout); 981 window.clearTimeout(long_press_timeout); 982 gesture_in_progress = false; 983 } 984 985 element.addEventListener("touchend", function(e) { 986 987 // Do not handle if no gesture 988 if (!gesture_in_progress) 989 return; 990 991 // Ignore if more than one touch 992 if (e.touches.length !== 0 || e.changedTouches.length !== 1) { 993 end_gesture(); 994 return; 995 } 996 997 // Long-press, if any, is over 998 window.clearTimeout(long_press_timeout); 999 1000 // Always release mouse button if pressed 1001 release_button("left"); 1002 1003 // If finger hasn't moved enough to cancel the click 1004 if (!finger_moved(e)) { 1005 1006 e.preventDefault(); 1007 1008 // If not yet pressed, press and start delay release 1009 if (!guac_touchscreen.currentState.left) { 1010 1011 var touch = e.changedTouches[0]; 1012 move_mouse(touch.clientX, touch.clientY); 1013 press_button("left"); 1014 1015 // Release button after a delay, if not canceled 1016 click_release_timeout = window.setTimeout(function() { 1017 release_button("left"); 1018 end_gesture(); 1019 }, guac_touchscreen.clickTimingThreshold); 1020 1021 } 1022 1023 } // end if finger not moved 1024 1025 }, false); 1026 1027 element.addEventListener("touchstart", function(e) { 1028 1029 // Ignore if more than one touch 1030 if (e.touches.length !== 1) { 1031 end_gesture(); 1032 return; 1033 } 1034 1035 e.preventDefault(); 1036 1037 // New touch begins a new gesture 1038 begin_gesture(e); 1039 1040 // Keep button pressed if tap after left click 1041 window.clearTimeout(click_release_timeout); 1042 1043 // Click right button if this turns into a long-press 1044 long_press_timeout = window.setTimeout(function() { 1045 var touch = e.touches[0]; 1046 move_mouse(touch.clientX, touch.clientY); 1047 click_button("right"); 1048 end_gesture(); 1049 }, guac_touchscreen.longPressThreshold); 1050 1051 }, false); 1052 1053 element.addEventListener("touchmove", function(e) { 1054 1055 // Do not handle if no gesture 1056 if (!gesture_in_progress) 1057 return; 1058 1059 // Cancel long press if finger moved 1060 if (finger_moved(e)) 1061 window.clearTimeout(long_press_timeout); 1062 1063 // Ignore if more than one touch 1064 if (e.touches.length !== 1) { 1065 end_gesture(); 1066 return; 1067 } 1068 1069 // Update mouse position if dragging 1070 if (guac_touchscreen.currentState.left) { 1071 1072 e.preventDefault(); 1073 1074 // Update state 1075 var touch = e.touches[0]; 1076 move_mouse(touch.clientX, touch.clientY); 1077 1078 } 1079 1080 }, false); 1081 1082 }; 1083