1 |
|
2 | import React from 'react'
|
3 | import PropTypes from 'prop-types'
|
4 |
|
5 | const defaultProps = {
|
6 | preventDefaultTouchmoveEvent: false,
|
7 | delta: 10,
|
8 | rotationAngle: 0,
|
9 | trackMouse: false,
|
10 | trackTouch: true
|
11 | }
|
12 | const initialState = {
|
13 | xy: [0, 0],
|
14 | swiping: false,
|
15 | eventData: undefined,
|
16 | start: undefined
|
17 | }
|
18 | export const LEFT = 'Left'
|
19 | export const RIGHT = 'Right'
|
20 | export const UP = 'Up'
|
21 | export const DOWN = 'Down'
|
22 | const touchStart = 'touchstart'
|
23 | const touchMove = 'touchmove'
|
24 | const touchEnd = 'touchend'
|
25 | const mouseMove = 'mousemove'
|
26 | const mouseUp = 'mouseup'
|
27 |
|
28 | function 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 |
|
40 | function 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 |
|
48 | function getHandlers(set, handlerProps) {
|
49 | const onStart = event => {
|
50 |
|
51 | if (event.touches && event.touches.length > 1) return
|
52 |
|
53 | set((state, props) => {
|
54 |
|
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 |
|
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 |
|
94 |
|
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 |
|
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 |
|
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 |
|
141 | const tls = [[touchStart, onStart], [touchMove, onMove], [touchEnd, onEnd]]
|
142 | tls.forEach(([e, h]) => el.addEventListener(e, h))
|
143 |
|
144 | return () => tls.forEach(([e, h]) => el.removeEventListener(e, h))
|
145 | }
|
146 | }
|
147 |
|
148 | const onRef = el => {
|
149 |
|
150 |
|
151 | if (el === null) return
|
152 | set((state, props) => {
|
153 |
|
154 | if (state.el === el) return state
|
155 |
|
156 | let addState = {}
|
157 |
|
158 | if (state.el && state.el !== el && state.cleanUpTouch) {
|
159 | state.cleanUpTouch()
|
160 | addState.cleanUpTouch = null
|
161 | }
|
162 |
|
163 | if (props.trackTouch && el) {
|
164 | addState.cleanUpTouch = attachTouch(el)
|
165 | }
|
166 |
|
167 |
|
168 | return { ...state, el, ...addState }
|
169 | })
|
170 | }
|
171 |
|
172 |
|
173 | const output = { ref: onRef }
|
174 |
|
175 |
|
176 | if (handlerProps.trackMouse) {
|
177 | output.onMouseDown = onStart
|
178 | }
|
179 |
|
180 | return [output, attachTouch]
|
181 | }
|
182 |
|
183 | function updateTransientState(state, props, attachTouch) {
|
184 | let addState = {}
|
185 |
|
186 | if (!props.trackTouch && state.cleanUpTouch) {
|
187 | state.cleanUpTouch()
|
188 | addState.cleanUpTouch = null
|
189 | } else if (props.trackTouch && !state.cleanUpTouch) {
|
190 |
|
191 | if (state.el) {
|
192 | addState.cleanUpTouch = attachTouch(state.el)
|
193 | }
|
194 | }
|
195 | return { ...state, ...addState }
|
196 | }
|
197 |
|
198 | export 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 |
|
222 | export 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 | }
|