Source: Mouse.js

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