PageSourceSearch

https://www.middendrenthe.nl/_assets/901b4514440eb8fbfc3e64864b8e4928/JavaScript/liveresults.js?1790172981

js middendrenthe.nl collected 2026-09-24 19:34:23 UTC 31,153 bytes, 878 lines download raw bytes

1(function () {
2  'use strict';
3
4  const QUERY_PARAMETER = 'tx_windsolrliveresults_solrliveresults[queryString]';
5  const INITIALIZED_ATTRIBUTE = 'data-windsolrliveresults-initialized';
6  const HIDDEN_CLASS_NAME = 'wind-solr-live-results-hidden';
7  const VISIBLE_CLASS_NAME = 'is-visible';
8  const DEFAULT_CONFIG = Object.freeze({
9    queryParameter: QUERY_PARAMETER,
10    delay: 250,
11    minimumCharacters: 1,
12    inputSelector: '.tx-windsolrliveresults',
13    fallbackInputSelector: 'input[name="q"]',
14    searchContainerSelector: 'div.search',
15    searchResultsBoxSelector: '.search-results-box',
16    placeholderSelector: 'p',
17    listClassName: 'wind-solr-live-results-list',
18    listRole: '',
19    itemRole: '',
20    itemSelectedClassName: 'is-selected',
21    contentClassName: 'wind-solr-live-results-content',
22    resultItemIdPrefix: 'wind-solr-live-result-',
23    resultWrapperTag: 'article',
24    resultTitleTag: 'h2',
25    resultContentTag: 'p',
26    resultLinkSelector: 'a[href]',
27    excludeHomepageResults: true,
28    placeholderText: '',
29    placeholderClassName: 'wind-solr-live-results-placeholder',
30    showResultsFoundFor: false,
31    resultsFoundForTemplate: '{count} results found for "{query}"',
32    resultsFoundForClassName: 'wind-solr-live-results-found-for',
33    hideResultsFoundForVisually: true,
34    visuallyHiddenClassName: 'visually-hidden',
35    showMoreResultsButton: true,
36    moreResultsText: 'More results',
37    moreResultsClassName: 'wind-solr-live-results-more-results',
38    moreResultsQueryParameter: '',
39    showNoResultsMessage: true,
40    noResultsText: 'No results found for "{query}"',
41    noResultsClassName: 'wind-solr-live-results-no-results',
42  });
43  let listenersInitialized = false;
44  let listInstanceCount = 0;
45
46  function toText(rawHtml) {
47    const container = document.createElement('div');
48    container.innerHTML = String(rawHtml || '');
49    return container.textContent || '';
50  }
51
52  function getDataOption(form, optionName, fallbackValue) {
53    const attributeName = 'data-windsolrliveresults-' + optionName;
54    const rawValue = form.getAttribute(attributeName);
55    if (rawValue === null) {
56      return fallbackValue;
57    }
58
59    if (typeof fallbackValue === 'boolean') {
60      // Fluid may render a false boolean as an empty attribute value.
61      if (rawValue === '') {
62        return false;
63      }
64
65      return ['1', 'true', 'yes', 'on'].includes(rawValue.toLowerCase());
66    }
67
68    if (rawValue === '') {
69      return fallbackValue;
70    }
71
72    if (typeof fallbackValue === 'number') {
73      const parsedValue = Number(rawValue);
74      return Number.isFinite(parsedValue) ? parsedValue : fallbackValue;
75    }
76
77    return rawValue;
78  }
79
80  function normalizeTagName(tagName, fallbackTagName) {
81    if (typeof tagName !== 'string' || !/^[a-z][a-z0-9-]*$/i.test(tagName)) {
82      return fallbackTagName;
83    }
84
85    return tagName.toLowerCase();
86  }
87
88  function getFormConfig(form) {
89    return {
90      queryParameter: getDataOption(form, 'queryparameter', DEFAULT_CONFIG.queryParameter),
91      delay: Math.max(0, getDataOption(form, 'delay', DEFAULT_CONFIG.delay)),
92      minimumCharacters: Math.max(1, getDataOption(form, 'howmanycharacters', DEFAULT_CONFIG.minimumCharacters)),
93      inputSelector: getDataOption(form, 'inputselector', DEFAULT_CONFIG.inputSelector),
94      fallbackInputSelector: getDataOption(form, 'fallbackinputselector', DEFAULT_CONFIG.fallbackInputSelector),
95      searchContainerSelector: getDataOption(form, 'searchcontainerselector', DEFAULT_CONFIG.searchContainerSelector),
96      searchResultsBoxSelector: getDataOption(form, 'searchresultsboxselector', DEFAULT_CONFIG.searchResultsBoxSelector),
97      placeholderSelector: getDataOption(form, 'placeholderselector', DEFAULT_CONFIG.placeholderSelector),
98      listClassName: getDataOption(form, 'listclassname', DEFAULT_CONFIG.listClassName),
99      listRole: getDataOption(form, 'listrole', DEFAULT_CONFIG.listRole),
100      itemRole: getDataOption(form, 'itemrole', DEFAULT_CONFIG.itemRole),
101      itemSelectedClassName: getDataOption(form, 'itemselectedclassname', DEFAULT_CONFIG.itemSelectedClassName),
102      contentClassName: getDataOption(form, 'contentclassname', DEFAULT_CONFIG.contentClassName),
103      resultItemIdPrefix: getDataOption(form, 'resultitemidprefix', DEFAULT_CONFIG.resultItemIdPrefix),
104      resultWrapperTag: normalizeTagName(getDataOption(form, 'resultwrappertag', DEFAULT_CONFIG.resultWrapperTag), DEFAULT_CONFIG.resultWrapperTag),
105      resultTitleTag: normalizeTagName(getDataOption(form, 'resulttitletag', DEFAULT_CONFIG.resultTitleTag), DEFAULT_CONFIG.resultTitleTag),
106      resultContentTag: normalizeTagName(getDataOption(form, 'resultcontenttag', DEFAULT_CONFIG.resultContentTag), DEFAULT_CONFIG.resultContentTag),
107      resultLinkSelector: getDataOption(form, 'resultlinkselector', DEFAULT_CONFIG.resultLinkSelector),
108      excludeHomepageResults: getDataOption(form, 'excludehomepageresults', DEFAULT_CONFIG.excludeHomepageResults),
109      placeholderText: getDataOption(form, 'placeholdertext', DEFAULT_CONFIG.placeholderText),
110      placeholderClassName: getDataOption(form, 'placeholderclassname', DEFAULT_CONFIG.placeholderClassName),
111      showResultsFoundFor: getDataOption(form, 'showresultsfoundfor', DEFAULT_CONFIG.showResultsFoundFor),
112      resultsFoundForTemplate: getDataOption(form, 'resultsfoundfortemplate', DEFAULT_CONFIG.resultsFoundForTemplate),
113      resultsFoundForClassName: getDataOption(form, 'resultsfoundforclassname', DEFAULT_CONFIG.resultsFoundForClassName),
114      hideResultsFoundForVisually: getDataOption(form, 'hideresultsfoundforvisually', DEFAULT_CONFIG.hideResultsFoundForVisually),
115      visuallyHiddenClassName: getDataOption(form, 'visuallyhiddenclassname', DEFAULT_CONFIG.visuallyHiddenClassName),
116      showMoreResultsButton: getDataOption(form, 'showmoreresultsbutton', DEFAULT_CONFIG.showMoreResultsButton),
117      moreResultsText: getDataOption(form, 'moreresultstext', DEFAULT_CONFIG.moreResultsText),
118      moreResultsClassName: getDataOption(form, 'moreresultsclassname', DEFAULT_CONFIG.moreResultsClassName),
119      moreResultsQueryParameter: getDataOption(form, 'moreresultsqueryparameter', DEFAULT_CONFIG.moreResultsQueryParameter),
120      showNoResultsMessage: getDataOption(form, 'shownoresultsmessage', DEFAULT_CONFIG.showNoResultsMessage),
121      noResultsText: getDataOption(form, 'noresultstext', DEFAULT_CONFIG.noResultsText),
122      noResultsClassName: getDataOption(form, 'noresultsclassname', DEFAULT_CONFIG.noResultsClassName),
123    };
124  }
125
126  function clearResults(container) {
127    container.innerHTML = '';
128    container.hidden = true;
129  }
130
131  function buildResultListItem(resultDocument, index, config, itemIdPrefix) {
132    const item = window.document.createElement('li');
133    item.setAttribute('role', config.itemRole !== '' ? config.itemRole : 'option');
134
135    const article = window.document.createElement(config.resultWrapperTag);
136    const link = window.document.createElement('a');
137    link.id = itemIdPrefix + String(index + 1);
138    const title = toText(resultDocument.title) || resultDocument.url || resultDocument.link || '';
139    const content = toText(resultDocument.content);
140    const resultUrl = resultDocument.url || resultDocument.link || '#';
141
142    link.href = resultUrl;
143
144    const titleElement = window.document.createElement(config.resultTitleTag);
145    titleElement.textContent = title;
146    link.append(titleElement);
147
148    if (content !== '') {
149      const contentElement = window.document.createElement(config.resultContentTag);
150      contentElement.className = config.contentClassName;
151      contentElement.textContent = content;
152      link.append(contentElement);
153    }
154
155    article.append(link);
156    item.append(article);
157
158    return item;
159  }
160
161  function isHomepageResultUrl(resultUrl) {
162    if (typeof resultUrl !== 'string' || resultUrl.trim() === '') {
163      return false;
164    }
165
166    try {
167      const parsedUrl = new URL(resultUrl, window.location.origin);
168      return parsedUrl.origin === window.location.origin && parsedUrl.pathname === '/';
169    } catch (error) {
170      return false;
171    }
172  }
173
174  function replaceTemplateTokens(template, replacements) {
175    let output = String(template || '');
176
177    Object.keys(replacements).forEach((tokenName) => {
178      output = output.split('{' + tokenName + '}').join(String(replacements[tokenName]));
179    });
180
181    return output;
182  }
183
184  function applyClassNames(element, classNames) {
185    if (!element || typeof classNames !== 'string') {
186      return;
187    }
188
189    classNames
190      .split(/\s+/)
191      .filter((className) => className !== '')
192      .forEach((className) => element.classList.add(className));
193  }
194
195  function resolveSpinnerUrl() {
196    const currentScript = document.currentScript;
197    if (currentScript && typeof currentScript.src === 'string' && currentScript.src !== '') {
198      return currentScript.src.replace(/\/JavaScript\/liveresults\.js(?:\?.*)?$/, '/Images/spinner.svg');
199    }
200
201    const fallbackScript = Array.from(document.querySelectorAll('script[src]'))
202      .find((scriptElement) => /\/JavaScript\/liveresults\.js(?:\?.*)?$/.test(scriptElement.src));
203    if (fallbackScript) {
204      return fallbackScript.src.replace(/\/JavaScript\/liveresults\.js(?:\?.*)?$/, '/Images/spinner.svg');
205    }
206
207    return '/Images/spinner.svg';
208  }
209
210  function initVoiceCommand(form, searchInput, searchContainer) {
211    const startBtn = form.querySelector('[data-windsolrliveresults-voicebutton]');
212    if (!startBtn || startBtn.hasAttribute('data-windsolrliveresults-voice-initialized')) {
213      return;
214    }
215
216    const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
217    if (!SpeechRecognition) {
218      return;
219    }
220
221    const errorContainer = searchContainer
222      ? searchContainer.querySelector('[data-windsolrliveresults-voiceerror]')
223      : null;
224    const hideVoiceError = () => {
225      if (errorContainer) {
226        errorContainer.classList.add('d-none');
227      }
228    };
229    const showVoiceError = () => {
230      if (errorContainer) {
231        errorContainer.classList.remove('d-none');
232      }
233    };
234    const recognition = new SpeechRecognition();
235    recognition.lang = 'nl-NL';
236
237    recognition.onresult = (event) => {
238      let transcript = event.results[0][0].transcript;
239
240      if (transcript[transcript.length - 1] === '.') {
241        transcript = transcript.slice(0, -1);
242      }
243
244      searchInput.value = `${transcript}`;
245      searchInput.dispatchEvent(new Event('input', { bubbles: true }));
246
247      // Re-add focus to show search suggestions.
248      searchInput.blur();
249      searchInput.focus();
250    };
251
252    recognition.onerror = () => {
253      showVoiceError();
254    };
255
256    startBtn.addEventListener('click', () => {
257      hideVoiceError();
258      recognition.start();
259    });
260
261    form.addEventListener('keydown', (event) => {
262      if (event.key === 'Escape') {
263        hideVoiceError();
264      }
265    });
266
267    form.addEventListener('focusin', (event) => {
268      if (!errorContainer || errorContainer.classList.contains('d-none')) {
269        return;
270      }
271
272      if (!errorContainer.contains(event.target)) {
273        hideVoiceError();
274      }
275    });
276
277    form.addEventListener('focusout', () => {
278      window.setTimeout(() => {
279        if (!errorContainer || errorContainer.classList.contains('d-none')) {
280          return;
281        }
282
283        const activeElement = document.activeElement;
284        if (!activeElement) {
285          hideVoiceError();
286          return;
287        }
288
289        const focusWithinSearch = searchContainer
290          ? searchContainer.contains(activeElement)
291          : form.contains(activeElement);
292        if (!focusWithinSearch) {
293          hideVoiceError();
294        }
295      }, 0);
296    });
297
298    if (searchContainer) {
299      searchContainer.addEventListener('pointerdown', (event) => {
300        if (!errorContainer || errorContainer.classList.contains('d-none')) {
301          return;
302        }
303
304        if (!errorContainer.contains(event.target)) {
305          hideVoiceError();
306        }
307      });
308    }
309
310    startBtn.setAttribute('data-windsolrliveresults-voice-initialized', '1');
311  }
312
313  function createAutocompleteInstance(form) {
314    const config = getFormConfig(form);
315    const searchInput = form.querySelector(config.inputSelector) || form.querySelector(config.fallbackInputSelector);
316    if (!searchInput) {
317      return false;
318    }
319
320    const serviceUrl = form.getAttribute('data-windsolrliveresults') || '';
321    if (!serviceUrl) {
322      return false;
323    }
324
325    form.setAttribute('autocomplete', 'off');
326    const searchContainer = form.closest(config.searchContainerSelector);
327    const searchResultsBox = searchContainer ? searchContainer.querySelector(config.searchResultsBoxSelector) : null;
328    const focusScope = searchContainer || form;
329
330    initVoiceCommand(form, searchInput, searchContainer);
331    const shouldDisplaySearchUi = () => {
332      const activeElement = document.activeElement;
333      const isInputFocused = activeElement === searchInput;
334      const hasInputValue = searchInput.value.trim() !== '';
335      const isWithinSearch = searchContainer
336        ? searchContainer.contains(activeElement)
337        : form.contains(activeElement);
338
339      return isInputFocused || (hasInputValue && isWithinSearch);
340    };
341
342    const setExpandedState = (isExpanded) => {
343      searchInput.setAttribute('aria-expanded', isExpanded ? 'true' : 'false');
344    };
345
346    const hasVisibleResults = () => !resultsList.hidden && resultsList.childElementCount > 0;
347
348    const updateSearchResultsBoxVisibility = () => {
349      if (!searchResultsBox) {
350        setExpandedState(hasVisibleResults());
351        return;
352      }
353
354      const isVisible = shouldDisplaySearchUi();
355      searchResultsBox.hidden = !isVisible;
356      searchResultsBox.classList.toggle(VISIBLE_CLASS_NAME, isVisible);
357      setExpandedState(isVisible && hasVisibleResults());
358    };
359
360    let abortController = null;
361    let debounceTimer = null;
362    let selectedIndex = -1;
363    let pendingRequests = 0;
364    listInstanceCount += 1;
365    const resultItemIdPrefix = config.resultItemIdPrefix + String(listInstanceCount) + '-';
366    const resultsListId = 'wind-solr-live-results-list-' + String(listInstanceCount);
367    const resultsList = document.createElement('ul');
368    resultsList.id = resultsListId;
369    resultsList.className = config.listClassName;
370    resultsList.setAttribute('role', config.listRole !== '' ? config.listRole : 'listbox');
371    resultsList.setAttribute('data-windsolrliveresults-list', '1');
372    clearResults(resultsList);
373
374    searchInput.setAttribute('role', 'combobox');
375    searchInput.setAttribute('aria-autocomplete', 'list');
376    searchInput.setAttribute('aria-haspopup', 'listbox');
377    searchInput.setAttribute('aria-controls', resultsListId);
378    searchInput.setAttribute('aria-expanded', 'false');
379
380    if (searchResultsBox) {
381      searchResultsBox.append(resultsList);
382      updateSearchResultsBoxVisibility();
383    }
384
385    let placeholderElement = searchResultsBox
386      ? searchResultsBox.querySelector('[data-windsolrliveresults-placeholder]')
387      : null;
388    if (!placeholderElement && searchResultsBox) {
389      placeholderElement = Array.from(searchResultsBox.querySelectorAll(config.placeholderSelector))
390        .find((element) => !resultsList.contains(element)) || null;
391      if (placeholderElement) {
392        placeholderElement.setAttribute('data-windsolrliveresults-placeholder', '1');
393      }
394    }
395    if (placeholderElement) {
396      applyClassNames(placeholderElement, config.placeholderClassName);
397      if (config.placeholderText !== '') {
398        placeholderElement.textContent = config.placeholderText;
399      }
400    }
401
402    let resultsFoundForElement = null;
403    if (searchResultsBox) {
404      resultsFoundForElement = searchResultsBox.querySelector('[data-windsolrliveresults-resultsfoundfor]');
405      if (!resultsFoundForElement) {
406        resultsFoundForElement = document.createElement('p');
407        resultsFoundForElement.setAttribute('data-windsolrliveresults-resultsfoundfor', '1');
408        searchResultsBox.insertBefore(resultsFoundForElement, resultsList);
409      }
410
411      resultsFoundForElement.setAttribute('aria-hidden', 'true');
412      applyClassNames(resultsFoundForElement, config.resultsFoundForClassName);
413      if (config.hideResultsFoundForVisually && config.visuallyHiddenClassName !== '') {
414        applyClassNames(resultsFoundForElement, config.visuallyHiddenClassName);
415      }
416      resultsFoundForElement.classList.add(HIDDEN_CLASS_NAME);
417    }
418
419    let noResultsElement = null;
420    if (searchResultsBox) {
421      noResultsElement = searchResultsBox.querySelector('[data-windsolrliveresults-noresults]');
422      if (!noResultsElement) {
423        noResultsElement = document.createElement('p');
424        noResultsElement.setAttribute('data-windsolrliveresults-noresults', '1');
425        searchResultsBox.insertBefore(noResultsElement, resultsList);
426      }
427
428      noResultsElement.setAttribute('aria-hidden', 'true');
429      applyClassNames(noResultsElement, config.noResultsClassName);
430      noResultsElement.hidden = true;
431    }
432
433    let liveRegionElement = focusScope.querySelector('[data-windsolrliveresults-status]');
434    if (!liveRegionElement) {
435      liveRegionElement = document.createElement('p');
436      liveRegionElement.setAttribute('data-windsolrliveresults-status', '1');
437      focusScope.insertBefore(liveRegionElement, searchResultsBox || null);
438    }
439
440    liveRegionElement.setAttribute('role', 'status');
441    if (config.visuallyHiddenClassName !== '') {
442      applyClassNames(liveRegionElement, config.visuallyHiddenClassName);
443    }
444
445    let moreResultsElement = null;
446    if (searchResultsBox) {
447      moreResultsElement = searchResultsBox.querySelector('[data-windsolrliveresults-moreresults]');
448      if (!moreResultsElement) {
449        moreResultsElement = document.createElement('a');
450        moreResultsElement.setAttribute('data-windsolrliveresults-moreresults', '1');
451        moreResultsElement.textContent = config.moreResultsText;
452        searchResultsBox.append(moreResultsElement);
453      }
454
455      applyClassNames(moreResultsElement, config.moreResultsClassName);
456      moreResultsElement.hidden = true;
457    }
458
459    let spinnerElement = null;
460    if (searchResultsBox) {
461      spinnerElement = searchResultsBox.querySelector('[data-windsolrliveresults-spinner]');
462      if (!spinnerElement) {
463        spinnerElement = document.createElement('img');
464        spinnerElement.setAttribute('data-windsolrliveresults-spinner', '1');
465        spinnerElement.src = resolveSpinnerUrl();
466        spinnerElement.alt = '';
467        spinnerElement.setAttribute('aria-hidden', 'true');
468        searchResultsBox.insertBefore(spinnerElement, resultsList);
469      }
470
471      spinnerElement.hidden = true;
472    }
473
474    const setPlaceholderVisible = (isVisible) => {
475      if (!placeholderElement) {
476        return;
477      }
478
479      placeholderElement.classList.toggle(HIDDEN_CLASS_NAME, !(isVisible && shouldDisplaySearchUi()));
480    };
481    const syncPlaceholderVisibility = () => {
482      const hasInputValue = searchInput.value.trim() !== '';
483      const hasResults = !resultsList.hidden && resultsList.childElementCount > 0;
484      setPlaceholderVisible(!hasInputValue && !hasResults);
485    };
486
487    const setResultsFoundFor = (query, resultCount) => {
488      if (!resultsFoundForElement) {
489        return;
490      }
491
492      const setResultsFoundForVisibility = (isVisible) => {
493        if (resultsFoundForElement) {
494          resultsFoundForElement.classList.toggle(HIDDEN_CLASS_NAME, !isVisible);
495        }
496      };
497
498      const normalizedQuery = String(query || '').trim();
499      if (normalizedQuery === '' || resultCount <= 0) {
500        setResultsFoundForVisibility(false);
501        if (resultsFoundForElement) {
502          resultsFoundForElement.textContent = '';
503        }
504        liveRegionElement.textContent = '';
505        return;
506      }
507
508      const text = replaceTemplateTokens(config.resultsFoundForTemplate, {
509        count: resultCount,
510        query: normalizedQuery,
511      });
512
513      liveRegionElement.textContent = text;
514
515      if (config.showResultsFoundFor && resultsFoundForElement) {
516        resultsFoundForElement.textContent = text;
517        setResultsFoundForVisibility(true);
518      }
519    };
520
521    const setMoreResultsButton = (query, hasResults) => {
522      if (!moreResultsElement) {
523        return;
524      }
525
526      const normalizedQuery = String(query || '').trim();
527      if (!config.showMoreResultsButton || !hasResults || normalizedQuery === '') {
528        moreResultsElement.hidden = true;
529        return;
530      }
531
532      const queryParameter = config.moreResultsQueryParameter !== ''
533        ? config.moreResultsQueryParameter
534        : (searchInput.getAttribute('name') || 'q');
535      const actionUrl = new URL(form.getAttribute('action') || window.location.href, window.location.origin);
536      actionUrl.searchParams.set(queryParameter, normalizedQuery);
537
538      moreResultsElement.textContent = config.moreResultsText;
539      moreResultsElement.href = actionUrl.pathname + actionUrl.search + actionUrl.hash;
540      moreResultsElement.hidden = false;
541    };
542
543    const setNoResultsMessage = (query, showMessage) => {
544      if (!noResultsElement) {
545        return;
546      }
547
548      const normalizedQuery = String(query || '').trim();
549      if (!config.showNoResultsMessage || !showMessage || normalizedQuery.length < config.minimumCharacters) {
550        noResultsElement.hidden = true;
551        return;
552      }
553
554      const noResultsText = replaceTemplateTokens(config.noResultsText, {
555        query: normalizedQuery,
556      });
557      noResultsElement.textContent = noResultsText;
558      noResultsElement.hidden = false;
559      liveRegionElement.textContent = noResultsText;
560    };
561
562    const updateSearchResultsBoxFocusTarget = () => {
563      if (!searchResultsBox) {
564        return;
565      }
566
567      searchResultsBox.setAttribute('tabindex', '-1');
568    };
569
570    const setLoadingState = (isLoading) => {
571      form.classList.toggle('loading', isLoading);
572      form.setAttribute('aria-busy', isLoading ? 'true' : 'false');
573      if (searchResultsBox) {
574        searchResultsBox.setAttribute('data-windsolrliveresults-loading', isLoading ? '1' : '0');
575      }
576      if (spinnerElement) {
577        spinnerElement.hidden = !isLoading;
578      }
579      if (isLoading) {
580        resultsList.hidden = true;
581        setPlaceholderVisible(false);
582        if (resultsFoundForElement) {
583          resultsFoundForElement.classList.add(HIDDEN_CLASS_NAME);
584          resultsFoundForElement.textContent = '';
585        }
586        liveRegionElement.textContent = '';
587        if (noResultsElement) {
588          noResultsElement.hidden = true;
589        }
590        if (moreResultsElement) {
591          moreResultsElement.hidden = true;
592        }
593      }
594      updateSearchResultsBoxFocusTarget();
595    };
596
597    const resetAutocompleteState = (clearInputValue) => {
598      if (abortController) {
599        abortController.abort();
600      }
601      if (debounceTimer) {
602        window.clearTimeout(debounceTimer);
603      }
604
605      pendingRequests = 0;
606      selectedIndex = -1;
607
608      if (clearInputValue) {
609        searchInput.value = '';
610      }
611
612      setLoadingState(false);
613      clearResults(resultsList);
614      setResultsFoundFor('', 0);
615      setMoreResultsButton('', false);
616      setNoResultsMessage('', false);
617      setPlaceholderVisible(true);
618      updateSearchResultsBoxVisibility();
619      updateSearchResultsBoxFocusTarget();
620    };
621
622    resultsList.windSolrReset = () => {
623      resetAutocompleteState(true);
624    };
625
626    const updateSelection = () => {
627      const items = Array.from(resultsList.querySelectorAll('li'));
628      items.forEach((item, index) => {
629        const isSelected = index === selectedIndex;
630        item.classList.toggle(config.itemSelectedClassName, isSelected);
631      });
632    };
633
634    const renderDocuments = (documents, totalFound) => {
635      resultsList.innerHTML = '';
636      selectedIndex = -1;
637
638      if (!documents.length) {
639        clearResults(resultsList);
640        setPlaceholderVisible(searchInput.value === '');
641        setResultsFoundFor(searchInput.value, 0);
642        setMoreResultsButton(searchInput.value, false);
643        setNoResultsMessage(searchInput.value, true);
644        updateSearchResultsBoxFocusTarget();
645        return;
646      }
647
648      const filteredDocuments = config.excludeHomepageResults
649        ? documents.filter((resultDocument) => !isHomepageResultUrl(resultDocument.url || resultDocument.link || ''))
650        : documents;
651
652      if (!filteredDocuments.length) {
653        clearResults(resultsList);
654        setPlaceholderVisible(searchInput.value === '');
655        setResultsFoundFor(searchInput.value, 0);
656        setMoreResultsButton(searchInput.value, false);
657        setNoResultsMessage(searchInput.value, true);
658        updateSearchResultsBoxFocusTarget();
659        return;
660      }
661
662      const listItems = filteredDocuments.map(
663        (resultDocument, index) => buildResultListItem(resultDocument, index, config, resultItemIdPrefix)
664      );
665      const hasMoreResults = Number.isFinite(totalFound) ? totalFound > documents.length : false;
666
667      listItems.forEach((item) => resultsList.append(item));
668      resultsList.hidden = false;
669      setPlaceholderVisible(false);
670      setResultsFoundFor(searchInput.value, filteredDocuments.length);
671      setMoreResultsButton(searchInput.value, hasMoreResults);
672      setNoResultsMessage(searchInput.value, false);
673      updateSearchResultsBoxVisibility();
674      updateSearchResultsBoxFocusTarget();
675    };
676
677    const fetchResults = (query) => {
678      if (abortController) {
679        abortController.abort();
680      }
681
682      abortController = new AbortController();
683      pendingRequests += 1;
684      setLoadingState(true);
685      const requestUrl = new URL(serviceUrl, window.location.origin);
686      requestUrl.searchParams.set(config.queryParameter, query);
687
688      fetch(requestUrl.toString(), {
689        method: 'GET',
690        headers: {
691          Accept: 'application/json',
692        },
693        signal: abortController.signal,
694      })
695        .then((response) => {
696          if (!response.ok) {
697            throw new Error('Live results request failed');
698          }
699
700          return response.json();
701        })
702        .then((response) => {
703          const documents = Array.isArray(response.documents) ? response.documents : [];
704          const totalFound = Number(response.found);
705          renderDocuments(documents, Number.isFinite(totalFound) ? totalFound : documents.length);
706        })
707        .catch((error) => {
708          if (error.name === 'AbortError') {
709            return;
710          }
711
712          renderDocuments([]);
713        })
714        .finally(() => {
715          pendingRequests = Math.max(0, pendingRequests - 1);
716          setLoadingState(pendingRequests > 0);
717        });
718    };
719
720    searchInput.addEventListener('input', () => {
721      const query = searchInput.value.trim();
722      updateSearchResultsBoxVisibility();
723
724      if (debounceTimer) {
725        window.clearTimeout(debounceTimer);
726      }
727
728      if (query.length < config.minimumCharacters) {
729        if (abortController) {
730          abortController.abort();
731        }
732        pendingRequests = 0;
733        setLoadingState(false);
734        renderDocuments([]);
735        return;
736      }
737
738      debounceTimer = window.setTimeout(() => {
739        fetchResults(query);
740      }, config.delay);
741    });
742
743    focusScope.addEventListener('focusin', () => {
744      updateSearchResultsBoxVisibility();
745      syncPlaceholderVisibility();
746      updateSearchResultsBoxFocusTarget();
747    });
748
749    focusScope.addEventListener('focusout', () => {
750      window.setTimeout(() => {
751        updateSearchResultsBoxVisibility();
752        syncPlaceholderVisibility();
753        updateSearchResultsBoxFocusTarget();
754      }, 0);
755    });
756
757    if (searchResultsBox) {
758      searchResultsBox.setAttribute('tabindex', '-1');
759
760      searchResultsBox.addEventListener('mousedown', (event) => {
761        const clickedElement = event.target instanceof Element ? event.target : null;
762        if (!clickedElement) {
763          return;
764        }
765
766        const focusableTarget = clickedElement.closest(
767          'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
768        );
769        if (focusableTarget) {
770          return;
771        }
772
773        searchResultsBox.focus({ preventScroll: true });
774      });
775    }
776
777    searchInput.addEventListener('keydown', (event) => {
778      if (event.key === 'Escape') {
779        event.preventDefault();
780        resetAutocompleteState(true);
781        return;
782      }
783
784      const items = Array.from(resultsList.querySelectorAll('li'));
785      if (!items.length) {
786        return;
787      }
788
789      if (event.key === 'ArrowDown') {
790        event.preventDefault();
791        selectedIndex = selectedIndex < items.length - 1 ? selectedIndex + 1 : 0;
792        updateSelection();
793        return;
794      }
795
796      if (event.key === 'ArrowUp') {
797        event.preventDefault();
798        selectedIndex = selectedIndex > 0 ? selectedIndex - 1 : items.length - 1;
799        updateSelection();
800        return;
801      }
802
803      if (event.key === 'Enter' && selectedIndex >= 0) {
804        const link = items[selectedIndex].querySelector(config.resultLinkSelector);
805        if (link) {
806          event.preventDefault();
807          window.location.href = link.getAttribute('href') || '';
808        }
809      }
810    });
811
812    form.addEventListener('submit', (event) => {
813      if (searchInput.value === '') {
814        event.preventDefault();
815        searchInput.focus();
816      }
817    });
818
819    const initialQuery = searchInput.value.trim();
820    if (initialQuery.length >= config.minimumCharacters) {
821      fetchResults(initialQuery);
822    }
823
824    return true;
825  }
826
827  function setupGlobalListeners() {
828    if (listenersInitialized) {
829      return;
830    }
831
832    listenersInitialized = true;
833
834    document.body.addEventListener('keyup', (event) => {
835      if (event.key === 'Escape') {
836        document.querySelectorAll('[data-windsolrliveresults-list]').forEach((list) => {
837          if (typeof list.windSolrReset === 'function') {
838            list.windSolrReset();
839          } else {
840            list.innerHTML = '';
841            list.hidden = true;
842          }
843        });
844      }
845    });
846
847    document.body.addEventListener('tx_solr_updated', () => {
848      window.windSolrLiveResults.init();
849    });
850
851    document.addEventListener('tx_solr_updated', () => {
852      window.windSolrLiveResults.init();
853    });
854  }
855
856  function WindSolrLiveResults() {
857    this.init = function () {
858      setupGlobalListeners();
859
860      document.querySelectorAll('form[data-windsolrliveresults]').forEach((form) => {
861        if (form.hasAttribute(INITIALIZED_ATTRIBUTE)) {
862          return;
863        }
864
865        if (createAutocompleteInstance(form)) {
866          form.setAttribute(INITIALIZED_ATTRIBUTE, '1');
867        }
868      });
869    };
870  }
871
872  window.WindSolrLiveResults = WindSolrLiveResults;
873
874  document.addEventListener('DOMContentLoaded', () => {
875    window.windSolrLiveResults = new WindSolrLiveResults();
876    window.windSolrLiveResults.init();
877  });
878}());

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.