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 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-dots">
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
- thumbs: [...document.querySelectorAll('.dot')],
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
- Use the `thumbs` option to add clickable dots that navigate to specific slides. The slider automatically manages the `active` class.
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
- thumbs: [...document.querySelectorAll('.dot')],
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 (Math.abs(state.velocity) > 1) {
235
- applyMomentum(state, feed, slides, smoothScrollTo, onDragEnd);
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.cachedFeedRect = null;
700
- refresh();
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