/** Shopify CDN: Minification failed Line 16:0 Transforming class syntax to the configured target environment ("es5") is not supported yet Line 17:13 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 29:21 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 38:10 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 42:10 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 44:4 Transforming const to the configured target environment ("es5") is not supported yet Line 64:14 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 68:13 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 78:9 Transforming object literal extensions to the configured target environment ("es5") is not supported yet Line 79:4 Transforming const to the configured target environment ("es5") is not supported yet ... and 31 more hidden warnings **/ class PredictiveSearch extends SearchForm { constructor() { super(); this.cachedResults = {}; this.predictiveSearchResults = this.querySelector('[data-predictive-search]'); this.allPredictiveSearchInstances = document.querySelectorAll('predictive-search'); this.isOpen = false; this.abortController = new AbortController(); this.searchTerm = ''; this.setupEventListeners(); } setupEventListeners() { this.input.form.addEventListener('submit', this.onFormSubmit.bind(this)); this.input.addEventListener('focus', this.onFocus.bind(this)); this.addEventListener('focusout', this.onFocusOut.bind(this)); this.addEventListener('keyup', this.onKeyup.bind(this)); this.addEventListener('keydown', this.onKeydown.bind(this)); } getQuery() { return this.input.value.trim(); } onChange() { super.onChange(); const newSearchTerm = this.getQuery(); if (!this.searchTerm || !newSearchTerm.startsWith(this.searchTerm)) { // Remove the results when they are no longer relevant for the new search term // so they don't show up when the dropdown opens again this.querySelector('#predictive-search-results-groups-wrapper')?.remove(); } // Update the term asap, don't wait for the predictive search query to finish loading this.updateSearchForTerm(this.searchTerm, newSearchTerm); this.searchTerm = newSearchTerm; if (!this.searchTerm.length) { this.close(true); return; } this.getSearchResults(this.searchTerm); } onFormSubmit(event) { if (!this.getQuery().length || this.querySelector('[aria-selected="true"] a')) event.preventDefault(); } onFormReset(event) { super.onFormReset(event); if (super.shouldResetForm()) { this.searchTerm = ''; this.abortController.abort(); this.abortController = new AbortController(); this.closeResults(true); } } onFocus() { const currentSearchTerm = this.getQuery(); if (!currentSearchTerm.length) return; if (this.searchTerm !== currentSearchTerm) { // Search term was changed from other search input, treat it as a user change this.onChange(); } else if (this.getAttribute('results') === 'true') { this.open(); } else { this.getSearchResults(this.searchTerm); } } onFocusOut() { setTimeout(() => { if (!this.contains(document.activeElement)) this.close(); }); } onKeyup(event) { if (!this.getQuery().length) this.close(true); event.preventDefault(); switch (event.code) { case 'ArrowUp': this.switchOption('up'); break; case 'ArrowDown': this.switchOption('down'); break; case 'Enter': this.selectOption(); break; } } onKeydown(event) { // Prevent the cursor from moving in the input when using the up and down arrow keys if (event.code === 'ArrowUp' || event.code === 'ArrowDown') { event.preventDefault(); } } updateSearchForTerm(previousTerm, newTerm) { const searchForTextElement = this.querySelector('[data-predictive-search-search-for-text]'); const currentButtonText = searchForTextElement?.innerText; if (currentButtonText) { if (currentButtonText.match(new RegExp(previousTerm, 'g')).length > 1) { // The new term matches part of the button text and not just the search term, do not replace to avoid mistakes return; } const newButtonText = currentButtonText.replace(previousTerm, newTerm); searchForTextElement.innerText = newButtonText; } } switchOption(direction) { if (!this.getAttribute('open')) return; const moveUp = direction === 'up'; const selectedElement = this.querySelector('[aria-selected="true"]'); // Filter out hidden elements (duplicated page and article resources) thanks // to this https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent const allVisibleElements = Array.from(this.querySelectorAll('li, button.predictive-search__item')).filter( (element) => element.offsetParent !== null ); let activeElementIndex = 0; if (moveUp && !selectedElement) return; let selectedElementIndex = -1; let i = 0; while (selectedElementIndex === -1 && i <= allVisibleElements.length) { if (allVisibleElements[i] === selectedElement) { selectedElementIndex = i; } i++; } this.statusElement.textContent = ''; if (!moveUp && selectedElement) { activeElementIndex = selectedElementIndex === allVisibleElements.length - 1 ? 0 : selectedElementIndex + 1; } else if (moveUp) { activeElementIndex = selectedElementIndex === 0 ? allVisibleElements.length - 1 : selectedElementIndex - 1; } if (activeElementIndex === selectedElementIndex) return; const activeElement = allVisibleElements[activeElementIndex]; activeElement.setAttribute('aria-selected', true); if (selectedElement) selectedElement.setAttribute('aria-selected', false); this.input.setAttribute('aria-activedescendant', activeElement.id); } selectOption() { const selectedOption = this.querySelector('[aria-selected="true"] a, button[aria-selected="true"]'); if (selectedOption) selectedOption.click(); } getSearchResults(searchTerm) { const queryKey = searchTerm.replace(' ', '-').toLowerCase(); this.setLiveRegionLoadingState(); if (this.cachedResults[queryKey]) { this.renderSearchResults(this.cachedResults[queryKey]); return; } fetch(`${routes.predictive_search_url}?q=${encodeURIComponent(searchTerm)}§ion_id=predictive-search`, { signal: this.abortController.signal, }) .then((response) => { if (!response.ok) { var error = new Error(response.status); this.close(); throw error; } return response.text(); }) .then((text) => { const resultsMarkup = new DOMParser() .parseFromString(text, 'text/html') .querySelector('#shopify-section-predictive-search').innerHTML; // Save bandwidth keeping the cache in all instances synced this.allPredictiveSearchInstances.forEach((predictiveSearchInstance) => { predictiveSearchInstance.cachedResults[queryKey] = resultsMarkup; }); this.renderSearchResults(resultsMarkup); }) .catch((error) => { if (error?.code === 20) { // Code 20 means the call was aborted return; } this.close(); throw error; }); } setLiveRegionLoadingState() { this.statusElement = this.statusElement || this.querySelector('.predictive-search-status'); this.loadingText = this.loadingText || this.getAttribute('data-loading-text'); this.setLiveRegionText(this.loadingText); this.setAttribute('loading', true); } setLiveRegionText(statusText) { this.statusElement.setAttribute('aria-hidden', 'false'); this.statusElement.textContent = statusText; setTimeout(() => { this.statusElement.setAttribute('aria-hidden', 'true'); }, 1000); } renderSearchResults(resultsMarkup) { this.predictiveSearchResults.innerHTML = resultsMarkup; this.setAttribute('results', true); this.setLiveRegionResults(); this.open(); } setLiveRegionResults() { this.removeAttribute('loading'); this.setLiveRegionText(this.querySelector('[data-predictive-search-live-region-count-value]').textContent); } getResultsMaxHeight() { this.resultsMaxHeight = window.innerHeight - document.querySelector('.section-header').getBoundingClientRect().bottom; return this.resultsMaxHeight; } open() { this.predictiveSearchResults.style.maxHeight = this.resultsMaxHeight || `${this.getResultsMaxHeight()}px`; this.setAttribute('open', true); this.input.setAttribute('aria-expanded', true); this.isOpen = true; } close(clearSearchTerm = false) { this.closeResults(clearSearchTerm); this.isOpen = false; } closeResults(clearSearchTerm = false) { if (clearSearchTerm) { this.input.value = ''; this.removeAttribute('results'); } const selected = this.querySelector('[aria-selected="true"]'); if (selected) selected.setAttribute('aria-selected', false); this.input.setAttribute('aria-activedescendant', ''); this.removeAttribute('loading'); this.removeAttribute('open'); this.input.setAttribute('aria-expanded', false); this.resultsMaxHeight = false; this.predictiveSearchResults.removeAttribute('style'); } } customElements.define('predictive-search', PredictiveSearch);