1/* wp-nav.js â behaviour for partials/nav.html. 2 Extracted from inline <script> blocks so the ~28 KB is fetched once per 3 visitor instead of re-downloaded with every HTML response. 4 5 Loaded with `defer` from partials/head.html, so every IIFE below runs after 6 the nav markup is parsed but before DOMContentLoaded â the same ordering the 7 inline blocks had relative to each other, which matters for 8 window.wpcRefreshStickyChrome (defined near the bottom, called defensively 9 from the blocks above it). 10 11 No gettext here: a static .js file can't call _() and the site ships 33 12 locales, so translated strings arrive via data-attrs on the markup. */ 13 14/* ââ mobile hamburger panels + deferred top-pix thumbnails âââââââââââââââââââââââââââââââââââ */ 15 (function(){ 16 var rail = document.querySelector('.wpc-hm-rail'); 17 if (!rail) return; 18 19 // Deferred top-pix: build a panel's thumbnail row from its JSON payload 20 // the first time the panel is shown. Keeps ~12 <a>/<img> per panel out of 21 // the initial HTML (and Googlebot's parsed DOM) until actually needed. 22 // Built via DOM APIs (not innerHTML) so payload strings can't inject HTML. 23 function buildToppix(panel){ 24 if (!panel) return; 25 var row = panel.querySelector('.wpc-hm-toppix-row[data-toppix]'); 26 if (!row) return; 27 var items; 28 try { items = JSON.parse(row.getAttribute('data-toppix')); } catch (e) { return; } 29 var label = row.getAttribute('data-toppix-label') || ''; 30 var scroll = document.createElement('div'); 31 scroll.className = 'wpc-hm-toppix-scroll'; 32 items.forEach(function(it){ 33 var a = document.createElement('a'); 34 a.className = 'wpc-hm-toppix'; a.href = it.url; a.title = it.alt || ''; 35 var img = document.createElement('img'); 36 img.src = it.thumb_url; 37 img.alt = it.alt || it.title || (label + ' thumbnail'); 38 img.loading = 'lazy'; img.width = 72; img.height = 72; 39 a.appendChild(img); scroll.appendChild(a); 40 }); 41 row.appendChild(scroll); 42 row.removeAttribute('data-toppix'); 43 } 44 45 // Hydrate decorative panel art only after the user opens the hamburger 46 // and selects that panel. Text links/names remain server-rendered and 47 // crawlable; the closed global nav no longer competes with page imagery. 48 function hydratePanelImages(panel){ 49 if (!panel) return; 50 panel.querySelectorAll('source[data-srcset]').forEach(function(source){ 51 source.srcset = source.getAttribute('data-srcset'); 52 source.removeAttribute('data-srcset'); 53 }); 54 panel.querySelectorAll('img[data-src]').forEach(function(img){ 55 img.src = img.getAttribute('data-src'); 56 img.removeAttribute('data-src'); 57 }); 58 } 59 60 function activatePanel(panel){ 61 buildToppix(panel); 62 hydratePanelImages(panel); 63 } 64 window.wpcActivateHmPanel = activatePanel; 65 66 rail.addEventListener('click', function(e){ 67 var btn = e.target.closest('.wpc-hm-tab'); 68 if (!btn) return; 69 var key = btn.dataset.tab; 70 rail.querySelectorAll('.wpc-hm-tab').forEach(function(t){ t.classList.remove('is-active'); }); 71 btn.classList.add('is-active'); 72 var hm = btn.closest('.wpc-hm'); 73 var panels = hm.querySelectorAll('.wpc-hm-panel'); 74 panels.forEach(function(p){ 75 p.classList.toggle('is-active', p.dataset.panel === key); 76 }); 77 activatePanel(hm.querySelector('.wpc-hm-panel.is-active')); 78 var main = hm.querySelector('.wpc-hm-main'); 79 if (main) main.scrollTop = 0; 80 }); 81 82 // Toggle `.is-scrolled-x` on the tool-row and kw-grid scrollers so the 83 // CSS can paint a LEFT fade once the user has scrolled right (signals 84 // they can scroll back). Single delegated listener via capture phase 85 // so we catch scrolls on any descendant without per-element binding. 86 document.querySelectorAll('.wpc-hm-tools-scroll, .wpc-hm-kw-grid').forEach(function(el){ 87 if (el.classList.contains('wpc-hm-kw-grid--stack')) return; 88 el.addEventListener('scroll', function(){ 89 el.classList.toggle('is-scrolled-x', el.scrollLeft > 8); 90 }, {passive: true}); 91 }); 92 })(); 93 94/* ââ cart badge (desktop + mobile + rail) âââââââââââââââââââââââââââââââââââ */ 95 (function(){ 96 function paintBadge(n) { 97 var badge = document.getElementById('nav-cart-badge'); 98 if (badge) { 99 if (n > 0) { 100 badge.textContent = n > 99 ? '99+' : n; 101 badge.style.display = 'inline-block'; 102 } else { 103 badge.style.display = 'none'; 104 } 105 } 106 // Mobile cart (next to the hamburger) â only appears when the cart 107 // has items; the whole icon links to /cart for checkout. 108 var mCart = document.getElementById('m-nav-cart'); 109 var mBadge = document.getElementById('m-nav-cart-badge'); 110 if (mCart) { 111 mCart.style.display = n > 0 ? 'inline-flex' : 'none'; 112 if (mBadge && n > 0) mBadge.textContent = n > 99 ? '99+' : n; 113 } 114 // Cart entry inside the hamburger rail (first item). 115 var rCart = document.getElementById('m-rail-cart'); 116 var rBadge = document.getElementById('m-rail-cart-badge'); 117 if (rCart) { 118 rCart.style.display = n > 0 ? 'flex' : 'none'; 119 if (rBadge && n > 0) rBadge.textContent = n > 99 ? '99+' : n; 120 } 121 } 122 // Accept an OPTIONAL pre-fetched count so a caller (Add-to-Cart 123 // handler) can paint immediately without racing the cookie-Set vs 124 // /cart/count fetch (the cookie was sometimes not yet committed 125 // when the fetch fired, leaving the badge at 0). 126 function updateCartBadge(passedCount){ 127 if (typeof passedCount === 'number') { 128 paintBadge(passedCount); 129 return; 130 } 131 var sid = document.cookie.split(';').reduce(function(a,c){ 132 var p=c.trim().split('='); return p[0]==='wpc_cart' ? p[1] : a; }, ''); 133 if (!sid) { paintBadge(0); return; } 134 fetch('/cart/count', {credentials:'include'}) 135 .then(function(r){ return r.ok ? r.json() : null; }) 136 .then(function(j){ if (j) paintBadge(j.count || 0); }) 137 .catch(function(){}); 138 } 139 updateCartBadge(); 140 window.refreshCartBadge = updateCartBadge; 141 })(); 142 143/* ââ account dropdown: click-outside closes âââââââââââââââââââââââââââââââââââ */ 144 // Click-outside closes the touch-toggle dropdown. 145 (function(){ 146 var cb = document.getElementById('wpc-acc-toggle'); 147 if (!cb) return; 148 document.addEventListener('click', function(e){ 149 if (!cb.checked) return; 150 if (e.target.closest('.wpc-acc-menu')) return; 151 cb.checked = false;
152 }); 153 })(); 154 155/* ââ mobile search branded overlay toggle âââââââââââââââââââââââââââââââââââ */ 156 // Toggle the `.search-images` overlay based on focus/value (live-site pattern). 157 (function(){ 158 var inp = document.getElementById('m-headerSearch'); 159 if (!inp) return; 160 var ovr = inp.parentElement.querySelector('.search-images'); 161 if (!ovr) return; 162 function refresh(){ 163 var hide = (document.activeElement === inp) || inp.value.length > 0; 164 ovr.classList.toggle('active', !hide); 165 } 166 inp.addEventListener('focus', refresh); 167 inp.addEventListener('blur', refresh); 168 inp.addEventListener('input', refresh); 169 refresh(); 170 })(); 171 172/* ââ scroll chrome: drop the top ad after a short scroll âââââââââââââââââââââââââââââââââââ */ 173 // Scroll chrome: the ad starts above the logo/search row, then drops 174 // away after a short scroll. The header/nav rows remain sticky. 175 (function(){ 176 function refresh(){ 177 if (document.body.classList.contains('menu-open')) { 178 document.body.classList.add('wpc-top-ad-hidden'); 179 if (window.wpcRefreshStickyChrome) window.wpcRefreshStickyChrome(); 180 return; 181 } 182 var isDesktop = window.matchMedia && window.matchMedia('(min-width: 768px)').matches; 183 var threshold = isDesktop ? 96 : 44; 184 var y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 185 document.body.classList.toggle('wpc-top-ad-hidden', y > threshold); 186 if (window.wpcRefreshStickyChrome) window.wpcRefreshStickyChrome(); 187 } 188 window.addEventListener('scroll', refresh, {passive:true}); 189 window.addEventListener('resize', refresh); 190 refresh(); 191 })(); 192 193/* ââ hamburger open/close + scroll lock âââââââââââââââââââââââââââââââââââ */ 194 // Hamburger toggle â adds/removes `.show` on .navbar (CSS-driven open). 195 // Also click-outside-to-close + ESC-to-close + scroll-lock when open. 196 (function(){ 197 var btn = document.querySelector('.navbar-switch'); 198 var nav = document.getElementById('navbarMainContent'); 199 if (!btn || !nav) return; 200 function refreshMenuOffset(){ 201 var header = document.querySelector('header.new-sticky'); 202 var bottom = 50; 203 if (header) { 204 var rect = header.getBoundingClientRect(); 205 bottom = Math.max(0, Math.round(rect.bottom || rect.height || 50)); 206 } 207 document.documentElement.style.setProperty('--wpc-mobile-menu-top', bottom + 'px'); 208 } 209 function setOpen(open){ 210 nav.classList.toggle('show', open); 211 btn.setAttribute('aria-expanded', open ? 'true' : 'false'); 212 document.body.classList.toggle('menu-open', open); 213 if (open && window.wpcActivateHmPanel) { 214 window.wpcActivateHmPanel(nav.querySelector('.wpc-hm-panel.is-active')); 215 } 216 var y = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0; 217 var isDesktop = window.matchMedia && window.matchMedia('(min-width: 768px)').matches; 218 var threshold = isDesktop ? 96 : 44; 219 document.body.classList.toggle('wpc-top-ad-hidden', open || y > threshold); 220 // Hard scroll-lock: `overflow:hidden` alone leaks on iOS Safari 221 // (touch-scroll still propagates to the body). Pin the body at 222 // its current scroll offset with position:fixed, then restore 223 // the saved offset on close so the user lands back where they 224 // were instead of jumping to the top. 225 if (open) { 226 document.body.dataset.scrollLockY = y; 227 document.body.style.position = 'fixed'; 228 document.body.style.top = '-' + y + 'px'; 229 document.body.style.left = '0'; 230 document.body.style.right = '0'; 231 document.body.style.width = '100%'; 232 document.body.style.overflow = 'hidden'; 233 } else { 234 var savedY = parseInt(document.body.dataset.scrollLockY || '0', 10); 235 document.body.style.position = ''; 236 document.body.style.top = ''; 237 document.body.style.left = ''; 238 document.body.style.right = ''; 239 document.body.style.width = ''; 240 document.body.style.overflow = ''; 241 if (savedY) window.scrollTo(0, savedY); 242 delete document.body.dataset.scrollLockY; 243 } 244 if (window.wpcRefreshStickyChrome) window.wpcRefreshStickyChrome(); 245 if (open) { 246 refreshMenuOffset(); 247 window.requestAnimationFrame(refreshMenuOffset); 248 } 249 } 250 btn.addEventListener('click', function(e){ 251 e.stopPropagation(); 252 setOpen(!nav.classList.contains('show')); 253 }); 254 document.addEventListener('click', function(e){ 255 if (!nav.classList.contains('show')) return; 256 if (nav.contains(e.target) || btn.contains(e.target)) return; 257 setOpen(false); 258 }); 259 document.addEventListener('keydown', function(e){ 260 if (e.key === 'Escape' && nav.classList.contains('show')) setOpen(false); 261 }); 262 window.addEventListener('resize', function(){ 263 if (nav.classList.contains('show')) refreshMenuOffset(); 264 }); 265 window.addEventListener('orientationchange', function(){ 266 if (nav.classList.contains('show')) window.setTimeout(refreshMenuOffset, 80); 267 }); 268 })(); 269 270/* ââ search scope dropdown âââââââââââââââââââââââââââââââââââ */ 271 (function(){ 272 document.querySelectorAll('.scope-item').forEach(function(el){ 273 el.addEventListener('click', function(e){ 274 e.preventDefault(); 275 var v = this.getAttribute('data-value'); 276 var label = this.querySelector('span:nth-child(2)').textContent; 277 var icon = this.querySelector('span:nth-child(1)').innerHTML; 278 document.getElementById('scope-input').value = v; 279 document.getElementById('scope-label').textContent = label; 280 document.getElementById('scope-icon').innerHTML = icon; 281 // Keep the input placeholder in sync with the chosen scope. 282 // These strings are gettext-translated, so the server hands them 283 // over as data-attrs â a static .js file can never call _(). 284 var hs = document.getElementById('headerSearch'); 285 var placeholder = (v === 'all') 286 ? (hs.getAttribute('data-ph-all') || '') 287 : ((hs.getAttribute('data-ph-prefix') || '') + label.toLowerCase()); 288 hs.setAttribute('placeholder', placeholder); 289 document.querySelectorAll('.scope-item .ml-auto').forEach(function(c){ c.remove(); }); 290 var check = document.createElement('span'); 291 check.className = 'ml-auto'; 292 check.style.color = '#10b981'; 293 check.textContent = 'â'; 294 this.appendChild(check); 295 }); 296 }); 297 })(); 298 299/* ââ middle-nav + top-strip overflow collapse âââââââââââââââââââââââââââââââââââ */ 300 (function () { 301 var nav = document.getElementById('nav-middle'); 302 var row = document.getElementById('nav-top-row'); 303 var wrap = document.getElementById('nav-more-wrap'); 304 var btn = document.getElementById('nav-more-btn'); 305 var menu = document.getElementById('nav-more-menu'); 306 var cart = document.getElementById('nav-cart-btn'); 307 if (!nav || !row || !wrap || !btn || !menu) return; 308 309 // Snapshot the original ordered list of collapsible items 310 // (everything in nav EXCEPT the overflow wrap + cart). 311 var items = Array.prototype.filter.call(nav.children, function (el) { 312 return el !== wrap && el !== cart; 313 }); 314 // Each item gets a placeholder so we can restore it to its original slot.
315 items.forEach(function (el, idx) { el.dataset.navIdx = String(idx); }); 316 317 function inMenu(el) { return el.parentNode === menu; } 318 function inNav(el) { return el.parentNode === nav; } 319 320 function styleMenuItem(el) { 321 el.classList.add('nav-more-item'); 322 // Override display so the inline-flex/none from d-lg-inline-flex 323 // doesn't fight us inside the dropdown. 324 el.style.display = 'flex'; 325 el.style.alignItems = 'center'; 326 el.style.gap = '.5rem'; 327 el.style.padding = '.55rem .75rem'; 328 el.style.borderRadius = '6px'; 329 el.style.color = '#1f2937'; 330 el.style.fontSize = '.95rem'; 331 el.style.fontWeight = '600'; 332 } 333 function unstyleMenuItem(el) { 334 el.classList.remove('nav-more-item'); 335 // Reset the props we set above; everything else (color/weight from 336 // the inline `style=...` on the source markup) is intact. 337 el.style.display = ''; 338 el.style.alignItems = ''; 339 el.style.gap = ''; 340 el.style.padding = ''; 341 el.style.borderRadius = ''; 342 el.style.color = ''; 343 el.style.fontSize = ''; 344 el.style.fontWeight = ''; 345 } 346 347 function moveToMenu(el) { 348 if (inMenu(el)) return; 349 menu.appendChild(el); 350 styleMenuItem(el); 351 } 352 function moveToNav(el) { 353 if (inNav(el)) return; 354 unstyleMenuItem(el); 355 // Reinsert before the overflow wrap so the cart icon stays last 356 // and the original order (by data-nav-idx) is preserved. 357 var idx = parseInt(el.dataset.navIdx, 10); 358 var anchor = wrap; 359 // Find the next item with a higher idx that's currently in nav. 360 for (var i = 0; i < items.length; i++) { 361 var other = items[i]; 362 if (other === el) continue; 363 if (inNav(other) && parseInt(other.dataset.navIdx, 10) > idx) { 364 anchor = other; break; 365 } 366 } 367 nav.insertBefore(el, anchor); 368 } 369 370 // Hide menu on outside click / Escape. 371 function closeMenu() { 372 menu.hidden = true; btn.setAttribute('aria-expanded', 'false'); 373 } 374 btn.addEventListener('click', function (e) { 375 e.stopPropagation(); 376 var open = !menu.hidden; 377 if (open) { closeMenu(); return; } 378 menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); 379 }); 380 document.addEventListener('click', function (e) { 381 if (!wrap.contains(e.target)) closeMenu(); 382 }); 383 document.addEventListener('keydown', function (e) { 384 if (e.key === 'Escape') closeMenu(); 385 }); 386 387 function fits() { 388 // True only when both the middle nav and the full top row fit. 389 // Measuring the row catches cases where right-side CTAs squeeze the 390 // available width before the nav reports its own overflow. 391 var navFits = nav.scrollWidth <= nav.clientWidth + 1; 392 var rowFits = row.scrollWidth <= row.clientWidth + 1; 393 if (!rowFits) { 394 var rowRect = row.getBoundingClientRect(); 395 var lastRect = row.lastElementChild ? row.lastElementChild.getBoundingClientRect() : rowRect; 396 rowFits = lastRect.right <= rowRect.right + 1; 397 } 398 return navFits && rowFits; 399 } 400 401 function layout() { 402 // Step 1: bring everything back into the nav. Then progressively 403 // move items into the menu (from the END of the source order, 404 // EXCLUDING the cart which stays in the nav) until it fits. 405 items.forEach(moveToNav); 406 wrap.style.display = 'none'; 407 menu.hidden = true; 408 btn.setAttribute('aria-expanded', 'false'); 409 410 if (fits()) return; 411 412 wrap.style.display = 'inline-flex'; // show "â¦" 413 // Iterate items right-to-left, hiding them until it fits. 414 for (var i = items.length - 1; i >= 0; i--) { 415 moveToMenu(items[i]); 416 if (fits()) break; 417 } 418 wrap.style.display = menu.children.length ? 'inline-flex' : 'none'; 419 } 420 421 // Initial pass + on resize. RAF-debounce so we don't thrash. 422 var raf = null; 423 function schedule() { 424 if (raf) return; 425 raf = requestAnimationFrame(function () { raf = null; layout(); }); 426 } 427 schedule(); 428 window.addEventListener('resize', schedule); 429 window.addEventListener('orientationchange', schedule); 430 window.addEventListener('load', schedule); 431 // Webfont swap changes every label's width, so the first pass measured 432 // with the fallback face is wrong. Re-run the moment the real font is 433 // ready â the row is nowrap so this only moves items horizontally. 434 if (document.fonts && document.fonts.ready) {
435 document.fonts.ready.then(schedule).catch(function () {}); 436 } 437 if ('ResizeObserver' in window) { 438 new ResizeObserver(schedule).observe(row); 439 } 440 })(); 441 442 /* Top type-strip overflow â collapse trailing BLACK flat tiles into the 443 "â¯" dropdown when the strip is wider than the viewport (instead of 444 scrolling them off the right edge). Same priority+ pattern as the 445 middle-nav script above. */ 446 (function () { 447 var strip = document.querySelector('nav.navgation ul.navbar-nav.d-lg-flex'); 448 var moreLi = document.getElementById('wpc-strip-more'); 449 var btn = document.getElementById('wpc-strip-more-btn'); 450 var menu = document.getElementById('wpc-strip-more-menu'); 451 if (!strip || !moreLi || !btn || !menu) return; 452 453 // Collapsible = flat (black) tiles AND colored type tiles, in DOM 454 // order. We collapse from the END, so the black tiles (which sit last) 455 // always go first; colored type tiles only collapse as a last resort 456 // at the narrowest widths where the black ones aren't enough. 457 var items = Array.prototype.filter.call(strip.children, function (el) { 458 return el.classList.contains('wpc-h-flat') || el.classList.contains('wpc-h-type'); 459 }); 460 if (!items.length) return; 461 462 function fits() { return strip.scrollWidth <= strip.clientWidth + 1; } 463 function restore() { 464 items.forEach(function (el) { 465 if (el.parentNode !== strip) strip.insertBefore(el, moreLi); 466 }); 467 } 468 function closeMenu() { menu.hidden = true; btn.setAttribute('aria-expanded', 'false'); } 469 470 function layout() { 471 restore(); 472 moreLi.classList.remove('is-shown'); 473 closeMenu(); 474 if (fits()) return; 475 moreLi.classList.add('is-shown'); 476 for (var i = items.length - 1; i >= 0; i--) { 477 menu.insertBefore(items[i], menu.firstChild); // front-insert keeps order 478 if (fits()) break; 479 } 480 if (!menu.children.length) moreLi.classList.remove('is-shown'); 481 } 482 483 function positionMenu() { 484 // Anchor the fixed dropdown under the "â¯", right-aligned to it, and 485 // kept within the viewport's right edge. 486 var r = btn.getBoundingClientRect(); 487 menu.style.top = r.bottom + 'px'; 488 menu.style.right = Math.max(4, window.innerWidth - r.right) + 'px'; 489 menu.style.left = 'auto'; 490 } 491 btn.addEventListener('click', function (e) { 492 e.stopPropagation(); 493 if (!menu.hidden) { closeMenu(); return; } 494 positionMenu(); 495 menu.hidden = false; btn.setAttribute('aria-expanded', 'true'); 496 }); 497 document.addEventListener('click', function (e) { 498 if (!moreLi.contains(e.target) && !menu.contains(e.target)) closeMenu(); 499 }); 500 document.addEventListener('keydown', function (e) { 501 if (e.key === 'Escape') closeMenu(); 502 }); 503 504 var sraf = null; 505 function sschedule() { 506 if (sraf) return; 507 sraf = requestAnimationFrame(function () { sraf = null; layout(); }); 508 } 509 sschedule(); 510 window.addEventListener('resize', sschedule); 511 window.addEventListener('orientationchange', sschedule); 512 window.addEventListener('load', sschedule); 513 if ('ResizeObserver' in window && strip.parentElement) { 514 new ResizeObserver(sschedule).observe(strip.parentElement); 515 } 516 })(); 517 518/* ââ sticky-chrome measurement + type-bar toggle âââââââââââââââââââââââââââââââââââ */ 519 /* Keep every sticky layer in one measured stack: 520 ad rail -> logo/search header -> desktop typebar -> related pills. */ 521 (function(){ 522 function px(n) { return Math.max(0, Math.round(n || 0)) + 'px'; } 523 window.wpcRefreshStickyChrome = function(){ 524 var header = document.querySelector('header.new-sticky'); 525 var typebar = document.querySelector('.wpc-typebar'); 526 var headerRect = header ? header.getBoundingClientRect() : {top:0, height:0}; 527 var headerTop = Math.max(0, headerRect.top || 0); 528 var headerH = headerRect.height || 0; 529 var typebarH = 0; 530 if (typebar && window.getComputedStyle(typebar).display !== 'none') { 531 typebarH = typebar.getBoundingClientRect().height || 0; 532 } 533 document.documentElement.style.setProperty('--sticky-typebar-top', px(headerTop + headerH)); 534 document.documentElement.style.setProperty('--sticky-header-h', px(headerTop + headerH + typebarH)); 535 }; 536 ['DOMContentLoaded','load','resize','orientationchange'].forEach(function(evt){ 537 window.addEventListener(evt, window.wpcRefreshStickyChrome, {passive:true}); 538 }); 539 window.addEventListener('scroll', window.wpcRefreshStickyChrome, {passive:true}); 540 window.wpcRefreshStickyChrome(); 541 })(); 542 543 /* Click-to-toggle for keyboard / mobile users (hover handles desktop). */ 544 (function(){ 545 var pills = document.querySelectorAll('.wpc-typebar-pill');
546 pills.forEach(function(pill){ 547 var caret = pill.querySelector('.wpc-typebar-caret'); 548 if (!caret) return; 549 caret.addEventListener('click', function(e){ 550 e.preventDefault(); e.stopPropagation(); 551 var open = pill.classList.toggle('is-open'); 552 caret.setAttribute('aria-expanded', open ? 'true' : 'false'); 553 // Close other open pills 554 pills.forEach(function(p){ 555 if (p !== pill) { 556 p.classList.remove('is-open'); 557 var c = p.querySelector('.wpc-typebar-caret'); 558 if (c) c.setAttribute('aria-expanded', 'false'); 559 } 560 }); 561 }); 562 }); 563 // Click outside closes all 564 document.addEventListener('click', function(e){ 565 if (!e.target.closest('.wpc-typebar-pill')) { 566 pills.forEach(function(p){ 567 p.classList.remove('is-open'); 568 var c = p.querySelector('.wpc-typebar-caret'); 569 if (c) c.setAttribute('aria-expanded', 'false'); 570 }); 571 } 572 }); 573 })(); 574 575/* ââ search autocomplete âââââââââââââââââââââââââââââââââââ */ 576(function(){ 577 var inputs = ['headerSearch', 'm-headerSearch'] 578 .map(function(id){ return document.getElementById(id); }) 579 .filter(Boolean); 580 if (!inputs.length) return; 581 582 function attach(input){ 583 // Anchor parent: the <form> is positioned/inline already; we make the 584 // popover absolute relative to the form's bounding box. 585 var form = input.closest('form'); if (!form) return; 586 if (getComputedStyle(form).position === 'static') form.style.position = 'relative'; 587 588 var pop = document.createElement('div'); 589 pop.className = 'wpc-ac-pop'; 590 pop.setAttribute('role', 'listbox'); 591 // Inline styles win over any stray global rule that was making the 592 // dropdown look transparent. Anchored just below the search box. 593 pop.style.cssText = 594 'position:absolute;left:0;right:0;top:calc(100% + 4px);' + 595 'background:#fff;background-color:#fff;opacity:1;' + 596 'border:1px solid #e5e7eb;border-radius:8px;' + 597 'box-shadow:0 14px 36px rgba(0,0,0,.18);' + 598 'max-height:360px;overflow-y:auto;z-index:12000;isolation:isolate;'; 599 form.appendChild(pop); 600 601 var timer = null; 602 var items = []; 603 var active = -1; 604 605 function render(rows, q){ 606 pop.innerHTML = ''; 607 items = rows; 608 active = -1; 609 if (!rows.length) { pop.classList.remove('show'); return; } 610 var qre = q ? new RegExp('('+q.replace(/[.*+?^${}()|[\\]\\\\]/g,'\\\\$&')+')', 'ig') : null; 611 rows.forEach(function(r, i){ 612 var a = document.createElement('a'); 613 a.className = 'wpc-ac-item'; 614 a.href = r.url; 615 var label = qre ? r.name.replace(qre, '<mark>$1</mark>') : r.name; 616 // Foreign-locale matches carry a flag (e.g. ð©ðª for a German keyword on 617 // an English page) so the source locale is obvious at a glance. 618 var flag = r.flag ? '<span class="ac-flag" title="' + (r.locale||'') + '">' + r.flag + '</span>' : ''; 619 a.innerHTML = '<span class="ac-icon">ð</span><span>' + label + '</span>' + flag; 620 a.addEventListener('mousedown', function(e){ 621 e.preventDefault(); 622 window.location.href = r.url; 623 }); 624 pop.appendChild(a); 625 }); 626 pop.classList.add('show'); 627 } 628 629 function setActive(idx){ 630 var nodes = pop.querySelectorAll('.wpc-ac-item'); 631 nodes.forEach(function(n, i){ n.classList.toggle('active', i === idx); }); 632 active = idx; 633 } 634 635 function fetchSuggest(q){ 636 fetch('/api/search/autocomplete?q=' + encodeURIComponent(q) 637 + '&locale=' + encodeURIComponent( 638 (document.body.getAttribute('data-locale') || 'en')), 639 {credentials: 'same-origin'}) 640 .then(function(r){ return r.ok ? r.json() : []; }) 641 .then(function(rows){ render(rows || [], q); }) 642 .catch(function(){ pop.classList.remove('show'); }); 643 } 644 645 input.addEventListener('input', function(){ 646 var q = input.value.trim(); 647 clearTimeout(timer); 648 if (q.length < 2) { pop.classList.remove('show'); return; } 649 timer = setTimeout(function(){ fetchSuggest(q); }, 180); 650 }); 651 input.addEventListener('keydown', function(e){ 652 if (!pop.classList.contains('show')) return; 653 if (e.key === 'ArrowDown') { e.preventDefault(); setActive((active + 1) % items.length); } 654 else if (e.key === 'ArrowUp') { e.preventDefault(); setActive((active - 1 + items.length) % items.length); } 655 else if (e.key === 'Enter' && active >= 0) { 656 e.preventDefault(); 657 window.location.href = items[active].url; 658 } 659 else if (e.key === 'Escape') { pop.classList.remove('show'); } 660 }); 661 input.addEventListener('blur', function(){ 662 // Delay so click handlers fire first 663 setTimeout(function(){ pop.classList.remove('show'); }, 180); 664 }); 665 }
666 inputs.forEach(attach); 667})();
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.