@silverlila/slider 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +434 -2
- package/dist/chunk-OAWJ54V3.js +765 -0
- package/dist/core.d.ts +114 -0
- package/dist/core.js +14 -0
- package/dist/index.d.ts +117 -0
- package/dist/index.js +457 -0
- package/dist/styles.css +30 -0
- package/package.json +88 -4
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
// src/core/autoplay.ts
|
|
2
|
+
var DEFAULT_DELAY = 4e3;
|
|
3
|
+
var INTERACTIONS = ["pointerdown", "wheel", "keydown"];
|
|
4
|
+
function autoplay(slider, options = {}) {
|
|
5
|
+
const delay = options.delay ?? DEFAULT_DELAY;
|
|
6
|
+
if (!(delay > 0)) throw new RangeError(`delay must be > 0, got ${delay}`);
|
|
7
|
+
const root = options.root ?? slider.viewport;
|
|
8
|
+
const pauseOnHover = options.pauseOnHover ?? true;
|
|
9
|
+
const stopOnInteraction = options.stopOnInteraction ?? true;
|
|
10
|
+
const { viewport } = slider;
|
|
11
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
12
|
+
let playing = !matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
13
|
+
let hovered = false;
|
|
14
|
+
let focused = false;
|
|
15
|
+
let pressed = false;
|
|
16
|
+
let timer = null;
|
|
17
|
+
let index = slider.getState().index;
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
function isPaused() {
|
|
20
|
+
return hovered || focused || pressed || document.visibilityState === "hidden";
|
|
21
|
+
}
|
|
22
|
+
function schedule() {
|
|
23
|
+
if (timer !== null) clearTimeout(timer);
|
|
24
|
+
timer = playing && !isPaused() ? setTimeout(advance, delay) : null;
|
|
25
|
+
}
|
|
26
|
+
function advance() {
|
|
27
|
+
timer = null;
|
|
28
|
+
if (slider.getState().canNext) slider.next();
|
|
29
|
+
else slider.scrollTo(0);
|
|
30
|
+
schedule();
|
|
31
|
+
}
|
|
32
|
+
function setPlaying(value) {
|
|
33
|
+
if (playing === value) return;
|
|
34
|
+
playing = value;
|
|
35
|
+
schedule();
|
|
36
|
+
for (const listener of listeners) listener(playing);
|
|
37
|
+
}
|
|
38
|
+
function onSliderChange(state) {
|
|
39
|
+
if (state.index === index) return;
|
|
40
|
+
index = state.index;
|
|
41
|
+
schedule();
|
|
42
|
+
}
|
|
43
|
+
function onPointerEnter() {
|
|
44
|
+
hovered = true;
|
|
45
|
+
schedule();
|
|
46
|
+
}
|
|
47
|
+
function onPointerLeave() {
|
|
48
|
+
hovered = false;
|
|
49
|
+
schedule();
|
|
50
|
+
}
|
|
51
|
+
function onFocusIn(event) {
|
|
52
|
+
focused = event.target instanceof Element && event.target.matches(":focus-visible");
|
|
53
|
+
schedule();
|
|
54
|
+
}
|
|
55
|
+
function onFocusOut(event) {
|
|
56
|
+
focused = event.relatedTarget instanceof Node && root.contains(event.relatedTarget);
|
|
57
|
+
schedule();
|
|
58
|
+
}
|
|
59
|
+
function onPointerDown() {
|
|
60
|
+
pressed = true;
|
|
61
|
+
schedule();
|
|
62
|
+
}
|
|
63
|
+
function onPointerUp() {
|
|
64
|
+
pressed = false;
|
|
65
|
+
schedule();
|
|
66
|
+
}
|
|
67
|
+
function onInteraction() {
|
|
68
|
+
setPlaying(false);
|
|
69
|
+
}
|
|
70
|
+
const unsubscribe = slider.subscribe(onSliderChange);
|
|
71
|
+
if (pauseOnHover) {
|
|
72
|
+
root.addEventListener("pointerenter", onPointerEnter);
|
|
73
|
+
root.addEventListener("pointerleave", onPointerLeave);
|
|
74
|
+
}
|
|
75
|
+
root.addEventListener("focusin", onFocusIn);
|
|
76
|
+
root.addEventListener("focusout", onFocusOut);
|
|
77
|
+
viewport.addEventListener("pointerdown", onPointerDown);
|
|
78
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
79
|
+
window.addEventListener("pointercancel", onPointerUp);
|
|
80
|
+
document.addEventListener("visibilitychange", schedule);
|
|
81
|
+
if (stopOnInteraction) {
|
|
82
|
+
for (const type of INTERACTIONS) viewport.addEventListener(type, onInteraction);
|
|
83
|
+
}
|
|
84
|
+
schedule();
|
|
85
|
+
function assertAlive() {
|
|
86
|
+
if (destroyed) throw new Error("Autoplay was destroyed");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
play() {
|
|
90
|
+
assertAlive();
|
|
91
|
+
hovered = false;
|
|
92
|
+
focused = false;
|
|
93
|
+
setPlaying(true);
|
|
94
|
+
schedule();
|
|
95
|
+
},
|
|
96
|
+
stop() {
|
|
97
|
+
assertAlive();
|
|
98
|
+
setPlaying(false);
|
|
99
|
+
},
|
|
100
|
+
isPlaying() {
|
|
101
|
+
assertAlive();
|
|
102
|
+
return playing;
|
|
103
|
+
},
|
|
104
|
+
subscribe(listener) {
|
|
105
|
+
assertAlive();
|
|
106
|
+
listeners.add(listener);
|
|
107
|
+
return () => {
|
|
108
|
+
listeners.delete(listener);
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
destroy() {
|
|
112
|
+
if (destroyed) return;
|
|
113
|
+
destroyed = true;
|
|
114
|
+
if (timer !== null) clearTimeout(timer);
|
|
115
|
+
timer = null;
|
|
116
|
+
unsubscribe();
|
|
117
|
+
root.removeEventListener("pointerenter", onPointerEnter);
|
|
118
|
+
root.removeEventListener("pointerleave", onPointerLeave);
|
|
119
|
+
root.removeEventListener("focusin", onFocusIn);
|
|
120
|
+
root.removeEventListener("focusout", onFocusOut);
|
|
121
|
+
viewport.removeEventListener("pointerdown", onPointerDown);
|
|
122
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
123
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
124
|
+
document.removeEventListener("visibilitychange", schedule);
|
|
125
|
+
for (const type of INTERACTIONS) viewport.removeEventListener(type, onInteraction);
|
|
126
|
+
listeners.clear();
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/core/geometry.ts
|
|
132
|
+
import { readScroll } from "use-scroller/core";
|
|
133
|
+
var SNAP_MERGE_DISTANCE = 1;
|
|
134
|
+
var FIT_TOLERANCE = 0.5;
|
|
135
|
+
var MIRRORED = { start: "end", center: "center", end: "start" };
|
|
136
|
+
function readLayout(viewport, axis) {
|
|
137
|
+
const style = getComputedStyle(viewport);
|
|
138
|
+
if (style.position === "static") {
|
|
139
|
+
throw new Error(
|
|
140
|
+
"Slider viewport must be positioned; add position: relative (the stylesheet does this)"
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const children = childrenOf(viewport);
|
|
144
|
+
const slides = children.filter((child) => !isClone(child));
|
|
145
|
+
const clones = children.filter(isClone);
|
|
146
|
+
const direction = axis === "x" && style.direction === "rtl" ? -1 : 1;
|
|
147
|
+
const scroll = readScroll(viewport);
|
|
148
|
+
return {
|
|
149
|
+
slides,
|
|
150
|
+
clones,
|
|
151
|
+
clonesBefore: countLeadingClones(children),
|
|
152
|
+
clonesAfter: countLeadingClones([...children].reverse()),
|
|
153
|
+
boxes: slides.map((slide) => readBox(slide, axis)),
|
|
154
|
+
gap: parseGap(axis === "x" ? style.columnGap : style.rowGap),
|
|
155
|
+
port: readPort(viewport, style, axis),
|
|
156
|
+
range: axis === "x" ? { min: scroll.minX, max: scroll.maxX } : { min: 0, max: scroll.maxY },
|
|
157
|
+
direction
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function isClone(child) {
|
|
161
|
+
return child.hasAttribute("data-slider-clone");
|
|
162
|
+
}
|
|
163
|
+
function countLeadingClones(children) {
|
|
164
|
+
const firstReal = children.findIndex((child) => !isClone(child));
|
|
165
|
+
return firstReal === -1 ? children.length : firstReal;
|
|
166
|
+
}
|
|
167
|
+
function childrenOf(viewport) {
|
|
168
|
+
return Array.from(viewport.children).filter(
|
|
169
|
+
(child) => child instanceof HTMLElement
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
function readBox(slide, axis) {
|
|
173
|
+
if (axis === "x") return { start: slide.offsetLeft, end: slide.offsetLeft + slide.offsetWidth };
|
|
174
|
+
return { start: slide.offsetTop, end: slide.offsetTop + slide.offsetHeight };
|
|
175
|
+
}
|
|
176
|
+
function parseGap(value) {
|
|
177
|
+
return value === "normal" ? 0 : parseFloat(value);
|
|
178
|
+
}
|
|
179
|
+
function readPort(viewport, style, axis) {
|
|
180
|
+
const size = axis === "x" ? viewport.clientWidth : viewport.clientHeight;
|
|
181
|
+
return {
|
|
182
|
+
size,
|
|
183
|
+
paddingBefore: resolvePadding(
|
|
184
|
+
axis === "x" ? style.scrollPaddingLeft : style.scrollPaddingTop,
|
|
185
|
+
size
|
|
186
|
+
),
|
|
187
|
+
paddingAfter: resolvePadding(
|
|
188
|
+
axis === "x" ? style.scrollPaddingRight : style.scrollPaddingBottom,
|
|
189
|
+
size
|
|
190
|
+
)
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function resolvePadding(value, size) {
|
|
194
|
+
if (value === "auto") return 0;
|
|
195
|
+
if (value.endsWith("%")) return parseFloat(value) / 100 * size;
|
|
196
|
+
return parseFloat(value);
|
|
197
|
+
}
|
|
198
|
+
function alignedPosition(box, port, align, direction) {
|
|
199
|
+
const before = port.paddingBefore;
|
|
200
|
+
const after = port.paddingAfter;
|
|
201
|
+
const physicalAlign = direction === 1 ? align : MIRRORED[align];
|
|
202
|
+
switch (physicalAlign) {
|
|
203
|
+
case "start":
|
|
204
|
+
return box.start - before;
|
|
205
|
+
case "end":
|
|
206
|
+
return box.end - (port.size - after);
|
|
207
|
+
case "center":
|
|
208
|
+
return (box.start + box.end) / 2 - (before + (port.size - before - after) / 2);
|
|
209
|
+
default: {
|
|
210
|
+
const exhaustive = physicalAlign;
|
|
211
|
+
return exhaustive;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function groupSize(layout, slidesPerMove) {
|
|
216
|
+
if (slidesPerMove !== "auto") return slidesPerMove;
|
|
217
|
+
const available = layout.port.size - layout.port.paddingBefore - layout.port.paddingAfter;
|
|
218
|
+
let size = 1;
|
|
219
|
+
while (size < layout.boxes.length) {
|
|
220
|
+
const span = unionSize(layout.boxes.slice(0, size + 1));
|
|
221
|
+
if (span > available + FIT_TOLERANCE) break;
|
|
222
|
+
size++;
|
|
223
|
+
}
|
|
224
|
+
return size;
|
|
225
|
+
}
|
|
226
|
+
function unionSize(boxes) {
|
|
227
|
+
const start = Math.min(...boxes.map((box) => box.start));
|
|
228
|
+
const end = Math.max(...boxes.map((box) => box.end));
|
|
229
|
+
return end - start;
|
|
230
|
+
}
|
|
231
|
+
function anchors(slideCount, size, align) {
|
|
232
|
+
const result = [];
|
|
233
|
+
for (let first = 0; first < slideCount; first += size) {
|
|
234
|
+
const last = Math.min(first + size, slideCount) - 1;
|
|
235
|
+
result.push(anchorOf(first, last, align));
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
function anchorOf(first, last, align) {
|
|
240
|
+
if (align === "start") return first;
|
|
241
|
+
if (align === "end") return last;
|
|
242
|
+
return Math.floor((first + last) / 2);
|
|
243
|
+
}
|
|
244
|
+
function snapPositions(layout, anchorIndexes, align) {
|
|
245
|
+
const { boxes, port, range, direction } = layout;
|
|
246
|
+
const positions = [];
|
|
247
|
+
for (const index of anchorIndexes) {
|
|
248
|
+
const aligned = alignedPosition(boxes[index], port, align, direction);
|
|
249
|
+
const position = clamp(aligned, range.min, range.max);
|
|
250
|
+
const previous = positions[positions.length - 1];
|
|
251
|
+
if (previous !== void 0 && Math.abs(position - previous) <= SNAP_MERGE_DISTANCE) continue;
|
|
252
|
+
positions.push(position);
|
|
253
|
+
}
|
|
254
|
+
return positions;
|
|
255
|
+
}
|
|
256
|
+
function nearestSnap(positions, p) {
|
|
257
|
+
let nearest = 0;
|
|
258
|
+
for (let i = 1; i < positions.length; i++) {
|
|
259
|
+
if (Math.abs(positions[i] - p) < Math.abs(positions[nearest] - p)) nearest = i;
|
|
260
|
+
}
|
|
261
|
+
return nearest;
|
|
262
|
+
}
|
|
263
|
+
function progressOf(positions, p) {
|
|
264
|
+
if (positions.length < 2) return 0;
|
|
265
|
+
const first = positions[0];
|
|
266
|
+
const last = positions[positions.length - 1];
|
|
267
|
+
return clamp((p - first) / (last - first), 0, 1);
|
|
268
|
+
}
|
|
269
|
+
function periodOf(layout) {
|
|
270
|
+
if (layout.boxes.length === 0) return 0;
|
|
271
|
+
return unionSize(layout.boxes) + layout.gap;
|
|
272
|
+
}
|
|
273
|
+
function loopSnapPositions(layout, anchorIndexes, align) {
|
|
274
|
+
const { boxes, port, direction } = layout;
|
|
275
|
+
return anchorIndexes.map((index) => alignedPosition(boxes[index], port, align, direction));
|
|
276
|
+
}
|
|
277
|
+
function copiesNeeded(layout) {
|
|
278
|
+
const period = periodOf(layout);
|
|
279
|
+
return period > 0 ? Math.max(1, Math.ceil(2 * layout.port.size / period)) : 0;
|
|
280
|
+
}
|
|
281
|
+
function canonical(loop, p) {
|
|
282
|
+
return loop.snaps[0] + loop.direction * mod(logicalOffset(loop, p), loop.period);
|
|
283
|
+
}
|
|
284
|
+
function positionOf(loop, v) {
|
|
285
|
+
const n = loop.snaps.length;
|
|
286
|
+
const offset = logicalOffset(loop, loop.snaps[mod(v, n)]) + Math.floor(v / n) * loop.period;
|
|
287
|
+
return loop.snaps[0] + loop.direction * offset;
|
|
288
|
+
}
|
|
289
|
+
function virtualAt(loop, p) {
|
|
290
|
+
const n = loop.snaps.length;
|
|
291
|
+
const u = logicalOffset(loop, p);
|
|
292
|
+
const copy = Math.floor(u / loop.period);
|
|
293
|
+
const r = u - copy * loop.period;
|
|
294
|
+
let nearest = 0;
|
|
295
|
+
let nearestDistance = Infinity;
|
|
296
|
+
let wrapped = false;
|
|
297
|
+
loop.snaps.forEach((snap, i) => {
|
|
298
|
+
const direct = Math.abs(r - logicalOffset(loop, snap));
|
|
299
|
+
const around = loop.period - direct;
|
|
300
|
+
const distance = Math.min(direct, around);
|
|
301
|
+
if (distance >= nearestDistance) return;
|
|
302
|
+
nearest = i;
|
|
303
|
+
nearestDistance = distance;
|
|
304
|
+
wrapped = around < direct;
|
|
305
|
+
});
|
|
306
|
+
const ownerCopy = wrapped && r > logicalOffset(loop, loop.snaps[nearest]) ? copy + 1 : copy;
|
|
307
|
+
return ownerCopy * n + nearest;
|
|
308
|
+
}
|
|
309
|
+
function nearestCongruent(from, index, n) {
|
|
310
|
+
const base = from - mod(from, n) + mod(index, n);
|
|
311
|
+
let nearest = base + n;
|
|
312
|
+
for (const candidate of [base, base - n]) {
|
|
313
|
+
if (Math.abs(candidate - from) < Math.abs(nearest - from)) nearest = candidate;
|
|
314
|
+
}
|
|
315
|
+
return nearest;
|
|
316
|
+
}
|
|
317
|
+
function loopProgress(loop, p) {
|
|
318
|
+
return mod(logicalOffset(loop, p), loop.period) / loop.period;
|
|
319
|
+
}
|
|
320
|
+
function logicalOffset(loop, p) {
|
|
321
|
+
return loop.direction * (p - loop.snaps[0]);
|
|
322
|
+
}
|
|
323
|
+
function mod(a, n) {
|
|
324
|
+
return (a % n + n) % n;
|
|
325
|
+
}
|
|
326
|
+
function clamp(value, min, max) {
|
|
327
|
+
return Math.min(Math.max(value, min), max);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/core/clones.ts
|
|
331
|
+
function renderClones(slider) {
|
|
332
|
+
const { viewport } = slider;
|
|
333
|
+
let rendered = null;
|
|
334
|
+
function render({ loopCopies, slideCount }) {
|
|
335
|
+
if ((rendered == null ? void 0 : rendered.loopCopies) === loopCopies && rendered.slideCount === slideCount) return;
|
|
336
|
+
rendered = { loopCopies, slideCount };
|
|
337
|
+
removeClones(viewport);
|
|
338
|
+
const slides = childrenOf(viewport);
|
|
339
|
+
if (loopCopies === 0 || slides.length === 0) return;
|
|
340
|
+
slides[0].before(copiesOf(slides, loopCopies));
|
|
341
|
+
slides[slides.length - 1].after(copiesOf(slides, loopCopies));
|
|
342
|
+
}
|
|
343
|
+
render(slider.getState());
|
|
344
|
+
const unsubscribe = slider.subscribe(render);
|
|
345
|
+
return () => {
|
|
346
|
+
unsubscribe();
|
|
347
|
+
removeClones(viewport);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function removeClones(viewport) {
|
|
351
|
+
for (const clone of viewport.querySelectorAll(":scope > [data-slider-clone]")) clone.remove();
|
|
352
|
+
}
|
|
353
|
+
function copiesOf(slides, copies) {
|
|
354
|
+
const fragment = document.createDocumentFragment();
|
|
355
|
+
for (let copy = 0; copy < copies; copy++) {
|
|
356
|
+
slides.forEach((slide, index) => fragment.append(cloneSlide(slide, index)));
|
|
357
|
+
}
|
|
358
|
+
return fragment;
|
|
359
|
+
}
|
|
360
|
+
function cloneSlide(slide, index) {
|
|
361
|
+
const clone = slide.cloneNode(true);
|
|
362
|
+
if (!(clone instanceof HTMLElement)) throw new Error("Slider slides must be HTML elements");
|
|
363
|
+
clone.setAttribute("data-slider-clone", String(index));
|
|
364
|
+
clone.setAttribute("aria-hidden", "true");
|
|
365
|
+
clone.inert = true;
|
|
366
|
+
clone.removeAttribute("id");
|
|
367
|
+
for (const element of clone.querySelectorAll("[id]")) element.removeAttribute("id");
|
|
368
|
+
return clone;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// src/core/slide-progress.ts
|
|
372
|
+
import { observeScroll } from "use-scroller/core";
|
|
373
|
+
function slideProgress(slider) {
|
|
374
|
+
const { viewport } = slider;
|
|
375
|
+
const { axis, align } = slider.options;
|
|
376
|
+
let cached = null;
|
|
377
|
+
function currentMetrics(children) {
|
|
378
|
+
const key = {
|
|
379
|
+
childCount: children.length,
|
|
380
|
+
portSize: axis === "x" ? viewport.clientWidth : viewport.clientHeight
|
|
381
|
+
};
|
|
382
|
+
if (cached === null || !isSameKey(cached.key, key)) {
|
|
383
|
+
const { port, gap, direction } = readLayout(viewport, axis);
|
|
384
|
+
cached = { key, metrics: { port, gap, direction } };
|
|
385
|
+
}
|
|
386
|
+
return cached.metrics;
|
|
387
|
+
}
|
|
388
|
+
function write() {
|
|
389
|
+
const children = childrenOf(viewport);
|
|
390
|
+
const { port, gap, direction } = currentMetrics(children);
|
|
391
|
+
const position = axis === "x" ? viewport.scrollLeft : viewport.scrollTop;
|
|
392
|
+
const values = children.map((child) => {
|
|
393
|
+
const box = readBox(child, axis);
|
|
394
|
+
const aligned = alignedPosition(box, port, align, direction);
|
|
395
|
+
return direction * (aligned - position) / (box.end - box.start + gap);
|
|
396
|
+
});
|
|
397
|
+
children.forEach((child, i) => {
|
|
398
|
+
child.style.setProperty("--slide-progress", String(round(values[i])));
|
|
399
|
+
child.style.setProperty("--slide-distance", String(round(Math.abs(values[i]))));
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
const stopObserving = observeScroll(viewport, write);
|
|
403
|
+
return () => {
|
|
404
|
+
stopObserving();
|
|
405
|
+
for (const child of childrenOf(viewport)) {
|
|
406
|
+
child.style.removeProperty("--slide-progress");
|
|
407
|
+
child.style.removeProperty("--slide-distance");
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
function isSameKey(a, b) {
|
|
412
|
+
return a.childCount === b.childCount && a.portSize === b.portSize;
|
|
413
|
+
}
|
|
414
|
+
function round(value) {
|
|
415
|
+
return Math.round(value * 1e4) / 1e4;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// src/core/slider.ts
|
|
419
|
+
import {
|
|
420
|
+
animateScroll,
|
|
421
|
+
cancelScroll,
|
|
422
|
+
dragScroll,
|
|
423
|
+
observeScroll as observeScroll2
|
|
424
|
+
} from "use-scroller/core";
|
|
425
|
+
var DEFAULT_ANIMATION = { type: "tween" };
|
|
426
|
+
var SNAP_TOLERANCE = 1;
|
|
427
|
+
var STATE_KEYS = [
|
|
428
|
+
"index",
|
|
429
|
+
"snapCount",
|
|
430
|
+
"slideCount",
|
|
431
|
+
"canPrev",
|
|
432
|
+
"canNext",
|
|
433
|
+
"progress",
|
|
434
|
+
"loopCopies"
|
|
435
|
+
];
|
|
436
|
+
function createSlider(viewport, options = {}) {
|
|
437
|
+
const resolved = resolveOptions(options);
|
|
438
|
+
const startIndex = options.startIndex ?? 0;
|
|
439
|
+
assertInteger(startIndex, "startIndex");
|
|
440
|
+
const { axis, align } = resolved;
|
|
441
|
+
let layout = readLayout(viewport, axis);
|
|
442
|
+
let snaps = [];
|
|
443
|
+
let loopCopies = 0;
|
|
444
|
+
let loop = null;
|
|
445
|
+
const tracked = /* @__PURE__ */ new Set();
|
|
446
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
447
|
+
let pending = null;
|
|
448
|
+
let current = null;
|
|
449
|
+
let heldSnapType = null;
|
|
450
|
+
let pointerDown = false;
|
|
451
|
+
let scrolling = false;
|
|
452
|
+
let destroyed = false;
|
|
453
|
+
const resizeObserver = new ResizeObserver(remeasure);
|
|
454
|
+
const mutationObserver = new MutationObserver(remeasure);
|
|
455
|
+
applyLayout();
|
|
456
|
+
jumpToSnap(startIndex);
|
|
457
|
+
let state = computeState(livePosition());
|
|
458
|
+
const stopObservingScroll = observeScroll2(viewport, onScroll);
|
|
459
|
+
resizeObserver.observe(viewport);
|
|
460
|
+
mutationObserver.observe(viewport, { childList: true });
|
|
461
|
+
const stopDragging = resolved.draggable ? dragScroll(viewport, { axis, mouse: true }) : null;
|
|
462
|
+
const onPointerDown = () => {
|
|
463
|
+
pointerDown = true;
|
|
464
|
+
};
|
|
465
|
+
const onPointerUp = () => {
|
|
466
|
+
pointerDown = false;
|
|
467
|
+
recentre();
|
|
468
|
+
};
|
|
469
|
+
viewport.addEventListener("pointerdown", onPointerDown);
|
|
470
|
+
window.addEventListener("pointerup", onPointerUp);
|
|
471
|
+
window.addEventListener("pointercancel", onPointerUp);
|
|
472
|
+
function applyLayout() {
|
|
473
|
+
const size = groupSize(layout, resolved.slidesPerMove);
|
|
474
|
+
const anchorIndexes = anchors(layout.slides.length, size, align);
|
|
475
|
+
loopCopies = resolved.loop ? copiesNeeded(layout) : 0;
|
|
476
|
+
loop = isLoopActive(layout, loopCopies) ? {
|
|
477
|
+
snaps: loopSnapPositions(layout, anchorIndexes, align),
|
|
478
|
+
period: periodOf(layout),
|
|
479
|
+
direction: layout.direction
|
|
480
|
+
} : null;
|
|
481
|
+
snaps = loop ? loop.snaps : snapPositions(layout, anchorIndexes, align);
|
|
482
|
+
const anchorSet = new Set(anchorIndexes);
|
|
483
|
+
layout.slides.forEach((slide, i) => {
|
|
484
|
+
slide.style.scrollSnapAlign = anchorSet.has(i) ? align : "none";
|
|
485
|
+
});
|
|
486
|
+
for (const clone of layout.clones) {
|
|
487
|
+
const source = Number(clone.getAttribute("data-slider-clone"));
|
|
488
|
+
clone.style.scrollSnapAlign = loop && anchorSet.has(source) ? align : "none";
|
|
489
|
+
}
|
|
490
|
+
trackSlides(layout.slides);
|
|
491
|
+
}
|
|
492
|
+
function trackSlides(slides) {
|
|
493
|
+
const present = new Set(slides);
|
|
494
|
+
for (const slide of tracked) {
|
|
495
|
+
if (present.has(slide)) continue;
|
|
496
|
+
slide.style.removeProperty("scroll-snap-align");
|
|
497
|
+
resizeObserver.unobserve(slide);
|
|
498
|
+
tracked.delete(slide);
|
|
499
|
+
}
|
|
500
|
+
for (const slide of slides) {
|
|
501
|
+
if (tracked.has(slide)) continue;
|
|
502
|
+
resizeObserver.observe(slide);
|
|
503
|
+
tracked.add(slide);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
function remeasure() {
|
|
507
|
+
if (destroyed) return;
|
|
508
|
+
layout = readLayout(viewport, axis);
|
|
509
|
+
applyLayout();
|
|
510
|
+
if (pending === null && !pointerDown && !scrolling) jumpToSnap(state.index);
|
|
511
|
+
update(livePosition());
|
|
512
|
+
}
|
|
513
|
+
function onScroll(scroll) {
|
|
514
|
+
scrolling = scroll.isScrolling;
|
|
515
|
+
update(axis === "x" ? scroll.x : scroll.y);
|
|
516
|
+
recentre();
|
|
517
|
+
}
|
|
518
|
+
function recentre() {
|
|
519
|
+
if (loop === null || scrolling || pointerDown || pending !== null) return;
|
|
520
|
+
const position = livePosition();
|
|
521
|
+
const onSnap = Math.abs(positionOf(loop, virtualAt(loop, position)) - position);
|
|
522
|
+
if (onSnap > SNAP_TOLERANCE) return;
|
|
523
|
+
const target = canonical(loop, position);
|
|
524
|
+
if (Math.abs(target - position) > SNAP_TOLERANCE) jumpTo(target);
|
|
525
|
+
}
|
|
526
|
+
function computeState(position) {
|
|
527
|
+
const snapCount = snaps.length;
|
|
528
|
+
const slideCount = layout.slides.length;
|
|
529
|
+
if (loop) {
|
|
530
|
+
const looping = snapCount > 1;
|
|
531
|
+
return {
|
|
532
|
+
index: (pending == null ? void 0 : pending.index) ?? mod(virtualAt(loop, position), snapCount),
|
|
533
|
+
snapCount,
|
|
534
|
+
slideCount,
|
|
535
|
+
canPrev: looping,
|
|
536
|
+
canNext: looping,
|
|
537
|
+
progress: loopProgress(loop, position),
|
|
538
|
+
loopCopies
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
const index = (pending == null ? void 0 : pending.index) ?? nearestSnap(snaps, position);
|
|
542
|
+
const movable = snapCount > 1;
|
|
543
|
+
return {
|
|
544
|
+
index,
|
|
545
|
+
snapCount,
|
|
546
|
+
slideCount,
|
|
547
|
+
canPrev: movable && (resolved.rewind || index > 0),
|
|
548
|
+
canNext: movable && (resolved.rewind || index < snapCount - 1),
|
|
549
|
+
progress: progressOf(snaps, position),
|
|
550
|
+
loopCopies
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function update(position) {
|
|
554
|
+
const next = computeState(position);
|
|
555
|
+
if (STATE_KEYS.every((key) => next[key] === state[key])) return;
|
|
556
|
+
state = next;
|
|
557
|
+
for (const listener of listeners) listener(state);
|
|
558
|
+
}
|
|
559
|
+
function livePosition() {
|
|
560
|
+
return axis === "x" ? viewport.scrollLeft : viewport.scrollTop;
|
|
561
|
+
}
|
|
562
|
+
function clampIndex(index) {
|
|
563
|
+
return Math.min(Math.max(index, 0), snaps.length - 1);
|
|
564
|
+
}
|
|
565
|
+
function jumpToSnap(index) {
|
|
566
|
+
if (snaps.length === 0) return;
|
|
567
|
+
jumpTo(snaps[clampIndex(index)]);
|
|
568
|
+
}
|
|
569
|
+
function jumpTo(position) {
|
|
570
|
+
viewport.scrollTo(
|
|
571
|
+
axis === "x" ? { left: position, behavior: "instant" } : { top: position, behavior: "instant" }
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
function holdSnap() {
|
|
575
|
+
if (heldSnapType !== null) return;
|
|
576
|
+
cancelScroll(viewport);
|
|
577
|
+
heldSnapType = viewport.style.scrollSnapType;
|
|
578
|
+
viewport.style.scrollSnapType = "none";
|
|
579
|
+
}
|
|
580
|
+
function releaseSnap() {
|
|
581
|
+
if (heldSnapType === null) return;
|
|
582
|
+
viewport.style.scrollSnapType = heldSnapType;
|
|
583
|
+
heldSnapType = null;
|
|
584
|
+
}
|
|
585
|
+
function navigate(index, navigateOptions = {}) {
|
|
586
|
+
holdSnap();
|
|
587
|
+
animateTo({ index, virtual: index }, snaps[index], navigateOptions);
|
|
588
|
+
}
|
|
589
|
+
function navigateLoop(active, from, target, navigateOptions = {}) {
|
|
590
|
+
holdSnap();
|
|
591
|
+
const n = active.snaps.length;
|
|
592
|
+
const copy = Math.floor(from / n);
|
|
593
|
+
if (copy !== 0) jumpTo(livePosition() - active.direction * copy * active.period);
|
|
594
|
+
const virtual = target - copy * n;
|
|
595
|
+
animateTo({ index: mod(virtual, n), virtual }, positionOf(active, virtual), navigateOptions);
|
|
596
|
+
}
|
|
597
|
+
function loopFrom(active) {
|
|
598
|
+
return (pending == null ? void 0 : pending.virtual) ?? virtualAt(active, livePosition());
|
|
599
|
+
}
|
|
600
|
+
function animateTo(target, position, navigateOptions) {
|
|
601
|
+
pending = target;
|
|
602
|
+
const handle = animateScroll(viewport, axis === "x" ? { x: position } : { y: position }, {
|
|
603
|
+
animation: navigateOptions.animation ?? resolved.animation
|
|
604
|
+
});
|
|
605
|
+
current = handle;
|
|
606
|
+
update(livePosition());
|
|
607
|
+
void handle.finished.then(() => {
|
|
608
|
+
if (current !== handle) return;
|
|
609
|
+
current = null;
|
|
610
|
+
pending = null;
|
|
611
|
+
releaseSnap();
|
|
612
|
+
update(livePosition());
|
|
613
|
+
recentre();
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
function assertAlive() {
|
|
617
|
+
if (destroyed) throw new Error("Slider was destroyed");
|
|
618
|
+
}
|
|
619
|
+
return {
|
|
620
|
+
viewport,
|
|
621
|
+
options: resolved,
|
|
622
|
+
getState() {
|
|
623
|
+
assertAlive();
|
|
624
|
+
return state;
|
|
625
|
+
},
|
|
626
|
+
subscribe(listener) {
|
|
627
|
+
assertAlive();
|
|
628
|
+
listeners.add(listener);
|
|
629
|
+
return () => {
|
|
630
|
+
listeners.delete(listener);
|
|
631
|
+
};
|
|
632
|
+
},
|
|
633
|
+
next(navigateOptions) {
|
|
634
|
+
assertAlive();
|
|
635
|
+
if (loop) {
|
|
636
|
+
const from2 = loopFrom(loop);
|
|
637
|
+
if (snaps.length > 1) navigateLoop(loop, from2, from2 + 1, navigateOptions);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const last = snaps.length - 1;
|
|
641
|
+
const from = (pending == null ? void 0 : pending.index) ?? state.index;
|
|
642
|
+
if (from < last) navigate(from + 1, navigateOptions);
|
|
643
|
+
else if (resolved.rewind && last > 0) navigate(0, navigateOptions);
|
|
644
|
+
},
|
|
645
|
+
prev(navigateOptions) {
|
|
646
|
+
assertAlive();
|
|
647
|
+
if (loop) {
|
|
648
|
+
const from2 = loopFrom(loop);
|
|
649
|
+
if (snaps.length > 1) navigateLoop(loop, from2, from2 - 1, navigateOptions);
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const last = snaps.length - 1;
|
|
653
|
+
const from = (pending == null ? void 0 : pending.index) ?? state.index;
|
|
654
|
+
if (from > 0) navigate(from - 1, navigateOptions);
|
|
655
|
+
else if (resolved.rewind && last > 0) navigate(last, navigateOptions);
|
|
656
|
+
},
|
|
657
|
+
scrollTo(index, navigateOptions) {
|
|
658
|
+
assertAlive();
|
|
659
|
+
assertInteger(index, "index");
|
|
660
|
+
if (snaps.length === 0) return;
|
|
661
|
+
if (loop) {
|
|
662
|
+
const from = loopFrom(loop);
|
|
663
|
+
navigateLoop(loop, from, nearestCongruent(from, index, snaps.length), navigateOptions);
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
navigate(clampIndex(index), navigateOptions);
|
|
667
|
+
},
|
|
668
|
+
destroy() {
|
|
669
|
+
if (destroyed) return;
|
|
670
|
+
destroyed = true;
|
|
671
|
+
stopObservingScroll();
|
|
672
|
+
resizeObserver.disconnect();
|
|
673
|
+
mutationObserver.disconnect();
|
|
674
|
+
stopDragging == null ? void 0 : stopDragging();
|
|
675
|
+
viewport.removeEventListener("pointerdown", onPointerDown);
|
|
676
|
+
window.removeEventListener("pointerup", onPointerUp);
|
|
677
|
+
window.removeEventListener("pointercancel", onPointerUp);
|
|
678
|
+
current == null ? void 0 : current.cancel();
|
|
679
|
+
current = null;
|
|
680
|
+
pending = null;
|
|
681
|
+
releaseSnap();
|
|
682
|
+
for (const slide of tracked) slide.style.removeProperty("scroll-snap-align");
|
|
683
|
+
for (const clone of layout.clones) clone.style.removeProperty("scroll-snap-align");
|
|
684
|
+
tracked.clear();
|
|
685
|
+
listeners.clear();
|
|
686
|
+
}
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
function isLoopActive(layout, copies) {
|
|
690
|
+
const expected = copies * layout.slides.length;
|
|
691
|
+
return expected > 0 && layout.clonesBefore === expected && layout.clonesAfter === expected;
|
|
692
|
+
}
|
|
693
|
+
function resolveOptions(options) {
|
|
694
|
+
const slidesPerMove = options.slidesPerMove ?? 1;
|
|
695
|
+
if (slidesPerMove !== "auto" && !(Number.isInteger(slidesPerMove) && slidesPerMove >= 1)) {
|
|
696
|
+
throw new RangeError(`slidesPerMove must be an integer >= 1 or 'auto', got ${slidesPerMove}`);
|
|
697
|
+
}
|
|
698
|
+
return {
|
|
699
|
+
axis: options.axis ?? "x",
|
|
700
|
+
align: options.align ?? "start",
|
|
701
|
+
slidesPerMove,
|
|
702
|
+
loop: options.loop ?? false,
|
|
703
|
+
rewind: options.rewind ?? false,
|
|
704
|
+
draggable: options.draggable ?? true,
|
|
705
|
+
animation: options.animation ?? DEFAULT_ANIMATION
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
function assertInteger(value, name) {
|
|
709
|
+
if (!Number.isInteger(value)) throw new RangeError(`${name} must be an integer, got ${value}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/core/sync.ts
|
|
713
|
+
var CLONE_ATTRIBUTE = "data-slider-clone";
|
|
714
|
+
function syncSliders(main, thumbs) {
|
|
715
|
+
const { viewport } = thumbs;
|
|
716
|
+
let index = null;
|
|
717
|
+
function follow(state) {
|
|
718
|
+
if (state.index === index) return;
|
|
719
|
+
index = state.index;
|
|
720
|
+
thumbs.scrollTo(Math.min(index, thumbs.getState().snapCount - 1));
|
|
721
|
+
const selected = realSlides(viewport)[index];
|
|
722
|
+
for (const child of viewport.children) {
|
|
723
|
+
if (child === selected) {
|
|
724
|
+
child.setAttribute("aria-current", "true");
|
|
725
|
+
child.setAttribute("data-selected", "");
|
|
726
|
+
} else {
|
|
727
|
+
clearSelection(child);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function onClick(event) {
|
|
732
|
+
const { target } = event;
|
|
733
|
+
if (!(target instanceof Node)) return;
|
|
734
|
+
const child = Array.from(viewport.children).find((element) => element.contains(target));
|
|
735
|
+
if (child === void 0) return;
|
|
736
|
+
main.scrollTo(slideIndexOf(viewport, child));
|
|
737
|
+
}
|
|
738
|
+
follow(main.getState());
|
|
739
|
+
const unsubscribe = main.subscribe(follow);
|
|
740
|
+
viewport.addEventListener("click", onClick);
|
|
741
|
+
return () => {
|
|
742
|
+
unsubscribe();
|
|
743
|
+
viewport.removeEventListener("click", onClick);
|
|
744
|
+
for (const child of viewport.children) clearSelection(child);
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
function realSlides(viewport) {
|
|
748
|
+
return Array.from(viewport.children).filter((child) => !child.hasAttribute(CLONE_ATTRIBUTE));
|
|
749
|
+
}
|
|
750
|
+
function slideIndexOf(viewport, child) {
|
|
751
|
+
const source = child.getAttribute(CLONE_ATTRIBUTE);
|
|
752
|
+
return source === null ? realSlides(viewport).indexOf(child) : Number(source);
|
|
753
|
+
}
|
|
754
|
+
function clearSelection(element) {
|
|
755
|
+
element.removeAttribute("aria-current");
|
|
756
|
+
element.removeAttribute("data-selected");
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export {
|
|
760
|
+
autoplay,
|
|
761
|
+
renderClones,
|
|
762
|
+
slideProgress,
|
|
763
|
+
createSlider,
|
|
764
|
+
syncSliders
|
|
765
|
+
};
|