PageSourceSearch

https://www.brotherssupply.com/wp-content/plugins/hozio-studio/components/marquee/marquee.js?ver=a131ff6c6479

js brotherssupply.com collected 2026-10-09 00:31:01 UTC 12,764 bytes, 226 lines download raw bytes

1/* ═════════════════════════════════════════════════════════════════════════
2   MARQUEE — widget behavior (Phase-2 widget system; moved out of main.js)
3
4   Auto-drifting horizontal strip (logos, badges, card rows). Self-contained,
5   no hard dependencies. As a shortcode widget the marquee ALWAYS loads:
6   marquee.php un-gates it via the hzostudio_component_markers filter, because
7   [hzo_marquee] can render from surfaces the loader's post_content scan can't
8   see (header/footer parts run through do_shortcode, text widgets, template
9   fields). The marquee IIFE was removed from main.js so behavior lives here.
10
11   HOW IT WORKS — the root is a REAL overflow-x scroller (touch users can
12   always swipe it; see marquee.css). setup() clones the item set once (clones
13   aria-hidden + unfocusable) and drifts root.scrollLeft in a rAF loop,
14   wrapping at the half-way seam. Pauses on hover, focus-within, touch,
15   mouse-drag, and while offscreen (IntersectionObserver — no idle rAF burn).
16
17   OPTION CONTRACT — every setting is a data-attribute on `.hzo-marquee`, so
18   the block pattern (author writes attributes) and the [hzo_marquee]
19   shortcode (marquee.php generates them from settings) drive the SAME
20   behavior. Every option defaults to the legacy behavior — markup carrying
21   only the bare [data-hzo-marquee] marker behaves exactly as before:
22
23     data-hzo-marquee            opt-in marker (required on the root)
24     data-hzo-marquee-speed      drift speed in px/second (number, default 40)
25     data-hzo-marquee-direction  "right" → content drifts rightward;
26                                 absent/"left" = leftward (legacy default)
27     data-hzo-marquee-paused     "true" → starts paused; the user presses the
28                                 [data-hzo-marquee-toggle] button to start the
29                                 drift (default false — drifts on load)
30
31   Related markup (a sibling within the same parent, NOT inside the root):
32     [data-hzo-marquee-toggle]   optional explicit pause/play button — the
33                                 WCAG 2.2.2 control for auto-moving content.
34                                 aria-pressed + aria-label reflect state.
35
36   A11y: clones are aria-hidden and pulled from the tab order; hover / focus-
37   within / touch pausing plus the explicit toggle satisfy WCAG 2.2.2.
38   Reduced motion → the module EXITS (no clones, no drift): the strip is a
39   plain swipeable scroller and any toggle is hidden (nothing to pause).
40
41   Public API: window.hzo.marquee = { init, setup } — init(scope) for AJAX-
42   injected instances. No hard dep on hozio-studio-main (never add one:
43   main.js is conditionally enqueued and WP would silently drop this script).
44   ═════════════════════════════════════════════════════════════════════════ */
45(function () {
46    'use strict';
47
48    var prefersReduced = !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches);
49
50    function boolAttr(root, name, dflt) {
51        var v = root.getAttribute('data-hzo-marquee-' + name);
52        if (v === null) { return dflt; }
53        return v !== 'false' && v !== '0' && v !== 'no';
54    }
55
56    /* The toggle lives OUTSIDE the root (inside the root you'd hover-pause the
57       strip just by reaching for the pause button), found within the shared
58       parent. Legacy contract: parentNode.querySelector — kept, but when
59       several strips stack in ONE parent (repeated [hzo_marquee] shortcodes)
60       each root now pairs with the first toggle FOLLOWING it in document
61       order, so buttons wire 1:1 instead of all binding to the first match.
62       A single toggle anywhere in the parent (the block-pattern layout, where
63       it sits in a trailing .hzo-container) resolves exactly as before. */
64    function findToggle(root) {
65        if (!root.parentNode || !root.parentNode.querySelectorAll) { return null; }
66        var list = root.parentNode.querySelectorAll('[data-hzo-marquee-toggle]');
67        if (!list.length) { return null; }
68        for (var i = 0; i < list.length; i++) {
69            if (root.compareDocumentPosition(list[i]) & Node.DOCUMENT_POSITION_FOLLOWING) { return list[i]; }
70        }
71        return list[0];
72    }
73
74    function setup(root) {
75        if (!root || root.getAttribute('data-hzo-marquee-ready') === 'true') { return; }
76        var track = root.querySelector('.hzo-marquee__track');
77        if (!track || !track.children.length) { return; }   // no payload yet — a later init() may retry
78        root.setAttribute('data-hzo-marquee-ready', 'true');
79
80        var toggle = findToggle(root);
81        if (prefersReduced) {
82            if (toggle) { toggle.hidden = true; }   // nothing to pause
83            return;
84        }
85
86        var speed   = parseFloat(root.getAttribute('data-hzo-marquee-speed')) || 40;
87        var reverse = root.getAttribute('data-hzo-marquee-direction') === 'right';
88        var originals = Array.prototype.slice.call(track.children);
89
90        // One cloned set puts the seamless wrap point at exactly half the
91        // scroll run. Clones are decorative: hidden from AT, unfocusable.
92        for (var i = 0; i < originals.length; i++) {
93            var c = originals[i].cloneNode(true);
94            c.setAttribute('aria-hidden', 'true');
95            if (c.matches && c.matches('a, button, input, select, textarea, [tabindex]')) { c.setAttribute('tabindex', '-1'); }
96            var f = c.querySelectorAll('a, button, input, select, textarea, [tabindex]');
97            for (var j = 0; j < f.length; j++) { f[j].setAttribute('tabindex', '-1'); }
98            track.appendChild(c);
99        }
100
101        // Pause state is per-REASON, not one boolean: a single flag let a
102        // touch/drag settle-timer unpause the strip while the cursor was
103        // still hovering (drift under the user's pointer), and a second
104        // touch within the settle window got unpaused by the first timer.
105        var hoverPaused = false, focusPaused = false, resting = false;
106        var userPaused = boolAttr(root, 'paused', false);
107        var restTimer = null, inView = false, raf = null, last = null, pos = null;
108        function isPaused() { return hoverPaused || focusPaused || resting || userPaused; }
109        function restFor(ms) {   // touch/drag settle window — refreshed, never overlapping
110            resting = true;
111            if (restTimer) { clearTimeout(restTimer); }
112            restTimer = setTimeout(function () { resting = false; }, ms);
113        }
114
115        function tick(ts) {
116            raf = null;
117            if (last === null) { last = ts; }
118            var dt = Math.min((ts - last) / 1000, 0.1);   // clamp tab-switch jumps
119            last = ts;
120            if (!isPaused()) {
121                var half = track.scrollWidth / 2;
122                // Bail while unscrollable (images not loaded yet, or too few
123                // items) — scrollLeft can't advance on an unscrollable root.
124                if (half > 0 && track.scrollWidth > root.clientWidth) {
125                    // Float accumulator: at 40px/s the per-frame delta is
126                    // sub-pixel and integer scrollLeft snapping would stall
127                    // the drift entirely at DPR 1.
128                    if (pos === null) {
129                        pos = root.scrollLeft;
130                        // Reverse strips seed at the TOP of the cycle when the
131                        // root is effectively unscrolled: seeding at 0 w
131ould
132                        // cross the pos < 0 wrap on the very first frame and
133                        // snap scrollLeft by half the doubled track — a jump-
134                        // cut on load / first viewport entry / resume-from-
135                        // unscrolled. A real manual position still re-syncs.
136                        if (reverse && pos < 1) { pos = half; }
137                    }
138                    pos += (reverse ? -speed : speed) * dt;
139                    if (reverse) {
140                        // Rightward drift wraps ONLY at the leading seam. The
141                        // seed frame has dt=0 (start() nulls `last`), so a
142                        // shared `pos >= half` check would false-trigger on a
143                        // strip seeded AT half, wrap it back to 0 and re-cross
144                        // zero next frame — the exact jump the seed prevents.
145                        if (pos < 0) { pos += half; }
146                    } else if (pos >= half) { pos -= half; }   // legacy leftward wrap, unchanged
147                    root.scrollLeft = pos;
148                }
149            } else {
150                pos = null;   // re-sync with any manual scrolling on resume
151            }
152            if (inView) { raf = requestAnimationFrame(tick); }
153        }
154        function start() { if (!raf && inView) { last = null; raf = requestAnimationFrame(tick); } }
155        function stop()  { if (raf) { cancelAnimationFrame(raf); raf = null; } }
156
157        root.addEventListener('mouseenter', function () { hoverPaused = true; });
158        root.addEventListener('mouseleave', function () { hoverPaused = false; });
159        root.addEventListener('focusin',  function () { focusPaused = true; });
160        root.addEventListener('focusout', function () { if (!root.contains(document.activeElement)) { focusPaused = false; } });
161        root.addEventListener('touchstart', function () { restFor(2500); }, { passive: true });
162        root.addEventListener('touchmove',  function () { restFor(2500); }, { passive: true });
163        root.addEventListener('touchend',   function () { restFor(2500); }, { passive: true });
164
165        // Grab-to-scroll for mouse users (touch scrolls natively). A drag of
166        // >10px swallows the click so dragging over a logo link doesn't follow it.
167        var dragging = false, dragX = 0, dragScroll = 0, dragMoved = false;
168        root.addEventListener('pointerdown', function (e) {
169            if (e.pointerType !== 'mouse' || e.button !== 0) { return; }
170            dragging = true; dragMoved = false; dragX = e.clientX; dragScroll = root.scrollLeft;
171        });
172        window.addEventListener('pointermove', function (e) {
173            if (!dragging) { return; }
174            var dx = e.clientX - dragX;
175            if (Math.abs(dx) > 10) { dragMoved = true; }
176            root.scrollLeft = dragScroll - dx;
177        });
178        window.addEventListener('pointerup', function () {
179            if (!dragging) { return; }
180            dragging = false;
181            restFor(800);   // hover pause still holds independently after the settle
182        });
183        root.addEventListener('click', function (e) {
184            if (dragMoved) { e.preventDefault(); e.stopPropagation(); dragMoved = false; }
185        }, true);
186
187        if (toggle) {
188            toggle.setAttribute('aria-pressed', userPaused ? 'true' : 'false');
189            if (!toggle.getAttribute('aria-label')) { toggle.setAttribute('aria-label', userPaused ? 'Play scrolling content' : 'Pause scrolling content'); }
190            toggle.addEventListener('click', function () {
191                userPaused = !userPaused;
192                toggle.setAttribute('aria-pressed', userPaused ? 'true' : 'false');
193                toggle.setAttribute('aria-label', userPaused ? 'Play scrolling content' : 'Pause scrolling content');
194            });
195        }
196
197        // Only burn rAF while the strip is actually visible. Batched IO
198        // callbacks can deliver enter+leave together — the LAST entry is
199        // the current state (entries[0] would act on the stale one).
200        if ('IntersectionObserver' in window) {
201            new IntersectionObserver(function (entries) {
202                inView = entries[entries.length - 1].isIntersecting;
203                if (inView) { start(); } else { stop(); }
204            }).observe(root);
205        } else {
206            inView = true;
207            start();
208        }
209    }
210
211    function init(scope) {
212        var roots = (scope || document).querySelectorAll('[data-hzo-marquee]');
213        for (var r = 0; r < roots.length; r++) { setup(roots[r]); }
214    }
215
216    if (document.readyState === 'loading') {
217        document.addEventListener('DOMContentLoaded', function () { init(); });
218    } else {
219        init();
220    }
221
222    // Shared engine namespace — the widget-convention surface for dynamically-
223    // injected instances (AJAX loops). Never a new top-level global.
224    window.hzo = window.hzo || {};
225    window.hzo.marquee = { init: init, setup: setup };
226})();

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.