UNPKG

12.1 kBJavaScriptView Raw
1'use strict';
2
3const gestureController = require('./gesture-controller-29adda71.js');
4
5const addEventListener = (el, eventName, callback, opts) => {
6 // use event listener options when supported
7 // otherwise it's just a boolean for the "capture" arg
8 const listenerOpts = supportsPassive(el) ? {
9 'capture': !!opts.capture,
10 'passive': !!opts.passive,
11 } : !!opts.capture;
12 let add;
13 let remove;
14 if (el['__zone_symbol__addEventListener']) {
15 add = '__zone_symbol__addEventListener';
16 remove = '__zone_symbol__removeEventListener';
17 }
18 else {
19 add = 'addEventListener';
20 remove = 'removeEventListener';
21 }
22 el[add](eventName, callback, listenerOpts);
23 return () => {
24 el[remove](eventName, callback, listenerOpts);
25 };
26};
27const supportsPassive = (node) => {
28 if (_sPassive === undefined) {
29 try {
30 const opts = Object.defineProperty({}, 'passive', {
31 get: () => {
32 _sPassive = true;
33 }
34 });
35 node.addEventListener('optsTest', () => { return; }, opts);
36 }
37 catch (e) {
38 _sPassive = false;
39 }
40 }
41 return !!_sPassive;
42};
43let _sPassive;
44
45const MOUSE_WAIT = 2000;
46const createPointerEvents = (el, pointerDown, pointerMove, pointerUp, options) => {
47 let rmTouchStart;
48 let rmTouchMove;
49 let rmTouchEnd;
50 let rmTouchCancel;
51 let rmMouseStart;
52 let rmMouseMove;
53 let rmMouseUp;
54 let lastTouchEvent = 0;
55 const handleTouchStart = (ev) => {
56 lastTouchEvent = Date.now() + MOUSE_WAIT;
57 if (!pointerDown(ev)) {
58 return;
59 }
60 if (!rmTouchMove && pointerMove) {
61 rmTouchMove = addEventListener(el, 'touchmove', pointerMove, options);
62 }
63 /**
64 * Events are dispatched on the element that is tapped and bubble up to
65 * the reference element in the gesture. In the event that the element this
66 * event was first dispatched on is removed from the DOM, the event will no
67 * longer bubble up to our reference element. This leaves the gesture in an
68 * unusable state. To account for this, the touchend and touchcancel listeners
69 * should be added to the event target so that they still fire even if the target
70 * is removed from the DOM.
71 */
72 if (!rmTouchEnd) {
73 rmTouchEnd = addEventListener(ev.target, 'touchend', handleTouchEnd, options);
74 }
75 if (!rmTouchCancel) {
76 rmTouchCancel = addEventListener(ev.target, 'touchcancel', handleTouchEnd, options);
77 }
78 };
79 const handleMouseDown = (ev) => {
80 if (lastTouchEvent > Date.now()) {
81 return;
82 }
83 if (!pointerDown(ev)) {
84 return;
85 }
86 if (!rmMouseMove && pointerMove) {
87 rmMouseMove = addEventListener(getDocument(el), 'mousemove', pointerMove, options);
88 }
89 if (!rmMouseUp) {
90 rmMouseUp = addEventListener(getDocument(el), 'mouseup', handleMouseUp, options);
91 }
92 };
93 const handleTouchEnd = (ev) => {
94 stopTouch();
95 if (pointerUp) {
96 pointerUp(ev);
97 }
98 };
99 const handleMouseUp = (ev) => {
100 stopMouse();
101 if (pointerUp) {
102 pointerUp(ev);
103 }
104 };
105 const stopTouch = () => {
106 if (rmTouchMove) {
107 rmTouchMove();
108 }
109 if (rmTouchEnd) {
110 rmTouchEnd();
111 }
112 if (rmTouchCancel) {
113 rmTouchCancel();
114 }
115 rmTouchMove = rmTouchEnd = rmTouchCancel = undefined;
116 };
117 const stopMouse = () => {
118 if (rmMouseMove) {
119 rmMouseMove();
120 }
121 if (rmMouseUp) {
122 rmMouseUp();
123 }
124 rmMouseMove = rmMouseUp = undefined;
125 };
126 const stop = () => {
127 stopTouch();
128 stopMouse();
129 };
130 const enable = (isEnabled = true) => {
131 if (!isEnabled) {
132 if (rmTouchStart) {
133 rmTouchStart();
134 }
135 if (rmMouseStart) {
136 rmMouseStart();
137 }
138 rmTouchStart = rmMouseStart = undefined;
139 stop();
140 }
141 else {
142 if (!rmTouchStart) {
143 rmTouchStart = addEventListener(el, 'touchstart', handleTouchStart, options);
144 }
145 if (!rmMouseStart) {
146 rmMouseStart = addEventListener(el, 'mousedown', handleMouseDown, options);
147 }
148 }
149 };
150 const destroy = () => {
151 enable(false);
152 pointerUp = pointerMove = pointerDown = undefined;
153 };
154 return {
155 enable,
156 stop,
157 destroy
158 };
159};
160const getDocument = (node) => {
161 return node instanceof Document ? node : node.ownerDocument;
162};
163
164const createPanRecognizer = (direction, thresh, maxAngle) => {
165 const radians = maxAngle * (Math.PI / 180);
166 const isDirX = direction === 'x';
167 const maxCosine = Math.cos(radians);
168 const threshold = thresh * thresh;
169 let startX = 0;
170 let startY = 0;
171 let dirty = false;
172 let isPan = 0;
173 return {
174 start(x, y) {
175 startX = x;
176 startY = y;
177 isPan = 0;
178 dirty = true;
179 },
180 detect(x, y) {
181 if (!dirty) {
182 return false;
183 }
184 const deltaX = (x - startX);
185 const deltaY = (y - startY);
186 const distance = deltaX * deltaX + deltaY * deltaY;
187 if (distance < threshold) {
188 return false;
189 }
190 const hypotenuse = Math.sqrt(distance);
191 const cosine = (isDirX ? deltaX : deltaY) / hypotenuse;
192 if (cosine > maxCosine) {
193 isPan = 1;
194 }
195 else if (cosine < -maxCosine) {
196 isPan = -1;
197 }
198 else {
199 isPan = 0;
200 }
201 dirty = false;
202 return true;
203 },
204 isGesture() {
205 return isPan !== 0;
206 },
207 getDirection() {
208 return isPan;
209 }
210 };
211};
212
213const createGesture = (config) => {
214 let hasCapturedPan = false;
215 let hasStartedPan = false;
216 let hasFiredStart = true;
217 let isMoveQueued = false;
218 const finalConfig = Object.assign({ disableScroll: false, direction: 'x', gesturePriority: 0, passive: true, maxAngle: 40, threshold: 10 }, config);
219 const canStart = finalConfig.canStart;
220 const onWillStart = finalConfig.onWillStart;
221 const onStart = finalConfig.onStart;
222 const onEnd = finalConfig.onEnd;
223 const notCaptured = finalConfig.notCaptured;
224 const onMove = finalConfig.onMove;
225 const threshold = finalConfig.threshold;
226 const passive = finalConfig.passive;
227 const blurOnStart = finalConfig.blurOnStart;
228 const detail = {
229 type: 'pan',
230 startX: 0,
231 startY: 0,
232 startTime: 0,
233 currentX: 0,
234 currentY: 0,
235 velocityX: 0,
236 velocityY: 0,
237 deltaX: 0,
238 deltaY: 0,
239 currentTime: 0,
240 event: undefined,
241 data: undefined
242 };
243 const pan = createPanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
244 const gesture = gestureController.GESTURE_CONTROLLER.createGesture({
245 name: config.gestureName,
246 priority: config.gesturePriority,
247 disableScroll: config.disableScroll
248 });
249 const pointerDown = (ev) => {
250 const timeStamp = now(ev);
251 if (hasStartedPan || !hasFiredStart) {
252 return false;
253 }
254 updateDetail(ev, detail);
255 detail.startX = detail.currentX;
256 detail.startY = detail.currentY;
257 detail.startTime = detail.currentTime = timeStamp;
258 detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
259 detail.event = ev;
260 // Check if gesture can start
261 if (canStart && canStart(detail) === false) {
262 return false;
263 }
264 // Release fallback
265 gesture.release();
266 // Start gesture
267 if (!gesture.start()) {
268 return false;
269 }
270 hasStartedPan = true;
271 if (threshold === 0) {
272 return tryToCapturePan();
273 }
274 pan.start(detail.startX, detail.startY);
275 return true;
276 };
277 const pointerMove = (ev) => {
278 // fast path, if gesture is currently captured
279 // do minimum job to get user-land even dispatched
280 if (hasCapturedPan) {
281 if (!isMoveQueued && hasFiredStart) {
282 isMoveQueued = true;
283 calcGestureData(detail, ev);
284 requestAnimationFrame(fireOnMove);
285 }
286 return;
287 }
288 // gesture is currently being detected
289 calcGestureData(detail, ev);
290 if (pan.detect(detail.currentX, detail.currentY)) {
291 if (!pan.isGesture() || !tryToCapturePan()) {
292 abortGesture();
293 }
294 }
295 };
296 const fireOnMove = () => {
297 // Since fireOnMove is called inside a RAF, onEnd() might be called,
298 // we must double check hasCapturedPan
299 if (!hasCapturedPan) {
300 return;
301 }
302 isMoveQueued = false;
303 if (onMove) {
304 onMove(detail);
305 }
306 };
307 const tryToCapturePan = () => {
308 if (gesture && !gesture.capture()) {
309 return false;
310 }
311 hasCapturedPan = true;
312 hasFiredStart = false;
313 // reset start position since the real user-land event starts here
314 // If the pan detector threshold is big, not resetting the start position
315 // will cause a jump in the animation equal to the detector threshold.
316 // the array of positions used to calculate the gesture velocity does not
317 // need to be cleaned, more points in the positions array always results in a
318 // more accurate value of the velocity.
319 detail.startX = detail.currentX;
320 detail.startY = detail.currentY;
321 detail.startTime = detail.currentTime;
322 if (onWillStart) {
323 onWillStart(detail).then(fireOnStart);
324 }
325 else {
326 fireOnStart();
327 }
328 return true;
329 };
330 const blurActiveElement = () => {
331 /* tslint:disable-next-line */
332 if (typeof document !== 'undefined') {
333 const activeElement = document.activeElement;
334 if (activeElement !== null && activeElement.blur) {
335 activeElement.blur();
336 }
337 }
338 };
339 const fireOnStart = () => {
340 if (blurOnStart) {
341 blurActiveElement();
342 }
343 if (onStart) {
344 onStart(detail);
345 }
346 hasFiredStart = true;
347 };
348 const reset = () => {
349 hasCapturedPan = false;
350 hasStartedPan = false;
351 isMoveQueued = false;
352 hasFiredStart = true;
353 gesture.release();
354 };
355 // END *************************
356 const pointerUp = (ev) => {
357 const tmpHasCaptured = hasCapturedPan;
358 const tmpHasFiredStart = hasFiredStart;
359 reset();
360 if (!tmpHasFiredStart) {
361 return;
362 }
363 calcGestureData(detail, ev);
364 // Try to capture press
365 if (tmpHasCaptured) {
366 if (onEnd) {
367 onEnd(detail);
368 }
369 return;
370 }
371 // Not captured any event
372 if (notCaptured) {
373 notCaptured(detail);
374 }
375 };
376 const pointerEvents = createPointerEvents(finalConfig.el, pointerDown, pointerMove, pointerUp, {
377 capture: false,
378 passive
379 });
380 const abortGesture = () => {
381 reset();
382 pointerEvents.stop();
383 if (notCaptured) {
384 notCaptured(detail);
385 }
386 };
387 return {
388 enable(enable = true) {
389 if (!enable) {
390 if (hasCapturedPan) {
391 pointerUp(undefined);
392 }
393 reset();
394 }
395 pointerEvents.enable(enable);
396 },
397 destroy() {
398 gesture.destroy();
399 pointerEvents.destroy();
400 }
401 };
402};
403const calcGestureData = (detail, ev) => {
404 if (!ev) {
405 return;
406 }
407 const prevX = detail.currentX;
408 const prevY = detail.currentY;
409 const prevT = detail.currentTime;
410 updateDetail(ev, detail);
411 const currentX = detail.currentX;
412 const currentY = detail.currentY;
413 const timestamp = detail.currentTime = now(ev);
414 const timeDelta = timestamp - prevT;
415 if (timeDelta > 0 && timeDelta < 100) {
416 const velocityX = (currentX - prevX) / timeDelta;
417 const velocityY = (currentY - prevY) / timeDelta;
418 detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
419 detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
420 }
421 detail.deltaX = currentX - detail.startX;
422 detail.deltaY = currentY - detail.startY;
423 detail.event = ev;
424};
425const updateDetail = (ev, detail) => {
426 // get X coordinates for either a mouse click
427 // or a touch depending on the given event
428 let x = 0;
429 let y = 0;
430 if (ev) {
431 const changedTouches = ev.changedTouches;
432 if (changedTouches && changedTouches.length > 0) {
433 const touch = changedTouches[0];
434 x = touch.clientX;
435 y = touch.clientY;
436 }
437 else if (ev.pageX !== undefined) {
438 x = ev.pageX;
439 y = ev.pageY;
440 }
441 }
442 detail.currentX = x;
443 detail.currentY = y;
444};
445const now = (ev) => {
446 return ev.timeStamp || Date.now();
447};
448
449exports.GESTURE_CONTROLLER = gestureController.GESTURE_CONTROLLER;
450exports.createGesture = createGesture;