1// SchlieÃt das Such-Modal und setzt den Suchzustand zurück. 2// Global, damit auch custom.js (Quersteuerung via #nav-toggle) darauf zugreifen kann. 3function closeSearch() { 4 const modal = document.getElementById('search-modal'); 5 6 if (!modal) { 7 return; 8 } 9 10 modal.classList.remove('active'); 11 document.body.classList.remove('search-open'); 12 13 jQuery('#show-popup').show(); 14} 15 16jQuery(document).ready(function ($) { 17 $('.menu-search').click(function () { 18 document.getElementById('search-modal').classList.add('active'); 19 $('#show-popup, #contact-popup, .eael-ext-scroll-to-top-wrap').hide(); 20 21 document.body.classList.add('search-open'); 22 }); 23 24 $('#search-close').click(function () { 25 closeSearch(); 26 }); 27 28 let backdropMouseDown = false; 29 30 $('#search-modal').on('mousedown', function (evt) { 31 backdropMouseDown = evt.target === this; 32 }); 33 34 $('#search-modal').on('click', function (evt) { 35 if (evt.target === this && backdropMouseDown) { 36 closeSearch(); 37 } 38 backdropMouseDown = false; 39 }); 40}); 41 42(function () { 43 'use strict'; 44 45 // ################################################################# 46 // DICTIONARY 47 // ################################################################# 48 49 const dictionaries = { 50 de: { 51 no_results: 'Keine Ergebnisse gefunden.', 52 search_error: 'Die Suche ist momentan nicht verfügbar.', 53 }, 54 en: { 55 no_results: 'No results found.', 56 search_error: 'Search is currently unavailable.', 57 }, 58 nl: { 59 no_results: 'Geen resultaten gevonden.', 60 search_error: 'De zoekfunctie is momenteel niet beschikbaar.', 61 }, 62 }; 63 64 function resolveSearchLanguage(lang) { 65 const code = (lang || 'en').slice(0, 2).toLowerCase(); 66 67 if (code === 'de' || code === 'nl') { 68 return code; 69 } 70 71 return 'en'; 72 } 73 74 // Spracherkennung via html lang Attribut 75 const languageCode = resolveSearchLanguage(document.documentElement.lang); 76 const dictionary = dictionaries[languageCode]; 77 78 // ################################################################# 79 // VARIABLEN 80 // ################################################################# 81 82 let debounceTimer = null; 83 let trackingTimer = null; 84 let issuedRequestId = 0; 85 let resultsWrapper = null; 86 let results = null; 87 let resultsText = null; 88 89 // ################################################################# 90 // FUNKTIONEN 91 // ################################################################# 92 93 function debounce(func, delay) { 94 return function (...args) { 95 clearTimeout(debounceTimer); 96 debounceTimer = setTimeout(() => func.apply(this, args), delay); 97 }; 98 } 99 100 // Schickt den Suchbegriff als Matomo-Event. Guard, da _paq durch 101 // Cookie-Consent/Adblocker fehlen kann. 102 function trackSearch(query) { 103 if (typeof window._paq === 'undefined') { 104 return; 105 } 106 107 window._paq.push(['trackEvent', 'search', 'query', query]); 108 } 109 110 // Eigener Debounce fürs Tracking (unabhängig vom Such-Debounce), damit nur 111 // die fertig getippte Query gezaehlt wird und keine Teil-Eingaben. 112 function scheduleTracking(query) { 113 clearTimeout(trackingTimer); 114 trackingTimer = setTimeout(() => trackSearch(query), 1500); 115 } 116 117 function createExcerpt(html, maxWords = 15) { 118 // Umweg der Text-Interpretation über den Browser -> Wir erhalten den Text ohne HTML 119 const temp = document.createElement('div'); 120 temp.innerHTML = html; 121 const text = temp.textContent || temp.innerText || ''; 122 123 const words = text.trim().split(/\s+/); 124 125 if (words.length <= maxWords) { 126 return text.trim(); 127 } 128 129 return words.slice(0, maxWords).join(' ') + ' [...]'; 130 } 131 132 async function renderResults(items) { 133 if (!resultsWrapper) { 134 return; 135 } 136 137 if (!items || items.length === 0) { 138 resultsText.innerHTML = '<strong>' + dictionary.no_results + '</strong>'; 139 results.innerHTML = ''; 140 return; 141 } 142 143 const itemHtml = await Promise.all( 144 items.map(async (item, index) => { 145 const metadata = item.metadata; 146 const type = metadata.type; 147 const title = metadata.title; 148 const url = metadata.url; 149 150 return `<div class="search-result type-${type}" data-href="${url}" style="--delay: ${index * 80}ms"> 151 <p class="title"><strong>${title}</strong></p> 152 <p class="excerpt">${createExcerpt(item.text)}</p> 153 </div>`; 154 }), 155 ); 156 157 resultsText.innerHTML = ''; 158 results.innerHTML = itemHtml.join(''); 159 } 160 161 function renderError(message) { 162 if (!resultsWrapper) { 163 return; 164 } 165 166 resultsText.innerHTML = '<strong>' + dictionary.search_error + '</strong>'; 167 168 results.innerHTML = message ? `<p class="search-error">${message}</p>` : ''; 169 } 170 171 function resetResults() { 172 if (!resultsWrapper) { 173 return; 174 } 175 176 issuedRequestId++; // entwertet alle noch laufenden fetches 177 resultsWrapper.classList.remove('loading'); 178 resultsText.innerHTML = ''; 179 results.innerHTML = ''; 180 } 181 182 function performSearch(query) { 183 resultsWrapper.classList.add('loading'); 184 const requestId = ++issuedRequestId; 185 186 const formData = new FormData(); 187 188 formData.append('action', 'search_query'); 189 formData.append('nonce', searchConfig.nonce); 190 formData.append('query', query); 191 formData.append('language', languageCode); 192 193 fetch(searchConfig.ajaxUrl, { 194 method: 'POST', 195 body: formData, 196 }) 197 .then((response) => response.json()) 198 .then((data) => { 199 // Nur die aktuellste Anfrage zählt; alles andere ist veraltet oder entwertet. 200 if (requestId !== issuedRequestId) { 201 return; 202 } 203 204 if (data.success) { 205 renderResults(data.data.results); 206 } else { 207 renderError(data?.data?.message); 208 console.error('Fehler:', data.data.message); 209 } 210 }) 211 .catch((error) => { 212 if (requestId === issuedRequestId) { 213 renderError(); 214 } 215 console.error('Verbindungsfehler:', error); 216 }) 217 .finally(() => { 218 // Nur der aktuellste Request darf den Loading-State finalisieren. 219 if (requestId === issuedRequestId) { 220 resultsWrapper.classList.remove('loading'); 221 } 222 }); 223 } 224 225 const debouncedSearch = debounce(performSearch, 400); 226 227 // ################################################################# 228 // EVENT LISTENER 229 // ################################################################# 230 231 document.addEventListener('DOMContentLoaded', function () { 232 const search = document.getElementById('search-text'); 233 const submitBtn = document.getElementById('search-submit'); 234 resultsWrapper = document.querySelector('.search-results'); 235 results = document.getElementById('search-results'); 236 resultsText = document.querySelector('.search-results > p'); 237 238 if (!search || !resultsWrapper) { 239 return; 240 } 241 242 if (submitBtn) { 243 submitBtn.addEventListener('click', function () { 244 executeSearch(); 245 }); 246 } 247 248 // Click-Handler für Suchergebnisse mit data-href (Event Delegation) 249 results.addEventListener('click', function (e) { 250 const result = e.target.closest('.search-result[data-href]'); 251 if (result) { 252 window.location.href = result.dataset.href; 253 } 254 }); 255 256 search.addEventListener('input', function () { 257 const query = this.value.trim(); 258 259 if (query.length >= 3) { 260 debouncedSearch(query); 261 scheduleTracking(query); 262 } else { 263 clearTimeout(debounceTimer); 264 clearTimeout(trackingTimer); 265 resetResults(); 266 } 267 }); 268 269 search.addEventListener('keydown', function (e) { 270 if (e.key === 'Enter') { 271 e.preventDefault(); 272 executeSearch(); 273 } 274 }); 275 276 function executeSearch() { 277 clearTimeout(debounceTimer); 278 clearTimeout(trackingTimer); 279 const query = search.value.trim(); 280 281 if (query.length >= 3) { 282 performSearch(query); 283 trackSearch(query); 284 } 285 } 286 }); 287})();
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.