1 |
|
2 | const React = require('react');
|
3 | const PropTypes = require('prop-types');
|
4 | const DetectPassiveEvents = require('detect-passive-events').default;
|
5 |
|
6 | function getInitialState() {
|
7 | return {
|
8 | x: null,
|
9 | y: null,
|
10 | swiping: false,
|
11 | start: 0,
|
12 | };
|
13 | }
|
14 |
|
15 | function getMovingPosition(e) {
|
16 |
|
17 | return 'changedTouches' in e
|
18 | ? { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }
|
19 | : { x: e.clientX, y: e.clientY };
|
20 | }
|
21 | function getPosition(e) {
|
22 |
|
23 | return 'touches' in e
|
24 | ? { x: e.touches[0].clientX, y: e.touches[0].clientY }
|
25 | : { x: e.clientX, y: e.clientY };
|
26 | }
|
27 |
|
28 | function rotateByAngle(pos, angle) {
|
29 | if (angle === 0) {
|
30 | return pos;
|
31 | }
|
32 |
|
33 | const { x, y } = pos;
|
34 |
|
35 | const angleInRadians = (Math.PI / 180) * angle;
|
36 | const rotatedX = x * Math.cos(angleInRadians) + y * Math.sin(angleInRadians);
|
37 | const rotatedY = y * Math.cos(angleInRadians) - x * Math.sin(angleInRadians);
|
38 | return { x: rotatedX, y: rotatedY };
|
39 | }
|
40 |
|
41 | function calculatePos(e, state) {
|
42 | const { x, y } = rotateByAngle(getMovingPosition(e), state.rotationAngle);
|
43 |
|
44 | const deltaX = state.x - x;
|
45 | const deltaY = state.y - y;
|
46 |
|
47 | const absX = Math.abs(deltaX);
|
48 | const absY = Math.abs(deltaY);
|
49 |
|
50 | const time = Date.now() - state.start;
|
51 | const velocity = Math.sqrt(absX * absX + absY * absY) / time;
|
52 |
|
53 | return { deltaX, deltaY, absX, absY, velocity };
|
54 | }
|
55 |
|
56 | class Swipeable extends React.Component {
|
57 | constructor(props, context) {
|
58 | super(props, context);
|
59 |
|
60 |
|
61 | this.swipeable = getInitialState();
|
62 |
|
63 |
|
64 | this.eventStart = this.eventStart.bind(this);
|
65 | this.eventMove = this.eventMove.bind(this);
|
66 | this.eventEnd = this.eventEnd.bind(this);
|
67 | this.mouseDown = this.mouseDown.bind(this);
|
68 | this.mouseMove = this.mouseMove.bind(this);
|
69 | this.mouseUp = this.mouseUp.bind(this);
|
70 | this.cleanupMouseListeners = this.cleanupMouseListeners.bind(this);
|
71 | this.setupMouseListeners = this.setupMouseListeners.bind(this);
|
72 | this.elementRef = this.elementRef.bind(this);
|
73 | this.setupTouchmoveEvent = this.setupTouchmoveEvent.bind(this);
|
74 | this.cleanupTouchmoveEvent = this.cleanupTouchmoveEvent.bind(this);
|
75 |
|
76 |
|
77 | this.hasPassiveSupport = DetectPassiveEvents.hasSupport;
|
78 | }
|
79 |
|
80 | componentDidMount() {
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | if (this.props.preventDefaultTouchmoveEvent) {
|
86 | this.setupTouchmoveEvent();
|
87 | }
|
88 | }
|
89 |
|
90 | componentDidUpdate(prevProps) {
|
91 |
|
92 | if (prevProps.disabled !== this.props.disabled) {
|
93 | this.cleanupMouseListeners();
|
94 |
|
95 | this.swipeable = getInitialState();
|
96 | }
|
97 |
|
98 |
|
99 | if (prevProps.preventDefaultTouchmoveEvent && !this.props.preventDefaultTouchmoveEvent) {
|
100 | this.cleanupTouchmoveEvent();
|
101 |
|
102 |
|
103 | } else if (!prevProps.preventDefaultTouchmoveEvent && this.props.preventDefaultTouchmoveEvent) {
|
104 | this.setupTouchmoveEvent();
|
105 | }
|
106 | }
|
107 |
|
108 | componentWillUnmount() {
|
109 | this.cleanupMouseListeners();
|
110 | }
|
111 |
|
112 | setupTouchmoveEvent() {
|
113 | if (this.element && this.hasPassiveSupport) {
|
114 | this.element.addEventListener('touchmove', this.eventMove, { passive: false });
|
115 | }
|
116 | }
|
117 |
|
118 | setupMouseListeners() {
|
119 | document.addEventListener('mousemove', this.mouseMove);
|
120 | document.addEventListener('mouseup', this.mouseUp);
|
121 | }
|
122 |
|
123 | cleanupTouchmoveEvent() {
|
124 | if (this.element && this.hasPassiveSupport) {
|
125 | this.element.removeEventListener('touchmove', this.eventMove, { passive: false });
|
126 | }
|
127 | }
|
128 |
|
129 | cleanupMouseListeners() {
|
130 |
|
131 | document.removeEventListener('mousemove', this.mouseMove);
|
132 | document.removeEventListener('mouseup', this.mouseUp);
|
133 | }
|
134 |
|
135 | mouseDown(e) {
|
136 | if (!this.props.trackMouse || e.type !== 'mousedown') {
|
137 | return;
|
138 | }
|
139 |
|
140 |
|
141 | if (typeof this.props.onMouseDown === 'function') this.props.onMouseDown(e);
|
142 |
|
143 |
|
144 | this.setupMouseListeners();
|
145 |
|
146 | this.eventStart(e);
|
147 | }
|
148 |
|
149 | mouseMove(e) {
|
150 | this.eventMove(e);
|
151 | }
|
152 |
|
153 | mouseUp(e) {
|
154 | this.cleanupMouseListeners();
|
155 | this.eventEnd(e);
|
156 | }
|
157 |
|
158 | eventStart(e) {
|
159 |
|
160 | if (e.touches && e.touches.length > 1) return;
|
161 |
|
162 | const { rotationAngle } = this.props;
|
163 | const { x, y } = rotateByAngle(getPosition(e), rotationAngle);
|
164 |
|
165 | if (this.props.stopPropagation) e.stopPropagation();
|
166 |
|
167 | this.swipeable = { start: Date.now(), x, y, swiping: false, rotationAngle };
|
168 | }
|
169 |
|
170 | eventMove(e) {
|
171 | const {
|
172 | stopPropagation,
|
173 | delta,
|
174 | onSwiping, onSwiped,
|
175 | onSwipingLeft, onSwipedLeft,
|
176 | onSwipingRight, onSwipedRight,
|
177 | onSwipingUp, onSwipedUp,
|
178 | onSwipingDown, onSwipedDown,
|
179 | preventDefaultTouchmoveEvent,
|
180 | } = this.props;
|
181 |
|
182 | if (!this.swipeable.x || !this.swipeable.y || e.touches && e.touches.length > 1) {
|
183 | return;
|
184 | }
|
185 |
|
186 | const pos = calculatePos(e, this.swipeable);
|
187 |
|
188 |
|
189 | if (pos.absX < delta && pos.absY < delta && !this.swipeable.swiping) return;
|
190 |
|
191 | if (stopPropagation) e.stopPropagation();
|
192 |
|
193 | if (onSwiping) {
|
194 | onSwiping(e, pos.deltaX, pos.deltaY, pos.absX, pos.absY, pos.velocity);
|
195 | }
|
196 |
|
197 |
|
198 |
|
199 | let cancelablePageSwipe = false;
|
200 | if (onSwiping || onSwiped) {
|
201 | cancelablePageSwipe = true;
|
202 | }
|
203 |
|
204 | if (pos.absX > pos.absY) {
|
205 | if (pos.deltaX > 0) {
|
206 | if (onSwipingLeft || onSwipedLeft) {
|
207 | onSwipingLeft && onSwipingLeft(e, pos.absX);
|
208 | cancelablePageSwipe = true;
|
209 | }
|
210 | } else if (onSwipingRight || onSwipedRight) {
|
211 | onSwipingRight && onSwipingRight(e, pos.absX);
|
212 | cancelablePageSwipe = true;
|
213 | }
|
214 | } else if (pos.deltaY > 0) {
|
215 | if (onSwipingUp || onSwipedUp) {
|
216 | onSwipingUp && onSwipingUp(e, pos.absY);
|
217 | cancelablePageSwipe = true;
|
218 | }
|
219 | } else if (onSwipingDown || onSwipedDown) {
|
220 | onSwipingDown && onSwipingDown(e, pos.absY);
|
221 | cancelablePageSwipe = true;
|
222 | }
|
223 |
|
224 | this.swipeable.swiping = true;
|
225 |
|
226 | if (cancelablePageSwipe && preventDefaultTouchmoveEvent) e.preventDefault();
|
227 | }
|
228 |
|
229 | eventEnd(e) {
|
230 | const {
|
231 | stopPropagation,
|
232 | flickThreshold,
|
233 | onSwiped,
|
234 | onSwipedLeft,
|
235 | onSwipedRight,
|
236 | onSwipedUp,
|
237 | onSwipedDown,
|
238 | onTap,
|
239 | } = this.props;
|
240 |
|
241 | if (this.swipeable.swiping) {
|
242 | const pos = calculatePos(e, this.swipeable);
|
243 |
|
244 | if (stopPropagation) e.stopPropagation();
|
245 |
|
246 | const isFlick = pos.velocity > flickThreshold;
|
247 |
|
248 | onSwiped && onSwiped(e, pos.deltaX, pos.deltaY, isFlick, pos.velocity);
|
249 |
|
250 | if (pos.absX > pos.absY) {
|
251 | if (pos.deltaX > 0) {
|
252 | onSwipedLeft && onSwipedLeft(e, pos.deltaX, isFlick);
|
253 | } else {
|
254 | onSwipedRight && onSwipedRight(e, pos.deltaX, isFlick);
|
255 | }
|
256 | } else if (pos.deltaY > 0) {
|
257 | onSwipedUp && onSwipedUp(e, pos.deltaY, isFlick);
|
258 | } else {
|
259 | onSwipedDown && onSwipedDown(e, pos.deltaY, isFlick);
|
260 | }
|
261 | } else {
|
262 | onTap && onTap(e);
|
263 | }
|
264 |
|
265 |
|
266 | this.swipeable = getInitialState();
|
267 | }
|
268 |
|
269 | elementRef(element) {
|
270 | this.element = element;
|
271 | this.props.innerRef && this.props.innerRef(element);
|
272 | }
|
273 |
|
274 | render() {
|
275 | const newProps = { ...this.props };
|
276 | if (!this.props.disabled) {
|
277 | newProps.onTouchStart = this.eventStart;
|
278 |
|
279 |
|
280 |
|
281 |
|
282 | if (!this.props.preventDefaultTouchmoveEvent || !this.hasPassiveSupport) {
|
283 | newProps.onTouchMove = this.eventMove;
|
284 | }
|
285 |
|
286 | newProps.onTouchEnd = this.eventEnd;
|
287 | newProps.onMouseDown = this.mouseDown;
|
288 | }
|
289 |
|
290 | newProps.ref = this.elementRef;
|
291 |
|
292 |
|
293 | delete newProps.onSwiped;
|
294 | delete newProps.onSwiping;
|
295 | delete newProps.onSwipingUp;
|
296 | delete newProps.onSwipingRight;
|
297 | delete newProps.onSwipingDown;
|
298 | delete newProps.onSwipingLeft;
|
299 | delete newProps.onSwipedUp;
|
300 | delete newProps.onSwipedRight;
|
301 | delete newProps.onSwipedDown;
|
302 | delete newProps.onSwipedLeft;
|
303 | delete newProps.onTap;
|
304 | delete newProps.flickThreshold;
|
305 | delete newProps.delta;
|
306 | delete newProps.preventDefaultTouchmoveEvent;
|
307 | delete newProps.stopPropagation;
|
308 | delete newProps.nodeName;
|
309 | delete newProps.children;
|
310 | delete newProps.trackMouse;
|
311 | delete newProps.disabled;
|
312 | delete newProps.innerRef;
|
313 | delete newProps.rotationAngle;
|
314 |
|
315 | return React.createElement(
|
316 | this.props.nodeName,
|
317 | newProps,
|
318 | this.props.children,
|
319 | );
|
320 | }
|
321 | }
|
322 |
|
323 | Swipeable.propTypes = {
|
324 | onSwiped: PropTypes.func,
|
325 | onSwiping: PropTypes.func,
|
326 | onSwipingUp: PropTypes.func,
|
327 | onSwipingRight: PropTypes.func,
|
328 | onSwipingDown: PropTypes.func,
|
329 | onSwipingLeft: PropTypes.func,
|
330 | onSwipedUp: PropTypes.func,
|
331 | onSwipedRight: PropTypes.func,
|
332 | onSwipedDown: PropTypes.func,
|
333 | onSwipedLeft: PropTypes.func,
|
334 | onTap: PropTypes.func,
|
335 | flickThreshold: PropTypes.number,
|
336 | delta: PropTypes.number,
|
337 | preventDefaultTouchmoveEvent: PropTypes.bool,
|
338 | stopPropagation: PropTypes.bool,
|
339 | nodeName: PropTypes.string,
|
340 | trackMouse: PropTypes.bool,
|
341 | disabled: PropTypes.bool,
|
342 | innerRef: PropTypes.func,
|
343 | children: PropTypes.node,
|
344 | rotationAngle: PropTypes.number,
|
345 | };
|
346 |
|
347 | Swipeable.defaultProps = {
|
348 | flickThreshold: 0.6,
|
349 | delta: 10,
|
350 | preventDefaultTouchmoveEvent: false,
|
351 | stopPropagation: false,
|
352 | nodeName: 'div',
|
353 | disabled: false,
|
354 | rotationAngle: 0,
|
355 | };
|
356 |
|
357 | module.exports = Swipeable;
|