@via-profit/ui-kit 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/Swiper/Swiper.d.ts +68 -0
- package/Swiper/Swiper.js +268 -135
- package/Swiper/SwiperSlide.js +3 -0
- package/Swiper/SwiperSlidesRenderer.d.ts +4 -0
- package/Swiper/SwiperSlidesRenderer.js +2 -1
- package/Swiper/SwiperTrack.d.ts +20 -0
- package/Swiper/SwiperTrack.js +28 -5
- package/Swiper/SwiperWrapper.d.ts +3 -0
- package/Swiper/SwiperWrapper.js +5 -4
- package/esm/Swiper/Swiper.js +268 -135
- package/esm/Swiper/SwiperSlide.js +3 -0
- package/esm/Swiper/SwiperSlidesRenderer.js +2 -1
- package/esm/Swiper/SwiperTrack.js +28 -5
- package/esm/Swiper/SwiperWrapper.js +5 -4
- package/package.json +1 -1
package/esm/Swiper/Swiper.js
CHANGED
|
@@ -16,44 +16,64 @@ import Track from './SwiperTrack';
|
|
|
16
16
|
import SwiperSlidesRenderer from './SwiperSlidesRenderer';
|
|
17
17
|
export * from './SwiperSlide';
|
|
18
18
|
const defaultSlideLabel = (index, total) => `${index + 1} / ${total}`;
|
|
19
|
+
const prefersReducedMotion = () => typeof window !== 'undefined' &&
|
|
20
|
+
typeof window.matchMedia === 'function' &&
|
|
21
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
19
22
|
export const Swiper = React.forwardRef((props, ref) => {
|
|
20
|
-
const { children, dragThreshold = 240, snap = true, draggable = true, overrides, initialIndex = 0, infinite = false, onSlideChange, autoplay = false, autoplayInterval = 3000, pauseOnHover = true, threshold = 20, resistance = true, slidesPerView = 1, keyboardControl = true, slideLabel = defaultSlideLabel } = props, restProps = __rest(props, ["children", "dragThreshold", "snap", "draggable", "overrides", "initialIndex", "infinite", "onSlideChange", "autoplay", "autoplayInterval", "pauseOnHover", "threshold", "resistance", "slidesPerView", "keyboardControl", "slideLabel"]);
|
|
23
|
+
const { children, dragThreshold = 240, snap = true, draggable: draggableProp = true, overrides, initialIndex = 0, infinite = false, onSlideChange, autoplay = false, autoplayInterval = 3000, pauseOnHover = true, threshold = 20, resistance = true, slidesPerView: slidesPerViewProp = 1, centered = false, direction = 'horizontal', effect = 'slide', speed = 300, autoScroll, keyboardControl: keyboardControlProp = true, slideLabel = defaultSlideLabel } = props, restProps = __rest(props, ["children", "dragThreshold", "snap", "draggable", "overrides", "initialIndex", "infinite", "onSlideChange", "autoplay", "autoplayInterval", "pauseOnHover", "threshold", "resistance", "slidesPerView", "centered", "direction", "effect", "speed", "autoScroll", "keyboardControl", "slideLabel"]);
|
|
24
|
+
const isFade = effect === 'fade';
|
|
25
|
+
const isTicker = typeof autoScroll === 'number' && autoScroll > 0 && !isFade;
|
|
26
|
+
const isVertical = direction === 'vertical';
|
|
27
|
+
const slidesPerView = isFade ? 1 : Math.max(slidesPerViewProp, 0.1);
|
|
28
|
+
const draggable = draggableProp && !isTicker;
|
|
29
|
+
const keyboardControl = keyboardControlProp && !isTicker;
|
|
30
|
+
// The current slide is moved to the middle of the view by this number of slides
|
|
31
|
+
const centerShift = centered && !isFade ? (slidesPerView - 1) / 2 : 0;
|
|
21
32
|
// #region Slides
|
|
22
|
-
const slides = React.useMemo(() => {
|
|
33
|
+
const { slides, clones } = React.useMemo(() => {
|
|
23
34
|
// toArray skips null/false and keeps the user keys (prefixed), so the slides are not remounted
|
|
24
35
|
// when a slide is added to the start
|
|
25
36
|
const childrenSlides = React.Children.toArray(children).filter(React.isValidElement);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
37
|
+
// The fade effect does not move the track, the loop does not need the copies
|
|
38
|
+
if (!(infinite || isTicker) || isFade) {
|
|
39
|
+
return { slides: childrenSlides, clones: 0 };
|
|
40
|
+
}
|
|
41
|
+
// The copies on each side cover the whole view (also its partially visible slides)
|
|
42
|
+
// and the part of the view in front of the centered slide
|
|
43
|
+
const count = Math.ceil(slidesPerView) + Math.ceil(centerShift);
|
|
44
|
+
if (childrenSlides.length < Math.max(2, count)) {
|
|
45
|
+
console.error(`For infinite loop with slidesPerView=${slidesPerView}${centered ? ' and centered' : ''}, need at least ${Math.max(2, count)} slides`);
|
|
46
|
+
return { slides: childrenSlides, clones: 0 };
|
|
47
|
+
}
|
|
48
|
+
const headClones = childrenSlides.slice(-count).map((slide, i) => React.cloneElement(slide, {
|
|
49
|
+
key: `clone-head-${i}`,
|
|
50
|
+
'data-clone': 'head',
|
|
51
|
+
}));
|
|
52
|
+
const tailClones = childrenSlides.slice(0, count).map((slide, i) => React.cloneElement(slide, {
|
|
53
|
+
key: `clone-tail-${i}`,
|
|
54
|
+
'data-clone': 'tail',
|
|
55
|
+
}));
|
|
56
|
+
return { slides: [...headClones, ...childrenSlides, ...tailClones], clones: count };
|
|
57
|
+
}, [children, infinite, isTicker, isFade, slidesPerView, centerShift, centered]);
|
|
58
|
+
// The infinite loop by the copies of the slides (slide effect and ticker)
|
|
59
|
+
const isLoop = clones > 0;
|
|
60
|
+
// The fade effect loops by the index alone
|
|
61
|
+
const isFadeLoop = isFade && infinite && slides.length > 1;
|
|
48
62
|
const total = slides.length;
|
|
49
|
-
const realSlidesCount =
|
|
50
|
-
|
|
63
|
+
const realSlidesCount = total - 2 * clones;
|
|
64
|
+
// Without the loop the last position shows the last slide at the end of the view
|
|
65
|
+
// (in the middle of the view when centered). With a fractional slidesPerView it is not a whole
|
|
66
|
+
// slide: the index goes up to the next whole number and the track stops at the last position
|
|
67
|
+
const lastPosition = isFade || centerShift > 0 ? Math.max(0, total - 1) : Math.max(0, total - slidesPerView);
|
|
68
|
+
const maxIndex = Math.ceil(lastPosition - 0.001);
|
|
69
|
+
// The index while the track is between the slides (drag, free mode) without the loop
|
|
70
|
+
const maxFreeIndex = Math.floor(lastPosition + 0.001);
|
|
51
71
|
// #region States
|
|
52
72
|
const [index, setIndex] = React.useState(() => {
|
|
53
|
-
if (!
|
|
73
|
+
if (!isLoop) {
|
|
54
74
|
return Math.max(0, Math.min(initialIndex, maxIndex));
|
|
55
75
|
}
|
|
56
|
-
return initialIndex +
|
|
76
|
+
return initialIndex + clones;
|
|
57
77
|
});
|
|
58
78
|
const [offset, setOffset] = React.useState(0);
|
|
59
79
|
// The long decelerating transition after a free swipe (snap=false)
|
|
@@ -67,8 +87,8 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
67
87
|
const isPaused = isPausedByApi || isHovered || isFocused;
|
|
68
88
|
// #region Refs
|
|
69
89
|
const dragging = React.useRef(false);
|
|
70
|
-
const
|
|
71
|
-
const
|
|
90
|
+
const startCoord = React.useRef(0);
|
|
91
|
+
const lastCoord = React.useRef(0);
|
|
72
92
|
const lastTime = React.useRef(0);
|
|
73
93
|
const velocity = React.useRef(0);
|
|
74
94
|
const wrapperRef = React.useRef(null);
|
|
@@ -76,12 +96,13 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
76
96
|
const isAnimating = React.useRef(false);
|
|
77
97
|
const mounted = React.useRef(true);
|
|
78
98
|
const suppressClick = React.useRef(false);
|
|
99
|
+
const getCoord = React.useCallback((e) => (isVertical ? e.clientY : e.clientX), [isVertical]);
|
|
79
100
|
// #region Real Index
|
|
80
101
|
const realIndex = React.useMemo(() => {
|
|
81
|
-
if (!
|
|
102
|
+
if (!isLoop) {
|
|
82
103
|
return index;
|
|
83
104
|
}
|
|
84
|
-
const realIndex = index -
|
|
105
|
+
const realIndex = index - clones;
|
|
85
106
|
if (realIndex < 0) {
|
|
86
107
|
return realSlidesCount + realIndex;
|
|
87
108
|
}
|
|
@@ -90,7 +111,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
90
111
|
}
|
|
91
112
|
// In the infinite mode any slide can be the first visible one, so there is no clamp
|
|
92
113
|
return realIndex;
|
|
93
|
-
}, [index,
|
|
114
|
+
}, [index, isLoop, clones, realSlidesCount]);
|
|
94
115
|
const prevRealIndex = React.useRef(realIndex);
|
|
95
116
|
// #region Effects
|
|
96
117
|
React.useEffect(() => {
|
|
@@ -103,18 +124,18 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
103
124
|
if (!mounted.current)
|
|
104
125
|
return;
|
|
105
126
|
// The slides were removed: keep the index inside the allowed range
|
|
106
|
-
const lastIndex =
|
|
127
|
+
const lastIndex = isLoop ? total - clones : maxIndex;
|
|
107
128
|
if (index > lastIndex) {
|
|
108
|
-
setIndex(
|
|
129
|
+
setIndex(isLoop ? clones : maxIndex);
|
|
109
130
|
setOffset(0);
|
|
110
131
|
}
|
|
111
|
-
}, [index,
|
|
132
|
+
}, [index, isLoop, total, clones, maxIndex]);
|
|
112
133
|
// #region Normalization
|
|
113
134
|
const normalizeIndex = React.useCallback(() => {
|
|
114
|
-
if (!
|
|
135
|
+
if (!isLoop || !mounted.current)
|
|
115
136
|
return;
|
|
116
|
-
const firstRealIndex =
|
|
117
|
-
const lastRealIndex = total -
|
|
137
|
+
const firstRealIndex = clones;
|
|
138
|
+
const lastRealIndex = total - clones - 1;
|
|
118
139
|
if (index < firstRealIndex || index > lastRealIndex) {
|
|
119
140
|
// A clone position shows the same slides as the real position shifted by the slides count.
|
|
120
141
|
// Not always the first/last real slide: after several fast clicks the index may be deeper in the clones
|
|
@@ -134,7 +155,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
134
155
|
});
|
|
135
156
|
});
|
|
136
157
|
}
|
|
137
|
-
}, [index,
|
|
158
|
+
}, [index, isLoop, clones, total, realSlidesCount]);
|
|
138
159
|
// #region Navigation API
|
|
139
160
|
const indexRef = React.useRef(index);
|
|
140
161
|
React.useEffect(() => {
|
|
@@ -143,13 +164,13 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
143
164
|
const goToIndex = React.useCallback((i) => {
|
|
144
165
|
// In the infinite mode the index is normalized after the transition, so wait for it.
|
|
145
166
|
// In the regular mode the transition is simply retargeted
|
|
146
|
-
if (
|
|
167
|
+
if (isLoop && isAnimating.current)
|
|
147
168
|
return;
|
|
148
169
|
let targetIndex = i;
|
|
149
|
-
if (
|
|
150
|
-
targetIndex = i +
|
|
151
|
-
const minIndex =
|
|
152
|
-
const maxIndex =
|
|
170
|
+
if (isLoop) {
|
|
171
|
+
targetIndex = i + clones;
|
|
172
|
+
const minIndex = clones;
|
|
173
|
+
const maxIndex = clones + realSlidesCount - 1;
|
|
153
174
|
targetIndex = Math.max(minIndex, Math.min(maxIndex, targetIndex));
|
|
154
175
|
}
|
|
155
176
|
else {
|
|
@@ -163,17 +184,20 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
163
184
|
setOffset(0);
|
|
164
185
|
setIndex(targetIndex);
|
|
165
186
|
isAnimating.current = true;
|
|
166
|
-
}, [
|
|
187
|
+
}, [isLoop, clones, realSlidesCount, maxIndex]);
|
|
167
188
|
const next = React.useCallback(() => {
|
|
168
189
|
setDisableAnimation(false);
|
|
169
190
|
setIsMomentum(false);
|
|
170
191
|
setOffset(0);
|
|
171
192
|
setIndex(prev => {
|
|
172
193
|
let nextIndex;
|
|
173
|
-
if (
|
|
194
|
+
if (isLoop) {
|
|
174
195
|
// The last position is the first tail clone: it mirrors the first real slide.
|
|
175
196
|
// Further positions would show the empty space when slidesPerView > 1
|
|
176
|
-
nextIndex = Math.min(prev + 1, total -
|
|
197
|
+
nextIndex = Math.min(prev + 1, total - clones);
|
|
198
|
+
}
|
|
199
|
+
else if (isFadeLoop) {
|
|
200
|
+
nextIndex = (prev + 1) % total;
|
|
177
201
|
}
|
|
178
202
|
else {
|
|
179
203
|
nextIndex = Math.min(prev + 1, maxIndex);
|
|
@@ -185,50 +209,61 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
185
209
|
isAnimating.current = true;
|
|
186
210
|
return nextIndex;
|
|
187
211
|
});
|
|
188
|
-
}, [
|
|
212
|
+
}, [isLoop, isFadeLoop, maxIndex, total, clones]);
|
|
189
213
|
const prev = React.useCallback(() => {
|
|
190
214
|
setDisableAnimation(false);
|
|
191
215
|
setIsMomentum(false);
|
|
192
216
|
setOffset(0);
|
|
193
217
|
setIndex(prev => {
|
|
194
|
-
const nextIndex = Math.max(prev - 1, 0);
|
|
218
|
+
const nextIndex = isFadeLoop ? (prev - 1 + total) % total : Math.max(prev - 1, 0);
|
|
195
219
|
if (nextIndex === prev) {
|
|
196
220
|
return prev;
|
|
197
221
|
}
|
|
198
222
|
isAnimating.current = true;
|
|
199
223
|
return nextIndex;
|
|
200
224
|
});
|
|
201
|
-
}, []);
|
|
225
|
+
}, [isFadeLoop, total]);
|
|
202
226
|
// #region Position
|
|
203
|
-
// The position is the distance in pixels from the start of the track to the
|
|
204
|
-
// The state keeps it as index (whole slides) + offset (pixels)
|
|
205
|
-
const
|
|
227
|
+
// The position is the distance in pixels from the start of the track to the start of the view
|
|
228
|
+
// without the centering shift. The state keeps it as index (whole slides) + offset (pixels)
|
|
229
|
+
const getSlideSize = React.useCallback(() => {
|
|
230
|
+
const wrapper = wrapperRef.current;
|
|
231
|
+
const size = (isVertical ? wrapper === null || wrapper === void 0 ? void 0 : wrapper.clientHeight : wrapper === null || wrapper === void 0 ? void 0 : wrapper.clientWidth) || 0;
|
|
232
|
+
return size / slidesPerView;
|
|
233
|
+
}, [isVertical, slidesPerView]);
|
|
206
234
|
// The last allowed position. In the infinite mode it is the first tail clone
|
|
207
|
-
const maxPosition = React.useCallback(() => (
|
|
235
|
+
const maxPosition = React.useCallback(() => (isLoop ? total - clones : lastPosition) * getSlideSize(), [isLoop, total, clones, lastPosition, getSlideSize]);
|
|
236
|
+
// The index of the track: without the loop the track stops at the last position
|
|
237
|
+
// even if the index is the next whole number
|
|
238
|
+
const trackIndex = (isLoop ? index : Math.min(index, lastPosition)) - centerShift;
|
|
208
239
|
// The position on the screen right now, also in the middle of a transition
|
|
209
240
|
const readPosition = React.useCallback(() => {
|
|
210
241
|
const track = trackRef.current;
|
|
242
|
+
const size = getSlideSize();
|
|
211
243
|
if (track && typeof DOMMatrixReadOnly !== 'undefined') {
|
|
212
244
|
const { transform } = getComputedStyle(track);
|
|
213
245
|
if (transform && transform !== 'none') {
|
|
214
|
-
|
|
246
|
+
const matrix = new DOMMatrixReadOnly(transform);
|
|
247
|
+
return -(isVertical ? matrix.m42 : matrix.m41) + centerShift * size;
|
|
215
248
|
}
|
|
216
249
|
}
|
|
217
|
-
return
|
|
218
|
-
}, [
|
|
250
|
+
return (trackIndex + centerShift) * size - offset;
|
|
251
|
+
}, [trackIndex, centerShift, offset, getSlideSize, isVertical]);
|
|
219
252
|
// Infinite mode: the same view is shown by the real slides and by the clones,
|
|
220
253
|
// so the position is moved by the whole slides count when it comes close to the edge of the track
|
|
221
254
|
const wrapPosition = React.useCallback((position) => {
|
|
222
|
-
const
|
|
223
|
-
const span = realSlidesCount *
|
|
224
|
-
if (!
|
|
255
|
+
const size = getSlideSize();
|
|
256
|
+
const span = realSlidesCount * size;
|
|
257
|
+
if (!isLoop || size === 0)
|
|
225
258
|
return 0;
|
|
226
|
-
if (position <
|
|
259
|
+
if (position < size * 0.5)
|
|
227
260
|
return span;
|
|
228
|
-
if (position > maxPosition() -
|
|
261
|
+
if (position > maxPosition() - size * 0.5)
|
|
229
262
|
return -span;
|
|
230
263
|
return 0;
|
|
231
|
-
}, [
|
|
264
|
+
}, [isLoop, realSlidesCount, getSlideSize, maxPosition]);
|
|
265
|
+
// Without the loop the index between the slides does not go past the last position
|
|
266
|
+
const clampFreeIndex = React.useCallback((i) => (isLoop ? i : Math.max(0, Math.min(maxFreeIndex, i))), [isLoop, maxFreeIndex]);
|
|
232
267
|
const dragBasePosition = React.useRef(0);
|
|
233
268
|
const dragAnchorIndex = React.useRef(0);
|
|
234
269
|
const dragPosition = React.useRef(0);
|
|
@@ -239,49 +274,61 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
239
274
|
// Only the main mouse button, the touch or the pen
|
|
240
275
|
if (e.pointerType === 'mouse' && e.button !== 0)
|
|
241
276
|
return;
|
|
242
|
-
|
|
277
|
+
suppressClick.current = false;
|
|
278
|
+
dragging.current = true;
|
|
279
|
+
startCoord.current = getCoord(e);
|
|
280
|
+
lastCoord.current = getCoord(e);
|
|
281
|
+
lastTime.current = performance.now();
|
|
282
|
+
velocity.current = 0;
|
|
283
|
+
// The capture fails for a pointer that is already gone: the drag still works without it
|
|
284
|
+
try {
|
|
285
|
+
e.target.setPointerCapture(e.pointerId);
|
|
286
|
+
}
|
|
287
|
+
catch (_a) {
|
|
288
|
+
// nothing
|
|
289
|
+
}
|
|
290
|
+
// The fade does not move the track: the slide changes after the release
|
|
291
|
+
if (isFade)
|
|
292
|
+
return;
|
|
293
|
+
const size = getSlideSize();
|
|
243
294
|
// Grab the track where it is now: a running transition stops under the finger instead of jumping
|
|
244
295
|
let position = readPosition();
|
|
245
296
|
position += wrapPosition(position);
|
|
246
|
-
const anchor =
|
|
297
|
+
const anchor = clampFreeIndex(size > 0 ? Math.round(position / size) : index);
|
|
247
298
|
dragBasePosition.current = position;
|
|
248
299
|
dragPosition.current = position;
|
|
249
300
|
dragAnchorIndex.current = anchor;
|
|
250
|
-
suppressClick.current = false;
|
|
251
|
-
dragging.current = true;
|
|
252
301
|
isAnimating.current = false;
|
|
253
302
|
setDisableAnimation(true);
|
|
254
303
|
setIsMomentum(false);
|
|
255
304
|
setIsDragging(true);
|
|
256
305
|
setIndex(anchor);
|
|
257
|
-
setOffset(anchor *
|
|
258
|
-
|
|
259
|
-
lastX.current = e.clientX;
|
|
260
|
-
lastTime.current = performance.now();
|
|
261
|
-
velocity.current = 0;
|
|
262
|
-
e.target.setPointerCapture(e.pointerId);
|
|
263
|
-
}, [draggable, getSlideWidth, readPosition, wrapPosition, index]);
|
|
306
|
+
setOffset(anchor * size - position);
|
|
307
|
+
}, [draggable, isFade, getCoord, getSlideSize, readPosition, wrapPosition, clampFreeIndex, index]);
|
|
264
308
|
const onPointerMove = React.useCallback((e) => {
|
|
265
309
|
if (!dragging.current)
|
|
266
310
|
return;
|
|
311
|
+
const coord = getCoord(e);
|
|
267
312
|
const now = performance.now();
|
|
268
|
-
const
|
|
313
|
+
const delta = coord - lastCoord.current;
|
|
269
314
|
const dt = now - lastTime.current;
|
|
270
315
|
if (dt > 0) {
|
|
271
|
-
velocity.current =
|
|
316
|
+
velocity.current = delta / dt;
|
|
272
317
|
}
|
|
273
|
-
|
|
318
|
+
lastCoord.current = coord;
|
|
274
319
|
lastTime.current = now;
|
|
275
|
-
|
|
276
|
-
|
|
320
|
+
if (isFade)
|
|
321
|
+
return;
|
|
322
|
+
const size = getSlideSize();
|
|
323
|
+
let position = dragBasePosition.current - (coord - startCoord.current);
|
|
277
324
|
// Endless drag in the infinite mode: move the base together with the position
|
|
278
325
|
const shift = wrapPosition(position);
|
|
279
326
|
if (shift !== 0) {
|
|
280
327
|
dragBasePosition.current += shift;
|
|
281
|
-
dragAnchorIndex.current += Math.round(shift /
|
|
328
|
+
dragAnchorIndex.current += Math.round(shift / size);
|
|
282
329
|
position += shift;
|
|
283
330
|
}
|
|
284
|
-
if (!
|
|
331
|
+
if (!isLoop) {
|
|
285
332
|
const max = maxPosition();
|
|
286
333
|
if (position < 0) {
|
|
287
334
|
position = resistance ? position * 0.3 : 0;
|
|
@@ -293,27 +340,55 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
293
340
|
dragPosition.current = position;
|
|
294
341
|
// snap: the index stays the same during the drag, the slide changes after the release.
|
|
295
342
|
// free: the index follows the position, so onSlideChange reports the slide under the view
|
|
296
|
-
const nextIndex = snap
|
|
343
|
+
const nextIndex = snap
|
|
344
|
+
? dragAnchorIndex.current
|
|
345
|
+
: clampFreeIndex(Math.round(position / size));
|
|
297
346
|
setIndex(nextIndex);
|
|
298
|
-
setOffset(nextIndex *
|
|
299
|
-
}, [
|
|
347
|
+
setOffset(nextIndex * size - position);
|
|
348
|
+
}, [
|
|
349
|
+
getCoord,
|
|
350
|
+
isFade,
|
|
351
|
+
getSlideSize,
|
|
352
|
+
wrapPosition,
|
|
353
|
+
isLoop,
|
|
354
|
+
maxPosition,
|
|
355
|
+
resistance,
|
|
356
|
+
snap,
|
|
357
|
+
clampFreeIndex,
|
|
358
|
+
]);
|
|
300
359
|
const onPointerUp = React.useCallback((e) => {
|
|
301
360
|
if (!dragging.current)
|
|
302
361
|
return;
|
|
303
|
-
const
|
|
304
|
-
const position = dragPosition.current;
|
|
305
|
-
const delta = lastX.current - startX.current;
|
|
306
|
-
// The pointer stopped before the release: no inertia
|
|
307
|
-
const v = performance.now() - lastTime.current > 100 ? 0 : velocity.current;
|
|
308
|
-
const lastIndex = infinite ? total - slidesPerView : maxIndex;
|
|
362
|
+
const delta = lastCoord.current - startCoord.current;
|
|
309
363
|
// It was a drag, not a click: the click on a link or a button inside the slide must not fire
|
|
310
364
|
if (Math.abs(delta) > threshold) {
|
|
311
365
|
suppressClick.current = true;
|
|
312
366
|
}
|
|
367
|
+
dragging.current = false;
|
|
368
|
+
try {
|
|
369
|
+
e.target.releasePointerCapture(e.pointerId);
|
|
370
|
+
}
|
|
371
|
+
catch (_a) {
|
|
372
|
+
// nothing
|
|
373
|
+
}
|
|
374
|
+
if (isFade) {
|
|
375
|
+
if (delta < -threshold) {
|
|
376
|
+
next();
|
|
377
|
+
}
|
|
378
|
+
else if (delta > threshold) {
|
|
379
|
+
prev();
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
const size = getSlideSize();
|
|
384
|
+
const position = dragPosition.current;
|
|
385
|
+
// The pointer stopped before the release: no inertia
|
|
386
|
+
const v = performance.now() - lastTime.current > 100 ? 0 : velocity.current;
|
|
387
|
+
const lastIndex = isLoop ? total - clones : maxIndex;
|
|
313
388
|
setDisableAnimation(false);
|
|
314
389
|
if (snap) {
|
|
315
390
|
const anchor = dragAnchorIndex.current;
|
|
316
|
-
const displacement = position - anchor *
|
|
391
|
+
const displacement = position - anchor * size;
|
|
317
392
|
const velocityFactor = Math.min(Math.abs(v) * 250, 0.65);
|
|
318
393
|
const dynamicThreshold = Math.max(threshold, Math.min(dragThreshold * (1 - velocityFactor), dragThreshold));
|
|
319
394
|
let nextIndex = anchor;
|
|
@@ -321,37 +396,42 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
321
396
|
// At least one slide, more if the slide was dragged further
|
|
322
397
|
nextIndex =
|
|
323
398
|
displacement > 0
|
|
324
|
-
? Math.max(anchor + 1, Math.round(position /
|
|
325
|
-
: Math.min(anchor - 1, Math.round(position /
|
|
399
|
+
? Math.max(anchor + 1, Math.round(position / size))
|
|
400
|
+
: Math.min(anchor - 1, Math.round(position / size));
|
|
326
401
|
}
|
|
327
402
|
nextIndex = Math.max(0, Math.min(lastIndex, nextIndex));
|
|
328
403
|
setIndex(nextIndex);
|
|
329
404
|
setOffset(0);
|
|
330
405
|
// A click without movement does not start a transition, so transitionend never comes
|
|
331
406
|
// and the flag would block autoplay and goToIndex forever
|
|
332
|
-
|
|
407
|
+
const target = (isLoop ? nextIndex : Math.min(nextIndex, lastPosition)) * size;
|
|
408
|
+
isAnimating.current = Math.abs(target - position) > 0.5;
|
|
333
409
|
}
|
|
334
410
|
else {
|
|
335
411
|
// Free mode: the track keeps moving by inertia and stops where it stops
|
|
336
|
-
const
|
|
337
|
-
const
|
|
412
|
+
const maxProjected = (isLoop ? lastIndex : lastPosition) * size;
|
|
413
|
+
const projected = Math.max(0, Math.min(maxProjected, position - v * 300));
|
|
414
|
+
const nextIndex = clampFreeIndex(size > 0 ? Math.round(projected / size) : index);
|
|
338
415
|
setIsMomentum(true);
|
|
339
416
|
setIndex(nextIndex);
|
|
340
|
-
setOffset(nextIndex *
|
|
417
|
+
setOffset(nextIndex * size - projected);
|
|
341
418
|
isAnimating.current = Math.abs(projected - position) > 0.5;
|
|
342
419
|
}
|
|
343
|
-
dragging.current = false;
|
|
344
420
|
setIsDragging(false);
|
|
345
|
-
e.target.releasePointerCapture(e.pointerId);
|
|
346
421
|
}, [
|
|
347
|
-
|
|
348
|
-
|
|
422
|
+
isFade,
|
|
423
|
+
next,
|
|
424
|
+
prev,
|
|
425
|
+
getSlideSize,
|
|
426
|
+
isLoop,
|
|
349
427
|
total,
|
|
350
|
-
|
|
428
|
+
clones,
|
|
351
429
|
maxIndex,
|
|
430
|
+
lastPosition,
|
|
352
431
|
threshold,
|
|
353
432
|
snap,
|
|
354
433
|
dragThreshold,
|
|
434
|
+
clampFreeIndex,
|
|
355
435
|
index,
|
|
356
436
|
]);
|
|
357
437
|
const onLostPointerCapture = React.useCallback((e) => {
|
|
@@ -363,7 +443,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
363
443
|
const autoplayStep = React.useRef(() => undefined);
|
|
364
444
|
autoplayStep.current = () => {
|
|
365
445
|
// Without the infinite loop the autoplay returns to the first slide instead of stopping at the end
|
|
366
|
-
if (!
|
|
446
|
+
if (!isLoop && !isFadeLoop && indexRef.current >= maxIndex) {
|
|
367
447
|
goToIndex(0);
|
|
368
448
|
}
|
|
369
449
|
else {
|
|
@@ -373,7 +453,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
373
453
|
const pause = React.useCallback(() => setIsPausedByApi(true), []);
|
|
374
454
|
const resume = React.useCallback(() => setIsPausedByApi(false), []);
|
|
375
455
|
React.useEffect(() => {
|
|
376
|
-
if (!autoplay || isPaused)
|
|
456
|
+
if (!autoplay || isTicker || isPaused)
|
|
377
457
|
return undefined;
|
|
378
458
|
const timer = setInterval(() => {
|
|
379
459
|
if (!dragging.current && !isAnimating.current && mounted.current) {
|
|
@@ -381,13 +461,57 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
381
461
|
}
|
|
382
462
|
}, autoplayInterval);
|
|
383
463
|
return () => clearInterval(timer);
|
|
384
|
-
}, [autoplay, autoplayInterval, isPaused]);
|
|
464
|
+
}, [autoplay, isTicker, autoplayInterval, isPaused]);
|
|
465
|
+
// #region Ticker
|
|
466
|
+
// The continuous scroll writes the transform directly: 60 renders per second are not needed for it.
|
|
467
|
+
// The position is kept in slides, so it stays in place when the size of the swiper changes
|
|
468
|
+
const tickerPosition = React.useRef(null);
|
|
469
|
+
React.useEffect(() => {
|
|
470
|
+
const track = trackRef.current;
|
|
471
|
+
if (!isTicker || !isLoop || !track)
|
|
472
|
+
return undefined;
|
|
473
|
+
if (isPaused || prefersReducedMotion())
|
|
474
|
+
return undefined;
|
|
475
|
+
let frame = 0;
|
|
476
|
+
let last = performance.now();
|
|
477
|
+
const step = (now) => {
|
|
478
|
+
var _a;
|
|
479
|
+
const size = getSlideSize();
|
|
480
|
+
if (size > 0) {
|
|
481
|
+
let position = (_a = tickerPosition.current) !== null && _a !== void 0 ? _a : clones;
|
|
482
|
+
position += ((autoScroll || 0) * (now - last)) / 1000 / size;
|
|
483
|
+
if (position >= clones + realSlidesCount) {
|
|
484
|
+
position -= realSlidesCount;
|
|
485
|
+
}
|
|
486
|
+
tickerPosition.current = position;
|
|
487
|
+
const translate = -(position - centerShift) * size;
|
|
488
|
+
track.style.transition = 'none';
|
|
489
|
+
track.style.transform = isVertical
|
|
490
|
+
? `translateY(${translate}px)`
|
|
491
|
+
: `translateX(${translate}px)`;
|
|
492
|
+
}
|
|
493
|
+
last = now;
|
|
494
|
+
frame = requestAnimationFrame(step);
|
|
495
|
+
};
|
|
496
|
+
frame = requestAnimationFrame(step);
|
|
497
|
+
return () => cancelAnimationFrame(frame);
|
|
498
|
+
}, [
|
|
499
|
+
isTicker,
|
|
500
|
+
isLoop,
|
|
501
|
+
isPaused,
|
|
502
|
+
autoScroll,
|
|
503
|
+
getSlideSize,
|
|
504
|
+
realSlidesCount,
|
|
505
|
+
clones,
|
|
506
|
+
centerShift,
|
|
507
|
+
isVertical,
|
|
508
|
+
]);
|
|
385
509
|
// #region Hover and focus
|
|
386
510
|
// The autoplay pauses while the pointer is over the swiper (pauseOnHover)
|
|
387
511
|
// and always while the focus is inside it, so the keyboard user is not interrupted
|
|
388
512
|
React.useEffect(() => {
|
|
389
513
|
const wrapper = wrapperRef.current;
|
|
390
|
-
if (!autoplay || !wrapper)
|
|
514
|
+
if (!(autoplay || isTicker) || !wrapper)
|
|
391
515
|
return undefined;
|
|
392
516
|
const handleMouseEnter = () => pauseOnHover && setIsHovered(true);
|
|
393
517
|
const handleMouseLeave = () => setIsHovered(false);
|
|
@@ -407,7 +531,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
407
531
|
wrapper.removeEventListener('focusin', handleFocusIn);
|
|
408
532
|
wrapper.removeEventListener('focusout', handleFocusOut);
|
|
409
533
|
};
|
|
410
|
-
}, [autoplay, pauseOnHover]);
|
|
534
|
+
}, [autoplay, isTicker, pauseOnHover]);
|
|
411
535
|
// #region Keyboard
|
|
412
536
|
const onKeyDown = React.useCallback((event) => {
|
|
413
537
|
if (!keyboardControl)
|
|
@@ -416,15 +540,15 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
416
540
|
const target = event.target;
|
|
417
541
|
if (target.closest('input, textarea, select, [contenteditable="true"]'))
|
|
418
542
|
return;
|
|
419
|
-
if (event.key === 'ArrowLeft') {
|
|
543
|
+
if (event.key === (isVertical ? 'ArrowUp' : 'ArrowLeft')) {
|
|
420
544
|
event.preventDefault();
|
|
421
545
|
prev();
|
|
422
546
|
}
|
|
423
|
-
if (event.key === 'ArrowRight') {
|
|
547
|
+
if (event.key === (isVertical ? 'ArrowDown' : 'ArrowRight')) {
|
|
424
548
|
event.preventDefault();
|
|
425
549
|
next();
|
|
426
550
|
}
|
|
427
|
-
}, [keyboardControl, prev, next]);
|
|
551
|
+
}, [keyboardControl, isVertical, prev, next]);
|
|
428
552
|
const onClickCapture = React.useCallback((event) => {
|
|
429
553
|
if (suppressClick.current) {
|
|
430
554
|
suppressClick.current = false;
|
|
@@ -443,14 +567,16 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
443
567
|
const track = trackRef.current;
|
|
444
568
|
if (!track)
|
|
445
569
|
return;
|
|
570
|
+
// The fade changes the opacity of the slides, the event bubbles up to the track
|
|
571
|
+
const property = isFade ? 'opacity' : 'transform';
|
|
446
572
|
const handleTransitionEnd = (e) => {
|
|
447
|
-
if (e.propertyName !==
|
|
573
|
+
if (e.propertyName !== property)
|
|
448
574
|
return;
|
|
449
575
|
isAnimating.current = false;
|
|
450
|
-
normalizeIndex();
|
|
576
|
+
normalizeIndex();
|
|
451
577
|
};
|
|
452
578
|
const handleTransitionCancel = (e) => {
|
|
453
|
-
if (e.propertyName !==
|
|
579
|
+
if (e.propertyName !== property)
|
|
454
580
|
return;
|
|
455
581
|
isAnimating.current = false;
|
|
456
582
|
};
|
|
@@ -460,7 +586,7 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
460
586
|
track.removeEventListener('transitionend', handleTransitionEnd);
|
|
461
587
|
track.removeEventListener('transitioncancel', handleTransitionCancel);
|
|
462
588
|
};
|
|
463
|
-
}, [normalizeIndex]);
|
|
589
|
+
}, [normalizeIndex, isFade]);
|
|
464
590
|
// #region Mount
|
|
465
591
|
React.useEffect(() => {
|
|
466
592
|
mounted.current = true;
|
|
@@ -485,34 +611,41 @@ export const Swiper = React.forwardRef((props, ref) => {
|
|
|
485
611
|
Wrapper: (overrides === null || overrides === void 0 ? void 0 : overrides.Wrapper) || Wrapper,
|
|
486
612
|
}), [overrides]);
|
|
487
613
|
const getSlideRealIndex = React.useCallback((domIndex) => {
|
|
488
|
-
if (!
|
|
614
|
+
if (!isLoop) {
|
|
489
615
|
return domIndex;
|
|
490
616
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
return totalSlides - slidesPerView + domIndex;
|
|
617
|
+
if (domIndex < clones) {
|
|
618
|
+
return realSlidesCount - clones + domIndex;
|
|
494
619
|
}
|
|
495
|
-
if (domIndex >= total -
|
|
496
|
-
return domIndex - (total -
|
|
620
|
+
if (domIndex >= total - clones) {
|
|
621
|
+
return domIndex - (total - clones);
|
|
497
622
|
}
|
|
498
|
-
return domIndex -
|
|
499
|
-
}, [
|
|
623
|
+
return domIndex - clones;
|
|
624
|
+
}, [isLoop, realSlidesCount, clones, total]);
|
|
500
625
|
// The slides at least partially in the view: they are not inert, so a partially visible slide
|
|
501
|
-
// can be clicked. Between the slides (drag, free mode)
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
626
|
+
// can be clicked. Between the slides (drag, free mode), with a fractional slidesPerView
|
|
627
|
+
// and with the centering one more slide is partially visible
|
|
628
|
+
const wrapper = wrapperRef.current;
|
|
629
|
+
const slideSize = wrapper
|
|
630
|
+
? (isVertical ? wrapper.clientHeight : wrapper.clientWidth) / slidesPerView
|
|
631
|
+
: 0;
|
|
632
|
+
const viewPosition = slideSize > 0 ? trackIndex - offset / slideSize : trackIndex;
|
|
633
|
+
let firstVisibleDomIndex = Math.floor(viewPosition + 0.001);
|
|
634
|
+
let visibleCount = Math.ceil(viewPosition + slidesPerView - 0.001) - firstVisibleDomIndex;
|
|
635
|
+
if (isFade) {
|
|
636
|
+
firstVisibleDomIndex = index;
|
|
637
|
+
visibleCount = 1;
|
|
510
638
|
}
|
|
511
639
|
firstVisibleDomIndex = Math.max(0, Math.min(total - 1, firstVisibleDomIndex));
|
|
640
|
+
// The ticker moves without the state: all the slides are rendered
|
|
641
|
+
if (isTicker) {
|
|
642
|
+
firstVisibleDomIndex = clones;
|
|
643
|
+
visibleCount = realSlidesCount;
|
|
644
|
+
}
|
|
512
645
|
return (React.createElement(overridesMap.Container, Object.assign({ role: "region", "aria-roledescription": "carousel" }, restProps),
|
|
513
|
-
React.createElement(overridesMap.Wrapper, { ref: wrapperRef, draggable: draggable, slidesPerView: slidesPerView, tabIndex: keyboardControl ? 0 : undefined, "aria-live": autoplay && !isPaused ? 'off' : 'polite', onKeyDown: onKeyDown, onClickCapture: onClickCapture, onDragStart: onDragStart, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onLostPointerCapture: onLostPointerCapture },
|
|
514
|
-
React.createElement(overridesMap.Track, { ref: trackRef, dragging: isDragging, index:
|
|
515
|
-
React.createElement(SwiperSlidesRenderer, { slides: slides, slidesPerView: slidesPerView, realIndex: realIndex, firstVisibleIndex: getSlideRealIndex(firstVisibleDomIndex), visibleCount: visibleCount, realSlidesCount: realSlidesCount, getSlideRealIndex: getSlideRealIndex, slideLabel: slideLabel })))));
|
|
646
|
+
React.createElement(overridesMap.Wrapper, { ref: wrapperRef, draggable: draggable, slidesPerView: slidesPerView, vertical: isVertical, tabIndex: keyboardControl ? 0 : undefined, "aria-live": (autoplay || isTicker) && !isPaused ? 'off' : 'polite', onKeyDown: onKeyDown, onClickCapture: onClickCapture, onDragStart: onDragStart, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onLostPointerCapture: onLostPointerCapture },
|
|
647
|
+
React.createElement(overridesMap.Track, { ref: trackRef, dragging: isDragging, index: isFade ? 0 : trackIndex, offset: isFade ? 0 : offset, disableAnimation: disableAnimation, momentum: isMomentum, slidesPerView: slidesPerView, vertical: isVertical, effect: effect, speed: speed },
|
|
648
|
+
React.createElement(SwiperSlidesRenderer, { slides: slides, slidesPerView: slidesPerView, realIndex: realIndex, firstVisibleIndex: getSlideRealIndex(firstVisibleDomIndex), visibleCount: visibleCount, realSlidesCount: realSlidesCount, getSlideRealIndex: getSlideRealIndex, slideLabel: slideLabel, activeDomIndex: isFade ? index : null })))));
|
|
516
649
|
});
|
|
517
650
|
Swiper.displayName = 'Swiper';
|
|
518
651
|
export default Swiper;
|