PageSourceSearch

https://acu.edu/wp-content/themes/acu/ebenezer-chrome/scripts/header-navigation.js?ver=1790730678

js acu.edu collected 2026-10-01 09:04:29 UTC 10,580 bytes, 261 lines download raw bytes

1(function () {
2  'use strict';
3
4  const header = document.querySelector('.site-header');
5  const nav = document.getElementById('primary-navigation');
6  const toggle = document.querySelector('.site-header__toggle');
7  const drawer = document.getElementById('ebenezer-primary-menu');
8  const masthead = document.querySelector('.site-masthead');
9  const topNav = document.querySelector('.top-nav');
10
11  const togglers = [];
12
13  // ---- Primary-nav submenus (shared between desktop dropdowns and the
14  //      mobile accordion; both are driven by the link's aria-expanded). ----
15  if (nav) {
16    nav.querySelectorAll(':scope > ul > li').forEach(function (li) {
17      const link = li.querySelector(':scope > a');
18      const submenu = li.querySelector(':scope > ul');
19      if (!link || !submenu) return;
20
21      link.setAttribute('aria-haspopup', 'true');
22      link.setAttribute('aria-expanded', 'false');
23
24      link.addEventListener('click', function (event) {
25        event.preventDefault();
26        const wasOpen = link.getAttribute('aria-expanded') === 'true';
27        closeSubmenus();
28        if (!wasOpen) link.setAttribute('aria-expanded', 'true');
29      });
30
31      togglers.push(link);
32    });
33  }
34
35  function closeSubmenus() {
36    togglers.forEach(function (link) {
37      link.setAttribute('aria-expanded', 'false');
38    });
39  }
40
41  // ---- Mobile drawer (hamburger) ----
42  function closeDrawer() {
43    if (toggle) toggle.setAttribute('aria-expanded', 'false');
44  }
45
46  // The open drawer is position: fixed, so it would cover the purple top-nav
47  // and yank the logo/X up to the viewport top. Instead its top edge
48  // (--drawer-top in the CSS) is anchored to the bottom of the top-nav when
49  // the bar is on screen: the bar stays visible and nothing jumps. Scrolled
50  // past the bar the offset is 0, a full-screen overlay.
51  // Body scrolling is locked while open, so the value can't go stale except
52  // on resize/rotation, which re-measures below.
53  function setDrawerTop() {
54    if (!header) return;
55    const offset = topNav ? Math.max(0, topNav.getBoundingClientRect().bottom) : 0;
56    header.style.setProperty('--drawer-top', offset + 'px');
57  }
58
59  // Opening mid-scroll can catch the top-nav half off-screen; anchoring there
60  // would strand a clipped, unreachable bar above the locked drawer. Nudge the
61  // page scroll to the nearest edge first, so the bar is either fully visible
62  // or fully hidden (at most half its height, ~16px, framed as a scroll).
63  function snapTopNavEdge() {
64    if (!topNav) return;
65    const rect = topNav.getBoundingClientRect();
66    if (rect.bottom <= 0 || rect.bottom >= rect.height) return;
67    const delta = rect.bottom >= rect.height / 2 ? rect.top : rect.bottom;
68    window.scrollBy({ top: delta, behavior: 'instant' });
69  }
70
71  if (toggle && drawer) {
72    toggle.addEventListener('click', function () {
73      const isOpen = toggle.getAttribute('aria-expanded') === 'true';
74      if (!isOpen) {
75        snapTopNavEdge();
76        setDrawerTop();
77      }
78      toggle.setAttribute('aria-expanded', String(!isOpen));
79      if (isOpen) closeSubmenus();
80    });
81
82    window.addEventListener('resize', function () {
83      if (toggle.getAttribute('aria-expanded') === 'true') setDrawerTop();
84    });
85
86    // Following a real link closes the drawer; submenu parents only expand.
87    drawer.addEventListener('click', function (event) {
88      const link = event.target.closest('a');
89      if (!link) return;
90      if (link.getAttribute('aria-haspopup') === 'true') return;
91      closeDrawer();
92    });
93  }
94
95  // ---- Search overlay (the top-nav "Search ACU" link opens a panel that
96  //      covers the header; without JS the link just navigates to /?s=). ----
97  const searchTrigger = document.querySelector('.top-nav__search');
98  const searchOverlay = document.getElementById('search-overlay');
99  const searchClose = searchOverlay && searchOverlay.querySelector('.search-overlay__close');
100  const searchInput = searchOverlay && searchOverlay.querySelector('.search-overlay__input');
101
102  function openSearch() {
103    if (!searchOverlay) return;
104    searchOverlay.classList.add('is-open');
105    if (searchTrigger) searchTrigger.setAttribute('aria-expanded', 'true');
106    if (searchInput) focusSearchInput(0);
107  }
108
109  // The panel animates in from visibility:hidden, which blocks focus, and the
110  // browser can also bounce focus back to the just-clicked trigger link. Force
111  // the open styles to apply (reading layout flushes them), focus, and retry
112  // over the next few frames until it sticks, then give up quietly.
113  function focusSearchInput(attempt) {
114    if (!searchInput) return;
115    void searchOverlay.offsetWidth;
116    searchInput.focus({ preventScroll: true });
117    if (document.activeElement !== searchInput && attempt < 6) {
118      requestAnimationFrame(function () { focusSearchInput(attempt + 1); });
119    }
120  }
121
122  function closeSearch(returnFocus) {
123    if (!searchOverlay) return;
124    searchOverlay.classList.remove('is-open');
125    if (searchTrigger) {
126      searchTrigger.setAttribute('aria-expanded', 'false');
127      if (returnFocus) searchTrigger.focus();
128    }
129  }
130
131  if (searchTrigger && searchOverlay) {
132    searchTrigger.setAttribute('aria-haspopup', 'dialog');
133    searchTrigger.setAttribute('aria-expanded', 'false');
134    searchTrigger.setAttribute('aria-controls', 'search-overlay');
135
136    searchTrigger.addEventListener('click', function (event) {
137      event.preventDefault();
138      if (searchOverlay.classList.contains('is-open')) closeSearch(true);
139      else openSearch();
140    });
141
142    if (searchClose) {
143      searchClose.addEventListener('click', function () {
144        closeSearch(true);
145      });
146    }
147  }
148
149  // ---- Focus trap: the overlay is aria-modal, so while it is open Tab must
150  //      cycle within it instead of reaching the page behind. openSearch drops
151  //      focus into the input and closeSearch returns it to the trigger; this
152  //      keeps it from escaping in between. ----
153  if (searchOverlay) {
154    const focusableSelector =
155      'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not
155([tabindex="-1"])';
156
157    document.addEventListener('keydown', function (event) {
158      if (event.key !== 'Tab') return;
159      if (!searchOverlay.classList.contains('is-open')) return;
160
161      const items = Array.prototype.filter.call(
162        searchOverlay.querySelectorAll(focusableSelector),
163        function (el) { return el.offsetWidth > 0 || el.offsetHeight > 0; }
164      );
165      if (!items.length) return;
166
167      const first = items[0];
168      const last = items[items.length - 1];
169      const active = document.activeElement;
170
171      if (!searchOverlay.contains(active)) {
172        // Focus has drifted out of (or never entered) the open panel - pull it
173        // back to the first control.
174        event.preventDefault();
175        first.focus();
176      } else if (event.shiftKey && active === first) {
177        event.preventDefault();
178        last.focus();
179      } else if (!event.shiftKey && active === last) {
180        event.preventDefault();
181        first.focus();
182      }
183    });
184  }
185
186  // ---- Search submit feedback: search is a full-page GET navigation (not
187  //      AJAX), so the magnifying-glass icon is swapped for a spinner the
188  //      moment the form is submitted, giving the click an acknowledgement
189  //      while the results page loads. Covers the desktop overlay and the
190  //      mobile inline box; the CSS reads the .is-searching class. ----
191  const searchForms = document.querySelectorAll(
192    '.search-overlay__form, .site-header__search'
193  );
194
195  searchForms.forEach(function (form) {
196    form.addEventListener('submit', function (event) {
197      // A re-entrant submit (the deferred form.submit() below, or a second
198      // Enter press) is already on its way - let it navigate.
199      if (form.classList.contains('is-searching')) return;
200      // Navigation would otherwise replace the page before the browser
201      // repaints, so the icon swap never showed on a fast connection. Hold the
202      // submit for two frames - long enough to paint the spinner, too short to
203      // feel like a delay - then navigate.
204      event.preventDefault();
205      form.classList.add('is-searching');
206      requestAnimationFrame(function () {
207        requestAnimationFrame(function () {
208          form.submit();
209        });
210      });
211    });
212  });
213
214  // Back/forward can restore this page from the bfcache with the submitted
215  // form's DOM intact, which would leave the spinner stuck (and the guard
216  // above latched). Reset it whenever the page is shown again so the search
217  // icon is back and the next search animates.
218  window.addEventListener('pageshow', function () {
219    searchForms.forEach(function (form) {
220      form.classList.remove('is-searching');
221    });
222  });
223
224  // ---- Outside click: collapse open submenus, the drawer, and the search
225  //      overlay. A click anywhere outside the search form (the cream panel,
226  //      or the page below it) dismisses it; the trigger toggles separately. ----
227  document.addEventListener('click', function (event) {
228    if (nav && !nav.contains(event.target)) closeSubmenus();
229    if (header && !header.contains(event.target)) closeDrawer();
230    if (searchOverlay && searchOverlay.classList.contains('is-open')) {
231      const form = searchOverlay.querySelector('.search-overlay__form');
232      const onForm = form && form.contains(event.target);
233      const onTrigger = searchTrigger && searchTrigger.contains(event.target);
234      if (!onForm && !onTrigger) closeSearch(false);
235    }
236  });
237
238  // ---- Sticky masthead: the wrapper sticks at a negative top equal to the
239  //      top-nav's height (see .site-masthead in header-navigation.css), so
240  //      scrolling slides the purple bar away while the site-header stays
241  //      pinned. The height is observed rather than set once because the bar
242  //      reflows when the viewport changes or the webfont loads. ----
243  if (masthead && topNav && 'ResizeObserver' in window) {
244    const setTopNavHeight = function () {
245      masthead.style.setProperty('--top-nav-height', topNav.offsetHeight + 'px');
246    };
247    setTopNavHeight();
248    new ResizeObserver(setTopNavHeight).observe(topNav);
249  }
250
251  // ---- Escape: close everything and drop focus out of the header. ----
252  document.addEventListener('keydown', function (event) {
253    if (event.key !== 'Escape') return;
254    closeSubmenus();
255    closeDrawer();
256    closeSearch(true);
257    if (header && document.activeElement && header.contains(document.activeElement)) {
258      document.activeElement.blur();
259    }
260  });
261})();

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.