@exmg/lit-base
Version:
Lit Base Elements for ExMachina
1,065 lines • 33.5 kB
JavaScript
/* eslint-disable */
/**
@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
*/
/**
* @fileoverview
*
* 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
*
* @summary Module for adding cross-platform gesture event listeners.
*/
import { timeOut, microTask } from './debounce/async.js';
import { Debouncer } from './debounce/debounce.js';
/**
* Setting to cancel synthetic click events fired by older mobile browsers. Modern browsers
* no longer fire synthetic click events, and the cancellation behavior can interfere
* when programmatically clicking on elements.
*/
const cancelSyntheticClickEvents = true;
/**
* Globally settable property to make Polymer Gestures use passive TouchEvent listeners when recognizing gestures.
* When set to `true`, gestures made from touch will not be able to prevent scrolling, allowing for smoother
* scrolling performance.
* Defaults to `false` for backwards compatibility.
*/
const passiveTouchGestures = false;
export const wrap = (n) => n;
// detect native touch action support
const HAS_NATIVE_TA = typeof document.head.style.touchAction === 'string';
const GESTURE_KEY = '__polymerGestures';
const HANDLED_OBJ = '__polymerGesturesHandled';
const TOUCH_ACTION = '__polymerGesturesTouchAction';
// radius for tap and track
const TAP_DISTANCE = 25;
const TRACK_DISTANCE = 5;
// number of last N track positions to keep
const TRACK_LENGTH = 2;
// Disabling "mouse" handlers for 2500ms is enough
const MOUSE_TIMEOUT = 2500;
const MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click'];
// an array of bitmask values for mapping MouseEvent.which to MouseEvent.buttons
const MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2];
const 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 supportsPassive = false;
(function () {
try {
const opts = Object.defineProperty({}, 'passive', {
get() {
supportsPassive = true;
return true;
},
});
// @ts-ignore
window.addEventListener('test', null, opts);
// @ts-ignore
window.removeEventListener('test', null, opts);
}
catch (e) { }
})();
/**
* Generate settings for event listeners, dependant on `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 && supportsPassive && passiveTouchGestures) {
return { passive: true };
}
else {
return;
}
}
// Check for touch-only devices
const IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/);
// 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,
};
// Defined at https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute
/** @type {!Object<boolean>} */
const canBeDisabled = {
button: true,
command: true,
fieldset: true,
input: true,
keygen: true,
optgroup: true,
option: true,
select: true,
textarea: 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 = Array.prototype.slice.call(/** @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 = [];
try {
const root = el.getRootNode();
// if there is an id on `el`, check for all labels with a matching `for` attribute
if (el.id) {
const matching = root.querySelectorAll(`label[for = '${el.id}']`);
for (let i = 0; i < matching.length; i++) {
labels.push(/** @type {!HTMLLabelElement} */ matching[i]);
}
}
}
catch (e) {
// Either:
// 1. el.getRootNode() failed.
// 2. el.id cannot be used in `querySelectorAll`
// In both cases, do nothing.
}
}
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
const 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
const 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;
const path = getComposedPath(mouseEvent);
for (let i = 0; i < path.length; i++) {
if (path[i].nodeType === Node.ELEMENT_NODE) {
if (path[i].localName === 'label') {
// @ts-ignore
clickedLabels.push(/** @type {!HTMLLabelElement} */ path[i]);
}
else if (canBeLabelled(/** @type {!HTMLElement} */ path[i])) {
const ownerLabels = matchingLabels(/** @type {!HTMLElement} */ path[i]);
// check if one of the clicked labels is labelling this element
for (let j = 0; j < ownerLabels.length; j++) {
// @ts-ignore
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) {
const 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 (!cancelSyntheticClickEvents) {
return;
}
if (!POINTERSTATE.mouse.mouseIgnoreJob) {
setupTeardownMouseCanceller(true);
}
const unset = function () {
setupTeardownMouseCanceller();
POINTERSTATE.mouse.target = null;
POINTERSTATE.mouse.mouseIgnoreJob = null;
};
POINTERSTATE.mouse.target = getComposedPath(e)[0];
// @ts-ignore
POINTERSTATE.mouse.mouseIgnoreJob = Debouncer.debounce(POINTERSTATE.mouse.mouseIgnoreJob, 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) {
const 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
const 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 >:(
const t = _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;
}
const bcr = /** @type {Element} */ t.getBoundingClientRect();
// use page x/y to account for scrolling
const x = ev.pageX;
const 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;
}
const POINTERSTATE = {
mouse: {
target: null,
mouseIgnoreJob: null,
},
touch: {
x: 0,
y: 0,
id: -1,
scrollDecided: false,
},
};
function firstTouchAction(ev) {
let ta = 'auto';
const path = getComposedPath(ev);
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;
}
if (cancelSyntheticClickEvents) {
// 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, supportsPassive ? { passive: true } : false);
}
/**
* Returns the composedPath for the given event.
* @param {Event} event to process
* @return {!Array<!EventTarget>} Path of the event
*/
const getComposedPath = (event) => (event.composedPath && event.composedPath()) || [];
/** @type {!Object<string, !GestureRecognizer>} */
export const gestures = {};
/** @type {!Array<!GestureRecognizer>} */
export const recognizers = [];
/**
* Finds the element rendered on the screen at the provided coordinates.
*
* Similar to `document.elementFromPoint`, but pierces through
* shadow roots.
*
* @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.
*/
export function deepTargetFind(x, y) {
let node = document.elementFromPoint(x, y);
let next = node;
while (next && next.shadowRoot) {
// if there is a node at x/y in the shadowroot, look deeper
const oldNext = next;
// @ts-ignore
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|Touch} ev Event.
* @return {EventTarget} Returns the event target.
*/
function _findOriginalTarget(ev) {
const path = getComposedPath(/** @type {?Event} */ ev);
// It shouldn't be, but sometimes path is empty (window on Safari).
return path.length > 0 ? path[0] : ev.target;
}
/**
* @private
* @param {Event} ev Event.
* @return {void}
*/
function _handleNative(ev) {
const type = ev.type;
const node = ev.currentTarget;
const gobj = node[GESTURE_KEY];
if (!gobj) {
return;
}
const 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
const 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') {
_handleTouchAction(ev);
}
}
}
}
const handled = ev[HANDLED_OBJ];
// used to ignore synthetic mouse events
if (handled.skip) {
return;
}
// reset recognizer state
for (let i = 0, r; i < recognizers.length; i++) {
r = 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 < recognizers.length; i++) {
r = recognizers[i];
if (gs[r.name] && !handled[r.name]) {
handled[r.name] = true;
r[type](ev);
}
}
}
/**
* @private
* @param {TouchEvent} ev Event.
* @return {void}
*/
function _handleTouchAction(ev) {
const t = ev.changedTouches[0];
const 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;
const ta = firstTouchAction(ev);
let shouldPrevent = false;
const dx = Math.abs(POINTERSTATE.touch.x - t.clientX);
const dy = Math.abs(POINTERSTATE.touch.y - t.clientY);
if (!ev.cancelable) {
// scrolling is happening
}
else if (ta === 'none') {
shouldPrevent = true;
}
else if (ta === 'pan-x') {
shouldPrevent = dy > dx;
}
else if (ta === 'pan-y') {
shouldPrevent = dx > dy;
}
if (shouldPrevent) {
ev.preventDefault();
}
else {
prevent('track');
}
}
}
/**
* Adds an event listener to a node for the given gesture type.
*
* @param {!EventTarget} 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.
*/
export function addListener(node, evType, handler) {
if (gestures[evType]) {
_add(node, evType, handler);
return true;
}
return false;
}
/**
* Removes an event listener from a node for the given gesture type.
*
* @param {!EventTarget} 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.
*/
export function removeListener(node, evType, handler) {
if (gestures[evType]) {
_remove(node, evType, handler);
return true;
}
return false;
}
/**
* automate the event listeners for the native events
*
* @private
* @param {!EventTarget} node Node on which to add the event.
* @param {string} evType Event type to add.
* @param {function(!Event)} handler Event handler function.
* @return {void}
*/
function _add(node, evType, handler) {
const recognizer = gestures[evType];
const deps = recognizer.deps;
const 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, _handleNative, PASSIVE_TOUCH(dep));
}
gd[name] = (gd[name] || 0) + 1;
gd._count = (gd._count || 0) + 1;
}
node.addEventListener(evType, handler);
if (recognizer.touchAction) {
setTouchAction(node, recognizer.touchAction);
}
}
/**
* automate event listener removal for native events
*
* @private
* @param {!EventTarget} node Node on which to remove the event.
* @param {string} evType Event type to remove.
* @param {function(!Event): void} handler Event handler function.
* @return {void}
*/
function _remove(node, evType, handler) {
const recognizer = gestures[evType];
const deps = recognizer.deps;
const name = recognizer.name;
const 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, _handleNative, PASSIVE_TOUCH(dep));
}
}
}
}
node.removeEventListener(evType, handler);
}
/**
* Registers a new gesture event recognizer for adding new custom
* gesture event types.
*
* @param {!GestureRecognizer} recog Gesture recognizer descriptor
* @return {void}
*/
export function register(recog) {
// @ts-ignore
recognizers.push(recog);
for (let i = 0; i < recog.emits.length; i++) {
gestures[recog.emits[i]] = recog;
}
}
/**
* @private
* @param {string} evName Event name.
* @return {Object} Returns the gesture for the given event name.
*/
function _findRecognizerByEvent(evName) {
for (let i = 0, r; i < recognizers.length; i++) {
r = 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.
*
* @param {!EventTarget} node Node to set touch action setting on
* @param {string} value Touch action value
* @return {void}
*/
export function setTouchAction(node, value) {
if (HAS_NATIVE_TA && node instanceof HTMLElement) {
// 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.
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}
*/
function _fire(target, type, detail) {
const ev = new Event(type, { bubbles: true, cancelable: true, composed: true });
// @ts-ignore
ev.detail = detail;
wrap(/** @type {!Node} */ target).dispatchEvent(ev);
// forward `preventDefault` in a clean way
if (ev.defaultPrevented) {
const preventer = detail.preventer || detail.sourceEvent;
if (preventer && preventer.preventDefault) {
preventer.preventDefault();
}
}
}
/**
* Prevents the dispatch and default action of the given event name.
*
* @param {string} evName Event name.
* @return {void}
*/
export function prevent(evName) {
const recognizer = _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.
*
* @return {void}
*/
export function resetMouseCanceller() {
if (POINTERSTATE.mouse.mouseIgnoreJob) {
// @ts-ignore
POINTERSTATE.mouse.mouseIgnoreJob.flush();
}
}
/* eslint-disable valid-jsdoc */
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;
}
const t = _findOriginalTarget(e);
const self = this;
const movefn = function movefn(e) {
if (!hasLeftMouseButton(e)) {
downupFire('up', t, e);
untrackDocument(self.info);
}
};
const upfn = function upfn(e) {
if (hasLeftMouseButton(e)) {
downupFire('up', t, e);
}
untrackDocument(self.info);
};
trackDocument(this.info, movefn, upfn);
downupFire('down', t, e);
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchstart: function (e) {
downupFire('down', _findOriginalTarget(e), e.changedTouches[0], e);
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchend: function (e) {
downupFire('up', _findOriginalTarget(e), e.changedTouches[0], e);
},
});
/**
* @param {string} type
* @param {EventTarget} target
* @param {Event|Touch} event
* @param {Event=} preventer
* @return {void}
*/
function downupFire(type, target, event, preventer) {
if (!target) {
return;
}
_fire(target, type, {
x: event.clientX,
y: event.clientY,
sourceEvent: event,
preventer: preventer,
prevent: function (e) {
return prevent(e);
},
});
}
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 {GestureInfo} */
addMove: function (move) {
if (this.moves.length > TRACK_LENGTH) {
this.moves.shift();
}
// @ts-ignore
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 {MouseEvent} e
* @return {void}
*/
mousedown: function (e) {
if (!hasLeftMouseButton(e)) {
return;
}
const t = _findOriginalTarget(e);
const self = this;
const movefn = function movefn(e) {
const x = e.clientX;
const y = e.clientY;
if (trackHasMovedEnough(self.info, 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
prevent('tap');
}
self.info.addMove({ x: x, y: y });
if (!hasLeftMouseButton(e)) {
// always fire "end"
self.info.state = 'end';
untrackDocument(self.info);
}
if (t) {
trackFire(self.info, t, e);
}
self.info.started = true;
}
};
const 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) {
const ct = e.changedTouches[0];
this.info.x = ct.clientX;
this.info.y = ct.clientY;
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchmove: function (e) {
const t = _findOriginalTarget(e);
const ct = e.changedTouches[0];
const x = ct.clientX;
const y = ct.clientY;
if (trackHasMovedEnough(this.info, x, y)) {
if (this.info.state === 'start') {
// if and only if tracking, always prevent tap
prevent('tap');
}
this.info.addMove({ x: x, y: y });
trackFire(this.info, t, ct);
this.info.state = 'track';
this.info.started = true;
}
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchend: function (e) {
const t = _findOriginalTarget(e);
const 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 });
trackFire(this.info, t, ct);
}
},
});
/**
* @param {!GestureInfo} info
* @param {number} x
* @param {number} y
* @return {boolean}
*/
function trackHasMovedEnough(info, x, y) {
if (info.prevent) {
return false;
}
if (info.started) {
return true;
}
const dx = Math.abs(info.x - x);
const dy = Math.abs(info.y - y);
return dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE;
}
/**
* @param {!GestureInfo} info
* @param {?EventTarget} target
* @param {Touch} touch
* @return {void}
*/
function trackFire(info, target, touch) {
if (!target) {
return;
}
const secondlast = info.moves[info.moves.length - 2];
const lastmove = info.moves[info.moves.length - 1];
const dx = lastmove.x - info.x;
const dy = lastmove.y - info.y;
let ddx;
let ddy = 0;
if (secondlast) {
ddx = lastmove.x - secondlast.x;
ddy = lastmove.y - secondlast.y;
}
_fire(target, 'track', {
state: info.state,
x: touch.clientX,
y: touch.clientY,
dx: dx,
dy: dy,
ddx: ddx,
ddy: ddy,
sourceEvent: touch,
hover: function () {
return deepTargetFind(touch.clientX, touch.clientY);
},
});
}
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}
*/
mousedown: function (e) {
if (hasLeftMouseButton(e)) {
this.info.x = e.clientX;
this.info.y = e.clientY;
}
},
/**
* @this {GestureRecognizer}
* @param {MouseEvent} e
* @return {void}
*/
click: function (e) {
if (hasLeftMouseButton(e)) {
trackForward(this.info, e);
}
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchstart: function (e) {
const touch = e.changedTouches[0];
this.info.x = touch.clientX;
this.info.y = touch.clientY;
},
/**
* @this {GestureRecognizer}
* @param {TouchEvent} e
* @return {void}
*/
touchend: function (e) {
trackForward(this.info, e.changedTouches[0], e);
},
});
/**
* @param {!GestureInfo} info
* @param {Event | Touch} e
* @param {Event=} preventer
* @return {void}
*/
function trackForward(info, e, preventer) {
const dx = Math.abs(e.clientX - info.x);
const dy = Math.abs(e.clientY - info.y);
// find original target from `preventer` for TouchEvents, or `e` for MouseEvents
const t = _findOriginalTarget(preventer || e);
if (!t || (canBeDisabled[ /** @type {!HTMLElement} */t.localName] && t.hasAttribute('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 (!info.prevent) {
_fire(t, 'tap', {
x: e.clientX,
y: e.clientY,
sourceEvent: e,
preventer: preventer,
});
}
}
}
/* eslint-enable valid-jsdoc */
/** @deprecated */
export const findOriginalTarget = _findOriginalTarget;
/** @deprecated */
export const add = addListener;
/** @deprecated */
export const remove = removeListener;
//# sourceMappingURL=gestures.js.map