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,'&').replace(/</g,'<').replace(/>/g,'>') 254 .replace(/"/g,'"').replace(/'/g,'''); 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 = '×'; 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.