| OLD | NEW |
| (Empty) | |
| 1 <!-- |
| 2 @license |
| 3 Copyright (c) 2017 The Polymer Project Authors. All rights reserved. |
| 4 This code may only be used under the BSD style license found at http://polymer.g
ithub.io/LICENSE.txt |
| 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| 6 The complete set of contributors may be found at http://polymer.github.io/CONTRI
BUTORS.txt |
| 7 Code distributed by Google as part of the polymer project is also |
| 8 subject to an additional IP rights grant found at http://polymer.github.io/PATEN
TS.txt |
| 9 --> |
| 10 <link rel="import" href="boot.html"> |
| 11 <link rel="import" href="async.html"> |
| 12 <link rel="import" href="debounce.html"> |
| 13 |
| 14 <script> |
| 15 (function() { |
| 16 |
| 17 'use strict'; |
| 18 |
| 19 // detect native touch action support |
| 20 let HAS_NATIVE_TA = typeof document.head.style.touchAction === 'string'; |
| 21 let GESTURE_KEY = '__polymerGestures'; |
| 22 let HANDLED_OBJ = '__polymerGesturesHandled'; |
| 23 let TOUCH_ACTION = '__polymerGesturesTouchAction'; |
| 24 // radius for tap and track |
| 25 let TAP_DISTANCE = 25; |
| 26 let TRACK_DISTANCE = 5; |
| 27 // number of last N track positions to keep |
| 28 let TRACK_LENGTH = 2; |
| 29 |
| 30 // Disabling "mouse" handlers for 2500ms is enough |
| 31 let MOUSE_TIMEOUT = 2500; |
| 32 let MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click']; |
| 33 // an array of bitmask values for mapping MouseEvent.which to MouseEvent.butto
ns |
| 34 let MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2]; |
| 35 let MOUSE_HAS_BUTTONS = (function() { |
| 36 try { |
| 37 return new MouseEvent('test', {buttons: 1}).buttons === 1; |
| 38 } catch (e) { |
| 39 return false; |
| 40 } |
| 41 })(); |
| 42 |
| 43 /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ |
| 44 // check for passive event listeners |
| 45 let SUPPORTS_PASSIVE = false; |
| 46 (function() { |
| 47 try { |
| 48 let opts = Object.defineProperty({}, 'passive', {get: function() {SUPPORTS
_PASSIVE = true;}}) |
| 49 window.addEventListener('test', null, opts); |
| 50 window.removeEventListener('test', null, opts); |
| 51 } catch(e) {} |
| 52 })(); |
| 53 |
| 54 // Check for touch-only devices |
| 55 let IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/); |
| 56 |
| 57 // touch will make synthetic mouse events |
| 58 // `preventDefault` on touchend will cancel them, |
| 59 // but this breaks `<input>` focus and link clicks |
| 60 // disable mouse handlers for MOUSE_TIMEOUT ms after |
| 61 // a touchend to ignore synthetic mouse events |
| 62 let mouseCanceller = function(mouseEvent) { |
| 63 // Check for sourceCapabilities, used to distinguish synthetic events |
| 64 // if mouseEvent did not come from a device that fires touch events, |
| 65 // it was made by a real mouse and should be counted |
| 66 // http://wicg.github.io/InputDeviceCapabilities/#dom-inputdevicecapabilitie
s-firestouchevents |
| 67 let sc = mouseEvent.sourceCapabilities; |
| 68 if (sc && !sc.firesTouchEvents) { |
| 69 return; |
| 70 } |
| 71 // skip synthetic mouse events |
| 72 mouseEvent[HANDLED_OBJ] = {skip: true}; |
| 73 // disable "ghost clicks" |
| 74 if (mouseEvent.type === 'click') { |
| 75 let path = mouseEvent.composedPath && mouseEvent.composedPath(); |
| 76 if (path) { |
| 77 for (let i = 0; i < path.length; i++) { |
| 78 if (path[i] === POINTERSTATE.mouse.target) { |
| 79 return; |
| 80 } |
| 81 } |
| 82 } |
| 83 mouseEvent.preventDefault(); |
| 84 mouseEvent.stopPropagation(); |
| 85 } |
| 86 }; |
| 87 |
| 88 /** |
| 89 * @param {boolean=} setup True to add, false to remove. |
| 90 */ |
| 91 function setupTeardownMouseCanceller(setup) { |
| 92 let events = IS_TOUCH_ONLY ? ['click'] : MOUSE_EVENTS; |
| 93 for (let i = 0, en; i < events.length; i++) { |
| 94 en = events[i]; |
| 95 if (setup) { |
| 96 document.addEventListener(en, mouseCanceller, true); |
| 97 } else { |
| 98 document.removeEventListener(en, mouseCanceller, true); |
| 99 } |
| 100 } |
| 101 } |
| 102 |
| 103 function ignoreMouse(e) { |
| 104 if (!POINTERSTATE.mouse.mouseIgnoreJob) { |
| 105 setupTeardownMouseCanceller(true); |
| 106 } |
| 107 let unset = function() { |
| 108 setupTeardownMouseCanceller(); |
| 109 POINTERSTATE.mouse.target = null; |
| 110 POINTERSTATE.mouse.mouseIgnoreJob = null; |
| 111 }; |
| 112 POINTERSTATE.mouse.target = e.composedPath()[0]; |
| 113 POINTERSTATE.mouse.mouseIgnoreJob = Polymer.Debouncer.debounce( |
| 114 POINTERSTATE.mouse.mouseIgnoreJob |
| 115 , Polymer.Async.timeOut.after(MOUSE_TIMEOUT) |
| 116 , unset); |
| 117 } |
| 118 |
| 119 function hasLeftMouseButton(ev) { |
| 120 let type = ev.type; |
| 121 // exit early if the event is not a mouse event |
| 122 if (MOUSE_EVENTS.indexOf(type) === -1) { |
| 123 return false; |
| 124 } |
| 125 // ev.button is not reliable for mousemove (0 is overloaded as both left but
ton and no buttons) |
| 126 // instead we use ev.buttons (bitmask of buttons) or fall back to ev.which (
deprecated, 0 for no buttons, 1 for left button) |
| 127 if (type === 'mousemove') { |
| 128 // allow undefined for testing events |
| 129 let buttons = ev.buttons === undefined ? 1 : ev.buttons; |
| 130 if ((ev instanceof window.MouseEvent) && !MOUSE_HAS_BUTTONS) { |
| 131 buttons = MOUSE_WHICH_TO_BUTTONS[ev.which] || 0; |
| 132 } |
| 133 // buttons is a bitmask, check that the left button bit is set (1) |
| 134 return Boolean(buttons & 1); |
| 135 } else { |
| 136 // allow undefined for testing events |
| 137 let button = ev.button === undefined ? 0 : ev.button; |
| 138 // ev.button is 0 in mousedown/mouseup/click for left button activation |
| 139 return button === 0; |
| 140 } |
| 141 } |
| 142 |
| 143 function isSyntheticClick(ev) { |
| 144 if (ev.type === 'click') { |
| 145 // ev.detail is 0 for HTMLElement.click in most browsers |
| 146 if (ev.detail === 0) { |
| 147 return true; |
| 148 } |
| 149 // in the worst case, check that the x/y position of the click is within |
| 150 // the bounding box of the target of the event |
| 151 // Thanks IE 10 >:( |
| 152 let t = Gestures._findOriginalTarget(ev); |
| 153 // make sure the target of the event is an element so we can use getBoundi
ngClientRect, |
| 154 // if not, just assume it is a synthetic click |
| 155 if (t.nodeType !== Node.ELEMENT_NODE) { |
| 156 return true; |
| 157 } |
| 158 let bcr = t.getBoundingClientRect(); |
| 159 // use page x/y to account for scrolling |
| 160 let x = ev.pageX, y = ev.pageY; |
| 161 // ev is a synthetic click if the position is outside the bounding box of
the target |
| 162 return !((x >= bcr.left && x <= bcr.right) && (y >= bcr.top && y <= bcr.bo
ttom)); |
| 163 } |
| 164 return false; |
| 165 } |
| 166 |
| 167 let POINTERSTATE = { |
| 168 mouse: { |
| 169 target: null, |
| 170 mouseIgnoreJob: null |
| 171 }, |
| 172 touch: { |
| 173 x: 0, |
| 174 y: 0, |
| 175 id: -1, |
| 176 scrollDecided: false |
| 177 } |
| 178 }; |
| 179 |
| 180 function firstTouchAction(ev) { |
| 181 let ta = 'auto'; |
| 182 let path = ev.composedPath && ev.composedPath(); |
| 183 if (path) { |
| 184 for (let i = 0, n; i < path.length; i++) { |
| 185 n = path[i]; |
| 186 if (n[TOUCH_ACTION]) { |
| 187 ta = n[TOUCH_ACTION]; |
| 188 break; |
| 189 } |
| 190 } |
| 191 } |
| 192 return ta; |
| 193 } |
| 194 |
| 195 function trackDocument(stateObj, movefn, upfn) { |
| 196 stateObj.movefn = movefn; |
| 197 stateObj.upfn = upfn; |
| 198 document.addEventListener('mousemove', movefn); |
| 199 document.addEventListener('mouseup', upfn); |
| 200 } |
| 201 |
| 202 function untrackDocument(stateObj) { |
| 203 document.removeEventListener('mousemove', stateObj.movefn); |
| 204 document.removeEventListener('mouseup', stateObj.upfn); |
| 205 stateObj.movefn = null; |
| 206 stateObj.upfn = null; |
| 207 } |
| 208 |
| 209 // use a document-wide touchend listener to start the ghost-click prevention m
echanism |
| 210 // Use passive event listeners, if supported, to not affect scrolling performa
nce |
| 211 document.addEventListener('touchend', ignoreMouse, SUPPORTS_PASSIVE ? {passive
: true} : false); |
| 212 |
| 213 /** |
| 214 * Module for adding listeners to a node for the following normalized |
| 215 * cross-platform "gesture" events: |
| 216 * - `down` - mouse or touch went down |
| 217 * - `up` - mouse or touch went up |
| 218 * - `tap` - mouse click or finger tap |
| 219 * - `track` - mouse drag or touch move |
| 220 * |
| 221 * @namespace |
| 222 * @memberof Polymer |
| 223 * @summary Module for adding cross-platform gesture event listeners. |
| 224 */ |
| 225 const Gestures = { |
| 226 gestures: {}, |
| 227 recognizers: [], |
| 228 |
| 229 /** |
| 230 * Finds the element rendered on the screen at the provided coordinates. |
| 231 * |
| 232 * Similar to `document.elementFromPoint`, but pierces through |
| 233 * shadow roots. |
| 234 * |
| 235 * @memberof Polymer.Gestures |
| 236 * @param {number} x Horizontal pixel coordinate |
| 237 * @param {number} y Vertical pixel coordinate |
| 238 * @return {HTMLElement} Returns the deepest shadowRoot inclusive element |
| 239 * found at the screen position given. |
| 240 */ |
| 241 deepTargetFind: function(x, y) { |
| 242 let node = document.elementFromPoint(x, y); |
| 243 let next = node; |
| 244 // this code path is only taken when native ShadowDOM is used |
| 245 // if there is a shadowroot, it may have a node at x/y |
| 246 // if there is not a shadowroot, exit the loop |
| 247 while (next && next.shadowRoot && !window.ShadyDOM) { |
| 248 // if there is a node at x/y in the shadowroot, look deeper |
| 249 let oldNext = next; |
| 250 next = next.shadowRoot.elementFromPoint(x, y); |
| 251 // on Safari, elementFromPoint may return the shadowRoot host |
| 252 if (oldNext === next) { |
| 253 break; |
| 254 } |
| 255 if (next) { |
| 256 node = next; |
| 257 } |
| 258 } |
| 259 return node; |
| 260 }, |
| 261 /** |
| 262 * a cheaper check than ev.composedPath()[0]; |
| 263 * |
| 264 * @private |
| 265 * @param {Event} ev Event. |
| 266 * @return {HTMLElement} Returns the event target. |
| 267 */ |
| 268 _findOriginalTarget: function(ev) { |
| 269 // shadowdom |
| 270 if (ev.composedPath) { |
| 271 return ev.composedPath()[0]; |
| 272 } |
| 273 // shadydom |
| 274 return ev.target; |
| 275 }, |
| 276 |
| 277 /** |
| 278 * @private |
| 279 * @param {Event} ev Event. |
| 280 */ |
| 281 _handleNative: function(ev) { |
| 282 let handled; |
| 283 let type = ev.type; |
| 284 let node = ev.currentTarget; |
| 285 let gobj = node[GESTURE_KEY]; |
| 286 if (!gobj) { |
| 287 return; |
| 288 } |
| 289 let gs = gobj[type]; |
| 290 if (!gs) { |
| 291 return; |
| 292 } |
| 293 if (!ev[HANDLED_OBJ]) { |
| 294 ev[HANDLED_OBJ] = {}; |
| 295 if (type.slice(0, 5) === 'touch') { |
| 296 let t = ev.changedTouches[0]; |
| 297 if (type === 'touchstart') { |
| 298 // only handle the first finger |
| 299 if (ev.touches.length === 1) { |
| 300 POINTERSTATE.touch.id = t.identifier; |
| 301 } |
| 302 } |
| 303 if (POINTERSTATE.touch.id !== t.identifier) { |
| 304 return; |
| 305 } |
| 306 if (!HAS_NATIVE_TA) { |
| 307 if (type === 'touchstart' || type === 'touchmove') { |
| 308 Gestures._handleTouchAction(ev); |
| 309 } |
| 310 } |
| 311 } |
| 312 } |
| 313 handled = ev[HANDLED_OBJ]; |
| 314 // used to ignore synthetic mouse events |
| 315 if (handled.skip) { |
| 316 return; |
| 317 } |
| 318 let recognizers = Gestures.recognizers; |
| 319 // reset recognizer state |
| 320 for (let i = 0, r; i < recognizers.length; i++) { |
| 321 r = recognizers[i]; |
| 322 if (gs[r.name] && !handled[r.name]) { |
| 323 if (r.flow && r.flow.start.indexOf(ev.type) > -1 && r.reset) { |
| 324 r.reset(); |
| 325 } |
| 326 } |
| 327 } |
| 328 // enforce gesture recognizer order |
| 329 for (let i = 0, r; i < recognizers.length; i++) { |
| 330 r = recognizers[i]; |
| 331 if (gs[r.name] && !handled[r.name]) { |
| 332 handled[r.name] = true; |
| 333 r[type](ev); |
| 334 } |
| 335 } |
| 336 }, |
| 337 |
| 338 /** |
| 339 * @private |
| 340 * @param {Event} ev Event. |
| 341 */ |
| 342 _handleTouchAction: function(ev) { |
| 343 let t = ev.changedTouches[0]; |
| 344 let type = ev.type; |
| 345 if (type === 'touchstart') { |
| 346 POINTERSTATE.touch.x = t.clientX; |
| 347 POINTERSTATE.touch.y = t.clientY; |
| 348 POINTERSTATE.touch.scrollDecided = false; |
| 349 } else if (type === 'touchmove') { |
| 350 if (POINTERSTATE.touch.scrollDecided) { |
| 351 return; |
| 352 } |
| 353 POINTERSTATE.touch.scrollDecided = true; |
| 354 let ta = firstTouchAction(ev); |
| 355 let prevent = false; |
| 356 let dx = Math.abs(POINTERSTATE.touch.x - t.clientX); |
| 357 let dy = Math.abs(POINTERSTATE.touch.y - t.clientY); |
| 358 if (!ev.cancelable) { |
| 359 // scrolling is happening |
| 360 } else if (ta === 'none') { |
| 361 prevent = true; |
| 362 } else if (ta === 'pan-x') { |
| 363 prevent = dy > dx; |
| 364 } else if (ta === 'pan-y') { |
| 365 prevent = dx > dy; |
| 366 } |
| 367 if (prevent) { |
| 368 ev.preventDefault(); |
| 369 } else { |
| 370 Gestures.prevent('track'); |
| 371 } |
| 372 } |
| 373 }, |
| 374 |
| 375 /** |
| 376 * Adds an event listener to a node for the given gesture type. |
| 377 * |
| 378 * @memberof Polymer.Gestures |
| 379 * @param {Node} node Node to add listener on |
| 380 * @param {string} evType Gesture type: `down`, `up`, `track`, or `tap` |
| 381 * @param {Function} handler Event listener function to call |
| 382 * @return {boolean} Returns true if a gesture event listener was added. |
| 383 */ |
| 384 addListener: function(node, evType, handler) { |
| 385 if (this.gestures[evType]) { |
| 386 this._add(node, evType, handler); |
| 387 return true; |
| 388 } |
| 389 }, |
| 390 |
| 391 /** |
| 392 * Removes an event listener from a node for the given gesture type. |
| 393 * |
| 394 * @memberof Polymer.Gestures |
| 395 * @param {Node} node Node to remove listener from |
| 396 * @param {string} evType Gesture type: `down`, `up`, `track`, or `tap` |
| 397 * @param {Function} handler Event listener function previously passed to |
| 398 * `addListener`. |
| 399 * @return {boolean} Returns true if a gesture event listener was removed. |
| 400 */ |
| 401 removeListener: function(node, evType, handler) { |
| 402 if (this.gestures[evType]) { |
| 403 this._remove(node, evType, handler); |
| 404 return true; |
| 405 } |
| 406 }, |
| 407 |
| 408 /** |
| 409 * automate the event listeners for the native events |
| 410 * |
| 411 * @private |
| 412 * @param {HTMLElement} node Node on which to add the event. |
| 413 * @param {string} evType Event type to add. |
| 414 * @param {function} handler Event handler function. |
| 415 */ |
| 416 _add: function(node, evType, handler) { |
| 417 let recognizer = this.gestures[evType]; |
| 418 let deps = recognizer.deps; |
| 419 let name = recognizer.name; |
| 420 let gobj = node[GESTURE_KEY]; |
| 421 if (!gobj) { |
| 422 node[GESTURE_KEY] = gobj = {}; |
| 423 } |
| 424 for (let i = 0, dep, gd; i < deps.length; i++) { |
| 425 dep = deps[i]; |
| 426 // don't add mouse handlers on iOS because they cause gray selection ove
rlays |
| 427 if (IS_TOUCH_ONLY && MOUSE_EVENTS.indexOf(dep) > -1 && dep !== 'click')
{ |
| 428 continue; |
| 429 } |
| 430 gd = gobj[dep]; |
| 431 if (!gd) { |
| 432 gobj[dep] = gd = {_count: 0}; |
| 433 } |
| 434 if (gd._count === 0) { |
| 435 node.addEventListener(dep, this._handleNative); |
| 436 } |
| 437 gd[name] = (gd[name] || 0) + 1; |
| 438 gd._count = (gd._count || 0) + 1; |
| 439 } |
| 440 node.addEventListener(evType, handler); |
| 441 if (recognizer.touchAction) { |
| 442 this.setTouchAction(node, recognizer.touchAction); |
| 443 } |
| 444 }, |
| 445 |
| 446 /** |
| 447 * automate event listener removal for native events |
| 448 * |
| 449 * @private |
| 450 * @param {HTMLElement} node Node on which to remove the event. |
| 451 * @param {string} evType Event type to remove. |
| 452 * @param {function} handler Event handler function. |
| 453 */ |
| 454 _remove: function(node, evType, handler) { |
| 455 let recognizer = this.gestures[evType]; |
| 456 let deps = recognizer.deps; |
| 457 let name = recognizer.name; |
| 458 let gobj = node[GESTURE_KEY]; |
| 459 if (gobj) { |
| 460 for (let i = 0, dep, gd; i < deps.length; i++) { |
| 461 dep = deps[i]; |
| 462 gd = gobj[dep]; |
| 463 if (gd && gd[name]) { |
| 464 gd[name] = (gd[name] || 1) - 1; |
| 465 gd._count = (gd._count || 1) - 1; |
| 466 if (gd._count === 0) { |
| 467 node.removeEventListener(dep, this._handleNative); |
| 468 } |
| 469 } |
| 470 } |
| 471 } |
| 472 node.removeEventListener(evType, handler); |
| 473 }, |
| 474 |
| 475 /** |
| 476 * Registers a new gesture event recognizer for adding new custom |
| 477 * gesture event types. |
| 478 * |
| 479 * @memberof Polymer.Gestures |
| 480 * @param {Object} recog Gesture recognizer descriptor |
| 481 */ |
| 482 register: function(recog) { |
| 483 this.recognizers.push(recog); |
| 484 for (let i = 0; i < recog.emits.length; i++) { |
| 485 this.gestures[recog.emits[i]] = recog; |
| 486 } |
| 487 }, |
| 488 |
| 489 /** |
| 490 * @private |
| 491 * @param {string} evName Event name. |
| 492 * @return {Object} Returns the gesture for the given event name. |
| 493 */ |
| 494 _findRecognizerByEvent: function(evName) { |
| 495 for (let i = 0, r; i < this.recognizers.length; i++) { |
| 496 r = this.recognizers[i]; |
| 497 for (let j = 0, n; j < r.emits.length; j++) { |
| 498 n = r.emits[j]; |
| 499 if (n === evName) { |
| 500 return r; |
| 501 } |
| 502 } |
| 503 } |
| 504 return null; |
| 505 }, |
| 506 |
| 507 /** |
| 508 * Sets scrolling direction on node. |
| 509 * |
| 510 * This value is checked on first move, thus it should be called prior to |
| 511 * adding event listeners. |
| 512 * |
| 513 * @memberof Polymer.Gestures |
| 514 * @param {Node} node Node to set touch action setting on |
| 515 * @param {string} value Touch action value |
| 516 */ |
| 517 setTouchAction: function(node, value) { |
| 518 if (HAS_NATIVE_TA) { |
| 519 node.style.touchAction = value; |
| 520 } |
| 521 node[TOUCH_ACTION] = value; |
| 522 }, |
| 523 |
| 524 /** |
| 525 * Dispatches an event on the `target` element of `type` with the given |
| 526 * `detail`. |
| 527 * @private |
| 528 * @param {HTMLElement} target The element on which to fire an event. |
| 529 * @param {string} type The type of event to fire. |
| 530 * @param {Object=} detail The detail object to populate on the event. |
| 531 */ |
| 532 _fire: function(target, type, detail) { |
| 533 let ev = new Event(type, { bubbles: true, cancelable: true, composed: true
}); |
| 534 ev.detail = detail; |
| 535 target.dispatchEvent(ev); |
| 536 // forward `preventDefault` in a clean way |
| 537 if (ev.defaultPrevented) { |
| 538 let preventer = detail.preventer || detail.sourceEvent; |
| 539 if (preventer && preventer.preventDefault) { |
| 540 preventer.preventDefault(); |
| 541 } |
| 542 } |
| 543 }, |
| 544 |
| 545 /** |
| 546 * Prevents the dispatch and default action of the given event name. |
| 547 * |
| 548 * @memberof Polymer.Gestures |
| 549 * @param {string} evName Event name. |
| 550 */ |
| 551 prevent: function(evName) { |
| 552 let recognizer = this._findRecognizerByEvent(evName); |
| 553 if (recognizer.info) { |
| 554 recognizer.info.prevent = true; |
| 555 } |
| 556 }, |
| 557 |
| 558 /** |
| 559 * Reset the 2500ms timeout on processing mouse input after detecting touch
input. |
| 560 * |
| 561 * Touch inputs create synthesized mouse inputs anywhere from 0 to 2000ms af
ter the touch. |
| 562 * This method should only be called during testing with simulated touch inp
uts. |
| 563 * Calling this method in production may cause duplicate taps or other Gestu
res. |
| 564 * |
| 565 * @memberof Polymer.Gestures |
| 566 */ |
| 567 resetMouseCanceller: function() { |
| 568 if (POINTERSTATE.mouse.mouseIgnoreJob) { |
| 569 POINTERSTATE.mouse.mouseIgnoreJob.flush(); |
| 570 } |
| 571 } |
| 572 }; |
| 573 |
| 574 Gestures.register({ |
| 575 name: 'downup', |
| 576 deps: ['mousedown', 'touchstart', 'touchend'], |
| 577 flow: { |
| 578 start: ['mousedown', 'touchstart'], |
| 579 end: ['mouseup', 'touchend'] |
| 580 }, |
| 581 emits: ['down', 'up'], |
| 582 |
| 583 info: { |
| 584 movefn: null, |
| 585 upfn: null |
| 586 }, |
| 587 |
| 588 reset: function() { |
| 589 untrackDocument(this.info); |
| 590 }, |
| 591 |
| 592 mousedown: function(e) { |
| 593 if (!hasLeftMouseButton(e)) { |
| 594 return; |
| 595 } |
| 596 let t = Gestures._findOriginalTarget(e); |
| 597 let self = this; |
| 598 let movefn = function movefn(e) { |
| 599 if (!hasLeftMouseButton(e)) { |
| 600 self._fire('up', t, e); |
| 601 untrackDocument(self.info); |
| 602 } |
| 603 }; |
| 604 let upfn = function upfn(e) { |
| 605 if (hasLeftMouseButton(e)) { |
| 606 self._fire('up', t, e); |
| 607 } |
| 608 untrackDocument(self.info); |
| 609 }; |
| 610 trackDocument(this.info, movefn, upfn); |
| 611 this._fire('down', t, e); |
| 612 }, |
| 613 touchstart: function(e) { |
| 614 this._fire('down', Gestures._findOriginalTarget(e), e.changedTouches[0], e
); |
| 615 }, |
| 616 touchend: function(e) { |
| 617 this._fire('up', Gestures._findOriginalTarget(e), e.changedTouches[0], e); |
| 618 }, |
| 619 _fire: function(type, target, event, preventer) { |
| 620 Gestures._fire(target, type, { |
| 621 x: event.clientX, |
| 622 y: event.clientY, |
| 623 sourceEvent: event, |
| 624 preventer: preventer, |
| 625 prevent: function(e) { |
| 626 return Gestures.prevent(e); |
| 627 } |
| 628 }); |
| 629 } |
| 630 }); |
| 631 |
| 632 Gestures.register({ |
| 633 name: 'track', |
| 634 touchAction: 'none', |
| 635 deps: ['mousedown', 'touchstart', 'touchmove', 'touchend'], |
| 636 flow: { |
| 637 start: ['mousedown', 'touchstart'], |
| 638 end: ['mouseup', 'touchend'] |
| 639 }, |
| 640 emits: ['track'], |
| 641 |
| 642 info: { |
| 643 x: 0, |
| 644 y: 0, |
| 645 state: 'start', |
| 646 started: false, |
| 647 moves: [], |
| 648 addMove: function(move) { |
| 649 if (this.moves.length > TRACK_LENGTH) { |
| 650 this.moves.shift(); |
| 651 } |
| 652 this.moves.push(move); |
| 653 }, |
| 654 movefn: null, |
| 655 upfn: null, |
| 656 prevent: false |
| 657 }, |
| 658 |
| 659 reset: function() { |
| 660 this.info.state = 'start'; |
| 661 this.info.started = false; |
| 662 this.info.moves = []; |
| 663 this.info.x = 0; |
| 664 this.info.y = 0; |
| 665 this.info.prevent = false; |
| 666 untrackDocument(this.info); |
| 667 }, |
| 668 |
| 669 hasMovedEnough: function(x, y) { |
| 670 if (this.info.prevent) { |
| 671 return false; |
| 672 } |
| 673 if (this.info.started) { |
| 674 return true; |
| 675 } |
| 676 let dx = Math.abs(this.info.x - x); |
| 677 let dy = Math.abs(this.info.y - y); |
| 678 return (dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE); |
| 679 }, |
| 680 |
| 681 mousedown: function(e) { |
| 682 if (!hasLeftMouseButton(e)) { |
| 683 return; |
| 684 } |
| 685 let t = Gestures._findOriginalTarget(e); |
| 686 let self = this; |
| 687 let movefn = function movefn(e) { |
| 688 let x = e.clientX, y = e.clientY; |
| 689 if (self.hasMovedEnough(x, y)) { |
| 690 // first move is 'start', subsequent moves are 'move', mouseup is 'end
' |
| 691 self.info.state = self.info.started ? (e.type === 'mouseup' ? 'end' :
'track') : 'start'; |
| 692 if (self.info.state === 'start') { |
| 693 // if and only if tracking, always prevent tap |
| 694 Gestures.prevent('tap'); |
| 695 } |
| 696 self.info.addMove({x: x, y: y}); |
| 697 if (!hasLeftMouseButton(e)) { |
| 698 // always _fire "end" |
| 699 self.info.state = 'end'; |
| 700 untrackDocument(self.info); |
| 701 } |
| 702 self._fire(t, e); |
| 703 self.info.started = true; |
| 704 } |
| 705 }; |
| 706 let upfn = function upfn(e) { |
| 707 if (self.info.started) { |
| 708 movefn(e); |
| 709 } |
| 710 |
| 711 // remove the temporary listeners |
| 712 untrackDocument(self.info); |
| 713 }; |
| 714 // add temporary document listeners as mouse retargets |
| 715 trackDocument(this.info, movefn, upfn); |
| 716 this.info.x = e.clientX; |
| 717 this.info.y = e.clientY; |
| 718 }, |
| 719 |
| 720 touchstart: function(e) { |
| 721 let ct = e.changedTouches[0]; |
| 722 this.info.x = ct.clientX; |
| 723 this.info.y = ct.clientY; |
| 724 }, |
| 725 |
| 726 touchmove: function(e) { |
| 727 let t = Gestures._findOriginalTarget(e); |
| 728 let ct = e.changedTouches[0]; |
| 729 let x = ct.clientX, y = ct.clientY; |
| 730 if (this.hasMovedEnough(x, y)) { |
| 731 if (this.info.state === 'start') { |
| 732 // if and only if tracking, always prevent tap |
| 733 Gestures.prevent('tap'); |
| 734 } |
| 735 this.info.addMove({x: x, y: y}); |
| 736 this._fire(t, ct); |
| 737 this.info.state = 'track'; |
| 738 this.info.started = true; |
| 739 } |
| 740 }, |
| 741 |
| 742 touchend: function(e) { |
| 743 let t = Gestures._findOriginalTarget(e); |
| 744 let ct = e.changedTouches[0]; |
| 745 // only trackend if track was started and not aborted |
| 746 if (this.info.started) { |
| 747 // reset started state on up |
| 748 this.info.state = 'end'; |
| 749 this.info.addMove({x: ct.clientX, y: ct.clientY}); |
| 750 this._fire(t, ct, e); |
| 751 } |
| 752 }, |
| 753 |
| 754 _fire: function(target, touch) { |
| 755 let secondlast = this.info.moves[this.info.moves.length - 2]; |
| 756 let lastmove = this.info.moves[this.info.moves.length - 1]; |
| 757 let dx = lastmove.x - this.info.x; |
| 758 let dy = lastmove.y - this.info.y; |
| 759 let ddx, ddy = 0; |
| 760 if (secondlast) { |
| 761 ddx = lastmove.x - secondlast.x; |
| 762 ddy = lastmove.y - secondlast.y; |
| 763 } |
| 764 return Gestures._fire(target, 'track', { |
| 765 state: this.info.state, |
| 766 x: touch.clientX, |
| 767 y: touch.clientY, |
| 768 dx: dx, |
| 769 dy: dy, |
| 770 ddx: ddx, |
| 771 ddy: ddy, |
| 772 sourceEvent: touch, |
| 773 hover: function() { |
| 774 return Gestures.deepTargetFind(touch.clientX, touch.clientY); |
| 775 } |
| 776 }); |
| 777 } |
| 778 |
| 779 }); |
| 780 |
| 781 Gestures.register({ |
| 782 name: 'tap', |
| 783 deps: ['mousedown', 'click', 'touchstart', 'touchend'], |
| 784 flow: { |
| 785 start: ['mousedown', 'touchstart'], |
| 786 end: ['click', 'touchend'] |
| 787 }, |
| 788 emits: ['tap'], |
| 789 info: { |
| 790 x: NaN, |
| 791 y: NaN, |
| 792 prevent: false |
| 793 }, |
| 794 reset: function() { |
| 795 this.info.x = NaN; |
| 796 this.info.y = NaN; |
| 797 this.info.prevent = false; |
| 798 }, |
| 799 save: function(e) { |
| 800 this.info.x = e.clientX; |
| 801 this.info.y = e.clientY; |
| 802 }, |
| 803 |
| 804 mousedown: function(e) { |
| 805 if (hasLeftMouseButton(e)) { |
| 806 this.save(e); |
| 807 } |
| 808 }, |
| 809 click: function(e) { |
| 810 if (hasLeftMouseButton(e)) { |
| 811 this.forward(e); |
| 812 } |
| 813 }, |
| 814 |
| 815 touchstart: function(e) { |
| 816 this.save(e.changedTouches[0], e); |
| 817 }, |
| 818 touchend: function(e) { |
| 819 this.forward(e.changedTouches[0], e); |
| 820 }, |
| 821 |
| 822 forward: function(e, preventer) { |
| 823 let dx = Math.abs(e.clientX - this.info.x); |
| 824 let dy = Math.abs(e.clientY - this.info.y); |
| 825 let t = Gestures._findOriginalTarget(e); |
| 826 // dx,dy can be NaN if `click` has been simulated and there was no `down`
for `start` |
| 827 if (isNaN(dx) || isNaN(dy) || (dx <= TAP_DISTANCE && dy <= TAP_DISTANCE) |
| isSyntheticClick(e)) { |
| 828 // prevent taps from being generated if an event has canceled them |
| 829 if (!this.info.prevent) { |
| 830 Gestures._fire(t, 'tap', { |
| 831 x: e.clientX, |
| 832 y: e.clientY, |
| 833 sourceEvent: e, |
| 834 preventer: preventer |
| 835 }); |
| 836 } |
| 837 } |
| 838 } |
| 839 }); |
| 840 |
| 841 /** @deprecated */ |
| 842 Gestures.findOriginalTarget = Gestures._findOriginalTarget; |
| 843 |
| 844 /** @deprecated */ |
| 845 Gestures.add = Gestures.addListener; |
| 846 |
| 847 /** @deprecated */ |
| 848 Gestures.remove = Gestures.removeListener; |
| 849 |
| 850 Polymer.Gestures = Gestures; |
| 851 |
| 852 })(); |
| 853 </script> |
| OLD | NEW |