/*
* 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 += '
';
$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(
'