1/** 2 * Site-wide front-end behavior. 3 * 4 * - [site_nav class="site-nav__dropdown"] menus: progressive enhancement. Mouse 5 * hover is handled entirely in CSS; this adds the click/tap and keyboard path 6 * â a chevron toggle button per parent item, with aria-expanded, single-open, 7 * Escape + outside-click to close. If this script never loads, hover still works. 8 */ 9(function () { 10 'use strict'; 11 12 var DROPDOWN = '.site-nav__dropdown'; 13 var ITEM = '.site-nav__item'; 14 var SUBLIST = '.site-nav__sublist'; 15 var idSeq = 0; 16 17 function closeItem(item) { 18 if (!item || !item.classList.contains('is-open')) { 19 return; 20 } 21 item.classList.remove('is-open'); 22 var toggle = item.querySelector(':scope > .site-nav__toggle'); 23 if (toggle) { 24 toggle.setAttribute('aria-expanded', 'false'); 25 } 26 } 27 28 function setupNav(nav) { 29 // Top-level items that actually have a sub-menu. 30 var items = nav.querySelectorAll(':scope > .site-nav__list > ' + ITEM); 31 32 Array.prototype.forEach.call(items, function (item) { 33 var sublist = item.querySelector(':scope > ' + SUBLIST); 34 if (!sublist) { 35 return; 36 } 37 38 item.classList.add('site-nav__item--has-dropdown'); 39 40 if (!sublist.id) { 41 idSeq += 1; 42 sublist.id = 'site-nav__dropdown-' + idSeq; 43 } 44 45 // Label the toggle from the parent link/text. 46 var label = item.querySelector(':scope > .site-nav__link, :scope > .site-nav__text'); 47 var labelText = label ? label.textContent.trim() : 'submenu'; 48 49 var toggle = document.createElement('button'); 50 toggle.type = 'button'; 51 toggle.className = 'site-nav__toggle'; 52 toggle.setAttribute('aria-expanded', 'false'); 53 toggle.setAttribute('aria-controls', sublist.id); 54 toggle.setAttribute('aria-label', 'Toggle ' + labelText + ' submenu'); 55 56 // Insert right after the label (so the label itself still navigates). 57 if (label && label.nextSibling) { 58 item.insertBefore(toggle, label.nextSibling); 59 } else if (label) { 60 item.insertBefore(toggle, sublist); 61 } else { 62 item.insertBefore(toggle, item.firstChild); 63 } 64 65 toggle.addEventListener('click', function () { 66 var isOpen = item.classList.contains('is-open'); 67 68 // Single-open: close siblings in this nav first. 69 Array.prototype.forEach.call(items, function (other) { 70 if (other !== item) { 71 closeItem(other); 72 } 73 }); 74 75 if (isOpen) { 76 closeItem(item); 77 } else { 78 item.classList.add('is-open'); 79 toggle.setAttribute('aria-expanded', 'true'); 80 } 81 }); 82 }); 83 } 84 85 function init() { 86 // This file only enhances the .site-nav__dropdown menus. 87 var navs = document.querySelectorAll(DROPDOWN); 88 if (!navs.length) { 89 return; 90 } 91 92 Array.prototype.forEach.call(navs, setupNav); 93 94 // Click outside any open dropdown closes it. 95 document.addEventListener('click', function (e) { 96 Array.prototype.forEach.call( 97 document.querySelectorAll(DROPDOWN + ' ' + ITEM + '.is-open'), 98 function (item) { 99 if (!item.contains(e.target)) { 100 closeItem(item); 101 } 102 } 103 ); 104 }); 105 106 // Escape closes the open dropdown and returns focus to its toggle. 107 // With more than one nav able to be open at once, close the one that 108 // contains focus (falling back to the first open item). 109 document.addEventListener('keydown', function (e) { 110 if (e.key !== 'Escape' && e.key !== 'Esc') { 111 return; 112 } 113 var openItems = document.querySelectorAll(DROPDOWN + ' ' + ITEM + '.is-open'); 114 if (!openItems.length) { 115 return; 116 } 117 118 var active = document.activeElement; 119 var target = null; 120 Array.prototype.forEach.call(openItems, function (item) { 121 if (active && item.contains(active)) { 122 target = item; 123 } 124 }); 125 if (!target) { 126 target = openItems[0]; 127 } 128 129 var toggle = target.querySelector(':scope > .site-nav__toggle'); 130 closeItem(target); 131 if (toggle) { 132 toggle.focus(); 133 } 134 }); 135 } 136 137 if (document.readyState === 'loading') { 138 document.addEventListener('DOMContentLoaded', init); 139 } else { 140 init(); 141 } 142})(); 143 144 145/** 146 * Keep --sem-utility-bar-h in sync with the dark utility bar's height. 147 * 148 * The header uses `display: contents` and its two bars are sticky individually 149 * (see _site-header.scss); the beige nav sticks just below the utility bar, so 150 * its sticky `top` is that bar's height. Runs site-wide (the header is global), 151 * independent of the flyout. 152 */ 153(function () { 154 'use strict'; 155 156 var toggle = document.querySelector('header [data-site-nav-toggle]'); 157 if (!toggle) { 158 return; 159 } 160 var header = toggle.closest('header'); 161 var bar = toggle; 162 while (bar.parentElement && bar.parentElement !== header) { 163 bar = bar.parentElement; 164 } 165 166 // Publish the element so the flyout's mobile drawer can measure this bar at 167 // open time, when it works out how far to scroll the page for the menu's top 168 // to land flush under it. Cheaper than a third copy of the walk above (the 169 // ride's navBottom() is the second). 170 window.semUtilityBar = bar; 171 172 function sync() { 173 document.documentElement.style.setProperty('--sem-utility-bar-h', bar.offsetHeight + 'px'); 174 } 175 176 sync(); 177 window.addEventListener('load', sync); 178 window.addEventListener('resize', sync); 179})(); 180 181 182/** 183 * #site-nav-flyout â desktop scroll ride ("pinMenu", ported from the local 184 * flyout demo and adapted to the real markup). 185 *
186 * The panel lives in a Beaver Builder block (.fl-builder-content-100000117007) 187 * that is a child of .fl-page (position:relative), right after the sticky 188 * <header>. One scroller: the window. The panel has no internal scroll; it is 189 * moved between fixed-top / absolute / fixed-bottom so it rides with the page 190 * and clamps at BOTH ends â the top is reachable scrolling up, the bottom 191 * scrolling down. Smoothness comes from riding as position:absolute (the browser 192 * scrolls it natively â no per-frame JS reposition); position:fixed is used only 193 * at the two clamped ends. 194 * 195 * Everything is measured live from the header's bounding box, so it tracks the 196 * leaderboard ad scrolling away AND the header's shrink-on-scroll automatically. 197 * No-ops on pages without the flyout, and at <=768px, where the panel drops into 198 * the document flow and is scrolled by the page itself (see _site-header.scss 199 * and the open/close module below). 200 */ 201(function () { 202 'use strict'; 203 204 // `fl-builder-edit` is BB's own body class, set whenever the builder UI is 205 // open (FLBuilder::body_class()). Bail there: the ride writes inline 206 // position/top/left onto the wrapper, and inline styles outrank the 207 // editor-mode rules in _site-header.scss that put the panel back into normal 208 // flow so it can be laid out. 209 var wrap = document.querySelector('.fl-builder-content-100000117007'); 210 var header = document.querySelector('.fl-page > header'); 211 var flpage = document.querySelector('.fl-page'); 212 if (!wrap || !header || !flpage || document.body.classList.contains('fl-builder-edit')) { 213 return; 214 } 215 216 // Width, not touch: the old `ontouchstart` test was true on touch-screen 217 // laptops at 1440px and was captured once at init, so it never noticed a 218 // resize. 768px is the Beaver Builder medium breakpoint and the theme's 219 // dominant one (see _post-grid.scss). 220 var mobile = window.matchMedia('(max-width: 768px)'); 221 var mode = 'pinTop'; // 'pinTop' | 'pinBottom' | 'riding' 222 var anchorDoc = 0; // document-y the panel's top is anchored to while riding 223 var lastY = window.pageYOffset; 224 var queued = false; 225 226 // .fl-page's top in document coords (the panel is absolutely positioned within 227 // it, so ride offsets are measured from here). Ad-independent: the ad sits 228 // above .fl-page, so it never enters the panel's own coordinate space. 229 function flpageTop() { 230 return flpage.getBoundingClientRect().top + window.pageYOffset; 231 } 232 function flpageLeft() { 233 return flpage.getBoundingClientRect().left; 234 } 235 236 // The panel's home offset below the top of .fl-page: the height of the bar that 237 // CONTAINS the flyout's hamburger (the dark utility bar). That bar sits at the 238 // top of .fl-page, so its own height IS both its resting bottom offset and (once 239 // it's pinned) its viewport bottom â constant, and unaffected by the ad above 240 // .fl-page or the header's `display: contents` (whose own box measures as zero). 241 function navBottom() { 242 var toggle = header.querySelector('[data-site-nav-toggle]'); 243 if (toggle) { 244 var bar = toggle; 245 while (bar.parentElement && bar.parentElement !== header) { 246 bar = bar.parentElement; 247 } 248 return bar.offsetHeight; 249 } 250 return header.offsetHeight; 251 } 252 253 // Ride with the page: absolute, anchored at document-y `docPos` (measured 254 // within .fl-page). The browser scrolls it â no per-frame updates needed. 255 function toAbsolute(docPos) { 256 wrap.style.position = 'absolute'; 257 wrap.style.top = (docPos - flpageTop()) + 'px'; 258 wrap.style.bottom = 'auto'; 259 wrap.style.left = '0'; 260 } 261 // Pinned under the header (fixed is viewport-relative, so mirror .fl-page's x). 262 function toFixedTop(viewportTop) { 263 wrap.style.position = 'fixed'; 264 wrap.style.top = viewportTop + 'px'; 265 wrap.style.bottom = 'auto'; 266 wrap.style.left = flpageLeft() + 'px'; 267 } 268 // Pinned at the viewport bottom. 269 function toFixedBottom() { 270 wrap.style.position = 'fixed'; 271 wrap.style.top = 'auto'; 272 wrap.style.bottom = '0'; 273 wrap.style.left = flpageLeft() + 'px'; 274 } 275 276 function ride() { 277 queued = false; 278 if (mobile.matches) { 279 // The open/close module parks this wrapper itself while the drawer
280 // slides shut, so don't yank it out from under that. 281 if (window.semFlyoutParked) { 282 return; 283 } 284 // Otherwise CSS owns the mobile drawer. Inline styles outrank the 285 // media query, so hand placement back before returning â `resize` 286 // already re-runs ride(), so crossing the breakpoint cleans up after 287 // itself. 288 if (wrap.style.position) { 289 wrap.style.position = ''; 290 wrap.style.top = ''; 291 wrap.style.bottom = ''; 292 wrap.style.left = ''; 293 wrap.style.right = ''; 294 } 295 return; 296 } 297 298 var pos = window.pageYOffset; 299 var vh = window.innerHeight; 300 var ph = wrap.offsetHeight; 301 var flTop = flpageTop(); // doc-y of .fl-page's top (just below the ad) 302 var headerH = navBottom(); // visible nav bottom (not the taller header box) 303 var home = flTop + headerH; // doc-y of the panel's home = the nav's bottom edge 304 var pinned = pos >= flTop; // has the sticky header pinned to the viewport top? 305 306 // NEAR TOP: the header is still riding up as the leaderboard ad scrolls off. 307 // Anchor the panel at home as position:absolute so the BROWSER scrolls it in 308 // lockstep with the header â no per-frame JS write, so no wobble. 309 if (!pinned) { 310 toAbsolute(home); 311 mode = 'pinTop'; 312 lastY = pos; 313 return; 314 } 315 316 // HEADER PINNED: its bottom edge is now a constant (headerH). 317 var H = headerH; 318 319 // FITS under the header -> pin it there. The fixed `top` is a constant, so 320 // re-writing it each frame moves nothing (no wobble). 321 if (ph <= vh - H) { 322 toFixedTop(H); 323 mode = 'pinTop'; 324 lastY = pos; 325 return; 326 } 327 328 // TALL. While riding, leave the absolute anchor untouched (native scroll); 329 // only switch state at the clamps. 330 if (mode === 'riding') { 331 var vtop = anchorDoc - pos; 332 if (vtop >= H) { 333 toFixedTop(H); 334 mode = 'pinTop'; 335 } else if (vtop + ph <= vh) { 336 toFixedBottom(); 337 mode = 'pinBottom'; 338 } 339 lastY = pos; 340 return; 341 } 342 343 if (mode === 'pinBottom') { 344 if (pos < lastY) { // scrolling up -> release, ride down 345 anchorDoc = (vh - ph) + pos; 346 toAbsolute(anchorDoc); 347 mode = 'riding'; 348 } else { 349 toFixedBottom(); 350 } 351 lastY = pos; 352 return; 353 } 354 355 // pinTop 356 if (pos > lastY) { // scrolling down -> release, ride up 357 anchorDoc = H + pos; 358 toAbsolute(anchorDoc); 359 mode = 'riding'; 360 } else { 361 toFixedTop(H); 362 } 363 lastY = pos; 364 } 365 366 function schedule() { 367 if (!queued) { 368 queued = true; 369 window.requestAnimationFrame(ride); 370 } 371 } 372 373 window.addEventListener('scroll', schedule, { passive: true }); 374 window.addEventListener('resize', schedule, { passive: true }); 375 ride(); 376 377 // Let the open/close module below re-seed the ride the moment the panel is 378 // opened â otherwise a menu opened mid-page keeps whatever placement the 379 // last scroll left behind until the next scroll event. Passing `true` forces 380 // the next frame back to the pinned-top placement: with mode reset and no 381 // scroll delta, ride() falls straight through to toFixedTop(H), so the panel 382 // always opens showing its top. The reset is synchronous and the rAF fires 383 // after it, so a frame already queued by a scroll still sees the new state. 384 window.semFlyoutRide = function (reset) { 385 if (reset) { 386 mode = 'pinTop'; 387 lastY = window.pageYOffset; 388 } 389 schedule(); 390 }; 391})(); 392 393 394/** 395 * Flyout open / close. 396 * 397 * CSS owns every visual: the panel rests off-screen left (translateX(-100%) on 398 * #site-nav-flyout) and the toggle's three bars morph into an X off its own 399 * aria-expanded. This module only flips that state, so with JS off the menu 400 * simply stays closed rather than sitting open over the page. 401 * 402 * At <=768px it also drives the drawer. There the menu is pulled INTO the 403 * document flow and everything below it is hidden (CSS, off the html class this 404 * module sets), so the page is exactly as tall as the menu and the window's own 405 * scrolling does all the work: the ad scrolls away, the utility bar pins, the 406 * menu rides up under it, and the scroll ends where the menu ends. This module 407 * only has to flip the class, land the page at the menu's top, and put the 408 * scroll position back on close. 409 */ 410(function () { 411 'use strict'; 412 413 // Scoped to the header, like the two modules above: the toggle is the one in 414 // the dark utility bar, not any other element that happens to carry the hook. 415 // Bail inside the BB editor for the same reason as the ride above: this 416 // module would otherwise hold the panel closed (and the drawer would hide 417 // the page) while you are trying to edit it. 418 var toggle = document.querySelector('header [data-site-nav-toggle]'); 419 var flyout = document.getElementById('site-nav-flyout'); 420 if (!toggle || !flyout || document.body.classList.contains('fl-builder
420-edit')) { 421 return; 422 } 423 424 var mobile = window.matchMedia('(max-width: 768px)'); 425 var root = document.documentElement; 426 var wrap = flyout.closest('.fl-builder-content-100000117007'); 427 var savedY = 0; // page scroll to restore when the drawer closes 428 var thawTimer = 0; 429 430 // The button's markup lives in a Beaver Builder HTML module (outside this 431 // repo), so don't assume its ARIA is intact â a missing aria-expanded would 432 // cost the CSS its X state and leave the control with no announced state. 433 // Only fill in what's absent; authored values win. 434 if (!toggle.hasAttribute('aria-controls')) { 435 toggle.setAttribute('aria-controls', flyout.id); 436 } 437 if (!toggle.hasAttribute('aria-expanded')) { 438 toggle.setAttribute('aria-expanded', 'false'); 439 } 440 if (!toggle.hasAttribute('aria-label') && !toggle.textContent.trim()) { 441 toggle.setAttribute('aria-label', 'Open menu'); 442 } 443 444 function isOpen() { 445 return toggle.getAttribute('aria-expanded') === 'true'; 446 } 447 448 // Undo the parking below. Time-driven rather than `transitionend`-driven, 449 // because prefers-reduced-motion drops the slide entirely and then that event 450 // never fires at all. 451 function thaw() { 452 window.clearTimeout(thawTimer); 453 window.semFlyoutParked = false; 454 if (!wrap) { 455 return; 456 } 457 wrap.style.position = ''; 458 wrap.style.top = ''; 459 wrap.style.bottom = ''; 460 wrap.style.left = ''; 461 wrap.style.right = ''; 462 } 463 464 function enterDrawer() { 465 // Clears any inline placement on the wrapper before anything is measured: 466 // a quick re-open can land mid slide-out, and crossing the breakpoint with 467 // the menu open arrives here while the desktop ride's `position: fixed` is 468 // still on the element (its own cleanup is a rAF behind this handler). 469 thaw(); 470 savedY = window.pageYOffset; 471 root.classList.add('sem-flyout-drawer'); 472 473 // Measured only now: the menu is in flow â and the page below it gone â 474 // solely because of the class above. This is where the page has to sit for 475 // the menu's first item to be flush under the pinned utility bar. 476 var barH = window.semUtilityBar ? window.semUtilityBar.offsetHeight : 0; 477 var flush = Math.max(0, flyout.getBoundingClientRect().top + window.pageYOffset - barH); 478 479 // Only ever scrolls back UP to that point, never down to it. Below it â the 480 // case that matters, opening the menu from deep in an article â the page 481 // rises until the menu's top meets the bar. Above it the leaderboard ad is 482 // still on screen with the menu's top already visible underneath, so going 483 // down to `flush` would whip the ad away, when the whole point is that the 484 // reader scrolls it away themselves. 485 if (window.pageYOffset > flush) { 486 window.scrollTo(0, flush); 487 } 488 } 489 490 function exitDrawer() { 491 if (!root.classList.contains('sem-flyout-drawer')) { 492 return; 493 } 494 495 // The menu spends the next 0.25s sliding off to the left, but the class 496 // comes off now â which would snap it back up under the utility bar 497 // mid-slide, since it's no longer riding the page. Park it fixed exactly 498 // where it currently sits and let the slide play out from there. Measure 499 // first: going fixed is itself a layout change. 500 if (wrap && mobile.matches) { 501 var top = wrap.getBoundingClientRect().top; 502 wrap.style.position = 'fixed'; 503 wrap.style.top = top + 'px'; 504 wrap.style.bottom = 'auto'; 505 wrap.style.left = '0'; 506 wrap.style.right = '0'; 507 window.semFlyoutParked = true; 508 window.clearTimeout(thawTimer); 509 thawTimer = window.setTimeout(thaw, 400); 510 } 511 512 root.classList.remove('sem-flyout-drawer'); 513 window.scrollTo(0, savedY); 514 } 515 516 function setOpen(open) { 517 flyout.classList.toggle('is-open', open); 518 toggle.setAttribute('aria-expanded', open ? 'true' : 'false'); 519 toggle.setAttribute('aria-label', open ? 'Close menu' : 'Open menu'); 520 521 if (!open) { 522 exitDrawer(); 523 return; 524 } 525 526 if (mobile.matches) { 527 enterDrawer(); 528 return; 529 } 530 531 // Re-seed the ride so the panel shows its top no matter where the page 532 // was scrolled to. 533 if (typeof window.semFlyoutRide === 'function') { 534 window.semFlyoutRide(true); 535 } 536 } 537 538 // Crossing the breakpoint must not strand the drawer â it hides the page body 539 // while it's on â and an open menu has to be re-placed for the mode it just 540 // landed in. exitDrawer() no-ops when the drawer isn't on, so the closed and 541 // already-desktop cases cost nothing. 542 mobile.addEventListener('change', function () { 543 if (!isOpen()) { 544 exitDrawer(); 545 return; 546 } 547 if (mobile.matches) { 548 enterDrawer(); 549 return; 550 } 551 // Back to desktop: the ride sat out the whole mobile stretch, so its 552 // mode/lastY are stale â re-seed rather than let it resume from them. 553 exitDrawer(); 554 if (typeof window.semFlyoutRide === 'function') { 555 window.semFlyoutRide(true); 556 } 557 }); 558 559 toggle.addEventListener('click', function () { 560 setOpen(!isOpen()); 561 }); 562 563 // Escape closes and hands focus back to the toggle. 564 document.addEventListener('keydown', function (e) { 565 if ((e.key === 'Escape' || e.key === 'Esc') && isOpen()) { 566 setOpen(false); 567 toggle.focus(); 568 } 569 }); 570
571 // A click anywhere outside the panel closes it (the toggle handles itself). 572 document.addEventListener('click', function (e) { 573 if (!isOpen() || flyout.contains(e.target) || toggle.contains(e.target)) { 574 return; 575 } 576 setOpen(false); 577 }); 578})();
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.