How do I assign colour variant to product image for that specific colour on dawn theme 15.0 - code

Topic summary

A Shopify store owner using Dawn theme 15.0 seeks to link color variant selections to specific product images, so clicking a color (e.g., orange) displays only that color’s images.

Two Main Solutions Emerged:

  1. No-Code Method: Assign featured images to variants in product admin and enable “Pick an option” in the theme customizer. Limitation: shows only one image per variant, not multiple images for the same color.

  2. Code-Based Solution: Requires three steps:

    • Add alt text to product images matching variant color names exactly
    • Replace code in product-media-gallery.liquid file
    • Add JavaScript to main-product.js

Key Challenge: Users want all product images visible initially, but filtered to show only relevant color images when a variant is selected. The default behavior shows either all images or only single variant images.

Current Status: Mixed results reported. Some users successfully implemented the code solution, while others (particularly on Trade theme) report it’s not working. The discussion remains active with ongoing troubleshooting, particularly around displaying multiple images per color variant while maintaining proper filtering functionality.

Summarized with AI on October 29. AI used: claude-sonnet-4-5-20250929.

@kpsclothing Alright So i am pretty much done. This took a lot of looking up and workarounds, i will admit that. Let me explain the steps. There are four steps please look carefully and copy paste code. the part of naming of alt images will be the same as before. Just make these changes.

/////////// First Step ///////////

Open product-media-gallery.liquid file and replace with this code

@media(min-width:767px){ .product__media-list li:not(.is-active){ display: none !important; } }

{% comment %}
Renders a product media gallery. Should be used with ‘media-gallery.js’
Also see ‘product-media-modal’

Accepts:

  • product: {Object} Product liquid object
  • variant_images: {Array} Product images associated with a variant
  • limit: {Number} (optional) When passed, limits the number of media items to render

Usage:
{% render ‘product-media-gallery’ %}
{% endcomment %}

{%- liquid
if section.settings.hide_variants and variant_images.size == product.media.size
assign single_media_visible = true
endif

if limit == 1
assign single_media_visible = true
endif

assign media_count = product.media.size
if section.settings.hide_variants and media_count > 1 and variant_images.size > 0
assign media_count = media_count | minus: variant_images.size | plus: 1
endif

if media_count == 1 or single_media_visible
assign single_media_visible_mobile = true
endif

if media_count == 0 or single_media_visible_mobile or section.settings.mobile_thumbnails == ‘show’ or section.settings.mobile_thumbnails == ‘columns’ and media_count < 3
assign hide_mobile_slider = true
endif

if section.settings.media_size == ‘large’
assign media_width = 0.65
elsif section.settings.media_size == ‘medium’
assign media_width = 0.55
elsif section.settings.media_size == ‘small’
assign media_width = 0.45
endif
-%}

<media-gallery
id=“MediaGallery-{{ section.id }}”
role=“region”
{% if section.settings.enable_sticky_info %}
class=“product__column-sticky”
{% endif %}
aria-label=“{{ ‘products.product.media.gallery_viewer’ | t }}”
data-desktop-layout=“{{ section.settings.gallery_layout }}”

{{ 'accessibility.skip_to_product_info' | t }} {{ isMobileView }}
    {%- if product.selected_or_first_available_variant.featured_media != null -%} {%- assign featured_media = product.selected_or_first_available_variant.featured_media -%}
  • {%- assign media_position = 1 -%} {% render 'product-thumbnail', media: featured_media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: false %}
  • {%- endif -%} {%- for media in product.media -%} {% if media_position >= limit or media_position >= 1 and section.settings.hide_variants and variant_images contains media.src %} {% continue %} {% endif %} {%- unless media.id == product.selected_or_first_available_variant.featured_media.id -%}
  • {%- liquid assign media_position = media_position | default: 0 | plus: 1 assign lazy_load = false if media_position > 1 assign lazy_load = true endif -%} {% render 'product-thumbnail', media: media, media_count: media_count, position: media_position, desktop_layout: section.settings.gallery_layout, mobile_layout: section.settings.mobile_thumbnails, loop: section.settings.enable_video_looping, modal_id: section.id, xr_button: true, media_width: media_width, media_fit: section.settings.media_fit, constrain_to_viewport: section.settings.constrain_to_viewport, lazy_load: lazy_load %}
  • {%- endunless -%} {%- endfor -%}
{% render 'icon-caret' %}
1 / {{ 'general.slider.of' | t }} {{ media_count }}
{% render 'icon-caret' %}
{%- if first_3d_model -%} {% render 'icon-3d-model' %} {{ 'products.product.xr_button' | t }} {%- endif -%} {%- liquid assign is_not_limited_to_single_item = false if limit == null or limit > 1 assign is_not_limited_to_single_item = true endif -%} {%- if is_not_limited_to_single_item and media_count > 1 and section.settings.gallery_layout contains 'thumbnail' or section.settings.mobile_thumbnails == 'show' -%} {% render 'icon-caret' %}
    {%- capture sizes -%} (min-width: {{ settings.page_width }}px) calc(({{ settings.page_width | minus: 100 | times: media_width | round }} - 4rem) / 4), (min-width: 990px) calc(({{ media_width | times: 100 }}vw - 4rem) / 4), (min-width: 750px) calc((100vw - 15rem) / 8), calc((100vw - 8rem) / 3) {%- endcapture -%}

    {%- if featured_media != null -%}
    {%- liquid
    capture media_index
    if featured_media.media_type == ‘model’
    increment model_index
    elsif featured_media.media_type == ‘video’ or featured_media.media_type == ‘external_video’
    increment video_index
    elsif featured_media.media_type == ‘image’
    increment image_index
    endif
    endcapture
    assign media_index = media_index | plus: 1
    -%}

  • {%- capture thumbnail_id -%} Thumbnail-{{ section.id }}-0 {%- endcapture -%} {{ featured_media.preview_image | image_url: width: 416 | image_tag: loading: 'lazy', sizes: sizes, widths: '54, 74, 104, 162, 208, 324, 416', id: thumbnail_id, alt: featured_media.alt | escape }}
  • {%- endif -%} {%- for media in product.media -%} {%- unless media.id == product.selected_or_first_available_variant.featured_media.id -%} {%- liquid capture media_index if media.media_type == 'model' increment model_index elsif media.media_type == 'video' or media.media_type == 'external_video' increment video_index elsif media.media_type == 'image' increment image_index endif endcapture assign media_index = media_index | plus: 1 -%}
  • {%- if media.media_type == 'model' -%} {%- render 'icon-3d-model' -%} {%- elsif media.media_type == 'video' or media.media_type == 'external_video' -%} {%- render 'icon-play' -%} {%- endif -%} {%- capture thumbnail_id -%} Thumbnail-{{ section.id }}-{{ forloop.index }} {%- endcapture -%} {{ media.preview_image | image_url: width: 416 | image_tag: loading: 'lazy', sizes: sizes, widths: '54, 74, 104, 162, 208, 324, 416', id: thumbnail_id, alt: media.alt | escape }}
  • {%- endunless -%} {%- endfor -%}
{% render 'icon-caret' %} {%- endif -%}

