function searchFilter(name, scope) { const datasetName = makeDatasetName(name); const selector = 'input[type="text"][name="' + name + '"]'; const searchEl = (scope||document).querySelector(selector); searchEl.addEventListener('keyup', function() { const len = this.value.length; if (len === 0 || len > 2) { this.form.requestSubmit(); } }); return { getDatasetName: function() { return datasetName; }, getValue: function() { return searchEl.value; }, setValue: function(value) { searchEl.value = value; }, matches: function(value) { const target = this.getValue(); // TODO: we probably want an index for this, but this API is not right for that return target === '' || -1 !== value.indexOf(target); }, }; } function selectFilter(name, scope) { const datasetName = makeDatasetName(name); const selectEl = (scope||document).querySelector('select[name="' + name + '"]'); const optionEls = selectEl.querySelectorAll('option'); const options = {}; for (const el of optionEls) { options[el.value] = el.innerText; } selectEl.addEventListener('change', function() { this.form.requestSubmit(); }); return { getDatasetName: function() { return datasetName; }, getValue: function() { return selectEl.value; }, getLabel: function() { const target = this.getValue(); if (target !== '') { return options[selectEl.value]; } }, setValue: function(value) { if (options.hasOwnProperty(value)) { selectEl.value = value; } }, matches: function(value) { const target = this.getValue(); return target === '' || value === target; }, }; } function checkboxFilter(name, scope) { const datasetName = makeDatasetName(name); const selector = 'input[type="checkbox"][name="' + name + '"]' const checkboxEl = (scope||document).querySelector(selector); checkboxEl.addEventListener('change', function() { this.form.requestSubmit(); }); return { getDatasetName: function() { return datasetName; }, getValue: function() { return checkboxEl.checked ? checkboxEl.value : null; }, setValue: function(value) { checkboxEl.checked = value === checkboxEl.value; }, matches: function(value) { const target = this.getValue(); return target === null || value === target; }, }; } const filterTypes = { checkboxFilter, searchFilter, selectFilter, }; function makeDatasetName(name) { const firstLetterUpperCase = name.substring(0, 1).toUpperCase(); const rest = name.substring(1); return 'filter' + firstLetterUpperCase + rest; } function filterElements(filter, elements) { const filterName = filter.getDatasetName(); for (const el of elements) { const value = el.dataset[filterName]; const target = filter.getValue(); if (!filter.matches(value)) { el.classList.add('filter-hide'); } } } function filterable(options) { const filterScope = document.querySelector(options.filterScope || 'body'); const filterableScope = document.querySelector(options.filterableScope || 'body'); const filterableSelector = options.filterableSelector || '.filterable'; const filterables = filterableScope.querySelectorAll(filterableSelector); const filters = {}; for (const filterOptions of options.filters || {}) { if (!!filterOptions.name && !!filterOptions.ty) { if (filterTypes.hasOwnProperty(filterOptions.ty)) { const filterConstructor = filterTypes[filterOptions.ty]; const filter = filterConstructor(filterOptions.name, filterScope); filters[filterOptions.name] = filter; continue; } } console.log('Skipping filter: ', filterOptions); } const groups = {}; for (const el of filterables) { const elGroup = el.dataset.filterGroup; const elParent = el.dataset.filterParent; const groupName = elGroup || elParent || ''; const group = groups[groupName] || (groups[groupName] = newGroup()); if (!!elGroup) { group.headers.push(el); } else { group.members.push(el); } function newGroup() { return { headers: [], members: [], }; } } return { getGroups: function(name) { return groups; }, getFilterValue: function(name) { if (filters.hasOwnProperty(name)) { return filters[name].getValue(); } }, getFilterLabel: function(name) { if (filters.hasOwnProperty(name)) { if (!!filters[name].getLabel) { return filters[name].getLabel(); } } }, setFilterValue: function(name, value) { if (filters.hasOwnProperty(name)) { return filters[name].setValue(value); } }, filterElements: function() { for (const group of Object.values(groups)) { for (const el of group.members) { el.classList.remove('filter-hide'); } for (const filter of Object.values(filters)) { filterElements(filter, group.members); } let anyVisible = false; for (const el of group.members) { if (!el.classList.contains('filter-hide')) { anyVisible = true; break; } } for (const el of group.headers) { if (anyVisible) { el.classList.remove('filter-hide'); } else { el.classList.add('filter-hide'); } } } }, }; } function linkRewriter(options) { const linkScope = document.querySelector(options.linkScope || 'body'); const linkSelector = options.linkSelector || 'a[href]'; const links = linkScope.querySelectorAll(linkSelector); if (!!options.parameterName) { const parameterName = options.parameterName; return { setValue: function(parameter) { for (const el of links) { const url = new URL(el.href); url.searchParams.set(parameterName, parameter); el.href = url.href; } } }; } if (!!options.path && options.path === 'full') { return { setValue: function(parameter) { const target = new URL(parameter, document.location); for (const el of links) { const url = new URL(el.href); url.pathname = target.pathname; url.search = target.search; url.hash = target.hash; el.href = url.href; } } }; } console.error('linkRewriter requires either parameterName or path option!'); } function globalLinkRewriter() { const loginRewriter = linkRewriter({ linkSelector: 'a[href$="/login"]', parameterName: 'forward', }); const txfrRewriter = linkRewriter({ linkSelector: 'a[href].kiva-txfr', path: 'full', }); return { update: function() { const url = new URL(document.location); loginRewriter.setValue(url.pathname + url.search + url.hash); txfrRewriter.setValue(url.pathname + url.search + url.hash); } }; } function cookieManager(name) { let cache = ''; for (const cookie of document.cookie.split(';')) { const [key, value] = cookie.split('='); if (key === name) { cache = value; } } return { getValue: function() { return cache; }, setValue: function(value) { if (cache != value) { document.cookie = name + '=' + value + '; SameSite=Strict'; cache = value; } }, }; } function historyManager(options) { const timeout = options.stateChangeTimeout || 3000; const onPopState = options.onPopState || function() { console.error('Unhandled popstate event'); }; window.addEventListener('popstate', function(evt) { onPopState(evt.state); }); let timeoutId; return { pushState: function(state, url) { if (!!timeoutId) { clearTimeout(timeoutId); history.replaceState(state, '', url); } else { history.pushState(state, '', url); } timeoutId = setTimeout(function() { timeoutId = null; }, timeout); }, }; } function titleManager(options) { const delimiter = options.delimiter || ' - '; const regex = new RegExp('^.*' + delimiter); const segments = []; const values = {}; for (const item of options.segments || { name: 'value' }) { if (!!item.name || !!item.value) { const name = item.name || '' + segments.length; segments.push(name); values[name] = item.value; } } function setTitle() { const parts = []; for (const segment of segments) { const value = values[segment]; if (!!value) { parts.push(value); } } const value = parts.join(' '); document.title = document.title.replace(regex, value + delimiter); } return { setValue: function(name, value) { if (-1 !== segments.indexOf(name)) { values[name] = value; setTitle(); } }, }; } function shopApp() { const shopFilters = filterable({ filterScope: 'form[action="/shop"]', filterableScope: '#shop-form', filters: [ { name: 'product', ty: 'searchFilter', }, { name: 'brand', ty: 'selectFilter', }, { name: 'type', ty: 'selectFilter', }, { name: 'new', ty: 'checkboxFilter', }, ], }); const viewFilter = filterable({ filterScope: 'form[action="/shop"]', filterableScope: '#shop-form', filterableSelector: '[data-filter-view]', filters: [ { name: 'view', ty: 'selectFilter', }, ], }); const globalRewriter = globalLinkRewriter(); const locationRewriter = linkRewriter({ linkSelector: 'a[href^="/product"],a[href^="/shop"]', parameterName: 'location', }); const viewCookie = cookieManager('view_as'); const shopTitle = titleManager({ segments: [ { value: 'Shop', }, { name: 'new', }, { name: 'brand', }, { name: 'type', }, ], }); const shopParams = ['product', 'brand', 'type', 'new']; const urlParams = shopParams.concat(['view']); const appState = { get: function(name) { if (-1 !== shopParams.indexOf(name)) { return shopFilters.getFilterValue(name); } else if (name === 'view') { return viewFilter.getFilterValue(name); } }, set: function(name, value) { if (-1 !== shopParams.indexOf(name)) { return shopFilters.setFilterValue(name, value); } else if (name === 'view') { return viewFilter.setFilterValue(name, value); } }, loadFromUrl: function(state) { const page = new URL(document.location); for (const param of urlParams) { const value = page.searchParams.get(param); if (!!value) { appState.set(param, value); } else if (!!state && !!state[param]) { appState.set(param, state[param]); } } }, updateGlobalLinks: function() { globalRewriter.update(); }, updatePageTitle: function() { shopTitle.setValue('new', !!shopFilters.getFilterValue('new') ? 'New' : null); shopTitle.setValue('brand', shopFilters.getFilterLabel('brand')); shopTitle.setValue('type', shopFilters.getFilterLabel('type')); }, updateFilterElements: function() { shopFilters.filterElements(); viewFilter.filterElements(); }, updatePage: function() { appState.updateFilterElements(); appState.updatePageTitle(); }, getUrl: function() { var action = new URLSearchParams(); for (const param of urlParams) { const value = appState.get(param); if (!!value) { if (param != 'view') { action.append(param, value); } else if (viewCookie.getValue() !== value) { action.append(param, value); } } } const query = action.toString(); return '/shop' + (!!query ? '?' + query : ''); }, }; const page = new URL(document.location); viewCookie.setValue(appState.get('view')); const originalLocation = page.searchParams.get('location'); if (!!originalLocation) { locationRewriter.setValue(originalLocation); } appState.updateGlobalLinks(); const appHistory = historyManager({ onPopState: function(state) { const newPage = new URL(document.location); if (originalLocation !== newPage.searchParams.get('location')) { // nard refresh new warehouse data document.location = document.location; } appState.loadFromUrl(state); appState.updatePage(); appState.updateGlobalLinks(); }, }); document.querySelector('form[action="/shop"]').addEventListener('submit', function(evt) { evt.preventDefault(); appState.updatePage(); const newUrl = appState.getUrl(); appHistory.pushState({ view: appState.get('view') }, newUrl); viewCookie.setValue(appState.get('view')); appState.updateGlobalLinks(); }); } const page = new URL(document.location); if (page.pathname === '/shop') { window.app = shopApp(); } else { const globalRewriter = globalLinkRewriter(); globalRewriter.update(); const locationId = page.searchParams.get('location'); if (!!locationId) { const locationRewriter = linkRewriter({ linkSelector: 'a[href^="/product"],a[href^="/shop"]', parameterName: 'location', }); locationRewriter.setValue(locationId); } } document.querySelectorAll('button[aria-controls]').forEach(function(buttonEl) { const controlledEl = document.querySelector('#' + buttonEl.getAttribute('aria-controls')); function openList() { controlledEl.classList.add('open'); buttonEl.setAttribute('aria-expanded', 'true'); } function closeList() { controlledEl.classList.remove('open'); buttonEl.setAttribute('aria-expanded', 'false'); } buttonEl.addEventListener('click', function(evt) { evt.preventDefault(); if (controlledEl.classList.contains('open')) { closeList(); } else { openList(); } }); buttonEl.parentElement.addEventListener('keyup', function(evt) { if (evt.key === 'Escape') { if (controlledEl.classList.contains('open')) { evt.preventDefault(); closeList(); buttonEl.focus(); } } }); });