1document.addEventListener('DOMContentLoaded', function () { 2 const menu = document.querySelector('.menu-toggle'); 3 const nav = document.querySelector('.primary-nav'); 4 if (menu && nav) { 5 menu.addEventListener('click', function () { 6 const open = nav.classList.toggle('open'); 7 menu.setAttribute('aria-expanded', open ? 'true' : 'false'); 8 }); 9 } 10 11 document.querySelectorAll('.nav-group > button').forEach(function (button) { 12 button.addEventListener('click', function () { 13 const parent = button.parentElement; 14 const open = parent.classList.toggle('open'); 15 button.setAttribute('aria-expanded', open ? 'true' : 'false'); 16 }); 17 }); 18 19 const league = document.querySelector('#event-league'); 20 const type = document.querySelector('#event-type'); 21 function filterEvents() { 22 if (!league || !type) return; 23 let visible = 0; 24 document.querySelectorAll('.event-row').forEach(function (row) { 25 const leagueMatches = league.value === 'all' || row.dataset.league === league.value; 26 const typeMatches = type.value === 'all' || row.dataset.type === type.value; 27 row.hidden = !(leagueMatches && typeMatches); 28 if (!row.hidden) visible += 1; 29 }); 30 const count = document.querySelector('#event-result-count'); 31 if (count) count.textContent = visible + ' event(s) shown'; 32 } 33 if (league && type) { 34 league.addEventListener('change', filterEvents); 35 type.addEventListener('change', filterEvents); 36 filterEvents(); 37 } 38 39 document.querySelectorAll('form[data-static-mail-form="true"]').forEach(function (form) { 40 form.addEventListener('submit', function (event) { 41 event.preventDefault(); 42 if (!form.reportValidity()) return; 43 const data = new FormData(form); 44 if ((data.get('website') || '').toString().trim()) return; 45 const typeValue = (data.get('type') || 'website').toString(); 46 const leagueValue = (data.get('league') || '').toString(); 47 const subject = 'LWV Montana ' + typeValue + (leagueValue ? ' â ' + leagueValue : ''); 48 const lines = []; 49 data.forEach(function (value, key) { 50 if (['website', 'type'].includes(key)) return; 51 lines.push(key.replace(/_/g, ' ') + ': ' + value); 52 }); 53 const mailto = 'mailto:[email protected]?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(lines.join('\n')); 54 const status = form.querySelector('.form-status'); 55 if (status) { 56 status.textContent = 'Your email application is opening. Review the message and press Send.'; 57 status.style.color = '#146b3a'; 58 } 59 showToast('Opening your email applicationâ¦'); 60 window.location.href = mailto; 61 }); 62 }); 63 64 const searchForm = document.querySelector('.search-form'); 65 const searchHolder = document.querySelector('#static-search-results'); 66 if (searchForm && searchHolder) { 67 const input = searchForm.querySelector('input[name="q"]'); 68 const params = new URLSearchParams(window.location.search); 69 const initialQuery = (params.get('q') || '').trim(); 70 if (input) input.value = initialQuery; 71 72 function escapeHTML(value) { 73 return String(value).replace(/[&<>'"]/g, function (char) { 74 return {'&':'&','<':'<','>':'>',"'":''','"':'"'}[char]; 75 }); 76 } 77 function urlFor(group, item) { 78 if (group === 'news') return '/news/' + item.slug + '/'; 79 if (group === 'studies') return '/studies/' + item.slug + '/'; 80 if (group === 'local_leagues') return item.legacy || '/local-leagues/'; 81 if (group === 'resources') return item.url || '/voter-resources/'; 82 if (group === 'events') return '/calendar/'; 83 if (group === 'positions') return '/positions/'; 84 return '/'; 85 } 86 function runSearch(query) { 87 if (!query) { 88 searchHolder.innerHTML = '<div class="empty-state"><h2>Search the archive</h2><p>Enter a term such as vote, constitution, Helena, education, or elections.</p></div>'; 89 return; 90 } 91 searchHolder.innerHTML = '<p>Searchingâ¦</p>'; 92 fetch('/data/content.json').then(function (response) { 93 if (!response.ok) throw new Error('Search data unavailable'); 94 return response.json(); 95 }).then(function (data) { 96 const needle = query.toLowerCase(); 97 const groups = ['events','news','studies','local_leagues','resources','positions']; 98 const results = []; 99 groups.forEach(function (group) {
100 (data[group] || []).forEach(function (item) { 101 if (JSON.stringify(item).toLowerCase().includes(needle)) results.push({group:group,item:item}); 102 }); 103 }); 104 if (!results.length) { 105 searchHolder.innerHTML = '<div class="empty-state"><h2>No results found</h2><p>Try a broader term such as vote, constitution, Helena, or education.</p></div>'; 106 return; 107 } 108 searchHolder.innerHTML = '<p class="result-count">' + results.length + ' result(s) for â' + escapeHTML(query) + 'â</p>' + results.map(function (result) { 109 const item = result.item; 110 const title = item.title || item.name || 'Resource'; 111 const summary = item.summary || item.focus || item.desc || item.text || ''; 112 const external = /^https?:/.test(urlFor(result.group,item)); 113 return '<article><span class="pill">' + escapeHTML(result.group.replace(/_/g,' ')) + '</span><h2><a href="' + escapeHTML(urlFor(result.group,item)) + '"' + (external ? ' target="_blank" rel="noopener"' : '') + '>' + escapeHTML(title) + '</a></h2><p>' + escapeHTML(summary) + '</p></article>'; 114 }).join(''); 115 }).catch(function () { 116 searchHolder.innerHTML = '<div class="empty-state"><h2>Search unavailable</h2><p>Open this site through a web server rather than directly from your computer, or browse using the main navigation.</p></div>'; 117 }); 118 } 119 searchForm.addEventListener('submit', function (event) { 120 event.preventDefault(); 121 const query = (input.value || '').trim(); 122 const next = query ? '/search/?q=' + encodeURIComponent(query) : '/search/'; 123 history.replaceState({}, '', next); 124 runSearch(query); 125 }); 126 runSearch(initialQuery); 127 } 128 129 function showToast(message) { 130 const toast = document.querySelector('#toast'); 131 if (!toast) return; 132 toast.textContent = message; 133 toast.classList.add('show'); 134 window.setTimeout(function () { toast.classList.remove('show'); }, 4200); 135 } 136 window.showToast = showToast; 137});
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.