1(function () { 2 function debounce(fn, ms) { 3 var t; 4 return function () { 5 clearTimeout(t); 6 t = setTimeout(fn, ms); 7 }; 8 } 9 10 function hashString(str) { 11 // djb2 12 var h = 5381; 13 for (var i = 0; i < str.length; i++) { 14 h = ((h << 5) + h) + str.charCodeAt(i); 15 h = h & 0xffffffff; 16 } 17 return String(h >>> 0); 18 } 19 20 function canUseStorage(s) { 21 try { 22 if (!s) return false; 23 var k = '__p9m_test__'; 24 s.setItem(k, '1'); 25 s.removeItem(k); 26 return true; 27 } catch (e) { 28 return false; 29 } 30 } 31 32 var storeSession = canUseStorage(window.sessionStorage) ? window.sessionStorage : null; 33 var storeLocal = canUseStorage(window.localStorage) ? window.localStorage : null; 34 35 function cookieGet(name) { 36 try { 37 var m = document.cookie.match(new RegExp('(?:^|; )' + name.replace(/([.$?*|{}()[\]\\\/\+^])/g, '\\$1') + '=([^;]*)')); 38 return m ? decodeURIComponent(m[1]) : null; 39 } catch (e) { 40 return null; 41 } 42 } 43 44 function cookieSet(name, value) { 45 try { 46 document.cookie = name + '=' + encodeURIComponent(value) + '; path=/; SameSite=Lax'; 47 } catch (e) {} 48 } 49 50 function anchorKey(root, item, speed, gap, direction) { 51 var dk = root.getAttribute('data-key'); 52 if (dk && dk.trim()) return 'p9marquee_anchor_' + dk.trim(); 53 54 var sig = String(speed) + '|' + String(gap) + '|' + direction + '|' + (item.textContent || '').trim().slice(0, 200); 55 return 'p9marquee_anchor_' + hashString(sig); 56 } 57 58 function getObj(key) { 59 var v = null; 60 try { if (storeSession) v = storeSession.getItem(key); } catch (e) {} 61 if (!v) { try { if (storeLocal) v = storeLocal.getItem(key); } catch (e) {} } 62 if (!v) v = cookieGet(key); 63 if (!v) return null; 64 try { return JSON.parse(v); } catch (e) { return null; } 65 } 66 67 function setObj(key, obj) { 68 var v = ''; 69 try { v = JSON.stringify(obj); } catch (e) { return; } 70 try { if (storeSession) storeSession.setItem(key, v); } catch (e) {} 71 try { if (storeLocal) storeLocal.setItem(key, v); } catch (e) {} 72 cookieSet(key, v); 73 } 74 75 function prefersReducedMotion() { 76 try { 77 return !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches); 78 } catch (e) { 79 return false; 80 } 81 } 82 83 function initMarquee(root) { 84 if (!root) return; 85 86 var viewport = root.querySelector('.p9marquee__viewport'); 87 var track = root.querySelector('.p9marquee__track'); 88 var item = track ? track.querySelector('.p9marquee__item') : null; 89 90 if (!viewport || !track || !item) return; 91 if (prefersReducedMotion()) return; 92 93 // Cancel any previous RAF loop for this element 94 if (root.__p9m && root.__p9m.rafId) { 95 cancelAnimationFrame(root.__p9m.rafId); 96 } 97 root.__p9m = root.__p9m || {}; 98 root.__p9m.rafId = 0; 99 root.__p9m.paused = false; 100 101 // Remove previous clones (if any) 102 Array.prototype.slice.call(track.querySelectorAll('.p9marquee__item--clone')).forEach(function (n) { 103 n.parentNode && n.parentNode.removeChild(n); 104 }); 105 106 var gap = parseInt(root.getAttribute('data-gap'), 10); 107 if (isNaN(gap) || gap < 0) gap = 40; 108 109 var speed = parseInt(root.getAttribute('data-speed'), 10); 110 if (isNaN(speed) || speed < 10) speed = 80; 111 112 var direction = root.getAttribute('data-direction') === 'right' ? 'right' : 'left'; 113 var dirSign = direction === 'right' ? 1 : -1; 114 115 var pauseHover = root.getAttribute('data-pausehover') === '1'; 116 117 track.style.setProperty('--p9marquee-gap', gap + 'px'); 118 119 // Force layout 120 var itemRect = item.getBoundingClientRect(); 121 var viewportRect = viewport.getBoundingClientRect(); 122 123 var itemWidth = itemRect.width; 124 var viewportWidth = viewportRect.width; 125 126 if (!itemWidth || !viewportWidth) return; 127 128 var segmentWidth = itemWidth + gap; 129 130 // How many repeats needed so the viewport is always filled 131 var repeats = Math.max(2, Math.ceil((viewportWidth + segmentWidth) / segmentWidth) + 2); 132 133 for (var i = 0; i < repeats; i++) { 134 var clone = item.cloneNode(true); 135 clone.classList.add('p9marquee__item--clone'); 136 clone.setAttribute('aria-hidden', 'true'); 137 track.appendChild(clone); 138 } 139 140 // IMPORTANT: we no longer rely on CSS animation for the loop, 141 // because some mobile browsers appear to restart CSS animations on
141navigation. 142 // We drive the loop with requestAnimationFrame and a shared timestamp. 143 144 var key = anchorKey(root, item, speed, gap, direction); 145 var st = getObj(key); 146 var now = Date.now(); 147 148 var startOffset = (dirSign === -1) ? viewportWidth : -viewportWidth; 149 var introDuration = Math.max(0.2, Math.abs(startOffset) / speed); // seconds 150 var loopStartTs = (st && st.loopStartTs) ? parseInt(st.loopStartTs, 10) : NaN; 151 152 if (isNaN(loopStartTs) || loopStartTs <= 0) { 153 loopStartTs = now + Math.round(introDuration * 1000); 154 setObj(key, { 155 loopStartTs: loopStartTs, 156 createdAt: now, 157 introDuration: introDuration, 158 startOffset: startOffset 159 }); 160 st = getObj(key); 161 } 162 163 // Helper: start RAF loop, with phase continuity based on loopStartTs. 164 function startLoop() { 165 // Stop CSS animations just in case 166 track.classList.remove('is-intro'); 167 track.classList.remove('is-animated'); 168 track.style.animation = 'none'; 169 track.style.animationDelay = ''; 170 track.style.willChange = 'transform'; 171 172 function tick() { 173 if (!root.isConnected) return; 174 175 if (!root.__p9m) root.__p9m = {}; 176 if (root.__p9m.paused || document.visibilityState === 'hidden') { 177 root.__p9m.rafId = requestAnimationFrame(tick); 178 return; 179 } 180 181 var tNow = Date.now(); 182 var elapsed = (tNow - loopStartTs) / 1000; 183 if (elapsed < 0) elapsed = 0; 184 185 var dist = (elapsed * speed) % segmentWidth; 186 var tx = dirSign * dist; 187 188 track.style.transform = 'translate3d(' + tx + 'px,0,0)'; 189 root.__p9m.rafId = requestAnimationFrame(tick); 190 } 191 192 root.__p9m.rafId = requestAnimationFrame(tick); 193 } 194 195 // Intro: start empty line then reach position 0 at loopStartTs, then start loop. 196 function runIntroIfNeeded() { 197 var tNow = Date.now(); 198 if (tNow >= loopStartTs) { 199 startLoop(); 200 return; 201 } 202 203 // Reset state for intro 204 track.classList.remove('is-animated'); 205 track.classList.remove('is-intro');
206 track.style.animation = 'none'; 207 track.style.animationDelay = ''; 208 track.style.willChange = 'transform'; 209 210 function introTick() { 211 if (!root.isConnected) return; 212 213 if (!root.__p9m) root.__p9m = {}; 214 if (root.__p9m.paused || document.visibilityState === 'hidden') { 215 root.__p9m.rafId = requestAnimationFrame(introTick); 216 return; 217 } 218 219 var now2 = Date.now(); 220 var remainingMs = loopStartTs - now2; 221 222 if (remainingMs <= 0) { 223 track.style.transform = 'translate3d(0px,0,0)'; 224 startLoop(); 225 return; 226 } 227 228 var remaining = remainingMs / 1000; 229 var ratio = introDuration > 0 ? (remaining / introDuration) : 0; 230 ratio = Math.max(0, Math.min(1, ratio)); 231 var currentOffset = startOffset * ratio; 232 233 track.style.transform = 'translate3d(' + currentOffset + 'px,0,0)'; 234 root.__p9m.rafId = requestAnimationFrame(introTick); 235 } 236 237 root.__p9m.rafId = requestAnimationFrame(introTick); 238 } 239 240 // Pause on hover (desktop); on touch devices it simply does nothing. 241 if (pauseHover) { 242 root.addEventListener('mouseenter', function () { if (root.__p9m) root.__p9m.paused = true; }, { passive: true }); 243 root.addEventListener('mouseleave', function () { if (root.__p9m) root.__p9m.paused = false; }, { passive: true }); 244 } 245 246 runIntroIfNeeded(); 247 } 248 249 function initAll() { 250 var nodes = document.querySelectorAll('.p9marquee'); 251 Array.prototype.forEach.call(nodes, initMarquee); 252 } 253 254 // On mobile Chromium browsers (incl. Brave), the address bar show/hide can fire resize events 255 // during scroll because the viewport height changes. Only re-init when layout width changes. 256 function layoutWidth() { 257 return Math.round(document.documentElement.clientWidth || window.innerWidth || 0); 258 } 259 260 var lastW = layoutWidth(); 261 262 function reinitIfWidthChanged() { 263 var w = layoutWidth(); 264 if (!w) return; 265 if (w === lastW) return; 266 lastW = w; 267 initAll(); 268 } 269 270 window.addEventListener('load', initAll); 271 window.addEventListener('pageshow', initAll); 272 window.addEventListener('resize', debounce(reinitIfWidthChanged, 150)); 273 window.addEventListener('orientationchange', debounce(function () { lastW = layoutWidth(); initAll(); }, 150)); 274 275 if (window.visualViewport && window.visualViewport.addEventListener) { 276 window.visualViewport.addEventListener('resize', debounce(reinitIfWidthChanged, 150)); 277 } 278})();
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.