UNPKG

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