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.