lazer-slider 1.0.5 → 1.0.7

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/dist/index.d.cts CHANGED
@@ -6,6 +6,10 @@ type EasingFunction = (t: number) => number;
6
6
  * Direction of slider navigation
7
7
  */
8
8
  type SliderDirection = 'prev' | 'next';
9
+ /**
10
+ * Direction of marquee scroll
11
+ */
12
+ type MarqueeDirection = 'left' | 'right';
9
13
  /**
10
14
  * Parameters passed to scroll start callback
11
15
  */
@@ -64,6 +68,12 @@ interface SliderSettings {
64
68
  autoplayInterval?: number;
65
69
  /** Pause autoplay on hover/touch (default: true) */
66
70
  pauseOnHover?: boolean;
71
+ /** Enable marquee mode (continuous scroll, overrides autoplay/loop) */
72
+ marquee?: boolean;
73
+ /** Marquee scroll speed in pixels per second (default: 50) */
74
+ marqueeSpeed?: number;
75
+ /** Marquee scroll direction (default: 'left') */
76
+ marqueeDirection?: MarqueeDirection;
67
77
  /** Custom easing function for smooth scroll animation */
68
78
  easing?: EasingFunction;
69
79
  /** Callback fired when scroll animation starts */
@@ -97,6 +107,12 @@ interface SliderState {
97
107
  autoplayIntervalId: ReturnType<typeof setInterval> | null;
98
108
  /** Whether autoplay is currently paused */
99
109
  autoplayPaused: boolean;
110
+ /** Animation frame ID for marquee */
111
+ marqueeAnimationId: number | null;
112
+ /** Whether marquee is currently paused */
113
+ marqueePaused: boolean;
114
+ /** Last timestamp for marquee animation (for delta time calculation) */
115
+ marqueeLastTimestamp: number;
100
116
  }
101
117
  /**
102
118
  * Drag state for drag-to-scroll functionality
@@ -164,4 +180,4 @@ declare const easeOutQuad: EasingFunction;
164
180
  */
165
181
  declare const linear: EasingFunction;
166
182
 
167
- export { type DragState, type EasingFunction, type ScrollParams, type ScrollStartParams, type Slider, type SliderDirection, type SliderSettings, type SliderState, createSlider, easeInOutCubic, easeOutCubic, easeOutExpo, easeOutQuad, linear };
183
+ export { type DragState, type EasingFunction, type MarqueeDirection, type ScrollParams, type ScrollStartParams, type Slider, type SliderDirection, type SliderSettings, type SliderState, createSlider, easeInOutCubic, easeOutCubic, easeOutExpo, easeOutQuad, linear };
package/dist/index.d.ts CHANGED
@@ -6,6 +6,10 @@ type EasingFunction = (t: number) => number;
6
6
  * Direction of slider navigation
7
7
  */
8
8
  type SliderDirection = 'prev' | 'next';
9
+ /**
10
+ * Direction of marquee scroll
11
+ */
12
+ type MarqueeDirection = 'left' | 'right';
9
13
  /**
10
14
  * Parameters passed to scroll start callback
11
15
  */
@@ -64,6 +68,12 @@ interface SliderSettings {
64
68
  autoplayInterval?: number;
65
69
  /** Pause autoplay on hover/touch (default: true) */
66
70
  pauseOnHover?: boolean;
71
+ /** Enable marquee mode (continuous scroll, overrides autoplay/loop) */
72
+ marquee?: boolean;
73
+ /** Marquee scroll speed in pixels per second (default: 50) */
74
+ marqueeSpeed?: number;
75
+ /** Marquee scroll direction (default: 'left') */
76
+ marqueeDirection?: MarqueeDirection;
67
77
  /** Custom easing function for smooth scroll animation */
68
78
  easing?: EasingFunction;
69
79
  /** Callback fired when scroll animation starts */
@@ -97,6 +107,12 @@ interface SliderState {
97
107
  autoplayIntervalId: ReturnType<typeof setInterval> | null;
98
108
  /** Whether autoplay is currently paused */
99
109
  autoplayPaused: boolean;
110
+ /** Animation frame ID for marquee */
111
+ marqueeAnimationId: number | null;
112
+ /** Whether marquee is currently paused */
113
+ marqueePaused: boolean;
114
+ /** Last timestamp for marquee animation (for delta time calculation) */
115
+ marqueeLastTimestamp: number;
100
116
  }
101
117
  /**
102
118
  * Drag state for drag-to-scroll functionality
@@ -164,4 +180,4 @@ declare const easeOutQuad: EasingFunction;
164
180
  */
165
181
  declare const linear: EasingFunction;
166
182
 
167
- export { type DragState, type EasingFunction, type ScrollParams, type ScrollStartParams, type Slider, type SliderDirection, type SliderSettings, type SliderState, createSlider, easeInOutCubic, easeOutCubic, easeOutExpo, easeOutQuad, linear };
183
+ export { type DragState, type EasingFunction, type MarqueeDirection, type ScrollParams, type ScrollStartParams, type Slider, type SliderDirection, type SliderSettings, type SliderState, createSlider, easeInOutCubic, easeOutCubic, easeOutExpo, easeOutQuad, linear };
package/dist/index.js CHANGED
@@ -234,6 +234,108 @@ var cleanupDrag = (state) => {
234
234
  }
235
235
  };
236
236
 
237
+ // src/core/marquee.ts
238
+ var createMarqueeState = () => ({
239
+ initialized: false,
240
+ clonedSlides: []
241
+ });
242
+ var setupMarqueeClones = (settings, marqueeState) => {
243
+ if (marqueeState.initialized) return;
244
+ settings.slides.forEach((slide) => {
245
+ const clone = slide.cloneNode(true);
246
+ clone.setAttribute("data-lazer-marquee-clone", "true");
247
+ clone.setAttribute("aria-hidden", "true");
248
+ settings.feed.appendChild(clone);
249
+ marqueeState.clonedSlides.push(clone);
250
+ });
251
+ marqueeState.initialized = true;
252
+ };
253
+ var cleanupMarqueeClones = (marqueeState) => {
254
+ if (!marqueeState.initialized) return;
255
+ marqueeState.clonedSlides.forEach((clone) => {
256
+ clone.remove();
257
+ });
258
+ marqueeState.clonedSlides = [];
259
+ marqueeState.initialized = false;
260
+ };
261
+ var handleMarqueeLoop = (settings) => {
262
+ const direction = settings.marqueeDirection ?? "left";
263
+ const halfWidth = settings.feed.scrollWidth / 2;
264
+ if (direction === "left") {
265
+ if (settings.feed.scrollLeft >= halfWidth) {
266
+ settings.feed.scrollLeft = settings.feed.scrollLeft - halfWidth;
267
+ }
268
+ } else {
269
+ if (settings.feed.scrollLeft <= 0) {
270
+ settings.feed.scrollLeft = halfWidth;
271
+ }
272
+ }
273
+ };
274
+ var startMarquee = (settings, state) => {
275
+ if (state.marqueeAnimationId) return;
276
+ const speed = settings.marqueeSpeed ?? 50;
277
+ const direction = settings.marqueeDirection ?? "left";
278
+ const directionMultiplier = direction === "left" ? 1 : -1;
279
+ if (direction === "right") {
280
+ settings.feed.scrollLeft = settings.feed.scrollWidth / 2;
281
+ }
282
+ const animate = (timestamp) => {
283
+ if (state.marqueePaused) {
284
+ state.marqueeLastTimestamp = timestamp;
285
+ state.marqueeAnimationId = requestAnimationFrame(animate);
286
+ return;
287
+ }
288
+ const deltaTime = state.marqueeLastTimestamp ? (timestamp - state.marqueeLastTimestamp) / 1e3 : 0;
289
+ state.marqueeLastTimestamp = timestamp;
290
+ const movement = speed * deltaTime * directionMultiplier;
291
+ settings.feed.scrollLeft += movement;
292
+ handleMarqueeLoop(settings);
293
+ state.marqueeAnimationId = requestAnimationFrame(animate);
294
+ };
295
+ state.marqueeAnimationId = requestAnimationFrame(animate);
296
+ };
297
+ var stopMarquee = (state) => {
298
+ if (state.marqueeAnimationId) {
299
+ cancelAnimationFrame(state.marqueeAnimationId);
300
+ state.marqueeAnimationId = null;
301
+ }
302
+ state.marqueeLastTimestamp = 0;
303
+ };
304
+ var pauseMarquee = (state) => {
305
+ state.marqueePaused = true;
306
+ };
307
+ var resumeMarquee = (state) => {
308
+ state.marqueePaused = false;
309
+ };
310
+ var setupMarquee = (settings, state, marqueeState) => {
311
+ if (!settings.marquee) return;
312
+ setupMarqueeClones(settings, marqueeState);
313
+ startMarquee(settings, state);
314
+ };
315
+ var attachMarqueeEventListeners = (settings, state, signal) => {
316
+ if (!settings.marquee || settings.pauseOnHover === false) return;
317
+ settings.feed.addEventListener(
318
+ "mouseenter",
319
+ () => pauseMarquee(state),
320
+ { signal }
321
+ );
322
+ settings.feed.addEventListener(
323
+ "mouseleave",
324
+ () => resumeMarquee(state),
325
+ { signal }
326
+ );
327
+ settings.feed.addEventListener(
328
+ "touchstart",
329
+ () => pauseMarquee(state),
330
+ { passive: true, signal }
331
+ );
332
+ settings.feed.addEventListener(
333
+ "touchend",
334
+ () => resumeMarquee(state),
335
+ { signal }
336
+ );
337
+ };
338
+
237
339
  // src/core/slider.ts
238
340
  var ANIMATION = {
239
341
  MIN_DURATION: 400,
@@ -260,9 +362,19 @@ var createSlider = (settings) => {
260
362
  scrollEndTimeout: null,
261
363
  abortController: new AbortController(),
262
364
  autoplayIntervalId: null,
263
- autoplayPaused: false
365
+ autoplayPaused: false,
366
+ marqueeAnimationId: null,
367
+ marqueePaused: false,
368
+ marqueeLastTimestamp: 0
264
369
  };
265
370
  let dragState = null;
371
+ const loopState = {
372
+ initialized: false,
373
+ clonedSlides: [],
374
+ realSlides: [...settings.slides],
375
+ clonesPerSide: 0
376
+ };
377
+ const marqueeState = createMarqueeState();
266
378
  const easing = settings.easing ?? easeOutExpo;
267
379
  const getFeedRect = () => {
268
380
  const currentWidth = settings.feed.clientWidth;
@@ -278,6 +390,71 @@ var createSlider = (settings) => {
278
390
  const isDesktop = () => {
279
391
  return window.matchMedia(DESKTOP_BREAKPOINT).matches;
280
392
  };
393
+ const getLoopClonesCount = () => {
394
+ const perView = isDesktop() ? settings.desktopSlidesPerView : settings.mobileSlidesPerView;
395
+ if (!perView || perView === "auto") {
396
+ return 1;
397
+ }
398
+ return Math.ceil(perView);
399
+ };
400
+ const setupLoopClones = () => {
401
+ if (!settings.loop || loopState.initialized) return;
402
+ const realSlides = loopState.realSlides;
403
+ const clonesCount = getLoopClonesCount();
404
+ loopState.clonesPerSide = clonesCount;
405
+ for (let i = realSlides.length - clonesCount; i < realSlides.length; i++) {
406
+ const slide = realSlides[i];
407
+ if (!slide) continue;
408
+ const clone = slide.cloneNode(true);
409
+ clone.setAttribute("data-lazer-clone", "prepend");
410
+ clone.setAttribute("aria-hidden", "true");
411
+ settings.feed.insertBefore(clone, settings.feed.firstChild);
412
+ loopState.clonedSlides.push(clone);
413
+ }
414
+ for (let i = 0; i < clonesCount; i++) {
415
+ const slide = realSlides[i];
416
+ if (!slide) continue;
417
+ const clone = slide.cloneNode(true);
418
+ clone.setAttribute("data-lazer-clone", "append");
419
+ clone.setAttribute("aria-hidden", "true");
420
+ settings.feed.appendChild(clone);
421
+ loopState.clonedSlides.push(clone);
422
+ }
423
+ requestAnimationFrame(() => {
424
+ const firstRealSlide = realSlides[0];
425
+ if (firstRealSlide) {
426
+ settings.feed.scrollLeft = firstRealSlide.offsetLeft;
427
+ }
428
+ });
429
+ loopState.initialized = true;
430
+ };
431
+ const handleLoopReposition = (direction) => {
432
+ if (!settings.loop || !loopState.initialized) return;
433
+ const realSlides = loopState.realSlides;
434
+ const totalRealSlides = realSlides.length;
435
+ if (direction === "next") {
436
+ const firstRealSlide = realSlides[0];
437
+ if (firstRealSlide) {
438
+ settings.feed.scrollLeft = firstRealSlide.offsetLeft;
439
+ }
440
+ state.currentSlideIndex = 0;
441
+ } else {
442
+ const lastRealSlide = realSlides[totalRealSlides - 1];
443
+ if (lastRealSlide) {
444
+ settings.feed.scrollLeft = lastRealSlide.offsetLeft;
445
+ }
446
+ state.currentSlideIndex = totalRealSlides - 1;
447
+ }
448
+ };
449
+ const cleanupLoopClones = () => {
450
+ if (!loopState.initialized) return;
451
+ loopState.clonedSlides.forEach((clone) => {
452
+ clone.remove();
453
+ });
454
+ loopState.clonedSlides = [];
455
+ loopState.initialized = false;
456
+ loopState.clonesPerSide = 0;
457
+ };
281
458
  const applySlideWidths = () => {
282
459
  const perView = isDesktop() ? settings.desktopSlidesPerView : settings.mobileSlidesPerView;
283
460
  const gap = settings.slideGap ?? 0;
@@ -347,14 +524,14 @@ var createSlider = (settings) => {
347
524
  };
348
525
  const updateCurrentSlideIndex = () => {
349
526
  const feedRect = getFeedRect();
350
- const allVisibleSlides = getVisibleSlides();
351
- const viewportVisibleSlides = settings.slides.filter((slide) => {
527
+ const slidesToCheck = loopState.initialized ? loopState.realSlides : getVisibleSlides();
528
+ const viewportVisibleSlides = slidesToCheck.filter((slide) => {
352
529
  const slideRect = slide.getBoundingClientRect();
353
530
  const tolerance = 20;
354
531
  return slideRect.right > feedRect.left + tolerance && slideRect.left < feedRect.right - tolerance;
355
532
  });
356
533
  if (viewportVisibleSlides.length && viewportVisibleSlides[0]) {
357
- const newIndex = allVisibleSlides.indexOf(viewportVisibleSlides[0]);
534
+ const newIndex = slidesToCheck.indexOf(viewportVisibleSlides[0]);
358
535
  if (newIndex !== -1) {
359
536
  state.currentSlideIndex = newIndex;
360
537
  settings.onScroll?.({
@@ -364,7 +541,7 @@ var createSlider = (settings) => {
364
541
  }
365
542
  }
366
543
  };
367
- const smoothScrollTo = (target, customEasing = easing) => {
544
+ const smoothScrollTo = (target, customEasing = easing, onComplete) => {
368
545
  const start = settings.feed.scrollLeft;
369
546
  const distance = Math.abs(target - start);
370
547
  const duration = Math.min(
@@ -381,6 +558,7 @@ var createSlider = (settings) => {
381
558
  requestAnimationFrame(animateScroll);
382
559
  } else {
383
560
  settings.feed.scrollLeft = target;
561
+ onComplete?.();
384
562
  }
385
563
  };
386
564
  requestAnimationFrame(animateScroll);
@@ -401,34 +579,51 @@ var createSlider = (settings) => {
401
579
  smoothScrollTo(settings.slides[index].offsetLeft);
402
580
  };
403
581
  const handleNavButtonClick = (direction) => {
404
- const visibleSlides = getVisibleSlides();
582
+ const realSlides = loopState.initialized ? loopState.realSlides : getVisibleSlides();
405
583
  const slidesToScroll = isDesktop() ? settings.desktopSlidesPerScroll ?? 1 : settings.mobileSlidesPerScroll ?? 1;
406
- const totalSlides = visibleSlides.length;
584
+ const totalRealSlides = realSlides.length;
407
585
  updateCurrentSlideIndex();
586
+ let targetSlide;
587
+ let needsReposition = false;
408
588
  if (direction === "prev") {
409
- if (settings.loop && state.currentSlideIndex === 0) {
410
- state.currentSlideIndex = totalSlides - 1;
589
+ if (settings.loop && loopState.initialized && state.currentSlideIndex === 0) {
590
+ const prependedClones = loopState.clonedSlides.filter(
591
+ (clone) => clone.getAttribute("data-lazer-clone") === "prepend"
592
+ );
593
+ targetSlide = prependedClones[prependedClones.length - 1];
594
+ needsReposition = true;
411
595
  } else {
412
596
  state.currentSlideIndex = Math.max(0, state.currentSlideIndex - slidesToScroll);
597
+ targetSlide = realSlides[state.currentSlideIndex];
413
598
  }
414
599
  } else {
415
- if (settings.loop && state.currentSlideIndex >= totalSlides - 1) {
416
- state.currentSlideIndex = 0;
600
+ if (settings.loop && loopState.initialized && state.currentSlideIndex >= totalRealSlides - 1) {
601
+ const appendedClones = loopState.clonedSlides.filter(
602
+ (clone) => clone.getAttribute("data-lazer-clone") === "append"
603
+ );
604
+ targetSlide = appendedClones[0];
605
+ needsReposition = true;
417
606
  } else {
418
607
  state.currentSlideIndex = Math.min(
419
- totalSlides - 1,
608
+ totalRealSlides - 1,
420
609
  state.currentSlideIndex + slidesToScroll
421
610
  );
611
+ targetSlide = realSlides[state.currentSlideIndex];
422
612
  }
423
613
  }
424
- const targetSlide = visibleSlides[state.currentSlideIndex];
425
614
  if (!targetSlide) return;
426
615
  settings.onScrollStart?.({
427
616
  currentScroll: settings.feed.scrollLeft,
428
617
  target: targetSlide,
429
618
  direction
430
619
  });
431
- smoothScrollTo(targetSlide.offsetLeft);
620
+ if (needsReposition) {
621
+ smoothScrollTo(targetSlide.offsetLeft, easing, () => {
622
+ handleLoopReposition(direction);
623
+ });
624
+ } else {
625
+ smoothScrollTo(targetSlide.offsetLeft);
626
+ }
432
627
  };
433
628
  const updateScrollPosition = () => {
434
629
  updateScrollbarPosition();
@@ -528,6 +723,7 @@ var createSlider = (settings) => {
528
723
  { signal }
529
724
  );
530
725
  }
726
+ attachMarqueeEventListeners(settings, state, signal);
531
727
  };
532
728
  const startAutoplay = () => {
533
729
  if (state.autoplayIntervalId) return;
@@ -551,9 +747,9 @@ var createSlider = (settings) => {
551
747
  state.autoplayPaused = false;
552
748
  };
553
749
  const goToIndex = (index) => {
554
- const visibleSlides = getVisibleSlides();
555
- const safeIndex = Math.max(0, Math.min(index, visibleSlides.length - 1));
556
- const targetSlide = visibleSlides[safeIndex];
750
+ const slides = loopState.initialized ? loopState.realSlides : getVisibleSlides();
751
+ const safeIndex = Math.max(0, Math.min(index, slides.length - 1));
752
+ const targetSlide = slides[safeIndex];
557
753
  if (!targetSlide) return;
558
754
  state.currentSlideIndex = safeIndex;
559
755
  updateActiveThumb(settings.thumbs, safeIndex);
@@ -567,6 +763,7 @@ var createSlider = (settings) => {
567
763
  };
568
764
  const unload = () => {
569
765
  stopAutoplay();
766
+ stopMarquee(state);
570
767
  state.abortController.abort();
571
768
  window.removeEventListener("resize", handleWindowResize);
572
769
  if (state.updateThumbTimeout) {
@@ -578,22 +775,43 @@ var createSlider = (settings) => {
578
775
  if (dragState) {
579
776
  cleanupDrag(dragState);
580
777
  }
778
+ cleanupLoopClones();
779
+ cleanupMarqueeClones(marqueeState);
581
780
  state.cachedFeedRect = null;
582
781
  };
583
782
  initAria(settings);
584
783
  applySlideWidths();
784
+ if (settings.marquee) {
785
+ setupMarquee(settings, state, marqueeState);
786
+ } else {
787
+ setupLoopClones();
788
+ if (settings.autoplay) {
789
+ startAutoplay();
790
+ }
791
+ }
585
792
  updateControlsVisibility();
586
793
  attachEventListeners();
587
794
  updateScrollbar();
588
- if (settings.autoplay) {
589
- startAutoplay();
590
- }
795
+ const play = () => {
796
+ if (settings.marquee) {
797
+ startMarquee(settings, state);
798
+ } else if (settings.autoplay) {
799
+ startAutoplay();
800
+ }
801
+ };
802
+ const pause = () => {
803
+ if (settings.marquee) {
804
+ stopMarquee(state);
805
+ } else {
806
+ stopAutoplay();
807
+ }
808
+ };
591
809
  return {
592
810
  goToIndex,
593
811
  refresh,
594
812
  unload,
595
- play: startAutoplay,
596
- pause: stopAutoplay,
813
+ play,
814
+ pause,
597
815
  next: () => handleNavButtonClick("next"),
598
816
  prev: () => handleNavButtonClick("prev")
599
817
  };