278 lines
8.5 KiB
JavaScript
278 lines
8.5 KiB
JavaScript
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);
|