From 38b9aead655ca31a351ce7f1ecd05492cd20c8d9 Mon Sep 17 00:00:00 2001 From: Lars Hoffbeck Date: Thu, 28 Mar 2024 16:09:46 -0400 Subject: [PATCH] Re-merge PR #3246 --- assets/base.css | 1 + assets/constants.js | 5 + assets/global.js | 512 ++++++++++++++---------- assets/pickup-availability.js | 2 + assets/quick-add.css | 2 + assets/quick-add.js | 74 ++-- assets/quick-order-list.js | 10 + sections/main-product.liquid | 4 +- sections/related-products.liquid | 4 +- snippets/buy-buttons.liquid | 10 +- snippets/price.liquid | 2 + snippets/product-variant-options.liquid | 65 ++- snippets/product-variant-picker.liquid | 8 +- snippets/quick-order-list-row.liquid | 12 +- snippets/swatch-input.liquid | 4 + 15 files changed, 426 insertions(+), 289 deletions(-) diff --git a/assets/base.css b/assets/base.css index 5c3a813b4fa..0687885711c 100644 --- a/assets/base.css +++ b/assets/base.css @@ -3257,6 +3257,7 @@ details-disclosure > details { opacity: 1; animation: none; transition: none; + transform: none; } .scroll-trigger.scroll-trigger--design-mode.animate--slide-in { diff --git a/assets/constants.js b/assets/constants.js index 01a2fb4e0ed..1b016f6fc0f 100644 --- a/assets/constants.js +++ b/assets/constants.js @@ -5,4 +5,9 @@ const PUB_SUB_EVENTS = { quantityUpdate: 'quantity-update', variantChange: 'variant-change', cartError: 'cart-error', + sectionRefreshed: 'section-refreshed', +}; + +const SECTION_REFRESH_RESOURCE_TYPE = { + product: 'product', }; diff --git a/assets/global.js b/assets/global.js index e4324460f8c..1eb852b28f0 100644 --- a/assets/global.js +++ b/assets/global.js @@ -6,6 +6,53 @@ function getFocusableElements(container) { ); } +class HTMLUpdateUtility { + #preProcessCallbacks = []; + #postProcessCallbacks = []; + + constructor() {} + + addPreProcessCallback(callback) { + this.#preProcessCallbacks.push(callback); + } + + addPostProcessCallback(callback) { + this.#postProcessCallbacks.push(callback); + } + + /** + * Used to swap an HTML node with a new node. + * The new node is inserted as a previous sibling to the old node, the old node is hidden, and then the old node is removed. + * + * The function currently uses a double buffer approach, but this should be replaced by a view transition once it is more widely supported https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API + */ + viewTransition(oldNode, newContent) { + this.#preProcessCallbacks.forEach((callback) => callback(newContent)); + + const newNode = oldNode.cloneNode(); + HTMLUpdateUtility.setInnerHTML(newNode, newContent.innerHTML); + oldNode.parentNode.insertBefore(newNode, oldNode); + oldNode.style.display = 'none'; + + this.#postProcessCallbacks.forEach((callback) => callback(newNode)); + + setTimeout(() => oldNode.remove(), 1000); + } + + // Sets inner HTML and reinjects the script tags to allow execution. By default, scripts are disabled when using element.innerHTML. + static setInnerHTML(element, html) { + element.innerHTML = html; + element.querySelectorAll('script').forEach((oldScriptTag) => { + const newScriptTag = document.createElement('script'); + Array.from(oldScriptTag.attributes).forEach((attribute) => { + newScriptTag.setAttribute(attribute.name, attribute.value); + }); + newScriptTag.appendChild(document.createTextNode(oldScriptTag.innerHTML)); + oldScriptTag.parentNode.replaceChild(newScriptTag, oldScriptTag); + }); + } +} + document.querySelectorAll('[id^="Details-"] summary').forEach((summary) => { summary.setAttribute('role', 'button'); summary.setAttribute('aria-expanded', summary.parentNode.hasAttribute('open')); @@ -960,56 +1007,76 @@ customElements.define('slideshow-component', SlideshowComponent); class VariantSelects extends HTMLElement { constructor() { super(); - this.addEventListener('change', this.onVariantChange); + this.addEventListener('change', this.handleProductUpdate); + this.initializeProductSwapUtility(); + } + + initializeProductSwapUtility() { + this.swapProductUtility = new HTMLUpdateUtility(); + this.swapProductUtility.addPreProcessCallback((html) => { + html.querySelectorAll('.scroll-trigger').forEach((element) => element.classList.add('scroll-trigger--cancel')); + return html; + }); + this.swapProductUtility.addPostProcessCallback((newNode) => { + window?.Shopify?.PaymentButton?.init(); + window?.ProductModel?.loadShopifyXR(); + publish(PUB_SUB_EVENTS.sectionRefreshed, { + data: { + sectionId: this.dataset.section, + resource: { + type: SECTION_REFRESH_RESOURCE_TYPE.product, + id: newNode.querySelector('variant-selects').dataset.productId, + }, + }, + }); + }); } - onVariantChange(event) { - this.updateOptions(); - this.updateMasterId(); + handleProductUpdate(event) { + const input = this.getInputForEventTarget(event.target); + const targetId = input.id; + const targetUrl = input.dataset.productUrl; + this.currentVariant = this.getVariantData(targetId); + const sectionId = this.dataset.originalSection || this.dataset.section; this.updateSelectedSwatchValue(event); this.toggleAddButton(true, '', false); - this.updatePickupAvailability(); this.removeErrorMessage(); - this.updateVariantStatuses(); - if (!this.currentVariant) { - this.toggleAddButton(true, '', true); + let callback = () => {}; + if (this.dataset.url !== targetUrl) { + this.updateURL(targetUrl); + this.updateShareUrl(targetUrl); + callback = this.handleSwapProduct(sectionId); + } else if (!this.currentVariant) { this.setUnavailable(); + callback = (html) => { + this.updatePickupAvailability(); + this.updateOptionValues(html); + }; } else { - this.updateURL(); + this.updateURL(targetUrl); this.updateVariantInput(); - this.renderProductInfo(); - this.updateShareUrl(); + this.updateShareUrl(targetUrl); + callback = this.handleUpdateProductInfo(sectionId); } - } - updateOptions() { - this.options = Array.from(this.querySelectorAll('select, fieldset'), (element) => { - if (element.tagName === 'SELECT') { - return element.value; - } - if (element.tagName === 'FIELDSET') { - return Array.from(element.querySelectorAll('input')).find((radio) => radio.checked)?.value; - } - }); + this.renderProductInfo(sectionId, targetUrl, targetId, callback); } - updateMasterId() { - this.currentVariant = this.getVariantData().find((variant) => { - return !variant.options - .map((option, index) => { - return this.options[index] === option; - }) - .includes(false); - }); + getSelectedOptionValues() { + return Array.from(this.querySelectorAll('select, fieldset input:checked')).map( + (element) => element.dataset.optionValueId + ); } updateSelectedSwatchValue({ target }) { - const { name, value, tagName } = target; + const { value, tagName } = target; if (tagName === 'SELECT' && target.selectedOptions.length) { const swatchValue = target.selectedOptions[0].dataset.optionSwatchValue; - const selectedDropdownSwatchValue = this.querySelector(`[data-selected-dropdown-swatch="${name}"] > .swatch`); + const selectedDropdownSwatchValue = target + .closest('.product-form__input') + .querySelector('[data-selected-value] > .swatch'); if (!selectedDropdownSwatchValue) return; if (swatchValue) { selectedDropdownSwatchValue.style.setProperty('--swatch--background', swatchValue); @@ -1024,84 +1091,11 @@ class VariantSelects extends HTMLElement { target.selectedOptions[0].dataset.optionSwatchFocalPoint || 'unset' ); } else if (tagName === 'INPUT' && target.type === 'radio') { - const selectedSwatchValue = this.querySelector(`[data-selected-swatch-value="${name}"]`); + const selectedSwatchValue = target.closest(`.product-form__input`).querySelector('[data-selected-value]'); if (selectedSwatchValue) selectedSwatchValue.innerHTML = value; } } - updateURL() { - if (!this.currentVariant || this.dataset.updateUrl === 'false') return; - window.history.replaceState({}, '', `${this.dataset.url}?variant=${this.currentVariant.id}`); - } - - updateShareUrl() { - const shareButton = document.getElementById(`Share-${this.dataset.section}`); - if (!shareButton || !shareButton.updateUrl) return; - shareButton.updateUrl(`${window.shopUrl}${this.dataset.url}?variant=${this.currentVariant.id}`); - } - - updateVariantInput() { - const productForms = document.querySelectorAll( - `#product-form-${this.dataset.section}, #product-form-installment-${this.dataset.section}` - ); - productForms.forEach((productForm) => { - const input = productForm.querySelector('input[name="id"]'); - input.value = this.currentVariant.id; - input.dispatchEvent(new Event('change', { bubbles: true })); - }); - } - - updateVariantStatuses() { - const selectedOptionOneVariants = this.variantData.filter( - (variant) => this.querySelector(':checked').value === variant.option1 - ); - const inputWrappers = [...this.querySelectorAll('.product-form__input')]; - inputWrappers.forEach((option, index) => { - if (index === 0) return; - const optionInputs = [...option.querySelectorAll('input[type="radio"], option')]; - const previousOptionSelected = inputWrappers[index - 1].querySelector(':checked').value; - const availableOptionInputsValue = selectedOptionOneVariants - .filter((variant) => variant.available && variant[`option${index}`] === previousOptionSelected) - .map((variantOption) => variantOption[`option${index + 1}`]); - this.setInputAvailability(optionInputs, availableOptionInputsValue); - }); - } - - setInputAvailability(elementList, availableValuesList) { - elementList.forEach((element) => { - const value = element.getAttribute('value'); - const availableElement = availableValuesList.includes(value); - - if (element.tagName === 'INPUT') { - element.classList.toggle('disabled', !availableElement); - } else if (element.tagName === 'OPTION') { - element.innerText = availableElement - ? value - : window.variantStrings.unavailable_with_option.replace('[value]', value); - } - }); - } - - updatePickupAvailability() { - const pickUpAvailability = document.querySelector('pickup-availability'); - if (!pickUpAvailability) return; - - if (this.currentVariant && this.currentVariant.available) { - pickUpAvailability.fetchAvailability(this.currentVariant.id); - } else { - pickUpAvailability.removeAttribute('available'); - pickUpAvailability.innerHTML = ''; - } - } - - removeErrorMessage() { - const section = this.closest('section'); - if (!section) return; - - const productForm = section.querySelector('product-form'); - if (productForm) productForm.handleErrorMessage(); - } - updateMedia(html) { const mediaGallerySource = document.querySelector(`[id^="MediaGallery-${this.dataset.section}"] ul`); const mediaGalleryDestination = html.querySelector(`[id^="MediaGallery-${this.dataset.section}"] ul`); @@ -1166,88 +1160,172 @@ class VariantSelects extends HTMLElement { if (modalContent && newModalContent) modalContent.innerHTML = newModalContent.innerHTML; } - renderProductInfo() { - const requestedVariantId = this.currentVariant.id; - const sectionId = this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section; + updateURL(url) { + if (this.dataset.updateUrl === 'false') return; + window.history.replaceState({}, '', `${url}${this.currentVariant?.id ? `?variant=${this.currentVariant.id}` : ''}`); + } - fetch( - `${this.dataset.url}?variant=${requestedVariantId}§ion_id=${ - this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section - }` - ) - .then((response) => response.text()) - .then((responseText) => { - // prevent unnecessary ui changes from abandoned selections - if (this.currentVariant.id !== requestedVariantId) return; + updateShareUrl(url) { + const shareButton = document.getElementById(`Share-${this.dataset.section}`); + if (!shareButton || !shareButton.updateUrl) return; + shareButton.updateUrl( + `${window.shopUrl}${url}${this.currentVariant?.id ? `?variant=${this.currentVariant.id}` : ''}` + ); + } - const html = new DOMParser().parseFromString(responseText, 'text/html'); - const destination = document.getElementById(`price-${this.dataset.section}`); - const source = html.getElementById( - `price-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}` - ); - const skuSource = html.getElementById( - `Sku-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}` - ); - const skuDestination = document.getElementById(`Sku-${this.dataset.section}`); - const inventorySource = html.getElementById( - `Inventory-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}` - ); - const inventoryDestination = document.getElementById(`Inventory-${this.dataset.section}`); + updateVariantInput() { + const productForms = document.querySelectorAll( + `#product-form-${this.dataset.section}, #product-form-installment-${this.dataset.section}` + ); + productForms.forEach((productForm) => { + const input = productForm.querySelector('input[name="id"]'); + input.value = this.currentVariant.id; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + } - const volumePricingSource = html.getElementById( - `Volume-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}` - ); + updatePickupAvailability() { + const pickUpAvailability = document.querySelector('pickup-availability'); + if (!pickUpAvailability) return; - this.updateMedia(html); + if (this.currentVariant && this.currentVariant.available) { + pickUpAvailability.fetchAvailability(this.currentVariant.id); + } else { + pickUpAvailability.removeAttribute('available'); + pickUpAvailability.innerHTML = ''; + } + } - const pricePerItemDestination = document.getElementById(`Price-Per-Item-${this.dataset.section}`); - const pricePerItemSource = html.getElementById( - `Price-Per-Item-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}` - ); + getInputForEventTarget(target) { + return target.tagName === 'SELECT' ? target.selectedOptions[0] : target; + } - const volumePricingDestination = document.getElementById(`Volume-${this.dataset.section}`); - const qtyRules = document.getElementById(`Quantity-Rules-${this.dataset.section}`); - const volumeNote = document.getElementById(`Volume-Note-${this.dataset.section}`); + getVariantData(inputId) { + return JSON.parse(this.getVariantDataElement(inputId).textContent); + } - if (volumeNote) volumeNote.classList.remove('hidden'); - if (volumePricingDestination) volumePricingDestination.classList.remove('hidden'); - if (qtyRules) qtyRules.classList.remove('hidden'); + getVariantDataElement(inputId) { + return this.querySelector(`script[type="application/json"][data-resource="${inputId}"]`); + } - if (source && destination) destination.innerHTML = source.innerHTML; - if (inventorySource && inventoryDestination) inventoryDestination.innerHTML = inventorySource.innerHTML; - if (skuSource && skuDestination) { - skuDestination.innerHTML = skuSource.innerHTML; - skuDestination.classList.toggle('hidden', skuSource.classList.contains('hidden')); - } + removeErrorMessage() { + const section = this.closest('section'); + if (!section) return; - if (volumePricingSource && volumePricingDestination) { - volumePricingDestination.innerHTML = volumePricingSource.innerHTML; - } + const productForm = section.querySelector('product-form'); + if (productForm) productForm.handleErrorMessage(); + } - if (pricePerItemSource && pricePerItemDestination) { - pricePerItemDestination.innerHTML = pricePerItemSource.innerHTML; - pricePerItemDestination.classList.toggle('hidden', pricePerItemSource.classList.contains('hidden')); - } + getWrappingSection(sectionId) { + return ( + this.closest(`section[data-section="${sectionId}"]`) || // main-product + this.closest(`quick-add-modal`)?.modalContent || // quick-add + this.closest(`#shopify-section-${sectionId}`) || // featured-product + null + ); + } - const price = document.getElementById(`price-${this.dataset.section}`); + handleSwapProduct(sectionId) { + return (html) => { + const oldContent = this.getWrappingSection(sectionId); + if (!oldContent) { + return; + } - if (price) price.classList.remove('hidden'); + document.getElementById(`ProductModal-${sectionId}`)?.remove(); - if (inventoryDestination) inventoryDestination.classList.toggle('hidden', inventorySource.innerText === ''); + const response = + html.querySelector(`section[data-section="${sectionId}"]`) /* main/quick-add */ || + html.getElementById(`shopify-section-${sectionId}`); /* featured product*/ - const addButtonUpdated = html.getElementById(`ProductSubmitButton-${sectionId}`); - this.toggleAddButton( - addButtonUpdated ? addButtonUpdated.hasAttribute('disabled') : true, - window.variantStrings.soldOut - ); + this.swapProductUtility.viewTransition(oldContent, response); + }; + } - publish(PUB_SUB_EVENTS.variantChange, { - data: { - sectionId, - html, - variant: this.currentVariant, - }, - }); + handleUpdateProductInfo(sectionId) { + return (html) => { + this.updatePickupAvailability(); + this.updateMedia(html); + const priceDestination = document.getElementById(`price-${this.dataset.section}`); + const priceSource = html.getElementById(`price-${sectionId}`); + const skuSource = html.getElementById(`Sku-${sectionId}`); + const skuDestination = document.getElementById(`Sku-${this.dataset.section}`); + const inventorySource = html.getElementById(`Inventory-${sectionId}`); + const inventoryDestination = document.getElementById(`Inventory-${this.dataset.section}`); + + const volumePricingSource = html.getElementById(`Volume-${sectionId}`); + + const pricePerItemDestination = document.getElementById(`Price-Per-Item-${this.dataset.section}`); + const pricePerItemSource = html.getElementById(`Price-Per-Item-${sectionId}`); + + const volumePricingDestination = document.getElementById(`Volume-${this.dataset.section}`); + const qtyRules = document.getElementById(`Quantity-Rules-${this.dataset.section}`); + const volumeNote = document.getElementById(`Volume-Note-${this.dataset.section}`); + + if (volumeNote) volumeNote.classList.remove('hidden'); + if (volumePricingDestination) volumePricingDestination.classList.remove('hidden'); + if (qtyRules) qtyRules.classList.remove('hidden'); + if (priceSource && priceDestination) priceDestination.innerHTML = priceSource.innerHTML; + if (inventorySource && inventoryDestination) inventoryDestination.innerHTML = inventorySource.innerHTML; + if (skuSource && skuDestination) { + skuDestination.innerHTML = skuSource.innerHTML; + skuDestination.classList.toggle('hidden', skuSource.classList.contains('hidden')); + } + if (volumePricingSource && volumePricingDestination) { + volumePricingDestination.innerHTML = volumePricingSource.innerHTML; + } + if (pricePerItemSource && pricePerItemDestination) { + pricePerItemDestination.innerHTML = pricePerItemSource.innerHTML; + pricePerItemDestination.classList.toggle('hidden', pricePerItemSource.classList.contains('hidden')); + } + + const price = document.getElementById(`price-${this.dataset.section}`); + if (price) price.classList.remove('hidden'); + + if (inventoryDestination) inventoryDestination.classList.toggle('hidden', inventorySource.innerText === ''); + + const addButtonUpdated = html.getElementById(`ProductSubmitButton-${sectionId}`); + this.toggleAddButton( + addButtonUpdated ? addButtonUpdated.hasAttribute('disabled') : true, + window.variantStrings.soldOut + ); + + this.updateOptionValues(html); + + publish(PUB_SUB_EVENTS.variantChange, { + data: { + sectionId, + html, + variant: this.currentVariant, + }, + }); + }; + } + + updateOptionValues(html) { + const variantSelects = html.querySelector('variant-selects'); + if (variantSelects) this.innerHTML = variantSelects.innerHTML; + } + + renderProductInfo(sectionId, url, targetId, callback) { + const params = this.currentVariant + ? `variant=${this.currentVariant?.id}` + : `option_values=${this.getSelectedOptionValues().join(',')}`; + + this.abortController?.abort(); + this.abortController = new AbortController(); + + fetch(`${url}?section_id=${sectionId}&${params}`, { + signal: this.abortController.signal, + }) + .then((response) => response.text()) + .then((responseText) => { + const html = new DOMParser().parseFromString(responseText, 'text/html'); + callback(html); + }) + .then(() => { + // set focus to last clicked option value + document.getElementById(targetId).focus(); }); } @@ -1265,11 +1343,10 @@ class VariantSelects extends HTMLElement { addButton.removeAttribute('disabled'); addButtonText.textContent = window.variantStrings.addToCart; } - - if (!modifyClass) return; } setUnavailable() { + this.toggleAddButton(true, '', true); const button = document.getElementById(`product-form-${this.dataset.section}`); const addButton = button.querySelector('[name="add"]'); const addButtonText = button.querySelector('[name="add"] > span'); @@ -1292,9 +1369,8 @@ class VariantSelects extends HTMLElement { if (qtyRules) qtyRules.classList.add('hidden'); } - getVariantData() { - this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent); - return this.variantData; + getInputSelector() { + return 'variant-selects fieldset input[type="radio"], variant-selects select option'; } } @@ -1303,38 +1379,64 @@ customElements.define('variant-selects', VariantSelects); class ProductRecommendations extends HTMLElement { constructor() { super(); + this.observer = null; } connectedCallback() { - const handleIntersection = (entries, observer) => { - if (!entries[0].isIntersecting) return; - observer.unobserve(this); - - fetch(this.dataset.url) - .then((response) => response.text()) - .then((text) => { - const html = document.createElement('div'); - html.innerHTML = text; - const recommendations = html.querySelector('product-recommendations'); - - if (recommendations && recommendations.innerHTML.trim().length) { - this.innerHTML = recommendations.innerHTML; - } - - if (!this.querySelector('slideshow-component') && this.classList.contains('complementary-products')) { - this.remove(); - } - - if (html.querySelector('.grid__item')) { - this.classList.add('product-recommendations--loaded'); - } - }) - .catch((e) => { - console.error(e); - }); - }; + this.initializeRecommendations(); + + this.unsubscribeFromSectionRefresh = subscribe(PUB_SUB_EVENTS.sectionRefreshed, (event) => { + const sectionId = this.dataset.sectionId; + const isRelatedProduct = this.classList.contains('related-products'); + const isParentSectionUpdated = sectionId && (event.data?.sectionId ?? '') === `${sectionId.split('__')[0]}__main`; - new IntersectionObserver(handleIntersection.bind(this), { rootMargin: '0px 0px 400px 0px' }).observe(this); + if (isRelatedProduct && isParentSectionUpdated) { + this.dataset.productId = event.data.resource.id; + this.initializeRecommendations(); + } + }); + } + + disconnectedCallback() { + this.unsubscribeFromSectionRefresh(); + } + + initializeRecommendations() { + this.observer?.unobserve(this); + this.observer = new IntersectionObserver( + (entries, observer) => { + if (!entries[0].isIntersecting) return; + observer.unobserve(this); + this.loadRecommendations(); + }, + { rootMargin: '0px 0px 400px 0px' } + ); + this.observer.observe(this); + } + + loadRecommendations() { + fetch(`${this.dataset.url}&product_id=${this.dataset.productId}§ion_id=${this.dataset.sectionId}`) + .then((response) => response.text()) + .then((text) => { + const html = document.createElement('div'); + html.innerHTML = text; + const recommendations = html.querySelector('product-recommendations'); + + if (recommendations?.innerHTML.trim().length) { + this.innerHTML = recommendations.innerHTML; + } + + if (!this.querySelector('slideshow-component') && this.classList.contains('complementary-products')) { + this.remove(); + } + + if (html.querySelector('.grid__item')) { + this.classList.add('product-recommendations--loaded'); + } + }) + .catch((e) => { + console.error(e); + }); } } diff --git a/assets/pickup-availability.js b/assets/pickup-availability.js index 56c6f71fbf1..c272253628c 100644 --- a/assets/pickup-availability.js +++ b/assets/pickup-availability.js @@ -13,6 +13,8 @@ if (!customElements.get('pickup-availability')) { } fetchAvailability(variantId) { + if (!variantId) return; + let rootUrl = this.dataset.rootUrl; if (!rootUrl.endsWith('/')) { rootUrl = rootUrl + '/'; diff --git a/assets/quick-add.css b/assets/quick-add.css index b9d889ad5a9..d3272da52c4 100644 --- a/assets/quick-add.css +++ b/assets/quick-add.css @@ -32,6 +32,7 @@ .quick-add-modal .scroll-trigger.scroll-trigger { animation: none; opacity: 1; + transform: none; } .quick-add-modal__content.quick-add-modal__content--bulk { @@ -282,6 +283,7 @@ quick-add-modal .product__column-sticky { } quick-add-modal .product:not(.product--no-media) .product__info-wrapper { + padding-top: 2rem; padding-left: 4rem; max-width: 54%; width: calc(54% - var(--grid-desktop-horizontal-spacing) / 2); diff --git a/assets/quick-add.js b/assets/quick-add.js index 6a9d5573ce3..5365b4011fd 100644 --- a/assets/quick-add.js +++ b/assets/quick-add.js @@ -25,83 +25,85 @@ if (!customElements.get('quick-add-modal')) { .then((response) => response.text()) .then((responseText) => { const responseHTML = new DOMParser().parseFromString(responseText, 'text/html'); - this.productElement = responseHTML.querySelector('section[id^="MainProduct-"]'); - this.productElement.classList.forEach((classApplied) => { - if (classApplied.startsWith('color-') || classApplied === 'gradient') - this.modalContent.classList.add(classApplied); - }); - this.preventDuplicatedIDs(); - this.removeDOMElements(); - this.setInnerHTML(this.modalContent, this.productElement.innerHTML); + const productElement = responseHTML.querySelector('section[id^="MainProduct-"]'); + + this.preprocessHTML(productElement); + HTMLUpdateUtility.setInnerHTML(this.modalContent, productElement.innerHTML); if (window.Shopify && Shopify.PaymentButton) { Shopify.PaymentButton.init(); } - if (window.ProductModel) window.ProductModel.loadShopifyXR(); - this.removeGalleryListSemantic(); - this.updateImageSizes(); - this.preventVariantURLSwitching(); super.show(opener); }) .finally(() => { + this.bindProductChangeCallbacks(); opener.removeAttribute('aria-disabled'); opener.classList.remove('loading'); opener.querySelector('.loading__spinner').classList.add('hidden'); }); } - setInnerHTML(element, html) { - element.innerHTML = html; - - // Reinjects the script tags to allow execution. By default, scripts are disabled when using element.innerHTML. - element.querySelectorAll('script').forEach((oldScriptTag) => { - const newScriptTag = document.createElement('script'); - Array.from(oldScriptTag.attributes).forEach((attribute) => { - newScriptTag.setAttribute(attribute.name, attribute.value); + bindProductChangeCallbacks() { + const swapProductUtility = this.querySelector('variant-selects')?.swapProductUtility; + if (swapProductUtility) { + swapProductUtility.addPreProcessCallback(this.preprocessHTML.bind(this)); + swapProductUtility.addPostProcessCallback(() => { + this.modalContent = this.querySelector('[id^="QuickAddInfo-"]'); + this.bindProductChangeCallbacks(); }); - newScriptTag.appendChild(document.createTextNode(oldScriptTag.innerHTML)); - oldScriptTag.parentNode.replaceChild(newScriptTag, oldScriptTag); + } + } + + preprocessHTML(productElement) { + productElement.classList.forEach((classApplied) => { + if (classApplied.startsWith('color-') || classApplied === 'gradient') + this.modalContent.classList.add(classApplied); }); + this.preventDuplicatedIDs(productElement); + this.removeDOMElements(productElement); + this.removeGalleryListSemantic(productElement); + this.updateImageSizes(productElement); + this.preventVariantURLSwitching(productElement); } - preventVariantURLSwitching() { - const variantPicker = this.modalContent.querySelector('variant-selects'); + preventVariantURLSwitching(productElement) { + const variantPicker = productElement.querySelector('variant-selects'); if (!variantPicker) return; variantPicker.setAttribute('data-update-url', 'false'); } - removeDOMElements() { - const pickupAvailability = this.productElement.querySelector('pickup-availability'); + removeDOMElements(productElement) { + const pickupAvailability = productElement.querySelector('pickup-availability'); if (pickupAvailability) pickupAvailability.remove(); - const productModal = this.productElement.querySelector('product-modal'); + const productModal = productElement.querySelector('product-modal'); if (productModal) productModal.remove(); - const modalDialog = this.productElement.querySelectorAll('modal-dialog'); + const modalDialog = productElement.querySelectorAll('modal-dialog'); if (modalDialog) modalDialog.forEach((modal) => modal.remove()); } - preventDuplicatedIDs() { - const sectionId = this.productElement.dataset.section; - this.productElement.innerHTML = this.productElement.innerHTML.replaceAll(sectionId, `quickadd-${sectionId}`); - this.productElement.querySelectorAll('variant-selects, product-info').forEach((element) => { + preventDuplicatedIDs(productElement) { + const sectionId = productElement.dataset.section; + productElement.innerHTML = productElement.innerHTML.replaceAll(sectionId, `quickadd-${sectionId}`); + productElement.querySelectorAll('variant-selects, product-info').forEach((element) => { element.dataset.originalSection = sectionId; }); } - removeGalleryListSemantic() { - const galleryList = this.modalContent.querySelector('[id^="Slider-Gallery"]'); + removeGalleryListSemantic(productElement) { + const galleryList = productElement.querySelector('[id^="Slider-Gallery"]'); if (!galleryList) return; galleryList.setAttribute('role', 'presentation'); galleryList.querySelectorAll('[id^="Slide-"]').forEach((li) => li.setAttribute('role', 'presentation')); } - updateImageSizes() { - const product = this.modalContent.querySelector('.product'); + updateImageSizes(productElement) { + const product = productElement.querySelector('.product'); const desktopColumns = product.classList.contains('product--columns'); if (!desktopColumns) return; diff --git a/assets/quick-order-list.js b/assets/quick-order-list.js index bc4d6d0843f..e9170f45382 100644 --- a/assets/quick-order-list.js +++ b/assets/quick-order-list.js @@ -112,6 +112,7 @@ if (!customElements.get('quick-order-list')) { } cartUpdateUnsubscriber = undefined; + sectionRefreshUnsubscriber = undefined; onSubmit(event) { event.preventDefault(); @@ -128,11 +129,20 @@ if (!customElements.get('quick-order-list')) { this.addMultipleDebounce(); }); }); + this.sectionRefreshUnsubscriber = subscribe(PUB_SUB_EVENTS.sectionRefreshed, (event) => { + const isParentSectionUpdated = + this.sectionId && (event.data?.sectionId ?? '') === `${this.sectionId.split('__')[0]}__main`; + + if (isParentSectionUpdated) { + this.refresh(); + } + }); this.sectionId = this.dataset.id; } disconnectedCallback() { this.cartUpdateUnsubscriber?.(); + this.sectionRefreshUnsubscriber?.(); } defineInputsAndQuickOrderTable() { diff --git a/sections/main-product.liquid b/sections/main-product.liquid index 3983a67003c..71d305fed8f 100644 --- a/sections/main-product.liquid +++ b/sections/main-product.liquid @@ -501,7 +501,9 @@ {%- when 'complementary' -%} {%- if recommendations.performed and recommendations.products_count > 0 -%}