///////// Second step ///////////

Open product-info.js and replace with this code

if (!customElements.get(‘product-info’)) {
customElements.define(
‘product-info’,
class ProductInfo extends HTMLElement {
quantityInput = undefined;
quantityForm = undefined;
onVariantChangeUnsubscriber = undefined;
onVariantLoadSubscriber = undefined;
cartUpdateUnsubscriber = undefined;
abortController = undefined;
pendingRequestUrl = null;
preProcessHtmlCallbacks = ;
postProcessHtmlCallbacks = ;

constructor() {
super();

this.quantityInput = this.querySelector(‘.quantity__input’);
}

connectedCallback() {
this.initializeProductSwapUtility();

this.onVariantChangeUnsubscriber = subscribe(
PUB_SUB_EVENTS.optionValueSelectionChange,
this.handleOptionValueChange.bind(this)
);
this.onVariantLoadSubscriber = subscribe(
PUB_SUB_EVENTS.onVariantLoadChange,
this.handleOptionValueLoad.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));
}
}

disconnectedCallback() {
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 } }) {
console.log(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),
});
}

handleOptionValueLoad({ data: { target } }) {
const selectedVariant = target.querySelector(‘variant-selects [data-selected-variant]’)?.innerHTML;
const variant = !!selectedVariant ? JSON.parse(selectedVariant) : null;
console.log(variant);

if(variant.featured_image && variant.featured_image.alt){

document.querySelectorAll(‘[thumbnail-alt]’).forEach(img => img.style.display = ‘none’);
const currentImageAlt = variant.featured_image.alt;
const thumbnailSelector= [thumbnail-alt= '${currentImageAlt}'];
document.querySelectorAll(thumbnailSelector).forEach((img) => {
img.style.display = ‘block’;
})
} else{
document.querySelectorAll(‘[thumbnail-alt]’).forEach(img => img.style.display = ‘block’);
}

}

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(',')});
}

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);
this.filterVariantImages(variant);
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,
},
});
};
}

filterVariantImages(varObj){
console.log(varObj)
if(varObj.featured_image && varObj.featured_image.alt){

document.querySelectorAll(‘[thumbnail-alt]’).forEach(img => img.style.display = ‘none’);
const currentImageAlt = varObj.featured_image.alt;
const thumbnailSelector= [thumbnail-alt= '${currentImageAlt}'];
document.querySelectorAll(thumbnailSelector).forEach((img) => {
img.style.display = ‘block’;
})
} else{
document.querySelectorAll(‘[thumbnail-alt]’).forEach(img => img.style.display = ‘block’);
}
}

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.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.quantityInput.min = min;

if (max) {
this.quantityInput.max = max;
} else {
this.quantityInput.removeAttribute(‘max’);
}
this.quantityInput.value = min;

publish(PUB_SUB_EVENTS.quantityUpdate, undefined);
}

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=${currentVariantId}&section_id=${this.dataset.section})
.then((response) => response.text())
.then((responseText) => {
const html = new DOMParser().parseFromString(responseText, ‘text/html’);
this.updateQuantityRules(this.dataset.section, html);
})
.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) {
const current = this.quantityForm.querySelector(selector);
const updated = quantityFormUpdated.querySelector(selector);
if (!current || !updated) continue;
if (selector === ‘.quantity__input’) {
const attributes = [‘data-cart-quantity’, ‘data-min’, ‘data-max’, ‘step’];
for (let attribute of attributes) {
const valueUpdated = updated.getAttribute(attribute);
if (valueUpdated !== null) {
current.setAttribute(attribute, valueUpdated);
} else {
current.removeAttribute(attribute);
}
}
} else {
current.innerHTML = updated.innerHTML;
}
}
}

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;
}
}
);
}

////////// Third step /////////////////

Open global.js and replace with this code

function getFocusableElements(container) {
return Array.from(
container.querySelectorAll(
“summary, a[href], button:enabled, [tabindex]:not([tabindex^=‘-’]), [draggable], area, input:not([type=hidden]):enabled, select:enabled, textarea:enabled, object, iframe”
)
);
}

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’));

if (summary.nextElementSibling.getAttribute(‘id’)) {
summary.setAttribute(‘aria-controls’, summary.nextElementSibling.id);
}

summary.addEventListener(‘click’, (event) => {
event.currentTarget.setAttribute(‘aria-expanded’, !event.currentTarget.closest(‘details’).hasAttribute(‘open’));
});

if (summary.closest(‘header-drawer, menu-drawer’)) return;
summary.parentElement.addEventListener(‘keyup’, onKeyUpEscape);
});

const trapFocusHandlers = {};

function trapFocus(container, elementToFocus = container) {
var elements = getFocusableElements(container);
var first = elements[0];
var last = elements[elements.length - 1];

removeTrapFocus();

trapFocusHandlers.focusin = (event) => {
if (event.target !== container && event.target !== last && event.target !== first) return;

document.addEventListener(‘keydown’, trapFocusHandlers.keydown);
};

trapFocusHandlers.focusout = function () {
document.removeEventListener(‘keydown’, trapFocusHandlers.keydown);
};

trapFocusHandlers.keydown = function (event) {
if (event.code.toUpperCase() !== ‘TAB’) return; // If not TAB key
// On the last focusable element and tab forward, focus the first element.
if (event.target === last && !event.shiftKey) {
event.preventDefault();
first.focus();
}

// On the first focusable element and tab backward, focus the last element.
if ((event.target === container || event.target === first) && event.shiftKey) {
event.preventDefault();
last.focus();
}
};

document.addEventListener(‘focusout’, trapFocusHandlers.focusout);
document.addEventListener(‘focusin’, trapFocusHandlers.focusin);

elementToFocus.focus();

if (
elementToFocus.tagName === ‘INPUT’ &&
[‘search’, ‘text’, ‘email’, ‘url’].includes(elementToFocus.type) &&
elementToFocus.value
) {
elementToFocus.setSelectionRange(0, elementToFocus.value.length);
}
}

