1 | "use client";
|
2 |
|
3 | import useEventCallback from '@restart/hooks/useEventCallback';
|
4 | import useUpdateEffect from '@restart/hooks/useUpdateEffect';
|
5 | import useCommittedRef from '@restart/hooks/useCommittedRef';
|
6 | import useTimeout from '@restart/hooks/useTimeout';
|
7 | import Anchor from '@restart/ui/Anchor';
|
8 | import classNames from 'classnames';
|
9 | import * as React from 'react';
|
10 | import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
11 | import { useUncontrolled } from 'uncontrollable';
|
12 | import CarouselCaption from './CarouselCaption';
|
13 | import CarouselItem from './CarouselItem';
|
14 | import { map, forEach } from './ElementChildren';
|
15 | import { useBootstrapPrefix, useIsRTL } from './ThemeProvider';
|
16 | import transitionEndListener from './transitionEndListener';
|
17 | import triggerBrowserReflow from './triggerBrowserReflow';
|
18 | import TransitionWrapper from './TransitionWrapper';
|
19 | import { jsx as _jsx } from "react/jsx-runtime";
|
20 | import { jsxs as _jsxs } from "react/jsx-runtime";
|
21 | import { Fragment as _Fragment } from "react/jsx-runtime";
|
22 | const SWIPE_THRESHOLD = 40;
|
23 | function isVisible(element) {
|
24 | if (!element || !element.style || !element.parentNode || !element.parentNode.style) {
|
25 | return false;
|
26 | }
|
27 | const elementStyle = getComputedStyle(element);
|
28 | return elementStyle.display !== 'none' && elementStyle.visibility !== 'hidden' && getComputedStyle(element.parentNode).display !== 'none';
|
29 | }
|
30 | const Carousel = React.forwardRef(({
|
31 | defaultActiveIndex = 0,
|
32 | ...uncontrolledProps
|
33 | }, ref) => {
|
34 | const {
|
35 |
|
36 | as: Component = 'div',
|
37 | bsPrefix,
|
38 | slide = true,
|
39 | fade = false,
|
40 | controls = true,
|
41 | indicators = true,
|
42 | indicatorLabels = [],
|
43 | activeIndex,
|
44 | onSelect,
|
45 | onSlide,
|
46 | onSlid,
|
47 | interval = 5000,
|
48 | keyboard = true,
|
49 | onKeyDown,
|
50 | pause = 'hover',
|
51 | onMouseOver,
|
52 | onMouseOut,
|
53 | wrap = true,
|
54 | touch = true,
|
55 | onTouchStart,
|
56 | onTouchMove,
|
57 | onTouchEnd,
|
58 | prevIcon = _jsx("span", {
|
59 | "aria-hidden": "true",
|
60 | className: "carousel-control-prev-icon"
|
61 | }),
|
62 | prevLabel = 'Previous',
|
63 | nextIcon = _jsx("span", {
|
64 | "aria-hidden": "true",
|
65 | className: "carousel-control-next-icon"
|
66 | }),
|
67 | nextLabel = 'Next',
|
68 | variant,
|
69 | className,
|
70 | children,
|
71 | ...props
|
72 | } = useUncontrolled({
|
73 | defaultActiveIndex,
|
74 | ...uncontrolledProps
|
75 | }, {
|
76 | activeIndex: 'onSelect'
|
77 | });
|
78 | const prefix = useBootstrapPrefix(bsPrefix, 'carousel');
|
79 | const isRTL = useIsRTL();
|
80 | const nextDirectionRef = useRef(null);
|
81 | const [direction, setDirection] = useState('next');
|
82 | const [paused, setPaused] = useState(false);
|
83 | const [isSliding, setIsSliding] = useState(false);
|
84 | const [renderedActiveIndex, setRenderedActiveIndex] = useState(activeIndex || 0);
|
85 | useEffect(() => {
|
86 | if (!isSliding && activeIndex !== renderedActiveIndex) {
|
87 | if (nextDirectionRef.current) {
|
88 | setDirection(nextDirectionRef.current);
|
89 | } else {
|
90 | setDirection((activeIndex || 0) > renderedActiveIndex ? 'next' : 'prev');
|
91 | }
|
92 | if (slide) {
|
93 | setIsSliding(true);
|
94 | }
|
95 | setRenderedActiveIndex(activeIndex || 0);
|
96 | }
|
97 | }, [activeIndex, isSliding, renderedActiveIndex, slide]);
|
98 | useEffect(() => {
|
99 | if (nextDirectionRef.current) {
|
100 | nextDirectionRef.current = null;
|
101 | }
|
102 | });
|
103 | let numChildren = 0;
|
104 | let activeChildInterval;
|
105 |
|
106 |
|
107 |
|
108 | forEach(children, (child, index) => {
|
109 | ++numChildren;
|
110 | if (index === activeIndex) {
|
111 | activeChildInterval = child.props.interval;
|
112 | }
|
113 | });
|
114 | const activeChildIntervalRef = useCommittedRef(activeChildInterval);
|
115 | const prev = useCallback(event => {
|
116 | if (isSliding) {
|
117 | return;
|
118 | }
|
119 | let nextActiveIndex = renderedActiveIndex - 1;
|
120 | if (nextActiveIndex < 0) {
|
121 | if (!wrap) {
|
122 | return;
|
123 | }
|
124 | nextActiveIndex = numChildren - 1;
|
125 | }
|
126 | nextDirectionRef.current = 'prev';
|
127 | onSelect == null ? void 0 : onSelect(nextActiveIndex, event);
|
128 | }, [isSliding, renderedActiveIndex, onSelect, wrap, numChildren]);
|
129 |
|
130 |
|
131 | const next = useEventCallback(event => {
|
132 | if (isSliding) {
|
133 | return;
|
134 | }
|
135 | let nextActiveIndex = renderedActiveIndex + 1;
|
136 | if (nextActiveIndex >= numChildren) {
|
137 | if (!wrap) {
|
138 | return;
|
139 | }
|
140 | nextActiveIndex = 0;
|
141 | }
|
142 | nextDirectionRef.current = 'next';
|
143 | onSelect == null ? void 0 : onSelect(nextActiveIndex, event);
|
144 | });
|
145 | const elementRef = useRef();
|
146 | useImperativeHandle(ref, () => ({
|
147 | element: elementRef.current,
|
148 | prev,
|
149 | next
|
150 | }));
|
151 |
|
152 |
|
153 | const nextWhenVisible = useEventCallback(() => {
|
154 | if (!document.hidden && isVisible(elementRef.current)) {
|
155 | if (isRTL) {
|
156 | prev();
|
157 | } else {
|
158 | next();
|
159 | }
|
160 | }
|
161 | });
|
162 | const slideDirection = direction === 'next' ? 'start' : 'end';
|
163 | useUpdateEffect(() => {
|
164 | if (slide) {
|
165 |
|
166 | return;
|
167 | }
|
168 | onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection);
|
169 | onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection);
|
170 | }, [renderedActiveIndex]);
|
171 | const orderClassName = `${prefix}-item-${direction}`;
|
172 | const directionalClassName = `${prefix}-item-${slideDirection}`;
|
173 | const handleEnter = useCallback(node => {
|
174 | triggerBrowserReflow(node);
|
175 | onSlide == null ? void 0 : onSlide(renderedActiveIndex, slideDirection);
|
176 | }, [onSlide, renderedActiveIndex, slideDirection]);
|
177 | const handleEntered = useCallback(() => {
|
178 | setIsSliding(false);
|
179 | onSlid == null ? void 0 : onSlid(renderedActiveIndex, slideDirection);
|
180 | }, [onSlid, renderedActiveIndex, slideDirection]);
|
181 | const handleKeyDown = useCallback(event => {
|
182 | if (keyboard && !/input|textarea/i.test(event.target.tagName)) {
|
183 | switch (event.key) {
|
184 | case 'ArrowLeft':
|
185 | event.preventDefault();
|
186 | if (isRTL) {
|
187 | next(event);
|
188 | } else {
|
189 | prev(event);
|
190 | }
|
191 | return;
|
192 | case 'ArrowRight':
|
193 | event.preventDefault();
|
194 | if (isRTL) {
|
195 | prev(event);
|
196 | } else {
|
197 | next(event);
|
198 | }
|
199 | return;
|
200 | default:
|
201 | }
|
202 | }
|
203 | onKeyDown == null ? void 0 : onKeyDown(event);
|
204 | }, [keyboard, onKeyDown, prev, next, isRTL]);
|
205 | const handleMouseOver = useCallback(event => {
|
206 | if (pause === 'hover') {
|
207 | setPaused(true);
|
208 | }
|
209 | onMouseOver == null ? void 0 : onMouseOver(event);
|
210 | }, [pause, onMouseOver]);
|
211 | const handleMouseOut = useCallback(event => {
|
212 | setPaused(false);
|
213 | onMouseOut == null ? void 0 : onMouseOut(event);
|
214 | }, [onMouseOut]);
|
215 | const touchStartXRef = useRef(0);
|
216 | const touchDeltaXRef = useRef(0);
|
217 | const touchUnpauseTimeout = useTimeout();
|
218 | const handleTouchStart = useCallback(event => {
|
219 | touchStartXRef.current = event.touches[0].clientX;
|
220 | touchDeltaXRef.current = 0;
|
221 | if (pause === 'hover') {
|
222 | setPaused(true);
|
223 | }
|
224 | onTouchStart == null ? void 0 : onTouchStart(event);
|
225 | }, [pause, onTouchStart]);
|
226 | const handleTouchMove = useCallback(event => {
|
227 | if (event.touches && event.touches.length > 1) {
|
228 | touchDeltaXRef.current = 0;
|
229 | } else {
|
230 | touchDeltaXRef.current = event.touches[0].clientX - touchStartXRef.current;
|
231 | }
|
232 | onTouchMove == null ? void 0 : onTouchMove(event);
|
233 | }, [onTouchMove]);
|
234 | const handleTouchEnd = useCallback(event => {
|
235 | if (touch) {
|
236 | const touchDeltaX = touchDeltaXRef.current;
|
237 | if (Math.abs(touchDeltaX) > SWIPE_THRESHOLD) {
|
238 | if (touchDeltaX > 0) {
|
239 | prev(event);
|
240 | } else {
|
241 | next(event);
|
242 | }
|
243 | }
|
244 | }
|
245 | if (pause === 'hover') {
|
246 | touchUnpauseTimeout.set(() => {
|
247 | setPaused(false);
|
248 | }, interval || undefined);
|
249 | }
|
250 | onTouchEnd == null ? void 0 : onTouchEnd(event);
|
251 | }, [touch, pause, prev, next, touchUnpauseTimeout, interval, onTouchEnd]);
|
252 | const shouldPlay = interval != null && !paused && !isSliding;
|
253 | const intervalHandleRef = useRef();
|
254 | useEffect(() => {
|
255 | var _ref, _activeChildIntervalR;
|
256 | if (!shouldPlay) {
|
257 | return undefined;
|
258 | }
|
259 | const nextFunc = isRTL ? prev : next;
|
260 | intervalHandleRef.current = window.setInterval(document.visibilityState ? nextWhenVisible : nextFunc, (_ref = (_activeChildIntervalR = activeChildIntervalRef.current) != null ? _activeChildIntervalR : interval) != null ? _ref : undefined);
|
261 | return () => {
|
262 | if (intervalHandleRef.current !== null) {
|
263 | clearInterval(intervalHandleRef.current);
|
264 | }
|
265 | };
|
266 | }, [shouldPlay, prev, next, activeChildIntervalRef, interval, nextWhenVisible, isRTL]);
|
267 | const indicatorOnClicks = useMemo(() => indicators && Array.from({
|
268 | length: numChildren
|
269 | }, (_, index) => event => {
|
270 | onSelect == null ? void 0 : onSelect(index, event);
|
271 | }), [indicators, numChildren, onSelect]);
|
272 | return _jsxs(Component, {
|
273 | ref: elementRef,
|
274 | ...props,
|
275 | onKeyDown: handleKeyDown,
|
276 | onMouseOver: handleMouseOver,
|
277 | onMouseOut: handleMouseOut,
|
278 | onTouchStart: handleTouchStart,
|
279 | onTouchMove: handleTouchMove,
|
280 | onTouchEnd: handleTouchEnd,
|
281 | className: classNames(className, prefix, slide && 'slide', fade && `${prefix}-fade`, variant && `${prefix}-${variant}`),
|
282 | children: [indicators && _jsx("div", {
|
283 | className: `${prefix}-indicators`,
|
284 | children: map(children, (_, index) => _jsx("button", {
|
285 | type: "button",
|
286 | "data-bs-target": ""
|
287 | ,
|
288 | "aria-label": indicatorLabels != null && indicatorLabels.length ? indicatorLabels[index] : `Slide ${index + 1}`,
|
289 | className: index === renderedActiveIndex ? 'active' : undefined,
|
290 | onClick: indicatorOnClicks ? indicatorOnClicks[index] : undefined,
|
291 | "aria-current": index === renderedActiveIndex
|
292 | }, index))
|
293 | }), _jsx("div", {
|
294 | className: `${prefix}-inner`,
|
295 | children: map(children, (child, index) => {
|
296 | const isActive = index === renderedActiveIndex;
|
297 | return slide ? _jsx(TransitionWrapper, {
|
298 | in: isActive,
|
299 | onEnter: isActive ? handleEnter : undefined,
|
300 | onEntered: isActive ? handleEntered : undefined,
|
301 | addEndListener: transitionEndListener,
|
302 | children: (status, innerProps) => React.cloneElement(child, {
|
303 | ...innerProps,
|
304 | className: classNames(child.props.className, isActive && status !== 'entered' && orderClassName, (status === 'entered' || status === 'exiting') && 'active', (status === 'entering' || status === 'exiting') && directionalClassName)
|
305 | })
|
306 | }) : React.cloneElement(child, {
|
307 | className: classNames(child.props.className, isActive && 'active')
|
308 | });
|
309 | })
|
310 | }), controls && _jsxs(_Fragment, {
|
311 | children: [(wrap || activeIndex !== 0) && _jsxs(Anchor, {
|
312 | className: `${prefix}-control-prev`,
|
313 | onClick: prev,
|
314 | children: [prevIcon, prevLabel && _jsx("span", {
|
315 | className: "visually-hidden",
|
316 | children: prevLabel
|
317 | })]
|
318 | }), (wrap || activeIndex !== numChildren - 1) && _jsxs(Anchor, {
|
319 | className: `${prefix}-control-next`,
|
320 | onClick: next,
|
321 | children: [nextIcon, nextLabel && _jsx("span", {
|
322 | className: "visually-hidden",
|
323 | children: nextLabel
|
324 | })]
|
325 | })]
|
326 | })]
|
327 | });
|
328 | });
|
329 | Carousel.displayName = 'Carousel';
|
330 | export default Object.assign(Carousel, {
|
331 | Caption: CarouselCaption,
|
332 | Item: CarouselItem
|
333 | }); |
\ | No newline at end of file |