PageSourceSearch

https://abc-usa.org/assets/search.js?v=1780066143

js abc-usa.org collected 2026-09-24 09:13:19 UTC 18,256 bytes, 521 lines download raw bytes

1/* ABC-USA Site Search — lazy-loaded, client-side, keyboard-accessible */
2(function() {
3  'use strict';
4
5  // ── CSS ──────────────────────────────────────────────────────────────
6  const STYLE = `
7.search-trigger {
8  background: none;
9  border: 1px solid #d6cdb8;
10  color: #0b1f24;
11  padding: 7px 14px 7px 36px;
12  border-radius: 999px;
13  font-family: inherit;
14  font-size: 13px;
15  cursor: pointer;
16  transition: border-color .2s, background .2s;
17  margin-right: 10px;
18  position: relative;
19  display: inline-flex;
20  align-items: center;
21  min-width: 140px;
22  color: #5e7a82;
23}
24.search-trigger::before {
25  content: '';
26  position: absolute;
27  left: 12px;
28  top: 50%;
29  width: 14px;
30  height: 14px;
31  transform: translateY(-50%);
32  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%235e7a82' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E");
33  background-size: contain;
34  background-repeat: no-repeat;
35}
36.search-trigger:hover {
37  border-color: #7a1e2b;
38  color: #0b1f24;
39}
40.search-trigger kbd {
41  margin-left: auto;
42  padding: 1px 6px;
43  font-size: 11px;
44  border: 1px solid #d6cdb8;
45  border-radius: 3px;
46  background: #f7f3ea;
47  font-family: ui-monospace, monospace;
48  color: #7a8a8e;
49}
50
51.search-overlay {
52  position: fixed;
53  inset: 0;
54  background: rgba(11, 31, 36, 0.55);
55  z-index: 9999;
56  display: none;
57  align-items: flex-start;
58  justify-content: center;
59  padding: 80px 16px 16px;
60  backdrop-filter: blur(4px);
61}
62.search-overlay.open { display: flex; }
63.search-panel {
64  background: #fff;
65  width: 100%;
66  max-width: 680px;
67  max-height: calc(100vh - 120px);
68  border-radius: 14px;
69  box-shadow: 0 20px 60px rgba(0,0,0,.3);
70  display: flex;
71  flex-direction: column;
72  overflow: hidden;
73}
74.search-input-row {
75  display: flex;
76  align-items: center;
77  padding: 16px 20px;
78  border-bottom: 1px solid #e6dfd2;
79  gap: 12px;
80}
81.search-input-row svg {
82  flex-shrink: 0;
83  color: #5e7a82;
84}
85.search-input-row input {
86  flex: 1;
87  border: 0;
88  outline: 0;
89  font-size: 18px;
90  font-family: inherit;
91  color: #0b1f24;
92  background: transparent;
93}
94.search-input-row .search-close {
95  border: 1px solid #d6cdb8;
96  background: #f7f3ea;
97  color: #5e7a82;
98  border-radius: 4px;
99  padding: 2px 10px;
100  font-size: 12px;
101  font-family: ui-monospace, monospace;
102  cursor: pointer;
103}
104.search-results {
105  overflow-y: auto;
106  padding: 8px 0 16px;
107}
108.search-section-header {
109  font-family: 'Fraunces', Georgia, serif;
110  font-size: 13px;
111  text-transform: uppercase;
112  letter-spacing: 1.5px;
113  color: #d4a64a;
114  padding: 18px 20px 8px;
115  font-weight: 600;
116}
117.search-result {
118  display: block;
119  padding: 12px 20px;
120  text-decoration: none;
121  color: inherit;
122  border-left: 3px solid transparent;
123  transition: background .12s, border-color .12s;
124  cursor: pointer;
125}
126.search-result.active, .search-result:hover {
127  background: #f7f3ea;
128  border-left-color: #7a1e2b;
129}
130.search-result-title {
131  font-family: 'Fraunces', Georgia, serif;
132  font-size: 16px;
133  color: #0b1f24;
134  margin: 0 0 4px;
135  line-height: 1.3;
136}
137.search-result-excerpt {
138  font-size: 13px;
139  line-height: 1.5;
140  color: #5e7a82;
141  margin: 0;
142}
143.search-result mark {
144  background: #fef3c7;
145  color: #0b1f24;
146  padding: 0 1px;
147  border-radius: 2px;
148  font-weight: 600;
149}
150.search-empty, .search-loading {
151  text-align: center;
152  color: #5e7a82;
153  font-size: 14px;
154  padding: 32px 20px;
155}
156.search-footer {
157  padding: 10px 20px;
158  border-top: 1px solid #e6dfd2;
159  font-size: 12px;
160  color: #7a8a8e;
161  display: flex;
162  justify-content: space-between;
163  background: #fafaf6;
164}
165.search-footer kbd {
166  display: inline-block;
167  padding: 1px 6px;
168  border: 1px solid #d6cdb8;
169  border-radius: 3px;
170  background: #fff;
171  font-family: ui-monospace, monospace;
172  margin: 0 2px;
173}
174@media (max-width: 600px) {
175  .search-overlay { padding-top: 16px; }
176  .search-trigger kbd { display: none; }
177  .search-trigger { min-width: 0; padding: 7px 12px 7px 32px; }
178}
179`;
180
181  // ── HTML for the trigger + overlay ──────────────────────────────────
182  const OVERLAY_HTML = `
183<div class="search-overlay" id="abcSearchOverlay" role="dialog" aria-modal="true" aria-label="Site search">
184  <div class="search-panel" role="document">
185    <div class="search-input-row">
186      <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
187        <circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/>
188      </svg>
189      <input type="text" id="abcSearchInput" placeholder="Search the site…" autocomplete="off" spellcheck="false" aria-label="Search">
190      <button type="button" class="search-close" aria-label="Close">Esc</button>
191    </div>
192    <div class="search-results" id="abcSearchResults"></div>
193    <div class="search-footer">
194      <span><kbd>↑</kbd><kbd>↓</kbd> navigate · <kbd>Enter</kbd> open · <kbd>Esc</kbd> close</span>
195      <span id="abcSearchStatus"></span>
196    </div>
197  </div>
198</div>
199`;
200
201  // ── Inject style + overlay ──────────────────────────────────────────
202  const styleEl = document.createElement('style');
203  styleEl.textContent = STYLE;
204  document.head.appendChild(styleEl);
205
206  const overlayWrap = document.createElement('div');
207  overlayWrap.innerHTML = OVERLAY_HTML;
208  document.body.appendChild(overlayWrap.firstElementChild);
209
210  const overlay   = document.getElementById('abcSearchOverlay');
211  const input     = document.getElementById('abcSearchInput');
212  const results   = document.getElementById('abcSearchResults');
213  const status    = document.getElementById('abcSearchStatus');
214  const closeBtn  = overlay.querySelector('.search-close');
215
216  // ── Lazy-load MiniSearch + index ─────────────────────────────────────
217  let miniSearch = null;
218  let indexData = null;
219  let activeIndex = -1;
220
221  function loadOnce() {
222    if (miniSearch) return Promise.resolve();
223    results.innerHTML = '<div class="search-loading">Loading search index…</div>';
224    return Promise.all([
225      fetch('assets/search-index.json').then(r => r.json()),
226      new Promise((resolve, reject) => {
227        if (window.MiniSearch) { resolve(); return; }
228        const s = document.createElement('script');
229        s.src = 'https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/index.min.js';
230        s.onload = resolve;
231        s.onerror = reject;
232        document.head.appendChild(s);
233      }),
234    ]).then(([data]) => {
235      indexData = data;
236      miniSearch = new MiniSearch({
237        fields: ['t', 'h', 'e', 'b', 's'],
238        storeFields: ['t', 's', 'e', 'u', 'd'],
239        idField: 'i',
240        searchOptions: {
241          boost: { t: 4, h: 2, s: 1.5, e: 1.2, b: 1 },
242          fuzzy: 0.2,
243          prefix: true,
244        },
245      });
246      miniSearch.addAll(data);
247      results.innerHTML = '';
248    });
249  }
250
251  // ── Search and render ──────────────────────────────────────────────
252  function escapeHtml(s) {
253    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
254                    .replace(/"/g,'&quot;').replace(/'/g,'&#x27;');
255  }
256
257  function highlight(text, terms) {
258    if (!terms || !terms.length) return escapeHtml(text);
259    const esc = escapeHtml(text);
260    const pattern = new RegExp('(' + terms.map(t => t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|') + ')', 'gi');
261    return esc.replace(pattern, '<mark>$1</mark>');
262  }
263
264  function render(query) {
265    activeIndex = -1;
266    if (!query.trim()) {
267      results.innerHTML = '<div class="search-empty">Start typing to search ' + (indexData ? indexData.length : '') + ' pages…</div>';
268      status.textContent = '';
269      return;
270    }
271    const hits = miniSearch.search(query, { combineWith: 'AND' }).slice(0, 25);
272    if (!hits.length) {
273      results.innerHTML = '<div class="search-empty">No results for "' + escapeHtml(query) + '"</div>';
274      status.textContent = '0 results';
275      return;
276    }
277    const terms = hits[0].terms;
278    // Group by section
279    const groups = {};
280    for (const h of hits) {
281      const sec = h.s || 'Other';
282      (groups[sec] = groups[sec] || []).push(h);
283    }
284    const order = ['Who We Are','Giving','Events','How We Serve','News',
285                   'Resources','Transition Ministries','Mission Summit Learnings',
286                   'About','Other'];
287    let html = '';
288    let n = 0;
289    for (const sec of order) {
290      if (!groups[sec]) continue;
291      html += '<div class="search-section-header">' + escapeHtml(sec) + '</div>';
292      for (const r of groups[sec]) {
293        const t = highlight(r.t || '(untitled)', terms);
294        const e = highlight(r.e || '', terms);
295        const dateBadge = r.d ? ' · ' + escapeHtml(r.d) : '';
296        html += `<a class="search-result" data-idx="${n}" href="${escapeHtml(r.u)}">
297                   <div class="search-result-title">${t}</div>
298                   <p class="search-result-excerpt">${e}${dateBadge}</p>
299                 </a>`;
300        n++;
301      }
302    }
303    results.innerHTML = html;
304    status.textContent = hits.length + ' result' + (hits.length === 1 ? '' : 's');
305  }
306
307  // ── Keyboard navigation ─────────────────────────────────────────────
308  function focusResult(delta) {
309    const items = results.querySelectorAll('.search-result');
310    if (!items.length) return;
311    if (activeIndex >= 0) items[activeIndex].classList.remove('active');
312    activeIndex = (activeIndex + delta + items.length) % items.length;
313    items[activeIndex].classList.add('active');
314    items[activeIndex].scrollIntoView({ block: 'nearest' });
315  }
316
317  // ── Open / close ────────────────────────────────────────────────────
318  function open() {
319    overlay.classList.add('open');
320    document.body.style.overflow = 'hidden';
321    loadOnce().then(() => {
322      input.focus();
323      render(input.value);
324    });
325  }
326  function close() {
327    overlay.classList.remove('open');
328    document.body.style.overflow = '';
329  }
330
331  // ── Wire up ─────────────────────────────────────────────────────────
332  closeBtn.addEventListener('click', close);
333  overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
334  input.addEventListener('input', () => { render(input.value); });
335  input.addEventListener('keydown', (e) => {
336    if (e.key === 'ArrowDown') { e.preventDefault(); focusResult(1); }
337    else if (e.key === 'ArrowUp') { e.preventDefault(); focusResult(-1); }
338    else if (e.key === 'Enter') {
339      const items = results.querySelectorAll('.search-result');
340      if (activeIndex >= 0 && items[activeIndex]) {
341        window.location.href = items[activeIndex].getAttribute('href');
342      } else if (items[0]) {
343        window.location.href = items[0].getAttribute('href');
344      }
345    } else if (e.key === 'Escape') {
346      close();
347    }
348  });
349
350  // Global hotkeys
351  document.addEventListener('keydown', (e) => {
352    if (overlay.classList.contains('open')) return;
353    if (e.key === '/' && !/^(INPUT|TEXTAREA|SELECT)$/i.test(e.target.tagName)) {
354      e.preventDefault(); open();
355    } else if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k') {
356      e.preventDefault(); open();
357    }
358  });
359
360  // Expose the open() to whatever button calls it
361  window.openAbcSearch = open;
362
363  // Find a trigger if one was already in the page (we'll inject one below)
364  document.addEventListener('click', (e) => {
365    const trg = e.target.closest('[data-abc-search-trigger]');
366    if (trg) { e.preventDefault(); open(); }
367  });
368})();
369
370
371/* ABC-USA Mobile Navigation — hamburger + slide-in drawer.
372   Progressive enhancement: reuses the existing .main-nav markup,
373   turns hover dropdowns into tap accordions on small screens. */
374(function () {
375  'use strict';
376
377  var header = document.querySelector('header');
378  var nav = header && header.querySelector('.main-nav');
379  if (!header || !nav) return;
380
381  function isMobile() { return window.matchMedia('(max-width: 768px)').matches; }
382
383  // ── Hamburger button (in the header) ──
384  var burger = document.createElement('button');
385  burger.className = 'abc-hamburger';
386  burger.type = 'button';
387  burger.setAttribute('aria-label', 'Open menu');
388  burger.setAttribute('aria-expanded', 'false');
389  burger.innerHTML =
390    '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" ' +
391    'stroke-linecap="round" aria-hidden="true">' +
392    '<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/>' +
393    '<line x1="3" y1="18" x2="21" y2="18"/></svg>';
394  var logo = header.querySelector('.header-logo');
395  if (logo && logo.nextSibling) header.insertBefore(burger, logo.nextSibling);
396  else header.insertBefore(burger, header.firstChild);
397
398  // ── Scrim ──
399  var scrim = document.createElement('div');
400  scrim.className = 'abc-nav-scrim';
401  document.body.appendChild(scrim);
402
403  // ── Drawer header (title + close) ──
404  var head = document.createElement('div');
405  head.className = 'abc-drawer-head';
406  head.innerHTML = '<span class="abc-drawer-title">Menu</span>';
407  var closeBtn = document.createElement('button');
408  closeBtn.className = 'abc-drawer-close';
409  closeBtn.type = 'button';
410  closeBtn.setAttribute('aria-label', 'Close menu');
411  closeBtn.innerHTML = '&times;';
412  head.appendChild(closeBtn);
413  nav.insertBefore(head, nav.firstChild);
414
415  // ── Build accordion toggles for top-level items that have a submenu ──
416  var items = nav.querySelectorAll('.nav-item-dropdown');
417  Array.prototype.forEach.call(items, function (item) {
418    // only handle direct children of .main-nav (skip nested sub items here)
419    if (item.parentNode !== nav) return;
420    var link = item.querySelector('.nav-main');
421    if (!link || link.parentNode !== item) return;
422    var submenu = item.querySelector('.dropdown-menu');
423    if (!submenu) return;
424
425    var row = document.createElement('div');
426    row.className = 'abc-acc-row';
427    item.insertBefore(row, link);
428    row.appendChild(link);
429
430    var tog = document.createElement('button');
431    tog.className = 'abc-acc-toggle';
432    tog.type = 'button';
433    tog.setAttribute('aria-expanded', 'false');
434    tog.setAttribute('aria-label', 'Expand ' + (link.textContent || '').trim());
435    tog.innerHTML =
436      '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" ' +
437      'stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
438      '<polyline points="6 9 12 15 18 9"/></svg>';
439    row.appendChild(tog);
440
441    tog.addEventListener('click', function (e) {
442      e.preventDefault();
443      var expanded = item.classList.toggle('abc-expanded');
444      tog.setAttribute('aria-expanded', expanded ? 'true' : 'false');
445    });
446
447    // A top-level label that points nowhere ("#") should just toggle.
448    if ((link.getAttribute('href') || '').trim() === '#') {
449      link.addEventListener('click', function (e) {
450        if (!isMobile()) return;
451        e.preventDefault();
452        tog.click();
453      });
454    }
455  });
456
457  // ── Nested sub-dropdowns (e.g. How We Serve → OGS) ──
458  var subParents = nav.querySelectorAll('.nav-sub-dropdown');
459  Array.prototype.forEach.call(subParents, function (sp) {
460    var parentLink = sp.querySelector('.nav-sub-parent');
461    if (!parentLink) return;
462    parentLink.addEventListener('click', function (e) {
463      if (!isMobile()) return;
464      e.preventDefault();
465      sp.classList.toggle('abc-expanded');
466    });
467  });
468
469  // ── Clone header CTAs into the drawer footer ──
470  var ctas = header.querySelector('.header-ctas');
471  if (ctas) {
472    var footer = document.createElement('div');
473    footer.className = 'abc-drawer-ctas';
474
475    var searchBtn = document.createElement('button');
476    searchBtn.type = 'button';
477    searchBtn.className = 'abc-drawer-search';
478    searchBtn.setAttribute('data-abc-search-trigger', '');
479    searchBtn.innerHTML =
480      '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" ' +
481      'stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">' +
482      '<circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg> Search';
483    footer.appendChild(searchBtn);
484
485    Array.prototype.forEach.call(ctas.querySelectorAll('.btn'), function (b) {
486      footer.appendChild(b.cloneNode(true));
487    });
488    nav.appendChild(footer);
489  }
490
491  // ── Open / close ──
492  function openMenu() {
493    document.body.classList.add('abc-nav-open');
494    burger.setAttribute('aria-expanded', 'true');
495  }
496  function closeMenu() {
497    document.body.classList.remove('abc-nav-open');
498    burger.setAttribute('aria-expanded', 'false');
499  }
500  burger.addEventListener('click', function () {
501    document.body.classList.contains('abc-nav-open') ? closeMenu() : openMenu();
502  });
503  closeBtn.addEventListener('click', closeMenu);
504  scrim.addEventListener('click', closeMenu);
505
506  // Close after tapping a real navigation link (and when opening search)
507  nav.addEventListener('click', function (e) {
508    if (e.target.closest('[data-abc-search-trigger]')) { closeMenu(); return; }
509    var a = e.target.closest('a');
510    if (!a) return;
511    var href = (a.getAttribute('href') || '').trim();
512    if (href && href !== '#') closeMenu();
513  });
514
515  document.addEventListener('keydown', function (e) {
516    if (e.key === 'Escape' && document.body.classList.contains('abc-nav-open')) closeMenu();
517  });
518  window.addEventListener('resize', function () {
519    if (!isMobile()) closeMenu();
520  });
521})();

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.