PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/com…/carousel/carousel.js?ver=1e85ce597141

js brotherssupply.com collected 2026-10-09 00:30:07 UTC 22,735 bytes, 436 lines download raw bytes

1/* ═══════════════════════════════════════════════════════════════════════════
2   CAROUSEL — widget behavior (Phase-2 widget system; moved out of main.js)
3
4   Self-contained, no dependencies. As a shortcode widget the carousel ALWAYS
5   loads: carousel.php un-gates it via the hzostudio_component_markers filter,
6   because [hzo_carousel] can render from surfaces the loader's post_content scan
7   can't see (header/footer parts run through do_shortcode, text widgets, template
8   fields). The carousel IIFE was removed from main.js so behavior lives here.
9
10   OPTION CONTRACT — every setting is a data-attribute on `.hzo-carousel`, so
11   the block pattern (author writes attributes) and the [hzo_carousel] shortcode
12   (carousel.php generates them from settings) drive the SAME behavior:
13
14     data-hzo-carousel-per-view        slides visible at once (int, default 1)
15       …-per-view-md / -per-view-sm    per-view at ≤1024 / ≤640 (responsive)
16     data-hzo-carousel-gap             px between slides (int, default 0)
17       …-gap-md / -gap-sm              gap at ≤1024 / ≤640
18     data-hzo-carousel-per-move        slides advanced per step (int, default 1)
19     data-hzo-carousel-center          center the active slide (bool)
20     data-hzo-carousel-loop            wrap past the ends (bool, default true)
21     data-hzo-carousel-start           initial slide index (int, default 0)
22     data-hzo-carousel-speed           transition ms (int; default = CSS token)
23     data-hzo-carousel-effect          "slide" | "fade"  (fade also via --fade)
24     data-hzo-carousel-autoplay        ms between advances (int; 0/absent = off)
25     data-hzo-carousel-pause-hover     pause autoplay on hover/focus (default true)
26     data-hzo-carousel-stop-on-interaction  stop autoplay after a manual nav (bool)
27     data-hzo-carousel-pagination      "none" | "dots" | "fraction" | "progress"
28     data-hzo-carousel-dots            legacy alias for pagination=dots (bool)
29     data-hzo-carousel-ring-dots       dots with a countdown ring (needs autoplay)
30     data-hzo-carousel-arrows          show prev/next (default true if present)
31     data-hzo-carousel-mousewheel      advance on vertical wheel (bool)
32     data-hzo-carousel-wheel-release   release the wheel to the page at the ends
33                                       (bool, default true — critical wheel UX)
34     data-hzo-carousel-drag            pointer/touch swipe (bool, default true)
35     data-hzo-carousel-lazy            add loading="lazy" to slide images (bool)
36     data-hzo-carousel-label           accessible name (string)
37
38   A11y (WAI-ARIA APG Carousel): container is role=group + aria-roledescription
39   "carousel"; each slide role=group + aria-roledescription "slide" + "n of N";
40   the viewport is aria-live=polite while NOT auto-rotating and off while it is;
41   the pause control is required whenever autoplay runs (WCAG 2.2.2) and is the
42   first stop; everything degrades to a plain swipeable scroller with no JS.
43   Reduced motion: no autoplay, no animated transforms.
44   ═══════════════════════════════════════════════════════════════════════════ */
45(function () {
46    'use strict';
47
48    var prefersReduced = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
49
50    function attr(root, name) { return root.getAttribute('data-hzo-carousel-' + name); }
51    function boolAttr(root, name, dflt) {
52        var v = attr(root, name);
53        if (v === null) { return dflt; }
54        return v !== 'false' && v !== '0' && v !== 'no';
55    }
56    function intAttr(root, name, dflt) {
57        var v = parseInt(attr(root, name), 10);
58        return isNaN(v) ? dflt : v;
59    }
60
61    function setup(root) {
62        if (root.hasAttribute('data-hzo-carousel-ready')) { return; } // idempotent
63        var track = root.querySelector('.hzo-carousel__track');
64        var slides = Array.prototype.slice.call(root.querySelectorAll('.hzo-carousel__slide'));
65        if (!track || !slides.length) { return; }
66        root.setAttribute('data-hzo-carousel-ready', '');
67
68        var viewport = root.querySelector('.hzo-carousel__viewport');
69        var prev = root.querySelector('.hzo-carousel__btn--prev');
70        var next = root.querySelector('.hzo-carousel__btn--next');
71        var toggle = root.querySelector('[data-hzo-carousel-toggle]');
72        var n = slides.length;
73
74        // ── Options ──────────────────────────────────────────────────────────
75        var fade = root.classList.contains('hzo-carousel--fade') || attr(root, 'effect') === 'fade';
76        var perViewCfg = {
77            base: fade ? 1 : Math.max(1, intAttr(root, 'per-view', 1)),
78            md:   fade ? 1 : intAttr(root, 'per-view-md', NaN),
79            sm:   fade ? 1 : intAttr(root, 'per-view-sm', NaN)
80        };
81        var gapCfg = {
82            base: fade ? 0 : Math.max(0, intAttr(root, 'gap', 0)),
83            md:   fade ? 0 : intAttr(root, 'gap-md', NaN),
84            sm:   fade ? 0 : intAttr(root, 'gap-sm', NaN)
85        };
86        var perMove = Math.max(1, intAttr(root, 'per-move', 1));
87        var center = !fade && boolAttr(root, 'center', false);
88        var loop = boolAttr(root, 'loop', true);
89        var speed = intAttr(root, 'speed', NaN);
90        var interval = intAttr(root, 'autoplay', 0);
91        var pauseHover = boolAttr(root, 'pause-hover', true);
92        var stopOnInteract = boolAttr(root, 'stop-on-interaction', false);
93        var wheelOn = boolAttr(root, 'mousewheel', false);
94        var wheelRelease = boolAttr(root, 'wheel-release', true);
95        var dragOn = boolAttr(root, 'drag', true);
96        var lazyOn = boolAttr(root, 'lazy', false);
97        var label = attr(root, 'label');
98        if (label && !root.getAttribute('aria-label')) { root.setAttribute('aria-label', label); }
99
100        // Pagination mode: explicit attr wins; legacy data-hzo-carousel-dots /
101        // -ring-dots map to "dots"; default none.
102        var pagAttr = (attr(root, 'pagination') || '').toLowerCase();
103        var ringReq = root.hasAttribute('data-hzo-carousel-ring-dots');
104        if (!pagAttr) {
105            if (root.hasAttribute('data-hzo-carousel-dots') || ringReq) { pagAttr = 'dots'; }
106            else { pagAttr = 'none'; }
107        }
108
109        // Current per-view / gap resolve responsively (recomputed on resize).
110        function perView() {
111            var w = window.innerWidth;
112            if (w <= 640 && !isNaN(perViewCfg.sm)) { return Math.max(1, perViewCfg.sm); }
113            if (w <= 1024 && !isNaN(perViewCfg.md)) { return Math.max(1, perViewCfg.md); }
114            return perViewCfg.base;
115        }
116        function gapPx() {
117            var w = window.innerWidth;
118            if (w <= 640 && !isNaN(gapCfg.sm)) { return Math.max(0, gapCfg.sm); }
119            if (w <= 1024 && !isNaN(gapCfg.md)) { return Math.max(0, gapCfg.md); }
120            return gapCfg.base;
121        }
122
123        // ── Geometry (translate-based; robust for any per-view/gap/per-move) ──
124        // The CSS reads --hzo-cv-per-view / --hzo-cv-gap to size the slides; the
125        // JS measures the real pixel step from the rendered slide so fractional
126        // widths and box-sizing never drift.
127        function applyGeometry() {
128            root.style.setProperty('--hzo-cv-per-view', perView());
129            root.style.setProperty('--hzo-cv-gap', gapPx() + 'px');
130        }
131        var index = 0; // active slide index (snap unit)
132
133        function maxIndex() {
134            if (fade || center) { return n - 1; }
135            var pv = perView();
136            return Math.max(0, n - pv); // last position that fills the viewport
137        }
138
139        function slideStep() {
140            // px advance for one slide = slide outer width + gap.
141            if (!slides[0]) { return 0; }
142            return slides[0].getBoundingClientRect().width + gapPx();
143        }
144
145        function render(instant) {
146            if (fade) {
147                for (var f = 0; f < n; f++) { slides[f].classList.toggle('is-active', f === index); }
148            } else {
149                var step = slideStep();
150                var offset = index * step;
151                if (center) {
152                    var vpW = viewport ? viewport.getBoundingClientRect().width : 0;
153                    var slW = slides[index] ? slides[index].getBoundingClientRect().width : 0;
154                    offset -= (vpW - slW) / 2;
155                }
156                var xform = 'translate3d(' + (-offset) + 'px,0,0)';
157                // Instant: disable the transition, apply the transform, force a
158                // reflow, then restore the transition (reverting to the CSS rule,
159                // which reads --hzo-cv-speed — so `speed` is never clobbered).
160                if (instant && !prefersReduced) {
161                    track.style.transition = 'none';
162                    track.style.transform = xform;
163                    void track.offsetWidth;
164                    track.style.transition = '';
165                } else {
166                    track.style.transform = xform;
167                }
168            }
169            // Inert everything OUTSIDE the visible window so off-screen slide
170            // content (links/buttons clipped by overflow:hidden) stays out of tab
171            // order — APG carousel focus order / WCAG 2.4.3. Works for any
172            // per-view; fade shows exactly one.
173            if (fade) {
174                for (var i = 0; i < n; i++) { slides[i].inert = (i !== index); }
175            } else {
176                var pv = perView();
177                var winStart = center ? index - Math.floor((pv - 1) / 2) : index;
178                var winEnd = winStart + pv - 1;
179                for (var j = 0; j < n; j++) { slides[j].inert = (j < winStart || j > winEnd); }
180            }
181            updateDots();
182            updateArrows();
183            updateStatus();
184        }
185
186        // ── Navigation ───────────────────────────────────────────────────────
187        function clamp(to) { return Math.max(0, Math.min(to, maxIndex())); }
188        function go(to, opts) {
189            opts = opts || {};
190            var mx = maxIndex();
191            var from = index;
192            if (loop) {
193                if (to > mx) { to = 0; }
194                else if (to < 0) { to = mx; }
195            } else {
196                to = clamp(to);
197            }
198            // A wrap jump (end→start or start→end) more than one step away snaps
199            // instantly to avoid a long rewind blur; adjacent steps animate.
200            var instant = !fade && Math.abs(to - from) > Math.max(perMove, 1) && !prefersReduced;
201            index = to;
202            render(instant);
203            if (opts.user && stopOnInteract) { setPlaying(false); }
204        }
205        function stepBy(dir) { go(index + dir * perMove, { user: true }); }
206
207        if (prev) { prev.addEventListener('click', function () { stepBy(-1); }); }
208        if (next) { next.addEventListener('click', function () { stepBy(1); }); }
209
210        root.addEventListener('keydown', function (e) {
211            var t = e.target;
212            if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT')) { return; }
213            if (e.key === 'ArrowLeft') { e.preventDefault(); stepBy(-1); }
214            else if (e.key === 'ArrowRight') { e.preventDefault(); stepBy(1); }
215        });
216
217        function updateArrows() {
218            if (loop) { return; } // wrap-enabled → arrows never disable
219            if (prev) { prev.disabled = index <= 0; }
220            if (next) { next.disabled = index >= maxIndex(); }
221        }
222
223        // ── Status: fraction pagination + a11y live region ───────────────────
224        var fractionEl = null;
225        if (pagAttr === 'fraction') {
226            fractionEl = root.querySelector('.hzo-carousel__fraction');
227            if (!fractionEl) {
228                fractionEl = document.createElement('div');
229                fractionEl.className = 'hzo-carousel__fraction';
230                root.appendChild(fractionEl);
231            }
232        }
233        var progressEl = null;
234        if (pagAttr === 'progress') {
235            progressEl = root.querySelector('.hzo-carousel__progress');
236            if (!progressEl) {
237                progressEl = document.createElement('div');
238                progressEl.className = 'hzo-carousel__progress';
239                progressEl.innerHTML = '<span class="hzo-carousel__progress-bar"></span>';
240                root.appendChild(progressEl);
241            }
242        }
243        function updateStatus() {
244            if (fractionEl) { fractionEl.textContent = (index + 1) + ' / ' + (maxIndex() + 1); }
245            if (progressEl) {
246                var bar = progressEl.firstChild;
247                var mx = maxIndex();
248                if (bar) { bar.style.transform = 'scaleX(' + (mx ? (index / mx) : 1) + ')'; }
249            }
250        }
251
252        // ── Dot indicators (+ optional countdown ring) ───────────────────────
253        var dotsWrap = root.querySelector('.hzo-carousel__dots');
254        var dots = [];
255        var ringOn = false;
256        if (pagAttr === 'dots') {
257            ringOn = !!(interval && !prefersReduced && ringReq);
258            var stops = maxIndex() + 1;
259            if (stops > 1) {
260                if (!dotsWrap) { // don't inject an empty wrapper when there's nothing to page
261                    dotsWrap = document.createElement('div');
262                    dotsWrap.className = 'hzo-carousel__dots';
263                    root.appendChild(dotsWrap);
264                }
265                dotsWrap.innerHTML = '';
266                if (ringOn) {
267                    dotsWrap.classList.add('hzo-carousel__dots--ring');
268                    dotsWrap.style.setProperty('--carousel-ring-duration', interval + 'ms');
269                }
270                for (var d = 0; d < stops; d++) {
271                    (function (di) {
272                        var b = document.createElement('button');
273                        b.type = 'button';
274                        b.className = 'hzo-carousel__dot';
275                        b.setAttribute('aria-label', 'Go to slide ' + (di + 1));
276                        if (ringOn) {
277                            b.innerHTML = '<svg class="hzo-carousel__dot-ring" viewBox="0 0 28 28" aria-hidden="true" focusable="false"><circle cx="14" cy="14" r="10"/></svg>';
278                        }
279                        b.addEventListener('click', function () { go(di, { user: true }); });
280                        dotsWrap.appendChild(b);
281                        dots.push(b);
282                    })(d);
283                }
284            }
285        }
286        function updateDots() {
287            for (var k = 0; k < dots.length; k++) {
288                dots[k].setAttribute('aria-current', k === index ? 'true' : 'false');
289            }
290        }
291        function restartRing() {
292            if (!ringOn || !dots[index]) { return; }
293            var svg = dots[index].querySelector('.hzo-carousel__dot-ring');
294            if (svg) { svg.parentNode.replaceChild(svg.cloneNode(true), svg); }
295        }
296
297        // ── Slides: roles + labels + lazy ────────────────────────────────────
298        for (var s = 0; s < n; s++) {
299            slides[s].setAttribute('role', 'group');
300            slides[s].setAttribute('aria-roledescription', 'slide');
301            if (!slides[s].getAttribute('aria-label')) { slides[s].setAttribute('aria-label', (s + 1) + ' of ' + n); }
302            if (lazyOn) {
303                var imgs = slides[s].querySelectorAll('img');
304                for (var im = 0; im < imgs.length; im++) {
305                    if (!imgs[im].getAttribute('loading')) { imgs[im].setAttribute('loading', 'lazy'); }
306                }
307            }
308        }
309
310        // ── Drag / swipe (threshold-commit; no live follow) ──────────────────
311        if (dragOn && viewport && window.PointerEvent) {
312            var sx = 0, sy = 0, sid = null, swiping = false;
313            viewport.addEventListener('pointerdown', function (e) {
314                if (e.pointerType === 'mouse' && e.button !== 0) { return; }
315                swiping = true; sid = e.pointerId; sx = e.clientX; sy = e.clientY;
316            });
317            viewport.addEventListener('pointerup', function (e) {
318                if (!swiping || e.pointerId !== sid) { return; }
319                swiping = false;
320                var dx = e.clientX - sx, dy = e.clientY - sy;
321                if (Math.abs(dx) > 45 && Math.abs(dx) > Math.abs(dy)) {
322                    stepBy(dx < 0 ? 1 : -1);
323                    suspend(); arm(); // resync autoplay countdown
324                }
325            });
326            viewport.addEventListener('pointercancel', function () { swiping = false; });
327        }
328
329        // ── Mousewheel (opt-in) with release-at-edges ────────────────────────
330        // Wheel navigation is BOUNDED (never wraps) so the ends are reachable and,
331        // with release on (default), the page keeps scrolling once you pass the
332        // last/first slide. Release is INDEPENDENT of `loop` — a looping carousel
333        // has no wheel "edge", so tying release to !loop would trap page scroll
334        // forever (the default is loop=true). preventDefault fires ONLY when we
335        // actually consume the gesture to move a slide.
336        if (wheelOn && viewport) {
337            var wheelLock = false;
338            viewport.addEventListener('wheel', function (e) {
339                var d = Math.abs(e.deltaY) >= Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
340                if (!d) { return; }
341                var dir = d > 0 ? 1 : -1;
342                var atEdge = (dir > 0 && index >= maxIndex()) || (dir < 0 && index <= 0);
343                if (atEdge) {
344                    if (wheelRelease) { return; } // let the page scroll past the carousel
345                    e.preventDefault();           // release off → swallow the wheel at the edge
346                    return;
347                }
348                e.preventDefault();
349                if (wheelLock) { return; }
350                wheelLock = true;
351                go(clamp(index + dir * perMove), { user: true }); // bounded (no wrap)
352                suspend(); arm();
353                setTimeout(function () { wheelLock = false; }, 260); // one step per gesture
354            }, { passive: false });
355        }
356
357        // ── Autoplay (a11y-safe; pause on hover/focus; reduced-motion off) ────
358        var timer = null, playing = false;
359        function suspend() {
360            if (timer) { clearInterval(timer); timer = null; }
361            if (ringOn && dotsWrap) { dotsWrap.classList.add('is-paused'); }
362        }
363        function arm() {
364            if (playing && !timer && !prefersReduced && interval) {
365                timer = setInterval(function () { go(index + perMove); }, interval);
366                if (ringOn && dotsWrap) { dotsWrap.classList.remove('is-paused'); restartRing(); }
367            }
368        }
369        function setPlaying(on) {
370            playing = on;
371            if (on) { arm(); } else { suspend(); }
372            // Live region: announce slide changes only while NOT auto-rotating.
373            if (viewport) { viewport.setAttribute('aria-live', on ? 'off' : 'polite'); }
374            if (toggle) {
375                toggle.setAttribute('aria-pressed', on ? 'false' : 'true');
376                toggle.setAttribute('aria-label', on ? 'Pause automatic slideshow' : 'Play automatic slideshow');
377                toggle.textContent = on ? 'Pause' : 'Play'; // Label-in-Name (WCAG 2.5.3)
378            }
379        }
380        if (interval && !prefersReduced) {
381            if (pauseHover) {
382                root.addEventListener('mouseenter', suspend);
383                root.addEventListener('mouseleave', arm);
384                root.addEventListener('focusin', suspend);
385                root.addEventListener('focusout', function () { if (!root.contains(document.activeElement)) { arm(); } });
386            }
387            if (toggle) {
388                toggle.hidden = false;
389                toggle.addEventListener('click', function () { setPlaying(!playing); });
390            }
391            setPlaying(true);
392        } else {
393            if (toggle) { toggle.hidden = true; }
394            if (viewport) { viewport.setAttribute('aria-live', 'polite'); }
395        }
396
397        // ── Re-resolve responsive per-view/gap + re-clamp on any width change ──
398        var rt = null;
399        function reflow() {
400            if (rt) { clearTimeout(rt); }
401            rt = setTimeout(function () {
402                applyGeometry();
403                index = clamp(index);
404                render(true);
405            }, 120);
406        }
407        window.addEventListener('resize', reflow);
408        // ResizeObserver catches CONTAINER-width changes with no window resize
409        // (sidebar/accordion collapse, parent flex/grid reflow) that would
410        // otherwise leave the px transform offset until the next nav.
411        if (window.ResizeObserver && viewport) {
412            new ResizeObserver(reflow).observe(viewport);
413        }
414
415        // ── Init ─────────────────────────────────────────────────────────────
416        applyGeometry();
417        if (speed && !isNaN(speed) && !fade) { root.style.setProperty('--hzo-cv-speed', speed + 'ms'); }
418        index = clamp(intAttr(root, 'start', 0));
419        render(true);
420    }
421
422    function init(scope) {
423        var roots = (scope || document).querySelectorAll('.hzo-carousel');
424        for (var r = 0; r < roots.length; r++) { setup(roots[r]); }
425    }
426
427    if (document.readyState === 'loading') {
428        document.addEventListener('DOMContentLoaded', function () { init(); });
429    } else {
430        init();
431    }
432    // Expose on the shared engine namespace (readme.txt documents window.hzo as
433    // "the shared primitive") for dynamically-injected carousels (AJAX loops).
434    window.hzo = window.hzo || {};
435    window.hzo.carousel = { init: init, setup: setup };
436})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.