if (!customElements.get('quick-order-list')) { customElements.define( 'quick-order-list', class QuickOrderList extends BulkAdd { cartUpdateUnsubscriber = undefined; hasPendingQuantityUpdate = false; constructor() { super(); this.isListInsideModal = this.closest('bulk-modal'); this.stickyHeaderElement = document.querySelector('sticky-header'); if (this.stickyHeaderElement) { this.stickyHeader = { height: this.stickyHeaderElement.offsetHeight, type: `${this.stickyHeaderElement.getAttribute('data-sticky-type')}`, }; } this.totalBar = this.getTotalBar(); if (this.totalBar) { this.totalBarPosition = window.innerHeight - this.totalBar.offsetHeight; this.handleResize = this.handleResize.bind(this); window.addEventListener('resize', this.handleResize); } this.querySelector('form').addEventListener('submit', (event) => event.preventDefault()); } connectedCallback() { this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, async (event) => { // skip if cart event was triggered by this section if (event.source === this.id) return; this.toggleTableLoading(true); await this.refresh(); this.toggleTableLoading(false); }); this.initEventListeners(); } disconnectedCallback() { this.cartUpdateUnsubscriber?.(); window.removeEventListener('resize', this.handleResize); } handleResize() { this.totalBarPosition = window.innerHeight - this.totalBar.offsetHeight; this.stickyHeader.height = this.stickyHeaderElement ? this.stickyHeaderElement.offsetHeight : 0; } initEventListeners() { this.querySelectorAll('.pagination__item').forEach((link) => { link.addEventListener('click', async (event) => { event.preventDefault(); event.stopPropagation(); const url = new URL(event.currentTarget.href); this.toggleTableLoading(true); await this.refresh(url.searchParams.get('page') || '1'); this.scrollTop(); this.toggleTableLoading(false); }); }); this.querySelector('.quick-order-list__contents').addEventListener( 'keyup', this.handleScrollIntoView.bind(this) ); this.quickOrderListTable.addEventListener('keydown', this.handleSwitchVariantOnEnter.bind(this)); this.initVariantEventListeners(); } initVariantEventListeners() { this.allInputsArray = Array.from(this.querySelectorAll('input[type="number"]')); this.querySelectorAll('quantity-input').forEach((qty) => { const debouncedOnChange = debounce(this.onChange.bind(this), BulkAdd.ASYNC_REQUEST_DELAY, true); qty.addEventListener('change', (event) => { this.hasPendingQuantityUpdate = true; debouncedOnChange(event); }); }); this.querySelectorAll('.quick-order-list-remove-button').forEach((button) => { button.addEventListener('click', (event) => { event.preventDefault(); this.toggleLoading(true); this.startQueue(button.dataset.index, 0); }); }); } get currentPage() { return this.querySelector('.pagination-wrapper')?.dataset?.page ?? '1'; } get cartVariantsForProduct() { return JSON.parse(this.querySelector('[data-cart-contents]')?.innerHTML || '[]'); } getCartQuantityForLine(id) { const cartQuantity = super.getCartQuantityForLine(id); if (cartQuantity > 0) return cartQuantity; return this.cartVariantsForProduct.includes(parseInt(id, 10)) ? 1 : 0; } onChange(event) { const inputValue = parseInt(event.target.value); this.cleanErrorMessageOnType(event); if (inputValue == 0) { event.target.setAttribute('value', inputValue); this.startQueue(event.target.dataset.index, inputValue); } else { this.validateQuantity(event); } } cleanErrorMessageOnType(event) { const handleKeydown = () => { event.target.setCustomValidity(' '); event.target.reportValidity(); event.target.removeEventListener('keydown', handleKeydown); }; event.target.addEventListener('keydown', handleKeydown); } validateInput(target) { const targetValue = parseInt(target.value); const targetMin = parseInt(target.dataset.min); const targetStep = parseInt(target.step); if (target.max) { return ( targetValue == 0 || (targetValue >= targetMin && targetValue <= parseInt(target.max) && targetValue % targetStep == 0) ); } else { return targetValue == 0 || (targetValue >= targetMin && targetValue % targetStep == 0); } } get quickOrderListTable() { return this.querySelector('.quick-order-list__table'); } getSectionsToRender() { return [ { id: this.id, section: this.dataset.section, selector: `#${this.id}`, }, { id: 'cart-icon-bubble', section: 'cart-icon-bubble', selector: '#shopify-section-cart-icon-bubble', }, { id: `quick-order-list-live-region-text-${this.dataset.productId}`, section: 'cart-live-region-text', selector: '.shopify-section', }, { id: 'CartDrawer', selector: '.drawer__inner', section: 'cart-drawer', }, ]; } toggleTableLoading(enable) { this.quickOrderListTable.classList.toggle('quick-order-list__container--disabled', enable); this.toggleLoading(enable); } async refresh(pageNumber = null) { const url = this.dataset.url || window.location.pathname; return fetch(`${url}?section_id=${this.dataset.section}&page=${pageNumber || this.currentPage}`) .then((response) => response.text()) .then((responseText) => { const html = new DOMParser().parseFromString(responseText, 'text/html'); const responseQuickOrderList = html.querySelector(`#${this.id}`); if (!responseQuickOrderList) { return; } this.innerHTML = responseQuickOrderList.innerHTML; this.initEventListeners(); }) .catch((e) => { console.error(e); }); } renderSections(parsedState) { const { items, sections } = parsedState; this.getSectionsToRender().forEach(({ id, selector, section }) => { const sectionElement = document.getElementById(id); if (!sectionElement) return; const newSection = new DOMParser().parseFromString(sections[section], 'text/html').querySelector(selector); if (section === this.dataset.section) { if (this.queue.length > 0 || this.hasPendingQuantityUpdate) return; const focusedElement = document.activeElement; let focusTarget = focusedElement?.dataset?.target; if (focusTarget?.includes('remove')) { focusTarget = focusedElement.closest('quantity-popover')?.querySelector('[data-target*="increment-"]') ?.dataset.target; } const total = this.getTotalBar(); if (total) { total.innerHTML = newSection.querySelector('.quick-order-list__total').innerHTML; } const table = this.quickOrderListTable; const newTable = newSection.querySelector('.quick-order-list__table'); // only update variants if they are from the active page const shouldUpdateVariants = this.currentPage === (newSection.querySelector('.pagination-wrapper')?.dataset.page ?? '1'); if (newTable && shouldUpdateVariants) { table.innerHTML = newTable.innerHTML; const newFocusTarget = this.querySelector(`[data-target='${focusTarget}']`); if (newFocusTarget) { newFocusTarget?.focus({ preventScroll: true }); } this.initVariantEventListeners(); } } else if (section === 'cart-drawer') { sectionElement.closest('cart-drawer')?.classList.toggle('is-empty', items.length === 0); sectionElement.querySelector(selector).innerHTML = newSection.innerHTML; } else { sectionElement.innerHTML = newSection.innerHTML; } }); } getTotalBar() { return this.querySelector('.quick-order-list__total'); } scrollTop() { const { top } = this.getBoundingClientRect(); if (this.isListInsideModal) { this.scrollIntoView(); } else { window.scrollTo({ top: top + window.scrollY - (this.stickyHeader?.height || 0), behavior: 'instant' }); } } scrollQuickOrderListTable(target) { const inputTopBorder = target.getBoundingClientRect().top; const inputBottomBorder = target.getBoundingClientRect().bottom; if (this.isListInsideModal) { const totalBarCrossesInput = inputBottomBorder > this.totalBar.getBoundingClientRect().top; const tableHeadCrossesInput = inputTopBorder < this.querySelector('.quick-order-list__table thead').getBoundingClientRect().bottom; if (totalBarCrossesInput || tableHeadCrossesInput) { this.scrollToCenter(target); } } else { const stickyHeaderBottomBorder = this.stickyHeaderElement?.getBoundingClientRect().bottom; const totalBarCrossesInput = inputBottomBorder > this.totalBarPosition; const inputOutsideOfViewPort = inputBottomBorder < this.querySelector('.variant-item__quantity-wrapper').offsetHeight; const stickyHeaderCrossesInput = this.stickyHeaderElement && this.stickyHeader.type !== 'on-scroll-up' && this.stickyHeader.height > inputTopBorder; const stickyHeaderScrollupCrossesInput = this.stickyHeaderElement && this.stickyHeader.type === 'on-scroll-up' && this.stickyHeader.height > inputTopBorder && stickyHeaderBottomBorder > 0; if ( totalBarCrossesInput || inputOutsideOfViewPort || stickyHeaderCrossesInput || stickyHeaderScrollupCrossesInput ) { this.scrollToCenter(target); } } } scrollToCenter(target) { target.scrollIntoView({ block: 'center', behavior: 'smooth', }); } handleScrollIntoView(event) { if ((event.key === 'Tab' || event.key === 'Enter') && this.allInputsArray.length !== 1) { this.scrollQuickOrderListTable(event.target); } } handleSwitchVariantOnEnter(event) { if (event.key !== 'Enter' || event.target.tagName !== 'INPUT') return; event.preventDefault(); event.target.blur(); if (!this.validateInput(event.target) || this.allInputsArray.length <= 1) return; const currentIndex = this.allInputsArray.indexOf(event.target); const offset = event.shiftKey ? -1 : 1; const nextIndex = (currentIndex + offset + this.allInputsArray.length) % this.allInputsArray.length; this.allInputsArray[nextIndex]?.select(); } updateMultipleQty(items) { if (this.queue.length == 0) this.hasPendingQuantityUpdate = false; this.toggleLoading(true); const url = this.dataset.url || window.location.pathname; const linesUpdate = this.startCartLinesUpdate(items); const body = JSON.stringify({ updates: items, sections: this.getSectionsToRender().map(({ section }) => section), sections_url: `${url}?page=${this.currentPage}`, }); this.updateMessage(); this.setErrorMessage(); fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }) .then((response) => response.json()) .then((parsedState) => { if (parsedState.errors) { throw Object.assign(new Error(parsedState.errors), { code: 'INVALID' }); } linesUpdate?.resolve(parsedState); this.renderSections(parsedState); publish(PUB_SUB_EVENTS.cartUpdate, { source: this.id, cartData: parsedState, }); }) .catch((e) => { if (e.code !== 'INVALID') console.error(e); this.dispatchCartErrorEvent( e.code === 'INVALID' ? e.message : window.cartStrings.error, e.code || 'SERVICE_UNAVAILABLE' ); linesUpdate?.reject(e); this.setErrorMessage(e.code === 'INVALID' ? e.message : window.cartStrings.error); }) .finally(() => { this.queue.length === 0 && this.toggleLoading(false); this.setRequestStarted(false); }); } setErrorMessage(message = null) { this.errorMessageTemplate = this.errorMessageTemplate ?? document.getElementById(`QuickOrderListErrorTemplate-${this.dataset.productId}`).cloneNode(true); const errorElements = document.querySelectorAll('.quick-order-list-error'); errorElements.forEach((errorElement) => { errorElement.innerHTML = ''; if (!message) return; const updatedMessageElement = this.errorMessageTemplate.cloneNode(true); updatedMessageElement.content.querySelector('.quick-order-list-error-message').innerText = message; errorElement.appendChild(updatedMessageElement.content); }); } updateMessage(quantity = null) { const messages = this.querySelectorAll('.quick-order-list__message-text'); const icons = this.querySelectorAll('.quick-order-list__message-icon'); if (quantity === null || isNaN(quantity)) { messages.forEach((message) => (message.innerHTML = '')); icons.forEach((icon) => icon.classList.add('hidden')); return; } const isQuantityNegative = quantity < 0; const absQuantity = Math.abs(quantity); const textTemplate = isQuantityNegative ? absQuantity === 1 ? window.quickOrderListStrings.itemRemoved : window.quickOrderListStrings.itemsRemoved : quantity === 1 ? window.quickOrderListStrings.itemAdded : window.quickOrderListStrings.itemsAdded; messages.forEach((msg) => (msg.innerHTML = textTemplate.replace('[quantity]', absQuantity))); if (!isQuantityNegative) { icons.forEach((i) => i.classList.remove('hidden')); } } updateError(updatedValue, id) { let message = ''; if (typeof updatedValue === 'undefined') { message = window.cartStrings.error; } else { message = window.cartStrings.quantityError.replace('[quantity]', updatedValue); } this.updateLiveRegions(id, message); } updateLiveRegions(id, message) { const variantItemErrorDesktop = document.getElementById(`Quick-order-list-item-error-desktop-${id}`); if (variantItemErrorDesktop) { variantItemErrorDesktop.querySelector('.variant-item__error-text').innerHTML = message; variantItemErrorDesktop.closest('tr').classList.remove('hidden'); } if (variantItemErrorMobile) variantItemErrorMobile.querySelector('.variant-item__error-text').innerHTML = message; this.querySelector('#shopping-cart-variant-item-status').setAttribute('aria-hidden', true); const cartStatus = document.getElementById('quick-order-list-live-region-text'); cartStatus.setAttribute('aria-hidden', false); setTimeout(() => { cartStatus.setAttribute('aria-hidden', true); }, 1000); } toggleLoading(loading, target = this) { target.querySelector('#shopping-cart-variant-item-status').toggleAttribute('aria-hidden', !loading); target .querySelectorAll('.variant-remove-total .loading__spinner') ?.forEach((spinner) => spinner.classList.toggle('hidden', !loading)); } } ); } if (!customElements.get('quick-order-list-remove-all-button')) { customElements.define( 'quick-order-list-remove-all-button', class QuickOrderListRemoveAllButton extends HTMLElement { constructor() { super(); this.quickOrderList = this.closest('quick-order-list'); this.actions = { confirm: 'confirm', remove: 'remove', cancel: 'cancel', }; this.addEventListener('click', (event) => { event.preventDefault(); if (this.dataset.action === this.actions.confirm) { this.toggleConfirmation(false, true); } else if (this.dataset.action === this.actions.remove) { const items = this.quickOrderList.cartVariantsForProduct.reduce( (acc, variantId) => ({ ...acc, [variantId]: 0 }), {} ); this.quickOrderList.updateMultipleQty(items); this.toggleConfirmation(true, false); } else if (this.dataset.action === this.actions.cancel) { this.toggleConfirmation(true, false); } }); } toggleConfirmation(showConfirmation, showInfo) { this.quickOrderList .querySelector('.quick-order-list-total__confirmation') .classList.toggle('hidden', showConfirmation); this.quickOrderList.querySelector('.quick-order-list-total__info').classList.toggle('hidden', showInfo); } } ); }