@react-slip-and-slide/web
Version:
react-slip-and-slide
688 lines (673 loc) • 19.6 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var utils = require('@react-slip-and-slide/utils');
var react = require('@use-gesture/react');
var lodash = require('lodash');
var React = require('react');
var reactSpring = require('react-spring');
function _interopDefault (e) { return e && e.__esModule ? e : { 'default': e }; }
var React__default = /*#__PURE__*/_interopDefault(React);
function _extends() {
_extends = Object.assign ? Object.assign.bind() : 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 ReactSlipAndSlideComponent({
data,
snap,
centered,
infinite: _infinite,
containerWidth,
overflowHidden = true,
itemHeight,
itemWidth: _itemWidth = 0,
pressToSlide,
interpolators,
animateStartup = true,
rubberbandElasticity = 4,
visibleItems = 0,
fullWidthItem,
renderItem,
onChange,
onEdges,
onReady
}, ref) {
const mode = _itemWidth && itemHeight || fullWidthItem ? "fixed" : "dynamic";
const infinite = mode === "fixed" && !!_infinite;
const eagerLoading = mode === "dynamic" || visibleItems === 0;
const shouldAnimatedStartup = animateStartup && eagerLoading;
const isFirstRender = utils.useIsFirstRender();
const index = React__default["default"].useRef(0);
const [_, reRender] = React__default["default"].useState(0);
const lastOffset = React__default["default"].useRef(0);
const [container, setContainerDimensions] = React__default["default"].useState({
width: containerWidth || 0,
height: itemHeight || 0
});
const itemWidth = fullWidthItem ? container.width : _itemWidth;
const [_wrapperWidth, _setWrapperWidth] = React__default["default"].useState(0);
const containerRef = React__default["default"].useRef(null);
const isDragging = React__default["default"].useRef(false);
const direction = React__default["default"].useRef("center");
const lastValidDirection = React__default["default"].useRef(null);
const isIntentionalDrag = React__default["default"].useRef(false);
const OffsetX = React__default["default"].useMemo(() => {
return new reactSpring.SpringValue(0, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, []);
const Opacity = React__default["default"].useMemo(() => {
const initialOpacity = shouldAnimatedStartup ? 0 : 1;
return new reactSpring.SpringValue(initialOpacity, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, [shouldAnimatedStartup]);
const {
width
} = utils.useScreenDimensions();
React__default["default"].useEffect(() => {
if (containerRef.current && (!containerWidth || !itemHeight)) {
const {
offsetWidth,
clientWidth,
offsetHeight,
clientHeight
} = containerRef.current;
setContainerDimensions({
width: containerWidth || offsetWidth || clientWidth,
height: itemHeight || offsetHeight || clientHeight
});
}
}, [containerWidth, containerRef, itemHeight, width]);
const {
itemRefs,
itemDimensionMap
} = utils.useDynamicDimension({
mode,
dataLength: data.length,
onMeasure: ({
itemWidthSum
}) => {
if (itemWidthSum) {
_setWrapperWidth(itemWidthSum);
}
}
});
const {
ranges
} = utils.useItemsRange({
mode,
itemDimensionMap,
offsetX: OffsetX.get()
});
React__default["default"].useEffect(() => {
if (!itemHeight && itemDimensionMap.length) {
setContainerDimensions(prev => _extends({}, prev, {
height: itemDimensionMap[0].height
}));
}
}, [itemDimensionMap, itemHeight]);
const processClampOffsets = React__default["default"].useCallback(({
wrapperWidth,
sideMargins
}) => {
const MIN = 0;
let MAX = -wrapperWidth + container.width;
if (centered) {
const _MAX_CENTERED = MAX - sideMargins * 2;
MAX = _MAX_CENTERED;
} else {
if (wrapperWidth < container.width) {
MAX = MIN;
}
}
return {
MIN,
MAX
};
}, [centered, container.width]);
const {
dataLength,
wrapperWidth,
clampOffset
} = React__default["default"].useMemo(() => {
const wrapperWidth = mode === "fixed" ? data.length * itemWidth : _wrapperWidth;
const sideMargins = (container.width - itemWidth) / 2;
const {
MIN,
MAX
} = processClampOffsets({
wrapperWidth,
sideMargins
});
return {
dataLength: data.length,
wrapperWidth,
sideMargins,
halfItem: itemWidth / 2,
clampOffset: {
MIN,
MAX
}
};
}, [_wrapperWidth, container.width, data.length, itemWidth, mode, processClampOffsets]);
const clampReleaseOffset = React__default["default"].useCallback(offset => {
if (infinite && mode === "fixed") {
return offset;
}
if (offset > clampOffset.MIN) {
return clampOffset.MIN;
} else if (offset < clampOffset.MAX) {
return clampOffset.MAX;
}
return offset;
}, [clampOffset.MAX, clampOffset.MIN, infinite, mode]);
const clampIndex = React__default["default"].useCallback(index => lodash.clamp(index, 0, dataLength - 1), [dataLength]);
const processIndex = React__default["default"].useCallback(({
offset
}) => {
if (itemWidth) {
const modIndex = offset / itemWidth % dataLength;
return offset <= 0 ? Math.abs(modIndex) : Math.abs(modIndex > 0 ? dataLength - modIndex : 0);
}
return 0;
}, [dataLength, itemWidth]);
const getCurrentIndex = React__default["default"].useCallback(({
offset
}) => {
if (infinite) {
return -Math.round(offset / itemWidth);
}
return Math.round(processIndex({
offset
}));
}, [infinite, itemWidth, processIndex]);
const getRelativeIndex = React__default["default"].useCallback(({
offset
}) => {
return Math.floor(processIndex({
offset
}));
}, [processIndex]);
const getCurrentOffset = React__default["default"].useCallback(({
index
}) => {
const finalOffset = -index * itemWidth;
return finalOffset;
}, [itemWidth]);
const checkEdges = React__default["default"].useCallback(({
offset
}) => {
let start = false;
let end = false;
if (offset >= clampOffset.MIN) {
start = true;
} else if (offset <= clampOffset.MAX) {
end = true;
} else {
start = false;
end = false;
}
if (clampOffset.MIN === clampOffset.MAX) {
start = true;
end = true;
}
return {
start,
end
};
}, [clampOffset]);
const springIt = React__default["default"].useCallback(({
offset,
immediate,
actionType,
onRest
}) => {
const clampedReleaseOffset = clampReleaseOffset(offset);
OffsetX.start({
to: actionType === "drag" || actionType === "correction" ? offset : clampedReleaseOffset,
immediate: immediate || actionType === "drag",
onRest: x => onRest == null ? void 0 : onRest(x)
});
if (actionType === "release") {
lastOffset.current = clampedReleaseOffset;
if (mode === "fixed") {
index.current = clampIndex(getRelativeIndex({
offset: clampedReleaseOffset
}));
} else {
index.current = utils.getCurrentDynamicIndex(offset, ranges);
}
if (!eagerLoading) {
reRender(index.current);
}
onChange == null ? void 0 : onChange(index.current);
if (!infinite) {
onEdges == null ? void 0 : onEdges(checkEdges({
offset
}));
}
}
}, [OffsetX, checkEdges, clampIndex, clampReleaseOffset, eagerLoading, getRelativeIndex, infinite, mode, onChange, onEdges, ranges]);
const getCurrentIndexByOffset = React__default["default"].useCallback(offset => {
let finalIndex = 0;
const neutralIndex = offset / wrapperWidth * dataLength;
const left = Math.ceil(neutralIndex);
const right = Math.floor(neutralIndex);
if (!snap) {
return right;
}
switch (direction.current) {
case "left":
finalIndex = left;
break;
case "right":
finalIndex = right;
break;
default:
if (lastValidDirection.current === "left") {
finalIndex = left;
} else if (lastValidDirection.current === "right") {
finalIndex = right;
}
break;
}
return finalIndex;
}, [dataLength, snap, wrapperWidth]);
const drag = React__default["default"].useCallback(x => {
const offset = infinite ? x : utils.rubberband(x, rubberbandElasticity, [clampOffset.MIN, clampOffset.MAX]);
springIt({
offset,
actionType: "drag"
});
}, [clampOffset, infinite, rubberbandElasticity, springIt]);
const withSnap = React__default["default"].useCallback(({
offset
}) => {
if (mode === "fixed") {
const page = getCurrentIndexByOffset(-offset);
const finalOffset = -page * itemWidth;
return finalOffset;
} else {
const edges = checkEdges({
offset
});
return utils.getNextDynamicOffset({
offsetX: edges.start ? clampOffset.MIN : offset,
ranges,
dir: lastValidDirection.current,
centered: !!centered
});
}
}, [centered, checkEdges, clampOffset.MIN, getCurrentIndexByOffset, itemWidth, mode, ranges]);
const withMomentum = React__default["default"].useCallback(({
offset,
v
}) => {
const velocity = direction.current === "left" ? -v : direction.current === "right" ? v : 0;
const momentumOffset = offset + velocity;
return momentumOffset;
}, []);
const release = React__default["default"].useCallback(({
offset,
v
}) => {
let offsetX = 0;
if (snap) {
if (isIntentionalDrag.current) {
offsetX = withSnap({
offset
});
} else {
springIt({
offset: lastOffset.current,
actionType: "correction"
});
return;
}
} else {
offsetX = withMomentum({
offset,
v
});
}
springIt({
offset: offsetX,
actionType: "release"
});
}, [snap, springIt, withMomentum, withSnap]);
const navigate = React__default["default"].useCallback(({
index: _index,
direction,
immediate
}) => {
let targetOffset = 0;
if (_index) {
targetOffset = getCurrentOffset({
index: _index
});
} else {
if (mode === "fixed") {
const page = getCurrentIndex({
offset: OffsetX.get()
});
if (direction === "next") {
const nextPage = page + 1;
targetOffset = -nextPage * itemWidth;
} else if (direction === "prev") {
const prevPage = page - 1;
targetOffset = -prevPage * itemWidth;
}
} else {
targetOffset = utils.getNextDynamicOffset({
offsetX: OffsetX.get(),
ranges,
dir: direction === "next" ? "left" : direction === "prev" ? "right" : null,
centered: !!centered
});
}
}
springIt({
offset: targetOffset,
immediate,
actionType: "release"
});
}, [OffsetX, centered, getCurrentIndex, getCurrentOffset, itemWidth, mode, ranges, springIt]);
const move = React__default["default"].useCallback(offset => {
springIt({
offset: OffsetX.get() + offset,
actionType: "release"
});
}, [OffsetX, springIt]);
const containerBind = react.useDrag(({
active,
movement: [mx],
direction: [dirX],
velocity: [vx]
}) => {
const dir = dirX < 0 ? "left" : dirX > 0 ? "right" : "center";
direction.current = dir;
if (dir !== "center") {
lastValidDirection.current = dir;
}
const offset = lastOffset.current + mx;
isIntentionalDrag.current = Math.abs(mx) >= 40;
isDragging.current = Math.abs(mx) !== 0;
if (active) {
drag(offset);
} else {
release({
offset,
v: vx * 100
});
}
}, {
filterTaps: true,
axis: "x"
});
const handlePressToSlide = React__default["default"].useCallback(idx => {
if (!pressToSlide || isDragging.current || isIntentionalDrag.current) {
return;
}
if (mode === "fixed") {
const prev = index.current === 0 && idx === dataLength - 1;
const next = index.current === dataLength - 1 && idx === 0;
const smaller = idx < index.current;
const bigger = idx > index.current;
if (prev) {
navigate({
direction: "prev"
});
} else if (next) {
navigate({
direction: "next"
});
} else if (smaller) {
navigate({
direction: "prev"
});
} else if (bigger) {
navigate({
direction: "next"
});
}
} else {
const currIndx = utils.getCurrentDynamicIndex(OffsetX.get(), ranges);
if (idx < currIndx) {
navigate({
direction: "prev"
});
} else if (idx > currIndx) {
navigate({
direction: "next"
});
}
}
}, [OffsetX, dataLength, index, mode, navigate, pressToSlide, ranges]);
React__default["default"].useEffect(() => {
if (shouldAnimatedStartup) {
if (mode === "dynamic") {
if (ranges.length && container.height) {
Opacity.start({
to: 1,
onRest: () => {
onReady == null ? void 0 : onReady(true);
}
});
}
} else {
Opacity.start({
to: 1,
delay: 100,
onRest: () => {
onReady == null ? void 0 : onReady(true);
}
});
}
} else {
onReady == null ? void 0 : onReady(true);
}
}, [Opacity, container.height, ranges.length, shouldAnimatedStartup]);
React__default["default"].useEffect(() => {
if (mode === "dynamic" && centered) {
var _ranges$;
const alignment = centered ? "center" : "start";
springIt({
offset: -(((_ranges$ = ranges[0]) == null ? void 0 : _ranges$.range[alignment]) || 0),
actionType: "release",
immediate: true
});
}
}, [centered, mode, ranges]);
React__default["default"].useEffect(() => {
const {
end
} = checkEdges({
offset: OffsetX.get()
});
if (end) {
springIt({
offset: clampOffset.MAX,
actionType: "release"
});
}
}, [clampOffset.MAX]);
React__default["default"].useEffect(() => {
if (!isFirstRender) {
onEdges == null ? void 0 : onEdges(checkEdges({
offset: OffsetX.get()
}));
}
}, [width, clampOffset.MAX]);
const prevContainerWidth = utils.usePreviousValue(containerWidth);
React__default["default"].useEffect(() => {
if (containerWidth && containerWidth !== prevContainerWidth) {
setContainerDimensions(prev => _extends({}, prev, {
width: containerWidth
}));
}
}, [containerWidth]);
React__default["default"].useEffect(() => {
if (!infinite) {
navigate({
index: 0,
immediate: true
});
}
}, [infinite]);
React__default["default"].useImperativeHandle(ref, () => ({
next: () => navigate({
direction: "next"
}),
previous: () => navigate({
direction: "prev"
}),
goTo: ({
index,
animated
}) => navigate({
index,
immediate: !animated
}),
move
}), [move, navigate]);
const shouldRender = React__default["default"].useCallback(i => {
if (eagerLoading) {
return true;
}
return utils.isInRange(i, {
dataLength,
viewSize: itemWidth,
visibleItems: visibleItems || Math.round(dataLength / 2),
offsetX: OffsetX.get()
});
}, [OffsetX, dataLength, eagerLoading, itemWidth, visibleItems]);
return React__default["default"].createElement(utils.Styled.Wrapper, _extends({
ref: containerRef,
className: "wrapper"
}, containerBind(), {
style: {
opacity: Opacity,
justifyContent: centered ? "center" : "flex-start",
width: containerWidth || "100%",
height: itemHeight || container.height || "100%",
overflow: overflowHidden ? "hidden" : undefined,
touchAction: "pan-y"
}
}), data.map((props, i) => {
var _ranges$i, _ranges$i2;
return React__default["default"].createElement(utils.LazyLoad, {
key: i,
render: shouldRender(i)
}, React__default["default"].createElement(Item, {
ref: itemRefs[i],
index: i,
mode: mode,
item: props,
dataLength: dataLength,
renderItem: renderItem,
infinite: infinite,
itemHeight: itemHeight,
itemWidth: mode === "fixed" ? itemWidth : ((_ranges$i = ranges[i]) == null ? void 0 : _ranges$i.width) || 0,
interpolators: interpolators || {},
dynamicOffset: ((_ranges$i2 = ranges[i]) == null ? void 0 : _ranges$i2.range[centered ? "center" : "start"]) || 0,
onPress: () => pressToSlide && handlePressToSlide(i),
offsetX: OffsetX.to(offsetX => infinite ? offsetX % wrapperWidth : offsetX),
isLazy: !eagerLoading
}));
}));
}
function ItemComponent({
offsetX,
dataLength,
index,
infinite,
itemWidth,
itemHeight,
item,
interpolators,
dynamicOffset,
mode,
isLazy,
renderItem,
onPress
}, ref) {
const Opacity = React__default["default"].useMemo(() => {
return new reactSpring.SpringValue(isLazy ? 0 : 1, {
config: {
tension: 260,
friction: 32,
mass: 1
}
});
}, [isLazy]);
const x = utils.displacement({
offsetX,
dataLength,
index,
itemWidth,
infinite
});
const keys = Object.entries(interpolators);
const translateX = React__default["default"].useMemo(() => {
if (mode === "fixed") {
return x.to(val => val / itemWidth).to([-1, 0, 1], [-itemWidth, 0, itemWidth]);
}
return reactSpring.to(offsetX, x => x + dynamicOffset);
}, [dynamicOffset, itemWidth, mode, offsetX, x]);
const mapInterpolators = React__default["default"].useMemo(() => keys.reduce((acc, [key, val]) => {
acc[key] = translateX.to(val => val / itemWidth).to([-1, 0, 1], [val, 1, val], "clamp");
return acc;
}, {}), [itemWidth, keys, translateX]);
React__default["default"].useEffect(() => {
if (isLazy) {
Opacity.start({
to: 1
});
}
}, [isLazy]);
const memoRenderItem = React__default["default"].useMemo(() => {
return renderItem({
item,
index
});
}, [index, item, renderItem]);
return React__default["default"].createElement(utils.Styled.Item, {
ref: ref,
onClick: onPress,
style: _extends({
translateX
}, mapInterpolators, {
width: itemWidth === 0 ? undefined : itemWidth,
height: itemHeight
}),
onDragStart: e => e.preventDefault()
}, React__default["default"].createElement(utils.AnimatedBox, {
style: {
width: "100%",
height: "100%",
opacity: Opacity
}
}, memoRenderItem));
}
const Item = React__default["default"].forwardRef(ItemComponent);
const ForwardReactSlipAndSlideRef = React__default["default"].forwardRef(ReactSlipAndSlideComponent);
const ReactSlipAndSlide = utils.typedMemo(ForwardReactSlipAndSlideRef);
exports.ReactSlipAndSlide = ReactSlipAndSlide;