PageSourceSearch

https://lwvmt.org/assets/js/app.js?v=3

js lwvmt.org collected 2026-10-06 15:18:10 UTC 6,510 bytes, 137 lines download raw bytes

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 {'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[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.