PageSourceSearch

https://trbc.org/wp-content/themes/trbc-divi/js/nav-modern.js?ver=1790871783

js trbc.org collected 2026-10-02 09:54:43 UTC 17,411 bytes, 474 lines download raw bytes

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.