/* * Custom code goes here. * A template should always ship with an empty custom.js */ // Adjust number of visible variants based on product card width function adjustVariantsDisplay() { $('.combinations-grid.flat-grid').each(function() { var $grid = $(this); var $wrapper = $grid.closest('.product-variants-wrap'); var availableWidth = $wrapper.width(); // Size of each item (image + gap) var itemSize = 52; // image width var gap = parseFloat($grid.css('gap')) || 3; var itemWithGap = itemSize + gap; var $items = $grid.find('.flat-grid-item'); var $badge = $grid.find('.flat-grid-overflow-badge'); var totalItems = $items.length; // If only 1-3 items, show all without badge if (totalItems <= 3) { $items.show(); $badge.hide(); return; } // First calculate without badge to see how many items can fit var maxItemsNoBadge = Math.floor(availableWidth / itemWithGap); // If all items fit, show all without badge if (maxItemsNoBadge >= totalItems) { $items.show(); $badge.hide(); return; } // Calculate with badge space reserved var badgeSize = 52 + gap; var maxItemsWithBadge = Math.floor((availableWidth - badgeSize) / itemWithGap); maxItemsWithBadge = Math.max(2, maxItemsWithBadge); // Minimum 2 items visible var hiddenCount = totalItems - maxItemsWithBadge; // If only 1 item would be hidden, show it instead of badge (if it fits) var showBadge = hiddenCount >= 2; var itemsToShow = showBadge ? maxItemsWithBadge : Math.min(totalItems, maxItemsNoBadge); // Show/hide items based on available space $items.each(function(index) { if (index < itemsToShow) { $(this).show(); } else { $(this).hide(); } }); // Update badge text and visibility if (showBadge) { $badge.text('+' + hiddenCount).show(); } else { $badge.hide(); } }); } // Re-adjust on window resize $(window).on('resize', function() { clearTimeout(window.resizeTimer); window.resizeTimer = setTimeout(adjustVariantsDisplay, 100); }); // Re-adjust when changing grid layout (4/3/2 columns or list) $(document).on('click', '.change-type span', function() { // Wait a bit for the layout to change, then adjust variants // console.log('[+N Badge] Clicked!'); setTimeout(adjustVariantsDisplay, 500); }); // Re-adjust after an AJAX product list update (pagination, filters, sorting). // PrestaShop replaces the product list DOM on 'updateProductList', so the new // cards must have their visible variants / +N badge recalculated. if (typeof prestashop !== 'undefined') { prestashop.on('updateProductList', function() { // Let the new product list DOM settle, then recalculate setTimeout(adjustVariantsDisplay, 100); }); } // ============================================================ // LOADING - indicateur visuel pendant le chargement AJAX du catalogue // (changement de page, filtres, tri) // ============================================================ (function() { if (typeof prestashop === 'undefined') return; // Injecte le style de l'overlay une seule fois if (!document.getElementById('catalog-loading-styles')) { $('head').append( '' ); } function showCatalogLoading() { var $container = $('#js-product-list'); if (!$container.length || $container.children('.catalog-loading-overlay').length) return; $container.append('
'); // Sécurité : retire l'overlay au bout de 10s si la requête ne revient pas clearTimeout(window.catalogLoadingTimer); window.catalogLoadingTimer = setTimeout(hideCatalogLoading, 10000); } function hideCatalogLoading() { clearTimeout(window.catalogLoadingTimer); $('.catalog-loading-overlay').remove(); } // updateFacets : émis dès le clic sur une page / un filtre / un tri prestashop.on('updateFacets', showCatalogLoading); // updateProductList : émis quand la nouvelle liste produits est reçue prestashop.on('updateProductList', hideCatalogLoading); // Sécurité : retire l'overlay si la requête AJAX échoue prestashop.on('handleError', hideCatalogLoading); })(); $(document).ready(function () { novBtCart(); novBtBuyNow(); adjustVariantsDisplay(); }); function novBtCart() { $(document).on('click', '.nov-bt-cart', function (event) { event.preventDefault(); event.stopPropagation(); var $element = $(this); var $form = $element.closest("form"); if (!$form.length) return; $element.find(".loading").show(); $element.addClass("active"); var query = $form.serialize() + "&add=1&action=update"; var actionURL = $form.attr("action"); $.ajax({ type: "POST", headers: {"cache-control": "no-cache"}, url: actionURL, async: true, cache: false, data: query, dataType: "json", success: function (result) { if (result.success) { prestashop.emit('updateCart', { reason: { idProduct: result.id_product, idProductAttribute: result.id_product_attribute, idCustomization: result.id_customization, linkAction: 'add-to-cart', cart: result.cart, }, resp: result, }); } }, complete: function() { $element.find(".loading").hide(); $element.removeClass("active"); } }); }); } function novBtBuyNow() { $('.nov-bt-buynow').each(function () { if (!$(this).hasClass("nov-enable")) { $(this).addClass('nov-enable'); $(this).click(function (event) { event.preventDefault(); event.stopPropagation(); $(this).find(".loading").show(); var orderlink = $(this).attr('data-orderlink'); var object_button_container = $(this).parents(".product-miniature"); var $element = $(this); var $form = $element.closest("form"); var query = $form.serialize() + "&add=1&action=update"; var actionURL = $form.attr("action"); $.ajax({ type: "POST", headers: {"cache-control": "no-cache"}, url: actionURL, async: !0, cache: !1, data: query, dataType: "json", success: function (result) { if (result.success) { window.location.href = orderlink; } } }) }) } }) } function showModalPopupCart(modal) { if ($("#blockcart-modal").length) { $("#blockcart-modal").remove(); } $("body").append(modal); $("#blockcart-modal").modal("show"); } function ajaxLoadVariant($self, formData) { $.ajax({ url: baseDir + 'modules/novelementor/ajax_variants.php', data: formData, async: !0, cache: !1, dataType: "json", headers: {"cache-control": "no-cache"}, success: function (res) { if (res.success) { var groups = res.groups; var product = res.product; $self.parents('.product-miniature').find('.product-title a').attr('href', product.url); $self.parents('.product-miniature').find('.thumbnail-container a').attr('href', product.url); $self.parents('.product-miniature').find('.product-price-and-shipping span.price').html(product.price); if (product.has_discount) { $self.parents('.product-miniature').find('.product-price-and-shipping span.regular-price').html(product.regular_price); if (product.discount_type == 'percentage') { $self.parents('.product-miniature').find('.product-price-and-shipping span.discount-percentage').html(product.discount_percentage); } else { $self.parents('.product-miniature').find('.product-price-and-shipping span.discount-percentage').html(product.discount_amount_to_display); } } $self.parents('.product-miniature').find('.image-cover').attr('src', product.images[0].large.url); $self.parents('.product-miniature').find('.image-cover').attr('data-full-size-image-url', product.images[0].large.url); // Reset original-src for hover behavior $self.parents('.product-miniature').find('.image-cover').data('original-src', product.images[0].large.url); if (product.images[1] != undefined) { $self.parents('.product-miniature').find('.image-secondary').attr('src', product.images[1].large.url); $self.parents('.product-miniature').find('.image-secondary').attr('data-full-size-image-url', product.images[1].large.url); } else { $self.parents('.product-miniature').find('.image-secondary').attr('src', product.images[0].large.url); $self.parents('.product-miniature').find('.image-secondary').attr('data-full-size-image-url', product.images[0].large.url); } if (product.available_for_order == '0') { $self.parents('.product-miniature').find('.nov-bt-cart').attr('disabled', 'disabled'); $self.parents('.product-miniature').find('.nov-bt-buynow').attr('disabled', 'disabled'); } else { $self.parents('.product-miniature').find('.nov-bt-cart').removeAttr('disabled'); $self.parents('.product-miniature').find('.nov-bt-buynow').removeAttr('disabled'); } if (product.availability == 'unavailable' || product.quantity <= 0) { $self.parents('.product-miniature').find('.nov-bt-cart').attr('disabled', 'disabled'); $self.parents('.product-miniature').find('.nov-bt-buynow').attr('disabled', 'disabled'); } // Update button data attributes with the variant's id_product_attribute and image if (product.images && product.images[0] && product.images[0].bySize && product.images[0].bySize.cart_default) { $self.parents('form').find('#button-add-to-cart').attr('data-product-attribute', product.id_product_attribute); $self.parents('form').find('#button-add-to-cart').attr('data-product-attribute-image', product.images[0].bySize.cart_default.url); } if (Object.keys(groups).length > 0) { var product_variants = ''; product_variants += '
'; $.each(groups, function (id_attribute_group, group) { // console.log(group); if (Object.keys(group.attributes).length > 0) { product_variants += '
'; if (res.showLabel == 'true') { product_variants += '' + group.name + ' : '; } if (group.group_type == 'select') { product_variants += ''; } else if (group.group_type == 'color') { product_variants += ''; } else if (group.group_type == 'radio') { product_variants += ''; } product_variants += '
'; } }) product_variants += '
'; $self.parents('form').find('.product-variants-wrap').html(product_variants); } } } }); } $(document).on('change', '.variants-product .product-variants-item > select', function (e) { e.preventDefault(); e.stopPropagation(); var $self = $(this); var $elm = $(e.currentTarget); var data = $(this).parents('form').serializeArray(); ajaxLoadVariant($self, data); return false; }); $(document).on('click', '.variants-product .product-variants-item ul li', function (e) { e.preventDefault(); var $self = $(this); // Manually check the radio inside this li (needed because radio is display:none) var $radio = $self.find('input[type="radio"]'); if ($radio.length) { $radio.prop('checked', true); } var $elm = $(e.currentTarget); var data = $(this).parents('form').serializeArray(); ajaxLoadVariant($self, data); return false; }); // Handle variant image hover - change main product image on mouseover $(document).on('mouseenter', '.variant-image-item', function () { var $img = $(this).find('.variant-thumb'); if ($img.length) { var largeSrc = $img.data('large-src'); if (largeSrc) { var $miniature = $(this).closest('.product-miniature'); var $mainImage = $miniature.find('.image-cover'); if (!$mainImage.data('original-src')) { $mainImage.data('original-src', $mainImage.attr('src')); } $mainImage.attr('src', largeSrc); } } }); // Restore original image on mouseleave $(document).on('mouseleave', '.variant-image-item', function () { var $miniature = $(this).closest('.product-miniature'); var $mainImage = $miniature.find('.image-cover'); var originalSrc = $mainImage.data('original-src'); if (originalSrc) { $mainImage.attr('src', originalSrc); } }); // Store combinations data for click handling (populated by server-side hook) if (!window.productCombinations) window.productCombinations = {}; // Le theme cache certains sélecteurs d'attribut (couleur via flat-grid, etc). // Du coup le form `#add-to-cart-or-refresh` ne contient pas les `group[X]` // correspondants. Quand PS reçoit l'AJAX de variant change, ces groupes lui // manquent → il fallback sur la combo par défaut (qui n'est pas la nôtre). // Fix : on injecte des `` // pour TOUS les groupes de la combo active qui n'ont pas déjà un input // dans le form. À refaire après chaque updatedProduct (PS remplace le form). function ensureFormHasAllVariantGroups() { try { var $wrapper = $('.productPageVariantsWrapper').first(); if (!$wrapper.length) return; var attrId = $('input[name="id_product_attribute"]').val(); var productId = $wrapper.data('id-product'); if (!attrId || !productId) return; var key = 'product_' + productId; var combo = window.productCombinations && window.productCombinations[key] && window.productCombinations[key][attrId]; if (!combo || !combo.attributes) return; var attrToGroup = (window.productAttributeToGroup && window.productAttributeToGroup[key]) || {}; var $form = $('form#add-to-cart-or-refresh').first(); if (!$form.length) $form = $('form[action*="cart"]').first(); if (!$form.length) return; // Retire d'abord les anciens hidden qu'on a injectés (sinon stale après variant change) $form.find('input[data-injected-attr="1"]').remove(); combo.attributes.forEach(function(attributeId) { var groupId = attrToGroup[attributeId]; if (!groupId) return; var name = 'group[' + groupId + ']'; // Si un input/select avec ce name existe déjà (visible ou non), on n'injecte pas if ($form.find('[name="' + name + '"]').length > 0) return; $form.append( '' ); }); } catch (e) { /* defensive */ } } $(document).ready(ensureFormHasAllVariantGroups); if (typeof prestashop !== 'undefined') { prestashop.on('updatedProduct', function() { // PS reconstruit le form au refresh → on ré-injecte setTimeout(ensureFormHasAllVariantGroups, 50); }); } // Après changement de variante (correction, couleur), on rétablit le // comportement par défaut : tous les items Sirv de la couleur attendue // sont activés, et tous les autres désactivés. Pareil que ce que ferait // Sirv au chargement initial d'une fiche produit pour cette variante. // On vérifie 3 fois (200ms, 600ms, 1200ms) pour absorber les refresh // Sirv tardifs qui pourraient écraser notre correction. function checkAndFixSirvColor() { try { if (typeof Sirv === 'undefined' || !Sirv.viewer) return; var inst = Sirv.viewer.getInstance('#sirv-gallery'); if (!inst) return; // Couleur attendue (variante active) var $wrapper = $('.productPageVariantsWrapper').first(); if (!$wrapper.length) return; var attrId = $('input[name="id_product_attribute"]').val(); var productId = $wrapper.data('id-product'); if (!attrId || !productId) return; var variant = window.productCombinations && window.productCombinations['product_' + productId] && window.productCombinations['product_' + productId][attrId]; if (!variant || !variant.reference) return; var expectedColor = variant.reference.replace(/(-\d+)+(?=(_\d+)?$)/, '').toLowerCase(); if (!expectedColor) return; // Couleur actuellement affichée (slide visible) var $shownItem = $('.SirvContainer .smv-slide.smv-shown [data-id]').first(); var visibleSrc = $shownItem.attr('data-src') || $('.SirvContainer .smv-slide.smv-shown img').first().attr('src') || ''; var mVisible = visibleSrc.match(/\/products\/([^\/]+)\//i); var currentColor = mVisible ? mVisible[1].toLowerCase() : null; // Déjà bonne couleur → on ne touche à rien if (currentColor && currentColor === expectedColor) return; // Collecte TOUS les items de la couleur attendue (et les autres pour les désactiver) var matchingIds = []; var matchingFirstIndex = -1; var otherIds = []; $('.SirvContainer .Sirv [data-id]').each(function(i) { var src = $(this).attr('data-src') || ''; var mm = src.match(/\/products\/([^\/]+)\//i); var id = $(this).attr('data-id'); if (mm && mm[1].toLowerCase() === expectedColor) { matchingIds.push(id); if (matchingFirstIndex < 0) matchingFirstIndex = i; } else if (id) { otherIds.push(id); } }); if (matchingIds.length === 0) return; // pas d'items pour cette couleur // Active tous les items de la bonne couleur, désactive les autres, // puis jump sur le premier item de la couleur (comportement init). otherIds.forEach(function(id) { try { inst.disableItem(id); } catch (e) {} }); matchingIds.forEach(function(id) { try { inst.enableItem(id); } catch (e) {} }); if (matchingFirstIndex >= 0) { try { inst.jump(matchingFirstIndex); } catch (e) {} } } catch (e) { /* Sirv API not ready / changed */ } } if (typeof prestashop !== 'undefined') { prestashop.on('updatedProduct', function() { // Re-vérifie plusieurs fois pour absorber les refresh Sirv tardifs setTimeout(checkAndFixSirvColor, 200); setTimeout(checkAndFixSirvColor, 600); setTimeout(checkAndFixSirvColor, 1200); }); } // Handle hover on flat grid item - change image, stock and price $(document).on('mouseenter', '.flat-grid-item', function() { var $item = $(this); var productId, $stockStatus, $mainImage, $priceEl; var $miniature = $item.closest('.product-miniature'); var $productPage = $item.closest('.productPageVariantsWrapper'); if ($miniature.length) { productId = $miniature.data('id-product'); $stockStatus = $miniature.find('.variant-stock-status'); $mainImage = $miniature.find('.thumbnail-container .image-cover'); $priceEl = $miniature.find('.price'); } else if ($productPage.length) { productId = $productPage.data('id-product'); $mainImage = $('img.smv-cursor-zoom-in').first(); $priceEl = $('span[itemprop="price"]').first(); } else { return; } var productAttr = $item.data('product-attribute'); var combinationsKey = 'product_' + productId; if (!window.productCombinations || !window.productCombinations[combinationsKey]) return; var variant = window.productCombinations[combinationsKey][productAttr]; if (!variant) return; // Change main image if ($mainImage.length) { if (!$mainImage.data('original-src')) $mainImage.data('original-src', $mainImage.attr('src')); var $sirvComponent = $mainImage.closest('.smv-zoom-view'); var originalDataSrc = $sirvComponent.length ? $sirvComponent.attr('data-src') : $mainImage.attr('src'); var newSrc = null; // If original is Sirv (product page), construct Sirv URL using variant reference (SKU) if (originalDataSrc && originalDataSrc.includes('sirv.com') && variant.reference) { // Extract query string from original // e.g., https://opaldemetz.sirv.com/products/MUD51BK67/MUD51BK67_01.webp?... var queryMatch = originalDataSrc.match(/(\?.*)$/); var queryString = queryMatch ? queryMatch[1] : ''; // Get the Sirv base domain var sirvBaseDomain = originalDataSrc.match(/^(https:\/\/[^\/]+\/products)/); if (sirvBaseDomain) { // Strippe les suffixes correction/taille (-NNN) pour ne garder // que la référence couleur (cas « Mise à la vue » : variante // NATD51-1010 → dossier Sirv NATD51). var colorRef = variant.reference.replace(/(-\d+)+(?=(_\d+)?$)/, ''); if (colorRef) { newSrc = sirvBaseDomain[1] + '/' + colorRef + '/' + colorRef + '_01.webp' + queryString; } } } else if ($miniature.length && variant.image_large) { // Catalogue: use variant image_large from variant data newSrc = variant.image_large; } if (newSrc) { $mainImage.attr('src', newSrc); // Also update Sirv data-src for SmartView component if ($sirvComponent.length) { if (!$sirvComponent.data('original-data-src')) { $sirvComponent.data('original-data-src', $sirvComponent.attr('data-src')); } $sirvComponent.attr('data-src', newSrc); } } } // Update stock status (catalogue only) if ($miniature.length && $stockStatus && $stockStatus.length) { $stockStatus.css('display', variant.out_of_stock ? 'flex' : 'none'); } // Update price if ($priceEl.length && variant.price_formatted) { if (!$priceEl.data('original-price')) $priceEl.data('original-price', $priceEl.text()); $priceEl.text(variant.price_formatted); } // Update feature texts dynamically from variant glass_types if (Array.isArray(variant.glass_types)) { // Collect all treatment values var treatmentValues = []; variant.glass_types.forEach(function(f) { if (!f.name || !f.value) return; // Update catalogue text labels if ($miniature.length) { if (f.name === 'Type de verres') { var $gt = $miniature.find('.variant-glass-type'); if ($gt.length) { if (!$gt.data('original-text')) $gt.data('original-text', $gt.text()); $gt.text(f.value); } } if (f.name === 'Traitements de verres') { treatmentValues.push(f.value); } } // Update medfeaturespictures icons (product page) if ($productPage.length && f.picto_url) { var $imgs = $('.medfeaturespictures img[data-feature-name="' + f.name + '"]'); $imgs.each(function() { if (!$(this).data('original-src')) $(this).data('original-src', $(this).attr('src')); $(this).attr('src', f.picto_url); }); } }); // Update treatment text with all values joined by " / " (catalogue only) if ($miniature.length && treatmentValues.length > 0) { var $gtr = $miniature.find('.variant-glass-treatment'); if ($gtr.length) { if (!$gtr.data('original-text')) $gtr.data('original-text', $gtr.text()); $gtr.text(treatmentValues.join(' / ')); } } // Update tech image (product page only) - from glass_type with attribute_type "glass" if ($productPage.length) { var glassFeat = variant.glass_types.find(function(f) { return f.attribute_type === 'glass'; }); var techImage = glassFeat && glassFeat.tech_image ? glassFeat.tech_image : null; var $techImg = $('#stn-product-tech-image'); if ($techImg.length) { if (!$techImg.data('original-src')) $techImg.data('original-src', $techImg.attr('src')); if (techImage) { $techImg.attr('src', techImage); } } } } }); // Reset to server-rendered defaults when mouse leaves variants area $(document).on('mouseleave', '.variantsProductWrapper, .productPageVariantsWrapper', function() { var $wrapper = $(this); var $miniature = $wrapper.closest('.product-miniature'); if ($miniature.length) { // Catalogue: restore image, price, stock, feature texts var $mainImage = $miniature.find('.thumbnail-container .image-cover'); var $priceEl = $miniature.find('.price'); var $stockStatus = $miniature.find('.variant-stock-status'); var originalSrc = $mainImage.data('original-src'); if (originalSrc) $mainImage.attr('src', originalSrc); var originalPrice = $priceEl.data('original-price'); if (originalPrice) $priceEl.text(originalPrice); var defaultDisplay = $stockStatus.data('default-display') || 'none'; $stockStatus.css('display', defaultDisplay); // Restore feature texts var $gt = $miniature.find('.variant-glass-type'); var origGt = $gt.data('original-text'); if (origGt) $gt.text(origGt); var $gtr = $miniature.find('.variant-glass-treatment'); var origGtr = $gtr.data('original-text'); if (origGtr) $gtr.text(origGtr); } else { // Fiche produit: restore main image, price, medfeaturespictures icons var $mainImage = $('img.smv-cursor-zoom-in').first(); var originalSrc = $mainImage.data('original-src'); if (originalSrc) { $mainImage.attr('src', originalSrc); // Restore Sirv data-src var $sirvComponent = $mainImage.closest('.smv-zoom-view'); if ($sirvComponent.length) { var originalDataSrc = $sirvComponent.data('original-data-src'); if (originalDataSrc) $sirvComponent.attr('data-src', originalDataSrc); } } var $priceEl = $('span[itemprop="price"]').first(); var originalPrice = $priceEl.data('original-price'); if (originalPrice) $priceEl.text(originalPrice); // Restore medfeaturespictures icons $('.medfeaturespictures img[data-feature-name]').each(function() { var orig = $(this).data('original-src'); if (orig) $(this).attr('src', orig); }); // Restore tech image var $techImg = $('#stn-product-tech-image'); if ($techImg.length) { var origTechSrc = $techImg.data('original-src'); if (origTechSrc) $techImg.attr('src', origTechSrc); } } }); // Store original image on page load and mark active variant $(document).ready(function() { $('.product-miniature .image-cover').each(function() { var $img = $(this); if (!$img.data('original-src')) { $img.data('original-src', $img.attr('src')); } }); // Mark the active/current variant on product page by matching SKU from displayed image // Wrapped in try-catch to avoid blocking mouseenter handlers try { $('.productPageVariantsWrapper').each(function() { var $wrapper = $(this); var $gridWrapper = $wrapper.find('.combinations-grid.flat-grid'); var productId = $wrapper.data('id-product'); var combinationsKey = 'product_' + productId; // Extract SKU from the currently displayed Sirv image URL var $mainImage = $('img.smv-cursor-zoom-in').first(); if (!$mainImage.length) return; // Skip if image not found var imageSrc = $mainImage.attr('src') || $mainImage.attr('data-src') || ''; if (!imageSrc) return; // Skip if no src // Extract SKU from Sirv URL pattern: https://opaldemetz.sirv.com/products/SKU/SKU_01.webp var currentSku = null; var sirvMatch = imageSrc.match(/\/products\/([^\/]+)\//); if (sirvMatch && sirvMatch[1]) { currentSku = sirvMatch[1]; } // If we found the SKU, find the matching variant and mark it as active if (currentSku && window.productCombinations && window.productCombinations[combinationsKey]) { var combinations = window.productCombinations[combinationsKey]; var activeAttrId = null; // Find the variant with matching SKU (reference) $.each(combinations, function(_, variant) { if (variant && variant.reference === currentSku) { activeAttrId = variant.id_product_attribute; return false; // break } }); // Mark the corresponding flat-grid-item as active if (activeAttrId) { $gridWrapper.find('.flat-grid-item[data-product-attribute="' + activeAttrId + '"]').addClass('active').siblings('.flat-grid-item').removeClass('active'); } } }); } catch(e) { console.error('Error marking active variant:', e); } }); // Handle click on flat grid item - select combination directly $(document).on('click', '.flat-grid-item', function(e) { e.preventDefault(); var productUrl = $(this).data('product-url'); if (productUrl) { window.location.href = productUrl; } }); // Mobile: handle tap directly via touchend to bypass iOS double-tap requirement $(document).on('touchstart', '.flat-grid-item', function() { $(this).data('touch-moved', false); }); $(document).on('touchmove', '.flat-grid-item', function() { $(this).data('touch-moved', true); }); $(document).on('touchend', '.flat-grid-item', function(e) { if ($(this).data('touch-moved')) return; var productUrl = $(this).data('product-url'); if (productUrl) { e.preventDefault(); window.location.href = productUrl; } }); // ============================================================ // MODAL - Toutes les variantes // ============================================================ // Build modal shell once $(document).ready(function() { if (!$('#variants-modal').length) { $('body').append( '
' + '
' + '
' + '
' + '
' + '

Toutes les variantes

' + ' ' + '
' + '
' + ' ' + ' ' + ' ' + ' Rupture de stock' + ' ' + '
' + '
' + '
' + '
' + '
' + '
' ); } }); function variantsModalResetInfo() { var $modal = $('#variants-modal'); $('.modal-product-glass-type').text($modal.data('default-glass-type') || ''); $('.modal-product-treatment').text($modal.data('default-treatment') || ''); $('.modal-stock-status').hide(); } function variantsModalClose() { $('#variants-modal').removeClass('active'); $('body').css('overflow', ''); variantsModalResetInfo(); } function variantsModalOpen(productId) { var combinationsKey = 'product_' + productId; var combinations = window.productCombinations && window.productCombinations[combinationsKey]; if (!combinations) return; // Extract product-level features from first combination var glassTypeValue = ''; var glassTreatmentValue = []; var firstKey = Object.keys(combinations)[0]; if (firstKey && Array.isArray(combinations[firstKey].glass_types)) { combinations[firstKey].glass_types.forEach(function(f) { console.log(f); if (f.name === 'Type de verres' && !glassTypeValue) glassTypeValue = f.value; if (f.name === 'Traitements de verres') glassTreatmentValue.push(f.value); }); } var gridHtml = ''; $.each(combinations, function(idProdAttr, comb) { if (!comb.image_small || !comb.url) return; gridHtml += '
' + '' + (comb.out_of_stock ? '' : '') + '
'; }); $('#variants-modal-grid').html(gridHtml); var $modal = $('#variants-modal'); $modal.data('product-id', productId); $modal.data('default-glass-type', glassTypeValue); $modal.data('default-treatment', glassTreatmentValue.join(' / ')); // Show static product features in header (type de verres · traitements) $('.modal-product-glass-type').text(glassTypeValue || '').toggle(!!glassTypeValue); $('.modal-product-treatment').text(glassTreatmentValue.join(' / ') || '').toggle(!!glassTreatmentValue); $('#variants-modal').addClass('active'); $('body').css('overflow', 'hidden'); } // Open on badge click (catalogue + fiche produit) $(document).on('click', '.flat-grid-overflow-badge', function(e) { e.preventDefault(); e.stopPropagation(); var $miniature = $(this).closest('.product-miniature'); var $productPage = $(this).closest('.productPageVariantsWrapper'); var productId = $miniature.length ? $miniature.data('id-product') : ($productPage.length ? $productPage.data('id-product') : null); if (productId) variantsModalOpen(productId); }); // Close on X button $(document).on('click', '.variants-modal-close', function(e) { e.stopPropagation(); variantsModalClose(); }); // Close on backdrop click $(document).on('click', '#variants-modal', function(e) { if (e.target === this) variantsModalClose(); }); // Close on ESC $(document).on('keyup', function(e) { if (e.key === 'Escape' && $('#variants-modal').hasClass('active')) { variantsModalClose(); } }); // Navigate to product on item click $(document).on('click', '.variants-modal-item', function() { var url = $(this).data('url'); if (url) window.location.href = url; }); // Show info in header on item hover $(document).on('mouseenter', '.variants-modal-item', function() { var outOfStock = $(this).attr('data-out-of-stock') === '1'; $('.modal-stock-status').css('display', outOfStock ? 'flex' : 'none'); var productId = $('#variants-modal').data('product-id'); var productAttr = $(this).data('product-attribute'); var combinationsKey = 'product_' + productId; var variant = window.productCombinations && window.productCombinations[combinationsKey] && window.productCombinations[combinationsKey][productAttr]; if (variant && Array.isArray(variant.glass_types)) { var glassType = '', treatment = []; variant.glass_types.forEach(function(f) { if (f.name === 'Type de verres') glassType = f.value; if (f.name === 'Traitements de verres') treatment.push(f.value); }); $('.modal-product-glass-type').text(glassType); $('.modal-product-treatment').text(treatment.join(' / ')); } }); // Clear header info when mouse leaves the grid $(document).on('mouseleave', '#variants-modal-grid', function(e) { var related = e.relatedTarget || e.toElement; if (related && $(related).closest('#variants-modal-grid').length) return; variantsModalResetInfo(); }); // Fitting box catalogue badges const fittingBoxBadges = document.querySelectorAll('.product-flags .fitting-box'); if (fittingBoxBadges) { fittingBoxBadges.forEach(function (badge) { let ean = badge.getAttribute('data-ean'); fetch("https://product-api.fittingbox.com/glasses-metadata/availability?apiKey=3X2HsDJLa4khYIsSbvh93LttKOt4BaV3K7SGWz4I&uidList="+ean, { "method": "GET", }) .then(function(response) { return response.json(); }) .then(function(data) { let available = data[0].available; if (!available) { badge.style.display = 'none'; } }) .catch(console.error.bind(console)); }); } document.addEventListener("DOMContentLoaded", function () { function sendGAEvent(productSKU) { // Ensure GA4 is loaded if (typeof gtag !== "function") { console.warn("Google Analytics 4 (gtag) is not loaded."); return; } // Send event to GA4 gtag("event", "fitting_box_open", { product_sku: productSKU }); // console.log("GA4 event sent: fitting_box_open", { product_sku: productSKU }); } // Track click on #fittingBoxOpen (product page) const fittingBoxButton = document.getElementById("fittingBoxOpen"); if (fittingBoxButton) { fittingBoxButton.addEventListener("click", function () { let skuElement = document.querySelector('span[itemprop="sku"]'); let productSKU = skuElement ? skuElement.textContent.trim() : "unknown_sku"; sendGAEvent(productSKU); }); } // Track click on fitting page if (document.body.id === "module-myFittingBoxIntegration-fitting") { document.querySelectorAll(".fb-product").forEach(element => { element.addEventListener("click", function () { let skuElement = this.querySelector("small"); let productSKU = skuElement ? skuElement.textContent.trim() : "unknown_sku"; sendGAEvent(productSKU); }); }); } });