@mozert-studio/mozert-slider 1.1.0 → 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.
@@ -1,10 +1,10 @@
1
1
  /*!
2
- * Mozert 1.1.0
2
+ * Mozert 1.1.1
3
3
  * A dependency-free, responsive, touch-friendly carousel / slider.
4
4
  * MIT License
5
5
  */
6
6
  const Mozert = (function () {
7
- const VERSION = '1.1.0';
7
+ const VERSION = '1.1.1';
8
8
 
9
9
  const DEFAULTS = {
10
10
  accessibility: true,
@@ -70,7 +70,7 @@ const DEFAULTS = {
70
70
  on: null,
71
71
  };
72
72
 
73
- const LABELS = { slide: 'Slide {n} of {total}', prev: 'Previous slide', next: 'Next slide', goTo: 'Go to slide {n}' };
73
+ const LABELS = { slide: 'Slide {n} of {total}', prev: 'Previous slide', next: 'Next slide', goTo: 'Go to slide {n}', fullscreen: 'Toggle fullscreen', close: 'Close', zoom: 'Zoom', lightbox: 'Image viewer', imageOf: 'Image {n} of {total}', magnifier: 'Magnifier' };
74
74
  const DEFAULT_PREV = '<button type="button" class="mozert-prev" aria-label="Previous slide">Previous</button>';
75
75
  const DEFAULT_NEXT = '<button type="button" class="mozert-next" aria-label="Next slide">Next</button>';
76
76
  const FOCUSABLE = 'a[href],button,input,select,textarea,[tabindex],[contenteditable="true"]';
@@ -463,7 +463,7 @@ class Mozert {
463
463
  s.slidesToShow = 1;
464
464
  s.slidesToScroll = 1;
465
465
  }
466
- if (s.effect === 'coverflow') s.centerMode = true;
466
+ if (s.effect === 'coverflow' || s.effect === 'expand') s.centerMode = true;
467
467
  if (s.freeMode || s.virtual || s.bleed) s.infinite = false;
468
468
  if (s.marquee) {
469
469
  s.infinite = true;
@@ -1040,7 +1040,18 @@ class Mozert {
1040
1040
 
1041
1041
  _slideTo(target, opts = {}) {
1042
1042
  const s = this.s;
1043
- if (!this.active || !this.canNav) return false;
1043
+ if (!this.active) return false;
1044
+ if (!this.canNav) {
1045
+ // A strip that shows every slide cannot move, but a synced partner can still change which slide is highlighted.
1046
+ if (opts.fromSync) {
1047
+ const sel = opts.sel !== undefined && opts.sel !== 0 ? opts.sel : null;
1048
+ if (sel !== this._sel) {
1049
+ this._sel = sel;
1050
+ this._updateSlideClasses(this._logical);
1051
+ }
1052
+ }
1053
+ return false;
1054
+ }
1044
1055
  if (this.animating && !opts.fromSync && s.waitForAnimate) return false;
1045
1056
  let next = target;
1046
1057
  if (!s.infinite || s.stacked) next = s.stacked ? this._norm(next) : clamp(next, 0, this.maxIndex);
@@ -1049,9 +1060,19 @@ class Mozert {
1049
1060
  // (a thumbnail strip stops at its last full page, but the main slider should still go to the clicked one).
1050
1061
  const want = s.infinite || s.stacked ? real : clamp(target, 0, this.n - 1);
1051
1062
  if (next === this.current && !this.animating && !opts.force) {
1052
- if (!opts.fromSync && want !== next) {
1053
- this._sel = want;
1054
- this._updateSlideClasses(this._logical);
1063
+ if (opts.fromSync) {
1064
+ // Already at the clamped position: only move the highlight to the slide the partner asked for.
1065
+ const sel = opts.sel !== undefined && opts.sel !== next ? opts.sel : null;
1066
+ if (sel !== this._sel) {
1067
+ this._sel = sel;
1068
+ this._updateSlideClasses(this._logical);
1069
+ }
1070
+ } else {
1071
+ // The strip already sits on this index (its last full page), but the partner may still be elsewhere.
1072
+ if (want !== next) {
1073
+ this._sel = want;
1074
+ this._updateSlideClasses(this._logical);
1075
+ }
1055
1076
  this._syncNav(want);
1056
1077
  }
1057
1078
  return false;
@@ -1313,11 +1334,19 @@ class Mozert {
1313
1334
  this._dragged = false;
1314
1335
  return;
1315
1336
  }
1316
- if (s.focusOnSelect && this.canNav) {
1337
+ if (s.focusOnSelect && (this.canNav || s.asNavFor)) {
1317
1338
  const slide = e.target.closest ? e.target.closest('.mozert-slide') : null;
1318
1339
  if (slide && this.track.contains(slide)) {
1319
1340
  const i = parseInt(slide.getAttribute('data-mozert-index'), 10);
1320
- if (!isNaN(i)) this.goTo(i);
1341
+ if (!isNaN(i)) {
1342
+ if (this.canNav) this.goTo(i);
1343
+ else {
1344
+ // Nothing to scroll: just highlight the clicked slide and tell the partner.
1345
+ this._sel = i;
1346
+ this._updateSlideClasses(this._logical);
1347
+ this._syncNav(i);
1348
+ }
1349
+ }
1321
1350
  }
1322
1351
  }
1323
1352
  },
@@ -1558,11 +1587,11 @@ class Mozert {
1558
1587
  const v = this.s.asNavFor;
1559
1588
  if (!v) return [];
1560
1589
  let els = [];
1561
- if (v instanceof Mozert) return [v];
1590
+ if (v instanceof Mozert) return v.active && v !== this ? [v] : [];
1562
1591
  if (typeof v === 'string') els = Array.from(document.querySelectorAll(v));
1563
1592
  else if (isEl(v)) els = [v];
1564
1593
  else if (v.length) els = Array.from(v);
1565
- return els.map((e) => registry.get(e)).filter((i) => i && i !== this && i.active && i.canNav);
1594
+ return els.map((e) => registry.get(e)).filter((i) => i && i !== this && i.active);
1566
1595
  }
1567
1596
 
1568
1597
  _syncNav(index) {
@@ -1739,11 +1768,12 @@ Mozert._bridge = null;
1739
1768
  const pct = (v, size) => (typeof v === 'string' && v.indexOf('%') > -1 ? (parseFloat(v) / 100) * size : parseFloat(v) || 0);
1740
1769
 
1741
1770
  /* ------------------------------------------------------------ effects */
1742
- const FX = ['coverflow', 'cube', 'flip', 'cards', 'creative'];
1771
+ const FX = ['coverflow', 'cube', 'flip', 'cards', 'creative', 'expand'];
1743
1772
  Mozert.use({
1744
1773
  name: 'effects',
1745
1774
  defaults: {
1746
- coverflowEffect: { rotate: 45, depth: 120, scale: 1, stretch: 0, modifier: 1, shadow: true },
1775
+ coverflowEffect: { rotate: 45, depth: 120, scale: 1, stretch: 0, modifier: 1, lift: 0, fade: 0, shadow: true },
1776
+ expandEffect: { extra: 1.4 },
1747
1777
  cubeEffect: { shadow: true, slideShadows: true, perspective: 1200 },
1748
1778
  flipEffect: { perspective: 1400, limitRotation: true },
1749
1779
  cardsEffect: { perSlideOffset: 10, perSlideRotate: 2, rotate: true, visible: 4 },
@@ -1794,8 +1824,22 @@ Mozert._bridge = null;
1794
1824
  const rot = clampN(-r, -3, 3) * o.rotate * o.modifier;
1795
1825
  const tz = -a * o.depth;
1796
1826
  const sc = 1 - Math.min(a, 3) * (1 - o.scale) * 0.5;
1797
- tr = 'translate3d(' + -r * o.stretch + 'px,0,' + tz + 'px) rotateY(' + rot + 'deg) scale(' + sc + ')';
1798
- op = a > 4 ? 0 : 1;
1827
+ tr = 'translate3d(' + -r * o.stretch + 'px,' + -Math.min(a, 3) * o.lift + 'px,' + tz + 'px) rotateY(' + rot + 'deg) scale(' + sc + ')';
1828
+ op = a > 4 ? 0 : Math.max(0, 1 - Math.min(a, 4) * o.fade);
1829
+ } else if (fx === 'expand') {
1830
+ // The active card grows wider, its neighbours slide out of the way.
1831
+ // Both follow the same linear rule, so dragging stays continuous.
1832
+ const m = Math.min(1, a);
1833
+ const w0 = e.offsetWidth;
1834
+ const grow = o.extra * w0 * (1 - m);
1835
+ tr = 'translate3d(' + (r < 0 ? -1 : 1) * ((o.extra * w0) / 2) * m + 'px,0,0)';
1836
+ const card = e.firstElementChild;
1837
+ if (card) {
1838
+ card.style.transition = animate && !jump ? 'width ' + speed + 'ms ' + s.cssEase + ', margin-left ' + speed + 'ms ' + s.cssEase : 'none';
1839
+ card.style.width = w0 + grow + 'px';
1840
+ card.style.marginLeft = -grow / 2 + 'px';
1841
+ card.style.maxWidth = 'none';
1842
+ }
1799
1843
  } else if (fx === 'cube') {
1800
1844
  tr = 'translate3d(0,0,' + -W / 2 + 'px) rotateY(' + r * 90 + 'deg) translate3d(0,0,' + W / 2 + 'px)';
1801
1845
  op = a <= 1.01 ? 1 : 0;
@@ -1842,6 +1886,7 @@ Mozert._bridge = null;
1842
1886
  list.style.overflow = '';
1843
1887
  sl.el.classList.remove('mozert-fx-' + fx);
1844
1888
  slides().forEach((e) => {
1889
+ if (fx === 'expand' && e.firstElementChild) e.firstElementChild.style.width = e.firstElementChild.style.marginLeft = e.firstElementChild.style.transition = e.firstElementChild.style.maxWidth = '';
1845
1890
  e.style.transform = e.style.opacity = e.style.zIndex = e.style.transition = e.style.pointerEvents = e.style.backfaceVisibility = '';
1846
1891
  });
1847
1892
  },
@@ -2606,6 +2651,515 @@ Mozert._bridge = null;
2606
2651
  };
2607
2652
  },
2608
2653
  });
2654
+
2655
+ /* ------------------------------------------------------------- thumbs */
2656
+ // thumbs: '#nav' (an existing strip), true (create one below the slider) or { el, options, count }.
2657
+ // An empty strip is filled automatically from each slide's data-thumb, first <img> or background.
2658
+ Mozert.use({
2659
+ name: 'thumbs',
2660
+ defaults: { thumbs: false },
2661
+ create(sl) {
2662
+ const cfg = sl.s.thumbs;
2663
+ if (!cfg || sl.n < 2) return null;
2664
+ const o = objOpt(cfg, { el: null, options: {}, count: 5, position: 'bottom', breakpoint: 640 });
2665
+ const want = ['left', 'right', 'top', 'bottom'].indexOf(o.position) > -1 ? o.position : 'bottom';
2666
+ // A side strip moves below the picture on narrow screens (viewport <= breakpoint px).
2667
+ const mq = (want === 'left' || want === 'right') && o.breakpoint > 0 && typeof matchMedia === 'function' ? matchMedia('(max-width:' + o.breakpoint + 'px)') : null;
2668
+ const pos = mq && mq.matches ? 'bottom' : want;
2669
+ const side = pos === 'left' || pos === 'right';
2670
+ const onMq = () => sl.active && sl.refresh();
2671
+ if (mq) mq.addEventListener ? mq.addEventListener('change', onMq) : mq.addListener(onMq);
2672
+ let host = null;
2673
+ let made = false;
2674
+ let wrap = null;
2675
+ const ref = cfg === true ? null : typeof cfg === 'object' && !cfg.nodeType ? o.el : cfg;
2676
+ if (ref) host = typeof ref === 'string' ? document.querySelector(ref) : ref && ref.nodeType ? ref : null;
2677
+ if (!host) {
2678
+ if (ref) return null;
2679
+ host = mk('div', 'mozert-thumbs');
2680
+ made = true;
2681
+ if (side) {
2682
+ wrap = mk('div', 'mozert-gallery mozert-gallery-' + pos);
2683
+ sl.el.parentNode.insertBefore(wrap, sl.el);
2684
+ wrap.appendChild(sl.el);
2685
+ wrap.appendChild(host);
2686
+ } else if (pos === 'top') sl.el.parentNode.insertBefore(host, sl.el);
2687
+ else sl.el.parentNode.insertBefore(host, sl.el.nextSibling);
2688
+ }
2689
+ host.setAttribute('data-mozert-thumbs', pos);
2690
+ const auto = host.children.length === 0;
2691
+ if (auto) {
2692
+ (sl._items || []).forEach((src, i) => {
2693
+ const cell = mk('div');
2694
+ const first = src.firstElementChild;
2695
+ const img = src.querySelector('img');
2696
+ const url = src.getAttribute('data-thumb') || (img && (img.getAttribute('data-thumb') || img.getAttribute('src') || img.getAttribute('data-lazy')));
2697
+ if (url) {
2698
+ const t = mk('img', 'mozert-thumb');
2699
+ t.src = url;
2700
+ t.alt = '';
2701
+ t.draggable = false;
2702
+ cell.appendChild(t);
2703
+ } else {
2704
+ const t = mk('div', 'mozert-thumb mozert-thumb-fill');
2705
+ const bg = first ? getComputedStyle(first).backgroundImage : 'none';
2706
+ if (bg && bg !== 'none') t.style.backgroundImage = bg;
2707
+ else t.style.background = (first && getComputedStyle(first).backgroundColor) || '#ccd';
2708
+ t.textContent = String(i + 1);
2709
+ cell.appendChild(t);
2710
+ }
2711
+ host.appendChild(cell);
2712
+ });
2713
+ }
2714
+ const count = Math.max(1, Math.min(o.count, sl.n));
2715
+ const gap = o.options && o.options.spaceBetween != null ? o.options.spaceBetween : 10;
2716
+ const fit = () => {
2717
+ // vertical strip: size each thumb so `count` of them fill exactly the height of the main slider
2718
+ const H = sl.el.offsetHeight;
2719
+ if (!H) return;
2720
+ host.style.setProperty('--mozert-thumb-h', Math.max(24, (H - gap * (count - 1)) / count) + 'px');
2721
+ host.style.height = H + 'px';
2722
+ th.refresh();
2723
+ };
2724
+ const th = new Mozert(host, Object.assign({ slidesToShow: count, slidesToScroll: 1, arrows: false, dots: false, infinite: false, swipeToSlide: true, spaceBetween: gap, focusOnSelect: true, initialSlide: 0, vertical: side }, o.options, { asNavFor: sl, thumbs: false, lightbox: false }));
2725
+ let ro = null;
2726
+ if (side) {
2727
+ fit();
2728
+ if (typeof ResizeObserver !== 'undefined') {
2729
+ let lastH = sl.el.offsetHeight;
2730
+ ro = new ResizeObserver(() => {
2731
+ if (sl.el.offsetHeight !== lastH) {
2732
+ lastH = sl.el.offsetHeight;
2733
+ fit();
2734
+ }
2735
+ });
2736
+ ro.observe(sl.el);
2737
+ }
2738
+ }
2739
+ const prevNav = sl.s.asNavFor;
2740
+ if (!prevNav) sl.s.asNavFor = th;
2741
+ host.classList.add('mozert-thumbs');
2742
+ sl._syncNav(sl.current);
2743
+ return {
2744
+ destroy() {
2745
+ if (sl.s.asNavFor === th) sl.s.asNavFor = prevNav;
2746
+ if (ro) ro.disconnect();
2747
+ if (mq) mq.removeEventListener ? mq.removeEventListener('change', onMq) : mq.removeListener(onMq);
2748
+ host.removeAttribute('data-mozert-thumbs');
2749
+ host.style.removeProperty('--mozert-thumb-h');
2750
+ if (side) host.style.height = '';
2751
+ th.destroy();
2752
+ if (wrap && wrap.parentNode) {
2753
+ wrap.parentNode.insertBefore(sl.el, wrap);
2754
+ wrap.parentNode.removeChild(wrap);
2755
+ } else if (made) host.parentNode && host.parentNode.removeChild(host);
2756
+ else if (auto) host.innerHTML = '';
2757
+ },
2758
+ };
2759
+ },
2760
+ });
2761
+
2762
+ /* ----------------------------------------------------------- lightbox */
2763
+ // lightbox: true | { zoom: true, loop: true }. Click a slide to open it full screen.
2764
+ Mozert.use({
2765
+ name: 'lightbox',
2766
+ defaults: { lightbox: false },
2767
+ create(sl) {
2768
+ if (!sl.s.lightbox) return null;
2769
+ const o = objOpt(sl.s.lightbox, { zoom: true, loop: true, maxZoom: 2.5 });
2770
+ let box = null;
2771
+ let idx = 0;
2772
+ let opener = null;
2773
+ let zoomed = false;
2774
+ let zrect = null;
2775
+ let drag = null;
2776
+ const items = () => sl._items || [];
2777
+ const isImg = (h) => /^data:image\//i.test(h || '') || /\.(jpe?g|png|gif|webp|avif|svg)(\?|#|$)/i.test(h || '');
2778
+ const info = (src) => {
2779
+ const img = src.querySelector('img');
2780
+ const a = src.querySelector('a[href]');
2781
+ const full = src.getAttribute('data-full') || (img && img.getAttribute('data-full')) || (a && isImg(a.getAttribute('href')) ? a.getAttribute('href') : '') || (img && (img.currentSrc || img.getAttribute('src'))) || '';
2782
+ return { src: full, alt: (img && img.getAttribute('alt')) || '', cap: src.getAttribute('data-caption') || (img && img.getAttribute('data-caption')) || '', node: src };
2783
+ };
2784
+ const stageContent = (it) => {
2785
+ if (it.src) {
2786
+ const im = mk('img', 'mozert-lb-img');
2787
+ im.src = it.src;
2788
+ im.alt = it.alt;
2789
+ im.draggable = false;
2790
+ return im;
2791
+ }
2792
+ // Slides without an image: show a larger copy of the slide itself.
2793
+ const w = mk('div', 'mozert-lb-clone');
2794
+ w.innerHTML = it.node.innerHTML;
2795
+ return w;
2796
+ };
2797
+ const setZoom = (on, ev) => {
2798
+ const el = box && box.querySelector('.mozert-lb-img,.mozert-lb-clone');
2799
+ if (!el || !o.zoom) return;
2800
+ zoomed = on;
2801
+ box.classList.toggle('mozert-lb-zoomed', on);
2802
+ if (on) {
2803
+ const r = el.getBoundingClientRect();
2804
+ zrect = r;
2805
+ const ox = ev ? ((ev.clientX - r.left) / r.width) * 100 : 50;
2806
+ const oy = ev ? ((ev.clientY - r.top) / r.height) * 100 : 50;
2807
+ el.style.transformOrigin = ox + '% ' + oy + '%';
2808
+ el.style.transform = 'scale(' + o.maxZoom + ')';
2809
+ } else {
2810
+ el.style.transform = '';
2811
+ }
2812
+ };
2813
+ const show = (i, dir) => {
2814
+ const list = items();
2815
+ if (!list.length) return;
2816
+ if (o.loop) i = (i + list.length) % list.length;
2817
+ else i = clampN(i, 0, list.length - 1);
2818
+ idx = i;
2819
+ setZoom(false);
2820
+ const it = info(list[i]);
2821
+ const stage = box.querySelector('.mozert-lb-stage');
2822
+ stage.innerHTML = '';
2823
+ const c = stageContent(it);
2824
+ stage.appendChild(c);
2825
+ c.classList.add('mozert-lb-in');
2826
+ if (dir) c.style.setProperty('--lb-from', dir > 0 ? '40px' : '-40px');
2827
+ box.querySelector('.mozert-lb-count').textContent = i + 1 + ' / ' + list.length;
2828
+ box.querySelector('.mozert-lb-cap').textContent = it.cap;
2829
+ box.setAttribute('aria-label', sl._label('imageOf', { n: i + 1, total: list.length }));
2830
+ const edge = !o.loop && list.length > 1;
2831
+ box.querySelector('.mozert-lb-prev').disabled = edge && i === 0;
2832
+ box.querySelector('.mozert-lb-next').disabled = edge && i === list.length - 1;
2833
+ // Preload the neighbours.
2834
+ [i - 1, i + 1].forEach((k) => {
2835
+ const n = list[(k + list.length) % list.length];
2836
+ const u = n && info(n).src;
2837
+ if (u) new Image().src = u;
2838
+ });
2839
+ if (sl.emit) sl.emit('lightboxChange', i);
2840
+ };
2841
+ const key = (e) => {
2842
+ if (!box) return;
2843
+ if (e.key === 'Escape') {
2844
+ e.preventDefault();
2845
+ close();
2846
+ } else if (e.key === 'ArrowRight') show(idx + 1, 1);
2847
+ else if (e.key === 'ArrowLeft') show(idx - 1, -1);
2848
+ else if (e.key === 'Tab') {
2849
+ const f = Array.from(box.querySelectorAll('button:not([disabled])'));
2850
+ if (!f.length) return;
2851
+ const first = f[0];
2852
+ const last = f[f.length - 1];
2853
+ if (e.shiftKey && document.activeElement === first) {
2854
+ e.preventDefault();
2855
+ last.focus();
2856
+ } else if (!e.shiftKey && document.activeElement === last) {
2857
+ e.preventDefault();
2858
+ first.focus();
2859
+ }
2860
+ }
2861
+ };
2862
+ const open = (i) => {
2863
+ if (box || !items().length) return;
2864
+ opener = document.activeElement;
2865
+ const lb = (k) => sl._label(k);
2866
+ const single = items().length < 2;
2867
+ box = mk('div', 'mozert-lb');
2868
+ box.setAttribute('role', 'dialog');
2869
+ box.setAttribute('aria-modal', 'true');
2870
+ box.innerHTML =
2871
+ '<div class="mozert-lb-stage"></div>' +
2872
+ '<div class="mozert-lb-bar"><span class="mozert-lb-count"></span>' +
2873
+ (o.zoom ? '<button type="button" class="mozert-lb-zoom" aria-label="' + lb('zoom') + '"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3M11 8v6M8 11h6"/></svg></button>' : '') +
2874
+ '<button type="button" class="mozert-lb-close" aria-label="' + lb('close') + '"><svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><path d="M5 5l14 14M19 5L5 19"/></svg></button></div>' +
2875
+ (single ? '' : '<button type="button" class="mozert-lb-prev" aria-label="' + lb('prev') + '"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg></button><button type="button" class="mozert-lb-next" aria-label="' + lb('next') + '"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2.4" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg></button>') +
2876
+ '<div class="mozert-lb-cap"></div>';
2877
+ if (single) {
2878
+ box.insertAdjacentHTML('beforeend', '<button type="button" class="mozert-lb-prev" hidden></button><button type="button" class="mozert-lb-next" hidden></button>');
2879
+ }
2880
+ document.body.appendChild(box);
2881
+ document.documentElement.classList.add('mozert-lb-open');
2882
+ show(i);
2883
+ requestAnimationFrame(() => box && box.classList.add('mozert-lb-show'));
2884
+ const stage = box.querySelector('.mozert-lb-stage');
2885
+ box.querySelector('.mozert-lb-close').addEventListener('click', close);
2886
+ const pv = box.querySelector('.mozert-lb-prev');
2887
+ const nx = box.querySelector('.mozert-lb-next');
2888
+ pv.addEventListener('click', () => show(idx - 1, -1));
2889
+ nx.addEventListener('click', () => show(idx + 1, 1));
2890
+ const zb = box.querySelector('.mozert-lb-zoom');
2891
+ if (zb) zb.addEventListener('click', () => setZoom(!zoomed));
2892
+ // Click outside the picture closes, click on the picture toggles zoom, swipe changes picture.
2893
+ stage.addEventListener('click', (e) => {
2894
+ if (drag && drag.moved) return;
2895
+ if (e.target.closest && e.target.closest('.mozert-lb-img,.mozert-lb-clone')) setZoom(!zoomed, e);
2896
+ else if (e.target === stage) close();
2897
+ });
2898
+ stage.addEventListener('pointerdown', (e) => {
2899
+ drag = { x: e.clientX, y: e.clientY, moved: false, id: e.pointerId };
2900
+ });
2901
+ stage.addEventListener('pointermove', (e) => {
2902
+ // While zoomed, the mouse pans the picture (the zoom follows the pointer).
2903
+ if (zoomed && zrect && e.pointerType === 'mouse' && !drag) {
2904
+ const z = box.querySelector('.mozert-lb-img,.mozert-lb-clone');
2905
+ if (z) z.style.transformOrigin = clampN(((e.clientX - zrect.left) / zrect.width) * 100, 0, 100) + '% ' + clampN(((e.clientY - zrect.top) / zrect.height) * 100, 0, 100) + '%';
2906
+ }
2907
+ if (!drag) return;
2908
+ const dx = e.clientX - drag.x;
2909
+ if (Math.abs(dx) > 6 || Math.abs(e.clientY - drag.y) > 6) drag.moved = true;
2910
+ });
2911
+ stage.addEventListener('pointerup', (e) => {
2912
+ if (!drag) return;
2913
+ const dx = e.clientX - drag.x;
2914
+ const moved = drag.moved;
2915
+ if (moved && !zoomed && Math.abs(dx) > 50) show(idx + (dx < 0 ? 1 : -1), dx < 0 ? 1 : -1);
2916
+ setTimeout(() => (drag = null), 0);
2917
+ });
2918
+ document.addEventListener('keydown', key, true);
2919
+ (box.querySelector('.mozert-lb-close') || box).focus();
2920
+ if (sl.emit) sl.emit('lightboxOpen', i);
2921
+ };
2922
+ const close = () => {
2923
+ if (!box) return;
2924
+ const b = box;
2925
+ box = null;
2926
+ document.removeEventListener('keydown', key, true);
2927
+ document.documentElement.classList.remove('mozert-lb-open');
2928
+ b.classList.remove('mozert-lb-show');
2929
+ setTimeout(() => b.parentNode && b.parentNode.removeChild(b), 180);
2930
+ if (sl.active && idx !== sl.current && sl.canNav) sl.goTo(idx);
2931
+ if (opener && opener.focus) {
2932
+ try {
2933
+ opener.focus({ preventScroll: true });
2934
+ } catch (e) {}
2935
+ }
2936
+ if (sl.emit) sl.emit('lightboxClose', idx);
2937
+ };
2938
+ sl.el.classList.add('mozert-has-lightbox');
2939
+ sl._on(sl.list, 'click', (e) => {
2940
+ const slide = e.target.closest && e.target.closest('.mozert-slide');
2941
+ if (!slide || !sl.track.contains(slide)) return;
2942
+ if (e.target.closest('button, input, select, textarea, [data-no-lightbox]')) return;
2943
+ const a = e.target.closest('a[href]');
2944
+ if (a && !isImg(a.getAttribute('href'))) return;
2945
+ const i = parseInt(slide.getAttribute('data-mozert-index'), 10);
2946
+ if (isNaN(i)) return;
2947
+ if (a) e.preventDefault();
2948
+ open(i % Math.max(1, items().length));
2949
+ });
2950
+ sl.lightbox = { open, close, get isOpen() { return !!box; } };
2951
+ return {
2952
+ destroy() {
2953
+ if (box) {
2954
+ box.parentNode && box.parentNode.removeChild(box);
2955
+ box = null;
2956
+ document.removeEventListener('keydown', key, true);
2957
+ document.documentElement.classList.remove('mozert-lb-open');
2958
+ }
2959
+ sl.el.classList.remove('mozert-has-lightbox');
2960
+ delete sl.lightbox;
2961
+ },
2962
+ };
2963
+ },
2964
+ });
2965
+
2966
+ /* ---------------------------------------------------------- magnifier */
2967
+ // magnifier: true | { zoom: 2.2, button: true, active: false }. Hover zoom like a product page: with the magnifier on,
2968
+ // the picture under the mouse is enlarged around the pointer. A small button in the top right corner turns it on and off.
2969
+ // A slide's image can name a sharper file with data-zoom (or data-full), which is loaded the first time it is magnified.
2970
+ Mozert.use({
2971
+ name: 'magnifier',
2972
+ defaults: { magnifier: false },
2973
+ create(sl) {
2974
+ if (!sl.s.magnifier) return null;
2975
+ const o = objOpt(sl.s.magnifier, { zoom: 2.2, button: true, active: false });
2976
+ let on = sl._magOn !== undefined ? sl._magOn : !!o.active;
2977
+ let cur = null;
2978
+ let btn = null;
2979
+ let pressed = false;
2980
+ sl.el.classList.add('mozert-has-magnifier');
2981
+ const targetOf = (slide) => slide.querySelector('img,picture,canvas,video') || slide.firstElementChild;
2982
+ const reset = () => {
2983
+ if (!cur) return;
2984
+ cur.node.style.transform = '';
2985
+ cur.node.style.transformOrigin = '';
2986
+ cur.node.classList.remove('mozert-mag-node');
2987
+ cur.slide.classList.remove('mozert-mag-host');
2988
+ cur = null;
2989
+ };
2990
+ const hires = (node) => {
2991
+ const img = node.tagName === 'IMG' ? node : node.querySelector && node.querySelector('img');
2992
+ const url = img && (img.getAttribute('data-zoom') || img.getAttribute('data-full'));
2993
+ if (!img || !url || img.__mozMag) return;
2994
+ img.__mozMag = 1;
2995
+ const pre = new Image();
2996
+ pre.onload = () => {
2997
+ img.src = url;
2998
+ };
2999
+ pre.src = url;
3000
+ };
3001
+ const move = (e) => {
3002
+ if (!on || pressed || e.pointerType !== 'mouse') return;
3003
+ const slide = e.target.closest && e.target.closest('.mozert-slide');
3004
+ if (!slide || !sl.track.contains(slide) || (e.target.closest && e.target.closest('.mozert-mag-btn'))) {
3005
+ reset();
3006
+ return;
3007
+ }
3008
+ if (!cur || cur.slide !== slide) {
3009
+ reset();
3010
+ const node = targetOf(slide);
3011
+ if (!node) return;
3012
+ cur = { slide, node };
3013
+ slide.classList.add('mozert-mag-host');
3014
+ node.classList.add('mozert-mag-node');
3015
+ hires(node);
3016
+ }
3017
+ const r = cur.slide.getBoundingClientRect();
3018
+ cur.node.style.transformOrigin = clampN(((e.clientX - r.left) / r.width) * 100, 0, 100) + '% ' + clampN(((e.clientY - r.top) / r.height) * 100, 0, 100) + '%';
3019
+ cur.node.style.transform = 'scale(' + o.zoom + ')';
3020
+ };
3021
+ const paint = () => {
3022
+ sl.el.classList.toggle('mozert-mag-on', on);
3023
+ if (btn) {
3024
+ btn.setAttribute('aria-pressed', on ? 'true' : 'false');
3025
+ btn.classList.toggle('mozert-on', on);
3026
+ }
3027
+ if (!on) reset();
3028
+ };
3029
+ const set = (v) => {
3030
+ on = !!v;
3031
+ sl._magOn = on;
3032
+ paint();
3033
+ sl.emit('magnifier', on);
3034
+ };
3035
+ sl._on(sl.list, 'pointermove', move);
3036
+ sl._on(sl.list, 'pointerleave', reset);
3037
+ // Dragging the slider must feel normal: drop the zoom while the button is held down.
3038
+ sl._on(sl.list, 'pointerdown', () => {
3039
+ pressed = true;
3040
+ reset();
3041
+ });
3042
+ sl._on(window, 'pointerup', () => {
3043
+ pressed = false;
3044
+ });
3045
+ sl._sub('beforeChange', reset);
3046
+ if (o.button) {
3047
+ btn = mk('button', 'mozert-mag-btn');
3048
+ btn.type = 'button';
3049
+ btn.setAttribute('aria-label', sl._label('magnifier'));
3050
+ btn.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5L21 21"/></svg>';
3051
+ btn.addEventListener('click', (e) => {
3052
+ e.stopPropagation();
3053
+ set(!on);
3054
+ });
3055
+ sl.el.appendChild(btn);
3056
+ }
3057
+ paint();
3058
+ sl.magnifier = { enable: () => set(true), disable: () => set(false), toggle: () => set(!on), isOn: () => on };
3059
+ return {
3060
+ destroy() {
3061
+ reset();
3062
+ if (btn && btn.parentNode) btn.parentNode.removeChild(btn);
3063
+ sl.el.classList.remove('mozert-has-magnifier', 'mozert-mag-on');
3064
+ delete sl.magnifier;
3065
+ },
3066
+ };
3067
+ },
3068
+ });
3069
+
3070
+ /* -------------------------------------------------------- scrollDriven */
3071
+ // scrollDriven: true | { speed: 1.6, step: 0, top: 'center' | 0, offset: 0 }.
3072
+ // The slider sticks while the user scrolls and the scroll moves the slides. `speed` is how many px the slides travel
3073
+ // per px of page scroll (higher = less page height used); `step` forces a fixed number of px per slide instead.
3074
+ // `top: 'center'` pins the slider in the middle of the screen, a number pins it that many px from the top;
3075
+ // `offset` adds room for a sticky header. Works with non-looping, horizontal sliders.
3076
+ Mozert.use({
3077
+ name: 'scrollDriven',
3078
+ defaults: { scrollDriven: false },
3079
+ create(sl) {
3080
+ const c = sl.s.scrollDriven;
3081
+ if (!c || sl.s.vertical || sl.s.infinite || sl.s.stacked || !sl.canNav) return null;
3082
+ const o = objOpt(c, { step: 0, top: 0, speed: 1.6, offset: 0, fill: false, bottom: 0 });
3083
+ const el = sl.el;
3084
+ const wrap = mk('div', 'mozert-sd');
3085
+ el.parentNode.insertBefore(wrap, el);
3086
+ wrap.appendChild(el);
3087
+ el.classList.add('mozert-sd-stick');
3088
+ let topPx = 0;
3089
+ const place = () => {
3090
+ if (o.fill) {
3091
+ // fill: the pinned slider takes the whole screen height (minus offset/bottom), so there is no empty space around it
3092
+ topPx = (o.top === 'center' ? 0 : parseFloat(o.top) || 0) + (parseFloat(o.offset) || 0);
3093
+ el.classList.add('mozert-sd-fill');
3094
+ el.style.height = Math.max(160, window.innerHeight - topPx - (parseFloat(o.bottom) || 0)) + 'px';
3095
+ el.style.top = topPx + 'px';
3096
+ return;
3097
+ }
3098
+ const h = el.offsetHeight;
3099
+ topPx = o.top === 'center' ? Math.max(0, Math.round((window.innerHeight - h) / 2)) : parseFloat(o.top) || 0;
3100
+ topPx += parseFloat(o.offset) || 0;
3101
+ el.style.top = topPx + 'px';
3102
+ };
3103
+ let raf = 0;
3104
+ let lastIdx = -1;
3105
+ const steps = () => Math.max(1, sl.maxIndex);
3106
+ const totalPx = () => {
3107
+ if (o.step) return steps() * o.step;
3108
+ const travel = Math.abs(sl._pos(sl.maxIndex) - sl._pos(0));
3109
+ return Math.max(240, Math.round(travel / Math.max(0.2, o.speed)));
3110
+ };
3111
+ const size = () => {
3112
+ place();
3113
+ wrap.style.height = el.offsetHeight + totalPx() + 'px';
3114
+ };
3115
+ const update = () => {
3116
+ raf = 0;
3117
+ if (!sl.active) return;
3118
+ const r = wrap.getBoundingClientRect();
3119
+ const total = totalPx();
3120
+ const p = clampN((topPx - r.top) / total, 0, 1);
3121
+ const f = p * steps();
3122
+ const i0 = Math.floor(f);
3123
+ const i1 = Math.min(sl.maxIndex, i0 + 1);
3124
+ const k = f - i0;
3125
+ const idx = Math.round(f);
3126
+ if (idx !== lastIdx) {
3127
+ lastIdx = idx;
3128
+ sl.goTo(idx, true);
3129
+ }
3130
+ sl._setPos(sl._pos(i0) + (sl._pos(i1) - sl._pos(i0)) * k, false);
3131
+ wrap.style.setProperty('--sd-progress', p.toFixed(4));
3132
+ };
3133
+ const queue = () => {
3134
+ if (!raf) raf = requestAnimationFrame(update);
3135
+ };
3136
+ size();
3137
+ sl._on(window, 'scroll', queue, { passive: true });
3138
+ sl._on(window, 'resize', () => {
3139
+ size();
3140
+ queue();
3141
+ });
3142
+ sl._sub('reInit', () => {
3143
+ size();
3144
+ queue();
3145
+ });
3146
+ update();
3147
+ sl.scrollDriven = { update };
3148
+ return {
3149
+ destroy() {
3150
+ if (raf) cancelAnimationFrame(raf);
3151
+ el.classList.remove('mozert-sd-stick', 'mozert-sd-fill');
3152
+ el.style.top = '';
3153
+ if (o.fill) el.style.height = '';
3154
+ if (wrap.parentNode) {
3155
+ wrap.parentNode.insertBefore(el, wrap);
3156
+ wrap.parentNode.removeChild(wrap);
3157
+ }
3158
+ delete sl.scrollDriven;
3159
+ },
3160
+ };
3161
+ },
3162
+ });
2609
3163
  })();
2610
3164
 
2611
3165