react-swipeable
Version:
React Swipe event handler component & hook
344 lines (286 loc) • 9.85 kB
JavaScript
import React from 'react';
import PropTypes from 'prop-types';
function _extends() {
_extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
return _extends.apply(this, arguments);
}
function _inheritsLoose(subClass, superClass) {
subClass.prototype = Object.create(superClass.prototype);
subClass.prototype.constructor = subClass;
subClass.__proto__ = superClass;
}
var defaultProps = {
preventDefaultTouchmoveEvent: false,
delta: 10,
rotationAngle: 0,
trackMouse: false,
trackTouch: true
};
var initialState = {
xy: [0, 0],
swiping: false,
eventData: undefined,
start: undefined
};
var LEFT = 'Left';
var RIGHT = 'Right';
var UP = 'Up';
var DOWN = 'Down';
var touchStart = 'touchstart';
var touchMove = 'touchmove';
var touchEnd = 'touchend';
var mouseMove = 'mousemove';
var mouseUp = 'mouseup';
function getDirection(absX, absY, deltaX, deltaY) {
if (absX > absY) {
if (deltaX > 0) {
return LEFT;
}
return RIGHT;
} else if (deltaY > 0) {
return UP;
}
return DOWN;
}
function rotateXYByAngle(pos, angle) {
if (angle === 0) return pos;
var angleInRadians = Math.PI / 180 * angle;
var x = pos[0] * Math.cos(angleInRadians) + pos[1] * Math.sin(angleInRadians);
var y = pos[1] * Math.cos(angleInRadians) - pos[0] * Math.sin(angleInRadians);
return [x, y];
}
function getHandlers(set, handlerProps) {
var onStart = function onStart(event) {
// if more than a single touch don't track, for now...
if (event.touches && event.touches.length > 1) return;
set(function (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);
}
var _ref = event.touches ? event.touches[0] : event,
clientX = _ref.clientX,
clientY = _ref.clientY;
var xy = rotateXYByAngle([clientX, clientY], props.rotationAngle);
return _extends({}, state, initialState, {
eventData: {
initial: [].concat(xy)
},
xy: xy,
start: event.timeStamp || 0
});
});
};
var onMove = function onMove(event) {
set(function (state, props) {
if (!state.xy[0] || !state.xy[1] || event.touches && event.touches.length > 1) {
return state;
}
var _ref2 = event.touches ? event.touches[0] : event,
clientX = _ref2.clientX,
clientY = _ref2.clientY;
var _rotateXYByAngle = rotateXYByAngle([clientX, clientY], props.rotationAngle),
x = _rotateXYByAngle[0],
y = _rotateXYByAngle[1];
var deltaX = state.xy[0] - x;
var deltaY = state.xy[1] - y;
var absX = Math.abs(deltaX);
var absY = Math.abs(deltaY);
var time = (event.timeStamp || 0) - state.start;
var velocity = Math.sqrt(absX * absX + absY * absY) / (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;
var dir = getDirection(absX, absY, deltaX, deltaY);
var eventData = _extends({}, state.eventData, {
event: event,
absX: absX,
absY: absY,
deltaX: deltaX,
deltaY: deltaY,
velocity: velocity,
dir: dir
});
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
var cancelablePageSwipe = false;
if (props.onSwiping || props.onSwiped || props["onSwiped" + dir]) {
cancelablePageSwipe = true;
}
if (cancelablePageSwipe && props.preventDefaultTouchmoveEvent && props.trackTouch && event.cancelable) event.preventDefault();
return _extends({}, state, {
eventData: eventData,
swiping: true
});
});
};
var onEnd = function onEnd(event) {
set(function (state, props) {
var eventData;
if (state.swiping) {
eventData = _extends({}, state.eventData, {
event: event
});
props.onSwiped && props.onSwiped(eventData);
props["onSwiped" + eventData.dir] && props["onSwiped" + eventData.dir](eventData);
}
return _extends({}, state, initialState, {
eventData: eventData
});
});
};
var cleanUpMouse = function cleanUpMouse() {
// safe to just call removeEventListener
document.removeEventListener(mouseMove, onMove);
document.removeEventListener(mouseUp, onUp);
};
var onUp = function onUp(e) {
cleanUpMouse();
onEnd(e);
};
var attachTouch = function attachTouch(el) {
if (el && el.addEventListener) {
// attach touch event listeners and handlers
var tls = [[touchStart, onStart], [touchMove, onMove], [touchEnd, onEnd]];
tls.forEach(function (_ref3) {
var e = _ref3[0],
h = _ref3[1];
return el.addEventListener(e, h);
}); // return properly scoped cleanup method for removing listeners
return function () {
return tls.forEach(function (_ref4) {
var e = _ref4[0],
h = _ref4[1];
return el.removeEventListener(e, h);
});
};
}
};
var onRef = function 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(function (state, props) {
// if the same DOM el as previous just return state
if (state.el === el) return state;
var addState = {}; // if new DOM el clean up old DOM and reset cleanUpTouch
if (state.el && state.el !== el && state.cleanUpTouch) {
state.cleanUpTouch();
addState.cleanUpTouch = null;
} // only attach if we want to track touch
if (props.trackTouch && el) {
addState.cleanUpTouch = attachTouch(el);
} // store event attached DOM el for comparison, clean up, and re-attachment
return _extends({}, state, {
el: el
}, addState);
});
}; // set ref callback to attach touch event listeners
var output = {
ref: onRef // if track mouse attach mouse down listener
};
if (handlerProps.trackMouse) {
output.onMouseDown = onStart;
}
return [output, attachTouch];
}
function updateTransientState(state, props, attachTouch) {
var addState = {}; // clean up touch handlers if no longer tracking touches
if (!props.trackTouch && state.cleanUpTouch) {
state.cleanUpTouch();
addState.cleanUpTouch = null;
} else if (props.trackTouch && !state.cleanUpTouch) {
// attach/re-attach touch handlers
if (state.el) {
addState.cleanUpTouch = attachTouch(state.el);
}
}
return _extends({}, state, addState);
}
function useSwipeable(props) {
var trackMouse = props.trackMouse;
var transientState = React.useRef(_extends({}, initialState, {
type: 'hook'
}));
var transientProps = React.useRef();
transientProps.current = _extends({}, defaultProps, props);
var _React$useMemo = React.useMemo(function () {
return getHandlers(function (cb) {
return transientState.current = cb(transientState.current, transientProps.current);
}, {
trackMouse: trackMouse
});
}, [trackMouse]),
handlers = _React$useMemo[0],
attachTouch = _React$useMemo[1];
transientState.current = updateTransientState(transientState.current, transientProps.current, attachTouch);
return handlers;
}
var Swipeable =
/*#__PURE__*/
function (_React$PureComponent) {
_inheritsLoose(Swipeable, _React$PureComponent);
function Swipeable(props) {
var _this;
_this = _React$PureComponent.call(this, props) || this;
_this._set = function (cb) {
_this.transientState = cb(_this.transientState, _this.props);
};
_this.transientState = _extends({}, initialState, {
type: 'class'
});
return _this;
}
var _proto = Swipeable.prototype;
_proto.render = function render() {
var _this$props = this.props,
className = _this$props.className,
style = _this$props.style,
_this$props$nodeName = _this$props.nodeName,
nodeName = _this$props$nodeName === void 0 ? 'div' : _this$props$nodeName,
innerRef = _this$props.innerRef,
children = _this$props.children,
trackMouse = _this$props.trackMouse;
var _getHandlers = getHandlers(this._set, {
trackMouse: trackMouse
}),
handlers = _getHandlers[0],
attachTouch = _getHandlers[1];
this.transientState = updateTransientState(this.transientState, this.props, attachTouch);
var ref = innerRef ? function (el) {
return innerRef(el), handlers.ref(el);
} : handlers.ref;
return React.createElement(nodeName, _extends({}, handlers, {
className: className,
style: style,
ref: ref
}), children);
};
return Swipeable;
}(React.PureComponent);
Swipeable.propTypes = {
onSwiped: PropTypes.func,
onSwiping: PropTypes.func,
onSwipedUp: PropTypes.func,
onSwipedRight: PropTypes.func,
onSwipedDown: PropTypes.func,
onSwipedLeft: PropTypes.func,
delta: PropTypes.number,
preventDefaultTouchmoveEvent: PropTypes.bool,
nodeName: PropTypes.string,
trackMouse: PropTypes.bool,
trackTouch: PropTypes.bool,
innerRef: PropTypes.func,
rotationAngle: PropTypes.number
};
Swipeable.defaultProps = defaultProps;
export { DOWN, LEFT, RIGHT, Swipeable, UP, useSwipeable };