diff --git a/assets/base.css b/assets/base.css index ad036deda83..eea9a207517 100644 --- a/assets/base.css +++ b/assets/base.css @@ -2027,7 +2027,7 @@ input[type='checkbox'] { position: relative; } -product-info .loading__spinner:not(.hidden) ~ *, +.product__info-container .loading__spinner:not(.hidden) ~ *, .quantity__rules-cart .loading__spinner:not(.hidden) ~ * { visibility: hidden; } diff --git a/assets/constants.js b/assets/constants.js index 1b016f6fc0f..6af3f9ba4d6 100644 --- a/assets/constants.js +++ b/assets/constants.js @@ -3,6 +3,7 @@ const ON_CHANGE_DEBOUNCE_TIMER = 300; const PUB_SUB_EVENTS = { cartUpdate: 'cart-update', quantityUpdate: 'quantity-update', + variantChangeStart: 'variant-change-start', variantChange: 'variant-change', cartError: 'cart-error', sectionRefreshed: 'section-refreshed', diff --git a/assets/global.js b/assets/global.js index c80b93e194a..d3a7cf35225 100644 --- a/assets/global.js +++ b/assets/global.js @@ -29,7 +29,7 @@ class HTMLUpdateUtility { viewTransition(oldNode, newContent) { this.#preProcessCallbacks.forEach((callback) => callback(newContent)); - const newNode = oldNode.cloneNode(); + const newNode = newContent.cloneNode(); HTMLUpdateUtility.setInnerHTML(newNode, newContent.innerHTML); oldNode.parentNode.insertBefore(newNode, oldNode); oldNode.style.display = 'none'; @@ -995,75 +995,35 @@ customElements.define('slideshow-component', SlideshowComponent); class VariantSelects extends HTMLElement { constructor() { super(); - 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, { + connectedCallback() { + this.addEventListener('change', (event) => { + const input = this.getInputForEventTarget(event.target); + const targetId = input.id; + const targetUrl = input.dataset.productUrl; + this.currentVariant = this.getVariantData(targetId); + this.updateSelectedSwatchValue(event); + + publish(PUB_SUB_EVENTS.variantChangeStart, { data: { - sectionId: this.dataset.section, - resource: { - type: SECTION_REFRESH_RESOURCE_TYPE.product, - id: newNode.querySelector('variant-selects').dataset.productId, - }, + event, + targetId, + targetUrl, + variant: this.currentVariant, }, }); }); } - 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.removeErrorMessage(); - - 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.updateMedia(); - this.updateURL(targetUrl); - this.updateVariantInput(); - this.updateShareUrl(targetUrl); - callback = this.handleUpdateProductInfo(sectionId); - } - - this.renderProductInfo(sectionId, targetUrl, targetId, callback); - } - - getSelectedOptionValues() { - return Array.from(this.querySelectorAll('select, fieldset input:checked')).map( - (element) => element.dataset.optionValueId - ); - } - updateSelectedSwatchValue({ target }) { const { value, tagName } = target; if (tagName === 'SELECT' && target.selectedOptions.length) { const swatchValue = target.selectedOptions[0].dataset.optionSwatchValue; - const selectedDropdownSwatchValue = target.closest('.product-form__input').querySelector('[data-selected-value] > .swatch'); + const selectedDropdownSwatchValue = target + .closest('.product-form__input') + .querySelector('[data-selected-value] > .swatch'); if (!selectedDropdownSwatchValue) return; if (swatchValue) { selectedDropdownSwatchValue.style.setProperty('--swatch--background', swatchValue); @@ -1078,57 +1038,6 @@ class VariantSelects extends HTMLElement { } } - updateMedia() { - if (!this.currentVariant) return; - if (!this.currentVariant.featured_media) return; - - const mediaGalleries = document.querySelectorAll(`[id^="MediaGallery-${this.dataset.section}"]`); - mediaGalleries.forEach((mediaGallery) => - mediaGallery.setActiveMedia(`${this.dataset.section}-${this.currentVariant.featured_media.id}`, true) - ); - - const modalContent = document.querySelector(`#ProductModal-${this.dataset.section} .product-media-modal__content`); - if (!modalContent) return; - const newMediaModal = modalContent.querySelector(`[data-media-id="${this.currentVariant.featured_media.id}"]`); - modalContent.prepend(newMediaModal); - } - - updateURL(url) { - if (this.dataset.updateUrl === 'false') return; - window.history.replaceState({}, '', `${url}${this.currentVariant?.id ? `?variant=${this.currentVariant.id}` : ''}`); - } - - 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}` : ''}` - ); - } - - 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 })); - }); - } - - 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 = ''; - } - } - getInputForEventTarget(target) { return target.tagName === 'SELECT' ? target.selectedOptions[0] : target; } @@ -1141,169 +1050,11 @@ class VariantSelects extends HTMLElement { return this.querySelector(`script[type="application/json"][data-resource="${inputId}"]`); } - removeErrorMessage() { - const section = this.closest('section'); - if (!section) return; - - const productForm = section.querySelector('product-form'); - if (productForm) productForm.handleErrorMessage(); - } - - 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 + get selectedOptionValues() { + return Array.from(this.querySelectorAll('select, fieldset input:checked')).map( + ({ dataset }) => dataset.optionValueId ); } - - handleSwapProduct(sectionId) { - return (html) => { - const oldContent = this.getWrappingSection(sectionId); - if (!oldContent) { - return; - } - - document.getElementById(`ProductModal-${sectionId}`)?.remove(); - - const response = - html.querySelector(`section[data-section="${sectionId}"]`) /* main/quick-add */ || - html.getElementById(`shopify-section-${sectionId}`); /* featured product*/ - - this.swapProductUtility.viewTransition(oldContent, response); - }; - } - - handleUpdateProductInfo(sectionId) { - return (html) => { - this.updatePickupAvailability(); - 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(); - }); - } - - toggleAddButton(disable = true, text, modifyClass = true) { - const productForm = document.getElementById(`product-form-${this.dataset.section}`); - if (!productForm) return; - const addButton = productForm.querySelector('[name="add"]'); - const addButtonText = productForm.querySelector('[name="add"] > span'); - if (!addButton) return; - - if (disable) { - addButton.setAttribute('disabled', 'disabled'); - if (text) addButtonText.textContent = text; - } else { - addButton.removeAttribute('disabled'); - addButtonText.textContent = window.variantStrings.addToCart; - } - } - - 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'); - const price = document.getElementById(`price-${this.dataset.section}`); - const inventory = document.getElementById(`Inventory-${this.dataset.section}`); - const sku = document.getElementById(`Sku-${this.dataset.section}`); - const pricePerItem = document.getElementById(`Price-Per-Item-${this.dataset.section}`); - const volumeNote = document.getElementById(`Volume-Note-${this.dataset.section}`); - const volumeTable = document.getElementById(`Volume-${this.dataset.section}`); - const qtyRules = document.getElementById(`Quantity-Rules-${this.dataset.section}`); - - if (!addButton) return; - addButtonText.textContent = window.variantStrings.unavailable; - if (price) price.classList.add('hidden'); - if (inventory) inventory.classList.add('hidden'); - if (sku) sku.classList.add('hidden'); - if (pricePerItem) pricePerItem.classList.add('hidden'); - if (volumeNote) volumeNote.classList.add('hidden'); - if (volumeTable) volumeTable.classList.add('hidden'); - if (qtyRules) qtyRules.classList.add('hidden'); - } - - getInputSelector() { - return 'variant-selects fieldset input[type="radio"], variant-selects select option'; - } } customElements.define('variant-selects', VariantSelects); diff --git a/assets/pickup-availability.js b/assets/pickup-availability.js index c272253628c..1b5ebd63579 100644 --- a/assets/pickup-availability.js +++ b/assets/pickup-availability.js @@ -36,10 +36,19 @@ if (!customElements.get('pickup-availability')) { }); } - onClickRefreshList(evt) { + onClickRefreshList() { this.fetchAvailability(this.dataset.variantId); } + update(variant) { + if (variant?.available) { + this.fetchAvailability(variant.id); + } else { + this.removeAttribute('available'); + this.innerHTML = ''; + } + } + renderError() { this.innerHTML = ''; this.appendChild(this.errorHtml); diff --git a/assets/product-form.js b/assets/product-form.js index da186473160..3fafed85276 100644 --- a/assets/product-form.js +++ b/assets/product-form.js @@ -6,10 +6,11 @@ if (!customElements.get('product-form')) { super(); this.form = this.querySelector('form'); - this.form.querySelector('[name=id]').disabled = false; + this.variantIdInput.disabled = false; this.form.addEventListener('submit', this.onSubmitHandler.bind(this)); this.cart = document.querySelector('cart-notification') || document.querySelector('cart-drawer'); this.submitButton = this.querySelector('[type="submit"]'); + this.submitButtonText = this.querySelector('[type="submit"] > span'); if (document.querySelector('cart-drawer')) this.submitButton.setAttribute('aria-haspopup', 'dialog'); @@ -113,6 +114,20 @@ if (!customElements.get('product-form')) { this.errorMessage.textContent = errorMessage; } } + + toggleSubmitButton(disable = true, text) { + if (disable) { + this.submitButton.setAttribute('disabled', 'disabled'); + if (text) this.submitButtonText.textContent = text; + } else { + this.submitButton.removeAttribute('disabled'); + this.submitButtonText.textContent = window.variantStrings.addToCart; + } + } + + get variantIdInput() { + return this.form.querySelector('[name=id]'); + } } ); } diff --git a/assets/product-info.js b/assets/product-info.js index cbdeb9e7d57..fc2f6a6d45d 100644 --- a/assets/product-info.js +++ b/assets/product-info.js @@ -2,47 +2,288 @@ if (!customElements.get('product-info')) { customElements.define( 'product-info', class ProductInfo extends HTMLElement { + quantityInput = undefined; + quantityForm = undefined; + onVariantChangeUnsubscriber = undefined; + cartUpdateUnsubscriber = undefined; + swapProductUtility = undefined; + abortController = undefined; + constructor() { super(); - this.input = this.querySelector('.quantity__input'); - this.currentVariant = this.querySelector('.product-variant-id'); - this.submitButton = this.querySelector('[type="submit"]'); - } - cartUpdateUnsubscriber = undefined; - variantChangeUnsubscriber = undefined; + this.quantityInput = this.querySelector('.quantity__input'); + } connectedCallback() { - if (!this.input) return; + this.#initializeProductSwapUtility(); + + this.onVariantChangeUnsubscriber = subscribe( + PUB_SUB_EVENTS.variantChangeStart, + this.#handleOptionValueChange.bind(this) + ); + + this.#initQuantityHandlers(); + } + + #initQuantityHandlers() { + if (!this.quantityInput) return; + this.quantityForm = this.querySelector('.product-form__quantity'); if (!this.quantityForm) return; - this.setQuantityBoundries(); + + this.#setQuantityBoundries(); if (!this.dataset.originalSection) { - this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, this.fetchQuantityRules.bind(this)); + this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, this.#fetchQuantityRules.bind(this)); } - this.variantChangeUnsubscriber = subscribe(PUB_SUB_EVENTS.variantChange, (event) => { - const sectionId = this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section; - if (event.data.sectionId !== sectionId) return; - this.updateQuantityRules(event.data.sectionId, event.data.html); - this.setQuantityBoundries(); - }); } disconnectedCallback() { - if (this.cartUpdateUnsubscriber) { - this.cartUpdateUnsubscriber(); + this.onVariantChangeUnsubscriber(); + this.cartUpdateUnsubscriber?.(); + } + + #initializeProductSwapUtility() { + this.swapProductUtility = new HTMLUpdateUtility(); + this.swapProductUtility.addPreProcessCallback((html) => + html.querySelectorAll('.scroll-trigger').forEach((element) => element.classList.add('scroll-trigger--cancel')) + ); + 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.dataset.productId, + }, + }, + }); + }); + } + + #handleOptionValueChange({ data: { event, targetId, targetUrl, variant } }) { + if (!this.contains(event.target)) return; + + const productForm = this.productForm; + productForm?.toggleSubmitButton(true); + productForm?.handleErrorMessage(); + + let callback = () => {}; + if (this.dataset.url !== targetUrl) { + this.#updateURL(targetUrl, variant?.id); + this.#updateShareUrl(targetUrl, variant?.id); + callback = this.#handleSwapProduct(); + } else if (!variant) { + this.#setUnavailable(); + callback = (html) => { + this.pickupAvailability?.update(variant); + this.#updateOptionValues(html); + }; + } else { + this.#updateURL(targetUrl, variant.id); + this.#updateShareUrl(targetUrl, variant.id); + this.#updateVariantInputs(variant.id); + callback = this.#handleUpdateProductInfo(variant); } - if (this.variantChangeUnsubscriber) { - this.variantChangeUnsubscriber(); + + this.#renderProductInfo(targetUrl, variant?.id, targetId, callback); + } + + #handleSwapProduct() { + return (html) => { + this.productModal?.remove(); + this.swapProductUtility.viewTransition(this, html.querySelector('product-info')); + }; + } + + #renderProductInfo(url, variantId, targetId, callback) { + this.abortController?.abort(); + this.abortController = new AbortController(); + + fetch(this.#getProductInfoUrl(url, variantId), { + 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 + this.querySelector(`#${targetId}`)?.focus(); + }); + } + + #getProductInfoUrl(url, variantId) { + const sectionId = this.dataset.originalSection || this.dataset.section; + + let params; + if (variantId) { + params = `variant=${variantId}`; + } else { + const optionValues = this.variantSelectors.selectedOptionValues; + if (optionValues.length) { + params = `option_values=${optionValues.join(',')}`; + } } + + return `${url}?section_id=${sectionId}&${params}`; + } + + #updateOptionValues(html) { + const variantSelects = html.querySelector('variant-selects'); + if (variantSelects) this.variantSelectors.innerHTML = variantSelects.innerHTML; } - setQuantityBoundries() { + #handleUpdateProductInfo(variant) { + const sectionId = this.dataset.originalSection || this.dataset.section; + + return (html) => { + this.pickupAvailability?.update(variant); + this.#updateMedia(html, variant?.featured_media?.id); + this.#updateOptionValues(html); + + const updateSourceFromDestination = (id, shouldHide = (source) => false) => { + const source = html.getElementById(`${id}-${sectionId}`); + const destination = this.querySelector(`#${id}-${this.dataset.section}`); + if (source && destination) { + destination.innerHTML = source.innerHTML; + destination.classList.toggle('hidden', shouldHide(source)); + } + }; + + updateSourceFromDestination('price'); + updateSourceFromDestination('Sku', ({ classList }) => classList.contains('hidden')); + updateSourceFromDestination('Inventory', ({ innerText }) => innerText === ''); + updateSourceFromDestination('Volume'); + updateSourceFromDestination('Price-Per-Item', ({ classList }) => classList.contains('hidden')); + + this.#updateQuantityRules(sectionId, html); + this.querySelector(`#Quantity-Rules-${this.dataset.section}`)?.classList.remove('hidden'); + this.querySelector(`#Volume-Note-${this.dataset.section}`)?.classList.remove('hidden'); + + this.productForm?.toggleSubmitButton( + html.getElementById(`ProductSubmitButton-${sectionId}`)?.hasAttribute('disabled') ?? true, + window.variantStrings.soldOut + ); + + publish(PUB_SUB_EVENTS.variantChange, { + data: { + sectionId, + html, + variant, + }, + }); + }; + } + + #updateVariantInputs(variantId) { + document + .querySelectorAll(`#product-form-${this.dataset.section}, #product-form-installment-${this.dataset.section}`) + .forEach((productForm) => { + const input = productForm.querySelector('input[name="id"]'); + input.value = variantId; + input.dispatchEvent(new Event('change', { bubbles: true })); + }); + } + + #updateURL(url, variantId) { + if (this.dataset.updateUrl === 'false') return; + window.history.replaceState({}, '', `${url}${variantId ? `?variant=${variantId}` : ''}`); + } + + #updateShareUrl(url, variantId) { + this.querySelector('share-url')?.updateUrl( + `${window.shopUrl}${url}${variantId ? `?variant=${variantId}` : ''}` + ); + } + + #setUnavailable() { + this.productForm?.toggleSubmitButton(true, window.variantStrings.unavailable); + + const selectors = ['price', 'Inventory', 'Sku', 'Price-Per-Item', 'Volume-Note', 'Volume', 'Quantity-Rules'] + .map((id) => `#${id}-${this.dataset.section}`) + .join(', '); + document.querySelectorAll(selectors).forEach(({ classList }) => classList.add('hidden')); + } + + #updateMedia(html, variantFeaturedMediaId) { + const mediaGallerySource = this.querySelector('media-gallery ul'); + const mediaGalleryDestination = html.querySelector(`media-gallery ul`); + + const refreshSourceData = () => { + const mediaGallerySourceItems = Array.from(mediaGallerySource.querySelectorAll('li[data-media-id]')); + const sourceSet = new Set(mediaGallerySourceItems.map((item) => item.dataset.mediaId)); + const sourceMap = new Map( + mediaGallerySourceItems.map((item, index) => [item.dataset.mediaId, { item, index }]) + ); + return [mediaGallerySourceItems, sourceSet, sourceMap]; + }; + + if (mediaGallerySource && mediaGalleryDestination) { + let [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData(); + const mediaGalleryDestinationItems = Array.from( + mediaGalleryDestination.querySelectorAll('li[data-media-id]') + ); + const destinationSet = new Set(mediaGalleryDestinationItems.map(({ dataset }) => dataset.mediaId)); + let shouldRefresh = false; + + // add items from new data not present in DOM + for (let i = mediaGalleryDestinationItems.length - 1; i >= 0; i--) { + if (!sourceSet.has(mediaGalleryDestinationItems[i].dataset.mediaId)) { + mediaGallerySource.prepend(mediaGalleryDestinationItems[i]); + shouldRefresh = true; + } + } + + // remove items from DOM not present in new data + for (let i = 0; i < mediaGallerySourceItems.length; i++) { + if (!destinationSet.has(mediaGallerySourceItems[i].dataset.mediaId)) { + mediaGallerySourceItems[i].remove(); + shouldRefresh = true; + } + } + + // refresh + if (shouldRefresh) [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData(); + + // if media galleries don't match, sort to match new data order + mediaGalleryDestinationItems.forEach((destinationItem, destinationIndex) => { + const sourceData = sourceMap.get(destinationItem.dataset.mediaId); + + if (sourceData && sourceData.index !== destinationIndex) { + mediaGallerySource.insertBefore( + sourceData.item, + mediaGallerySource.querySelector(`li:nth-of-type(${destinationIndex + 1})`) + ); + + // refresh source now that it has been modified + [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData(); + } + }); + } + + if (variantFeaturedMediaId) { + // set featured media as active in the media gallery + this.querySelector(`media-gallery`).setActiveMedia( + `${this.dataset.section}-${variantFeaturedMediaId}`, + false + ); + + // update media modal + const modalContent = this.productModal?.querySelector(`.product-media-modal__content`); + modalContent?.prepend(modalContent.querySelector(`[data-media-id="${variantFeaturedMediaId}"]`)); + } + } + + #setQuantityBoundries() { const data = { - cartQuantity: this.input.dataset.cartQuantity ? parseInt(this.input.dataset.cartQuantity) : 0, - min: this.input.dataset.min ? parseInt(this.input.dataset.min) : 1, - max: this.input.dataset.max ? parseInt(this.input.dataset.max) : null, - step: this.input.step ? parseInt(this.input.step) : 1, + cartQuantity: this.quantityInput.dataset.cartQuantity ? parseInt(this.quantityInput.dataset.cartQuantity) : 0, + min: this.quantityInput.dataset.min ? parseInt(this.quantityInput.dataset.min) : 1, + max: this.quantityInput.dataset.max ? parseInt(this.quantityInput.dataset.max) : null, + step: this.quantityInput.step ? parseInt(this.quantityInput.step) : 1, }; let min = data.min; @@ -50,33 +291,34 @@ if (!customElements.get('product-info')) { if (max !== null) min = Math.min(min, max); if (data.cartQuantity >= data.min) min = Math.min(min, data.step); - this.input.min = min; - this.input.max = max; - this.input.value = min; + this.quantityInput.min = min; + this.quantityInput.max = max; + this.quantityInput.value = min; publish(PUB_SUB_EVENTS.quantityUpdate, undefined); } - fetchQuantityRules() { - if (!this.currentVariant || !this.currentVariant.value) return; + #fetchQuantityRules() { + const currentVariantId = this.productForm?.variantIdInput?.value; + if (!currentVariantId) return; + this.querySelector('.quantity__rules-cart .loading__spinner').classList.remove('hidden'); - fetch(`${this.dataset.url}?variant=${this.currentVariant.value}§ion_id=${this.dataset.section}`) + fetch(`${this.dataset.url}?variant=${currentVariantId}§ion_id=${this.dataset.section}`) .then((response) => { return response.text(); }) .then((responseText) => { const html = new DOMParser().parseFromString(responseText, 'text/html'); - this.updateQuantityRules(this.dataset.section, html); - this.setQuantityBoundries(); + this.#updateQuantityRules(this.dataset.section, html); }) .catch((e) => { console.error(e); }) - .finally(() => { - this.querySelector('.quantity__rules-cart .loading__spinner').classList.add('hidden'); - }); + .finally(() => this.querySelector('.quantity__rules-cart .loading__spinner').classList.add('hidden')); } - updateQuantityRules(sectionId, html) { + #updateQuantityRules(sectionId, html) { + this.#setQuantityBoundries(); + const quantityFormUpdated = html.getElementById(`Quantity-Form-${sectionId}`); const selectors = ['.quantity__input', '.quantity__rules', '.quantity__label']; for (let selector of selectors) { @@ -94,6 +336,22 @@ if (!customElements.get('product-info')) { } } } + + get productForm() { + return this.querySelector(`product-form`); + } + + get productModal() { + return document.querySelector(`#ProductModal-${this.dataset.section}`); + } + + get pickupAvailability() { + return this.querySelector(`pickup-availability`); + } + + get variantSelectors() { + return this.querySelector('variant-selects'); + } } ); } diff --git a/assets/quick-add.js b/assets/quick-add.js index 5365b4011fd..b0a05ecd642 100644 --- a/assets/quick-add.js +++ b/assets/quick-add.js @@ -69,10 +69,7 @@ if (!customElements.get('quick-add-modal')) { } preventVariantURLSwitching(productElement) { - const variantPicker = productElement.querySelector('variant-selects'); - if (!variantPicker) return; - - variantPicker.setAttribute('data-update-url', 'false'); + productElement.querySelector('product-info')?.setAttribute('data-update-url', 'false'); } removeDOMElements(productElement) { @@ -89,9 +86,7 @@ if (!customElements.get('quick-add-modal')) { 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; - }); + productElement.querySelector('product-info').dataset.originalSection = sectionId; } removeGalleryListSemantic(productElement) { diff --git a/sections/featured-product.liquid b/sections/featured-product.liquid index 4c920b73657..d559d51d62d 100644 --- a/sections/featured-product.liquid +++ b/sections/featured-product.liquid @@ -1,3 +1,4 @@ + {{ 'section-main-product.css' | asset_url | stylesheet_tag }} {{ 'section-featured-product.css' | asset_url | stylesheet_tag }} {{ 'component-accordion.css' | asset_url | stylesheet_tag }} @@ -144,11 +145,9 @@
- {%- assign product_form_id = 'product-form-' | append: section.id -%} @@ -452,8 +451,7 @@ {% render 'product-variant-picker', product: product, block: block, - product_form_id: product_form_id, - update_url: false + product_form_id: product_form_id %} {%- when 'buy_buttons' -%} {%- render 'buy-buttons', @@ -515,7 +513,7 @@ {{ 'products.product.view_full_details' | t }} {% render 'icon-arrow' %} - +
{%- if section.settings.media_position == 'right' -%}
@@ -693,6 +691,7 @@ {% endif %} + {% schema %} { diff --git a/sections/main-product.liquid b/sections/main-product.liquid index 530848017b7..1354b3d0503 100644 --- a/sections/main-product.liquid +++ b/sections/main-product.liquid @@ -3,6 +3,7 @@ class="section-{{ section.id }}-padding gradient color-{{ section.settings.color_scheme }}" data-section="{{ section.id }}" > + {{ 'section-main-product.css' | asset_url | stylesheet_tag }} {{ 'component-accordion.css' | asset_url | stylesheet_tag }} {{ 'component-price.css' | asset_url | stylesheet_tag }} @@ -72,10 +73,8 @@ {% render 'product-media-gallery', variant_images: variant_images %}
- {%- assign product_form_id = 'product-form-' | append: section.id -%} @@ -548,7 +547,7 @@ {{ 'products.product.view_full_details' | t }} {% render 'icon-arrow' %} - +
{%- if section.settings.media_position == 'right' -%} {% comment %} Duplicate gallery to display after product content on tablet/desktop breakpoint {% endcomment %} @@ -676,6 +675,7 @@ } +
{% schema %} @@ -2211,4 +2211,4 @@ } ] } -{% endschema %} +{% endschema %} \ No newline at end of file diff --git a/snippets/product-variant-picker.liquid b/snippets/product-variant-picker.liquid index a5f760962fa..d92d5d88a08 100644 --- a/snippets/product-variant-picker.liquid +++ b/snippets/product-variant-picker.liquid @@ -5,7 +5,6 @@ - product: {Object} product object. - block: {Object} passing the block information. - product_form_id: {String} Id of the product form to which the variant picker is associated. - - update_url: {Boolean} whether or not to update url when changing variants. If false, the url isn't updated. Default: true (optional). Usage: {% render 'product-variant-picker', product: product, block: block, product_form_id: product_form_id %} {% endcomment %} @@ -14,11 +13,6 @@ id="variant-selects-{{ section.id }}" class="no-js-hidden" data-section="{{ section.id }}" - data-url="{{ product.url }}" - data-product-id="{{ product.id }}" - {% if update_url == false %} - data-update-url="false" - {% endif %} {{ block.shopify_attributes }} > {%- for option in product.options_with_values -%}