antd-mobile
Version:
<div align="center">
342 lines • 11.3 kB
JavaScript
import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
import { withNativeProps } from '../../utils/native-props';
import { mergeProps } from '../../utils/with-default-props';
import classNames from 'classnames';
import { SwiperItem } from './swiper-item';
import { devWarning } from '../../utils/dev-log';
import { useSpring, animated } from '@react-spring/web';
import { useDrag } from '@use-gesture/react';
import PageIndicator from '../page-indicator';
import { staged } from 'staged-components';
import { useRefState } from '../../utils/use-ref-state';
import { bound } from '../../utils/bound';
import { useIsomorphicLayoutEffect, useGetState } from 'ahooks';
import { mergeFuncProps } from '../../utils/with-func-props';
const classPrefix = `adm-swiper`;
const eventToPropRecord = {
'mousedown': 'onMouseDown',
'mousemove': 'onMouseMove',
'mouseup': 'onMouseUp'
};
const defaultProps = {
defaultIndex: 0,
allowTouchMove: true,
autoplay: false,
autoplayInterval: 3000,
loop: false,
direction: 'horizontal',
slideSize: 100,
trackOffset: 0,
stuckAtBoundary: true,
rubberband: true,
stopPropagation: []
};
let currentUid;
export const Swiper = forwardRef(staged((p, ref) => {
const props = mergeProps(defaultProps, p);
const {
direction,
total,
children,
indicator
} = props;
const [uid] = useState({});
const timeoutRef = useRef(null);
const isVertical = direction === 'vertical';
const slideRatio = props.slideSize / 100;
const offsetRatio = props.trackOffset / 100;
const {
validChildren,
count,
renderChildren
} = useMemo(() => {
let count = 0;
let renderChildren = undefined;
let validChildren = undefined;
if (typeof children === 'function') {
renderChildren = children;
} else {
validChildren = React.Children.map(children, child => {
if (!React.isValidElement(child)) return null;
if (child.type !== SwiperItem) {
devWarning('Swiper', 'The children of `Swiper` must be `Swiper.Item` components.');
return null;
}
count++;
return child;
});
}
return {
renderChildren,
validChildren,
count
};
}, [children]);
const mergedTotal = total !== null && total !== void 0 ? total : count;
if (mergedTotal === 0 || !validChildren && !renderChildren) {
devWarning('Swiper', '`Swiper` needs at least one child.');
return null;
}
return () => {
let loop = props.loop;
if (slideRatio * (mergedTotal - 1) < 1) {
loop = false;
}
const trackRef = useRef(null);
function getSlidePixels() {
const track = trackRef.current;
if (!track) return 0;
const trackPixels = isVertical ? track.offsetHeight : track.offsetWidth;
return trackPixels * props.slideSize / 100;
}
const [current, setCurrent, getCurrent] = useGetState(props.defaultIndex);
const [dragging, setDragging, draggingRef] = useRefState(false);
function boundIndex(current) {
let min = 0;
let max = mergedTotal - 1;
if (props.stuckAtBoundary) {
min += offsetRatio / slideRatio;
max -= (1 - slideRatio - offsetRatio) / slideRatio;
}
return bound(current, min, max);
}
const [{
position
}, api] = useSpring(() => ({
position: boundIndex(current) * 100,
config: {
tension: 200,
friction: 30
},
onRest: () => {
if (draggingRef.current) return;
if (!loop) return;
const rawX = position.get();
const totalWidth = 100 * mergedTotal;
const standardPosition = modulus(rawX, totalWidth);
if (standardPosition === rawX) return;
api.start({
position: standardPosition,
immediate: true
});
}
}), [mergedTotal]);
const dragCancelRef = useRef(null);
function forceCancelDrag() {
var _a;
(_a = dragCancelRef.current) === null || _a === void 0 ? void 0 : _a.call(dragCancelRef);
draggingRef.current = false;
}
const bind = useDrag(state => {
dragCancelRef.current = state.cancel;
if (!state.intentional) return;
if (state.first && !currentUid) {
currentUid = uid;
}
if (currentUid !== uid) return;
currentUid = state.last ? undefined : uid;
const slidePixels = getSlidePixels();
if (!slidePixels) return;
const paramIndex = isVertical ? 1 : 0;
const offset = state.offset[paramIndex];
const direction = state.direction[paramIndex];
const velocity = state.velocity[paramIndex];
setDragging(true);
if (!state.last) {
api.start({
position: offset * 100 / slidePixels,
immediate: true
});
} else {
const minIndex = Math.floor(offset / slidePixels);
const maxIndex = minIndex + 1;
const index = Math.round((offset + velocity * 2000 * direction) / slidePixels);
swipeTo(bound(index, minIndex, maxIndex));
window.setTimeout(() => {
setDragging(false);
});
}
}, {
transform: ([x, y]) => [-x, -y],
from: () => {
const slidePixels = getSlidePixels();
return [position.get() / 100 * slidePixels, position.get() / 100 * slidePixels];
},
triggerAllEvents: true,
bounds: () => {
if (loop) return {};
const slidePixels = getSlidePixels();
const lowerBound = boundIndex(0) * slidePixels;
const upperBound = boundIndex(mergedTotal - 1) * slidePixels;
return isVertical ? {
top: lowerBound,
bottom: upperBound
} : {
left: lowerBound,
right: upperBound
};
},
rubberband: props.rubberband,
axis: isVertical ? 'y' : 'x',
preventScroll: !isVertical,
pointer: {
touch: true
}
});
function swipeTo(index, immediate = false) {
var _a;
const roundedIndex = Math.round(index);
const targetIndex = loop ? modulus(roundedIndex, mergedTotal) : bound(roundedIndex, 0, mergedTotal - 1);
if (targetIndex !== getCurrent()) {
(_a = props.onIndexChange) === null || _a === void 0 ? void 0 : _a.call(props, targetIndex);
}
setCurrent(targetIndex);
api.start({
position: (loop ? roundedIndex : boundIndex(roundedIndex)) * 100,
immediate
});
}
function swipeNext() {
swipeTo(Math.round(position.get() / 100) + 1);
}
function swipePrev() {
swipeTo(Math.round(position.get() / 100) - 1);
}
useImperativeHandle(ref, () => ({
swipeTo,
swipeNext,
swipePrev
}));
useIsomorphicLayoutEffect(() => {
const maxIndex = mergedTotal - 1;
if (current > maxIndex) {
swipeTo(maxIndex, true);
}
});
const {
autoplay,
autoplayInterval
} = props;
const runTimeSwiper = () => {
timeoutRef.current = window.setTimeout(() => {
if (autoplay === 'reverse') {
swipePrev();
} else {
swipeNext();
}
runTimeSwiper();
}, autoplayInterval);
};
useEffect(() => {
if (!autoplay || dragging) return;
runTimeSwiper();
return () => {
if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
};
}, [autoplay, autoplayInterval, dragging, mergedTotal]);
// ============================== Render ==============================
// Render Item
function renderItem(index, child) {
let itemStyle = {};
if (loop) {
itemStyle = {
[isVertical ? 'y' : 'x']: position.to(position => {
let finalPosition = -position + index * 100;
const totalWidth = mergedTotal * 100;
const flagWidth = totalWidth / 2;
finalPosition = modulus(finalPosition + flagWidth, totalWidth) - flagWidth;
return `${finalPosition}%`;
}),
[isVertical ? 'top' : 'left']: `-${index * 100}%`
};
}
return React.createElement(animated.div, {
className: classNames(`${classPrefix}-slide`, {
[`${classPrefix}-slide-active`]: current === index
}),
style: itemStyle,
key: index
}, child);
}
function renderItems() {
if (renderChildren && total) {
const offsetCount = 2;
const startIndex = Math.max(current - offsetCount, 0);
const endIndex = Math.min(current + offsetCount, total - 1);
const items = [];
for (let index = startIndex; index <= endIndex; index += 1) {
items.push(renderItem(index, renderChildren(index)));
}
return React.createElement(React.Fragment, null, React.createElement("div", {
className: `${classPrefix}-slide-placeholder`,
style: {
width: `${startIndex * 100}%`
}
}), items);
}
return React.Children.map(validChildren, (child, index) => {
return renderItem(index, child);
});
}
// Render Track Inner
function renderTrackInner() {
if (loop) {
return React.createElement("div", {
className: `${classPrefix}-track-inner`
}, renderItems());
} else {
return React.createElement(animated.div, {
className: `${classPrefix}-track-inner`,
style: {
[isVertical ? 'y' : 'x']: position.to(position => `${-position}%`)
}
}, renderItems());
}
}
// Render
const style = {
'--slide-size': `${props.slideSize}%`,
'--track-offset': `${props.trackOffset}%`
};
const dragProps = Object.assign({}, props.allowTouchMove ? bind() : {});
const stopPropagationProps = {};
for (const key of props.stopPropagation) {
const prop = eventToPropRecord[key];
stopPropagationProps[prop] = function (e) {
e.stopPropagation();
};
}
const mergedProps = mergeFuncProps(dragProps, stopPropagationProps);
let indicatorNode = null;
if (typeof indicator === 'function') {
indicatorNode = indicator(mergedTotal, current);
} else if (indicator !== false) {
indicatorNode = React.createElement("div", {
className: `${classPrefix}-indicator`
}, React.createElement(PageIndicator, Object.assign({}, props.indicatorProps, {
total: mergedTotal,
current: current,
direction: direction
})));
}
return withNativeProps(props, React.createElement("div", {
className: classNames(classPrefix, `${classPrefix}-${direction}`),
style: style
}, React.createElement("div", Object.assign({
ref: trackRef,
className: classNames(`${classPrefix}-track`, {
[`${classPrefix}-track-allow-touch-move`]: props.allowTouchMove
}),
onClickCapture: e => {
if (draggingRef.current) {
e.stopPropagation();
}
forceCancelDrag();
}
}, mergedProps), renderTrackInner()), indicatorNode));
};
}));
function modulus(value, division) {
const remainder = value % division;
return remainder < 0 ? remainder + division : remainder;
}