// Here run the querySelector to figure out if the browser supports :focus-visible or not and run code based on it.
try {
document.querySelector(‘:focus-visible’);
} catch (e) {
focusVisiblePolyfill();
}

function focusVisiblePolyfill() {
const navKeys = [
‘ARROWUP’,
‘ARROWDOWN’,
‘ARROWLEFT’,
‘ARROWRIGHT’,
‘TAB’,
‘ENTER’,
‘SPACE’,
‘ESCAPE’,
‘HOME’,
‘END’,
‘PAGEUP’,
‘PAGEDOWN’,
];
let currentFocusedElement = null;
let mouseClick = null;

window.addEventListener(‘keydown’, (event) => {
if (navKeys.includes(event.code.toUpperCase())) {
mouseClick = false;
}
});

window.addEventListener(‘mousedown’, (event) => {
mouseClick = true;
});

window.addEventListener(
‘focus’,
() => {
if (currentFocusedElement) currentFocusedElement.classList.remove(‘focused’);

if (mouseClick) return;

currentFocusedElement = document.activeElement;
currentFocusedElement.classList.add(‘focused’);
},
true
);
}

function pauseAllMedia() {
document.querySelectorAll(‘.js-youtube’).forEach((video) => {
video.contentWindow.postMessage(‘{“event”:“command”,“func”:"’ + ‘pauseVideo’ + ‘“,“args”:”"}’, ‘‘);
});
document.querySelectorAll(’.js-vimeo’).forEach((video) => {
video.contentWindow.postMessage(‘{“method”:“pause”}’, '
’);
});
document.querySelectorAll(‘video’).forEach((video) => video.pause());
document.querySelectorAll(‘product-model’).forEach((model) => {
if (model.modelViewerUI) model.modelViewerUI.pause();
});
}

function removeTrapFocus(elementToFocus = null) {
document.removeEventListener(‘focusin’, trapFocusHandlers.focusin);
document.removeEventListener(‘focusout’, trapFocusHandlers.focusout);
document.removeEventListener(‘keydown’, trapFocusHandlers.keydown);

if (elementToFocus) elementToFocus.focus();
}

function onKeyUpEscape(event) {
if (event.code.toUpperCase() !== ‘ESCAPE’) return;

const openDetailsElement = event.target.closest(‘details[open]’);
if (!openDetailsElement) return;

const summaryElement = openDetailsElement.querySelector(‘summary’);
openDetailsElement.removeAttribute(‘open’);
summaryElement.setAttribute(‘aria-expanded’, false);
summaryElement.focus();
}

class QuantityInput extends HTMLElement {
constructor() {
super();
this.input = this.querySelector(‘input’);
this.changeEvent = new Event(‘change’, { bubbles: true });
this.input.addEventListener(‘change’, this.onInputChange.bind(this));
this.querySelectorAll(‘button’).forEach((button) =>
button.addEventListener(‘click’, this.onButtonClick.bind(this))
);
}

quantityUpdateUnsubscriber = undefined;

connectedCallback() {
this.validateQtyRules();
this.quantityUpdateUnsubscriber = subscribe(PUB_SUB_EVENTS.quantityUpdate, this.validateQtyRules.bind(this));
}

disconnectedCallback() {
if (this.quantityUpdateUnsubscriber) {
this.quantityUpdateUnsubscriber();
}
}

onInputChange(event) {
this.validateQtyRules();
}

onButtonClick(event) {
event.preventDefault();
const previousValue = this.input.value;

if (event.target.name === ‘plus’) {
if (parseInt(this.input.dataset.min) > parseInt(this.input.step) && this.input.value == 0) {
this.input.value = this.input.dataset.min;
} else {
this.input.stepUp();
}
} else {
this.input.stepDown();
}

if (previousValue !== this.input.value) this.input.dispatchEvent(this.changeEvent);

if (this.input.dataset.min === previousValue && event.target.name === ‘minus’) {
this.input.value = parseInt(this.input.min);
}
}

validateQtyRules() {
const value = parseInt(this.input.value);
if (this.input.min) {
const buttonMinus = this.querySelector(“.quantity__button[name=‘minus’]”);
buttonMinus.classList.toggle(‘disabled’, parseInt(value) <= parseInt(this.input.min));
}
if (this.input.max) {
const max = parseInt(this.input.max);
const buttonPlus = this.querySelector(“.quantity__button[name=‘plus’]”);
buttonPlus.classList.toggle(‘disabled’, value >= max);
}
}
}

customElements.define(‘quantity-input’, QuantityInput);

function debounce(fn, wait) {
let t;
return (…args) => {
clearTimeout(t);
t = setTimeout(() => fn.apply(this, args), wait);
};
}

function throttle(fn, delay) {
let lastCall = 0;
return function (…args) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return fn(…args);
};
}

function fetchConfig(type = ‘json’) {
return {
method: ‘POST’,
headers: { ‘Content-Type’: ‘application/json’, Accept: application/${type} },
};
}

/*

  • Shopify Common JS

*/
if (typeof window.Shopify == ‘undefined’) {
window.Shopify = {};
}

Shopify.bind = function (fn, scope) {
return function () {
return fn.apply(scope, arguments);
};
};

Shopify.setSelectorByValue = function (selector, value) {
for (var i = 0, count = selector.options.length; i < count; i++) {
var option = selector.options[i];
if (value == option.value || value == option.innerHTML) {
selector.selectedIndex = i;
return i;
}
}
};

Shopify.addListener = function (target, eventName, callback) {
target.addEventListener
? target.addEventListener(eventName, callback, false)
: target.attachEvent(‘on’ + eventName, callback);
};

