class PredictiveSearch extends HTMLElement {
constructor() {
super();
this.cachedResults = {};
this.input = this.querySelector('input[type="search"]');
this.predictiveSearchResults = this.querySelector('[data-predictive-search]');
this.isOpen = false;
this.setupEventListeners();
}
setupEventListeners() {
const form = this.querySelector('form.search');
form.addEventListener('submit', this.onFormSubmit.bind(this));
this.input.addEventListener('input', debounce((event) => {
this.onChange(event);
}, 300).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() {
const searchTerm = this.getQuery();
if (!searchTerm.length) {
this.close(true);
return;
}
this.getSearchResults(searchTerm);
}
onFormSubmit(event) {
if (!this.getQuery().length || this.querySelector('[aria-selected="true"] a')) event.preventDefault();
}
onFocus() {
const searchTerm = this.getQuery();
if (!searchTerm.length) return;
if (this.getAttribute('results') === 'true') {
this.open();
} else {
this.getSearchResults(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();
}
}
switchOption(direction) {
if (!this.getAttribute('open')) return;
const moveUp = direction === 'up';
const selectedElement = this.querySelector('[aria-selected="true"]');
const allElements = this.querySelectorAll('li');
let activeElement = this.querySelector('li');
if (moveUp && !selectedElement) return;
this.statusElement.textContent = '';
if (!moveUp && selectedElement) {
activeElement = selectedElement.nextElementSibling || allElements[0];
} else if (moveUp) {
activeElement = selectedElement.previousElementSibling || allElements[allElements.length - 1];
}
if (activeElement === selectedElement) return;
activeElement.setAttribute('aria-selected', true);
if (selectedElement) selectedElement.setAttribute('aria-selected', false);
this.setLiveRegionText(activeElement.textContent);
this.input.setAttribute('aria-activedescendant', activeElement.id);
}
selectOption() {
const selectedProduct = this.querySelector('[aria-selected="true"] a, [aria-selected="true"] button');
if (selectedProduct) selectedProduct.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)}&${encodeURIComponent('resources[type]')}=product&${encodeURIComponent('resources[limit]')}=4§ion_id=predictive-search`)
.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;
this.cachedResults[queryKey] = resultsMarkup;
this.renderSearchResults(resultsMarkup);
})
.catch((error) => {
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.getElementById('shopify-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) {
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('open');
this.input.setAttribute('aria-expanded', false);
this.resultsMaxHeight = false
this.predictiveSearchResults.removeAttribute('style');
this.isOpen = false;
}
}
customElements.define('predictive-search', PredictiveSearch);