1 | import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
2 | import { withNativeProps } from '../../utils/native-props';
|
3 | import { mergeProps } from '../../utils/with-default-props';
|
4 | import classNames from 'classnames';
|
5 | import { SwiperItem } from './swiper-item';
|
6 | import { devWarning } from '../../utils/dev-log';
|
7 | import { useSpring, animated } from '@react-spring/web';
|
8 | import { useDrag } from '@use-gesture/react';
|
9 | import PageIndicator from '../page-indicator';
|
10 | import { staged } from 'staged-components';
|
11 | import { useRefState } from '../../utils/use-ref-state';
|
12 | import { bound } from '../../utils/bound';
|
13 | import { useIsomorphicLayoutEffect, useGetState } from 'ahooks';
|
14 | import { mergeFuncProps } from '../../utils/with-func-props';
|
15 | const classPrefix = `adm-swiper`;
|
16 | const eventToPropRecord = {
|
17 | 'mousedown': 'onMouseDown',
|
18 | 'mousemove': 'onMouseMove',
|
19 | 'mouseup': 'onMouseUp'
|
20 | };
|
21 | const defaultProps = {
|
22 | defaultIndex: 0,
|
23 | allowTouchMove: true,
|
24 | autoplay: false,
|
25 | autoplayInterval: 3000,
|
26 | loop: false,
|
27 | direction: 'horizontal',
|
28 | slideSize: 100,
|
29 | trackOffset: 0,
|
30 | stuckAtBoundary: true,
|
31 | rubberband: true,
|
32 | stopPropagation: []
|
33 | };
|
34 | let currentUid;
|
35 | export const Swiper = forwardRef(staged((p, ref) => {
|
36 | const props = mergeProps(defaultProps, p);
|
37 | const {
|
38 | direction,
|
39 | total,
|
40 | children,
|
41 | indicator
|
42 | } = props;
|
43 | const [uid] = useState({});
|
44 | const timeoutRef = useRef(null);
|
45 | const isVertical = direction === 'vertical';
|
46 | const slideRatio = props.slideSize / 100;
|
47 | const offsetRatio = props.trackOffset / 100;
|
48 | const {
|
49 | validChildren,
|
50 | count,
|
51 | renderChildren
|
52 | } = useMemo(() => {
|
53 | let count = 0;
|
54 | let renderChildren = undefined;
|
55 | let validChildren = undefined;
|
56 | if (typeof children === 'function') {
|
57 | renderChildren = children;
|
58 | } else {
|
59 | validChildren = React.Children.map(children, child => {
|
60 | if (!React.isValidElement(child)) return null;
|
61 | if (child.type !== SwiperItem) {
|
62 | devWarning('Swiper', 'The children of `Swiper` must be `Swiper.Item` components.');
|
63 | return null;
|
64 | }
|
65 | count++;
|
66 | return child;
|
67 | });
|
68 | }
|
69 | return {
|
70 | renderChildren,
|
71 | validChildren,
|
72 | count
|
73 | };
|
74 | }, [children]);
|
75 | const mergedTotal = total !== null && total !== void 0 ? total : count;
|
76 | if (mergedTotal === 0 || !validChildren && !renderChildren) {
|
77 | devWarning('Swiper', '`Swiper` needs at least one child.');
|
78 | return null;
|
79 | }
|
80 | return () => {
|
81 | let loop = props.loop;
|
82 | if (slideRatio * (mergedTotal - 1) < 1) {
|
83 | loop = false;
|
84 | }
|
85 | const trackRef = useRef(null);
|
86 | function getSlidePixels() {
|
87 | const track = trackRef.current;
|
88 | if (!track) return 0;
|
89 | const trackPixels = isVertical ? track.offsetHeight : track.offsetWidth;
|
90 | return trackPixels * props.slideSize / 100;
|
91 | }
|
92 | const [current, setCurrent, getCurrent] = useGetState(props.defaultIndex);
|
93 | const [dragging, setDragging, draggingRef] = useRefState(false);
|
94 | function boundIndex(current) {
|
95 | let min = 0;
|
96 | let max = mergedTotal - 1;
|
97 | if (props.stuckAtBoundary) {
|
98 | min += offsetRatio / slideRatio;
|
99 | max -= (1 - slideRatio - offsetRatio) / slideRatio;
|
100 | }
|
101 | return bound(current, min, max);
|
102 | }
|
103 | const [{
|
104 | position
|
105 | }, api] = useSpring(() => ({
|
106 | position: boundIndex(current) * 100,
|
107 | config: {
|
108 | tension: 200,
|
109 | friction: 30
|
110 | },
|
111 | onRest: () => {
|
112 | if (draggingRef.current) return;
|
113 | if (!loop) return;
|
114 | const rawX = position.get();
|
115 | const totalWidth = 100 * mergedTotal;
|
116 | const standardPosition = modulus(rawX, totalWidth);
|
117 | if (standardPosition === rawX) return;
|
118 | api.start({
|
119 | position: standardPosition,
|
120 | immediate: true
|
121 | });
|
122 | }
|
123 | }), [mergedTotal]);
|
124 | const dragCancelRef = useRef(null);
|
125 | function forceCancelDrag() {
|
126 | var _a;
|
127 | (_a = dragCancelRef.current) === null || _a === void 0 ? void 0 : _a.call(dragCancelRef);
|
128 | draggingRef.current = false;
|
129 | }
|
130 | const bind = useDrag(state => {
|
131 | dragCancelRef.current = state.cancel;
|
132 | if (!state.intentional) return;
|
133 | if (state.first && !currentUid) {
|
134 | currentUid = uid;
|
135 | }
|
136 | if (currentUid !== uid) return;
|
137 | currentUid = state.last ? undefined : uid;
|
138 | const slidePixels = getSlidePixels();
|
139 | if (!slidePixels) return;
|
140 | const paramIndex = isVertical ? 1 : 0;
|
141 | const offset = state.offset[paramIndex];
|
142 | const direction = state.direction[paramIndex];
|
143 | const velocity = state.velocity[paramIndex];
|
144 | setDragging(true);
|
145 | if (!state.last) {
|
146 | api.start({
|
147 | position: offset * 100 / slidePixels,
|
148 | immediate: true
|
149 | });
|
150 | } else {
|
151 | const minIndex = Math.floor(offset / slidePixels);
|
152 | const maxIndex = minIndex + 1;
|
153 | const index = Math.round((offset + velocity * 2000 * direction) / slidePixels);
|
154 | swipeTo(bound(index, minIndex, maxIndex));
|
155 | window.setTimeout(() => {
|
156 | setDragging(false);
|
157 | });
|
158 | }
|
159 | }, {
|
160 | transform: ([x, y]) => [-x, -y],
|
161 | from: () => {
|
162 | const slidePixels = getSlidePixels();
|
163 | return [position.get() / 100 * slidePixels, position.get() / 100 * slidePixels];
|
164 | },
|
165 | triggerAllEvents: true,
|
166 | bounds: () => {
|
167 | if (loop) return {};
|
168 | const slidePixels = getSlidePixels();
|
169 | const lowerBound = boundIndex(0) * slidePixels;
|
170 | const upperBound = boundIndex(mergedTotal - 1) * slidePixels;
|
171 | return isVertical ? {
|
172 | top: lowerBound,
|
173 | bottom: upperBound
|
174 | } : {
|
175 | left: lowerBound,
|
176 | right: upperBound
|
177 | };
|
178 | },
|
179 | rubberband: props.rubberband,
|
180 | axis: isVertical ? 'y' : 'x',
|
181 | preventScroll: !isVertical,
|
182 | pointer: {
|
183 | touch: true
|
184 | }
|
185 | });
|
186 | function swipeTo(index, immediate = false) {
|
187 | var _a;
|
188 | const roundedIndex = Math.round(index);
|
189 | const targetIndex = loop ? modulus(roundedIndex, mergedTotal) : bound(roundedIndex, 0, mergedTotal - 1);
|
190 | if (targetIndex !== getCurrent()) {
|
191 | (_a = props.onIndexChange) === null || _a === void 0 ? void 0 : _a.call(props, targetIndex);
|
192 | }
|
193 | setCurrent(targetIndex);
|
194 | api.start({
|
195 | position: (loop ? roundedIndex : boundIndex(roundedIndex)) * 100,
|
196 | immediate
|
197 | });
|
198 | }
|
199 | function swipeNext() {
|
200 | swipeTo(Math.round(position.get() / 100) + 1);
|
201 | }
|
202 | function swipePrev() {
|
203 | swipeTo(Math.round(position.get() / 100) - 1);
|
204 | }
|
205 | useImperativeHandle(ref, () => ({
|
206 | swipeTo,
|
207 | swipeNext,
|
208 | swipePrev
|
209 | }));
|
210 | useIsomorphicLayoutEffect(() => {
|
211 | const maxIndex = mergedTotal - 1;
|
212 | if (current > maxIndex) {
|
213 | swipeTo(maxIndex, true);
|
214 | }
|
215 | });
|
216 | const {
|
217 | autoplay,
|
218 | autoplayInterval
|
219 | } = props;
|
220 | const runTimeSwiper = () => {
|
221 | timeoutRef.current = window.setTimeout(() => {
|
222 | if (autoplay === 'reverse') {
|
223 | swipePrev();
|
224 | } else {
|
225 | swipeNext();
|
226 | }
|
227 | runTimeSwiper();
|
228 | }, autoplayInterval);
|
229 | };
|
230 | useEffect(() => {
|
231 | if (!autoplay || dragging) return;
|
232 | runTimeSwiper();
|
233 | return () => {
|
234 | if (timeoutRef.current) window.clearTimeout(timeoutRef.current);
|
235 | };
|
236 | }, [autoplay, autoplayInterval, dragging, mergedTotal]);
|
237 |
|
238 |
|
239 | function renderItem(index, child) {
|
240 | let itemStyle = {};
|
241 | if (loop) {
|
242 | itemStyle = {
|
243 | [isVertical ? 'y' : 'x']: position.to(position => {
|
244 | let finalPosition = -position + index * 100;
|
245 | const totalWidth = mergedTotal * 100;
|
246 | const flagWidth = totalWidth / 2;
|
247 | finalPosition = modulus(finalPosition + flagWidth, totalWidth) - flagWidth;
|
248 | return `${finalPosition}%`;
|
249 | }),
|
250 | [isVertical ? 'top' : 'left']: `-${index * 100}%`
|
251 | };
|
252 | }
|
253 | return React.createElement(animated.div, {
|
254 | className: classNames(`${classPrefix}-slide`, {
|
255 | [`${classPrefix}-slide-active`]: current === index
|
256 | }),
|
257 | style: itemStyle,
|
258 | key: index
|
259 | }, child);
|
260 | }
|
261 | function renderItems() {
|
262 | if (renderChildren && total) {
|
263 | const offsetCount = 2;
|
264 | const startIndex = Math.max(current - offsetCount, 0);
|
265 | const endIndex = Math.min(current + offsetCount, total - 1);
|
266 | const items = [];
|
267 | for (let index = startIndex; index <= endIndex; index += 1) {
|
268 | items.push(renderItem(index, renderChildren(index)));
|
269 | }
|
270 | return React.createElement(React.Fragment, null, React.createElement("div", {
|
271 | className: `${classPrefix}-slide-placeholder`,
|
272 | style: {
|
273 | width: `${startIndex * 100}%`
|
274 | }
|
275 | }), items);
|
276 | }
|
277 | return React.Children.map(validChildren, (child, index) => {
|
278 | return renderItem(index, child);
|
279 | });
|
280 | }
|
281 |
|
282 | function renderTrackInner() {
|
283 | if (loop) {
|
284 | return React.createElement("div", {
|
285 | className: `${classPrefix}-track-inner`
|
286 | }, renderItems());
|
287 | } else {
|
288 | return React.createElement(animated.div, {
|
289 | className: `${classPrefix}-track-inner`,
|
290 | style: {
|
291 | [isVertical ? 'y' : 'x']: position.to(position => `${-position}%`)
|
292 | }
|
293 | }, renderItems());
|
294 | }
|
295 | }
|
296 |
|
297 | const style = {
|
298 | '--slide-size': `${props.slideSize}%`,
|
299 | '--track-offset': `${props.trackOffset}%`
|
300 | };
|
301 | const dragProps = Object.assign({}, props.allowTouchMove ? bind() : {});
|
302 | const stopPropagationProps = {};
|
303 | for (const key of props.stopPropagation) {
|
304 | const prop = eventToPropRecord[key];
|
305 | stopPropagationProps[prop] = function (e) {
|
306 | e.stopPropagation();
|
307 | };
|
308 | }
|
309 | const mergedProps = mergeFuncProps(dragProps, stopPropagationProps);
|
310 | let indicatorNode = null;
|
311 | if (typeof indicator === 'function') {
|
312 | indicatorNode = indicator(mergedTotal, current);
|
313 | } else if (indicator !== false) {
|
314 | indicatorNode = React.createElement("div", {
|
315 | className: `${classPrefix}-indicator`
|
316 | }, React.createElement(PageIndicator, Object.assign({}, props.indicatorProps, {
|
317 | total: mergedTotal,
|
318 | current: current,
|
319 | direction: direction
|
320 | })));
|
321 | }
|
322 | return withNativeProps(props, React.createElement("div", {
|
323 | className: classNames(classPrefix, `${classPrefix}-${direction}`),
|
324 | style: style
|
325 | }, React.createElement("div", Object.assign({
|
326 | ref: trackRef,
|
327 | className: classNames(`${classPrefix}-track`, {
|
328 | [`${classPrefix}-track-allow-touch-move`]: props.allowTouchMove
|
329 | }),
|
330 | onClickCapture: e => {
|
331 | if (draggingRef.current) {
|
332 | e.stopPropagation();
|
333 | }
|
334 | forceCancelDrag();
|
335 | }
|
336 | }, mergedProps), renderTrackInner()), indicatorNode));
|
337 | };
|
338 | }));
|
339 | function modulus(value, division) {
|
340 | const remainder = value % division;
|
341 | return remainder < 0 ? remainder + division : remainder;
|
342 | } |
\ | No newline at end of file |