class FacetFiltersForm extends HTMLElement { constructor() { super(); this.onActiveFilterClick = this.onActiveFilterClick.bind(this); this.debouncedOnSubmit = debounce((event) => { this.onSubmitHandler(event); }, 800); const facetForm = this.querySelector('form'); facetForm.addEventListener('input', this.debouncedOnSubmit.bind(this)); const facetWrapper = this.querySelector('#FacetsWrapperDesktop'); if (facetWrapper) facetWrapper.addEventListener('keyup', onKeyUpEscape); } static setListeners() { const onHistoryChange = (event) => { const searchParams = event.state ? event.state.searchParams : FacetFiltersForm.searchParamsInitial; if (searchParams === FacetFiltersForm.searchParamsPrev) return; FacetFiltersForm.renderPage(searchParams, null, false); }; window.addEventListener('popstate', onHistoryChange); } static toggleActiveFacets(disable = true) { document.querySelectorAll('.js-facet-remove').forEach((element) => { element.classList.toggle('disabled', disable); }); } static renderPage(searchParams, event, updateURLHash = true) { FacetFiltersForm.searchParamsPrev = searchParams; const sections = FacetFiltersForm.getSections(); const updateEvent = FacetFiltersForm.startUpdateEvent(searchParams); const countContainer = document.getElementById('ProductCount'); const countContainerDesktop = document.getElementById('ProductCountDesktop'); const loadingSpinners = document.querySelectorAll( '.facets-container .loading__spinner, facet-filters-form .loading__spinner', ); loadingSpinners.forEach((spinner) => spinner.classList.remove('hidden')); document.getElementById('ProductGridContainer').querySelector('.collection').classList.add('loading'); if (countContainer) { countContainer.classList.add('loading'); } if (countContainerDesktop) { countContainerDesktop.classList.add('loading'); } sections.forEach((section) => { const url = `${window.location.pathname}?section_id=${section.section}&${searchParams}`; const filterDataUrl = (element) => element.url === url; FacetFiltersForm.filterData.some(filterDataUrl) ? FacetFiltersForm.renderSectionFromCache(filterDataUrl, event, updateEvent) : FacetFiltersForm.renderSectionFromFetch(url, event, updateEvent); }); if (updateURLHash) FacetFiltersForm.updateURLHash(searchParams); } static startUpdateEvent(searchParams) { const { SearchUpdateEvent, CollectionUpdateEvent } = window.StandardEvents || {}; if (!SearchUpdateEvent && !CollectionUpdateEvent) return; const facetsForm = document.querySelector('facet-filters-form'); const facetsContainer = document.querySelector('.facets-container'); const collectionId = facetsContainer?.dataset.collectionId || null; const collectionHandle = facetsContainer?.dataset.collectionHandle || ''; const currentCount = parseInt(document.getElementById('ProductCount')?.dataset.productCount) || 0; const urlSearchParams = new URLSearchParams(searchParams); const isSearchPage = facetsContainer?.dataset.template === 'search'; const isCollectionPage = facetsContainer?.dataset.template === 'collection'; const dispatchTarget = facetsForm || document; let deferred; if (isSearchPage) { deferred = SearchUpdateEvent.createPromise(); dispatchTarget.dispatchEvent( new SearchUpdateEvent({ search: { query: urlSearchParams.get('q') || '', productFilters: SearchUpdateEvent.parseProductFilters(urlSearchParams), sortKey: SearchUpdateEvent.getSortKey(urlSearchParams), }, promise: deferred.promise, }), ); } else if (isCollectionPage) { // collectionId is null for auto-generated collections (/collections/all, // /collections/vendors/, etc.) — the SSE schema accepts that. deferred = CollectionUpdateEvent.createPromise(); dispatchTarget.dispatchEvent( new CollectionUpdateEvent({ collection: { id: collectionId, handle: collectionHandle, productsCount: currentCount }, productFilters: CollectionUpdateEvent.parseProductFilters(urlSearchParams), sortKey: CollectionUpdateEvent.getSortKey(urlSearchParams), promise: deferred.promise, }), ); } if (!deferred) return; return { resolve: (filteredCount) => { deferred.resolve({ [isSearchPage ? 'totalCount' : 'productsCount']: filteredCount }); }, reject: deferred.reject, }; } static renderSectionFromFetch(url, event, updateEvent) { fetch(url) .then((response) => response.text()) .then((html) => { FacetFiltersForm.filterData = [...FacetFiltersForm.filterData, { html, url }]; FacetFiltersForm.renderSection(html, event, updateEvent); }) .catch((error) => { console.error(error); updateEvent?.reject(error); }); } static renderSectionFromCache(filterDataUrl, event, updateEvent) { const html = FacetFiltersForm.filterData.find(filterDataUrl).html; FacetFiltersForm.renderSection(html, event, updateEvent); } static renderSection(html, event, updateEvent) { FacetFiltersForm.renderFilters(html, event); FacetFiltersForm.renderProductGridContainer(html); FacetFiltersForm.renderProductCount(html, updateEvent); if (typeof initializeScrollAnimationTrigger === 'function') initializeScrollAnimationTrigger(html.innerHTML); } static renderProductGridContainer(html) { document.getElementById('ProductGridContainer').innerHTML = new DOMParser() .parseFromString(html, 'text/html') .getElementById('ProductGridContainer').innerHTML; document .getElementById('ProductGridContainer') .querySelectorAll('.scroll-trigger') .forEach((element) => { element.classList.add('scroll-trigger--cancel'); }); } static renderProductCount(html, updateEvent) { const parsedHtml = new DOMParser().parseFromString(html, 'text/html'); const sourceCount = parsedHtml.getElementById('ProductCount'); const count = sourceCount.innerHTML; const container = document.getElementById('ProductCount'); const containerDesktop = document.getElementById('ProductCountDesktop'); container.innerHTML = count; container.dataset.productCount = sourceCount.dataset.productCount || ''; container.dataset.totalCount = sourceCount.dataset.totalCount || ''; container.classList.remove('loading'); if (containerDesktop) { containerDesktop.innerHTML = count; containerDesktop.classList.remove('loading'); } const loadingSpinners = document.querySelectorAll( '.facets-container .loading__spinner, facet-filters-form .loading__spinner', ); loadingSpinners.forEach((spinner) => spinner.classList.add('hidden')); updateEvent?.resolve(parseInt(sourceCount.dataset.productCount) || 0); } static renderFilters(html, event) { const parsedHTML = new DOMParser().parseFromString(html, 'text/html'); const facetDetailsElementsFromFetch = parsedHTML.querySelectorAll( '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter', ); const facetDetailsElementsFromDom = document.querySelectorAll( '#FacetFiltersForm .js-filter, #FacetFiltersFormMobile .js-filter, #FacetFiltersPillsForm .js-filter', ); // Remove facets that are no longer returned from the server Array.from(facetDetailsElementsFromDom).forEach((currentElement) => { if (!Array.from(facetDetailsElementsFromFetch).some(({ id }) => currentElement.id === id)) { currentElement.remove(); } }); const matchesId = (element) => { const jsFilter = event ? event.target.closest('.js-filter') : undefined; return jsFilter ? element.id === jsFilter.id : false; }; const facetsToRender = Array.from(facetDetailsElementsFromFetch).filter((element) => !matchesId(element)); const countsToRender = Array.from(facetDetailsElementsFromFetch).find(matchesId); facetsToRender.forEach((elementToRender, index) => { const currentElement = document.getElementById(elementToRender.id); // Element already rendered in the DOM so just update the innerHTML if (currentElement) { document.getElementById(elementToRender.id).innerHTML = elementToRender.innerHTML; } else { if (index > 0) { const { className: previousElementClassName, id: previousElementId } = facetsToRender[index - 1]; // Same facet type (eg horizontal/vertical or drawer/mobile) if (elementToRender.className === previousElementClassName) { document.getElementById(previousElementId).after(elementToRender); return; } } if (elementToRender.parentElement) { document.querySelector(`#${elementToRender.parentElement.id} .js-filter`).before(elementToRender); } } }); FacetFiltersForm.renderActiveFacets(parsedHTML); FacetFiltersForm.renderAdditionalElements(parsedHTML); if (countsToRender) { const closestJSFilterID = event.target.closest('.js-filter').id; if (closestJSFilterID) { FacetFiltersForm.renderCounts(countsToRender, event.target.closest('.js-filter')); FacetFiltersForm.renderMobileCounts(countsToRender, document.getElementById(closestJSFilterID)); const newFacetDetailsElement = document.getElementById(closestJSFilterID); const isTextInput = event.target.getAttribute('type') === 'text'; if (!isTextInput) { // Try to return focus to the same checkbox the user just toggled, // re-selecting it from the freshly rendered HTML by its id. const originatingInputId = event.target.id; const matchingInput = originatingInputId ? newFacetDetailsElement.querySelector(`#${CSS.escape(originatingInputId)}`) : null; if (matchingInput) { matchingInput.focus(); } else { // Fallback to summary/close button if the checkbox can't be found const fallbackSelector = newFacetDetailsElement.classList.contains('mobile-facets__details') ? `.mobile-facets__close-button` : `.facets__summary`; const fallbackElement = newFacetDetailsElement.querySelector(fallbackSelector); if (fallbackElement) fallbackElement.focus(); } } } } } static renderActiveFacets(html) { const activeFacetElementSelectors = ['.active-facets-mobile', '.active-facets-desktop']; activeFacetElementSelectors.forEach((selector) => { const activeFacetsElement = html.querySelector(selector); if (!activeFacetsElement) return; document.querySelector(selector).innerHTML = activeFacetsElement.innerHTML; }); FacetFiltersForm.toggleActiveFacets(false); } static renderAdditionalElements(html) { const mobileElementSelectors = ['.mobile-facets__open', '.mobile-facets__count', '.sorting']; mobileElementSelectors.forEach((selector) => { if (!html.querySelector(selector)) return; document.querySelector(selector).innerHTML = html.querySelector(selector).innerHTML; }); document.getElementById('FacetFiltersFormMobile').closest('menu-drawer').bindEvents(); } static renderCounts(source, target) { const targetSummary = target.querySelector('.facets__summary'); const sourceSummary = source.querySelector('.facets__summary'); if (sourceSummary && targetSummary) { targetSummary.outerHTML = sourceSummary.outerHTML; } const targetHeaderElement = target.querySelector('.facets__header'); const sourceHeaderElement = source.querySelector('.facets__header'); if (sourceHeaderElement && targetHeaderElement) { targetHeaderElement.outerHTML = sourceHeaderElement.outerHTML; } const targetWrapElement = target.querySelector('.facets-wrap'); const sourceWrapElement = source.querySelector('.facets-wrap'); if (sourceWrapElement && targetWrapElement) { const isShowingMore = Boolean(target.querySelector('show-more-button .label-show-more.hidden')); if (isShowingMore) { sourceWrapElement .querySelectorAll('.facets__item.hidden') .forEach((hiddenItem) => hiddenItem.classList.replace('hidden', 'show-more-item')); } targetWrapElement.outerHTML = sourceWrapElement.outerHTML; } } static renderMobileCounts(source, target) { const targetFacetsList = target.querySelector('.mobile-facets__list'); const sourceFacetsList = source.querySelector('.mobile-facets__list'); if (sourceFacetsList && targetFacetsList) { targetFacetsList.outerHTML = sourceFacetsList.outerHTML; } } static updateURLHash(searchParams) { history.pushState({ searchParams }, '', `${window.location.pathname}${searchParams && '?'.concat(searchParams)}`); } static getSections() { return [ { section: document.getElementById('product-grid').dataset.id, }, ]; } createSearchParams(form) { const formData = new FormData(form); return new URLSearchParams(formData).toString(); } onSubmitForm(searchParams, event) { FacetFiltersForm.renderPage(searchParams, event); } onSubmitHandler(event) { event.preventDefault(); const sortFilterForms = document.querySelectorAll('facet-filters-form form'); if (event.srcElement.className == 'mobile-facets__checkbox') { const searchParams = this.createSearchParams(event.target.closest('form')); this.onSubmitForm(searchParams, event); } else { const forms = []; const isMobile = event.target.closest('form').id === 'FacetFiltersFormMobile'; sortFilterForms.forEach((form) => { if (!isMobile) { if (form.id === 'FacetSortForm' || form.id === 'FacetFiltersForm' || form.id === 'FacetSortDrawerForm') { forms.push(this.createSearchParams(form)); } } else if (form.id === 'FacetFiltersFormMobile') { forms.push(this.createSearchParams(form)); } }); this.onSubmitForm(forms.join('&'), event); } } onActiveFilterClick(event) { event.preventDefault(); FacetFiltersForm.toggleActiveFacets(); const url = event.currentTarget.href.indexOf('?') == -1 ? '' : event.currentTarget.href.slice(event.currentTarget.href.indexOf('?') + 1); FacetFiltersForm.renderPage(url); } } FacetFiltersForm.filterData = []; FacetFiltersForm.searchParamsInitial = window.location.search.slice(1); FacetFiltersForm.searchParamsPrev = window.location.search.slice(1); customElements.define('facet-filters-form', FacetFiltersForm); FacetFiltersForm.setListeners(); class PriceRange extends HTMLElement { constructor() { super(); this.querySelectorAll('input').forEach((element) => { element.addEventListener('change', this.onRangeChange.bind(this)); element.addEventListener('keydown', this.onKeyDown.bind(this)); }); this.setMinAndMaxValues(); } onRangeChange(event) { this.adjustToValidValues(event.currentTarget); this.setMinAndMaxValues(); } onKeyDown(event) { if (event.metaKey) return; const pattern = /[0-9]|\.|,|'| |Tab|Backspace|Enter|ArrowUp|ArrowDown|ArrowLeft|ArrowRight|Delete|Escape/; if (!event.key.match(pattern)) event.preventDefault(); } setMinAndMaxValues() { const inputs = this.querySelectorAll('input'); const minInput = inputs[0]; const maxInput = inputs[1]; if (maxInput.value) minInput.setAttribute('data-max', maxInput.value); if (minInput.value) maxInput.setAttribute('data-min', minInput.value); if (minInput.value === '') maxInput.setAttribute('data-min', 0); if (maxInput.value === '') minInput.setAttribute('data-max', maxInput.getAttribute('data-max')); } adjustToValidValues(input) { const value = Number(input.value); const min = Number(input.getAttribute('data-min')); const max = Number(input.getAttribute('data-max')); if (value < min) input.value = min; if (value > max) input.value = max; } } customElements.define('price-range', PriceRange); class FacetRemove extends HTMLElement { constructor() { super(); const facetLink = this.querySelector('a'); facetLink.setAttribute('role', 'button'); facetLink.addEventListener('click', this.closeFilter.bind(this)); facetLink.addEventListener('keyup', (event) => { event.preventDefault(); if (event.code.toUpperCase() === 'SPACE') this.closeFilter(event); }); } closeFilter(event) { event.preventDefault(); const form = this.closest('facet-filters-form') || document.querySelector('facet-filters-form'); form.onActiveFilterClick(event); } } customElements.define('facet-remove', FacetRemove);