1(function () { 2 'use strict'; 3 4 var header = document.getElementById('mn-header'); 5 if (!header) return; 6 7 var FOCUSABLE_SELECTOR = 8 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; 9 10 /* ââ Element references âââââââââââââââââââââââââ */ 11 var hamburgerToggle = header.querySelector('.mn-hamburger-toggle'); 12 var hmenuPanel = document.querySelector('.mn-hmenu-panel'); 13 var hmenuBackdrop = document.querySelector('.mn-hmenu-backdrop'); 14 var hmenuClose = hmenuPanel ? hmenuPanel.querySelector('.mn-hmenu-close') : null; 15 16 var searchToggle = header.querySelector('.mn-search-toggle'); 17 var searchBar = header.querySelector('.mn-search-bar'); 18 var searchInput = header.querySelector('.mn-search-input'); 19 20 var askToggle = header.querySelector('.mn-ask-toggle'); 21 var pyvToggle = header.querySelector('.mn-pyv-toggle'); 22 var pyvPanel = document.querySelector('.mn-pyv-panel'); 23 var pyvClose = document.querySelector('.mn-pyv-close'); 24 var pyvBackdrop = document.querySelector('.mn-pyv-backdrop'); 25 26 function setInert(element, isInert) { 27 if (!element) return; 28 element.inert = isInert; 29 if (isInert) { 30 element.setAttribute('inert', ''); 31 } else { 32 element.removeAttribute('inert'); 33 } 34 } 35 36 /* ââ Skip link / main landmark ââââââââââââââââââ */ 37 // Divi renders #main-content as a generic div and its smooth-scroll handler 38 // can swallow the native fragment jump. Promote the existing container to a 39 // landmark when the page has none, then move focus and scroll explicitly. 40 var skipLink = document.querySelector('.mn-skip-link'); 41 var mainContent = document.getElementById('main-content'); 42 43 if (mainContent) { 44 if (!document.querySelector('main, [role="main"]')) { 45 mainContent.setAttribute('role', 'main'); 46 } 47 if (!mainContent.hasAttribute('tabindex')) { 48 mainContent.setAttribute('tabindex', '-1'); 49 } 50 } 51 52 if (skipLink && mainContent) { 53 skipLink.addEventListener('click', function (event) { 54 event.preventDefault(); 55 try { 56 mainContent.focus({ preventScroll: true }); 57 } catch (error) { 58 mainContent.focus(); 59 } 60 mainContent.scrollIntoView({ block: 'start' }); 61 if (window.history && window.history.replaceState) { 62 window.history.replaceState(null, '', '#main-content'); 63 } 64 }); 65 } 66 67 /* ââ Admin bar offset âââââââââââââââââââââââââââ */ 68 // #wpadminbar is position:fixed on desktop but position:absolute below 69 // 782px, where it scrolls out of view. A static top offset would then leave 70 // a strip of page content showing above the header, so track how much of the 71 // bar is actually still on screen and hand it to CSS as --mn-admin-offset. 72 // Core renders the bar on wp_body_open, well before this footer script, but 73 // fall back to a re-lookup in case it lands via the wp_footer back-compat path. 74 var adminBar = document.getElementById('wpadminbar'); 75 var lastAdminOffset = -1; 76 77 function syncAdminOffset() { 78 if (!adminBar) { 79 adminBar = document.getElementById('wpadminbar'); 80 if (!adminBar) return; 81 } 82 // Clamped to the bar's own height: how much of it still overlaps the top of 83 // the viewport. Never larger than the bar (so a bar positioned oddly by 84 // another plugin degrades to the old static offset instead of doubling it) 85 // and never negative (once it has scrolled away the header sits flush). 86 var rect = adminBar.getBoundingClientRect(); 87 var offset = Math.max(0, Math.min(Math.round(rect.bottom), Math.round(rect.height))); 88 if (offset === lastAdminOffset) return; 89 lastAdminOffset = offset; 90 document.body.style.setProperty('--mn-admin-offset', offset + 'px'); 91 } 92 93 /* ââ Scroll-lock compensation âââââââââââââââââââ */ 94 // Locking body scroll removes the scrollbar; reserve its width so the page 95 // and the fixed header don't shift sideways when a panel opens. 96 function syncScrollbarWidth() { 97 var width = window.innerWidth - document.documentElement.clientWidth; 98 document.body.style.setProperty('--mn-scrollbar', Math.max(0, width) + 'px'); 99 } 100 101 /* ââ Focus management âââââââââââââââââââââââââââ */ 102 // The element focus returns to when the current panel closes. 103 var lastFocused = null; 104 // The dialog panel that currently owns the focus trap (or null). 105 var activePanel = null; 106 107 function getVisibleFocusable(container) { 108 var nodes = container.querySelectorAll(FOCUSABLE_SELECTOR); 109 return Array.prototype.filter.call(nodes, function (el) { 110 return el.offsetWidth > 0 || el.offsetHeight > 0 || el === document.activeElement; 111 }); 112 } 113 114 function focusFirst(container) { 115 var focusable = getVisibleFocusable(container); 116 if (focusable.length) focusable[0].focus(); 117 } 118 119 function restoreFocus() { 120 if (lastFocused && typeof lastFocused.focus === 'function') { 121 lastFocused.focus(); 122 } 123 lastFocused = null; 124 } 125 126 // Keep Tab / Shift+Tab inside the open dialog panel. 127 function trapTab(e) { 128 if (!activePanel) return; 129 var focusable = getVisibleFocusable(activePanel); 130 if (!focusable.length) return; 131 var first = focusable[0];
132 var last = focusable[focusable.length - 1]; 133 134 if (e.shiftKey && document.activeElement === first) { 135 e.preventDefault(); 136 last.focus(); 137 } else if (!e.shiftKey && document.activeElement === last) { 138 e.preventDefault(); 139 first.focus(); 140 } else if (!activePanel.contains(document.activeElement)) { 141 // Focus escaped the panel (e.g. via browser chrome) â pull it back. 142 e.preventDefault(); 143 first.focus(); 144 } 145 } 146 147 /* ââ Hamburger Panel (left side) ââââââââââââââââ */ 148 function closeHamburger() { 149 if (!hmenuPanel) return; 150 var wasOpen = hmenuPanel.classList.contains('mn-open'); 151 hmenuPanel.classList.remove('mn-open'); 152 hmenuBackdrop.classList.remove('mn-open'); 153 hmenuPanel.setAttribute('aria-hidden', 'true'); 154 setInert(hmenuPanel, true); 155 hmenuBackdrop.setAttribute('aria-hidden', 'true'); 156 hamburgerToggle.setAttribute('aria-expanded', 'false'); 157 hamburgerToggle.classList.remove('mn-active'); 158 document.body.classList.remove('mn-panel-open'); 159 if (activePanel === hmenuPanel) activePanel = null; 160 if (wasOpen) restoreFocus(); 161 } 162 163 function openHamburger() { 164 if (!hmenuPanel) return; 165 // Close others first (this may restore their focus; we override below). 166 closeSearch(); 167 closePYV(); 168 lastFocused = hamburgerToggle; 169 syncScrollbarWidth(); 170 hmenuPanel.classList.add('mn-open'); 171 hmenuBackdrop.classList.add('mn-open'); 172 setInert(hmenuPanel, false); 173 hmenuPanel.setAttribute('aria-hidden', 'false'); 174 hmenuBackdrop.setAttribute('aria-hidden', 'false'); 175 hamburgerToggle.setAttribute('aria-expanded', 'true'); 176 hamburgerToggle.classList.add('mn-active'); 177 document.body.classList.add('mn-panel-open'); 178 activePanel = hmenuPanel; 179 focusFirst(hmenuPanel); 180 } 181 182 if (hamburgerToggle) { 183 hamburgerToggle.addEventListener('click', function () { 184 if (hmenuPanel && hmenuPanel.classList.contains('mn-open')) { 185 closeHamburger(); 186 } else { 187 openHamburger(); 188 } 189 }); 190 } 191 192 if (hmenuClose) { 193 hmenuClose.addEventListener('click', closeHamburger); 194 } 195 196 if (hmenuBackdrop) { 197 hmenuBackdrop.addEventListener('click', closeHamburger); 198 } 199 200 /* ââ Search Toggle ââââââââââââââââââââââââââââââ */ 201 function closeSearch() { 202 if (!searchBar) return; 203 searchBar.classList.remove('mn-open'); 204 searchBar.setAttribute('aria-hidden', 'true'); 205 setInert(searchBar, true); 206 if (searchToggle) searchToggle.setAttribute('aria-expanded', 'false'); 207 } 208 209 function openSearch() { 210 if (!searchBar) return; 211 closeHamburger(); 212 closePYV(); 213 searchBar.classList.add('mn-open'); 214 setInert(searchBar, false); 215 searchBar.setAttribute('aria-hidden', 'false'); 216 if (searchToggle) searchToggle.setAttribute('aria-expanded', 'true'); 217 if (searchInput) searchInput.focus(); 218 } 219 220 if (searchToggle) { 221 searchToggle.addEventListener('click', function () { 222 if (searchBar.classList.contains('mn-open')) { 223 closeSearch(); 224 } else { 225 openSearch(); 226 } 227 }); 228 } 229 230 if (searchInput) { 231 searchInput.addEventListener('keydown', function (e) { 232 if (e.key === 'Escape') { 233 closeSearch(); 234 if (searchToggle) searchToggle.focus(); 235 } 236 }); 237 } 238 239 /* ââ Plan Your Visit Drawer (right side) ââââââââ */ 240 function closePYV() { 241 if (!pyvPanel) return; 242 var wasOpen = pyvPanel.classList.contains('mn-open'); 243 pyvPanel.classList.remove('mn-open'); 244 pyvBackdrop.classList.remove('mn-open'); 245 pyvPanel.setAttribute('aria-hidden', 'true'); 246 setInert(pyvPanel, true); 247 pyvBackdrop.setAttribute('aria-hidden', 'true'); 248 pyvToggle.setAttribute('aria-expanded', 'false'); 249 pyvToggle.classList.remove('mn-active'); 250 document.body.classList.remove('mn-panel-open'); 251 if (activePanel === pyvPanel) activePanel = null; 252 if (wasOpen) restoreFocus(); 253 } 254 255 function openPYV() { 256 if (!pyvPanel) return; 257 closeSearch(); 258 closeHamburger(); 259 lastFocused = pyvToggle; 260 syncScrollbarWidth(); 261 pyvPanel.classList.add('mn-open'); 262 pyvBackdrop.classList.add('mn-open'); 263 setInert(pyvPanel, false); 264 pyvPanel.setAttribute('aria-hidden', 'false'); 265 pyvBackdrop.setAttribute('aria-hidden', 'false'); 266 pyvToggle.setAttribute('aria-expanded', 'true'); 267 pyvToggle.classList.add('mn-active'); 268 document.body.classList.add('mn-panel-open'); 269 activePanel = pyvPanel; 270 focusFirst(pyvPanel); 271 } 272 273 if (pyvToggle) { 274 pyvToggle.addEventListener('click', function () { 275 if (pyvPanel && pyvPanel.classList.contains('mn-open')) { 276 closePYV(); 277 } else { 278 openPYV(); 279 } 280 }); 281 } 282 283 if (pyvClose) { 284 pyvClose.addEventListener('click', closePYV); 285 } 286 287 if (pyvBackdrop) { 288 pyvBackdrop.addEventListener('click', closePYV); 289 } 290 291 /* ââ Global keyboard handling âââââââââââââââââââ */ 292 document.addEventListener('keydown', function (e) { 293 if (e.key === 'Escape' || e.key === 'Esc') { 294 if (searchBar && searchBar.classList.contains('mn-open')) { 295 closeSearch(); 296 if (searchToggle) searchToggle.focus(); 297 } 298 // Closing a panel restores focus to its trigger. 299 if (pyvPanel && pyvPanel.classList.contains('mn-open')) closePYV(); 300 if (hmenuPanel && hmenuPanel.classList.contains('mn-open')) closeHamburger(); 301 return; 302 } 303 304 if (e.key === 'Tab' && activePanel) { 305 trapTab(e); 306 } 307 }); 308 309 /* ââ Desktop Dropdown Keyboard Support ââââââââââ */ 310 // CSS reveals a dropdown on :hover and :focus-within. Here we keep 311 // aria-expanded in sync and let Escape collapse an open dropdown by 312 // adding .mn-kb-suppress (which CSS uses to override :focus-within).
313 var VIEWPORT_EDGE_GUTTER = 12; 314 315 // A dropdown wider than the space left of the viewport edge gets flipped to 316 // open toward the other side instead of being cut off. Measured on open: 317 // dropdowns keep their layout box while hidden (visibility, not display). 318 function positionDropdown(dropdown) { 319 dropdown.classList.remove('mn-flip'); 320 var rect = dropdown.getBoundingClientRect(); 321 if (rect.right > window.innerWidth - VIEWPORT_EDGE_GUTTER) { 322 dropdown.classList.add('mn-flip'); 323 } 324 } 325 326 var desktopParents = header.querySelectorAll('.mn-nav .mn-has-children'); 327 // Collapse callbacks for every open dropdown, so opening one closes the rest 328 // and an outside click or Escape closes whatever is open. 329 var closers = []; 330 331 function closeAllDropdowns(except) { 332 for (var i = 0; i < closers.length; i++) { 333 if (closers[i].parent !== except) closers[i].close(); 334 } 335 } 336 337 document.addEventListener('click', function (e) { 338 if (!e.target.closest || !e.target.closest('.mn-nav .mn-has-children')) { 339 closeAllDropdowns(null); 340 } 341 }); 342 343 document.addEventListener('keydown', function (e) { 344 if (e.key === 'Escape') closeAllDropdowns(null); 345 }); 346 347 for (var k = 0; k < desktopParents.length; k++) { 348 (function (parent) { 349 var link = parent.querySelector(':scope > .mn-link'); 350 if (!link) return; 351 352 var dropdown = parent.querySelector(':scope > .mn-dropdown'); 353 354 // A TOP-LEVEL item that has children is a disclosure, not a destination. 355 // It used to navigate to its own landing page (/about/, /engage/ ...), 356 // which on a touch device meant the page changed before the menu could 357 // even be read. The href stays in the markup so the destination is still 358 // crawlable and reachable without JS; the click just doesn't follow it. 359 // 360 // This only ever applied to the top level, but the selector above 361 // matches every .mn-has-children in the nav, so second-level parents 362 // got it too â Get Involved > Kids, > Adults and > Special Needs each 363 // swallowed their own click and went nowhere. Their flyouts were 364 // already open from :hover by the time the click landed, so the click 365 // had no visible effect at all. They are real destinations 366 // (/engage/adults/ and friends) and navigate again now. Everything 367 // below â hover, focus, keyboard, aria-expanded, flip positioning â 368 // still applies at every depth. 369 var isTopLevel = !!parent.parentElement 370 && parent.parentElement.classList.contains('mn-menu'); 371 372 if (isTopLevel) { 373 link.addEventListener('click', function (e) { 374 e.preventDefault(); 375 var willOpen = !parent.classList.contains('mn-open'); 376 closeAllDropdowns(parent); 377 parent.classList.toggle('mn-open', willOpen); 378 link.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); 379 if (willOpen && dropdown) positionDropdown(dropdown); 380 }); 381 } 382 383 function expand() { 384 parent.classList.remove('mn-kb-suppress'); 385 link.setAttribute('aria-expanded', 'true'); 386 if (dropdown) positionDropdown(dropdown); 387 } 388 389 closers.push({ 390 parent: parent, 391 close: function () { 392 parent.classList.remove('mn-open'); 393 link.setAttribute('aria-expanded', 'false'); 394 }, 395 }); 396 397 function collapse() { 398 parent.classList.remove('mn-kb-suppress'); 399 parent.classList.remove('mn-open'); 400 link.setAttribute('aria-expanded', 'false'); 401 } 402 403 parent.addEventListener('mouseenter', expand); 404 parent.addEventListener('mouseleave', collapse); 405 406 parent.addEventListener('focusin', expand); 407 parent.addEventListener('focusout', function (e) { 408 if (!parent.contains(e.relatedTarget)) collapse(); 409 }); 410 411 parent.addEventListener('keydown', function (e) { 412 if (e.key === 'Escape' || e.key === 'Esc') { 413 e.stopPropagation(); 414 parent.classList.add('mn-kb-suppress'); 415 link.setAttribute('aria-expanded', 'false'); 416 link.focus(); 417 } 418 }); 419 })(desktopParents[k]); 420 } 421 422 /* ââ Ask: open the Griswold chat widget âââââââââ */ 423 // The widget's embed script (Divi Theme Options > Integration > head) is 424 // async, so window.griswold may not exist yet when this runs. The button 425 // ships with [hidden] and is revealed only once the API is installed â 426 // either it is already there, or its loader fires griswold:ready. Nothing
427 // reveals it if the embed is blocked, which is the point: no dead control. 428 if (askToggle) { 429 var revealAsk = function () { 430 askToggle.hidden = false; 431 }; 432 433 if (window.griswold) { 434 revealAsk(); 435 } else { 436 window.addEventListener('griswold:ready', revealAsk, { once: true }); 437 } 438 439 askToggle.addEventListener('click', function () { 440 if (!window.griswold) return; 441 // Close our own panels first, same as openHamburger/openPYV/openSearch 442 // do for each other â the chat opens fullscreen on mobile and would 443 // otherwise cover a menu left open and scroll-locked behind it. 444 closeSearch(); 445 closeHamburger(); 446 closePYV(); 447 window.griswold.open(); 448 }); 449 } 450 451 /* ââ Scroll Behavior: Shrink Header âââââââââââââ */ 452 var SHRINK_AT = 20; 453 var scrolled = false; 454 455 function onScroll() { 456 var shouldShrink = window.scrollY > SHRINK_AT; 457 if (shouldShrink !== scrolled) { 458 scrolled = shouldShrink; 459 header.classList.toggle('mn-scrolled', scrolled); 460 } 461 syncAdminOffset(); 462 } 463 464 window.addEventListener('scroll', onScroll, { passive: true }); 465 466 window.addEventListener('resize', function () { 467 syncAdminOffset(); 468 if (!document.body.classList.contains('mn-panel-open')) syncScrollbarWidth(); 469 }, { passive: true }); 470 471 syncScrollbarWidth(); 472 onScroll(); 473 474})();
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.