1 |
|
2 | import * as React from "react";
|
3 | import {
|
4 | AttachTouch,
|
5 | SwipeDirections,
|
6 | DOWN,
|
7 | SwipeEventData,
|
8 | HandledEvents,
|
9 | LEFT,
|
10 | RIGHT,
|
11 | Setter,
|
12 | SwipeableHandlers,
|
13 | SwipeableProps,
|
14 | SwipeablePropsWithDefaultOptions,
|
15 | SwipeableState,
|
16 | SwipeCallback,
|
17 | TapCallback,
|
18 | UP,
|
19 | Vector2,
|
20 | } from "./types";
|
21 |
|
22 | export {
|
23 | LEFT,
|
24 | RIGHT,
|
25 | UP,
|
26 | DOWN,
|
27 | SwipeDirections,
|
28 | SwipeEventData,
|
29 | SwipeCallback,
|
30 | TapCallback,
|
31 | SwipeableHandlers,
|
32 | SwipeableProps,
|
33 | Vector2,
|
34 | };
|
35 |
|
36 | const defaultProps = {
|
37 | delta: 10,
|
38 | preventDefaultTouchmoveEvent: false,
|
39 | rotationAngle: 0,
|
40 | trackMouse: false,
|
41 | trackTouch: true,
|
42 | };
|
43 | const initialState: SwipeableState = {
|
44 | first: true,
|
45 | initial: [0, 0],
|
46 | start: 0,
|
47 | swiping: false,
|
48 | xy: [0, 0],
|
49 | };
|
50 | const mouseMove = "mousemove";
|
51 | const mouseUp = "mouseup";
|
52 | const touchEnd = "touchend";
|
53 | const touchMove = "touchmove";
|
54 | const touchStart = "touchstart";
|
55 |
|
56 | function getDirection(
|
57 | absX: number,
|
58 | absY: number,
|
59 | deltaX: number,
|
60 | deltaY: number
|
61 | ): SwipeDirections {
|
62 | if (absX > absY) {
|
63 | if (deltaX > 0) {
|
64 | return RIGHT;
|
65 | }
|
66 | return LEFT;
|
67 | } else if (deltaY > 0) {
|
68 | return DOWN;
|
69 | }
|
70 | return UP;
|
71 | }
|
72 |
|
73 | function rotateXYByAngle(pos: Vector2, angle: number): Vector2 {
|
74 | if (angle === 0) return pos;
|
75 | const angleInRadians = (Math.PI / 180) * angle;
|
76 | const x =
|
77 | pos[0] * Math.cos(angleInRadians) + pos[1] * Math.sin(angleInRadians);
|
78 | const y =
|
79 | pos[1] * Math.cos(angleInRadians) - pos[0] * Math.sin(angleInRadians);
|
80 | return [x, y];
|
81 | }
|
82 |
|
83 | function getHandlers(
|
84 | set: Setter,
|
85 | handlerProps: { trackMouse: boolean | undefined }
|
86 | ): [
|
87 | {
|
88 | ref: (element: HTMLElement | null) => void;
|
89 | onMouseDown?: (event: React.MouseEvent) => void;
|
90 | },
|
91 | AttachTouch
|
92 | ] {
|
93 | const onStart = (event: HandledEvents) => {
|
94 |
|
95 | if (event && "touches" in event && event.touches.length > 1) return;
|
96 |
|
97 | set((state, props) => {
|
98 |
|
99 | if (props.trackMouse) {
|
100 | document.addEventListener(mouseMove, onMove);
|
101 | document.addEventListener(mouseUp, onUp);
|
102 | }
|
103 | const { clientX, clientY } =
|
104 | "touches" in event ? event.touches[0] : event;
|
105 | const xy = rotateXYByAngle([clientX, clientY], props.rotationAngle);
|
106 | return {
|
107 | ...state,
|
108 | ...initialState,
|
109 | initial: [...xy],
|
110 | xy,
|
111 | start: event.timeStamp || 0,
|
112 | };
|
113 | });
|
114 | };
|
115 |
|
116 | const onMove = (event: HandledEvents) => {
|
117 | set((state, props) => {
|
118 |
|
119 |
|
120 | if ("touches" in event && event.touches.length > 1) {
|
121 | return state;
|
122 | }
|
123 | const { clientX, clientY } =
|
124 | "touches" in event ? event.touches[0] : event;
|
125 | const [x, y] = rotateXYByAngle([clientX, clientY], props.rotationAngle);
|
126 | const deltaX = x - state.xy[0];
|
127 | const deltaY = y - state.xy[1];
|
128 | const absX = Math.abs(deltaX);
|
129 | const absY = Math.abs(deltaY);
|
130 | const time = (event.timeStamp || 0) - state.start;
|
131 | const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1);
|
132 | const vxvy: Vector2 = [deltaX / (time || 1), deltaY / (time || 1)];
|
133 |
|
134 |
|
135 | if (absX < props.delta && absY < props.delta && !state.swiping)
|
136 | return state;
|
137 |
|
138 | const dir = getDirection(absX, absY, deltaX, deltaY);
|
139 | const eventData = {
|
140 | absX,
|
141 | absY,
|
142 | deltaX,
|
143 | deltaY,
|
144 | dir,
|
145 | event,
|
146 | first: state.first,
|
147 | initial: state.initial,
|
148 | velocity,
|
149 | vxvy,
|
150 | };
|
151 |
|
152 |
|
153 | eventData.first && props.onSwipeStart && props.onSwipeStart(eventData);
|
154 |
|
155 |
|
156 | props.onSwiping && props.onSwiping(eventData);
|
157 |
|
158 |
|
159 |
|
160 | let cancelablePageSwipe = false;
|
161 | if (props.onSwiping || props.onSwiped || `onSwiped${dir}` in props) {
|
162 | cancelablePageSwipe = true;
|
163 | }
|
164 |
|
165 | if (
|
166 | cancelablePageSwipe &&
|
167 | props.preventDefaultTouchmoveEvent &&
|
168 | props.trackTouch &&
|
169 | event.cancelable
|
170 | )
|
171 | event.preventDefault();
|
172 |
|
173 | return {
|
174 | ...state,
|
175 |
|
176 | first: false,
|
177 | eventData,
|
178 | swiping: true,
|
179 | };
|
180 | });
|
181 | };
|
182 |
|
183 | const onEnd = (event: HandledEvents) => {
|
184 | set((state, props) => {
|
185 | let eventData: SwipeEventData | undefined;
|
186 | if (state.swiping && state.eventData) {
|
187 | eventData = { ...state.eventData, event };
|
188 | props.onSwiped && props.onSwiped(eventData);
|
189 |
|
190 | const onSwipedDir = `onSwiped${eventData.dir}`;
|
191 | if (onSwipedDir in props) {
|
192 | ((props as any)[onSwipedDir] as SwipeCallback)(eventData);
|
193 | }
|
194 | } else {
|
195 | props.onTap && props.onTap({ event });
|
196 | }
|
197 | return { ...state, ...initialState, eventData };
|
198 | });
|
199 | };
|
200 |
|
201 | const cleanUpMouse = () => {
|
202 |
|
203 | document.removeEventListener(mouseMove, onMove);
|
204 | document.removeEventListener(mouseUp, onUp);
|
205 | };
|
206 |
|
207 | const onUp = (e: HandledEvents) => {
|
208 | cleanUpMouse();
|
209 | onEnd(e);
|
210 | };
|
211 |
|
212 | |
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | const attachTouch: AttachTouch = (el, passive) => {
|
221 | let cleanup = () => {};
|
222 | if (el && el.addEventListener) {
|
223 |
|
224 | const tls: [
|
225 | typeof touchStart | typeof touchMove | typeof touchEnd,
|
226 | (e: HandledEvents) => void
|
227 | ][] = [
|
228 | [touchStart, onStart],
|
229 | [touchMove, onMove],
|
230 | [touchEnd, onEnd],
|
231 | ];
|
232 | tls.forEach(([e, h]) => el.addEventListener(e, h, { passive }));
|
233 |
|
234 | cleanup = () => tls.forEach(([e, h]) => el.removeEventListener(e, h));
|
235 | }
|
236 | return cleanup;
|
237 | };
|
238 |
|
239 | const onRef = (el: HTMLElement | null) => {
|
240 |
|
241 |
|
242 | if (el === null) return;
|
243 | set((state, props) => {
|
244 |
|
245 | if (state.el === el) return state;
|
246 |
|
247 | const addState: { cleanUpTouch?: () => void } = {};
|
248 |
|
249 | if (state.el && state.el !== el && state.cleanUpTouch) {
|
250 | state.cleanUpTouch();
|
251 | addState.cleanUpTouch = undefined;
|
252 | }
|
253 |
|
254 | if (props.trackTouch && el) {
|
255 | addState.cleanUpTouch = attachTouch(
|
256 | el,
|
257 | !props.preventDefaultTouchmoveEvent
|
258 | );
|
259 | }
|
260 |
|
261 |
|
262 | return { ...state, el, ...addState };
|
263 | });
|
264 | };
|
265 |
|
266 |
|
267 | const output: { ref: typeof onRef; onMouseDown?: typeof onStart } = {
|
268 | ref: onRef,
|
269 | };
|
270 |
|
271 |
|
272 | if (handlerProps.trackMouse) {
|
273 | output.onMouseDown = onStart;
|
274 | }
|
275 |
|
276 | return [output, attachTouch];
|
277 | }
|
278 |
|
279 | function updateTransientState(
|
280 | state: SwipeableState,
|
281 | props: SwipeableProps,
|
282 | attachTouch: AttachTouch
|
283 | ) {
|
284 | const addState: { cleanUpTouch?(): void } = {};
|
285 |
|
286 | if (!props.trackTouch && state.cleanUpTouch) {
|
287 | state.cleanUpTouch();
|
288 | addState.cleanUpTouch = undefined;
|
289 | } else if (props.trackTouch && !state.cleanUpTouch) {
|
290 |
|
291 | if (state.el) {
|
292 | addState.cleanUpTouch = attachTouch(
|
293 | state.el,
|
294 | !props.preventDefaultTouchmoveEvent
|
295 | );
|
296 | }
|
297 | }
|
298 | return { ...state, ...addState };
|
299 | }
|
300 |
|
301 | export function useSwipeable(options: SwipeableProps): SwipeableHandlers {
|
302 | const { trackMouse } = options;
|
303 | const transientState = React.useRef({ ...initialState });
|
304 | const transientProps = React.useRef<SwipeablePropsWithDefaultOptions>({
|
305 | ...defaultProps,
|
306 | });
|
307 | transientProps.current = { ...defaultProps, ...options };
|
308 |
|
309 | const [handlers, attachTouch] = React.useMemo(
|
310 | () =>
|
311 | getHandlers(
|
312 | (stateSetter) =>
|
313 | (transientState.current = stateSetter(
|
314 | transientState.current,
|
315 | transientProps.current
|
316 | )),
|
317 | { trackMouse }
|
318 | ),
|
319 | [trackMouse]
|
320 | );
|
321 |
|
322 | transientState.current = updateTransientState(
|
323 | transientState.current,
|
324 | transientProps.current,
|
325 | attachTouch
|
326 | );
|
327 |
|
328 | return handlers;
|
329 | }
|