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.