UNPKG

10.5 kBJavaScriptView Raw
1/* global document */
2const React = require('react');
3const PropTypes = require('prop-types');
4const DetectPassiveEvents = require('detect-passive-events').default;
5
6function getInitialState() {
7 return {
8 x: null,
9 y: null,
10 swiping: false,
11 start: 0,
12 };
13}
14
15function getMovingPosition(e) {
16 // If not a touch, determine point from mouse coordinates
17 return 'changedTouches' in e
18 ? { x: e.changedTouches[0].clientX, y: e.changedTouches[0].clientY }
19 : { x: e.clientX, y: e.clientY };
20}
21function getPosition(e) {
22 // If not a touch, determine point from mouse coordinates
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
28function 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
41function 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
56class Swipeable extends React.Component {
57 constructor(props, context) {
58 super(props, context);
59
60 // setup internal swipeable state
61 this.swipeable = getInitialState();
62
63 // bind this context for internal methods
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 // check for passive event support
77 this.hasPassiveSupport = DetectPassiveEvents.hasSupport;
78 }
79
80 componentDidMount() {
81 // if we care about calling preventDefault and we have support for passive events
82 // we need to setup a custom event listener, sigh...
83 // there are a few jump ropes within the code for this case,
84 // but it is the best we can do to allow preventDefault for chrome 56+
85 if (this.props.preventDefaultTouchmoveEvent) {
86 this.setupTouchmoveEvent();
87 }
88 }
89
90 componentDidUpdate(prevProps) {
91 // swipeable toggled either on/off, so stop tracking swipes and clean up
92 if (prevProps.disabled !== this.props.disabled) {
93 this.cleanupMouseListeners();
94 // reset internal swipeable state
95 this.swipeable = getInitialState();
96 }
97
98 // preventDefaultTouchmoveEvent toggled off - clean up touch move if needed
99 if (prevProps.preventDefaultTouchmoveEvent && !this.props.preventDefaultTouchmoveEvent) {
100 this.cleanupTouchmoveEvent();
101
102 // preventDefaultTouchmoveEvent toggled on - add touch move if needed
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 // safe to call, if no match is found has no effect
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 // allow 'orig' props.onMouseDown to fire also
140 // eslint-disable-next-line react/prop-types
141 if (typeof this.props.onMouseDown === 'function') this.props.onMouseDown(e);
142
143 // setup document listeners to track mouse movement outside <Swipeable>'s area
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 // if more than a single touch don't track, for now...
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 // if swipe is under delta and we have not already started to track a swipe: return
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 // track if a swipe is cancelable
198 // so we can call prevenDefault if needed
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 // finished swipe tracking, reset swipeable state
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 // if we do not care about calling preventDefault then assign onTouchMove prop
280 // else we need to also check for passive support
281 // and set a custom eventListener for touchmove on mount/update
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 // clean up swipeable's props to avoid react warning
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
323Swipeable.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
347Swipeable.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
357module.exports = Swipeable;