Shopify.postLink = function (path, options) {
options = options || {};
var method = options[‘method’] || ‘post’;
var params = options[‘parameters’] || {};

var form = document.createElement(‘form’);
form.setAttribute(‘method’, method);
form.setAttribute(‘action’, path);

for (var key in params) {
var hiddenField = document.createElement(‘input’);
hiddenField.setAttribute(‘type’, ‘hidden’);
hiddenField.setAttribute(‘name’, key);
hiddenField.setAttribute(‘value’, params[key]);
form.appendChild(hiddenField);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
};

Shopify.CountryProvinceSelector = function (country_domid, province_domid, options) {
this.countryEl = document.getElementById(country_domid);
this.provinceEl = document.getElementById(province_domid);
this.provinceContainer = document.getElementById(options[‘hideElement’] || province_domid);

Shopify.addListener(this.countryEl, ‘change’, Shopify.bind(this.countryHandler, this));

this.initCountry();
this.initProvince();
};

Shopify.CountryProvinceSelector.prototype = {
initCountry: function () {
var value = this.countryEl.getAttribute(‘data-default’);
Shopify.setSelectorByValue(this.countryEl, value);
this.countryHandler();
},

initProvince: function () {
var value = this.provinceEl.getAttribute(‘data-default’);
if (value && this.provinceEl.options.length > 0) {
Shopify.setSelectorByValue(this.provinceEl, value);
}
},

countryHandler: function (e) {
var opt = this.countryEl.options[this.countryEl.selectedIndex];
var raw = opt.getAttribute(‘data-provinces’);
var provinces = JSON.parse(raw);

this.clearOptions(this.provinceEl);
if (provinces && provinces.length == 0) {
this.provinceContainer.style.display = ‘none’;
} else {
for (var i = 0; i < provinces.length; i++) {
var opt = document.createElement(‘option’);
opt.value = provinces[i][0];
opt.innerHTML = provinces[i][1];
this.provinceEl.appendChild(opt);
}

this.provinceContainer.style.display = ‘’;
}
},

clearOptions: function (selector) {
while (selector.firstChild) {
selector.removeChild(selector.firstChild);
}
},

setOptions: function (selector, values) {
for (var i = 0, count = values.length; i < values.length; i++) {
var opt = document.createElement(‘option’);
opt.value = values[i];
opt.innerHTML = values[i];
selector.appendChild(opt);
}
},
};

class MenuDrawer extends HTMLElement {
constructor() {
super();

this.mainDetailsToggle = this.querySelector(‘details’);

this.addEventListener(‘keyup’, this.onKeyUp.bind(this));
this.addEventListener(‘focusout’, this.onFocusOut.bind(this));
this.bindEvents();
}

bindEvents() {
this.querySelectorAll(‘summary’).forEach((summary) =>
summary.addEventListener(‘click’, this.onSummaryClick.bind(this))
);
this.querySelectorAll(
‘button:not(.localization-selector):not(.country-selector__close-button):not(.country-filter__reset-button)’
).forEach((button) => button.addEventListener(‘click’, this.onCloseButtonClick.bind(this)));
}

onKeyUp(event) {
if (event.code.toUpperCase() !== ‘ESCAPE’) return;

const openDetailsElement = event.target.closest(‘details[open]’);
if (!openDetailsElement) return;

openDetailsElement === this.mainDetailsToggle
? this.closeMenuDrawer(event, this.mainDetailsToggle.querySelector(‘summary’))
: this.closeSubmenu(openDetailsElement);
}

onSummaryClick(event) {
const summaryElement = event.currentTarget;
const detailsElement = summaryElement.parentNode;
const parentMenuElement = detailsElement.closest(‘.has-submenu’);
const isOpen = detailsElement.hasAttribute(‘open’);
const reducedMotion = window.matchMedia(‘(prefers-reduced-motion: reduce)’);

function addTrapFocus() {
trapFocus(summaryElement.nextElementSibling, detailsElement.querySelector(‘button’));
summaryElement.nextElementSibling.removeEventListener(‘transitionend’, addTrapFocus);
}

if (detailsElement === this.mainDetailsToggle) {
if (isOpen) event.preventDefault();
isOpen ? this.closeMenuDrawer(event, summaryElement) : this.openMenuDrawer(summaryElement);

if (window.matchMedia(‘(max-width: 990px)’)) {
document.documentElement.style.setProperty(‘–viewport-height’, ${window.innerHeight}px);
}
} else {
setTimeout(() => {
detailsElement.classList.add(‘menu-opening’);
summaryElement.setAttribute(‘aria-expanded’, true);
parentMenuElement && parentMenuElement.classList.add(‘submenu-open’);
!reducedMotion || reducedMotion.matches
? addTrapFocus()
: summaryElement.nextElementSibling.addEventListener(‘transitionend’, addTrapFocus);
}, 100);
}
}

openMenuDrawer(summaryElement) {
setTimeout(() => {
this.mainDetailsToggle.classList.add(‘menu-opening’);
});
summaryElement.setAttribute(‘aria-expanded’, true);
trapFocus(this.mainDetailsToggle, summaryElement);
document.body.classList.add(overflow-hidden-${this.dataset.breakpoint});
}

closeMenuDrawer(event, elementToFocus = false) {
if (event === undefined) return;

this.mainDetailsToggle.classList.remove(‘menu-opening’);
this.mainDetailsToggle.querySelectorAll(‘details’).forEach((details) => {
details.removeAttribute(‘open’);
details.classList.remove(‘menu-opening’);
});
this.mainDetailsToggle.querySelectorAll(‘.submenu-open’).forEach((submenu) => {
submenu.classList.remove(‘submenu-open’);
});
document.body.classList.remove(overflow-hidden-${this.dataset.breakpoint});
removeTrapFocus(elementToFocus);
this.closeAnimation(this.mainDetailsToggle);

if (event instanceof KeyboardEvent) elementToFocus?.setAttribute(‘aria-expanded’, false);
}

onFocusOut() {
setTimeout(() => {
if (this.mainDetailsToggle.hasAttribute(‘open’) && !this.mainDetailsToggle.contains(document.activeElement))
this.closeMenuDrawer();
});
}

onCloseButtonClick(event) {
const detailsElement = event.currentTarget.closest(‘details’);
this.closeSubmenu(detailsElement);
}

closeSubmenu(detailsElement) {
const parentMenuElement = detailsElement.closest(‘.submenu-open’);
parentMenuElement && parentMenuElement.classList.remove(‘submenu-open’);
detailsElement.classList.remove(‘menu-opening’);
detailsElement.querySelector(‘summary’).setAttribute(‘aria-expanded’, false);
removeTrapFocus(detailsElement.querySelector(‘summary’));
this.closeAnimation(detailsElement);
}

closeAnimation(detailsElement) {
let animationStart;

const handleAnimation = (time) => {
if (animationStart === undefined) {
animationStart = time;
}

const elapsedTime = time - animationStart;

if (elapsedTime < 400) {
window.requestAnimationFrame(handleAnimation);
} else {
detailsElement.removeAttribute(‘open’);
if (detailsElement.closest(‘details[open]’)) {
trapFocus(detailsElement.closest(‘details[open]’), detailsElement.querySelector(‘summary’));
}
}
};

window.requestAnimationFrame(handleAnimation);
}
}

customElements.define(‘menu-drawer’, MenuDrawer);

class HeaderDrawer extends MenuDrawer {
constructor() {
super();
}

openMenuDrawer(summaryElement) {
this.header = this.header || document.querySelector(‘.section-header’);
this.borderOffset =
this.borderOffset || this.closest(‘.header-wrapper’).classList.contains(‘header-wrapper–border-bottom’) ? 1 : 0;
document.documentElement.style.setProperty(
‘–header-bottom-position’,
${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px
);
this.header.classList.add(‘menu-open’);

setTimeout(() => {
this.mainDetailsToggle.classList.add(‘menu-opening’);
});

summaryElement.setAttribute(‘aria-expanded’, true);
window.addEventListener(‘resize’, this.onResize);
trapFocus(this.mainDetailsToggle, summaryElement);
document.body.classList.add(overflow-hidden-${this.dataset.breakpoint});
}

closeMenuDrawer(event, elementToFocus) {
if (!elementToFocus) return;
super.closeMenuDrawer(event, elementToFocus);
this.header.classList.remove(‘menu-open’);
window.removeEventListener(‘resize’, this.onResize);
}

onResize = () => {
this.header &&
document.documentElement.style.setProperty(
‘–header-bottom-position’,
${parseInt(this.header.getBoundingClientRect().bottom - this.borderOffset)}px
);
document.documentElement.style.setProperty(‘–viewport-height’, ${window.innerHeight}px);
};
}

customElements.define(‘header-drawer’, HeaderDrawer);

class ModalDialog extends HTMLElement {
constructor() {
super();
this.querySelector(‘[id^=“ModalClose-”]’).addEventListener(‘click’, this.hide.bind(this, false));
this.addEventListener(‘keyup’, (event) => {
if (event.code.toUpperCase() === ‘ESCAPE’) this.hide();
});
if (this.classList.contains(‘media-modal’)) {
this.addEventListener(‘pointerup’, (event) => {
if (event.pointerType === ‘mouse’ && !event.target.closest(‘deferred-media, product-model’)) this.hide();
});
} else {
this.addEventListener(‘click’, (event) => {
if (event.target === this) this.hide();
});
}
}

connectedCallback() {
if (this.moved) return;
this.moved = true;
document.body.appendChild(this);
}

show(opener) {
this.openedBy = opener;
const popup = this.querySelector(‘.template-popup’);
document.body.classList.add(‘overflow-hidden’);
this.setAttribute(‘open’, ‘’);
if (popup) popup.loadContent();
trapFocus(this, this.querySelector(‘[role=“dialog”]’));
window.pauseAllMedia();
}

hide() {
document.body.classList.remove(‘overflow-hidden’);
document.body.dispatchEvent(new CustomEvent(‘modalClosed’));
this.removeAttribute(‘open’);
removeTrapFocus(this.openedBy);
window.pauseAllMedia();
}
}
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();

const button = this.querySelector(‘button’);

if (!button) return;
button.addEventListener(‘click’, () => {
const modal = document.querySelector(this.getAttribute(‘data-modal’));
if (modal) modal.show(button);
});
}
}
customElements.define(‘modal-opener’, ModalOpener);

