@sandstreamdev/react-swipeable-list
Version:
Swipeable list component for React
678 lines (544 loc) • 23 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('react'), require('prop-types')) :
typeof define === 'function' && define.amd ? define(['exports', 'react', 'prop-types'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global['@sandstreamdev/react-swipeable-list'] = {}, global.React, global.PropTypes));
}(this, (function (exports, React, PropTypes) { 'use strict';
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefaultLegacy(React);
var PropTypes__default = /*#__PURE__*/_interopDefaultLegacy(PropTypes);
var SwipeableList = function SwipeableList(_ref) {
var children = _ref.children,
scrollStartThreshold = _ref.scrollStartThreshold,
swipeStartThreshold = _ref.swipeStartThreshold,
threshold = _ref.threshold;
return typeof children === 'function' ? children({
className: 'swipeable-list',
scrollStartThreshold,
swipeStartThreshold,
threshold
}) : /*#__PURE__*/React__default['default'].createElement("div", {
className: "swipeable-list"
}, React__default['default'].Children.map(children, function (child) {
return /*#__PURE__*/React__default['default'].cloneElement(child, {
scrollStartThreshold,
swipeStartThreshold,
threshold
});
}));
};
SwipeableList.propTypes = {
children: PropTypes__default['default'].oneOfType([PropTypes__default['default'].node, PropTypes__default['default'].func]),
scrollStartThreshold: PropTypes__default['default'].number,
swipeStartThreshold: PropTypes__default['default'].number,
threshold: PropTypes__default['default'].number
};
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _isNativeReflectConstruct() {
if (typeof Reflect === "undefined" || !Reflect.construct) return false;
if (Reflect.construct.sham) return false;
if (typeof Proxy === "function") return true;
try {
Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
return true;
} catch (e) {
return false;
}
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived),
result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
}
var ActionAnimations = {
RETURN: Symbol('Return'),
REMOVE: Symbol('Remove'),
NONE: Symbol('None')
};
var SwipeActionPropType = PropTypes__default['default'].shape({
action: PropTypes__default['default'].func.isRequired,
actionAnimation: PropTypes__default['default'].oneOf(Object.values(ActionAnimations)),
content: PropTypes__default['default'].node.isRequired
});
var DragDirection = {
UP: 1,
DOWN: 2,
LEFT: 3,
RIGHT: 4,
UNKNOWN: 5
};
var FPS_INTERVAL = 1000 / 60;
var SwipeableListItem = /*#__PURE__*/function (_PureComponent) {
_inherits(SwipeableListItem, _PureComponent);
var _super = _createSuper(SwipeableListItem);
function SwipeableListItem(_props) {
var _this;
_classCallCheck(this, SwipeableListItem);
_this = _super.call(this, _props);
_defineProperty(_assertThisInitialized(_this), "resetState", function () {
_this.dragStartPoint = {
x: -1,
y: -1
};
_this.dragDirection = DragDirection.UNKNOWN;
_this.left = 0;
_this.previousSwipeDistancePercent = 0;
});
_defineProperty(_assertThisInitialized(_this), "handleDragStartMouse", function (event) {
window.addEventListener('mouseup', _this.handleDragEndMouse);
window.addEventListener('mousemove', _this.handleMouseMove);
_this.wrapper.addEventListener('mouseup', _this.handleDragEndMouse);
_this.wrapper.addEventListener('mousemove', _this.handleMouseMove);
_this.handleDragStart(event);
});
_defineProperty(_assertThisInitialized(_this), "handleDragStartTouch", function (event) {
window.addEventListener('touchend', _this.handleDragEndTouch);
var touch = event.targetTouches[0];
_this.handleDragStart(touch);
});
_defineProperty(_assertThisInitialized(_this), "handleDragStart", function (_ref) {
var clientX = _ref.clientX,
clientY = _ref.clientY;
_this.resetState();
_this.dragStartPoint = {
x: clientX,
y: clientY
};
_this.listElement.className = 'swipeable-list-item__content';
if (_this.contentLeft !== null) {
_this.contentLeft.className = 'swipeable-list-item__content-left';
}
if (_this.contentRight !== null) {
_this.contentRight.className = 'swipeable-list-item__content-right';
}
_this.startTime = Date.now();
_this.scheduleUpdatePosition();
});
_defineProperty(_assertThisInitialized(_this), "handleMouseMove", function (event) {
if (_this.dragStartedWithinItem()) {
var clientX = event.clientX,
clientY = event.clientY;
_this.setDragDirection(clientX, clientY);
if (_this.isSwiping()) {
event.stopPropagation();
event.preventDefault();
_this.left = clientX - _this.dragStartPoint.x;
_this.scheduleUpdatePosition();
}
}
});
_defineProperty(_assertThisInitialized(_this), "handleTouchMove", function (event) {
if (_this.dragStartedWithinItem()) {
var _event$targetTouches$ = event.targetTouches[0],
clientX = _event$targetTouches$.clientX,
clientY = _event$targetTouches$.clientY;
_this.setDragDirection(clientX, clientY);
if (!event.cancelable) {
return;
}
if (_this.isSwiping()) {
event.stopPropagation();
event.preventDefault();
_this.left = clientX - _this.dragStartPoint.x;
_this.scheduleUpdatePosition();
}
}
});
_defineProperty(_assertThisInitialized(_this), "handleDragEndMouse", function () {
window.removeEventListener('mouseup', _this.handleDragEndMouse);
window.removeEventListener('mousemove', _this.handleMouseMove);
if (_this.wrapper) {
_this.wrapper.removeEventListener('mouseup', _this.handleDragEndMouse);
_this.wrapper.removeEventListener('mousemove', _this.handleMouseMove);
}
_this.handleDragEnd();
});
_defineProperty(_assertThisInitialized(_this), "handleDragEndTouch", function () {
window.removeEventListener('touchend', _this.handleDragEndTouch);
_this.handleDragEnd();
});
_defineProperty(_assertThisInitialized(_this), "playReturnAnimation", function () {
var _assertThisInitialize = _assertThisInitialized(_this),
contentLeft = _assertThisInitialize.contentLeft,
contentRight = _assertThisInitialize.contentRight,
listElement = _assertThisInitialize.listElement;
if (listElement) {
listElement.className = 'swipeable-list-item__content swipeable-list-item__content--return';
listElement.style.transform = 'translateX(0px)';
} // hide backgrounds
if (contentLeft !== null) {
contentLeft.style.opacity = 0;
contentLeft.className = 'swipeable-list-item__content-left swipeable-list-item__content-left--return';
}
if (contentRight !== null) {
contentRight.style.opacity = 0;
contentRight.className = 'swipeable-list-item__content-right swipeable-list-item__content-right--return';
}
});
_defineProperty(_assertThisInitialized(_this), "playRemoveAnimation", function (direction) {
var _assertThisInitialize2 = _assertThisInitialized(_this),
listElement = _assertThisInitialize2.listElement;
if (listElement) {
listElement.className = 'swipeable-list-item__content swipeable-list-item__content--remove';
listElement.style.transform = "translateX(".concat(listElement.offsetWidth * (direction === DragDirection.LEFT ? -1 : 1), "px)");
}
});
_defineProperty(_assertThisInitialized(_this), "playActionAnimation", function (type, direction) {
var _assertThisInitialize3 = _assertThisInitialized(_this),
listElement = _assertThisInitialize3.listElement;
if (listElement) {
switch (type) {
case ActionAnimations.REMOVE:
_this.playRemoveAnimation(direction);
break;
case ActionAnimations.NONE:
break;
default:
_this.playReturnAnimation();
}
}
});
_defineProperty(_assertThisInitialized(_this), "handleDragEnd", function () {
var _assertThisInitialize4 = _assertThisInitialized(_this),
left = _assertThisInitialize4.left,
listElement = _assertThisInitialize4.listElement,
props = _assertThisInitialize4.props;
var swipeLeft = props.swipeLeft,
swipeRight = props.swipeRight,
_props$threshold = props.threshold,
threshold = _props$threshold === void 0 ? 0.5 : _props$threshold;
var actionTriggered = false;
if (_this.isSwiping()) {
if (listElement) {
if (left < listElement.offsetWidth * threshold * -1) {
_this.playActionAnimation(swipeLeft.actionAnimation, DragDirection.LEFT);
_this.handleSwipedLeft();
actionTriggered = true;
} else if (left > listElement.offsetWidth * threshold) {
_this.playActionAnimation(swipeRight.actionAnimation, DragDirection.RIGHT);
_this.handleSwipedRight();
actionTriggered = true;
}
}
if (_this.props.onSwipeEnd) {
_this.props.onSwipeEnd();
}
}
_this.resetState();
if (!actionTriggered) {
_this.playReturnAnimation();
}
});
_defineProperty(_assertThisInitialized(_this), "dragStartedWithinItem", function () {
var _this$dragStartPoint = _this.dragStartPoint,
x = _this$dragStartPoint.x,
y = _this$dragStartPoint.y;
return x !== -1 && y !== -1;
});
_defineProperty(_assertThisInitialized(_this), "setDragDirection", function (x, y) {
if (_this.dragDirection === DragDirection.UNKNOWN) {
var _this$dragStartPoint2 = _this.dragStartPoint,
startX = _this$dragStartPoint2.x,
startY = _this$dragStartPoint2.y;
var horizontalDistance = Math.abs(x - startX);
var verticalDistance = Math.abs(y - startY);
if (horizontalDistance <= _this.dragHorizontalDirectionThreshold && verticalDistance <= _this.dragVerticalDirectionThreshold) {
return;
}
var angle = Math.atan2(y - startY, x - startX);
var octant = Math.round(8 * angle / (2 * Math.PI) + 8) % 8;
switch (octant) {
case 0:
if (_this.contentRight !== null && horizontalDistance > _this.dragHorizontalDirectionThreshold) {
_this.dragDirection = DragDirection.RIGHT;
}
break;
case 1:
case 2:
case 3:
if (verticalDistance > _this.dragVerticalDirectionThreshold) {
_this.dragDirection = DragDirection.DOWN;
}
break;
case 4:
if (_this.contentLeft !== null && horizontalDistance > _this.dragHorizontalDirectionThreshold) {
_this.dragDirection = DragDirection.LEFT;
}
break;
case 5:
case 6:
case 7:
if (verticalDistance > _this.dragVerticalDirectionThreshold) {
_this.dragDirection = DragDirection.UP;
}
break;
}
if (_this.props.onSwipeStart && _this.isSwiping()) {
_this.props.onSwipeStart();
}
}
});
_defineProperty(_assertThisInitialized(_this), "isSwiping", function () {
var blockSwipe = _this.props.blockSwipe;
var horizontalDrag = _this.dragDirection === DragDirection.LEFT || _this.dragDirection === DragDirection.RIGHT;
return !blockSwipe && _this.dragStartedWithinItem() && horizontalDrag;
});
_defineProperty(_assertThisInitialized(_this), "scheduleUpdatePosition", function () {
if (_this.requestedAnimationFrame) {
return;
}
_this.requestedAnimationFrame = requestAnimationFrame(function () {
_this.requestedAnimationFrame = null;
_this.updatePosition();
});
});
_defineProperty(_assertThisInitialized(_this), "updatePosition", function () {
var now = Date.now();
var elapsed = now - _this.startTime;
if (elapsed > FPS_INTERVAL && _this.isSwiping()) {
var contentToShow = _this.left < 0 ? _this.contentLeft : _this.contentRight;
if (_this.onlyLeftContent && _this.left > 0) {
_this.left = 0;
contentToShow = _this.contentLeft;
}
if (_this.onlyRightContent && _this.left < 0) {
_this.left = 0;
contentToShow = _this.contentRight;
}
if (!contentToShow) {
return;
}
if (_this.listElement) {
_this.listElement.style.transform = "translateX(".concat(_this.left, "px)");
}
var opacity = (Math.abs(_this.left) / 100).toFixed(2);
if (_this.props.onSwipeProgress && _this.listElement) {
var listElementWidth = _this.listElement.offsetWidth;
var swipeDistancePercent = _this.previousSwipeDistancePercent;
if (listElementWidth !== 0) {
var swipeDistance = Math.max(0, listElementWidth - Math.abs(_this.left));
swipeDistancePercent = 100 - Math.round(100 * swipeDistance / listElementWidth);
}
if (_this.previousSwipeDistancePercent !== swipeDistancePercent) {
_this.props.onSwipeProgress(swipeDistancePercent);
_this.previousSwipeDistancePercent = swipeDistancePercent;
}
}
if (opacity < 1 && opacity.toString() !== contentToShow.style.opacity) {
contentToShow.style.opacity = opacity.toString();
var contentToHide = _this.left < 0 ? _this.contentRight : _this.contentLeft;
if (contentToHide) {
contentToHide.style.opacity = '0';
}
}
if (opacity >= 1) {
contentToShow.style.opacity = '1';
}
_this.startTime = Date.now();
}
});
_defineProperty(_assertThisInitialized(_this), "handleSwipedLeft", function () {
var _this$props$swipeLeft = _this.props.swipeLeft;
_this$props$swipeLeft = _this$props$swipeLeft === void 0 ? {} : _this$props$swipeLeft;
var action = _this$props$swipeLeft.action;
if (action) {
action();
}
});
_defineProperty(_assertThisInitialized(_this), "handleSwipedRight", function () {
var _this$props$swipeRigh = _this.props.swipeRight;
_this$props$swipeRigh = _this$props$swipeRigh === void 0 ? {} : _this$props$swipeRigh;
var action = _this$props$swipeRigh.action;
if (action) {
action();
}
});
_defineProperty(_assertThisInitialized(_this), "bindContentLeft", function (ref) {
return _this.contentLeft = ref;
});
_defineProperty(_assertThisInitialized(_this), "bindContentRight", function (ref) {
return _this.contentRight = ref;
});
_defineProperty(_assertThisInitialized(_this), "bindListElement", function (ref) {
return _this.listElement = ref;
});
_defineProperty(_assertThisInitialized(_this), "bindWrapper", function (ref) {
return _this.wrapper = ref;
});
_this.contentLeft = null;
_this.contentRight = null;
_this.listElement = null;
_this.requestedAnimationFrame = null;
_this.wrapper = null;
_this.startTime = null;
_this.previousSwipeDistancePercent = 0;
_this.resetState();
return _this;
}
_createClass(SwipeableListItem, [{
key: "dragHorizontalDirectionThreshold",
get: function get() {
return this.props.swipeStartThreshold || 10;
}
}, {
key: "dragVerticalDirectionThreshold",
get: function get() {
return this.props.scrollStartThreshold || 10;
}
}, {
key: "componentDidMount",
value: function componentDidMount() {
this.wrapper.addEventListener('mousedown', this.handleDragStartMouse);
this.wrapper.addEventListener('touchstart', this.handleDragStartTouch);
this.wrapper.addEventListener('touchend', this.handleDragEndTouch);
this.wrapper.addEventListener('touchmove', this.handleTouchMove, {
capture: true,
passive: false
});
}
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
if (this.requestedAnimationFrame) {
cancelAnimationFrame(this.requestedAnimationFrame);
this.requestedAnimationFrame = null;
}
this.wrapper.removeEventListener('mousedown', this.handleDragStartMouse);
this.wrapper.removeEventListener('touchstart', this.handleDragStartTouch);
this.wrapper.removeEventListener('touchend', this.handleDragEndTouch);
this.wrapper.removeEventListener('touchmove', this.handleTouchMove, {
capture: true,
passive: false
});
}
}, {
key: "onlyLeftContent",
get: function get() {
return this.contentLeft !== null && this.contentRight === null;
}
}, {
key: "onlyRightContent",
get: function get() {
return this.contentLeft === null && this.contentRight !== null;
}
}, {
key: "render",
value: function render() {
var _this$props = this.props,
children = _this$props.children,
swipeLeft = _this$props.swipeLeft,
swipeRight = _this$props.swipeRight;
return /*#__PURE__*/React__default['default'].createElement("div", {
className: "swipeable-list-item",
ref: this.bindWrapper
}, swipeLeft && /*#__PURE__*/React__default['default'].createElement("div", {
className: "swipeable-list-item__content-left",
"data-testid": "swipe-left-content",
ref: this.bindContentLeft
}, swipeLeft.content), swipeRight && /*#__PURE__*/React__default['default'].createElement("div", {
className: "swipeable-list-item__content-right",
"data-testid": "swipe-right-content",
ref: this.bindContentRight
}, swipeRight.content), /*#__PURE__*/React__default['default'].createElement("div", {
className: "swipeable-list-item__content",
"data-testid": "content",
ref: this.bindListElement
}, children));
}
}]);
return SwipeableListItem;
}(React.PureComponent);
SwipeableListItem.propTypes = {
blockSwipe: PropTypes__default['default'].bool,
children: PropTypes__default['default'].node.isRequired,
swipeLeft: SwipeActionPropType,
swipeRight: SwipeActionPropType,
scrollStartThreshold: PropTypes__default['default'].number,
swipeStartThreshold: PropTypes__default['default'].number,
threshold: PropTypes__default['default'].number,
onSwipeEnd: PropTypes__default['default'].func,
onSwipeProgress: PropTypes__default['default'].func,
onSwipeStart: PropTypes__default['default'].func
};
exports.ActionAnimations = ActionAnimations;
exports.SwipeableList = SwipeableList;
exports.SwipeableListItem = SwipeableListItem;
Object.defineProperty(exports, '__esModule', { value: true });
})));