@vaadin/component-base 22.0.2 → 23.0.0-alpha4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +3 -2
- package/src/a11y-announcer.d.ts +10 -0
- package/src/a11y-announcer.js +32 -0
- package/src/active-mixin.d.ts +1 -1
- package/src/active-mixin.js +5 -5
- package/src/async.js +8 -5
- package/src/browser-utils.js +1 -1
- package/src/controller-mixin.d.ts +1 -1
- package/src/controller-mixin.js +1 -1
- package/src/debounce.js +10 -4
- package/src/dir-helper.d.ts +1 -1
- package/src/dir-helper.js +3 -2
- package/src/dir-mixin.d.ts +1 -1
- package/src/dir-mixin.js +7 -7
- package/src/disabled-mixin.d.ts +1 -1
- package/src/disabled-mixin.js +1 -1
- package/src/element-mixin.d.ts +1 -1
- package/src/element-mixin.js +2 -2
- package/src/focus-mixin.d.ts +1 -1
- package/src/focus-mixin.js +1 -1
- package/src/focus-trap-controller.d.ts +39 -0
- package/src/focus-trap-controller.js +139 -0
- package/src/focus-utils.d.ts +45 -0
- package/src/focus-utils.js +228 -0
- package/src/gestures.d.ts +76 -0
- package/src/gestures.js +932 -0
- package/src/iron-list-core.js +40 -38
- package/src/keyboard-mixin.d.ts +1 -1
- package/src/keyboard-mixin.js +1 -1
- package/src/resize-mixin.d.ts +19 -0
- package/src/resize-mixin.js +56 -0
- package/src/slot-controller.d.ts +8 -2
- package/src/slot-controller.js +74 -27
- package/src/slot-mixin.d.ts +1 -1
- package/src/slot-mixin.js +11 -3
- package/src/tabindex-mixin.d.ts +1 -1
- package/src/tabindex-mixin.js +1 -1
- package/src/templates.js +1 -1
- package/src/virtualizer-iron-list-adapter.js +5 -6
- package/src/virtualizer.js +6 -6
package/src/gestures.js
ADDED
|
@@ -0,0 +1,932 @@
|
|
|
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.github.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/CONTRIBUTORS.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/PATENTS.txt
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @fileoverview
|
|
13
|
+
*
|
|
14
|
+
* Module for adding listeners to a node for the following normalized
|
|
15
|
+
* cross-platform "gesture" events:
|
|
16
|
+
* - `down` - mouse or touch went down
|
|
17
|
+
* - `up` - mouse or touch went up
|
|
18
|
+
* - `tap` - mouse click or finger tap
|
|
19
|
+
* - `track` - mouse drag or touch move
|
|
20
|
+
*
|
|
21
|
+
* @summary Module for adding cross-platform gesture event listeners.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { microTask } from './async.js';
|
|
25
|
+
|
|
26
|
+
const passiveTouchGestures = false;
|
|
27
|
+
const wrap = (node) => node;
|
|
28
|
+
|
|
29
|
+
// detect native touch action support
|
|
30
|
+
const HAS_NATIVE_TA = typeof document.head.style.touchAction === 'string';
|
|
31
|
+
const GESTURE_KEY = '__polymerGestures';
|
|
32
|
+
const HANDLED_OBJ = '__polymerGesturesHandled';
|
|
33
|
+
const TOUCH_ACTION = '__polymerGesturesTouchAction';
|
|
34
|
+
// radius for tap and track
|
|
35
|
+
const TAP_DISTANCE = 25;
|
|
36
|
+
const TRACK_DISTANCE = 5;
|
|
37
|
+
// number of last N track positions to keep
|
|
38
|
+
const TRACK_LENGTH = 2;
|
|
39
|
+
|
|
40
|
+
const MOUSE_EVENTS = ['mousedown', 'mousemove', 'mouseup', 'click'];
|
|
41
|
+
// an array of bitmask values for mapping MouseEvent.which to MouseEvent.buttons
|
|
42
|
+
const MOUSE_WHICH_TO_BUTTONS = [0, 1, 4, 2];
|
|
43
|
+
const MOUSE_HAS_BUTTONS = (function () {
|
|
44
|
+
try {
|
|
45
|
+
return new MouseEvent('test', { buttons: 1 }).buttons === 1;
|
|
46
|
+
} catch (e) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
})();
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {string} name Possible mouse event name
|
|
53
|
+
* @return {boolean} true if mouse event, false if not
|
|
54
|
+
*/
|
|
55
|
+
function isMouseEvent(name) {
|
|
56
|
+
return MOUSE_EVENTS.indexOf(name) > -1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
|
|
60
|
+
// check for passive event listeners
|
|
61
|
+
let supportsPassive = false;
|
|
62
|
+
(function () {
|
|
63
|
+
try {
|
|
64
|
+
const opts = Object.defineProperty({}, 'passive', {
|
|
65
|
+
// eslint-disable-next-line getter-return
|
|
66
|
+
get() {
|
|
67
|
+
supportsPassive = true;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
window.addEventListener('test', null, opts);
|
|
71
|
+
window.removeEventListener('test', null, opts);
|
|
72
|
+
} catch (e) {}
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Generate settings for event listeners, dependant on `passiveTouchGestures`
|
|
77
|
+
*
|
|
78
|
+
* @param {string} eventName Event name to determine if `{passive}` option is
|
|
79
|
+
* needed
|
|
80
|
+
* @return {{passive: boolean} | undefined} Options to use for addEventListener
|
|
81
|
+
* and removeEventListener
|
|
82
|
+
*/
|
|
83
|
+
function PASSIVE_TOUCH(eventName) {
|
|
84
|
+
if (isMouseEvent(eventName) || eventName === 'touchend') {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (HAS_NATIVE_TA && supportsPassive && passiveTouchGestures) {
|
|
88
|
+
return { passive: true };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Check for touch-only devices
|
|
93
|
+
const IS_TOUCH_ONLY = navigator.userAgent.match(/iP(?:[oa]d|hone)|Android/);
|
|
94
|
+
|
|
95
|
+
// Defined at https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#enabling-and-disabling-form-controls:-the-disabled-attribute
|
|
96
|
+
/** @type {!Object<boolean>} */
|
|
97
|
+
const canBeDisabled = {
|
|
98
|
+
button: true,
|
|
99
|
+
command: true,
|
|
100
|
+
fieldset: true,
|
|
101
|
+
input: true,
|
|
102
|
+
keygen: true,
|
|
103
|
+
optgroup: true,
|
|
104
|
+
option: true,
|
|
105
|
+
select: true,
|
|
106
|
+
textarea: true
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @param {MouseEvent} ev event to test for left mouse button down
|
|
111
|
+
* @return {boolean} has left mouse button down
|
|
112
|
+
*/
|
|
113
|
+
function hasLeftMouseButton(ev) {
|
|
114
|
+
const type = ev.type;
|
|
115
|
+
// exit early if the event is not a mouse event
|
|
116
|
+
if (!isMouseEvent(type)) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
// ev.button is not reliable for mousemove (0 is overloaded as both left button and no buttons)
|
|
120
|
+
// instead we use ev.buttons (bitmask of buttons) or fall back to ev.which (deprecated, 0 for no buttons, 1 for left button)
|
|
121
|
+
if (type === 'mousemove') {
|
|
122
|
+
// allow undefined for testing events
|
|
123
|
+
let buttons = ev.buttons === undefined ? 1 : ev.buttons;
|
|
124
|
+
if (ev instanceof window.MouseEvent && !MOUSE_HAS_BUTTONS) {
|
|
125
|
+
buttons = MOUSE_WHICH_TO_BUTTONS[ev.which] || 0;
|
|
126
|
+
}
|
|
127
|
+
// buttons is a bitmask, check that the left button bit is set (1)
|
|
128
|
+
return Boolean(buttons & 1);
|
|
129
|
+
}
|
|
130
|
+
// allow undefined for testing events
|
|
131
|
+
const button = ev.button === undefined ? 0 : ev.button;
|
|
132
|
+
// ev.button is 0 in mousedown/mouseup/click for left button activation
|
|
133
|
+
return button === 0;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isSyntheticClick(ev) {
|
|
137
|
+
if (ev.type === 'click') {
|
|
138
|
+
// ev.detail is 0 for HTMLElement.click in most browsers
|
|
139
|
+
if (ev.detail === 0) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// in the worst case, check that the x/y position of the click is within
|
|
143
|
+
// the bounding box of the target of the event
|
|
144
|
+
// Thanks IE 10 >:(
|
|
145
|
+
const t = _findOriginalTarget(ev);
|
|
146
|
+
// make sure the target of the event is an element so we can use getBoundingClientRect,
|
|
147
|
+
// if not, just assume it is a synthetic click
|
|
148
|
+
if (!t.nodeType || /** @type {Element} */ (t).nodeType !== Node.ELEMENT_NODE) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
const bcr = /** @type {Element} */ (t).getBoundingClientRect();
|
|
152
|
+
// use page x/y to account for scrolling
|
|
153
|
+
const x = ev.pageX,
|
|
154
|
+
y = ev.pageY;
|
|
155
|
+
// ev is a synthetic click if the position is outside the bounding box of the target
|
|
156
|
+
return !(x >= bcr.left && x <= bcr.right && y >= bcr.top && y <= bcr.bottom);
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const POINTERSTATE = {
|
|
162
|
+
mouse: {
|
|
163
|
+
target: null,
|
|
164
|
+
mouseIgnoreJob: null
|
|
165
|
+
},
|
|
166
|
+
touch: {
|
|
167
|
+
x: 0,
|
|
168
|
+
y: 0,
|
|
169
|
+
id: -1,
|
|
170
|
+
scrollDecided: false
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
function firstTouchAction(ev) {
|
|
175
|
+
let ta = 'auto';
|
|
176
|
+
const path = getComposedPath(ev);
|
|
177
|
+
for (let i = 0, n; i < path.length; i++) {
|
|
178
|
+
n = path[i];
|
|
179
|
+
if (n[TOUCH_ACTION]) {
|
|
180
|
+
ta = n[TOUCH_ACTION];
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return ta;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function trackDocument(stateObj, movefn, upfn) {
|
|
188
|
+
stateObj.movefn = movefn;
|
|
189
|
+
stateObj.upfn = upfn;
|
|
190
|
+
document.addEventListener('mousemove', movefn);
|
|
191
|
+
document.addEventListener('mouseup', upfn);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function untrackDocument(stateObj) {
|
|
195
|
+
document.removeEventListener('mousemove', stateObj.movefn);
|
|
196
|
+
document.removeEventListener('mouseup', stateObj.upfn);
|
|
197
|
+
stateObj.movefn = null;
|
|
198
|
+
stateObj.upfn = null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Returns the composedPath for the given event.
|
|
203
|
+
* @param {Event} event to process
|
|
204
|
+
* @return {!Array<!EventTarget>} Path of the event
|
|
205
|
+
*/
|
|
206
|
+
const getComposedPath =
|
|
207
|
+
window.ShadyDOM && window.ShadyDOM.noPatch
|
|
208
|
+
? window.ShadyDOM.composedPath
|
|
209
|
+
: (event) => (event.composedPath && event.composedPath()) || [];
|
|
210
|
+
|
|
211
|
+
/** @type {!Object<string, !GestureRecognizer>} */
|
|
212
|
+
export const gestures = {};
|
|
213
|
+
|
|
214
|
+
/** @type {!Array<!GestureRecognizer>} */
|
|
215
|
+
export const recognizers = [];
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Finds the element rendered on the screen at the provided coordinates.
|
|
219
|
+
*
|
|
220
|
+
* Similar to `document.elementFromPoint`, but pierces through
|
|
221
|
+
* shadow roots.
|
|
222
|
+
*
|
|
223
|
+
* @param {number} x Horizontal pixel coordinate
|
|
224
|
+
* @param {number} y Vertical pixel coordinate
|
|
225
|
+
* @return {Element} Returns the deepest shadowRoot inclusive element
|
|
226
|
+
* found at the screen position given.
|
|
227
|
+
*/
|
|
228
|
+
export function deepTargetFind(x, y) {
|
|
229
|
+
let node = document.elementFromPoint(x, y);
|
|
230
|
+
let next = node;
|
|
231
|
+
// this code path is only taken when native ShadowDOM is used
|
|
232
|
+
// if there is a shadowroot, it may have a node at x/y
|
|
233
|
+
// if there is not a shadowroot, exit the loop
|
|
234
|
+
while (next && next.shadowRoot && !window.ShadyDOM) {
|
|
235
|
+
// if there is a node at x/y in the shadowroot, look deeper
|
|
236
|
+
const oldNext = next;
|
|
237
|
+
next = next.shadowRoot.elementFromPoint(x, y);
|
|
238
|
+
// on Safari, elementFromPoint may return the shadowRoot host
|
|
239
|
+
if (oldNext === next) {
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
if (next) {
|
|
243
|
+
node = next;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return node;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* a cheaper check than ev.composedPath()[0];
|
|
251
|
+
*
|
|
252
|
+
* @private
|
|
253
|
+
* @param {Event|Touch} ev Event.
|
|
254
|
+
* @return {EventTarget} Returns the event target.
|
|
255
|
+
*/
|
|
256
|
+
function _findOriginalTarget(ev) {
|
|
257
|
+
const path = getComposedPath(/** @type {?Event} */ (ev));
|
|
258
|
+
// It shouldn't be, but sometimes path is empty (window on Safari).
|
|
259
|
+
return path.length > 0 ? path[0] : ev.target;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* @private
|
|
264
|
+
* @param {Event} ev Event.
|
|
265
|
+
* @return {void}
|
|
266
|
+
*/
|
|
267
|
+
function _handleNative(ev) {
|
|
268
|
+
const type = ev.type;
|
|
269
|
+
const node = ev.currentTarget;
|
|
270
|
+
const gobj = node[GESTURE_KEY];
|
|
271
|
+
if (!gobj) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const gs = gobj[type];
|
|
275
|
+
if (!gs) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (!ev[HANDLED_OBJ]) {
|
|
279
|
+
ev[HANDLED_OBJ] = {};
|
|
280
|
+
if (type.slice(0, 5) === 'touch') {
|
|
281
|
+
// ev = /** @type {TouchEvent} */ (ev); // eslint-disable-line no-self-assign
|
|
282
|
+
const t = ev.changedTouches[0];
|
|
283
|
+
if (type === 'touchstart') {
|
|
284
|
+
// only handle the first finger
|
|
285
|
+
if (ev.touches.length === 1) {
|
|
286
|
+
POINTERSTATE.touch.id = t.identifier;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (POINTERSTATE.touch.id !== t.identifier) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (!HAS_NATIVE_TA) {
|
|
293
|
+
if (type === 'touchstart' || type === 'touchmove') {
|
|
294
|
+
_handleTouchAction(ev);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const handled = ev[HANDLED_OBJ];
|
|
300
|
+
// used to ignore synthetic mouse events
|
|
301
|
+
if (handled.skip) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// reset recognizer state
|
|
305
|
+
for (let i = 0, r; i < recognizers.length; i++) {
|
|
306
|
+
r = recognizers[i];
|
|
307
|
+
if (gs[r.name] && !handled[r.name]) {
|
|
308
|
+
if (r.flow && r.flow.start.indexOf(ev.type) > -1 && r.reset) {
|
|
309
|
+
r.reset();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// enforce gesture recognizer order
|
|
314
|
+
for (let i = 0, r; i < recognizers.length; i++) {
|
|
315
|
+
r = recognizers[i];
|
|
316
|
+
if (gs[r.name] && !handled[r.name]) {
|
|
317
|
+
handled[r.name] = true;
|
|
318
|
+
r[type](ev);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @private
|
|
325
|
+
* @param {TouchEvent} ev Event.
|
|
326
|
+
* @return {void}
|
|
327
|
+
*/
|
|
328
|
+
function _handleTouchAction(ev) {
|
|
329
|
+
const t = ev.changedTouches[0];
|
|
330
|
+
const type = ev.type;
|
|
331
|
+
if (type === 'touchstart') {
|
|
332
|
+
POINTERSTATE.touch.x = t.clientX;
|
|
333
|
+
POINTERSTATE.touch.y = t.clientY;
|
|
334
|
+
POINTERSTATE.touch.scrollDecided = false;
|
|
335
|
+
} else if (type === 'touchmove') {
|
|
336
|
+
if (POINTERSTATE.touch.scrollDecided) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
POINTERSTATE.touch.scrollDecided = true;
|
|
340
|
+
const ta = firstTouchAction(ev);
|
|
341
|
+
let shouldPrevent = false;
|
|
342
|
+
const dx = Math.abs(POINTERSTATE.touch.x - t.clientX);
|
|
343
|
+
const dy = Math.abs(POINTERSTATE.touch.y - t.clientY);
|
|
344
|
+
if (!ev.cancelable) {
|
|
345
|
+
// scrolling is happening
|
|
346
|
+
} else if (ta === 'none') {
|
|
347
|
+
shouldPrevent = true;
|
|
348
|
+
} else if (ta === 'pan-x') {
|
|
349
|
+
shouldPrevent = dy > dx;
|
|
350
|
+
} else if (ta === 'pan-y') {
|
|
351
|
+
shouldPrevent = dx > dy;
|
|
352
|
+
}
|
|
353
|
+
if (shouldPrevent) {
|
|
354
|
+
ev.preventDefault();
|
|
355
|
+
} else {
|
|
356
|
+
prevent('track');
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Adds an event listener to a node for the given gesture type.
|
|
363
|
+
*
|
|
364
|
+
* @param {!EventTarget} node Node to add listener on
|
|
365
|
+
* @param {string} evType Gesture type: `down`, `up`, `track`, or `tap`
|
|
366
|
+
* @param {!function(!Event):void} handler Event listener function to call
|
|
367
|
+
* @return {boolean} Returns true if a gesture event listener was added.
|
|
368
|
+
*/
|
|
369
|
+
export function addListener(node, evType, handler) {
|
|
370
|
+
if (gestures[evType]) {
|
|
371
|
+
_add(node, evType, handler);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Removes an event listener from a node for the given gesture type.
|
|
379
|
+
*
|
|
380
|
+
* @param {!EventTarget} node Node to remove listener from
|
|
381
|
+
* @param {string} evType Gesture type: `down`, `up`, `track`, or `tap`
|
|
382
|
+
* @param {!function(!Event):void} handler Event listener function previously passed to
|
|
383
|
+
* `addListener`.
|
|
384
|
+
* @return {boolean} Returns true if a gesture event listener was removed.
|
|
385
|
+
*/
|
|
386
|
+
export function removeListener(node, evType, handler) {
|
|
387
|
+
if (gestures[evType]) {
|
|
388
|
+
_remove(node, evType, handler);
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* automate the event listeners for the native events
|
|
396
|
+
*
|
|
397
|
+
* @private
|
|
398
|
+
* @param {!EventTarget} node Node on which to add the event.
|
|
399
|
+
* @param {string} evType Event type to add.
|
|
400
|
+
* @param {function(!Event)} handler Event handler function.
|
|
401
|
+
* @return {void}
|
|
402
|
+
*/
|
|
403
|
+
function _add(node, evType, handler) {
|
|
404
|
+
const recognizer = gestures[evType];
|
|
405
|
+
const deps = recognizer.deps;
|
|
406
|
+
const name = recognizer.name;
|
|
407
|
+
let gobj = node[GESTURE_KEY];
|
|
408
|
+
if (!gobj) {
|
|
409
|
+
node[GESTURE_KEY] = gobj = {};
|
|
410
|
+
}
|
|
411
|
+
for (let i = 0, dep, gd; i < deps.length; i++) {
|
|
412
|
+
dep = deps[i];
|
|
413
|
+
// don't add mouse handlers on iOS because they cause gray selection overlays
|
|
414
|
+
if (IS_TOUCH_ONLY && isMouseEvent(dep) && dep !== 'click') {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
gd = gobj[dep];
|
|
418
|
+
if (!gd) {
|
|
419
|
+
gobj[dep] = gd = { _count: 0 };
|
|
420
|
+
}
|
|
421
|
+
if (gd._count === 0) {
|
|
422
|
+
node.addEventListener(dep, _handleNative, PASSIVE_TOUCH(dep));
|
|
423
|
+
}
|
|
424
|
+
gd[name] = (gd[name] || 0) + 1;
|
|
425
|
+
gd._count = (gd._count || 0) + 1;
|
|
426
|
+
}
|
|
427
|
+
node.addEventListener(evType, handler);
|
|
428
|
+
if (recognizer.touchAction) {
|
|
429
|
+
setTouchAction(node, recognizer.touchAction);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* automate event listener removal for native events
|
|
435
|
+
*
|
|
436
|
+
* @private
|
|
437
|
+
* @param {!EventTarget} node Node on which to remove the event.
|
|
438
|
+
* @param {string} evType Event type to remove.
|
|
439
|
+
* @param {function(!Event): void} handler Event handler function.
|
|
440
|
+
* @return {void}
|
|
441
|
+
*/
|
|
442
|
+
function _remove(node, evType, handler) {
|
|
443
|
+
const recognizer = gestures[evType];
|
|
444
|
+
const deps = recognizer.deps;
|
|
445
|
+
const name = recognizer.name;
|
|
446
|
+
const gobj = node[GESTURE_KEY];
|
|
447
|
+
if (gobj) {
|
|
448
|
+
for (let i = 0, dep, gd; i < deps.length; i++) {
|
|
449
|
+
dep = deps[i];
|
|
450
|
+
gd = gobj[dep];
|
|
451
|
+
if (gd && gd[name]) {
|
|
452
|
+
gd[name] = (gd[name] || 1) - 1;
|
|
453
|
+
gd._count = (gd._count || 1) - 1;
|
|
454
|
+
if (gd._count === 0) {
|
|
455
|
+
node.removeEventListener(dep, _handleNative, PASSIVE_TOUCH(dep));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
node.removeEventListener(evType, handler);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Registers a new gesture event recognizer for adding new custom
|
|
465
|
+
* gesture event types.
|
|
466
|
+
*
|
|
467
|
+
* @param {!GestureRecognizer} recog Gesture recognizer descriptor
|
|
468
|
+
* @return {void}
|
|
469
|
+
*/
|
|
470
|
+
export function register(recog) {
|
|
471
|
+
recognizers.push(recog);
|
|
472
|
+
for (let i = 0; i < recog.emits.length; i++) {
|
|
473
|
+
gestures[recog.emits[i]] = recog;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* @private
|
|
479
|
+
* @param {string} evName Event name.
|
|
480
|
+
* @return {Object} Returns the gesture for the given event name.
|
|
481
|
+
*/
|
|
482
|
+
function _findRecognizerByEvent(evName) {
|
|
483
|
+
for (let i = 0, r; i < recognizers.length; i++) {
|
|
484
|
+
r = recognizers[i];
|
|
485
|
+
for (let j = 0, n; j < r.emits.length; j++) {
|
|
486
|
+
n = r.emits[j];
|
|
487
|
+
if (n === evName) {
|
|
488
|
+
return r;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Sets scrolling direction on node.
|
|
497
|
+
*
|
|
498
|
+
* This value is checked on first move, thus it should be called prior to
|
|
499
|
+
* adding event listeners.
|
|
500
|
+
*
|
|
501
|
+
* @param {!EventTarget} node Node to set touch action setting on
|
|
502
|
+
* @param {string} value Touch action value
|
|
503
|
+
* @return {void}
|
|
504
|
+
*/
|
|
505
|
+
export function setTouchAction(node, value) {
|
|
506
|
+
if (HAS_NATIVE_TA && node instanceof HTMLElement) {
|
|
507
|
+
// NOTE: add touchAction async so that events can be added in
|
|
508
|
+
// custom element constructors. Otherwise we run afoul of custom
|
|
509
|
+
// elements restriction against settings attributes (style) in the
|
|
510
|
+
// constructor.
|
|
511
|
+
microTask.run(() => {
|
|
512
|
+
node.style.touchAction = value;
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
node[TOUCH_ACTION] = value;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* Dispatches an event on the `target` element of `type` with the given
|
|
520
|
+
* `detail`.
|
|
521
|
+
* @private
|
|
522
|
+
* @param {!EventTarget} target The element on which to fire an event.
|
|
523
|
+
* @param {string} type The type of event to fire.
|
|
524
|
+
* @param {!Object=} detail The detail object to populate on the event.
|
|
525
|
+
* @return {void}
|
|
526
|
+
*/
|
|
527
|
+
function _fire(target, type, detail) {
|
|
528
|
+
const ev = new Event(type, { bubbles: true, cancelable: true, composed: true });
|
|
529
|
+
ev.detail = detail;
|
|
530
|
+
wrap(/** @type {!Node} */ (target)).dispatchEvent(ev);
|
|
531
|
+
// forward `preventDefault` in a clean way
|
|
532
|
+
if (ev.defaultPrevented) {
|
|
533
|
+
const preventer = detail.preventer || detail.sourceEvent;
|
|
534
|
+
if (preventer && preventer.preventDefault) {
|
|
535
|
+
preventer.preventDefault();
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Prevents the dispatch and default action of the given event name.
|
|
542
|
+
*
|
|
543
|
+
* @param {string} evName Event name.
|
|
544
|
+
* @return {void}
|
|
545
|
+
*/
|
|
546
|
+
export function prevent(evName) {
|
|
547
|
+
const recognizer = _findRecognizerByEvent(evName);
|
|
548
|
+
if (recognizer.info) {
|
|
549
|
+
recognizer.info.prevent = true;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
register({
|
|
554
|
+
name: 'downup',
|
|
555
|
+
deps: ['mousedown', 'touchstart', 'touchend'],
|
|
556
|
+
flow: {
|
|
557
|
+
start: ['mousedown', 'touchstart'],
|
|
558
|
+
end: ['mouseup', 'touchend']
|
|
559
|
+
},
|
|
560
|
+
emits: ['down', 'up'],
|
|
561
|
+
|
|
562
|
+
info: {
|
|
563
|
+
movefn: null,
|
|
564
|
+
upfn: null
|
|
565
|
+
},
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* @this {GestureRecognizer}
|
|
569
|
+
* @return {void}
|
|
570
|
+
*/
|
|
571
|
+
reset: function () {
|
|
572
|
+
untrackDocument(this.info);
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* @this {GestureRecognizer}
|
|
577
|
+
* @param {MouseEvent} e
|
|
578
|
+
* @return {void}
|
|
579
|
+
*/
|
|
580
|
+
mousedown: function (e) {
|
|
581
|
+
if (!hasLeftMouseButton(e)) {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const t = _findOriginalTarget(e);
|
|
585
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
586
|
+
const self = this;
|
|
587
|
+
const movefn = (e) => {
|
|
588
|
+
if (!hasLeftMouseButton(e)) {
|
|
589
|
+
downupFire('up', t, e);
|
|
590
|
+
untrackDocument(self.info);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
const upfn = (e) => {
|
|
594
|
+
if (hasLeftMouseButton(e)) {
|
|
595
|
+
downupFire('up', t, e);
|
|
596
|
+
}
|
|
597
|
+
untrackDocument(self.info);
|
|
598
|
+
};
|
|
599
|
+
trackDocument(this.info, movefn, upfn);
|
|
600
|
+
downupFire('down', t, e);
|
|
601
|
+
},
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* @this {GestureRecognizer}
|
|
605
|
+
* @param {TouchEvent} e
|
|
606
|
+
* @return {void}
|
|
607
|
+
*/
|
|
608
|
+
touchstart: function (e) {
|
|
609
|
+
downupFire('down', _findOriginalTarget(e), e.changedTouches[0], e);
|
|
610
|
+
},
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* @this {GestureRecognizer}
|
|
614
|
+
* @param {TouchEvent} e
|
|
615
|
+
* @return {void}
|
|
616
|
+
*/
|
|
617
|
+
touchend: function (e) {
|
|
618
|
+
downupFire('up', _findOriginalTarget(e), e.changedTouches[0], e);
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* @param {string} type
|
|
624
|
+
* @param {EventTarget} target
|
|
625
|
+
* @param {Event|Touch} event
|
|
626
|
+
* @param {Event=} preventer
|
|
627
|
+
* @return {void}
|
|
628
|
+
*/
|
|
629
|
+
function downupFire(type, target, event, preventer) {
|
|
630
|
+
if (!target) {
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
_fire(target, type, {
|
|
634
|
+
x: event.clientX,
|
|
635
|
+
y: event.clientY,
|
|
636
|
+
sourceEvent: event,
|
|
637
|
+
preventer: preventer,
|
|
638
|
+
prevent: function (e) {
|
|
639
|
+
return prevent(e);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
register({
|
|
645
|
+
name: 'track',
|
|
646
|
+
touchAction: 'none',
|
|
647
|
+
deps: ['mousedown', 'touchstart', 'touchmove', 'touchend'],
|
|
648
|
+
flow: {
|
|
649
|
+
start: ['mousedown', 'touchstart'],
|
|
650
|
+
end: ['mouseup', 'touchend']
|
|
651
|
+
},
|
|
652
|
+
emits: ['track'],
|
|
653
|
+
|
|
654
|
+
info: {
|
|
655
|
+
x: 0,
|
|
656
|
+
y: 0,
|
|
657
|
+
state: 'start',
|
|
658
|
+
started: false,
|
|
659
|
+
moves: [],
|
|
660
|
+
/** @this {GestureInfo} */
|
|
661
|
+
addMove: function (move) {
|
|
662
|
+
if (this.moves.length > TRACK_LENGTH) {
|
|
663
|
+
this.moves.shift();
|
|
664
|
+
}
|
|
665
|
+
this.moves.push(move);
|
|
666
|
+
},
|
|
667
|
+
movefn: null,
|
|
668
|
+
upfn: null,
|
|
669
|
+
prevent: false
|
|
670
|
+
},
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* @this {GestureRecognizer}
|
|
674
|
+
* @return {void}
|
|
675
|
+
*/
|
|
676
|
+
reset: function () {
|
|
677
|
+
this.info.state = 'start';
|
|
678
|
+
this.info.started = false;
|
|
679
|
+
this.info.moves = [];
|
|
680
|
+
this.info.x = 0;
|
|
681
|
+
this.info.y = 0;
|
|
682
|
+
this.info.prevent = false;
|
|
683
|
+
untrackDocument(this.info);
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* @this {GestureRecognizer}
|
|
688
|
+
* @param {MouseEvent} e
|
|
689
|
+
* @return {void}
|
|
690
|
+
*/
|
|
691
|
+
mousedown: function (e) {
|
|
692
|
+
if (!hasLeftMouseButton(e)) {
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
const t = _findOriginalTarget(e);
|
|
696
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
697
|
+
const self = this;
|
|
698
|
+
const movefn = (e) => {
|
|
699
|
+
const x = e.clientX,
|
|
700
|
+
y = e.clientY;
|
|
701
|
+
if (trackHasMovedEnough(self.info, x, y)) {
|
|
702
|
+
// first move is 'start', subsequent moves are 'move', mouseup is 'end'
|
|
703
|
+
self.info.state = self.info.started ? (e.type === 'mouseup' ? 'end' : 'track') : 'start';
|
|
704
|
+
if (self.info.state === 'start') {
|
|
705
|
+
// if and only if tracking, always prevent tap
|
|
706
|
+
prevent('tap');
|
|
707
|
+
}
|
|
708
|
+
self.info.addMove({ x: x, y: y });
|
|
709
|
+
if (!hasLeftMouseButton(e)) {
|
|
710
|
+
// always fire "end"
|
|
711
|
+
self.info.state = 'end';
|
|
712
|
+
untrackDocument(self.info);
|
|
713
|
+
}
|
|
714
|
+
if (t) {
|
|
715
|
+
trackFire(self.info, t, e);
|
|
716
|
+
}
|
|
717
|
+
self.info.started = true;
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
const upfn = (e) => {
|
|
721
|
+
if (self.info.started) {
|
|
722
|
+
movefn(e);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// remove the temporary listeners
|
|
726
|
+
untrackDocument(self.info);
|
|
727
|
+
};
|
|
728
|
+
// add temporary document listeners as mouse retargets
|
|
729
|
+
trackDocument(this.info, movefn, upfn);
|
|
730
|
+
this.info.x = e.clientX;
|
|
731
|
+
this.info.y = e.clientY;
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* @this {GestureRecognizer}
|
|
736
|
+
* @param {TouchEvent} e
|
|
737
|
+
* @return {void}
|
|
738
|
+
*/
|
|
739
|
+
touchstart: function (e) {
|
|
740
|
+
const ct = e.changedTouches[0];
|
|
741
|
+
this.info.x = ct.clientX;
|
|
742
|
+
this.info.y = ct.clientY;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* @this {GestureRecognizer}
|
|
747
|
+
* @param {TouchEvent} e
|
|
748
|
+
* @return {void}
|
|
749
|
+
*/
|
|
750
|
+
touchmove: function (e) {
|
|
751
|
+
const t = _findOriginalTarget(e);
|
|
752
|
+
const ct = e.changedTouches[0];
|
|
753
|
+
const x = ct.clientX,
|
|
754
|
+
y = ct.clientY;
|
|
755
|
+
if (trackHasMovedEnough(this.info, x, y)) {
|
|
756
|
+
if (this.info.state === 'start') {
|
|
757
|
+
// if and only if tracking, always prevent tap
|
|
758
|
+
prevent('tap');
|
|
759
|
+
}
|
|
760
|
+
this.info.addMove({ x: x, y: y });
|
|
761
|
+
trackFire(this.info, t, ct);
|
|
762
|
+
this.info.state = 'track';
|
|
763
|
+
this.info.started = true;
|
|
764
|
+
}
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* @this {GestureRecognizer}
|
|
769
|
+
* @param {TouchEvent} e
|
|
770
|
+
* @return {void}
|
|
771
|
+
*/
|
|
772
|
+
touchend: function (e) {
|
|
773
|
+
const t = _findOriginalTarget(e);
|
|
774
|
+
const ct = e.changedTouches[0];
|
|
775
|
+
// only trackend if track was started and not aborted
|
|
776
|
+
if (this.info.started) {
|
|
777
|
+
// reset started state on up
|
|
778
|
+
this.info.state = 'end';
|
|
779
|
+
this.info.addMove({ x: ct.clientX, y: ct.clientY });
|
|
780
|
+
trackFire(this.info, t, ct);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* @param {!GestureInfo} info
|
|
787
|
+
* @param {number} x
|
|
788
|
+
* @param {number} y
|
|
789
|
+
* @return {boolean}
|
|
790
|
+
*/
|
|
791
|
+
function trackHasMovedEnough(info, x, y) {
|
|
792
|
+
if (info.prevent) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
if (info.started) {
|
|
796
|
+
return true;
|
|
797
|
+
}
|
|
798
|
+
const dx = Math.abs(info.x - x);
|
|
799
|
+
const dy = Math.abs(info.y - y);
|
|
800
|
+
return dx >= TRACK_DISTANCE || dy >= TRACK_DISTANCE;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @param {!GestureInfo} info
|
|
805
|
+
* @param {?EventTarget} target
|
|
806
|
+
* @param {Touch} touch
|
|
807
|
+
* @return {void}
|
|
808
|
+
*/
|
|
809
|
+
function trackFire(info, target, touch) {
|
|
810
|
+
if (!target) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const secondlast = info.moves[info.moves.length - 2];
|
|
814
|
+
const lastmove = info.moves[info.moves.length - 1];
|
|
815
|
+
const dx = lastmove.x - info.x;
|
|
816
|
+
const dy = lastmove.y - info.y;
|
|
817
|
+
let ddx,
|
|
818
|
+
ddy = 0;
|
|
819
|
+
if (secondlast) {
|
|
820
|
+
ddx = lastmove.x - secondlast.x;
|
|
821
|
+
ddy = lastmove.y - secondlast.y;
|
|
822
|
+
}
|
|
823
|
+
_fire(target, 'track', {
|
|
824
|
+
state: info.state,
|
|
825
|
+
x: touch.clientX,
|
|
826
|
+
y: touch.clientY,
|
|
827
|
+
dx: dx,
|
|
828
|
+
dy: dy,
|
|
829
|
+
ddx: ddx,
|
|
830
|
+
ddy: ddy,
|
|
831
|
+
sourceEvent: touch,
|
|
832
|
+
hover: function () {
|
|
833
|
+
return deepTargetFind(touch.clientX, touch.clientY);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
register({
|
|
839
|
+
name: 'tap',
|
|
840
|
+
deps: ['mousedown', 'click', 'touchstart', 'touchend'],
|
|
841
|
+
flow: {
|
|
842
|
+
start: ['mousedown', 'touchstart'],
|
|
843
|
+
end: ['click', 'touchend']
|
|
844
|
+
},
|
|
845
|
+
emits: ['tap'],
|
|
846
|
+
info: {
|
|
847
|
+
x: NaN,
|
|
848
|
+
y: NaN,
|
|
849
|
+
prevent: false
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* @this {GestureRecognizer}
|
|
854
|
+
* @return {void}
|
|
855
|
+
*/
|
|
856
|
+
reset: function () {
|
|
857
|
+
this.info.x = NaN;
|
|
858
|
+
this.info.y = NaN;
|
|
859
|
+
this.info.prevent = false;
|
|
860
|
+
},
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @this {GestureRecognizer}
|
|
864
|
+
* @param {MouseEvent} e
|
|
865
|
+
* @return {void}
|
|
866
|
+
*/
|
|
867
|
+
mousedown: function (e) {
|
|
868
|
+
if (hasLeftMouseButton(e)) {
|
|
869
|
+
this.info.x = e.clientX;
|
|
870
|
+
this.info.y = e.clientY;
|
|
871
|
+
}
|
|
872
|
+
},
|
|
873
|
+
|
|
874
|
+
/**
|
|
875
|
+
* @this {GestureRecognizer}
|
|
876
|
+
* @param {MouseEvent} e
|
|
877
|
+
* @return {void}
|
|
878
|
+
*/
|
|
879
|
+
click: function (e) {
|
|
880
|
+
if (hasLeftMouseButton(e)) {
|
|
881
|
+
trackForward(this.info, e);
|
|
882
|
+
}
|
|
883
|
+
},
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* @this {GestureRecognizer}
|
|
887
|
+
* @param {TouchEvent} e
|
|
888
|
+
* @return {void}
|
|
889
|
+
*/
|
|
890
|
+
touchstart: function (e) {
|
|
891
|
+
const touch = e.changedTouches[0];
|
|
892
|
+
this.info.x = touch.clientX;
|
|
893
|
+
this.info.y = touch.clientY;
|
|
894
|
+
},
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* @this {GestureRecognizer}
|
|
898
|
+
* @param {TouchEvent} e
|
|
899
|
+
* @return {void}
|
|
900
|
+
*/
|
|
901
|
+
touchend: function (e) {
|
|
902
|
+
trackForward(this.info, e.changedTouches[0], e);
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* @param {!GestureInfo} info
|
|
908
|
+
* @param {Event | Touch} e
|
|
909
|
+
* @param {Event=} preventer
|
|
910
|
+
* @return {void}
|
|
911
|
+
*/
|
|
912
|
+
function trackForward(info, e, preventer) {
|
|
913
|
+
const dx = Math.abs(e.clientX - info.x);
|
|
914
|
+
const dy = Math.abs(e.clientY - info.y);
|
|
915
|
+
// find original target from `preventer` for TouchEvents, or `e` for MouseEvents
|
|
916
|
+
const t = _findOriginalTarget(preventer || e);
|
|
917
|
+
if (!t || (canBeDisabled[/** @type {!HTMLElement} */ (t).localName] && t.hasAttribute('disabled'))) {
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
// dx,dy can be NaN if `click` has been simulated and there was no `down` for `start`
|
|
921
|
+
if (isNaN(dx) || isNaN(dy) || (dx <= TAP_DISTANCE && dy <= TAP_DISTANCE) || isSyntheticClick(e)) {
|
|
922
|
+
// prevent taps from being generated if an event has canceled them
|
|
923
|
+
if (!info.prevent) {
|
|
924
|
+
_fire(t, 'tap', {
|
|
925
|
+
x: e.clientX,
|
|
926
|
+
y: e.clientY,
|
|
927
|
+
sourceEvent: e,
|
|
928
|
+
preventer: preventer
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|