UNPKG

11.3 kBJavaScriptView Raw
1import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
2import { withNativeProps } from '../../utils/native-props';
3import { mergeProps } from '../../utils/with-default-props';
4import classNames from 'classnames';
5import { SwiperItem } from './swiper-item';
6import { devWarning } from '../../utils/dev-log';
7import { useSpring, animated } from '@react-spring/web';
8import { useDrag } from '@use-gesture/react';
9import PageIndicator from '../page-indicator';
10import { staged } from 'staged-components';
11import { useRefState } from '../../utils/use-ref-state';
12import { bound } from '../../utils/bound';
13import { useIsomorphicLayoutEffect, useGetState } from 'ahooks';
14import { mergeFuncProps } from '../../utils/with-func-props';
15const classPrefix = `adm-swiper`;
16const eventToPropRecord = {
17 'mousedown': 'onMouseDown',
18 'mousemove': 'onMouseMove',
19 'mouseup': 'onMouseUp'
20};
21const 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};
34let currentUid;
35export 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 // ============================== Render ==============================
238 // Render Item
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 // Render Track Inner
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 // Render
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}));
339function modulus(value, division) {
340 const remainder = value % division;
341 return remainder < 0 ? remainder + division : remainder;
342}
\No newline at end of file