lazer-slider 1.0.9 → 1.1.1
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/README.md +96 -9
- package/dist/index.cjs +119 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +17 -56
- package/dist/index.d.ts +17 -56
- package/dist/index.js +118 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,6 +10,7 @@ A lightweight, accessible slider with smooth scroll-to-snap animations, drag-to-
|
|
|
10
10
|
- **Loop Mode** - Infinite loop navigation
|
|
11
11
|
- **Autoplay** - Automatic slide advancement with pause on hover
|
|
12
12
|
- **Marquee Mode** - Continuous smooth scrolling with seamless infinite loop
|
|
13
|
+
- **Automatic Bullets** - Auto-generate navigation bullets from slides
|
|
13
14
|
- **Accessible** - Full ARIA support, keyboard navigation (arrow keys)
|
|
14
15
|
- **Lightweight** - Zero dependencies, ~20KB unminified
|
|
15
16
|
- **TypeScript** - Full type definitions included
|
|
@@ -35,11 +36,7 @@ npm install lazer-slider
|
|
|
35
36
|
<div class="slide">Slide 3</div>
|
|
36
37
|
</div>
|
|
37
38
|
|
|
38
|
-
<div class="slider-
|
|
39
|
-
<button class="dot"></button>
|
|
40
|
-
<button class="dot"></button>
|
|
41
|
-
<button class="dot"></button>
|
|
42
|
-
</div>
|
|
39
|
+
<div class="slider-bullets"></div>
|
|
43
40
|
</div>
|
|
44
41
|
```
|
|
45
42
|
|
|
@@ -61,6 +58,27 @@ npm install lazer-slider
|
|
|
61
58
|
scroll-snap-align: start;
|
|
62
59
|
flex-shrink: 0;
|
|
63
60
|
}
|
|
61
|
+
|
|
62
|
+
.slider-bullets {
|
|
63
|
+
display: flex;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
justify-content: center;
|
|
66
|
+
margin-top: 16px;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.slider-bullet {
|
|
70
|
+
width: 12px;
|
|
71
|
+
height: 12px;
|
|
72
|
+
border-radius: 50%;
|
|
73
|
+
border: none;
|
|
74
|
+
background: #ccc;
|
|
75
|
+
cursor: pointer;
|
|
76
|
+
transition: background 0.3s;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.slider-bullet.active {
|
|
80
|
+
background: #333;
|
|
81
|
+
}
|
|
64
82
|
```
|
|
65
83
|
|
|
66
84
|
### JavaScript
|
|
@@ -73,7 +91,7 @@ const slider = createSlider({
|
|
|
73
91
|
slides: [...document.querySelectorAll('.slide')],
|
|
74
92
|
prevSlideButton: document.querySelector('.slider-prev'),
|
|
75
93
|
nextSlideButton: document.querySelector('.slider-next'),
|
|
76
|
-
|
|
94
|
+
bulletsContainer: document.querySelector('.slider-bullets'),
|
|
77
95
|
enableDragToScroll: true
|
|
78
96
|
})
|
|
79
97
|
|
|
@@ -96,7 +114,12 @@ slider.unload()
|
|
|
96
114
|
|--------|------|---------|-------------|
|
|
97
115
|
| `prevSlideButton` | `HTMLElement \| null` | `null` | Previous slide button |
|
|
98
116
|
| `nextSlideButton` | `HTMLElement \| null` | `null` | Next slide button |
|
|
99
|
-
| `thumbs` | `HTMLElement[]` | `undefined` | Thumbnail/dot elements for direct navigation |
|
|
117
|
+
| `thumbs` | `HTMLElement[]` | `undefined` | Thumbnail/dot elements for direct navigation (manual) |
|
|
118
|
+
| `bulletsContainer` | `HTMLElement \| null` | `null` | Container element for auto-generated bullets |
|
|
119
|
+
| `bulletsClass` | `string` | `'slider-bullet'` | CSS class for bullet elements |
|
|
120
|
+
| `bulletsActiveClass` | `string` | `'active'` | CSS class for active bullet element |
|
|
121
|
+
|
|
122
|
+
> **Note:** When `bulletsContainer` is provided, bullets are automatically generated. If `thumbs` is also provided, `bulletsContainer` is ignored. Bullets are only generated for visible slides.
|
|
100
123
|
|
|
101
124
|
### Responsive
|
|
102
125
|
|
|
@@ -296,7 +319,66 @@ marquee.play() // Resume marquee
|
|
|
296
319
|
|
|
297
320
|
### Bullets/Dots Navigation
|
|
298
321
|
|
|
299
|
-
|
|
322
|
+
You can add clickable dots that navigate to specific slides using either manual or automatic generation.
|
|
323
|
+
|
|
324
|
+
#### Automatic Bullets (Recommended)
|
|
325
|
+
|
|
326
|
+
The slider can automatically generate bullets from your slides. Just provide a container element:
|
|
327
|
+
|
|
328
|
+
```html
|
|
329
|
+
<div class="slider">
|
|
330
|
+
<div class="slider-feed">
|
|
331
|
+
<div class="slide">Slide 1</div>
|
|
332
|
+
<div class="slide">Slide 2</div>
|
|
333
|
+
<div class="slide">Slide 3</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div class="slider-bullets"></div>
|
|
337
|
+
</div>
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```css
|
|
341
|
+
.slider-bullets {
|
|
342
|
+
display: flex;
|
|
343
|
+
gap: 8px;
|
|
344
|
+
justify-content: center;
|
|
345
|
+
margin-top: 16px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.slider-bullet {
|
|
349
|
+
width: 12px;
|
|
350
|
+
height: 12px;
|
|
351
|
+
border-radius: 50%;
|
|
352
|
+
border: none;
|
|
353
|
+
background: #ccc;
|
|
354
|
+
cursor: pointer;
|
|
355
|
+
transition: background 0.3s;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
.slider-bullet.active {
|
|
359
|
+
background: #333;
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
```typescript
|
|
364
|
+
const slider = createSlider({
|
|
365
|
+
feed: document.querySelector('.slider-feed'),
|
|
366
|
+
slides: [...document.querySelectorAll('.slide')],
|
|
367
|
+
bulletsContainer: document.querySelector('.slider-bullets'),
|
|
368
|
+
bulletsClass: 'slider-bullet', // Optional: default is 'slider-bullet'
|
|
369
|
+
bulletsActiveClass: 'active' // Optional: default is 'active'
|
|
370
|
+
})
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Features:**
|
|
374
|
+
- Automatically generates one bullet per visible slide
|
|
375
|
+
- Full ARIA support (`role="tablist"`, `aria-selected`, etc.)
|
|
376
|
+
- Active state automatically managed
|
|
377
|
+
- Only generates bullets for visible slides (hidden slides are skipped)
|
|
378
|
+
|
|
379
|
+
#### Manual Bullets
|
|
380
|
+
|
|
381
|
+
Alternatively, you can create bullets manually and pass them via the `thumbs` option:
|
|
300
382
|
|
|
301
383
|
```html
|
|
302
384
|
<div class="slider">
|
|
@@ -345,6 +427,8 @@ const slider = createSlider({
|
|
|
345
427
|
})
|
|
346
428
|
```
|
|
347
429
|
|
|
430
|
+
> **Note:** If both `bulletsContainer` and `thumbs` are provided, `bulletsContainer` is ignored and manual `thumbs` are used instead.
|
|
431
|
+
|
|
348
432
|
### Custom Scrollbar
|
|
349
433
|
|
|
350
434
|
Create a draggable scrollbar that syncs with the slider position.
|
|
@@ -432,7 +516,7 @@ const slider = createSlider({
|
|
|
432
516
|
slides: [...document.querySelectorAll('.slide')],
|
|
433
517
|
prevSlideButton: document.querySelector('.prev'),
|
|
434
518
|
nextSlideButton: document.querySelector('.next'),
|
|
435
|
-
|
|
519
|
+
bulletsContainer: document.querySelector('.slider-bullets'), // Auto-generated bullets
|
|
436
520
|
|
|
437
521
|
// Responsive
|
|
438
522
|
mobileSlidesPerView: 1,
|
|
@@ -469,6 +553,7 @@ Full TypeScript support with exported types:
|
|
|
469
553
|
```typescript
|
|
470
554
|
import {
|
|
471
555
|
createSlider,
|
|
556
|
+
generateBullets,
|
|
472
557
|
type SliderSettings,
|
|
473
558
|
type Slider,
|
|
474
559
|
type EasingFunction,
|
|
@@ -479,6 +564,8 @@ import {
|
|
|
479
564
|
} from 'lazer-slider'
|
|
480
565
|
```
|
|
481
566
|
|
|
567
|
+
> **Note:** `generateBullets` is also exported if you need to manually generate bullets outside of the slider initialization.
|
|
568
|
+
|
|
482
569
|
## Browser Support
|
|
483
570
|
|
|
484
571
|
- Chrome (latest)
|
package/dist/index.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(index_exports, {
|
|
|
25
25
|
easeOutCubic: () => easeOutCubic,
|
|
26
26
|
easeOutExpo: () => easeOutExpo,
|
|
27
27
|
easeOutQuad: () => easeOutQuad,
|
|
28
|
+
generateBullets: () => generateBullets,
|
|
28
29
|
linear: () => linear
|
|
29
30
|
});
|
|
30
31
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -191,7 +192,7 @@ var getEventX = (event) => {
|
|
|
191
192
|
return event.clientX;
|
|
192
193
|
};
|
|
193
194
|
var setupDragToScroll = (config) => {
|
|
194
|
-
const { feed, slides, abortSignal, smoothScrollTo, onDragEnd } = config;
|
|
195
|
+
const { feed, slides, abortSignal, smoothScrollTo, onDragEnd, dragFree = false } = config;
|
|
195
196
|
const state = createDragState();
|
|
196
197
|
const handleDragStart = (event) => {
|
|
197
198
|
if (state.momentumId !== null) {
|
|
@@ -231,11 +232,33 @@ var setupDragToScroll = (config) => {
|
|
|
231
232
|
state.isDragging = false;
|
|
232
233
|
feed.style.cursor = "grab";
|
|
233
234
|
feed.style.userSelect = "";
|
|
234
|
-
if (
|
|
235
|
-
|
|
235
|
+
if (dragFree) {
|
|
236
|
+
if (Math.abs(state.velocity) > 1) {
|
|
237
|
+
applyMomentum(state, feed, slides, smoothScrollTo, onDragEnd);
|
|
238
|
+
} else {
|
|
239
|
+
const nearestSlide = findNearestSlide(feed, slides);
|
|
240
|
+
if (nearestSlide) {
|
|
241
|
+
smoothScrollTo(nearestSlide.offsetLeft, easeOutCubic);
|
|
242
|
+
}
|
|
243
|
+
onDragEnd?.();
|
|
244
|
+
}
|
|
236
245
|
} else {
|
|
246
|
+
const swipeDistance = state.startX - state.lastX;
|
|
247
|
+
const swipeThreshold = 50;
|
|
237
248
|
const nearestSlide = findNearestSlide(feed, slides);
|
|
238
|
-
if (nearestSlide) {
|
|
249
|
+
if (nearestSlide && Math.abs(swipeDistance) > swipeThreshold) {
|
|
250
|
+
const visibleSlides = slides.filter((slide) => slide.offsetParent !== null);
|
|
251
|
+
const visibleIndex = visibleSlides.indexOf(nearestSlide);
|
|
252
|
+
let targetSlide = null;
|
|
253
|
+
if (swipeDistance > 0 && visibleIndex < visibleSlides.length - 1) {
|
|
254
|
+
targetSlide = visibleSlides[visibleIndex + 1] ?? nearestSlide;
|
|
255
|
+
} else if (swipeDistance < 0 && visibleIndex > 0) {
|
|
256
|
+
targetSlide = visibleSlides[visibleIndex - 1] ?? nearestSlide;
|
|
257
|
+
} else {
|
|
258
|
+
targetSlide = nearestSlide;
|
|
259
|
+
}
|
|
260
|
+
smoothScrollTo(targetSlide.offsetLeft, easeOutCubic);
|
|
261
|
+
} else if (nearestSlide) {
|
|
239
262
|
smoothScrollTo(nearestSlide.offsetLeft, easeOutCubic);
|
|
240
263
|
}
|
|
241
264
|
onDragEnd?.();
|
|
@@ -251,7 +274,6 @@ var setupDragToScroll = (config) => {
|
|
|
251
274
|
});
|
|
252
275
|
feed.addEventListener("touchmove", handleDragMove, {
|
|
253
276
|
passive: false,
|
|
254
|
-
// Need to prevent default
|
|
255
277
|
signal: abortSignal
|
|
256
278
|
});
|
|
257
279
|
feed.addEventListener("touchend", handleDragEnd, { signal: abortSignal });
|
|
@@ -265,6 +287,52 @@ var cleanupDrag = (state) => {
|
|
|
265
287
|
}
|
|
266
288
|
};
|
|
267
289
|
|
|
290
|
+
// src/core/bullets.ts
|
|
291
|
+
var generateBullets = ({
|
|
292
|
+
bulletsContainer,
|
|
293
|
+
slides,
|
|
294
|
+
bulletClass,
|
|
295
|
+
bulletActiveClass,
|
|
296
|
+
feedId
|
|
297
|
+
}) => {
|
|
298
|
+
if (!bulletsContainer || !(bulletsContainer instanceof HTMLElement)) {
|
|
299
|
+
throw new Error("Invalid bulletsContainer: must be a valid HTMLElement");
|
|
300
|
+
}
|
|
301
|
+
if (!Array.isArray(slides) || slides.length === 0) {
|
|
302
|
+
throw new Error("Invalid slides: must be a non-empty array");
|
|
303
|
+
}
|
|
304
|
+
if (!feedId || typeof feedId !== "string") {
|
|
305
|
+
throw new Error("Invalid feedId: must be a non-empty string");
|
|
306
|
+
}
|
|
307
|
+
if (!bulletClass || typeof bulletClass !== "string") {
|
|
308
|
+
throw new Error("Invalid bulletClass: must be a non-empty string");
|
|
309
|
+
}
|
|
310
|
+
bulletsContainer.innerHTML = "";
|
|
311
|
+
bulletsContainer.setAttribute("role", "tablist");
|
|
312
|
+
bulletsContainer.setAttribute("aria-label", "Slide navigation");
|
|
313
|
+
const visibleSlides = slides.map((slide, originalIndex) => ({ slide, originalIndex })).filter(({ slide }) => slide.offsetParent !== null);
|
|
314
|
+
if (visibleSlides.length === 0) {
|
|
315
|
+
console.warn("No visible slides found");
|
|
316
|
+
return [];
|
|
317
|
+
}
|
|
318
|
+
const bullets = visibleSlides.map(({ slide, originalIndex }, visibleIndex) => {
|
|
319
|
+
const bullet = document.createElement("button");
|
|
320
|
+
bullet.type = "button";
|
|
321
|
+
bullet.classList.add(bulletClass);
|
|
322
|
+
if (visibleIndex === 0) {
|
|
323
|
+
bullet.classList.add(bulletActiveClass);
|
|
324
|
+
}
|
|
325
|
+
bullet.setAttribute("role", "tab");
|
|
326
|
+
bullet.setAttribute("aria-selected", visibleIndex === 0 ? "true" : "false");
|
|
327
|
+
bullet.setAttribute("aria-controls", feedId);
|
|
328
|
+
bullet.setAttribute("aria-label", `Go to slide ${visibleIndex + 1}`);
|
|
329
|
+
bullet.setAttribute("data-slide-index", String(visibleIndex));
|
|
330
|
+
bulletsContainer.appendChild(bullet);
|
|
331
|
+
return bullet;
|
|
332
|
+
});
|
|
333
|
+
return bullets;
|
|
334
|
+
};
|
|
335
|
+
|
|
268
336
|
// src/core/marquee.ts
|
|
269
337
|
var createMarqueeState = () => ({
|
|
270
338
|
initialized: false,
|
|
@@ -395,6 +463,19 @@ var createSlider = (settings) => {
|
|
|
395
463
|
if (!settings.slides?.length) {
|
|
396
464
|
throw new Error("lazer-slider: slides array is required and must not be empty");
|
|
397
465
|
}
|
|
466
|
+
if (!settings.feed.id) {
|
|
467
|
+
settings.feed.id = `lazer-slider-feed-${Math.random().toString(36).substr(2, 9)}`;
|
|
468
|
+
}
|
|
469
|
+
if (settings.bulletsContainer && !settings.thumbs) {
|
|
470
|
+
const bullets = generateBullets({
|
|
471
|
+
bulletsContainer: settings.bulletsContainer,
|
|
472
|
+
slides: settings.slides,
|
|
473
|
+
bulletClass: settings.bulletsClass ?? "slider-bullet",
|
|
474
|
+
bulletActiveClass: settings.bulletsActiveClass ?? "active",
|
|
475
|
+
feedId: settings.feed.id
|
|
476
|
+
});
|
|
477
|
+
settings.thumbs = bullets;
|
|
478
|
+
}
|
|
398
479
|
const state = {
|
|
399
480
|
currentSlideIndex: 0,
|
|
400
481
|
isScrolling: false,
|
|
@@ -403,12 +484,14 @@ var createSlider = (settings) => {
|
|
|
403
484
|
lastWidth: 0,
|
|
404
485
|
updateThumbTimeout: null,
|
|
405
486
|
scrollEndTimeout: null,
|
|
487
|
+
resizeTimeout: null,
|
|
406
488
|
abortController: new AbortController(),
|
|
407
489
|
autoplayIntervalId: null,
|
|
408
490
|
autoplayPaused: false,
|
|
409
491
|
marqueeAnimationId: null,
|
|
410
492
|
marqueePaused: false,
|
|
411
|
-
marqueeLastTimestamp: 0
|
|
493
|
+
marqueeLastTimestamp: 0,
|
|
494
|
+
isLoopRepositioning: false
|
|
412
495
|
};
|
|
413
496
|
let dragState = null;
|
|
414
497
|
const loopState = {
|
|
@@ -473,6 +556,7 @@ var createSlider = (settings) => {
|
|
|
473
556
|
};
|
|
474
557
|
const handleLoopReposition = (direction) => {
|
|
475
558
|
if (!settings.loop || !loopState.initialized) return;
|
|
559
|
+
state.isLoopRepositioning = true;
|
|
476
560
|
const realSlides = loopState.realSlides;
|
|
477
561
|
const totalRealSlides = realSlides.length;
|
|
478
562
|
if (direction === "next") {
|
|
@@ -488,6 +572,12 @@ var createSlider = (settings) => {
|
|
|
488
572
|
}
|
|
489
573
|
state.currentSlideIndex = totalRealSlides - 1;
|
|
490
574
|
}
|
|
575
|
+
requestAnimationFrame(() => {
|
|
576
|
+
requestAnimationFrame(() => {
|
|
577
|
+
state.isLoopRepositioning = false;
|
|
578
|
+
updateControlsVisibility();
|
|
579
|
+
});
|
|
580
|
+
});
|
|
491
581
|
};
|
|
492
582
|
const cleanupLoopClones = () => {
|
|
493
583
|
if (!loopState.initialized) return;
|
|
@@ -534,6 +624,9 @@ var createSlider = (settings) => {
|
|
|
534
624
|
settings.scrollbarThumb.style.transform = `translateX(${totalTransform * scrollProgress}px)`;
|
|
535
625
|
};
|
|
536
626
|
const updateControlsVisibility = () => {
|
|
627
|
+
if (state.isLoopRepositioning) {
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
537
630
|
const feedRect = getFeedRect();
|
|
538
631
|
const isAtStart = settings.feed.scrollLeft <= 1;
|
|
539
632
|
const isAtEnd = settings.feed.scrollLeft + feedRect.width >= settings.feed.scrollWidth - 1;
|
|
@@ -696,8 +789,20 @@ var createSlider = (settings) => {
|
|
|
696
789
|
}
|
|
697
790
|
};
|
|
698
791
|
const handleWindowResize = () => {
|
|
699
|
-
state.
|
|
700
|
-
|
|
792
|
+
if (state.resizeTimeout) {
|
|
793
|
+
clearTimeout(state.resizeTimeout);
|
|
794
|
+
}
|
|
795
|
+
state.resizeTimeout = setTimeout(() => {
|
|
796
|
+
state.cachedFeedRect = null;
|
|
797
|
+
updateCurrentSlideIndex();
|
|
798
|
+
const currentIndex = state.currentSlideIndex;
|
|
799
|
+
refresh();
|
|
800
|
+
const slides = loopState.initialized ? loopState.realSlides : getVisibleSlides();
|
|
801
|
+
const targetSlide = slides[currentIndex];
|
|
802
|
+
if (targetSlide) {
|
|
803
|
+
settings.feed.scrollLeft = targetSlide.offsetLeft;
|
|
804
|
+
}
|
|
805
|
+
}, 150);
|
|
701
806
|
};
|
|
702
807
|
const attachEventListeners = () => {
|
|
703
808
|
const { signal } = state.abortController;
|
|
@@ -741,7 +846,8 @@ var createSlider = (settings) => {
|
|
|
741
846
|
onDragEnd: () => {
|
|
742
847
|
updateCurrentSlideIndex();
|
|
743
848
|
updateActiveThumb(settings.thumbs, state.currentSlideIndex);
|
|
744
|
-
}
|
|
849
|
+
},
|
|
850
|
+
dragFree: settings.dragFree
|
|
745
851
|
});
|
|
746
852
|
}
|
|
747
853
|
if (settings.autoplay && settings.pauseOnHover !== false) {
|
|
@@ -818,6 +924,9 @@ var createSlider = (settings) => {
|
|
|
818
924
|
if (state.scrollEndTimeout) {
|
|
819
925
|
clearTimeout(state.scrollEndTimeout);
|
|
820
926
|
}
|
|
927
|
+
if (state.resizeTimeout) {
|
|
928
|
+
clearTimeout(state.resizeTimeout);
|
|
929
|
+
}
|
|
821
930
|
if (dragState) {
|
|
822
931
|
cleanupDrag(dragState);
|
|
823
932
|
}
|
|
@@ -873,6 +982,7 @@ var createSlider = (settings) => {
|
|
|
873
982
|
easeOutCubic,
|
|
874
983
|
easeOutExpo,
|
|
875
984
|
easeOutQuad,
|
|
985
|
+
generateBullets,
|
|
876
986
|
linear
|
|
877
987
|
});
|
|
878
988
|
//# sourceMappingURL=index.cjs.map
|