diff --git a/.vscode/settings.json b/.vscode/settings.json
index 0689f6914bd..90abaf1f399 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,12 +1,15 @@
{
"editor.formatOnSave": false,
"[javascript]": {
- "editor.formatOnSave": true
+ "editor.formatOnSave": true,
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[css]": {
"editor.formatOnSave": true
},
"[liquid]": {
+ "editor.defaultFormatter": "Shopify.theme-check-vscode",
"editor.formatOnSave": true
- }
+ },
+ "themeCheck.checkOnSave": true
}
diff --git a/assets/base.css b/assets/base.css
index 5c3a813b4fa..e758d3be7d5 100644
--- a/assets/base.css
+++ b/assets/base.css
@@ -3,6 +3,9 @@
--alpha-button-border: 1;
--alpha-link: 0.85;
--alpha-badge-border: 0.1;
+ --focused-base-outline: 0.2rem solid rgba(var(--color-foreground), 0.5);
+ --focused-base-outline-offset: 0.3rem;
+ --focused-base-box-shadow: 0 0 0 0.3rem rgb(var(--color-background)), 0 0 0.5rem 0.4rem rgba(var(--color-foreground), 0.3);
}
.product-card-wrapper .card,
@@ -264,8 +267,14 @@ h5,
word-break: break-word;
}
+.hxxl {
+ font-size: clamp(calc(var(--font-heading-scale) * 5.6rem), 14vw, calc(var(--font-heading-scale) * 7.2rem));
+ line-height: 1.1;
+}
+
.hxl {
font-size: calc(var(--font-heading-scale) * 5rem);
+ line-height: calc(1 + 0.3 / max(1, var(--font-heading-scale)));
}
@media only screen and (min-width: 750px) {
@@ -689,16 +698,16 @@ summary::-webkit-details-marker {
}
*:focus-visible {
- outline: 0.2rem solid rgba(var(--color-foreground), 0.5);
- outline-offset: 0.3rem;
- box-shadow: 0 0 0 0.3rem rgb(var(--color-background)), 0 0 0.5rem 0.4rem rgba(var(--color-foreground), 0.3);
+ outline: var(--focused-base-outline);
+ outline-offset: var(--focused-base-outline-offset);
+ box-shadow: var(--focused-base-box-shadow);
}
/* Fallback - for browsers that don't support :focus-visible, a fallback is set for :focus */
.focused {
- outline: 0.2rem solid rgba(var(--color-foreground), 0.5);
- outline-offset: 0.3rem;
- box-shadow: 0 0 0 0.3rem rgb(var(--color-background)), 0 0 0.5rem 0.4rem rgba(var(--color-foreground), 0.3);
+ outline: var(--focused-base-outline);
+ outline-offset: var(--focused-base-outline-offset);
+ box-shadow: var(--focused-base-box-shadow);
}
/*
@@ -1989,7 +1998,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;
}
@@ -2549,6 +2558,10 @@ product-info .loading__spinner:not(.hidden) ~ *,
--shop-avatar-size: 2.8rem;
}
+account-icon {
+ display: flex;
+}
+
/* Search */
menu-drawer + .header__search {
display: none;
@@ -3257,6 +3270,7 @@ details-disclosure > details {
opacity: 1;
animation: none;
transition: none;
+ transform: none;
}
.scroll-trigger.scroll-trigger--design-mode.animate--slide-in {
@@ -3454,3 +3468,101 @@ details-disclosure > details {
--border-offset: 0px; /* Prevent the border from growing on buttons when this effect is on. */
}
}
+
+/* Loading spinner */
+.loading__spinner {
+ position: absolute;
+ z-index: 1;
+ width: 1.8rem;
+}
+
+.loading__spinner {
+ width: 1.8rem;
+ display: inline-block;
+}
+
+.spinner {
+ animation: rotator 1.4s linear infinite;
+}
+
+@keyframes rotator {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(270deg);
+ }
+}
+
+.path {
+ stroke-dasharray: 280;
+ stroke-dashoffset: 0;
+ transform-origin: center;
+ stroke: rgb(var(--color-foreground));
+ animation: dash 1.4s ease-in-out infinite;
+}
+
+@media screen and (forced-colors: active) {
+ .path {
+ stroke: CanvasText;
+ }
+}
+
+@keyframes dash {
+ 0% {
+ stroke-dashoffset: 280;
+ }
+ 50% {
+ stroke-dashoffset: 75;
+ transform: rotate(135deg);
+ }
+ 100% {
+ stroke-dashoffset: 280;
+ transform: rotate(450deg);
+ }
+}
+
+.loading__spinner:not(.hidden) + .cart-item__price-wrapper,
+.loading__spinner:not(.hidden) ~ cart-remove-button {
+ opacity: 50%;
+}
+
+.loading__spinner:not(.hidden) ~ cart-remove-button {
+ pointer-events: none;
+ cursor: default;
+}
+
+/* Progress bar */
+.progress-bar-container {
+ width: 100%;
+ margin: auto;
+}
+
+.progress-bar {
+ height: 0.13rem;
+ width: 100%;
+}
+
+.progress-bar-value {
+ width: 100%;
+ height: 100%;
+ background-color: rgb(var(--color-foreground));
+ animation: indeterminateAnimation var(--duration-extra-longer) infinite ease-in-out;
+ transform-origin: 0;
+}
+
+.progress-bar .progress-bar-value {
+ display: block;
+}
+
+@keyframes indeterminateAnimation {
+ 0% {
+ transform: translateX(-20%) scaleX(0);
+ }
+ 40% {
+ transform: translateX(30%) scaleX(0.7);
+ }
+ 100% {
+ transform: translateX(100%) scaleX(0);
+ }
+}
diff --git a/assets/cart-drawer.js b/assets/cart-drawer.js
index 048e43232d2..ad37f3cb871 100644
--- a/assets/cart-drawer.js
+++ b/assets/cart-drawer.js
@@ -9,6 +9,8 @@ class CartDrawer extends HTMLElement {
setHeaderCartIconAccessibility() {
const cartLink = document.querySelector('#cart-icon-bubble');
+ if (!cartLink) return;
+
cartLink.setAttribute('role', 'button');
cartLink.setAttribute('aria-haspopup', 'dialog');
cartLink.addEventListener('click', (event) => {
@@ -76,6 +78,8 @@ class CartDrawer extends HTMLElement {
const sectionElement = section.selector
? document.querySelector(section.selector)
: document.getElementById(section.id);
+
+ if (!sectionElement) return;
sectionElement.innerHTML = this.getSectionInnerHTML(parsedState.sections[section.id], section.selector);
});
diff --git a/assets/cart.js b/assets/cart.js
index c9bcc076431..eef5f1f66d5 100644
--- a/assets/cart.js
+++ b/assets/cart.js
@@ -42,13 +42,48 @@ class CartItems extends HTMLElement {
}
}
+ 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,
+ document.activeElement.getAttribute('name'),
+ event.target.dataset.quantityVariantId
+ );
+ }
+ }
+
onChange(event) {
- this.updateQuantity(
- event.target.dataset.index,
- event.target.value,
- document.activeElement.getAttribute('name'),
- event.target.dataset.quantityVariantId
- );
+ this.validateQuantity(event);
}
onCartUpdate() {
@@ -187,7 +222,7 @@ class CartItems extends HTMLElement {
updateLiveRegions(line, message) {
const lineItemError =
document.getElementById(`Line-item-error-${line}`) || document.getElementById(`CartDrawer-LineItemError-${line}`);
- if (lineItemError) lineItemError.querySelector('.cart-item__error-text').innerHTML = message;
+ if (lineItemError) lineItemError.querySelector('.cart-item__error-text').textContent = message;
this.lineItemStatusElement.setAttribute('aria-hidden', true);
diff --git a/assets/component-loading-spinner.css b/assets/component-loading-spinner.css
deleted file mode 100644
index 6cc341a3ed7..00000000000
--- a/assets/component-loading-spinner.css
+++ /dev/null
@@ -1,61 +0,0 @@
-.loading__spinner {
- position: absolute;
- z-index: 1;
- width: 1.8rem;
-}
-
-.loading__spinner {
- width: 1.8rem;
- display: inline-block;
-}
-
-.spinner {
- animation: rotator 1.4s linear infinite;
-}
-
-@keyframes rotator {
- 0% {
- transform: rotate(0deg);
- }
- 100% {
- transform: rotate(270deg);
- }
-}
-
-.path {
- stroke-dasharray: 280;
- stroke-dashoffset: 0;
- transform-origin: center;
- stroke: rgb(var(--color-foreground));
- animation: dash 1.4s ease-in-out infinite;
-}
-
-@media screen and (forced-colors: active) {
- .path {
- stroke: CanvasText;
- }
-}
-
-@keyframes dash {
- 0% {
- stroke-dashoffset: 280;
- }
- 50% {
- stroke-dashoffset: 75;
- transform: rotate(135deg);
- }
- 100% {
- stroke-dashoffset: 280;
- transform: rotate(450deg);
- }
-}
-
-.loading__spinner:not(.hidden) + .cart-item__price-wrapper,
-.loading__spinner:not(.hidden) ~ cart-remove-button {
- opacity: 50%;
-}
-
-.loading__spinner:not(.hidden) ~ cart-remove-button {
- pointer-events: none;
- cursor: default;
-}
diff --git a/assets/component-slideshow.css b/assets/component-slideshow.css
index 16af7857d8a..01dab0142bf 100644
--- a/assets/component-slideshow.css
+++ b/assets/component-slideshow.css
@@ -9,6 +9,7 @@ slideshow-component .slideshow.banner {
flex-wrap: nowrap;
margin: 0;
gap: 0;
+ overflow-y: hidden;
}
.slideshow__slide {
diff --git a/assets/component-volume-pricing.css b/assets/component-volume-pricing.css
index 2729866c343..f23c9b21d68 100644
--- a/assets/component-volume-pricing.css
+++ b/assets/component-volume-pricing.css
@@ -19,7 +19,7 @@ volume-pricing li {
justify-content: space-between;
}
-.volume-pricing-note {
+div.volume-pricing-note {
margin-top: -2.6rem;
}
diff --git a/assets/constants.js b/assets/constants.js
index 01a2fb4e0ed..8c405e63e66 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',
+ optionValueSelectionChange: 'option-value-selection-change',
variantChange: 'variant-change',
cartError: 'cart-error',
};
diff --git a/assets/global.js b/assets/global.js
index e4324460f8c..ce13bf6514a 100644
--- a/assets/global.js
+++ b/assets/global.js
@@ -6,6 +6,68 @@ function getFocusableElements(container) {
);
}
+class SectionId {
+ static #separator = '__';
+
+ // for a qualified section id (e.g. 'template--22224696705326__main'), return just the section id (e.g. 'template--22224696705326')
+ static parseId(qualifiedSectionId) {
+ return qualifiedSectionId.split(SectionId.#separator)[0];
+ }
+
+ // for a qualified section id (e.g. 'template--22224696705326__main'), return just the section name (e.g. 'main')
+ static parseSectionName(qualifiedSectionId) {
+ return qualifiedSectionId.split(SectionId.#separator)[1];
+ }
+
+ // for a section id (e.g. 'template--22224696705326') and a section name (e.g. 'recommended-products'), return a qualified section id (e.g. 'template--22224696705326__recommended-products')
+ static getIdForSection(sectionId, sectionName) {
+ return `${sectionId}${SectionId.#separator}${sectionName}`;
+ }
+}
+
+class HTMLUpdateUtility {
+ /**
+ * 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
+ */
+ static viewTransition(oldNode, newContent, preProcessCallbacks = [], postProcessCallbacks = []) {
+ preProcessCallbacks?.forEach((callback) => callback(newContent));
+
+ const newNodeWrapper = document.createElement('div');
+ HTMLUpdateUtility.setInnerHTML(newNodeWrapper, newContent.outerHTML);
+ const newNode = newNodeWrapper.firstChild;
+
+ // dedupe IDs
+ const uniqueKey = Date.now();
+ oldNode.querySelectorAll('[id], [form]').forEach((element) => {
+ element.id && (element.id = `${element.id}-${uniqueKey}`);
+ element.form && element.setAttribute('form', `${element.form.getAttribute('id')}-${uniqueKey}`);
+ });
+
+ oldNode.parentNode.insertBefore(newNode, oldNode);
+ oldNode.style.display = 'none';
+
+ postProcessCallbacks?.forEach((callback) => callback(newNode));
+
+ setTimeout(() => oldNode.remove(), 500);
+ }
+
+ // 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'));
@@ -580,6 +642,38 @@ class ModalDialog extends HTMLElement {
}
customElements.define('modal-dialog', ModalDialog);
+class BulkModal extends HTMLElement {
+ constructor() {
+ super();
+ }
+
+ connectedCallback() {
+ const handleIntersection = (entries, observer) => {
+ if (!entries[0].isIntersecting) return;
+ observer.unobserve(this);
+ if (this.innerHTML.trim() === '') {
+ const productUrl = this.dataset.url.split('?')[0];
+ fetch(`${productUrl}?section_id=bulk-quick-order-list`)
+ .then((response) => response.text())
+ .then((responseText) => {
+ const html = new DOMParser().parseFromString(responseText, 'text/html');
+ const sourceQty = html.querySelector('.quick-order-list-container').parentNode;
+ this.innerHTML = sourceQty.innerHTML;
+ })
+ .catch((e) => {
+ console.error(e);
+ });
+ }
+ };
+
+ new IntersectionObserver(handleIntersection.bind(this)).observe(
+ document.querySelector(`#QuickBulk-${this.dataset.productId}-${this.dataset.sectionId}`)
+ );
+ }
+}
+
+customElements.define('bulk-modal', BulkModal);
+
class ModalOpener extends HTMLElement {
constructor() {
super();
@@ -960,56 +1054,36 @@ customElements.define('slideshow-component', SlideshowComponent);
class VariantSelects extends HTMLElement {
constructor() {
super();
- this.addEventListener('change', this.onVariantChange);
- }
-
- onVariantChange(event) {
- this.updateOptions();
- this.updateMasterId();
- this.updateSelectedSwatchValue(event);
- this.toggleAddButton(true, '', false);
- this.updatePickupAvailability();
- this.removeErrorMessage();
- this.updateVariantStatuses();
-
- if (!this.currentVariant) {
- this.toggleAddButton(true, '', true);
- this.setUnavailable();
- } else {
- this.updateURL();
- this.updateVariantInput();
- this.renderProductInfo();
- this.updateShareUrl();
- }
}
- 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;
- }
- });
- }
-
- updateMasterId() {
- this.currentVariant = this.getVariantData().find((variant) => {
- return !variant.options
- .map((option, index) => {
- return this.options[index] === option;
- })
- .includes(false);
+ connectedCallback() {
+ this.addEventListener('change', (event) => {
+ const target = this.getInputForEventTarget(event.target);
+ this.updateSelectionMetadata(event);
+
+ publish(PUB_SUB_EVENTS.optionValueSelectionChange, {
+ data: {
+ event,
+ target,
+ selectedOptionValues: this.selectedOptionValues,
+ },
+ });
});
}
- updateSelectedSwatchValue({ target }) {
- const { name, value, tagName } = target;
+ updateSelectionMetadata({ target }) {
+ const { value, tagName } = target;
if (tagName === 'SELECT' && target.selectedOptions.length) {
+ Array.from(target.options)
+ .find((option) => option.getAttribute('selected'))
+ .removeAttribute('selected');
+ target.selectedOptions[0].setAttribute('selected', 'selected');
+
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,338 +1098,171 @@ 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 }));
- });
+ getInputForEventTarget(target) {
+ return target.tagName === 'SELECT' ? target.selectedOptions[0] : target;
}
- updateVariantStatuses() {
- const selectedOptionOneVariants = this.variantData.filter(
- (variant) => this.querySelector(':checked').value === variant.option1
+ get selectedOptionValues() {
+ return Array.from(this.querySelectorAll('select option[selected], fieldset input:checked')).map(
+ ({ dataset }) => dataset.optionValueId
);
- 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 = '';
- }
- }
+customElements.define('variant-selects', VariantSelects);
- removeErrorMessage() {
- const section = this.closest('section');
- if (!section) return;
+class ProductRecommendations extends HTMLElement {
+ observer = undefined;
- const productForm = section.querySelector('product-form');
- if (productForm) productForm.handleErrorMessage();
+ constructor() {
+ super();
}
- updateMedia(html) {
- const mediaGallerySource = document.querySelector(`[id^="MediaGallery-${this.dataset.section}"] ul`);
- const mediaGalleryDestination = html.querySelector(`[id^="MediaGallery-${this.dataset.section}"] 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 (this.currentVariant.featured_media) {
- document
- .querySelector(`[id^="MediaGallery-${this.dataset.section}"]`)
- ?.setActiveMedia?.(`${this.dataset.section}-${this.currentVariant.featured_media.id}`);
- }
-
- // update media modal
- const modalContent = document.querySelector(`#ProductModal-${this.dataset.section} .product-media-modal__content`);
- const newModalContent = html.querySelector(`product-modal`);
- if (modalContent && newModalContent) modalContent.innerHTML = newModalContent.innerHTML;
+ connectedCallback() {
+ this.initializeRecommendations(this.dataset.productId);
+ }
+
+ initializeRecommendations(productId) {
+ this.observer?.unobserve(this);
+ this.observer = new IntersectionObserver(
+ (entries, observer) => {
+ if (!entries[0].isIntersecting) return;
+ observer.unobserve(this);
+ this.loadRecommendations(productId);
+ },
+ { rootMargin: '0px 0px 400px 0px' }
+ );
+ this.observer.observe(this);
}
- renderProductInfo() {
- const requestedVariantId = this.currentVariant.id;
- const sectionId = this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section;
-
- fetch(
- `${this.dataset.url}?variant=${requestedVariantId}§ion_id=${
- this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section
- }`
- )
+ loadRecommendations(productId) {
+ fetch(`${this.dataset.url}&product_id=${productId}§ion_id=${this.dataset.sectionId}`)
.then((response) => response.text())
- .then((responseText) => {
- // prevent unnecessary ui changes from abandoned selections
- if (this.currentVariant.id !== requestedVariantId) return;
-
- 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}`);
-
- const volumePricingSource = html.getElementById(
- `Volume-${this.dataset.originalSection ? this.dataset.originalSection : this.dataset.section}`
- );
-
- this.updateMedia(html);
-
- 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}`
- );
-
- 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}`);
+ .then((text) => {
+ const html = document.createElement('div');
+ html.innerHTML = text;
+ const recommendations = html.querySelector('product-recommendations');
- if (volumeNote) volumeNote.classList.remove('hidden');
- if (volumePricingDestination) volumePricingDestination.classList.remove('hidden');
- if (qtyRules) qtyRules.classList.remove('hidden');
-
- 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'));
+ if (recommendations?.innerHTML.trim().length) {
+ this.innerHTML = recommendations.innerHTML;
}
- if (volumePricingSource && volumePricingDestination) {
- volumePricingDestination.innerHTML = volumePricingSource.innerHTML;
+ if (!this.querySelector('slideshow-component') && this.classList.contains('complementary-products')) {
+ this.remove();
}
- if (pricePerItemSource && pricePerItemDestination) {
- pricePerItemDestination.innerHTML = pricePerItemSource.innerHTML;
- pricePerItemDestination.classList.toggle('hidden', pricePerItemSource.classList.contains('hidden'));
+ if (html.querySelector('.grid__item')) {
+ this.classList.add('product-recommendations--loaded');
}
-
- 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
- );
-
- publish(PUB_SUB_EVENTS.variantChange, {
- data: {
- sectionId,
- html,
- variant: this.currentVariant,
- },
- });
+ })
+ .catch((e) => {
+ console.error(e);
});
}
+}
- 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;
+customElements.define('product-recommendations', ProductRecommendations);
- if (disable) {
- addButton.setAttribute('disabled', 'disabled');
- if (text) addButtonText.textContent = text;
- } else {
- addButton.removeAttribute('disabled');
- addButtonText.textContent = window.variantStrings.addToCart;
- }
+class AccountIcon extends HTMLElement {
+ constructor() {
+ super();
- if (!modifyClass) return;
+ this.icon = this.querySelector('.icon');
}
- setUnavailable() {
- 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');
+ connectedCallback() {
+ document.addEventListener('storefront:signincompleted', this.handleStorefrontSignInCompleted.bind(this));
}
- getVariantData() {
- this.variantData = this.variantData || JSON.parse(this.querySelector('[type="application/json"]').textContent);
- return this.variantData;
+ handleStorefrontSignInCompleted(event) {
+ if (event?.detail?.avatar) {
+ this.icon?.replaceWith(event.detail.avatar.cloneNode());
+ }
}
}
-customElements.define('variant-selects', VariantSelects);
+customElements.define('account-icon', AccountIcon);
-class ProductRecommendations extends HTMLElement {
+class BulkAdd extends HTMLElement {
constructor() {
super();
+ this.queue = [];
+ this.requestStarted = false;
+ this.ids = [];
}
- 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();
- }
+ startQueue(id, quantity) {
+ this.queue.push({ id, quantity });
+ const interval = setInterval(() => {
+ if (this.queue.length > 0) {
+ if (!this.requestStarted) {
+ this.sendRequest(this.queue);
+ }
+ } else {
+ clearInterval(interval);
+ }
+ }, 250);
+ }
- if (html.querySelector('.grid__item')) {
- this.classList.add('product-recommendations--loaded');
- }
- })
- .catch((e) => {
- console.error(e);
- });
- };
+ sendRequest(queue) {
+ this.requestStarted = true;
+ const items = {};
+ queue.forEach((queueItem) => {
+ items[parseInt(queueItem.id)] = queueItem.quantity;
+ });
+ this.queue = this.queue.filter((queueElement) => !queue.includes(queueElement));
+ const quickBulkElement = this.closest('quick-order-list') || this.closest('quick-add-bulk');
+ quickBulkElement.updateMultipleQty(items);
+ }
- new IntersectionObserver(handleIntersection.bind(this), { rootMargin: '0px 0px 400px 0px' }).observe(this);
+ resetQuantityInput(id) {
+ const input = this.querySelector(`#Quantity-${id}`);
+ input.value = input.getAttribute('value');
+ this.isEnterPressed = false;
}
-}
-customElements.define('product-recommendations', ProductRecommendations);
+ setValidity(event, index, message) {
+ event.target.setCustomValidity(message);
+ event.target.reportValidity();
+ this.resetQuantityInput(index);
+ event.target.select();
+ }
-class AccountIcon extends HTMLElement {
- constructor() {
- super();
+ validateQuantity(event) {
+ const inputValue = parseInt(event.target.value);
+ const index = event.target.dataset.index;
- this.icon = this.querySelector('.icon');
+ if (inputValue < event.target.dataset.min) {
+ this.setValidity(event, index, window.quickOrderListStrings.min_error.replace('[min]', event.target.dataset.min));
+ } else if (inputValue > parseInt(event.target.max)) {
+ this.setValidity(event, index, window.quickOrderListStrings.max_error.replace('[max]', event.target.max));
+ } else if (inputValue % parseInt(event.target.step) != 0) {
+ this.setValidity(event, index, window.quickOrderListStrings.step_error.replace('[step]', event.target.step));
+ } else {
+ event.target.setCustomValidity('');
+ event.target.reportValidity();
+ this.startQueue(index, inputValue);
+ }
}
- connectedCallback() {
- document.addEventListener('storefront:signincompleted', this.handleStorefrontSignInCompleted.bind(this));
+ getSectionsUrl() {
+ if (window.pageNumber) {
+ return `${window.location.pathname}?page=${window.pageNumber}`;
+ } else {
+ return `${window.location.pathname}`;
+ }
}
- handleStorefrontSignInCompleted(event) {
- if (event?.detail?.avatar) {
- this.icon?.replaceWith(event.detail.avatar.cloneNode());
- }
+ getSectionInnerHTML(html, selector) {
+ return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML;
}
}
-customElements.define('account-icon', AccountIcon);
+if (!customElements.get('bulk-add')) {
+ customElements.define('bulk-add', BulkAdd);
+}
diff --git a/assets/localization-form.js b/assets/localization-form.js
index 4d532ece621..3eff4e41d4b 100644
--- a/assets/localization-form.js
+++ b/assets/localization-form.js
@@ -138,8 +138,15 @@ if (!customElements.get('localization-form')) {
}
}
+ normalizeString(str) {
+ return str
+ .normalize('NFD')
+ .replace(/\p{Diacritic}/gu, '')
+ .toLowerCase();
+ }
+
filterCountries() {
- const searchValue = this.elements.search.value.toLowerCase();
+ const searchValue = this.normalizeString(this.elements.search.value);
const popularCountries = this.querySelector('.popular-countries');
const allCountries = this.querySelectorAll('a');
let visibleCountries = allCountries.length;
@@ -151,7 +158,7 @@ if (!customElements.get('localization-form')) {
}
allCountries.forEach((item) => {
- const countryName = item.querySelector('.country').textContent.toLowerCase();
+ const countryName = this.normalizeString(item.querySelector('.country').textContent);
if (countryName.indexOf(searchValue) > -1) {
item.parentElement.classList.remove('hidden');
visibleCountries++;
diff --git a/assets/media-gallery.js b/assets/media-gallery.js
index a7cb8f7c3e5..c59cd4910ce 100644
--- a/assets/media-gallery.js
+++ b/assets/media-gallery.js
@@ -16,7 +16,7 @@ if (!customElements.get('media-gallery')) {
this.elements.thumbnails.querySelectorAll('[data-target]').forEach((mediaToSwitch) => {
mediaToSwitch
.querySelector('button')
- .addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target));
+ .addEventListener('click', this.setActiveMedia.bind(this, mediaToSwitch.dataset.target, false));
});
if (this.dataset.desktopLayout.includes('thumbnail') && this.mql.matches) this.removeListSemantic();
}
@@ -28,7 +28,7 @@ if (!customElements.get('media-gallery')) {
this.setActiveThumbnail(thumbnail);
}
- setActiveMedia(mediaId) {
+ setActiveMedia(mediaId, prepend) {
const activeMedia =
this.elements.viewer.querySelector(`[data-media-id="${mediaId}"]`) ||
this.elements.viewer.querySelector('[data-media-id]');
@@ -40,6 +40,17 @@ if (!customElements.get('media-gallery')) {
});
activeMedia?.classList?.add('is-active');
+ if (prepend) {
+ activeMedia.parentElement.firstChild !== activeMedia && activeMedia.parentElement.prepend(activeMedia);
+
+ if (this.elements.thumbnails) {
+ const activeThumbnail = this.elements.thumbnails.querySelector(`[data-target="${mediaId}"]`);
+ activeThumbnail.parentElement.firstChild !== activeThumbnail && activeThumbnail.parentElement.prepend(activeThumbnail);
+ }
+
+ if (this.elements.viewer.slider) this.elements.viewer.resetPages();
+ }
+
this.preventStickyHeader();
window.setTimeout(() => {
if (!this.mql.matches || this.elements.thumbnails) {
diff --git a/assets/pickup-availability.js b/assets/pickup-availability.js
index 56c6f71fbf1..1b5ebd63579 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 + '/';
@@ -34,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/predictive-search.js b/assets/predictive-search.js
index ed33c078d81..b30210be21c 100644
--- a/assets/predictive-search.js
+++ b/assets/predictive-search.js
@@ -240,7 +240,7 @@ class PredictiveSearch extends SearchForm {
getResultsMaxHeight() {
this.resultsMaxHeight =
- window.innerHeight - document.querySelector('.section-header').getBoundingClientRect().bottom;
+ window.innerHeight - document.querySelector('.section-header')?.getBoundingClientRect().bottom;
return this.resultsMaxHeight;
}
diff --git a/assets/product-form.js b/assets/product-form.js
index da186473160..59c19c9ec36 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.submitButton.querySelector('span');
if (document.querySelector('cart-drawer')) this.submitButton.setAttribute('aria-haspopup', 'dialog');
@@ -56,7 +57,7 @@ if (!customElements.get('product-form')) {
const soldOutMessage = this.submitButton.querySelector('.sold-out-message');
if (!soldOutMessage) return;
this.submitButton.setAttribute('aria-disabled', true);
- this.submitButton.querySelector('span').classList.add('hidden');
+ this.submitButtonText.classList.add('hidden');
soldOutMessage.classList.remove('hidden');
this.error = true;
return;
@@ -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 1fa35239125..5c362e35c12 100644
--- a/assets/product-info.js
+++ b/assets/product-info.js
@@ -2,85 +2,358 @@ if (!customElements.get('product-info')) {
customElements.define(
'product-info',
class ProductInfo extends HTMLElement {
+ quantityInput = undefined;
+ quantityForm = undefined;
+ onVariantChangeUnsubscriber = undefined;
+ cartUpdateUnsubscriber = undefined;
+ abortController = undefined;
+ pendingRequestUrl = null;
+ preProcessHtmlCallbacks = [];
+ postProcessHtmlCallbacks = [];
+
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.optionValueSelectionChange,
+ this.handleOptionValueChange.bind(this)
+ );
+
+ this.initQuantityHandlers();
+ this.dispatchEvent(new CustomEvent('product-info:loaded', { bubbles: true }));
+ }
+
+ addPreProcessCallback(callback) {
+ this.preProcessHtmlCallbacks.push(callback);
+ }
+
+ initQuantityHandlers() {
+ if (!this.quantityInput) return;
+
this.quantityForm = this.querySelector('.product-form__quantity');
if (!this.quantityForm) return;
+
this.setQuantityBoundries();
if (!this.dataset.originalSection) {
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.preProcessHtmlCallbacks.push((html) =>
+ html.querySelectorAll('.scroll-trigger').forEach((element) => element.classList.add('scroll-trigger--cancel'))
+ );
+ this.postProcessHtmlCallbacks.push((newNode) => {
+ window?.Shopify?.PaymentButton?.init();
+ window?.ProductModel?.loadShopifyXR();
+ });
+ }
+
+ handleOptionValueChange({ data: { event, target, selectedOptionValues } }) {
+ if (!this.contains(event.target)) return;
+
+ this.resetProductFormState();
+
+ const productUrl = target.dataset.productUrl || this.pendingRequestUrl || this.dataset.url;
+ this.pendingRequestUrl = productUrl;
+ const shouldSwapProduct = this.dataset.url !== productUrl;
+ const shouldFetchFullPage = this.dataset.updateUrl === 'true' && shouldSwapProduct;
+
+ this.renderProductInfo({
+ requestUrl: this.buildRequestUrlWithParams(productUrl, selectedOptionValues, shouldFetchFullPage),
+ targetId: target.id,
+ callback: shouldSwapProduct
+ ? this.handleSwapProduct(productUrl, shouldFetchFullPage)
+ : this.handleUpdateProductInfo(productUrl),
+ });
+ }
+
+ resetProductFormState() {
+ const productForm = this.productForm;
+ productForm?.toggleSubmitButton(true);
+ productForm?.handleErrorMessage();
+ }
+
+ handleSwapProduct(productUrl, updateFullPage) {
+ return (html) => {
+ this.productModal?.remove();
+
+ const selector = updateFullPage ? "product-info[id^='MainProduct']" : 'product-info';
+ const variant = this.getSelectedVariant(html.querySelector(selector));
+ this.updateURL(productUrl, variant?.id);
+
+ if (updateFullPage) {
+ document.querySelector('head title').innerHTML = html.querySelector('head title').innerHTML;
+
+ HTMLUpdateUtility.viewTransition(
+ document.querySelector('main'),
+ html.querySelector('main'),
+ this.preProcessHtmlCallbacks,
+ this.postProcessHtmlCallbacks
+ );
+ } else {
+ HTMLUpdateUtility.viewTransition(
+ this,
+ html.querySelector('product-info'),
+ this.preProcessHtmlCallbacks,
+ this.postProcessHtmlCallbacks
+ );
+ }
+ };
+ }
+
+ renderProductInfo({ requestUrl, targetId, callback }) {
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+
+ fetch(requestUrl, { signal: this.abortController.signal })
+ .then((response) => response.text())
+ .then((responseText) => {
+ this.pendingRequestUrl = null;
+ const html = new DOMParser().parseFromString(responseText, 'text/html');
+ callback(html);
+ })
+ .then(() => {
+ // set focus to last clicked option value
+ document.querySelector(`#${targetId}`)?.focus();
+ })
+ .catch((error) => {
+ if (error.name === 'AbortError') {
+ console.log('Fetch aborted by user');
+ } else {
+ console.error(error);
+ }
+ });
+ }
+
+ getSelectedVariant(productInfoNode) {
+ const selectedVariant = productInfoNode.querySelector('variant-selects [data-selected-variant]')?.innerHTML;
+ return !!selectedVariant ? JSON.parse(selectedVariant) : null;
+ }
+
+ buildRequestUrlWithParams(url, optionValues, shouldFetchFullPage = false) {
+ const params = [];
+
+ !shouldFetchFullPage && params.push(`section_id=${this.sectionId}`);
+
+ if (optionValues.length) {
+ params.push(`option_values=${optionValues.join(',')}`);
}
- if (this.variantChangeUnsubscriber) {
- this.variantChangeUnsubscriber();
+
+ return `${url}?${params.join('&')}`;
+ }
+
+ updateOptionValues(html) {
+ const variantSelects = html.querySelector('variant-selects');
+ if (variantSelects) {
+ HTMLUpdateUtility.viewTransition(this.variantSelectors, variantSelects, this.preProcessHtmlCallbacks);
}
}
+ handleUpdateProductInfo(productUrl) {
+ return (html) => {
+ const variant = this.getSelectedVariant(html);
+
+ this.pickupAvailability?.update(variant);
+ this.updateOptionValues(html);
+ this.updateURL(productUrl, variant?.id);
+ this.updateVariantInputs(variant?.id);
+
+ if (!variant) {
+ this.setUnavailable();
+ return;
+ }
+
+ this.updateMedia(html, variant?.featured_media?.id);
+
+ const updateSourceFromDestination = (id, shouldHide = (source) => false) => {
+ const source = html.getElementById(`${id}-${this.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(this.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-${this.sectionId}`)?.hasAttribute('disabled') ?? true,
+ window.variantStrings.soldOut
+ );
+
+ publish(PUB_SUB_EVENTS.variantChange, {
+ data: {
+ sectionId: this.sectionId,
+ html,
+ variant,
+ },
+ });
+ };
+ }
+
+ updateVariantInputs(variantId) {
+ this.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) {
+ this.querySelector('share-button')?.updateUrl(
+ `${window.shopUrl}${url}${variantId ? `?variant=${variantId}` : ''}`
+ );
+
+ if (this.dataset.updateUrl === 'false') return;
+ window.history.replaceState({}, '', `${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) {
+ if (!variantFeaturedMediaId) return;
+
+ const mediaGallerySource = this.querySelector('media-gallery ul');
+ const mediaGalleryDestination = html.querySelector(`media-gallery ul`);
+
+ const refreshSourceData = () => {
+ if (this.hasAttribute('data-zoom-on-hover')) enableZoomOnHover(2);
+ 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();
+ }
+ });
+ }
+
+ // set featured media as active in the media gallery
+ this.querySelector(`media-gallery`)?.setActiveMedia?.(
+ `${this.dataset.section}-${variantFeaturedMediaId}`,
+ true
+ );
+
+ // update media modal
+ const modalContent = this.productModal?.querySelector(`.product-media-modal__content`);
+ const newModalContent = html.querySelector(`product-modal .product-media-modal__content`);
+ if (modalContent && newModalContent) modalContent.innerHTML = newModalContent.innerHTML;
+ }
+
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;
const max = data.max === null ? data.max : data.max - data.cartQuantity;
if (max !== null) min = Math.min(min, max);
if (data.cartQuantity >= data.min) min = Math.min(min, data.step);
- this.input.min = min;
+
+ this.quantityInput.min = min;
if (max) {
- this.input.max = max;
+ this.quantityInput.max = max;
} else {
- this.input.removeAttribute('max');
+ this.quantityInput.removeAttribute('max');
}
- this.input.value = min;
+ this.quantityInput.value = min;
+
publish(PUB_SUB_EVENTS.quantityUpdate, undefined);
}
fetchQuantityRules() {
- if (!this.currentVariant || !this.currentVariant.value) return;
+ 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}`)
- .then((response) => {
- return response.text();
- })
+ fetch(`${this.dataset.url}?variant=${currentVariantId}§ion_id=${this.dataset.section}`)
+ .then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, 'text/html');
this.updateQuantityRules(this.dataset.section, html);
- this.setQuantityBoundries();
- })
- .catch((e) => {
- console.error(e);
})
- .finally(() => {
- this.querySelector('.quantity__rules-cart .loading__spinner').classList.add('hidden');
- });
+ .catch((e) => console.error(e))
+ .finally(() => this.querySelector('.quantity__rules-cart .loading__spinner').classList.add('hidden'));
}
updateQuantityRules(sectionId, html) {
+ if (!this.quantityInput) return;
+ this.setQuantityBoundries();
+
const quantityFormUpdated = html.getElementById(`Quantity-Form-${sectionId}`);
const selectors = ['.quantity__input', '.quantity__rules', '.quantity__label'];
for (let selector of selectors) {
@@ -102,6 +375,42 @@ 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');
+ }
+
+ get relatedProducts() {
+ const relatedProductsSectionId = SectionId.getIdForSection(
+ SectionId.parseId(this.sectionId),
+ 'related-products'
+ );
+ return document.querySelector(`product-recommendations[data-section-id^="${relatedProductsSectionId}"]`);
+ }
+
+ get quickOrderList() {
+ const quickOrderListSectionId = SectionId.getIdForSection(
+ SectionId.parseId(this.sectionId),
+ 'quick_order_list'
+ );
+ return document.querySelector(`quick-order-list[data-id^="${quickOrderListSectionId}"]`);
+ }
+
+ get sectionId() {
+ return this.dataset.originalSection || this.dataset.section;
+ }
}
);
}
diff --git a/assets/quick-add-bulk.js b/assets/quick-add-bulk.js
index 935d6f6a61d..f9195e2f087 100644
--- a/assets/quick-add-bulk.js
+++ b/assets/quick-add-bulk.js
@@ -1,16 +1,16 @@
if (!customElements.get('quick-add-bulk')) {
customElements.define(
'quick-add-bulk',
- class QuickAddBulk extends HTMLElement {
+ class QuickAddBulk extends BulkAdd {
constructor() {
super();
this.quantity = this.querySelector('quantity-input');
const debouncedOnChange = debounce((event) => {
- if (parseInt(event.target.dataset.cartQuantity) === 0) {
- this.addToCart(event);
+ if (parseInt(event.target.value) === 0) {
+ this.startQueue(event.target.dataset.index, parseInt(event.target.value));
} else {
- this.updateCart(event);
+ this.validateQuantity(event);
}
}, ON_CHANGE_DEBOUNCE_TIMER);
@@ -24,7 +24,11 @@ if (!customElements.get('quick-add-bulk')) {
connectedCallback() {
this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, (event) => {
- if (event.source === 'quick-add') {
+ if (
+ event.source === 'quick-add' ||
+ (event.cartData.items && !event.cartData.items.some((item) => item.id === parseInt(this.dataset.index))) ||
+ (event.cartData.variant_id && !(event.cartData.variant_id === parseInt(this.dataset.index)))
+ ) {
return;
}
// If its another section that made the update
@@ -65,12 +69,6 @@ if (!customElements.get('quick-add-bulk')) {
});
}
- resetQuantityInput(id) {
- const input = document.getElementById(id);
- input.value = input.getAttribute('value');
- this.isEnterPressed = false;
- }
-
cleanErrorMessageOnType(event) {
event.target.addEventListener(
'keypress',
@@ -102,81 +100,37 @@ if (!customElements.get('quick-add-bulk')) {
});
}
- updateCart(event) {
- this.lastActiveInputId = event.target.getAttribute('data-index');
- this.quantity.classList.add('quantity__input-disabled');
+ updateMultipleQty(items) {
this.selectProgressBar().classList.remove('hidden');
+
+ const ids = Object.keys(items);
const body = JSON.stringify({
- quantity: event.target.value,
- id: event.target.getAttribute('data-index'),
+ updates: items,
sections: this.getSectionsToRender().map((section) => section.section),
sections_url: this.getSectionsUrl(),
});
- fetch(`${routes.cart_change_url}`, { ...fetchConfig('javascript'), ...{ body } })
+ fetch(`${routes.cart_update_url}`, { ...fetchConfig(), ...{ body } })
.then((response) => {
return response.text();
})
.then((state) => {
const parsedState = JSON.parse(state);
- this.quantity.classList.remove('quantity__input-disabled');
- if (parsedState.description || parsedState.errors) {
- event.target.setCustomValidity(parsedState.description);
- event.target.reportValidity();
- this.resetQuantityInput(event.target.id);
- this.selectProgressBar().classList.add('hidden');
- event.target.select();
- this.cleanErrorMessageOnType(event);
- return;
- }
-
- this.renderSections(parsedState);
-
+ this.renderSections(parsedState, ids);
publish(PUB_SUB_EVENTS.cartUpdate, { source: 'quick-add', cartData: parsedState });
})
- .catch((error) => {
- console.log(error, 'error');
- });
- }
-
- addToCart(event) {
- this.quantity.classList.add('quantity__input-disabled');
- this.selectProgressBar().classList.remove('hidden');
- this.lastActiveInputId = event.target.getAttribute('data-index');
- const body = JSON.stringify({
- items: [
- {
- quantity: parseInt(event.target.value),
- id: parseInt(this.dataset.id),
- },
- ],
- sections: this.getSectionsToRender().map((section) => section.section),
- });
-
- fetch(`${routes.cart_add_url}`, { ...fetchConfig('javascript'), ...{ body } })
- .then((response) => {
- return response.text();
+ .catch(() => {
+ // Commented out for now and will be fixed when BE issue is done https://github.com/Shopify/shopify/issues/440605
+ // e.target.setCustomValidity(error);
+ // e.target.reportValidity();
+ // this.resetQuantityInput(ids[index]);
+ // this.selectProgressBar().classList.add('hidden');
+ // e.target.select();
+ // this.cleanErrorMessageOnType(e);
})
- .then((state) => {
- const parsedState = JSON.parse(state);
- this.quantity.classList.remove('quantity__input-disabled');
- if (parsedState.description || parsedState.errors) {
- event.target.setCustomValidity(parsedState.description);
- event.target.reportValidity();
- this.resetQuantityInput(event.target.id);
- this.selectProgressBar().classList.add('hidden');
- event.target.select();
- this.cleanErrorMessageOnType(event);
- // Error handling
- return;
- }
-
- this.renderSections(parsedState);
-
- publish(PUB_SUB_EVENTS.cartUpdate, { source: 'quick-add', cartData: parsedState });
- })
- .catch((error) => {
- console.error(error);
+ .finally(() => {
+ this.selectProgressBar().classList.add('hidden');
+ this.requestStarted = false;
});
}
@@ -200,19 +154,9 @@ if (!customElements.get('quick-add-bulk')) {
];
}
- getSectionsUrl() {
- if (window.pageNumber) {
- return `${window.location.pathname}?page=${window.pageNumber}`;
- } else {
- return `${window.location.pathname}`;
- }
- }
-
- getSectionInnerHTML(html, selector) {
- return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML;
- }
-
- renderSections(parsedState) {
+ renderSections(parsedState, ids) {
+ const intersection = this.queue.filter((element) => ids.includes(element.id));
+ if (intersection.length !== 0) return;
this.getSectionsToRender().forEach((section) => {
const sectionElement = document.getElementById(section.id);
if (
diff --git a/assets/quick-add.css b/assets/quick-add.css
index b9d889ad5a9..0e597b61d48 100644
--- a/assets/quick-add.css
+++ b/assets/quick-add.css
@@ -32,6 +32,11 @@
.quick-add-modal .scroll-trigger.scroll-trigger {
animation: none;
opacity: 1;
+ transform: none;
+}
+
+.quick-add-modal .quick-order-list__container {
+ padding-bottom: 1.5rem;
}
.quick-add-modal__content.quick-add-modal__content--bulk {
@@ -77,6 +82,10 @@
max-width: 100%;
}
+.quick-add-modal__content-info.quick-add-modal__content-info--bulk {
+ padding-bottom: 0;
+}
+
.quick-add-modal__content-info--bulk h3 {
margin-bottom: 0.5rem;
margin-top: 0;
@@ -87,7 +96,17 @@
display: inline-block;
}
+.section-bulk-quick-order-list-padding {
+ padding-top: 2.7rem;
+ padding-bottom: 2.7rem;
+}
+
@media screen and (min-width: 750px) {
+ .section-bulk-quick-order-list-padding {
+ padding-top: 3.6rem;
+ padding-bottom: 3.6rem;
+ }
+
.quick-add-modal__content-info--bulk .card__information-volume-pricing-note {
padding-left: 1.6rem;
}
@@ -121,6 +140,11 @@
padding-left: 1rem;
}
+.quick-add-modal__content-info--bulk-details > a:hover {
+ text-decoration: underline;
+ text-underline-offset: 0.3rem;
+}
+
@media screen and (min-width: 990px) {
.quick-add-modal__content-info--bulk .quick-add__product-media,
.quick-add-modal__content-info--bulk .quick-add__product-container,
@@ -153,7 +177,7 @@
width: auto;
}
-@media screen and (max-width: 990px) {
+@media screen and (max-width: 989px) {
.quick-add-modal__content-info--bulk .quick-add__content-info__media {
display: flex;
margin: 0;
@@ -169,7 +193,7 @@
}
}
-@media screen and (min-width: 989px) {
+@media screen and (min-width: 990px) {
.quick-add-modal__content-info--bulk .quick-add__info {
flex-direction: column;
position: sticky;
@@ -179,7 +203,7 @@
}
}
-@media screen and (max-width: 990px) {
+@media screen and (max-width: 989px) {
.quick-add-modal__content-info--bulk {
flex-direction: column;
}
@@ -201,6 +225,10 @@
width: 100%;
}
+.quick-add-modal__content-info > product-info {
+ padding: 0;
+}
+
@media screen and (max-width: 749px) {
quick-add-modal .slider .product__media-item.grid__item {
margin-left: 1.5rem;
@@ -251,6 +279,10 @@ quick-add-modal .product:not(.featured-product) .product__view-details {
display: block;
}
+.quick-add-modal__content--bulk .product__view-details .icon {
+ margin-left: 1.2rem;
+}
+
quick-add-modal .quick-add-hidden,
quick-add-modal .product__modal-opener:not(.product__modal-opener--image),
quick-add-modal .product__media-item:not(:first-child) {
@@ -282,6 +314,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);
@@ -355,6 +388,7 @@ quick-add-bulk .progress-bar-container {
overflow: hidden;
border-radius: var(--inputs-radius-outset);
border: var(--inputs-border-width) solid transparent;
+ z-index: -1;
}
quick-add-bulk quantity-input {
diff --git a/assets/quick-add.js b/assets/quick-add.js
index 6a9d5573ce3..5125a974c2f 100644
--- a/assets/quick-add.js
+++ b/assets/quick-add.js
@@ -5,6 +5,10 @@ if (!customElements.get('quick-add-modal')) {
constructor() {
super();
this.modalContent = this.querySelector('[id^="QuickAddInfo-"]');
+
+ this.addEventListener('product-info:loaded', ({ target }) => {
+ target.addPreProcessCallback(this.preprocessHTML.bind(this));
+ });
}
hide(preventFocus = false) {
@@ -25,24 +29,16 @@ 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('product-info');
+
+ this.preprocessHTML(productElement);
+ HTMLUpdateUtility.setInnerHTML(this.modalContent, productElement.outerHTML);
if (window.Shopify && Shopify.PaymentButton) {
Shopify.PaymentButton.init();
}
-
if (window.ProductModel) window.ProductModel.loadShopifyXR();
- this.removeGalleryListSemantic();
- this.updateImageSizes();
- this.preventVariantURLSwitching();
super.show(opener);
})
.finally(() => {
@@ -52,57 +48,59 @@ if (!customElements.get('quick-add-modal')) {
});
}
- 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);
- });
- 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');
- if (!variantPicker) return;
-
- variantPicker.setAttribute('data-update-url', 'false');
+ preventVariantURLSwitching(productElement) {
+ productElement.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) => {
- element.dataset.originalSection = sectionId;
+ preventDuplicatedIDs(productElement) {
+ const sectionId = productElement.dataset.section;
+
+ const oldId = sectionId;
+ const newId = `quickadd-${sectionId}`;
+ productElement.innerHTML = productElement.innerHTML.replaceAll(oldId, newId);
+ Array.from(productElement.attributes).forEach((attribute) => {
+ if (attribute.value.includes(oldId)) {
+ productElement.setAttribute(attribute.name, attribute.value.replace(oldId, newId));
+ }
});
+
+ productElement.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');
- const desktopColumns = product.classList.contains('product--columns');
+ updateImageSizes(productElement) {
+ const product = productElement.querySelector('.product');
+ const desktopColumns = product?.classList.contains('product--columns');
if (!desktopColumns) return;
const mediaImages = product.querySelectorAll('.product__media img');
diff --git a/assets/quick-order-list.css b/assets/quick-order-list.css
index e2a3c010ab9..fb1468e309a 100644
--- a/assets/quick-order-list.css
+++ b/assets/quick-order-list.css
@@ -36,6 +36,10 @@ quick-order-list .quantity__button {
z-index: 1;
}
+.variant-item__image-container.global-media-settings::after {
+ content: none;
+}
+
@media screen and (min-width: 990px) {
.quick-order-list__total {
position: sticky;
@@ -305,6 +309,10 @@ quick-order-list-remove-button .icon-remove {
left: 2rem;
top: 1.2rem;
}
+
+ .variant-remove-total--empty .loading__spinner {
+ top: -1rem;
+ }
}
quick-order-list-remove-button:hover .icon-remove {
@@ -442,6 +450,10 @@ quick-order-list-remove-button:hover .icon-remove {
}
}
+.quick-order-list__button-text {
+ text-align: center;
+}
+
.quick-order-list-total__confirmation {
display: flex;
justify-content: center;
diff --git a/assets/quick-order-list.js b/assets/quick-order-list.js
index bc4d6d0843f..d4a77ffac57 100644
--- a/assets/quick-order-list.js
+++ b/assets/quick-order-list.js
@@ -1,13 +1,12 @@
if (!customElements.get('quick-order-list-remove-button')) {
customElements.define(
'quick-order-list-remove-button',
- class QuickOrderListRemoveButton extends HTMLElement {
+ class QuickOrderListRemoveButton extends BulkAdd {
constructor() {
super();
this.addEventListener('click', (event) => {
event.preventDefault();
- const quickOrderList = this.closest('quick-order-list');
- quickOrderList.updateQuantity(this.dataset.index, 0);
+ this.startQueue(this.dataset.index, 0);
});
}
}
@@ -69,16 +68,11 @@ if (!customElements.get('quick-order-list-remove-all-button')) {
if (!customElements.get('quick-order-list')) {
customElements.define(
'quick-order-list',
- class QuickOrderList extends HTMLElement {
+ class QuickOrderList extends BulkAdd {
constructor() {
super();
this.cart = document.querySelector('cart-drawer');
- this.actions = {
- add: 'ADD',
- update: 'UPDATE',
- };
-
- this.quickOrderListId = `quick-order-list-${this.dataset.productId}`;
+ this.quickOrderListId = `${this.dataset.section}-${this.dataset.productId}`;
this.defineInputsAndQuickOrderTable();
this.variantItemStatusElement = document.getElementById('shopping-cart-variant-item-status');
@@ -119,7 +113,14 @@ if (!customElements.get('quick-order-list')) {
connectedCallback() {
this.cartUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.cartUpdate, (event) => {
- if (event.source === this.quickOrderListId) {
+ const variantIds = [];
+ this.querySelectorAll('.variant-item').forEach((item) => {
+ variantIds.push(parseInt(item.dataset.variantId));
+ });
+ if (
+ event.source === this.quickOrderListId ||
+ !event.cartData.items?.some((element) => variantIds.includes(element.variant_id))
+ ) {
return;
}
// If its another section that made the update
@@ -128,7 +129,7 @@ if (!customElements.get('quick-order-list')) {
this.addMultipleDebounce();
});
});
- this.sectionId = this.dataset.id;
+ this.sectionId = this.dataset.section;
}
disconnectedCallback() {
@@ -143,16 +144,11 @@ if (!customElements.get('quick-order-list')) {
onChange(event) {
const inputValue = parseInt(event.target.value);
- const cartQuantity = parseInt(event.target.dataset.cartQuantity);
- const index = event.target.dataset.index;
- const name = document.activeElement.getAttribute('name');
-
- const quantity = inputValue - cartQuantity;
this.cleanErrorMessageOnType(event);
if (inputValue == 0) {
- this.updateQuantity(index, inputValue, name, this.actions.update);
+ this.startQueue(event.target.dataset.index, inputValue);
} else {
- this.validateQuantity(event, name, index, inputValue, cartQuantity, quantity);
+ this.validateQuantity(event);
}
}
@@ -163,35 +159,6 @@ if (!customElements.get('quick-order-list')) {
});
}
- validateQuantity(event, name, index, inputValue, cartQuantity, quantity) {
- if (inputValue < event.target.dataset.min) {
- this.setValidity(
- event,
- index,
- window.quickOrderListStrings.min_error.replace('[min]', event.target.dataset.min)
- );
- } else if (inputValue > parseInt(event.target.max)) {
- this.setValidity(event, index, window.quickOrderListStrings.max_error.replace('[max]', event.target.max));
- } else if (inputValue % parseInt(event.target.step) != 0) {
- this.setValidity(event, index, window.quickOrderListStrings.step_error.replace('[step]', event.target.step));
- } else {
- event.target.setCustomValidity('');
- event.target.reportValidity();
- if (cartQuantity > 0) {
- this.updateQuantity(index, inputValue, name, this.actions.update);
- } else {
- this.updateQuantity(index, quantity, name, this.actions.add);
- }
- }
- }
-
- setValidity(event, index, message) {
- event.target.setCustomValidity(message);
- event.target.reportValidity();
- this.resetQuantityInput(index);
- event.target.select();
- }
-
validateInput(target) {
if (target.max) {
return (
@@ -232,7 +199,7 @@ if (!customElements.get('quick-order-list')) {
return [
{
id: this.quickOrderListId,
- section: document.getElementById(this.quickOrderListId).dataset.id,
+ section: document.getElementById(this.quickOrderListId).dataset.section,
selector: `#${this.quickOrderListId} .js-contents`,
},
{
@@ -246,8 +213,8 @@ if (!customElements.get('quick-order-list')) {
selector: '.shopify-section',
},
{
- id: `quick-order-list-total-${this.dataset.productId}`,
- section: document.getElementById(this.quickOrderListId).dataset.id,
+ id: `quick-order-list-total-${this.dataset.productId}-${this.dataset.section}`,
+ section: document.getElementById(this.quickOrderListId).dataset.section,
selector: `#${this.quickOrderListId} .quick-order-list__total`,
},
{
@@ -262,20 +229,16 @@ if (!customElements.get('quick-order-list')) {
this.querySelectorAll('quantity-input').forEach((qty) => {
const debouncedOnChange = debounce((event) => {
this.onChange(event);
- }, ON_CHANGE_DEBOUNCE_TIMER);
+ }, 100);
qty.addEventListener('change', debouncedOnChange.bind(this));
});
}
- addDebounce(id) {
- const element = this.querySelector(`#Variant-${id} quantity-input`);
- const debouncedOnChange = debounce((event) => {
- this.onChange(event);
- }, ON_CHANGE_DEBOUNCE_TIMER);
- element.addEventListener('change', debouncedOnChange.bind(this));
- }
+ renderSections(parsedState, ids) {
+ this.ids.push(ids);
+ const intersection = this.queue.filter((element) => ids.includes(element.id));
+ if (intersection.length !== 0) return;
- renderSections(parsedState, id) {
this.getSectionsToRender().forEach((section) => {
const sectionElement = document.getElementById(section.id);
if (
@@ -295,11 +258,13 @@ if (!customElements.get('quick-order-list')) {
? sectionElement.querySelector(section.selector)
: sectionElement;
if (elementToReplace) {
- if (section.selector === `#${this.quickOrderListId} .js-contents` && id !== undefined) {
- elementToReplace.querySelector(`#Variant-${id}`).innerHTML = this.getSectionInnerHTML(
- parsedState.sections[section.section],
- `#Variant-${id}`
- );
+ if (section.selector === `#${this.quickOrderListId} .js-contents` && this.ids.length > 0) {
+ this.ids.flat().forEach((i) => {
+ elementToReplace.querySelector(`#Variant-${i}`).innerHTML = this.getSectionInnerHTML(
+ parsedState.sections[section.section],
+ `#Variant-${i}`
+ );
+ });
} else {
elementToReplace.innerHTML = this.getSectionInnerHTML(
parsedState.sections[section.section],
@@ -309,11 +274,8 @@ if (!customElements.get('quick-order-list')) {
}
});
this.defineInputsAndQuickOrderTable();
- if (id) {
- this.addDebounce(id);
- } else {
- this.addMultipleDebounce();
- }
+ this.addMultipleDebounce();
+ this.ids = [];
}
getTableHead() {
@@ -407,12 +369,13 @@ if (!customElements.get('quick-order-list')) {
}
updateMultipleQty(items) {
- this.querySelector('.variant-remove-total .loading__spinner').classList.remove('hidden');
+ this.querySelector('.variant-remove-total .loading__spinner')?.classList.remove('hidden');
+ const ids = Object.keys(items);
const body = JSON.stringify({
updates: items,
sections: this.getSectionsToRender().map((section) => section.section),
- sections_url: this.getSectionsUrl(),
+ sections_url: this.dataset.url,
});
this.updateMessage();
@@ -424,121 +387,18 @@ if (!customElements.get('quick-order-list')) {
})
.then((state) => {
const parsedState = JSON.parse(state);
- this.renderSections(parsedState);
- })
- .catch(() => {
- this.setErrorMessage(window.cartStrings.error);
- })
- .finally(() => {
- this.querySelector('.variant-remove-total .loading__spinner').classList.add('hidden');
- });
- }
-
- getSectionsUrl() {
- if (window.pageNumber) {
- return `${window.location.pathname}?page=${window.pageNumber}`;
- } else {
- return `${window.location.pathname}`;
- }
- }
-
- updateQuantity(id, quantity, name, action) {
- this.toggleLoading(id, true);
- this.cleanErrors();
-
- let routeUrl = routes.cart_change_url;
- let body = JSON.stringify({
- quantity,
- id,
- sections: this.getSectionsToRender().map((section) => section.section),
- sections_url: this.getSectionsUrl(),
- });
- let fetchConfigType;
- if (action === this.actions.add) {
- fetchConfigType = 'javascript';
- routeUrl = routes.cart_add_url;
- body = JSON.stringify({
- items: [
- {
- quantity: parseInt(quantity),
- id: parseInt(id),
- },
- ],
- sections: this.getSectionsToRender().map((section) => section.section),
- sections_url: this.getSectionsUrl(),
- });
- }
-
- this.updateMessage();
- this.setErrorMessage();
-
- fetch(`${routeUrl}`, { ...fetchConfig(fetchConfigType), ...{ body } })
- .then((response) => {
- return response.text();
- })
- .then((state) => {
- const parsedState = JSON.parse(state);
- const quantityElement = document.getElementById(`Quantity-${id}`);
- const items = document.querySelectorAll('.variant-item');
-
- if (parsedState.description || parsedState.errors) {
- const variantItem = document.querySelector(
- `[id^="Variant-${id}"] .variant-item__totals.small-hide .loading__spinner`
- );
- variantItem.classList.add('loading__spinner--error');
- this.resetQuantityInput(id, quantityElement);
- if (parsedState.errors) {
- this.updateLiveRegions(id, parsedState.errors);
- } else {
- this.updateLiveRegions(id, parsedState.description);
- }
- return;
- }
-
- this.classList.toggle('is-empty', parsedState.item_count === 0);
-
- this.renderSections(parsedState, id);
-
- let hasError = false;
-
- const currentItem = parsedState.items.find((item) => item.variant_id === parseInt(id));
- const updatedValue = currentItem ? currentItem.quantity : undefined;
- if (updatedValue && updatedValue !== quantity) {
- this.updateError(updatedValue, id);
- hasError = true;
- }
-
+ this.renderSections(parsedState, ids);
publish(PUB_SUB_EVENTS.cartUpdate, { source: this.quickOrderListId, cartData: parsedState });
-
- if (hasError) {
- this.updateMessage();
- } else if (action === this.actions.add) {
- this.updateMessage(parseInt(quantity));
- } else if (action === this.actions.update) {
- this.updateMessage(parseInt(quantity - quantityElement.dataset.cartQuantity));
- } else {
- this.updateMessage(-parseInt(quantityElement.dataset.cartQuantity));
- }
})
- .catch((error) => {
- this.querySelectorAll('.loading__spinner').forEach((overlay) => overlay.classList.add('hidden'));
- this.resetQuantityInput(id);
- console.error(error);
+ .catch(() => {
this.setErrorMessage(window.cartStrings.error);
})
.finally(() => {
- this.toggleLoading(id);
- if (this.lastKey && this.lastElement === id) {
- this.querySelector(`#Variant-${id} input`).select();
- }
+ this.querySelector('.variant-remove-total .loading__spinner')?.classList.add('hidden');
+ this.requestStarted = false;
});
}
- resetQuantityInput(id, quantityElement) {
- const input = quantityElement ?? document.getElementById(`Quantity-${id}`);
- input.value = input.getAttribute('value');
- }
-
setErrorMessage(message = null) {
this.errorMessageTemplate =
this.errorMessageTemplate ??
@@ -592,9 +452,9 @@ if (!customElements.get('quick-order-list')) {
this.updateLiveRegions(id, message);
}
- cleanErrors() {
- this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden'));
- this.querySelectorAll(`.variant-item__error-text`).forEach((error) => (error.innerHTML = ''));
+ cleanErrors(id) {
+ // this.querySelectorAll('.desktop-row-error').forEach((error) => error.classList.add('hidden'));
+ // this.querySelectorAll(`.variant-item__error-text`).forEach((error) => error.innerHTML = '');
}
updateLiveRegions(id, message) {
@@ -617,10 +477,6 @@ if (!customElements.get('quick-order-list')) {
}, 1000);
}
- getSectionInnerHTML(html, selector) {
- return new DOMParser().parseFromString(html, 'text/html').querySelector(selector).innerHTML;
- }
-
toggleLoading(id, enable) {
const quickOrderListItems = this.querySelectorAll(`#Variant-${id} .loading__spinner`);
const quickOrderListItem = this.querySelector(`#Variant-${id}`);
diff --git a/assets/section-main-product.css b/assets/section-main-product.css
index 96e6763eaee..2b7afd744f0 100644
--- a/assets/section-main-product.css
+++ b/assets/section-main-product.css
@@ -1,3 +1,7 @@
+product-info {
+ display: block;
+}
+
.product {
margin: 0;
}
diff --git a/config/settings_schema.json b/config/settings_schema.json
index b06a9fc7047..c08c9e94fbe 100644
--- a/config/settings_schema.json
+++ b/config/settings_schema.json
@@ -2,7 +2,7 @@
{
"name": "theme_info",
"theme_name": "Dawn",
- "theme_version": "13.0.0",
+ "theme_version": "15.0.0",
"theme_author": "Shopify",
"theme_documentation_url": "https://help.shopify.com/manual/online-store/themes",
"theme_support_url": "https://support.shopify.com/"
diff --git a/layout/theme.liquid b/layout/theme.liquid
index aec1924cc34..35f9fb6f77f 100644
--- a/layout/theme.liquid
+++ b/layout/theme.liquid
@@ -26,6 +26,10 @@
+
+
+
+
{%- if settings.animations_reveal_on_scroll -%}
{%- endif -%}
@@ -241,6 +245,17 @@
{% endstyle %}
{{ 'base.css' | asset_url | stylesheet_tag }}
{{ 'custom-icletta.css' | asset_url | stylesheet_tag }}
+
+
+
+ {%- if settings.cart_type == 'drawer' -%}
+ {{ 'component-cart-drawer.css' | asset_url | stylesheet_tag }}
+ {{ 'component-cart.css' | asset_url | stylesheet_tag }}
+ {{ 'component-totals.css' | asset_url | stylesheet_tag }}
+ {{ 'component-price.css' | asset_url | stylesheet_tag }}
+ {{ 'component-discounts.css' | asset_url | stylesheet_tag }}
+ {%- endif -%}
+
{%- unless settings.type_body_font.system? -%}
{% comment %}theme-check-disable AssetPreload{% endcomment %}
@@ -335,6 +350,9 @@
{%- if settings.predictive_search_enabled -%}
{%- endif -%}
- {% render 'bc_banner' %}
+
+ {%- if settings.cart_type == 'drawer' -%}
+
+ {%- endif -%}