class DeferredMedia extends HTMLElement {
constructor() {
super();
const poster = this.querySelector(‘[id^=“Deferred-Poster-”]’);
if (!poster) return;
poster.addEventListener(‘click’, this.loadContent.bind(this));
}

loadContent(focus = true) {
window.pauseAllMedia();
if (!this.getAttribute(‘loaded’)) {
const content = document.createElement(‘div’);
content.appendChild(this.querySelector(‘template’).content.firstElementChild.cloneNode(true));

this.setAttribute(‘loaded’, true);
const deferredElement = this.appendChild(content.querySelector(‘video, model-viewer, iframe’));
if (focus) deferredElement.focus();
if (deferredElement.nodeName == ‘VIDEO’ && deferredElement.getAttribute(‘autoplay’)) {
// force autoplay for safari
deferredElement.play();
}
}
}
}

customElements.define(‘deferred-media’, DeferredMedia);

class SliderComponent extends HTMLElement {
constructor() {
super();
this.slider = this.querySelector(‘[id^=“Slider-”]’);
this.sliderItems = this.querySelectorAll(‘[id^=“Slide-”]’);
this.enableSliderLooping = false;
this.currentPageElement = this.querySelector(‘.slider-counter–current’);
this.pageTotalElement = this.querySelector(‘.slider-counter–total’);
this.prevButton = this.querySelector(‘button[name=“previous”]’);
this.nextButton = this.querySelector(‘button[name=“next”]’);

if (!this.slider || !this.nextButton) return;

this.initPages();
const resizeObserver = new ResizeObserver((entries) => this.initPages());
resizeObserver.observe(this.slider);

this.slider.addEventListener(‘scroll’, this.update.bind(this));
this.prevButton.addEventListener(‘click’, this.onButtonClick.bind(this));
this.nextButton.addEventListener(‘click’, this.onButtonClick.bind(this));
}

initPages() {
this.sliderItemsToShow = Array.from(this.sliderItems).filter((element) => element.clientWidth > 0);
if (this.sliderItemsToShow.length < 2) return;
this.sliderItemOffset = this.sliderItemsToShow[1].offsetLeft - this.sliderItemsToShow[0].offsetLeft;
this.slidesPerPage = Math.floor(
(this.slider.clientWidth - this.sliderItemsToShow[0].offsetLeft) / this.sliderItemOffset
);
this.totalPages = this.sliderItemsToShow.length - this.slidesPerPage + 1;
this.update();
}

resetPages() {
this.sliderItems = this.querySelectorAll(‘[id^=“Slide-”]’);
this.initPages();
}

update() {
// Temporarily prevents unneeded updates resulting from variant changes
// This should be refactored as part of https://github.com/Shopify/dawn/issues/2057
if (!this.slider || !this.nextButton) return;

const previousPage = this.currentPage;
this.currentPage = Math.round(this.slider.scrollLeft / this.sliderItemOffset) + 1;

if (this.currentPageElement && this.pageTotalElement) {
this.currentPageElement.textContent = this.currentPage;
this.pageTotalElement.textContent = this.totalPages;
}

if (this.currentPage != previousPage) {
this.dispatchEvent(
new CustomEvent(‘slideChanged’, {
detail: {
currentPage: this.currentPage,
currentElement: this.sliderItemsToShow[this.currentPage - 1],
},
})
);
}

if (this.enableSliderLooping) return;

if (this.isSlideVisible(this.sliderItemsToShow[0]) && this.slider.scrollLeft === 0) {
this.prevButton.setAttribute(‘disabled’, ‘disabled’);
} else {
this.prevButton.removeAttribute(‘disabled’);
}

if (this.isSlideVisible(this.sliderItemsToShow[this.sliderItemsToShow.length - 1])) {
this.nextButton.setAttribute(‘disabled’, ‘disabled’);
} else {
this.nextButton.removeAttribute(‘disabled’);
}
}

isSlideVisible(element, offset = 0) {
const lastVisibleSlide = this.slider.clientWidth + this.slider.scrollLeft - offset;
return element.offsetLeft + element.clientWidth <= lastVisibleSlide && element.offsetLeft >= this.slider.scrollLeft;
}

onButtonClick(event) {
event.preventDefault();
const step = event.currentTarget.dataset.step || 1;
this.slideScrollPosition =
event.currentTarget.name === ‘next’
? this.slider.scrollLeft + step * this.sliderItemOffset
: this.slider.scrollLeft - step * this.sliderItemOffset;
this.setSlidePosition(this.slideScrollPosition);
}

setSlidePosition(position) {
this.slider.scrollTo({
left: position,
});
}
}

customElements.define(‘slider-component’, SliderComponent);

class SlideshowComponent extends SliderComponent {
constructor() {
super();
this.sliderControlWrapper = this.querySelector(‘.slider-buttons’);
this.enableSliderLooping = true;

if (!this.sliderControlWrapper) return;

this.sliderFirstItemNode = this.slider.querySelector(‘.slideshow__slide’);
if (this.sliderItemsToShow.length > 0) this.currentPage = 1;

this.announcementBarSlider = this.querySelector(‘.announcement-bar-slider’);
// Value below should match --duration-announcement-bar CSS value
this.announcerBarAnimationDelay = this.announcementBarSlider ? 250 : 0;

this.sliderControlLinksArray = Array.from(this.sliderControlWrapper.querySelectorAll(‘.slider-counter__link’));
this.sliderControlLinksArray.forEach((link) => link.addEventListener(‘click’, this.linkToSlide.bind(this)));
this.slider.addEventListener(‘scroll’, this.setSlideVisibility.bind(this));
this.setSlideVisibility();

if (this.announcementBarSlider) {
this.announcementBarArrowButtonWasClicked = false;

this.reducedMotion = window.matchMedia(‘(prefers-reduced-motion: reduce)’);
this.reducedMotion.addEventListener(‘change’, () => {
if (this.slider.getAttribute(‘data-autoplay’) === ‘true’) this.setAutoPlay();
});

[this.prevButton, this.nextButton].forEach((button) => {
button.addEventListener(
‘click’,
() => {
this.announcementBarArrowButtonWasClicked = true;
},
{ once: true }
);
});
}

if (this.slider.getAttribute(‘data-autoplay’) === ‘true’) this.setAutoPlay();
}

setAutoPlay() {
this.autoplaySpeed = this.slider.dataset.speed * 1000;
this.addEventListener(‘mouseover’, this.focusInHandling.bind(this));
this.addEventListener(‘mouseleave’, this.focusOutHandling.bind(this));
this.addEventListener(‘focusin’, this.focusInHandling.bind(this));
this.addEventListener(‘focusout’, this.focusOutHandling.bind(this));

if (this.querySelector(‘.slideshow__autoplay’)) {
this.sliderAutoplayButton = this.querySelector(‘.slideshow__autoplay’);
this.sliderAutoplayButton.addEventListener(‘click’, this.autoPlayToggle.bind(this));
this.autoplayButtonIsSetToPlay = true;
this.play();
} else {
this.reducedMotion.matches || this.announcementBarArrowButtonWasClicked ? this.pause() : this.play();
}
}

onButtonClick(event) {
super.onButtonClick(event);
this.wasClicked = true;

const isFirstSlide = this.currentPage === 1;
const isLastSlide = this.currentPage === this.sliderItemsToShow.length;

if (!isFirstSlide && !isLastSlide) {
this.applyAnimationToAnnouncementBar(event.currentTarget.name);
return;
}

if (isFirstSlide && event.currentTarget.name === ‘previous’) {
this.slideScrollPosition =
this.slider.scrollLeft + this.sliderFirstItemNode.clientWidth * this.sliderItemsToShow.length;
} else if (isLastSlide && event.currentTarget.name === ‘next’) {
this.slideScrollPosition = 0;
}

this.setSlidePosition(this.slideScrollPosition);

this.applyAnimationToAnnouncementBar(event.currentTarget.name);
}

setSlidePosition(position) {
if (this.setPositionTimeout) clearTimeout(this.setPositionTimeout);
this.setPositionTimeout = setTimeout(() => {
this.slider.scrollTo({
left: position,
});
}, this.announcerBarAnimationDelay);
}

update() {
super.update();
this.sliderControlButtons = this.querySelectorAll(‘.slider-counter__link’);
this.prevButton.removeAttribute(‘disabled’);

if (!this.sliderControlButtons.length) return;

this.sliderControlButtons.forEach((link) => {
link.classList.remove(‘slider-counter__link–active’);
link.removeAttribute(‘aria-current’);
});
this.sliderControlButtons[this.currentPage - 1].classList.add(‘slider-counter__link–active’);
this.sliderControlButtons[this.currentPage - 1].setAttribute(‘aria-current’, true);
}

autoPlayToggle() {
this.togglePlayButtonState(this.autoplayButtonIsSetToPlay);
this.autoplayButtonIsSetToPlay ? this.pause() : this.play();
this.autoplayButtonIsSetToPlay = !this.autoplayButtonIsSetToPlay;
}

focusOutHandling(event) {
if (this.sliderAutoplayButton) {
const focusedOnAutoplayButton =
event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
if (!this.autoplayButtonIsSetToPlay || focusedOnAutoplayButton) return;
this.play();
} else if (!this.reducedMotion.matches && !this.announcementBarArrowButtonWasClicked) {
this.play();
}
}

focusInHandling(event) {
if (this.sliderAutoplayButton) {
const focusedOnAutoplayButton =
event.target === this.sliderAutoplayButton || this.sliderAutoplayButton.contains(event.target);
if (focusedOnAutoplayButton && this.autoplayButtonIsSetToPlay) {
this.play();
} else if (this.autoplayButtonIsSetToPlay) {
this.pause();
}
} else if (this.announcementBarSlider.contains(event.target)) {
this.pause();
}
}

play() {
this.slider.setAttribute(‘aria-live’, ‘off’);
clearInterval(this.autoplay);
this.autoplay = setInterval(this.autoRotateSlides.bind(this), this.autoplaySpeed);
}

pause() {
this.slider.setAttribute(‘aria-live’, ‘polite’);
clearInterval(this.autoplay);
}

togglePlayButtonState(pauseAutoplay) {
if (pauseAutoplay) {
this.sliderAutoplayButton.classList.add(‘slideshow__autoplay–paused’);
this.sliderAutoplayButton.setAttribute(‘aria-label’, window.accessibilityStrings.playSlideshow);
} else {
this.sliderAutoplayButton.classList.remove(‘slideshow__autoplay–paused’);
this.sliderAutoplayButton.setAttribute(‘aria-label’, window.accessibilityStrings.pauseSlideshow);
}
}

autoRotateSlides() {
const slideScrollPosition =
this.currentPage === this.sliderItems.length ? 0 : this.slider.scrollLeft + this.sliderItemOffset;

this.setSlidePosition(slideScrollPosition);
this.applyAnimationToAnnouncementBar();
}

setSlideVisibility(event) {
this.sliderItemsToShow.forEach((item, index) => {
const linkElements = item.querySelectorAll(‘a’);
if (index === this.currentPage - 1) {
if (linkElements.length)
linkElements.forEach((button) => {
button.removeAttribute(‘tabindex’);
});
item.setAttribute(‘aria-hidden’, ‘false’);
item.removeAttribute(‘tabindex’);
} else {
if (linkElements.length)
linkElements.forEach((button) => {
button.setAttribute(‘tabindex’, ‘-1’);
});
item.setAttribute(‘aria-hidden’, ‘true’);
item.setAttribute(‘tabindex’, ‘-1’);
}
});
this.wasClicked = false;
}

applyAnimationToAnnouncementBar(button = ‘next’) {
if (!this.announcementBarSlider) return;

const itemsCount = this.sliderItems.length;
const increment = button === ‘next’ ? 1 : -1;

const currentIndex = this.currentPage - 1;
let nextIndex = (currentIndex + increment) % itemsCount;
nextIndex = nextIndex === -1 ? itemsCount - 1 : nextIndex;

const nextSlide = this.sliderItems[nextIndex];
const currentSlide = this.sliderItems[currentIndex];

const animationClassIn = ‘announcement-bar-slider–fade-in’;
const animationClassOut = ‘announcement-bar-slider–fade-out’;

const isFirstSlide = currentIndex === 0;
const isLastSlide = currentIndex === itemsCount - 1;

const shouldMoveNext = (button === ‘next’ && !isLastSlide) || (button === ‘previous’ && isFirstSlide);
const direction = shouldMoveNext ? ‘next’ : ‘previous’;

currentSlide.classList.add(${animationClassOut}-${direction});
nextSlide.classList.add(${animationClassIn}-${direction});

setTimeout(() => {
currentSlide.classList.remove(${animationClassOut}-${direction});
nextSlide.classList.remove(${animationClassIn}-${direction});
}, this.announcerBarAnimationDelay * 2);
}

linkToSlide(event) {
event.preventDefault();
const slideScrollPosition =
this.slider.scrollLeft +
this.sliderFirstItemNode.clientWidth *
(this.sliderControlLinksArray.indexOf(event.currentTarget) + 1 - this.currentPage);
this.slider.scrollTo({
left: slideScrollPosition,
});
}
}

customElements.define(‘slideshow-component’, SlideshowComponent);

class VariantSelects extends HTMLElement {
constructor() {
super();
}

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,
},
});
});
document.addEventListener(‘DOMContentLoaded’, (event) => {
const target = this.getInputForEventTarget(event.target);
// this.updateSelectionMetadata(event);

publish(PUB_SUB_EVENTS.onVariantLoadChange, {
data: {
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 = target
.closest(‘.product-form__input’)
.querySelector(‘[data-selected-value] > .swatch’);
if (!selectedDropdownSwatchValue) return;
if (swatchValue) {
selectedDropdownSwatchValue.style.setProperty(‘–swatch–background’, swatchValue);
selectedDropdownSwatchValue.classList.remove(‘swatch–unavailable’);
} else {
selectedDropdownSwatchValue.style.setProperty(‘–swatch–background’, ‘unset’);
selectedDropdownSwatchValue.classList.add(‘swatch–unavailable’);
}

selectedDropdownSwatchValue.style.setProperty(
‘–swatch-focal-point’,
target.selectedOptions[0].dataset.optionSwatchFocalPoint || ‘unset’
);
} else if (tagName === ‘INPUT’ && target.type === ‘radio’) {
const selectedSwatchValue = target.closest(.product-form__input).querySelector(‘[data-selected-value]’);
if (selectedSwatchValue) selectedSwatchValue.innerHTML = value;
}
}

getInputForEventTarget(target) {
return target.tagName === ‘SELECT’ ? target.selectedOptions[0] : target;
}

get selectedOptionValues() {
return Array.from(this.querySelectorAll(‘select option[selected], fieldset input:checked’)).map(
({ dataset }) => dataset.optionValueId
);
}
}

customElements.define(‘variant-selects’, VariantSelects);

class ProductRecommendations extends HTMLElement {
observer = undefined;

constructor() {
super();
}

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);
}

loadRecommendations(productId) {
fetch(${this.dataset.url}&product_id=${productId}&section_id=${this.dataset.sectionId})
.then((response) => response.text())
.then((text) => {
const html = document.createElement(‘div’);
html.innerHTML = text;
const recommendations = html.querySelector(‘product-recommendations’);

if (recommendations?.innerHTML.trim().length) {
this.innerHTML = recommendations.innerHTML;
}

if (!this.querySelector(‘slideshow-component’) && this.classList.contains(‘complementary-products’)) {
this.remove();
}

if (html.querySelector(‘.grid__item’)) {
this.classList.add(‘product-recommendations–loaded’);
}
})
.catch((e) => {
console.error(e);
});
}
}

customElements.define(‘product-recommendations’, ProductRecommendations);

