diff options
Diffstat (limited to 'chromium/third_party/catapult/third_party/polymer2/bower_components/polymer/lib/utils/gestures.html')
-rw-r--r-- | chromium/third_party/catapult/third_party/polymer2/bower_components/polymer/lib/utils/gestures.html | 1093 |
1 files changed, 1093 insertions, 0 deletions
diff --git a/chromium/third_party/catapult/third_party/polymer2/bower_components/polymer/lib/utils/gestures.html b/chromium/third_party/catapult/third_party/polymer2/bower_components/polymer/lib/utils/gestures.html new file mode 100644 index 00000000000..2a05daa462e --- /dev/null +++ b/chromium/third_party/catapult/third_party/polymer2/bower_components/polymer/lib/utils/gestures.html @@ -0,0 +1,1093 @@ +<!-- +@license +Copyright (c) 2017 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt +Code distributed by Google as part of the polymer project is also +subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt +--> +<link rel="import" href="boot.html"> +<link rel="import" href="async.html"> +<link rel="import" href="debounce.html"> + +<script> +(function() { + + 'use strict'; + + // detect native touch action support + let HAS_NATIVE_TA = typeof document.head.style.touchAction === 'string'; + let GESTURE_KEY = '__polymerGestures'; + let HANDLED_OBJ = '__polymerGesturesHandled'; + let TOUCH_ACTION = '__polymerGesturesTouchAction'; + // radius for tap and track + let TAP_DISTANCE = 25; + let TRACK_DISTANCE = 5; + // number of last N track positions to keep + let TRACK_LENGTH = 2; + + // Disabling "mouse" handlers for 2500ms is enough + let MOUSE_TIMEOUT = 2500; + let MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click']; + // an array of bitmask values for mapping MouseEvent.which to MouseEvent.buttons + let MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2]; + let MOUSE_HAS_BUTTONS = (function() { + try { + return new MouseEvent('test', {buttons: 1}).buttons === 1; + } catch (e) { + return false; + } + })(); + + /** + * @param {string} name Possible mouse event name + * @return {boolean} true if mouse event, false if not + */ + function isMouseEvent(name) { + return MOUSE_EVENTS.indexOf(name) > -1; + } + + /* eslint no-empty: ["error", { "allowEmptyCatch": true }] */ + // check for passive event listeners + let SUPPORTS_PASSIVE = false; + (function() { + try { + let opts = Object.defineProperty({}, 'passive', {get() {SUPPORTS_PASSIVE = true;}}); + window.addEventListener('test', null, opts); + window.removeEventListener('test', null, opts); + } catch(e) {} + })(); + + /** + * Generate settings for event listeners, dependant on `Polymer.passiveTouchGestures` + * + * @param {string} eventName Event name to determine if `{passive}` option is needed + * @return {{passive: boolean} | undefined} Options to use for addEventListener and removeEventListener + */ + function PASSIVE_TOUCH(eventName) { + if (isMouseEvent(eventName) || eventName === 'touchend') { + return; + } + if (HAS_NATIVE_TA && SUPPORTS_PASSIVE && Polymer.passiveTouchGestures) { + return {passive: true}; + } else { + return; + } + } + + // Check for touch-only devices + let IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/); + + let GestureRecognizer = function(){}; // eslint-disable-line no-unused-vars + /** @type {function(): void} */ + GestureRecognizer.prototype.reset; + /** @type {function(MouseEvent): void | undefined} */ + GestureRecognizer.prototype.mousedown; + /** @type {(function(MouseEvent): void | undefined)} */ + GestureRecognizer.prototype.mousemove; + /** @type {(function(MouseEvent): void | undefined)} */ + GestureRecognizer.prototype.mouseup; + /** @type {(function(TouchEvent): void | undefined)} */ + GestureRecognizer.prototype.touchstart; + /** @type {(function(TouchEvent): void | undefined)} */ + GestureRecognizer.prototype.touchmove; + /** @type {(function(TouchEvent): void | undefined)} */ + GestureRecognizer.prototype.touchend; + /** @type {(function(MouseEvent): void | undefined)} */ + GestureRecognizer.prototype.click; + + // keep track of any labels hit by the mouseCanceller + /** @type {!Array<!HTMLLabelElement>} */ + const clickedLabels = []; + + /** @type {!Object<boolean>} */ + const labellable = { + 'button': true, + 'input': true, + 'keygen': true, + 'meter': true, + 'output': true, + 'textarea': true, + 'progress': true, + 'select': true + }; + + /** + * @param {HTMLElement} el Element to check labelling status + * @return {boolean} element can have labels + */ + function canBeLabelled(el) { + return labellable[el.localName] || false; + } + + /** + * @param {HTMLElement} el Element that may be labelled. + * @return {!Array<!HTMLLabelElement>} Relevant label for `el` + */ + function matchingLabels(el) { + let labels = [...(/** @type {HTMLInputElement} */(el).labels || [])]; + // IE doesn't have `labels` and Safari doesn't populate `labels` + // if element is in a shadowroot. + // In this instance, finding the non-ancestor labels is enough, + // as the mouseCancellor code will handle ancstor labels + if (!labels.length) { + labels = []; + let root = el.getRootNode(); + // if there is an id on `el`, check for all labels with a matching `for` attribute + if (el.id) { + let matching = root.querySelectorAll(`label[for = ${el.id}]`); + for (let i = 0; i < matching.length; i++) { + labels.push(/** @type {!HTMLLabelElement} */(matching[i])); + } + } + } + return labels; + } + + // touch will make synthetic mouse events + // `preventDefault` on touchend will cancel them, + // but this breaks `<input>` focus and link clicks + // disable mouse handlers for MOUSE_TIMEOUT ms after + // a touchend to ignore synthetic mouse events + let mouseCanceller = function(mouseEvent) { + // Check for sourceCapabilities, used to distinguish synthetic events + // if mouseEvent did not come from a device that fires touch events, + // it was made by a real mouse and should be counted + // http://wicg.github.io/InputDeviceCapabilities/#dom-inputdevicecapabilities-firestouchevents + let sc = mouseEvent.sourceCapabilities; + if (sc && !sc.firesTouchEvents) { + return; + } + // skip synthetic mouse events + mouseEvent[HANDLED_OBJ] = {skip: true}; + // disable "ghost clicks" + if (mouseEvent.type === 'click') { + let clickFromLabel = false; + let path = mouseEvent.composedPath && mouseEvent.composedPath(); + if (path) { + for (let i = 0; i < path.length; i++) { + if (path[i].nodeType === Node.ELEMENT_NODE) { + if (path[i].localName === 'label') { + clickedLabels.push(path[i]); + } else if (canBeLabelled(path[i])) { + let ownerLabels = matchingLabels(path[i]); + // check if one of the clicked labels is labelling this element + for (let j = 0; j < ownerLabels.length; j++) { + clickFromLabel = clickFromLabel || clickedLabels.indexOf(ownerLabels[j]) > -1; + } + } + } + if (path[i] === POINTERSTATE.mouse.target) { + return; + } + } + } + // if one of the clicked labels was labelling the target element, + // this is not a ghost click + if (clickFromLabel) { + return; + } + mouseEvent.preventDefault(); + mouseEvent.stopPropagation(); + } + }; + + /** + * @param {boolean=} setup True to add, false to remove. + * @return {void} + */ + function setupTeardownMouseCanceller(setup) { + let events = IS_TOUCH_ONLY ? ['click'] : MOUSE_EVENTS; + for (let i = 0, en; i < events.length; i++) { + en = events[i]; + if (setup) { + // reset clickLabels array + clickedLabels.length = 0; + document.addEventListener(en, mouseCanceller, true); + } else { + document.removeEventListener(en, mouseCanceller, true); + } + } + } + + function ignoreMouse(e) { + if (!POINTERSTATE.mouse.mouseIgnoreJob) { + setupTeardownMouseCanceller(true); + } + let unset = function() { + setupTeardownMouseCanceller(); + POINTERSTATE.mouse.target = null; + POINTERSTATE.mouse.mouseIgnoreJob = null; + }; + POINTERSTATE.mouse.target = e.composedPath()[0]; + POINTERSTATE.mouse.mouseIgnoreJob = Polymer.Debouncer.debounce( + POINTERSTATE.mouse.mouseIgnoreJob + , Polymer.Async.timeOut.after(MOUSE_TIMEOUT) + , unset); + } + + /** + * @param {MouseEvent} ev event to test for left mouse button down + * @return {boolean} has left mouse button down + */ + function hasLeftMouseButton(ev) { + let type = ev.type; + // exit early if the event is not a mouse event + if (!isMouseEvent(type)) { + return false; + } + // ev.button is not reliable for mousemove (0 is overloaded as both left button and no buttons) + // instead we use ev.buttons (bitmask of buttons) or fall back to ev.which (deprecated, 0 for no buttons, 1 for left button) + if (type === 'mousemove') { + // allow undefined for testing events + let buttons = ev.buttons === undefined ? 1 : ev.buttons; + if ((ev instanceof window.MouseEvent) && !MOUSE_HAS_BUTTONS) { + buttons = MOUSE_WHICH_TO_BUTTONS[ev.which] || 0; + } + // buttons is a bitmask, check that the left button bit is set (1) + return Boolean(buttons & 1); + } else { + // allow undefined for testing events + let button = ev.button === undefined ? 0 : ev.button; + // ev.button is 0 in mousedown/mouseup/click for left button activation + return button === 0; + } + } + + function isSyntheticClick(ev) { + if (ev.type === 'click') { + // ev.detail is 0 for HTMLElement.click in most browsers + if (ev.detail === 0) { + return true; + } + // in the worst case, check that the x/y position of the click is within + // the bounding box of the target of the event + // Thanks IE 10 >:( + let t = Gestures._findOriginalTarget(ev); + // make sure the target of the event is an element so we can use getBoundingClientRect, + // if not, just assume it is a synthetic click + if (!t.nodeType || /** @type {Element} */(t).nodeType !== Node.ELEMENT_NODE) { + return true; + } + let bcr = /** @type {Element} */(t).getBoundingClientRect(); + // use page x/y to account for scrolling + let x = ev.pageX, y = ev.pageY; + // ev is a synthetic click if the position is outside the bounding box of the target + return !((x >= bcr.left && x <= bcr.right) && (y >= bcr.top && y <= bcr.bottom)); + } + return false; + } + + let POINTERSTATE = { + mouse: { + target: null, + mouseIgnoreJob: null + }, + touch: { + x: 0, + y: 0, + id: -1, + scrollDecided: false + } + }; + + function firstTouchAction(ev) { + let ta = 'auto'; + let path = ev.composedPath && ev.composedPath(); + if (path) { + for (let i = 0, n; i < path.length; i++) { + n = path[i]; + if (n[TOUCH_ACTION]) { + ta = n[TOUCH_ACTION]; + break; + } + } + } + return ta; + } + + function trackDocument(stateObj, movefn, upfn) { + stateObj.movefn = movefn; + stateObj.upfn = upfn; + document.addEventListener('mousemove', movefn); + document.addEventListener('mouseup', upfn); + } + + function untrackDocument(stateObj) { + document.removeEventListener('mousemove', stateObj.movefn); + document.removeEventListener('mouseup', stateObj.upfn); + stateObj.movefn = null; + stateObj.upfn = null; + } + + // use a document-wide touchend listener to start the ghost-click prevention mechanism + // Use passive event listeners, if supported, to not affect scrolling performance + document.addEventListener('touchend', ignoreMouse, SUPPORTS_PASSIVE ? {passive: true} : false); + + /** + * Module for adding listeners to a node for the following normalized + * cross-platform "gesture" events: + * - `down` - mouse or touch went down + * - `up` - mouse or touch went up + * - `tap` - mouse click or finger tap + * - `track` - mouse drag or touch move + * + * @namespace + * @memberof Polymer + * @summary Module for adding cross-platform gesture event listeners. + */ + const Gestures = { + gestures: {}, + recognizers: [], + + /** + * Finds the element rendered on the screen at the provided coordinates. + * + * Similar to `document.elementFromPoint`, but pierces through + * shadow roots. + * + * @memberof Polymer.Gestures + * @param {number} x Horizontal pixel coordinate + * @param {number} y Vertical pixel coordinate + * @return {Element} Returns the deepest shadowRoot inclusive element + * found at the screen position given. + */ + deepTargetFind: function(x, y) { + let node = document.elementFromPoint(x, y); + let next = node; + // this code path is only taken when native ShadowDOM is used + // if there is a shadowroot, it may have a node at x/y + // if there is not a shadowroot, exit the loop + while (next && next.shadowRoot && !window.ShadyDOM) { + // if there is a node at x/y in the shadowroot, look deeper + let oldNext = next; + next = next.shadowRoot.elementFromPoint(x, y); + // on Safari, elementFromPoint may return the shadowRoot host + if (oldNext === next) { + break; + } + if (next) { + node = next; + } + } + return node; + }, + /** + * a cheaper check than ev.composedPath()[0]; + * + * @private + * @param {Event} ev Event. + * @return {EventTarget} Returns the event target. + */ + _findOriginalTarget: function(ev) { + // shadowdom + if (ev.composedPath) { + const targets = /** @type {!Array<!EventTarget>} */(ev.composedPath()); + // It shouldn't be, but sometimes targets is empty (window on Safari). + return targets.length > 0 ? targets[0] : ev.target; + } + // shadydom + return ev.target; + }, + + /** + * @private + * @param {Event} ev Event. + * @return {void} + */ + _handleNative: function(ev) { + let handled; + let type = ev.type; + let node = ev.currentTarget; + let gobj = node[GESTURE_KEY]; + if (!gobj) { + return; + } + let gs = gobj[type]; + if (!gs) { + return; + } + if (!ev[HANDLED_OBJ]) { + ev[HANDLED_OBJ] = {}; + if (type.slice(0, 5) === 'touch') { + ev = /** @type {TouchEvent} */(ev); // eslint-disable-line no-self-assign + let t = ev.changedTouches[0]; + if (type === 'touchstart') { + // only handle the first finger + if (ev.touches.length === 1) { + POINTERSTATE.touch.id = t.identifier; + } + } + if (POINTERSTATE.touch.id !== t.identifier) { + return; + } + if (!HAS_NATIVE_TA) { + if (type === 'touchstart' || type === 'touchmove') { + Gestures._handleTouchAction(ev); + } + } + } + } + handled = ev[HANDLED_OBJ]; + // used to ignore synthetic mouse events + if (handled.skip) { + return; + } + // reset recognizer state + for (let i = 0, r; i < Gestures.recognizers.length; i++) { + r = Gestures.recognizers[i]; + if (gs[r.name] && !handled[r.name]) { + if (r.flow && r.flow.start.indexOf(ev.type) > -1 && r.reset) { + r.reset(); + } + } + } + // enforce gesture recognizer order + for (let i = 0, r; i < Gestures.recognizers.length; i++) { + r = Gestures.recognizers[i]; + if (gs[r.name] && !handled[r.name]) { + handled[r.name] = true; + r[type](ev); + } + } + }, + + /** + * @private + * @param {TouchEvent} ev Event. + * @return {void} + */ + _handleTouchAction: function(ev) { + let t = ev.changedTouches[0]; + let type = ev.type; + if (type === 'touchstart') { + POINTERSTATE.touch.x = t.clientX; + POINTERSTATE.touch.y = t.clientY; + POINTERSTATE.touch.scrollDecided = false; + } else if (type === 'touchmove') { + if (POINTERSTATE.touch.scrollDecided) { + return; + } + POINTERSTATE.touch.scrollDecided = true; + let ta = firstTouchAction(ev); + let prevent = false; + let dx = Math.abs(POINTERSTATE.touch.x - t.clientX); + let dy = Math.abs(POINTERSTATE.touch.y - t.clientY); + if (!ev.cancelable) { + // scrolling is happening + } else if (ta === 'none') { + prevent = true; + } else if (ta === 'pan-x') { + prevent = dy > dx; + } else if (ta === 'pan-y') { + prevent = dx > dy; + } + if (prevent) { + ev.preventDefault(); + } else { + Gestures.prevent('track'); + } + } + }, + + /** + * Adds an event listener to a node for the given gesture type. + * + * @memberof Polymer.Gestures + * @param {!Node} node Node to add listener on + * @param {string} evType Gesture type: `down`, `up`, `track`, or `tap` + * @param {!function(!Event):void} handler Event listener function to call + * @return {boolean} Returns true if a gesture event listener was added. + * @this {Gestures} + */ + addListener: function(node, evType, handler) { + if (this.gestures[evType]) { + this._add(node, evType, handler); + return true; + } + return false; + }, + + /** + * Removes an event listener from a node for the given gesture type. + * + * @memberof Polymer.Gestures + * @param {!Node} node Node to remove listener from + * @param {string} evType Gesture type: `down`, `up`, `track`, or `tap` + * @param {!function(!Event):void} handler Event listener function previously passed to + * `addListener`. + * @return {boolean} Returns true if a gesture event listener was removed. + * @this {Gestures} + */ + removeListener: function(node, evType, handler) { + if (this.gestures[evType]) { + this._remove(node, evType, handler); + return true; + } + return false; + }, + + /** + * automate the event listeners for the native events + * + * @private + * @param {!HTMLElement} node Node on which to add the event. + * @param {string} evType Event type to add. + * @param {function(!Event)} handler Event handler function. + * @return {void} + * @this {Gestures} + */ + _add: function(node, evType, handler) { + let recognizer = this.gestures[evType]; + let deps = recognizer.deps; + let name = recognizer.name; + let gobj = node[GESTURE_KEY]; + if (!gobj) { + node[GESTURE_KEY] = gobj = {}; + } + for (let i = 0, dep, gd; i < deps.length; i++) { + dep = deps[i]; + // don't add mouse handlers on iOS because they cause gray selection overlays + if (IS_TOUCH_ONLY && isMouseEvent(dep) && dep !== 'click') { + continue; + } + gd = gobj[dep]; + if (!gd) { + gobj[dep] = gd = {_count: 0}; + } + if (gd._count === 0) { + node.addEventListener(dep, this._handleNative, PASSIVE_TOUCH(dep)); + } + gd[name] = (gd[name] || 0) + 1; + gd._count = (gd._count || 0) + 1; + } + node.addEventListener(evType, handler); + if (recognizer.touchAction) { + this.setTouchAction(node, recognizer.touchAction); + } + }, + + /** + * automate event listener removal for native events + * + * @private + * @param {!HTMLElement} node Node on which to remove the event. + * @param {string} evType Event type to remove. + * @param {function(Event?)} handler Event handler function. + * @return {void} + * @this {Gestures} + */ + _remove: function(node, evType, handler) { + let recognizer = this.gestures[evType]; + let deps = recognizer.deps; + let name = recognizer.name; + let gobj = node[GESTURE_KEY]; + if (gobj) { + for (let i = 0, dep, gd; i < deps.length; i++) { + dep = deps[i]; + gd = gobj[dep]; + if (gd && gd[name]) { + gd[name] = (gd[name] || 1) - 1; + gd._count = (gd._count || 1) - 1; + if (gd._count === 0) { + node.removeEventListener(dep, this._handleNative, PASSIVE_TOUCH(dep)); + } + } + } + } + node.removeEventListener(evType, handler); + }, + + /** + * Registers a new gesture event recognizer for adding new custom + * gesture event types. + * + * @memberof Polymer.Gestures + * @param {!GestureRecognizer} recog Gesture recognizer descriptor + * @return {void} + * @this {Gestures} + */ + register: function(recog) { + this.recognizers.push(recog); + for (let i = 0; i < recog.emits.length; i++) { + this.gestures[recog.emits[i]] = recog; + } + }, + + /** + * @private + * @param {string} evName Event name. + * @return {Object} Returns the gesture for the given event name. + * @this {Gestures} + */ + _findRecognizerByEvent: function(evName) { + for (let i = 0, r; i < this.recognizers.length; i++) { + r = this.recognizers[i]; + for (let j = 0, n; j < r.emits.length; j++) { + n = r.emits[j]; + if (n === evName) { + return r; + } + } + } + return null; + }, + + /** + * Sets scrolling direction on node. + * + * This value is checked on first move, thus it should be called prior to + * adding event listeners. + * + * @memberof Polymer.Gestures + * @param {!Element} node Node to set touch action setting on + * @param {string} value Touch action value + * @return {void} + */ + setTouchAction: function(node, value) { + if (HAS_NATIVE_TA) { + // NOTE: add touchAction async so that events can be added in + // custom element constructors. Otherwise we run afoul of custom + // elements restriction against settings attributes (style) in the + // constructor. + Polymer.Async.microTask.run(() => { + node.style.touchAction = value; + }); + } + node[TOUCH_ACTION] = value; + }, + + /** + * Dispatches an event on the `target` element of `type` with the given + * `detail`. + * @private + * @param {!EventTarget} target The element on which to fire an event. + * @param {string} type The type of event to fire. + * @param {!Object=} detail The detail object to populate on the event. + * @return {void} + */ + _fire: function(target, type, detail) { + let ev = new Event(type, { bubbles: true, cancelable: true, composed: true }); + ev.detail = detail; + target.dispatchEvent(ev); + // forward `preventDefault` in a clean way + if (ev.defaultPrevented) { + let preventer = detail.preventer || detail.sourceEvent; + if (preventer && preventer.preventDefault) { + preventer.preventDefault(); + } + } + }, + + /** + * Prevents the dispatch and default action of the given event name. + * + * @memberof Polymer.Gestures + * @param {string} evName Event name. + * @return {void} + * @this {Gestures} + */ + prevent: function(evName) { + let recognizer = this._findRecognizerByEvent(evName); + if (recognizer.info) { + recognizer.info.prevent = true; + } + }, + + /** + * Reset the 2500ms timeout on processing mouse input after detecting touch input. + * + * Touch inputs create synthesized mouse inputs anywhere from 0 to 2000ms after the touch. + * This method should only be called during testing with simulated touch inputs. + * Calling this method in production may cause duplicate taps or other Gestures. + * + * @memberof Polymer.Gestures + * @return {void} + */ + resetMouseCanceller: function() { + if (POINTERSTATE.mouse.mouseIgnoreJob) { + POINTERSTATE.mouse.mouseIgnoreJob.flush(); + } + } + }; + + /* eslint-disable valid-jsdoc */ + + Gestures.register({ + name: 'downup', + deps: ['mousedown', 'touchstart', 'touchend'], + flow: { + start: ['mousedown', 'touchstart'], + end: ['mouseup', 'touchend'] + }, + emits: ['down', 'up'], + + info: { + movefn: null, + upfn: null + }, + + /** + * @this {GestureRecognizer} + * @return {void} + */ + reset: function() { + untrackDocument(this.info); + }, + + /** + * @this {GestureRecognizer} + * @param {MouseEvent} e + * @return {void} + */ + mousedown: function(e) { + if (!hasLeftMouseButton(e)) { + return; + } + let t = Gestures._findOriginalTarget(e); + let self = this; + let movefn = function movefn(e) { + if (!hasLeftMouseButton(e)) { + self._fire('up', t, e); + untrackDocument(self.info); + } + }; + let upfn = function upfn(e) { + if (hasLeftMouseButton(e)) { + self._fire('up', t, e); + } + untrackDocument(self.info); + }; + trackDocument(this.info, movefn, upfn); + this._fire('down', t, e); + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchstart: function(e) { + this._fire('down', Gestures._findOriginalTarget(e), e.changedTouches[0], e); + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchend: function(e) { + this._fire('up', Gestures._findOriginalTarget(e), e.changedTouches[0], e); + }, + /** + * @param {string} type + * @param {!EventTarget} target + * @param {Event} event + * @param {Function} preventer + * @return {void} + */ + _fire: function(type, target, event, preventer) { + Gestures._fire(target, type, { + x: event.clientX, + y: event.clientY, + sourceEvent: event, + preventer: preventer, + prevent: function(e) { + return Gestures.prevent(e); + } + }); + } + }); + + Gestures.register({ + name: 'track', + touchAction: 'none', + deps: ['mousedown', 'touchstart', 'touchmove', 'touchend'], + flow: { + start: ['mousedown', 'touchstart'], + end: ['mouseup', 'touchend'] + }, + emits: ['track'], + + info: { + x: 0, + y: 0, + state: 'start', + started: false, + moves: [], + /** @this {GestureRecognizer} */ + addMove: function(move) { + if (this.moves.length > TRACK_LENGTH) { + this.moves.shift(); + } + this.moves.push(move); + }, + movefn: null, + upfn: null, + prevent: false + }, + + /** + * @this {GestureRecognizer} + * @return {void} + */ + reset: function() { + this.info.state = 'start'; + this.info.started = false; + this.info.moves = []; + this.info.x = 0; + this.info.y = 0; + this.info.prevent = false; + untrackDocument(this.info); + }, + + /** + * @this {GestureRecognizer} + * @param {number} x + * @param {number} y + * @return {boolean} + */ + hasMovedEnough: function(x, y) { + if (this.info.prevent) { + return false; + } + if (this.info.started) { + return true; + } + let dx = Math.abs(this.info.x - x); + let dy = Math.abs(this.info.y - y); + return (dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE); + }, + /** + * @this {GestureRecognizer} + * @param {MouseEvent} e + * @return {void} + */ + mousedown: function(e) { + if (!hasLeftMouseButton(e)) { + return; + } + let t = Gestures._findOriginalTarget(e); + let self = this; + let movefn = function movefn(e) { + let x = e.clientX, y = e.clientY; + if (self.hasMovedEnough(x, y)) { + // first move is 'start', subsequent moves are 'move', mouseup is 'end' + self.info.state = self.info.started ? (e.type === 'mouseup' ? 'end' : 'track') : 'start'; + if (self.info.state === 'start') { + // if and only if tracking, always prevent tap + Gestures.prevent('tap'); + } + self.info.addMove({x: x, y: y}); + if (!hasLeftMouseButton(e)) { + // always _fire "end" + self.info.state = 'end'; + untrackDocument(self.info); + } + self._fire(t, e); + self.info.started = true; + } + }; + let upfn = function upfn(e) { + if (self.info.started) { + movefn(e); + } + + // remove the temporary listeners + untrackDocument(self.info); + }; + // add temporary document listeners as mouse retargets + trackDocument(this.info, movefn, upfn); + this.info.x = e.clientX; + this.info.y = e.clientY; + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchstart: function(e) { + let ct = e.changedTouches[0]; + this.info.x = ct.clientX; + this.info.y = ct.clientY; + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchmove: function(e) { + let t = Gestures._findOriginalTarget(e); + let ct = e.changedTouches[0]; + let x = ct.clientX, y = ct.clientY; + if (this.hasMovedEnough(x, y)) { + if (this.info.state === 'start') { + // if and only if tracking, always prevent tap + Gestures.prevent('tap'); + } + this.info.addMove({x: x, y: y}); + this._fire(t, ct); + this.info.state = 'track'; + this.info.started = true; + } + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchend: function(e) { + let t = Gestures._findOriginalTarget(e); + let ct = e.changedTouches[0]; + // only trackend if track was started and not aborted + if (this.info.started) { + // reset started state on up + this.info.state = 'end'; + this.info.addMove({x: ct.clientX, y: ct.clientY}); + this._fire(t, ct, e); + } + }, + + /** + * @this {GestureRecognizer} + * @param {!EventTarget} target + * @param {Touch} touch + * @return {void} + */ + _fire: function(target, touch) { + let secondlast = this.info.moves[this.info.moves.length - 2]; + let lastmove = this.info.moves[this.info.moves.length - 1]; + let dx = lastmove.x - this.info.x; + let dy = lastmove.y - this.info.y; + let ddx, ddy = 0; + if (secondlast) { + ddx = lastmove.x - secondlast.x; + ddy = lastmove.y - secondlast.y; + } + Gestures._fire(target, 'track', { + state: this.info.state, + x: touch.clientX, + y: touch.clientY, + dx: dx, + dy: dy, + ddx: ddx, + ddy: ddy, + sourceEvent: touch, + hover: function() { + return Gestures.deepTargetFind(touch.clientX, touch.clientY); + } + }); + } + + }); + + Gestures.register({ + name: 'tap', + deps: ['mousedown', 'click', 'touchstart', 'touchend'], + flow: { + start: ['mousedown', 'touchstart'], + end: ['click', 'touchend'] + }, + emits: ['tap'], + info: { + x: NaN, + y: NaN, + prevent: false + }, + /** + * @this {GestureRecognizer} + * @return {void} + */ + reset: function() { + this.info.x = NaN; + this.info.y = NaN; + this.info.prevent = false; + }, + /** + * @this {GestureRecognizer} + * @param {MouseEvent} e + * @return {void} + */ + save: function(e) { + this.info.x = e.clientX; + this.info.y = e.clientY; + }, + /** + * @this {GestureRecognizer} + * @param {MouseEvent} e + * @return {void} + */ + mousedown: function(e) { + if (hasLeftMouseButton(e)) { + this.save(e); + } + }, + /** + * @this {GestureRecognizer} + * @param {MouseEvent} e + * @return {void} + */ + click: function(e) { + if (hasLeftMouseButton(e)) { + this.forward(e); + } + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchstart: function(e) { + this.save(e.changedTouches[0], e); + }, + /** + * @this {GestureRecognizer} + * @param {TouchEvent} e + * @return {void} + */ + touchend: function(e) { + this.forward(e.changedTouches[0], e); + }, + /** + * @this {GestureRecognizer} + * @param {Event | Touch} e + * @param {Event=} preventer + * @return {void} + */ + forward: function(e, preventer) { + let dx = Math.abs(e.clientX - this.info.x); + let dy = Math.abs(e.clientY - this.info.y); + // find original target from `preventer` for TouchEvents, or `e` for MouseEvents + let t = Gestures._findOriginalTarget(/** @type {Event} */(preventer || e)); + if (!t || t.disabled) { + return; + } + // dx,dy can be NaN if `click` has been simulated and there was no `down` for `start` + if (isNaN(dx) || isNaN(dy) || (dx <= TAP_DISTANCE && dy <= TAP_DISTANCE) || isSyntheticClick(e)) { + // prevent taps from being generated if an event has canceled them + if (!this.info.prevent) { + Gestures._fire(t, 'tap', { + x: e.clientX, + y: e.clientY, + sourceEvent: e, + preventer: preventer + }); + } + } + } + }); + + /* eslint-enable valid-jsdoc */ + + /** @deprecated */ + Gestures.findOriginalTarget = Gestures._findOriginalTarget; + + /** @deprecated */ + Gestures.add = Gestures.addListener; + + /** @deprecated */ + Gestures.remove = Gestures.removeListener; + + Polymer.Gestures = Gestures; + +})(); +</script> |