Created
February 14, 2021 04:02
-
-
Save cv2k10/e9dbba1fbf397a6a7826dcfd29b933b0 to your computer and use it in GitHub Desktop.
react-swipeable.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // https://www.npmjs.com/package/react-swipeable | |
| const { useRef, useMemo } = React; | |
| const LEFT = "Left"; | |
| const RIGHT = "Right"; | |
| const UP = "Up"; | |
| const DOWN = "Down"; | |
| /* global document */ | |
| const defaultProps = { | |
| delta: 10, | |
| preventDefaultTouchmoveEvent: false, | |
| rotationAngle: 0, | |
| trackMouse: false, | |
| trackTouch: true | |
| }; | |
| const initialState = { | |
| first: true, | |
| initial: [0, 0], | |
| start: 0, | |
| swiping: false, | |
| xy: [0, 0] | |
| }; | |
| const mouseMove = "mousemove"; | |
| const mouseUp = "mouseup"; | |
| const touchEnd = "touchend"; | |
| const touchMove = "touchmove"; | |
| const touchStart = "touchstart"; | |
| function getDirection(absX, absY, deltaX, deltaY) { | |
| if (absX > absY) { | |
| if (deltaX > 0) { | |
| return RIGHT; | |
| } | |
| return LEFT; | |
| } else if (deltaY > 0) { | |
| return DOWN; | |
| } | |
| return UP; | |
| } | |
| function rotateXYByAngle(pos, angle) { | |
| if (angle === 0) return pos; | |
| const angleInRadians = Math.PI / 180 * angle; | |
| const x = pos[0] * Math.cos(angleInRadians) + pos[1] * Math.sin(angleInRadians); | |
| const y = pos[1] * Math.cos(angleInRadians) - pos[0] * Math.sin(angleInRadians); | |
| return [x, y]; | |
| } | |
| function getHandlers(set, handlerProps) { | |
| const onStart = event => { | |
| // if more than a single touch don't track, for now... | |
| if (event && "touches" in event && event.touches.length > 1) return; | |
| set((state, props) => { | |
| // setup mouse listeners on document to track swipe since swipe can leave container | |
| if (props.trackMouse) { | |
| document.addEventListener(mouseMove, onMove); | |
| document.addEventListener(mouseUp, onUp); | |
| } | |
| const { | |
| clientX, | |
| clientY | |
| } = "touches" in event ? event.touches[0] : event; | |
| const xy = rotateXYByAngle([clientX, clientY], props.rotationAngle); | |
| return { ...state, | |
| ...initialState, | |
| initial: [...xy], | |
| xy, | |
| start: event.timeStamp || 0 | |
| }; | |
| }); | |
| }; | |
| const onMove = event => { | |
| set((state, props) => { | |
| // Discount a swipe if additional touches are present after | |
| // a swipe has started. | |
| if ("touches" in event && event.touches.length > 1) { | |
| return state; | |
| } | |
| const { | |
| clientX, | |
| clientY | |
| } = "touches" in event ? event.touches[0] : event; | |
| const [x, y] = rotateXYByAngle([clientX, clientY], props.rotationAngle); | |
| const deltaX = x - state.xy[0]; | |
| const deltaY = y - state.xy[1]; | |
| const absX = Math.abs(deltaX); | |
| const absY = Math.abs(deltaY); | |
| const time = (event.timeStamp || 0) - state.start; | |
| const velocity = Math.sqrt(absX * absX + absY * absY) / (time || 1); | |
| const vxvy = [deltaX / (time || 1), deltaY / (time || 1)]; // if swipe is under delta and we have not started to track a swipe: skip update | |
| if (absX < props.delta && absY < props.delta && !state.swiping) return state; | |
| const dir = getDirection(absX, absY, deltaX, deltaY); | |
| const eventData = { | |
| absX, | |
| absY, | |
| deltaX, | |
| deltaY, | |
| dir, | |
| event, | |
| first: state.first, | |
| initial: state.initial, | |
| velocity, | |
| vxvy | |
| }; | |
| props.onSwiping && props.onSwiping(eventData); // track if a swipe is cancelable(handler for swiping or swiped(dir) exists) | |
| // so we can call preventDefault if needed | |
| let cancelablePageSwipe = false; | |
| if (props.onSwiping || props.onSwiped || `onSwiped${dir}` in props) { | |
| cancelablePageSwipe = true; | |
| } | |
| if (cancelablePageSwipe && props.preventDefaultTouchmoveEvent && props.trackTouch && event.cancelable) event.preventDefault(); | |
| return { ...state, | |
| // first is now always false | |
| first: false, | |
| eventData, | |
| swiping: true | |
| }; | |
| }); | |
| }; | |
| const onEnd = event => { | |
| set((state, props) => { | |
| let eventData; | |
| if (state.swiping && state.eventData) { | |
| eventData = { ...state.eventData, | |
| event | |
| }; | |
| props.onSwiped && props.onSwiped(eventData); | |
| const onSwipedDir = `onSwiped${eventData.dir}`; | |
| if (onSwipedDir in props) { | |
| props[onSwipedDir](eventData); | |
| } | |
| } else { | |
| props.onTap && props.onTap({ | |
| event | |
| }); | |
| } | |
| return { ...state, | |
| ...initialState, | |
| eventData | |
| }; | |
| }); | |
| }; | |
| const cleanUpMouse = () => { | |
| // safe to just call removeEventListener | |
| document.removeEventListener(mouseMove, onMove); | |
| document.removeEventListener(mouseUp, onUp); | |
| }; | |
| const onUp = e => { | |
| cleanUpMouse(); | |
| onEnd(e); | |
| }; | |
| /** | |
| * Switch of "passive" property for now. | |
| * When `preventDefaultTouchmoveEvent` is: | |
| * - true => { passive: false } | |
| * - false => { passive: true } | |
| * | |
| * Could take entire `addEventListener` options object as a param later? | |
| */ | |
| const attachTouch = (el, passive) => { | |
| let cleanup = () => {}; | |
| if (el && el.addEventListener) { | |
| // attach touch event listeners and handlers | |
| const tls = [[touchStart, onStart], [touchMove, onMove], [touchEnd, onEnd]]; | |
| tls.forEach(([e, h]) => el.addEventListener(e, h, { | |
| passive | |
| })); // return properly scoped cleanup method for removing listeners, options not required | |
| cleanup = () => tls.forEach(([e, h]) => el.removeEventListener(e, h)); | |
| } | |
| return cleanup; | |
| }; | |
| const onRef = el => { | |
| // "inline" ref functions are called twice on render, once with null then again with DOM element | |
| // ignore null here | |
| if (el === null) return; | |
| set((state, props) => { | |
| // if the same DOM el as previous just return state | |
| if (state.el === el) return state; | |
| const addState = {}; // if new DOM el clean up old DOM and reset cleanUpTouch | |
| if (state.el && state.el !== el && state.cleanUpTouch) { | |
| state.cleanUpTouch(); | |
| addState.cleanUpTouch = undefined; | |
| } // only attach if we want to track touch | |
| if (props.trackTouch && el) { | |
| addState.cleanUpTouch = attachTouch(el, !props.preventDefaultTouchmoveEvent); | |
| } // store event attached DOM el for comparison, clean up, and re-attachment | |
| return { ...state, | |
| el, | |
| ...addState | |
| }; | |
| }); | |
| }; // set ref callback to attach touch event listeners | |
| const output = { | |
| ref: onRef | |
| }; // if track mouse attach mouse down listener | |
| if (handlerProps.trackMouse) { | |
| output.onMouseDown = onStart; | |
| } | |
| return [output, attachTouch]; | |
| } | |
| function updateTransientState(state, props, attachTouch) { | |
| const addState = {}; // clean up touch handlers if no longer tracking touches | |
| if (!props.trackTouch && state.cleanUpTouch) { | |
| state.cleanUpTouch(); | |
| addState.cleanUpTouch = undefined; | |
| } else if (props.trackTouch && !state.cleanUpTouch) { | |
| // attach/re-attach touch handlers | |
| if (state.el) { | |
| addState.cleanUpTouch = attachTouch(state.el, !props.preventDefaultTouchmoveEvent); | |
| } | |
| } | |
| return { ...state, | |
| ...addState | |
| }; | |
| } | |
| function useSwipeable(options) { | |
| const { | |
| trackMouse | |
| } = options; | |
| const transientState = useRef({ ...initialState | |
| }); | |
| const transientProps = useRef({ ...defaultProps | |
| }); | |
| transientProps.current = { ...defaultProps, | |
| ...options | |
| }; | |
| const [handlers, attachTouch] = useMemo(() => getHandlers(stateSetter => transientState.current = stateSetter(transientState.current, transientProps.current), { | |
| trackMouse | |
| }), [trackMouse]); | |
| transientState.current = updateTransientState(transientState.current, transientProps.current, attachTouch); | |
| return handlers; | |
| } | |
| // export { DOWN, LEFT, RIGHT, UP, useSwipeable }; | |
| //# sourceMappingURL=react-swipeable.modern.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment