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.quantityInput = this.querySelector('.quantity__input');
}
connectedCallback() {
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));
}
}
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 } }) {
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(() => {
// Zorg dat focus wordt gezet op de laatst aangeklikte optie
this.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 = [];
if (!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);
console.log("Nieuwe variant:", variant); // Debug
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'));
updateSourceFromDestination('variant-box-count');
// --- Custom update: SKU en inhoud per omdoos ---
const skuElement = this.querySelector("#variant-sku");
const boxCountElement = this.querySelector("#variant-box-count");
if (skuElement && boxCountElement && variant) {
skuElement.textContent = "SKU: " + (variant.sku || "N/A");
// Gebruik hier de 'wauw' namespace zoals in het oude thema
let boxCount = variant.metafields?.wauw?.box_amount || "N/A";
boxCountElement.textContent = "Inhoud per omdoos: " + boxCount;
}
// --- Einde custom update ---
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;
for (let i = mediaGalleryDestinationItems.length - 1; i >= 0; i--) {
if (!sourceSet.has(mediaGalleryDestinationItems[i].dataset.mediaId)) {
mediaGallerySource.prepend(mediaGalleryDestinationItems[i]);
shouldRefresh = true;
}
}
for (let i = 0; i < mediaGallerySourceItems.length; i++) {
if (!destinationSet.has(mediaGallerySourceItems[i].dataset.mediaId)) {
mediaGallerySourceItems[i].remove();
shouldRefresh = true;
}
}
if (shouldRefresh) [mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData();
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})`)
);
[mediaGallerySourceItems, sourceSet, sourceMap] = refreshSourceData();
}
});
}
this.querySelector('media-gallery')?.setActiveMedia?.(
`${this.dataset.section}-${variantFeaturedMediaId}`,
true
);
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}§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);
})
.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;
}
}
);
}
{formbuilder:MTEwNTIx}