UNPKG

7.77 kBJavaScriptView Raw
1/* global document */
2import React from 'react'
3import PropTypes from 'prop-types'
4
5const defaultProps = {
6 preventDefaultTouchmoveEvent: false,
7 delta: 10,
8 rotationAngle: 0,
9 trackMouse: false,
10 trackTouch: true
11}
12const initialState = {
13 xy: [0, 0],
14 swiping: false,
15 eventData: undefined,
16 start: undefined
17}
18export const LEFT = 'Left'
19export const RIGHT = 'Right'
20export const UP = 'Up'
21export const DOWN = 'Down'
22const touchStart = 'touchstart'
23const touchMove = 'touchmove'
24const touchEnd = 'touchend'
25const mouseMove = 'mousemove'
26const mouseUp = 'mouseup'
27
28function getDirection(absX, absY, deltaX, deltaY) {
29 if (absX > absY) {
30 if (deltaX > 0) {
31 return LEFT
32 }
33 return RIGHT
34 } else if (deltaY > 0) {
35 return UP
36 }
37 return DOWN
38}
39
40function rotateXYByAngle(pos, angle) {
41 if (angle === 0) return pos
42 const angleInRadians = (Math.PI / 180) * angle
43 const x = pos[0] * Math.cos(angleInRadians) + pos[1] * Math.sin(angleInRadians)
44 const y = pos[1] * Math.cos(angleInRadians) - pos[0] * Math.sin(angleInRadians)
45 return [x, y]
46}
47
48function getHandlers(set, handlerProps) {
49 const onStart = event => {
50 // if more than a single touch don't track, for now...
51 if (event.touches && event.touches.length > 1) return
52
53 set((state, props) => {
54 // setup mouse listeners on document to track swipe since swipe can leave container
55 if (props.trackMouse) {
56 document.addEventListener(mouseMove, onMove)
57 document.addEventListener(mouseUp, onUp)
58 }
59 const { clientX, clientY } = event.touches ? event.touches[0] : event
60 const xy = rotateXYByAngle([clientX, clientY], props.rotationAngle)
61 return {
62 ...state,
63 ...initialState,
64 eventData: { initial: [...xy], first: true },
65 xy,
66 start: event.timeStamp || 0
67 }
68 })
69 }
70
71 const onMove = event => {
72 set((state, props) => {
73 if (!state.xy[0] || !state.xy[1] || (event.touches && event.touches.length > 1)) {
74 return state
75 }
76 const { clientX, clientY } = event.touches ? event.touches[0] : event
77 const [x, y] = rotateXYByAngle([clientX, clientY], props.rotationAngle)
78 const deltaX = state.xy[0] - x
79 const deltaY = state.xy[1] - y
80 const absX = Math.abs(deltaX)
81 const absY = Math.abs(deltaY)
82 const time = (event.timeStamp || 0) - state.start
83 const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1)
84
85 // if swipe is under delta and we have not started to track a swipe: skip update
86 if (absX < props.delta && absY < props.delta && !state.swiping) return state
87
88 const dir = getDirection(absX, absY, deltaX, deltaY)
89 const eventData = { ...state.eventData, event, absX, absY, deltaX, deltaY, velocity, dir }
90
91 props.onSwiping && props.onSwiping(eventData)
92
93 // track if a swipe is cancelable(handler for swiping or swiped(dir) exists)
94 // so we can call preventDefault if needed
95 let cancelablePageSwipe = false
96 if (props.onSwiping || props.onSwiped || props[`onSwiped${dir}`]) {
97 cancelablePageSwipe = true
98 }
99
100 if (
101 cancelablePageSwipe &&
102 props.preventDefaultTouchmoveEvent &&
103 props.trackTouch &&
104 event.cancelable
105 )
106 event.preventDefault()
107
108 // first is now always false
109 return { ...state, eventData: { ...eventData, first: false }, swiping: true }
110 })
111 }
112
113 const onEnd = event => {
114 set((state, props) => {
115 let eventData
116 if (state.swiping) {
117 eventData = { ...state.eventData, event }
118
119 props.onSwiped && props.onSwiped(eventData)
120
121 props[`onSwiped${eventData.dir}`] && props[`onSwiped${eventData.dir}`](eventData)
122 }
123 return { ...state, ...initialState, eventData }
124 })
125 }
126
127 const cleanUpMouse = () => {
128 // safe to just call removeEventListener
129 document.removeEventListener(mouseMove, onMove)
130 document.removeEventListener(mouseUp, onUp)
131 }
132
133 const onUp = e => {
134 cleanUpMouse()
135 onEnd(e)
136 }
137
138 const attachTouch = el => {
139 if (el && el.addEventListener) {
140 // attach touch event listeners and handlers
141 const tls = [[touchStart, onStart], [touchMove, onMove], [touchEnd, onEnd]]
142 tls.forEach(([e, h]) => el.addEventListener(e, h))
143 // return properly scoped cleanup method for removing listeners
144 return () => tls.forEach(([e, h]) => el.removeEventListener(e, h))
145 }
146 }
147
148 const onRef = el => {
149 // "inline" ref functions are called twice on render, once with null then again with DOM element
150 // ignore null here
151 if (el === null) return
152 set((state, props) => {
153 // if the same DOM el as previous just return state
154 if (state.el === el) return state
155
156 let addState = {}
157 // if new DOM el clean up old DOM and reset cleanUpTouch
158 if (state.el && state.el !== el && state.cleanUpTouch) {
159 state.cleanUpTouch()
160 addState.cleanUpTouch = null
161 }
162 // only attach if we want to track touch
163 if (props.trackTouch && el) {
164 addState.cleanUpTouch = attachTouch(el)
165 }
166
167 // store event attached DOM el for comparison, clean up, and re-attachment
168 return { ...state, el, ...addState }
169 })
170 }
171
172 // set ref callback to attach touch event listeners
173 const output = { ref: onRef }
174
175 // if track mouse attach mouse down listener
176 if (handlerProps.trackMouse) {
177 output.onMouseDown = onStart
178 }
179
180 return [output, attachTouch]
181}
182
183function updateTransientState(state, props, attachTouch) {
184 let addState = {}
185 // clean up touch handlers if no longer tracking touches
186 if (!props.trackTouch && state.cleanUpTouch) {
187 state.cleanUpTouch()
188 addState.cleanUpTouch = null
189 } else if (props.trackTouch && !state.cleanUpTouch) {
190 // attach/re-attach touch handlers
191 if (state.el) {
192 addState.cleanUpTouch = attachTouch(state.el)
193 }
194 }
195 return { ...state, ...addState }
196}
197
198export function useSwipeable(props) {
199 const { trackMouse } = props
200 const transientState = React.useRef({ ...initialState, type: 'hook' })
201 const transientProps = React.useRef()
202 transientProps.current = { ...defaultProps, ...props }
203
204 const [handlers, attachTouch] = React.useMemo(
205 () =>
206 getHandlers(
207 cb => (transientState.current = cb(transientState.current, transientProps.current)),
208 { trackMouse }
209 ),
210 [trackMouse]
211 )
212
213 transientState.current = updateTransientState(
214 transientState.current,
215 transientProps.current,
216 attachTouch
217 )
218
219 return handlers
220}
221
222export class Swipeable extends React.PureComponent {
223 static propTypes = {
224 onSwiped: PropTypes.func,
225 onSwiping: PropTypes.func,
226 onSwipedUp: PropTypes.func,
227 onSwipedRight: PropTypes.func,
228 onSwipedDown: PropTypes.func,
229 onSwipedLeft: PropTypes.func,
230 delta: PropTypes.number,
231 preventDefaultTouchmoveEvent: PropTypes.bool,
232 nodeName: PropTypes.string,
233 trackMouse: PropTypes.bool,
234 trackTouch: PropTypes.bool,
235 innerRef: PropTypes.func,
236 rotationAngle: PropTypes.number
237 }
238
239 static defaultProps = defaultProps
240
241 constructor(props) {
242 super(props)
243 this.transientState = { ...initialState, type: 'class' }
244 }
245
246 _set = cb => {
247 this.transientState = cb(this.transientState, this.props)
248 }
249
250 render() {
251 const { className, style, nodeName = 'div', innerRef, children, trackMouse } = this.props
252 const [handlers, attachTouch] = getHandlers(this._set, { trackMouse })
253 this.transientState = updateTransientState(this.transientState, this.props, attachTouch)
254 const ref = innerRef ? el => (innerRef(el), handlers.ref(el)) : handlers.ref
255 return React.createElement(nodeName, { ...handlers, className, style, ref }, children)
256 }
257}