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