class AccountIcon extends HTMLElement {
constructor() {
super();

this.icon = this.querySelector(‘.icon’);
}

connectedCallback() {
document.addEventListener(‘storefront:signincompleted’, this.handleStorefrontSignInCompleted.bind(this));
}

handleStorefrontSignInCompleted(event) {
if (event?.detail?.avatar) {
this.icon?.replaceWith(event.detail.avatar.cloneNode());
}
}
}

customElements.define(‘account-icon’, AccountIcon);

class BulkAdd extends HTMLElement {
constructor() {
super();
this.queue = ;
this.requestStarted = false;
this.ids = ;
}

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);
}

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);
}

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;

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);
}
}

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;
}
}

if (!customElements.get(‘bulk-add’)) {
customElements.define(‘bulk-add’, BulkAdd);
}

//////// Fourth Step ////////

Open product-variant-options.liquid and replace this code. This is for the images inside color variants

{% comment %}
Renders product variant options

Accepts:

  • product: {Object} product object.
  • option: {Object} current product_option object.
  • block: {Object} block object.
  • picker_type: {String} type of picker to dispay

Usage:
{% render ‘product-variant-options’,
product: product,
option: option,
block: block
picker_type: picker_type
%}
{% endcomment %}
{%- liquid
assign product_form_id = ‘product-form-’ | append: section.id
-%}

{%- for value in option.values -%}
{%- liquid
assign swatch_focal_point = null
if value.swatch.image
assign image_url = value.swatch.image | image_url: width: 50
assign swatch_value = ‘url(’ | append: image_url | append: ‘)’
assign swatch_focal_point = value.swatch.image.presentation.focal_point
elsif value.swatch.color
assign swatch_value = ‘rgb(’ | append: value.swatch.color.rgb | append: ‘)’
else
assign swatch_value = null
endif

assign option_disabled = true
if value.available
assign option_disabled = false
endif
-%}

{%- capture input_id -%}
{{ section.id }}-{{ option.position }}-{{ forloop.index0 -}}
{%- endcapture -%}

{%- capture input_name -%}
{{ option.name }}-{{ option.position }}
{%- endcapture -%}

{%- capture input_dataset -%}
data-product-url=“{{ value.product_url }}”
data-option-value-id=“{{ value.id }}”
{%- endcapture -%}

{%- capture label_unavailable -%}

{{- ‘products.product.variant_sold_out_or_unavailable’ | t -}}

{%- endcapture -%}

{%- if picker_type == ‘swatch’ -%}
{%- capture help_text -%}
{{ value | escape }}
{{ label_unavailable }}
{%- endcapture -%}
{%
render ‘swatch-input’,
id: input_id,
name: input_name,
value: value | escape,
swatch: value.swatch,
product_form_id: product_form_id,
checked: value.selected,
visually_disabled: option_disabled,
shape: block.settings.swatch_shape,
help_text: help_text,
additional_props: input_dataset
%}
{%- elsif picker_type == ‘button’ -%}
<input
type=“radio”
id=“{{ input_id }}”
name=“{{ input_name }}”
value=“{{ value | escape }}”
form=“{{ product_form_id }}”
{% if value.selected %}
checked
{% endif %}
{% if option_disabled %}
class=“disabled”
{% endif %}
{{ input_dataset }}

{% if product.variants[forloop.index0].featured_image != blank and option.name == ‘Color’ %}

{{ label_unavailable }}

{% else %}

{{ value -}}
{{ label_unavailable }}

{% endif %}
{%- elsif picker_type == ‘dropdown’ or picker_type == ‘swatch_dropdown’ -%}

{% if option_disabled -%} {{- 'products.product.value_unavailable' | t: option_value: value -}} {%- else -%} {{- value -}} {%- endif %} {%- endif -%} {%- endfor -%}

we appreciate the work you are doing. Here are some corrections to be made for flawless interactions

  1. colour name/label missing on the variant

  2. refresh product image variants in the open lightbox under image zoom

  3. refresh product image inside colour variant

  4. can we have all images layout, this code is only for thumbnails which is working fine as expected!

Screenshot 2024-07-09 at 10-45-12 KPS Clothing ¡ Customize Trade ¡ Shopify.png

@kpsclothing hi, sorry but i am actually a bit confused. May be I am not completely getting your requirements right.

Honestly man I spent atleast around 2 hours in building the thumbnail feature you asked for where you just wanted to show selected color variant images but you kind of seem to change that.

Can we make this easier for ourselves may be mail me or what’s app me so we can get on a meet to get the requirements straight.

This is my WhatsApp no: 8274902468

Thanks and regards

Hi Shadab, Thanks. I previously used the codes you have provided earlier and it did not worked. However on going through the process I found the other codes you have provided for thumbnail option. I replaced the old codes and now it worked perfectly fone. I am thanksful to you for your help and will get in touch in future if I need any other solutions. Again, thanks a lot :slightly_smiling_face:

PS. And the codes still work even if I use ‘Black’ in variant name & ‘Black vegan leather wallet men’ in the alt txt

@IKONSWEDEN yeah had to to a lot lookups since the code base is pretty different from earlier versions of dawn.

But yeah all is well that ends well, ri8?

Glad it worked out for you.

Let me know if i could be of any help later not just in Shopify but any web dev project. You have my email.

Most importantly thanks you for that tip means lot. Your thankful messages are worth the effort.

By the way love your store and the photography of your products. Good luck on your store and your business.

Thanks & Regards

Hello,

Your code works just perfect, but I would like to make a small adjustment. Currently, the thumbnails display only images from the selected variant. I would like the thumbnails to first show the images from the selected variant and then display the images from the other variants afterwards.

Could you please adjust the code so that the thumbnails follow this order?

Thank you for your assistance!

Best regards,

Patryk

Tried it out. Kind of works but not as perfectly as you want it to be.

Check out my development store

https://shadabs-online-store.myshopify.com/products/adidas-kids-stan-smith?variant=42930094309516

Password: reafur

Check out adidas kids stan Smith product from the catalog.

Will try it again to make it perfect as you are looking for.

I had something like this before, but after I chose a different color, I only see the first variant image in the first position. Thank you for your help.

Yes as I told you it works but not complete on your requirements.

I am trying to come up with a solution.

Here is the solution which I tried to solve in my shopify store:

Here is for trade theme:

Hi, Thanks I had a similar issue. I followed all your instructions and copied the code into the file. The swatch images stopped showing up in the gallery but I am attempting to have the selected variant color image to show when selected but they are not seem to be just static in swatches.

We just launched an app which is helpful to achieve it.

Feel free to try it Productify Groups

Regards
Guleria