class CartRemoveButton extends HTMLElement { constructor() { super(); this.addEventListener('click', (event) => { event.preventDefault(); const cartItems = this.closest('cart-items') || this.closest('cart-drawer-items'); cartItems.updateQuantity(this.dataset.index, 0, event); }); } } customElements.define('cart-remove-button', CartRemoveButton); class CartItems extends window.StandardEvents.createViewEventElement(HTMLElement) { constructor() { super(); this.lineItemStatusElement = document.getElementById('shopping-cart-line-item-status') || document.getElementById('CartDrawer-LineItemStatus'); const debouncedOnChange = debounce((event) => { this.onChange(event); }, ON_CHANGE_DEBOUNCE_TIMER); this.addEventListener('change', debouncedOnChange.bind(this)); } cartUpdateUnsubscriber = undefined; static pendingCartDataPromise = null; connectedCallback() { // The factory base class auto-dispatches cart:view from the // `view-event-payload` attribute (Liquid filter output). The drawer // sets `view-event-trigger="manual"` to skip auto-dispatch. super.connectedCallback(); this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, (event) => { if (event.source === 'cart-items') return; return this.onCartUpdate(); }); } // Fetches the full cart shape (used to resolve the cart:lines-update event // promise after /cart/add.js, which only returns the added line — not the // post-mutation cart aggregates). De-duplicated across concurrent callers. static fetchCartData() { if (!CartItems.pendingCartDataPromise) { const pendingCartDataPromise = fetch(`${routes.cart_url}.json`) .then((response) => response.json()) .catch(() => null) .finally(() => { if (CartItems.pendingCartDataPromise === pendingCartDataPromise) CartItems.pendingCartDataPromise = null; }); CartItems.pendingCartDataPromise = pendingCartDataPromise; } return CartItems.pendingCartDataPromise; } disconnectedCallback() { if (this.cartUpdateUnsubscriber) { this.cartUpdateUnsubscriber(); } } resetQuantityInput(id) { const input = this.querySelector(`#Quantity-${id}`); input.value = input.getAttribute('value'); this.isEnterPressed = false; } setValidity(event, index, message) { event.target.setCustomValidity(message); event.target.reportValidity(); this.resetQuantityInput(index); event.target.select(); } validateQuantity(event) { const inputValue = parseInt(event.target.value); const index = event.target.dataset.index; let message = ''; if (inputValue < event.target.dataset.min) { message = window.quickOrderListStrings.min_error.replace('[min]', event.target.dataset.min); } else if (inputValue > parseInt(event.target.max)) { message = window.quickOrderListStrings.max_error.replace('[max]', event.target.max); } else if (inputValue % parseInt(event.target.step) !== 0) { message = window.quickOrderListStrings.step_error.replace('[step]', event.target.step); } if (message) { this.setValidity(event, index, message); } else { event.target.setCustomValidity(''); event.target.reportValidity(); this.updateQuantity( index, inputValue, event, document.activeElement.getAttribute('name'), event.target.dataset.quantityVariantId ); } } onChange(event) { this.validateQuantity(event); } onCartUpdate() { if (this.tagName === 'CART-DRAWER-ITEMS') { return fetch(`${routes.cart_url}?section_id=cart-drawer`) .then((response) => response.text()) .then((responseText) => { const html = new DOMParser().parseFromString(responseText, 'text/html'); const selectors = ['cart-drawer-items', '.cart-drawer__footer']; for (const selector of selectors) { const targetElement = document.querySelector(selector); const sourceElement = html.querySelector(selector); if (targetElement && sourceElement) { targetElement.replaceWith(sourceElement); } } }) .catch((e) => { console.error(e); }); } else { return fetch(`${routes.cart_url}?section_id=main-cart-items`) .then((response) => response.text()) .then((responseText) => { const html = new DOMParser().parseFromString(responseText, 'text/html'); const sourceQty = html.querySelector('cart-items'); this.innerHTML = sourceQty.innerHTML; }) .catch((e) => { console.error(e); }); } } getSectionsToRender() { return [ { id: 'main-cart-items', section: document.getElementById('main-cart-items').dataset.id, selector: '.js-contents', }, { id: 'cart-icon-bubble', section: 'cart-icon-bubble', selector: '.shopify-section', }, { id: 'cart-live-region-text', section: 'cart-live-region-text', selector: '.shopify-section', }, { id: 'main-cart-footer', section: document.getElementById('main-cart-footer').dataset.id, selector: '.js-contents', }, ]; } updateQuantity(line, quantity, event, name, variantId) { const eventTarget = event.currentTarget instanceof CartRemoveButton ? 'clear' : 'change'; const cartPerformanceUpdateMarker = CartPerformance.createStartingMarker(`${eventTarget}:user-action`); this.enableLoading(line); const action = quantity === 0 ? 'remove' : 'update'; const quantityInput = this.querySelector(`#Quantity-${line}`) || this.querySelector(`#Drawer-quantity-${line}`); const lineVariantId = variantId || quantityInput?.dataset.quantityVariantId; const lineKey = quantityInput?.dataset.quantityLineKey; const linesUpdateDeferred = this.createCartLinesUpdateEvent(action, lineVariantId, quantity, lineKey); // Cache sections before the fetch so we read dataset.id while elements still exist in the DOM const sectionsToRender = this.getSectionsToRender(); const body = JSON.stringify({ line, quantity, sections: sectionsToRender.map((section) => section.section), sections_url: window.location.pathname, }); fetch(`${routes.cart_change_url}`, { ...fetchConfig(), ...{ body } }) .then((response) => { return response.text(); }) .then((state) => { const parsedState = JSON.parse(state); if (parsedState.errors) { this.dispatchCartErrorEvent(parsedState.errors, 'INVALID'); linesUpdateDeferred?.reject(new Error(parsedState.errors)); } else { this.resolveCartLinesUpdate(linesUpdateDeferred, parsedState); } CartPerformance.measure(`${eventTarget}:paint-updated-sections`, () => { const quantityElement = document.getElementById(`Quantity-${line}`) || document.getElementById(`Drawer-quantity-${line}`); const items = document.querySelectorAll('.cart-item'); if (parsedState.errors) { quantityElement.value = quantityElement.getAttribute('value'); this.updateLiveRegions(line, parsedState.errors); return; } this.classList.toggle('is-empty', parsedState.item_count === 0); const cartDrawerWrapper = document.querySelector('cart-drawer'); const cartFooter = document.getElementById('main-cart-footer'); if (cartFooter) cartFooter.classList.toggle('is-empty', parsedState.item_count === 0); if (cartDrawerWrapper) cartDrawerWrapper.classList.toggle('is-empty', parsedState.item_count === 0); sectionsToRender.forEach((section) => { const elementToReplace = document.getElementById(section.id).querySelector(section.selector) || document.getElementById(section.id); elementToReplace.innerHTML = this.getSectionInnerHTML( parsedState.sections[section.section], section.selector ); }); const updatedValue = parsedState.items[line - 1] ? parsedState.items[line - 1].quantity : undefined; let message = ''; if (items.length === parsedState.items.length && updatedValue !== parseInt(quantityElement.value)) { if (typeof updatedValue === 'undefined') { message = window.cartStrings.error; } else { message = window.cartStrings.quantityError.replace('[quantity]', updatedValue); } } this.updateLiveRegions(line, message); const lineItem = document.getElementById(`CartItem-${line}`) || document.getElementById(`CartDrawer-Item-${line}`); if (lineItem && lineItem.querySelector(`[name="${name}"]`)) { cartDrawerWrapper ? trapFocus(cartDrawerWrapper, lineItem.querySelector(`[name="${name}"]`)) : lineItem.querySelector(`[name="${name}"]`).focus(); } else if (parsedState.item_count === 0 && cartDrawerWrapper?.querySelector('.drawer__inner-empty')) { trapFocus(cartDrawerWrapper.querySelector('.drawer__inner-empty'), cartDrawerWrapper.querySelector('a')); } else if (document.querySelector('.cart-item') && cartDrawerWrapper) { trapFocus(cartDrawerWrapper, document.querySelector('.cart-item__name')); } }); publish(PUB_SUB_EVENTS.cartUpdate, { source: 'cart-items', cartData: parsedState, variantId: variantId }); }) .catch((e) => { this.querySelectorAll('.loading__spinner').forEach((overlay) => overlay.classList.add('hidden')); const errors = document.getElementById('cart-errors') || document.getElementById('CartDrawer-CartErrors'); if (errors) errors.textContent = window.cartStrings.error; this.dispatchCartErrorEvent(window.cartStrings.error, 'SERVICE_UNAVAILABLE'); linesUpdateDeferred?.reject(e); }) .finally(() => { this.disableLoading(line); CartPerformance.measureFromMarker(`${eventTarget}:user-action`, cartPerformanceUpdateMarker); }); } createCartLinesUpdateEvent(action, variantId, quantity, lineKey) { const { CartLinesUpdateEvent } = window.StandardEvents || {}; if (!CartLinesUpdateEvent || !variantId) return null; // No AJAX line key on the row — likely cached HTML rendered before this // attribute landed. Skip dispatch rather than emit an event with id: ''. if (!lineKey) return null; const deferred = CartLinesUpdateEvent.createPromise(); this.dispatchEvent( new CartLinesUpdateEvent({ action, context: 'cart', lines: [{ id: lineKey, quantity }], promise: deferred.promise, }) ); return deferred; } resolveCartLinesUpdate(deferred, parsedState) { if (!deferred) return; const { CartLinesUpdateEvent } = window.StandardEvents || {}; if (!CartLinesUpdateEvent) return; deferred.resolve({ cart: CartLinesUpdateEvent.createCartFromAjaxResponse(parsedState) }); } dispatchCartErrorEvent(message, code) { const { CartErrorEvent } = window.StandardEvents || {}; if (!CartErrorEvent) return; this.dispatchEvent(new CartErrorEvent({ error: message, code })); } updateLiveRegions(line, message) { const lineItemError = document.getElementById(`Line-item-error-${line}`) || document.getElementById(`CartDrawer-LineItemError-${line}`); if (lineItemError) lineItemError.querySelector('.cart-item__error-text').textContent = message; this.lineItemStatusElement.setAttribute('aria-hidden', true); const cartStatus = document.getElementById('cart-live-region-text') || document.getElementById('CartDrawer-LiveRegionText'); cartStatus.setAttribute('aria-hidden', false); setTimeout(() => { cartStatus.setAttribute('aria-hidden', true); }, 1000); } getSectionInnerHTML(html, selector) { return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML; } enableLoading(line) { const mainCartItems = document.getElementById('main-cart-items') || document.getElementById('CartDrawer-CartItems'); mainCartItems.classList.add('cart__items--disabled'); const cartItemElements = this.querySelectorAll(`#CartItem-${line} .loading__spinner`); const cartDrawerItemElements = this.querySelectorAll(`#CartDrawer-Item-${line} .loading__spinner`); [...cartItemElements, ...cartDrawerItemElements].forEach((overlay) => overlay.classList.remove('hidden')); document.activeElement.blur(); this.lineItemStatusElement.setAttribute('aria-hidden', false); } disableLoading(line) { const mainCartItems = document.getElementById('main-cart-items') || document.getElementById('CartDrawer-CartItems'); mainCartItems.classList.remove('cart__items--disabled'); const cartItemElements = this.querySelectorAll(`#CartItem-${line} .loading__spinner`); const cartDrawerItemElements = this.querySelectorAll(`#CartDrawer-Item-${line} .loading__spinner`); cartItemElements.forEach((overlay) => overlay.classList.add('hidden')); cartDrawerItemElements.forEach((overlay) => overlay.classList.add('hidden')); } } customElements.define('cart-items', CartItems); if (!customElements.get('cart-note')) { customElements.define( 'cart-note', class CartNote extends HTMLElement { constructor() { super(); this.addEventListener( 'input', debounce((event) => { const newNote = event.target.value; const noteDeferred = this.dispatchNoteUpdateEvent(newNote); const body = JSON.stringify({ note: newNote }); fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } }) .then((r) => r.json()) .then((cart) => { if (!cart || cart.errors) { throw Object.assign(new Error(cart?.errors), { code: 'INVALID' }); } if (noteDeferred) { const { CartNoteUpdateEvent } = window.StandardEvents || {}; if (CartNoteUpdateEvent) { noteDeferred.resolve({ cart: CartNoteUpdateEvent.createCartFromAjaxResponse(cart) }); } } CartPerformance.measureFromEvent('note-update:user-action', event); }) .catch((e) => { noteDeferred?.reject(e); const { CartErrorEvent } = window.StandardEvents || {}; if (CartErrorEvent) { this.dispatchEvent( new CartErrorEvent({ error: e.message || 'Note update failed', code: e.code || 'SERVICE_UNAVAILABLE', }) ); } }); }, ON_CHANGE_DEBOUNCE_TIMER) ); } dispatchNoteUpdateEvent(newNote) { const { CartNoteUpdateEvent } = window.StandardEvents || {}; if (!CartNoteUpdateEvent) return null; const context = this.closest('dialog') || this.closest('cart-drawer') ? 'dialog' : 'cart'; const deferred = CartNoteUpdateEvent.createPromise(); this.dispatchEvent( new CartNoteUpdateEvent({ context, note: newNote, promise: deferred.promise, }) ); return deferred; } } ); }