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