PageSourceSearch

https://www.saporinostri.it/app/themes/saporinostri-classic/assets/js/main.js?ver=2.4.6

js saporinostri.it collected 2026-10-02 12:37:21 UTC 51,300 bytes, 1,337 lines download raw bytes

1/**
2 * Sapori Nostri Classic — main.js
3 */
4(function () {
5	'use strict';
6
7	document.addEventListener('DOMContentLoaded', function () {
8
9		/* ── Mobile menu toggle (drawer laterale da sinistra) ────────────────── */
10		(function initMobileMenu() {
11			var toggle    = document.querySelector('.mobile-menu-toggle');
12			var nav       = document.getElementById('primary-nav');
13			var overlay   = document.getElementById('mobile-nav-overlay');
14			var closeBtn  = document.getElementById('mobile-nav-close');
15			if (!toggle || !nav) return;
16
17			function openMenu() {
18				toggle.classList.add('is-active');
19				nav.classList.add('is-open');
20				toggle.setAttribute('aria-expanded', 'true');
21				if (overlay) overlay.classList.add('is-visible');
22				document.body.style.overflow = 'hidden';
23			}
24
25			function closeMenu() {
26				toggle.classList.remove('is-active');
27				nav.classList.remove('is-open');
28				toggle.setAttribute('aria-expanded', 'false');
29				if (overlay) overlay.classList.remove('is-visible');
30				document.body.style.overflow = '';
31			}
32
33			toggle.addEventListener('click', function () {
34				var expanded = toggle.getAttribute('aria-expanded') === 'true';
35				if (expanded) closeMenu(); else openMenu();
36			});
37
38			if (closeBtn) closeBtn.addEventListener('click', closeMenu);
39			if (overlay) overlay.addEventListener('click', closeMenu);
40
41			document.addEventListener('keydown', function (e) {
42				if (e.key === 'Escape' && nav.classList.contains('is-open')) closeMenu();
43			});
44		})();
45
46		/* ── Mobile sub-menu accordion (frecce) ──────────────────────────── */
47		document.querySelectorAll('.primary-nav-list > li.menu-item-has-children').forEach(function (li) {
48			if (li.querySelector('.sn-nav-arrow')) return; // già aggiunto
49			var subMenu = li.querySelector('.sub-menu');
50			if (!subMenu) return;
51
52			var arrowBtn = document.createElement('button');
53			arrowBtn.type = 'button';
54			arrowBtn.className = 'sn-nav-arrow';
55			arrowBtn.setAttribute('aria-label', 'Espandi sottomenu');
56			arrowBtn.setAttribute('aria-expanded', 'false');
57			arrowBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="6 9 12 15 18 9"/></svg>';
58
59			// Inserita subito dopo il link (non in coda alla <li>, dopo tutto il
60			// sub-menu): ordine di tab coerente e area di tap adiacente al link
61			// invece che lontana in fondo al DOM.
62			li.insertBefore(arrowBtn, subMenu);
63
64			function toggleSubMenu(e) {
65				e.preventDefault();
66				e.stopPropagation();
67				var open = subMenu.classList.toggle('is-open');
68				arrowBtn.classList.toggle('is-open', open);
69				arrowBtn.setAttribute('aria-expanded', String(open));
70			}
71
72			// Il drawer scrolla (.header-nav overflow-y:auto) e, quando il
73			// sub-menu si apre, il layout sotto la freccia cambia sotto al dito:
74			// affidarsi al solo evento "click" sintetico lascia che il browser
75			// reinterpreti a volte il secondo tap come inizio di uno scroll
76			// invece che come tap, "perdendo" la chiusura. Gestendo il toggle
77			// già su touchend (con preventDefault, che sopprime il click
78			// sintetico successivo) il risultato non dipende più da questa
79			// disambiguazione nativa. Il flag copre i browser/WebView che non
80			// sopprimono in modo affidabile il click dopo preventDefault su touchend.
81			var arrowTouchHandled = false;
82			arrowBtn.addEventListener('touchend', function (e) {
83				arrowTouchHandled = true;
84				toggleSubMenu(e);
85				setTimeout(function () { arrowTouchHandled = false; }, 400);
86			}, { passive: false });
87
88			arrowBtn.addEventListener('click', function (e) {
89				if (arrowTouchHandled) { e.preventDefault(); e.stopPropagation(); return; }
90				toggleSubMenu(e);
91			});
92		});
93
94		/* ── Shop sidebar mobile toggle ──────────────────────────────────── */
95		(function initShopSidebar() {
96			var sidebarToggle  = document.getElementById('sn-sidebar-toggle');
97			var sidebarClose   = document.getElementById('sn-sidebar-close');
98			var sidebarOverlay = document.getElementById('sn-sidebar-overlay');
99			var sidebar        = document.getElementById('shop-sidebar');
100			var sidebarHeader  = sidebar ? sidebar.querySelector('.sn-sidebar-header') : null;
101
102			if (!sidebarToggle || !sidebar) return;
103
104			// Mostra l'header mobile della sidebar (nascosto di default via style="display:none")
105			if (sidebarHeader) sidebarHeader.style.removeProperty('display');
106
107			function openSidebar() {
108				sidebar.classList.add('is-open');
109				sidebar.setAttribute('aria-hidden', 'false');
110				sidebarToggle.setAttribute('aria-expanded', 'true');
111				if (sidebarOverlay) sidebarOverlay.classList.add('is-visible');
112				document.body.style.overflow = 'hidden';
113			}
114
115			function closeSidebar() {
116				sidebar.classList.remove('is-open');
117				sidebar.setAttribute('aria-hidden', 'true');
118				sidebarToggle.setAttribute('aria-expanded', 'false');
119				if (sidebarOverlay) sidebarOverlay.classList.remove('is-visible');
120				document.body.style.overflow = '';
121			}
122
123			sidebarToggle.addEventListener('click', openSidebar);
124			if (sidebarClose) sidebarClose.addEventListener('click', closeSidebar);
125			if (sidebarOverlay) sidebarOverlay.addEventListener('click', closeSidebar);
126
127			document.addEventListener('keydown', function (e) {
128				if (e.key === 'Escape' && sidebar.classList.contains('is-open')) closeSidebar();
129			});
130
131			// Aggiorna badge contatore filtri attivi
132			function updateFilterCount() {
133				var countEl = document.getElementById('sn-filter-count');
134				if (!countEl) return;
135				var chosen = sidebar.querySelectorAll('.chosen').length;
136				countEl.textContent = chosen > 0 ? String(chosen) : '';
137			}
138			updateFilterCount();
139			// Ri-esegui dopo ogni refresh WooCommerce
140			document.body.addEventListener('wc_fragments_refreshed', updateFilterCount);
141		})();
142
143		/* ── Tab filtro categorie prodotti ───────────────────────────────── */
144		const tabList = document.querySelector('.product-tabs');
145		if (tabList) {
146			tabList.addEventListener('click', function (e) {
147				const btn = e.target.closest('.product-tab');
148				if (!btn) return;
149
150				// Deseleziona tutti
151				tabList.querySelectorAll('.product-tab').forEach(function (b) {
152					b.classList.remove('is-active');
153					b.setAttribute('aria-selected', 'false');
154				});
155
156				// Nascondi tutti i pannelli
157				document.querySelectorAll('.product-tab-pane').forEach(function (p) {
158					p.classList.remove('is-active');
159				});
160
161				// Attiva quello cliccato
162				btn.classList.add('is-active');
163				btn.setAttribute('aria-selected', 'true');
164
165				const target = document.getElementById('tab-pane-' + btn.dataset.tab);
166				if (target) target.classList.add('is-active');
167			});
168		}
169
170		/* ── Carousel categorie homepage ─────────────────────────────────── */
171		document.querySelectorAll('.categories-section').forEach(function (section) {
172			const list = section.querySelector('.sn-cat-list');
173			const prev = section.querySelector('[data-cat-scroll="prev"]');
174			const next = section.querySelector('[data-cat-scroll="next"]');
175			if (!list || !prev || !next) return;
176
177			function getStep() {
178				return Math.max(220, Math.round(list.clientWidth * 0.68));
179			}
180
181			function updateButtons() {
182				const max = list.scrollWidth - list.clientWidth;
183				if (max <= 8) {
184					prev.disabled = true;
185					next.disabled = true;
186					return;
187				}
188				prev.disabled = list.scrollLeft <= 4;
189				next.disabled = list.scrollLeft >= (max - 4);
190			}
191
192			prev.addEventListener('click', function () {
193				list.scrollBy({ left: -getStep(), behavior: 'smooth' });
194			});
195
196			next.addEventListener('click', function () {
197				list.scrollBy({ left: getStep(), behavior: 'smooth' });
198			});
199
200			list.addEventListener('scroll', updateButtons, { passive: true });
201			window.addEventListener('resize', updateButtons);
202			updateButtons();
203		});
204
205		/* ── Product card images lazyload fallback ───────────────────────── */
206		(function fixCardLazyImages() {
207			function promoteLazyImage(img) {
208				if (!img || !img.classList.contains('lazyload')) return;
209				const dataSrc = img.getAttribute('data-src');
210				if (!dataSrc) return;
211
212				const currentSrc = img.getAttribute('src') || '';
213				const isPlaceholder = currentSrc.startsWith('data:image/');
214				if (!isPlaceholder && img.naturalWidth > 2) return;
215
216				const dataSrcset = img.getAttribute('data-srcset');
217				const dataSizes  = img.getAttribute('data-sizes');
218
219				if (dataSrcset) img.setAttribute('srcset', dataSrcset);
220				if (dataSizes) img.setAttribute('sizes', dataSizes);
221				img.setAttribute('src', dataSrc);
222
223				img.classList.remove('lazyload');
224				img.classList.add('lazyloaded');
225				img.style.removeProperty('opacity');
226				img.style.setProperty('opacity', '1', 'important');
227				img.style.removeProperty('visibility');
228				img.style.setProperty('visibility', 'visible', 'important');
229			}
230
231			function scan(root) {
232				const scope = root && root.querySelectorAll ? root : document;
233				scope.querySelectorAll('.sn-product-card .product-img-wrap img.lazyload[data-src]').forEach(promoteLazy
233Image);
234			}
235
236			scan(document);
237
238			const observer = new MutationObserver(function (mutations) {
239				mutations.forEach(function (mutation) {
240					mutation.addedNodes.forEach(function (node) {
241						if (node.nodeType !== 1) return;
242						scan(node);
243					});
244				});
245			});
246
247			observer.observe(document.body, { childList: true, subtree: true });
248		})();
249
250		/* ── Sidebar rating filter count alignment ───────────────────────── */
251		(function normalizeRatingFilterCount() {
252			function enhance(root) {
253				const scope = root && root.querySelectorAll ? root : document;
254				scope.querySelectorAll('.shop-sidebar li.wc-layered-nav-rating > a').forEach(function (link) {
255					if (link.querySelector('.wc-layered-nav-rating__count')) return;
256
257					const rawHtml = link.innerHTML;
258					const match = rawHtml.match(/\((\d+)\)\s*$/);
259					if (!match) return;
260
261					const count = '(' + match[1] + ')';
262					link.innerHTML = rawHtml.replace(/\s*\(\d+\)\s*$/, '');
263
264					const countEl = document.createElement('span');
265					countEl.className = 'wc-layered-nav-rating__count';
266					countEl.textContent = count;
267					link.appendChild(countEl);
268				});
269			}
270
271			enhance(document);
272			const observer = new MutationObserver(function (mutations) {
273				mutations.forEach(function (mutation) {
274					mutation.addedNodes.forEach(function (node) {
275						if (node.nodeType !== 1) return;
276						enhance(node);
277					});
278				});
279			});
280			observer.observe(document.body, { childList: true, subtree: true });
281		})();
282
283		/* ── Qty +/- buttons ─────────────────────────────────────────────── */
284		document.body.addEventListener('click', function (e) {
285			const btn = e.target.closest('.sn-qty-btn');
286			if (!btn) return;
287			const group = btn.closest('.sn-qty-group');
288			if (!group) return;
289			const input = group.querySelector('input.qty');
290			if (!input) return;
291			const current = parseInt(input.value, 10) || 0;
292			const min     = parseInt(input.getAttribute('min'), 10) || 0;
293			const max     = parseInt(input.getAttribute('max'), 10) || Infinity;
294
295			if (btn.classList.contains('sn-qty-btn--minus')) {
296				input.value = Math.max(min, current - 1);
297			} else {
298				input.value = Math.min(max, current + 1);
299			}
300			// Trigger change per aggiornare totali WooCommerce
301			input.dispatchEvent(new Event('change', { bubbles: true }));
302		});
303
304		/* ── Reviews tab hash handling ───────────────────────────────────── */
305		function openReviewsTab() {
306			const tabsRoot = document.querySelector('.woocommerce-tabs');
307			if (!tabsRoot) return false;
308
309			const tabTitle = tabsRoot.querySelector('#tab-title-reviews');
310			const tabLink  = tabsRoot.querySelector('.wc-tabs a[href="#tab-reviews"]');
311			const panel    = tabsRoot.querySelector('#tab-reviews');
312			if (!tabTitle || !tabLink || !panel) return false;
313
314			// Prova prima ad attivare la tab via evento click (compatibile WooCommerce).
315			tabLink.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
316
317			// Fallback: forza stato active se nessuno script sta gestendo le tab.
318			if (!tabTitle.classList.contains('active')) {
319				tabsRoot.querySelectorAll('.wc-tabs > li').forEach(function (li) {
320					li.classList.remove('active');
321				});
322				tabsRoot.querySelectorAll('.wc-tab').forEach(function (p) {
323					p.style.display = 'none';
324				});
325				tabTitle.classList.add('active');
326				panel.style.display = 'block';
327			}
328
329			const reviewAnchor = panel.querySelector('#reviews') || panel;
330			reviewAnchor.scrollIntoView({ behavior: 'smooth', block: 'start' });
331			return true;
332		}
333
334		function shouldHandleReviewsHash(hash) {
335			const normalized = (hash || '').toLowerCase();
336			return normalized === '#reviews' || normalized === '#tab-reviews';
337		}
338
339		function syncReviewsHashAndOpen() {
340			if (!shouldHandleReviewsHash(window.location.hash)) return;
341			history.replaceState(null, '', window.location.pathname + window.location.search + '#tab-reviews');
342			window.requestAnimationFrame(openReviewsTab);
343		}
344
345		syncReviewsHashAndOpen();
346		window.addEventListener('hashchange', syncReviewsHashAndOpen);
347
348		document.body.addEventListener('click', function (e) {
349			const link = e.target.closest('a[href*="#reviews"], a[href*="#tab-reviews"]');
350			if (!link) return;
351
352			let targetUrl;
353			try {
354				targetUrl = new URL(link.getAttribute('href'), window.location.href);
355			} catch (err) {
356				return;
357			}
358
359			const samePage = targetUrl.pathname === window.location.pathname && targetUrl.search === window.location.search;
360			if (!samePage || !shouldHandleReviewsHash(targetUrl.hash)) return;
361
362			e.preventDefault();
363			history.replaceState(null, '', window.location.pathname + window.location.search + '#tab-reviews');
364			openReviewsTab();
365		});
366
367		/* ── Single product gallery trigger icon ─────────────────────────── */
368		(function initGalleryTriggerIcon() {
369			function applyGalleryTriggerEnhancements() {
370				const gallery = document.querySelector('.woocommerce-product-gallery');
371				if (!gallery) return false;
372
373				const trigger = gallery.querySelector('.woocommerce-product-gallery__trigger');
374				if (!trigger) return false;
375
376				const viewport = gallery.querySelector('.flex-viewport');
377				if (viewport && !viewport.contains(trigger)) {
378					viewport.appendChild(trigger);
379				}
380
381				trigger.classList.add('sn-gallery-trigger');
382				trigger.innerHTML = '<i class="fa-solid fa-up-right-and-down-left-from-center" aria-hidden="true"></i>';
383				return true;
384			}
385
386			if (applyGalleryTriggerEnhancements()) return;
387			window.setTimeout(applyGalleryTriggerEnhancements, 250);
388			window.setTimeout(applyGalleryTriggerEnhancements, 800);
389			window.addEventListener('load', applyGalleryTriggerEnhancements, { once: true });
390		})();
391
392		/* ── Update cart count via WooCommerce fragment ───────────────────── */
393		// WooCommerce aggiorna automaticamente il fragment '.cart-count' e
394		// '#cart-drawer-body' via AJAX dopo ogni aggiunta al carrello.
395		// Il badge è sempre nel DOM; viene nascosto/mostrato via classe CSS.
396		document.body.addEventListener('added_to_cart', function () {
397			openCartDrawer();
398			// Bounce sull'icona carrello
399			var toggle = document.getElementById('cart-drawer-toggle');
400			if (toggle) {
401				toggle.classList.remove('sn-cart-pulse');
402				void toggle.offsetWidth;
403				toggle.classList.add('sn-cart-pulse');
404				toggle.addEventListener('animationend', function h() {
405					toggle.classList.remove('sn-cart-pulse');
406					toggle.removeEventListener('animationend', h);
407				});
408			}
409		});
410
411		/* ── ATC button: spinner + icona carrello + fly-to-cart ──────────────── */
412		var CART_SVG_INNER = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>';
413
414		// Quando il btn-atc entra in .loading o .added inietta l'icona corretta.
415		function updateAtcIcon(btn) {
416			var prev = btn.querySelector('.sn-atc-icon, .sn-atc-spinner');
417			if (prev) prev.remove();
418
419			if (btn.classList.contains('loading')) {
420				var wrap = document.createElement('span');
421				wrap.className = 'sn-atc-icon';
422				var spinner = document.createElement('span');
423				spinner.className = 'sn-atc-spinner';
424				wrap.appendChild(spinner);
425				btn.appendChild(wrap);
426			} else if (btn.classList.contains('added')) {
427				var icon = document.createElement('span');
428				icon.className = 'sn-atc-icon';
429				icon.innerHTML = CART_SVG_INNER;
430				btn.appendChild(icon);
431				flyToCart(btn);
432			}
433		}
434
435		var atcObserver = new MutationObserver(function (mutations) {
436			mutations.forEach(function (m) {
437				if (m.attributeName !== 'class') return;
438				var btn = m.target;
439				if (btn.classList.contains('btn-atc')) updateAtcIcon(btn);
440			});
441		});
442
443		function observeAtcBtns() {
444			document.querySelectorAll('.btn-atc').forEach(function (btn) {
445				atcObserver.observe(btn, { attributes: true, attributeFilter: ['class'] });
446			});
447		}
448		observeAtcBtns();
449		document.body.addEventListener('wc_fragments_refreshed', observeAtcBtns);
450
451		// Anima una bolla con l'immagine del prodotto verso l'icona carrello.
452		var CART_SVG  = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2 3 6v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6l-3-4z"/><line x1="3" y1="6" x2="21" y2="6"/><path d="M16 10a4 4 0 0 1-8 0"/></svg>';
453		var CHECK_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>';
454
455		function swapCartIcon(cartBtn, svgHtml) {
456			var existing = cartBtn.querySelector('svg');
457			if (existing) existing.remove();
458			var tmp = document.createElement('div');
459			tmp.innerHTML = svgHtml;
460			cartBtn.insertBefore(tmp.firstElementChild, cartBtn.firstChild);
461		}
462
463		function animatedSwapIcon(cartBtn, svgHtml, onDone) {
464			var existing = cartBtn.querySelector('svg');
465			if (!existing) {
466				swapCartIcon(cartBtn, svgHtml);
467				if (onDone) onDone();
468				return;
469			}
470			var scaleDown = existing.animate([
471				{ transform: 'scale(1)', opacity: 1 },
472				{ transform: 'scale(0)', opacity: 0 }
473			], { duration: 130, easing: 'ease-in', fill: 'forwards' });
474			scaleDown.addEventListener('finish', function () {
475				swapCartIcon(cartBtn, svgHtml);
476				var newSvg = cartBtn.querySelector('svg');
477				if (newSvg) {
478					newSvg.animate([
479						{ transform: 'scale(0)', opacity: 0 },
480						{ transform: 'scale(1.25)', opacity: 1 },
481						{ transform: 'scale(1)',    opacity: 1 }
482					], { duration: 220, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)' });
483				}
484				if (onDone) onDone();
485			});
486		}
487
488		function flyToCart(btn) {
489			var cartBtn = document.getElementById('cart-drawer-toggle');
490			if (!cartBtn) return;
491
492			var card = btn.closest('.sn-product-card');
493			if (!card) return;
494
495			var img = card.querySelector('.product-img-wrap img');
496			if (!img) return;
497
498			var imgRect  = img.getBoundingClientRect();
499			var cartRect = cartBtn.getBoundingClientRect();
500
501			var SIZE   = 52;
502			var startX = imgRect.left  + imgRect.width  / 2 - SIZE / 2;
503			var startY = imgRect.top   + imgRect.height / 2 - SIZE / 2;
504
505			var bubble = document.createElement('div');
506			bubble.className = 'sn-fly-bubble';
507			bubble.style.left = startX + 'px';
508			bubble.style.top  = startY + 'px';
509
510			var bubbleImg = document.createElement('img');
511			bubbleImg.src = img.src;
512			bubbleImg.setAttribute('aria-hidden', 'true');
513			bubble.appendChild(bubbleImg);
514			document.body.appendChild(bubble);
515
516			var targetX = cartRect.left + cartRect.width  / 2 - SIZE / 2;
517			var targetY = cartRect.top  + cartRect.height / 2 - SIZE / 2;
518			var dx = targetX - startX;
519			var dy = targetY - startY;
520
521			var anim = bubble.animate([
522				{ transform: 'translate(0px, 0px) scale(1)',                              opacity: 0.9 },
523				{ transform: 'translate(' + dx + 'px, ' + dy + 'px) scale(0.15)',        opacity: 0 }
524			], {
525				duration: 900,
526				easing: 'ease-in',
527				fill: 'forwards'
528			});
529			anim.addEventListener('finish', function () {
530				bubble.remove();
531				var cartBtn = document.getElementById('cart-drawer-toggle');
532				if (cartBtn) {
533					cartBtn.classList.remove('sn-cart-land');
534					void cartBtn.offsetWidth;
535					// Swap → check con zoom, poi avvia il bounce
536					animatedSwapIcon(cartBtn, CHECK_SVG, function () {
537						cartBtn.classList.add('sn-cart-land');
538						cartBtn.addEventListener('animationend', function h() {
539							cartBtn.classList.remove('sn-cart-land');
540							cartBtn.removeEventListener('animationend', h);
541							// Swap → carrello con zoom a fine bounce
542							animatedSwapIcon(cartBtn, CART_SVG);
543						});
544					});
545				}
546			});
547		}
548
549		// Notifica WC Blocks che il carrello è cambiato (Block Cart → ricalcola)
550		function invalidateBlocksCart() {
551			if (!window.wp || !window.wp.data) return;
552			var dispatch = window.wp.data.dispatch('wc/store/cart');
553			if (dispatch && dispatch.invalidateResolutionForStore) {
554				dispatch.invalidateResolutionForStore();
555			}
556		}
557
558		// Aggiorna la quantità di un item nel mini-cart via AJAX.
559		function updateCartItemQty(key, qty) {
560			if (!window.snCart) return;
561			var drawerBody = document.getElementById('cart-drawer-body');
562			if (drawerBody) drawerBody.classList.add('sn-loading');
563
564			var fd = new FormData();
565			fd.append('action', 'sn_cart_update_qty');
566			fd.append('nonce', window.snCart.nonce);
567			fd.append('key', key);
568			fd.append('qty', qty);
569
570			fetch(window.snCart.ajaxUrl, { method: 'POST', body: fd })
571				.then(function (r) { return r.json(); })
572				.then(function (data) {
573					if (!data.success) return;
574					// Sostituisce il body del drawer con il nuovo HTML
575					var tmp = document.createElement('div');
576					tmp.innerHTML = data.data.cart_html;
577					var newBody = tmp.firstElementChild;
578					if (drawerBody && newBody) {
579						drawerBody.replaceWith(newBody);
580					}
581					// Aggiorna il badge carrello nell'header
582					var badge = document.querySelector('.cart-count');
583					if (badge && data.data.badge_html) {
584						var tmpBadge = document.createElement('div');
585						tmpBadge.innerHTML = data.data.badge_html;
586						badge.replaceWith(tmpBadge.firstElementChild);
587					}
588					// Aggiorna il footer (totale + pulsanti)
589					var footer = document.getElementById('cart-drawer-footer');
590					if (footer && data.data.footer_html) {
591						var tmpFooter = document.createElement('div');
592						tmpFooter.innerHTML = data.data.footer_html;
593						footer.replaceWith(tmpFooter.firstElementChild);
594					}
595				// Notifica Block Cart del cambio (se presente)
596				invalidateBlocksCart();
597				// Se siamo in checkout, aggiorna il riepilogo ordine
598				if (document.body.classList.contains('woocommerce-checkout') && typeof jQuery !== 'undefined') {
599					jQuery(document.body).trigger('update_checkout');
600				}
601			})
602			.catch(function () {
603				if (drawerBody) drawerBody.classList.remove('sn-loading');
604			});
605		}
606
607		const drawerToggle  = document.getElementById('cart-drawer-toggle');
608		const drawer        = document.getElementById('cart-drawer');
609		const drawerOverlay = document.getElementById('cart-drawer-overlay');
610		const drawerClose   = document.getElementById('cart-drawer-close');
611
612		function openCartDrawer() {
613			if (!drawer) return;
614			drawer.classList.add('is-open');
615			drawer.setAttribute('aria-hidden', 'false');
616			if (drawerOverlay) {
617				drawerOverlay.classList.add('is-open');
618				drawerOverlay.setAttribute('aria-hidden', 'false');
619			}
620			if (drawerToggle) drawerToggle.setAttribute('aria-expanded', 'true');
621			document.body.style.overflow = 'hidden';
622			// Focus primo elemento interattivo nel drawer
623			const firstFocus = drawer.querySelector('a, button');
624			if (firstFocus) firstFocus.focus();
625		}
626
627		function closeCartDrawer() {
628			if (!drawer) return;
629			drawer.classList.remove('is-open');
630			drawer.setAttribute('aria-hidden', 'true');
631			if (drawerOverlay) {
632				drawerOverlay.classList.remove('is-open');
633				drawerOverlay.setAttribute('aria-hidden', 'true');
634			}
635			if (drawerToggle) {
636				drawerToggle.setAttribute('aria-expanded', 'false');
637				drawerToggle.focus();
638			}
639			document.body.style.overflow = '';
640		}
641
642		if (drawerToggle) {
643			drawerToggle.addEventListener('click', function () {
644				if (drawer && drawer.classList.contains('is-open')) {
645					closeCartDrawer();
646				} else {
647					openCartDrawer();
648				}
649			});
650		}
651
652		if (drawerClose) {
653			drawerClose.addEventListener('click', closeCartDrawer);
654		}
655
656		if (drawerOverlay) {
657			drawerOverlay.addEventListener('click', closeCartDrawer);
658		}
659
660		// Chiude con Escape
661		document.addEventListener('keydown', function (e) {
662			if (e.key === 'Escape' && drawer && drawer.classList.contains('is-open')) {
663				closeCartDrawer();
664			}
665		});
666
667		// ── Delegated click: pulsanti quantità nel mini-cart ─────────────────
668		if (drawer) {
669			drawer.addEventListener('click', function (e) {
670				var qtyBtn = e.target.closest('.sn-mini-qty-btn');
671				if (!qtyBtn || qtyBtn.disabled) return;
672				var key = qtyBtn.dataset.key;
673				var qty = parseInt(qtyBtn.dataset.qty, 10);
674				if (!key || isNaN(qty)) return;
675				updateCartItemQty(key, qty);
676			});
677		}
678
679		// ── Delegated click: rimuovi item dal mini-cart ───────────────────────
680		if (drawer) {
681			drawer.addEventListener('click', function (e) {
682				var removeBtn = e.target.closest('.sn-mini-remove');
683				if (!removeBtn) return;
684				e.preventDefault();
685
686				var key = removeBtn.dataset.cart_item_key;
687				if (!key || !window.snCart) return;
688
689				var drawerBody = document.getElementById('cart-drawer-body');
690				if (drawerBody) drawerBody.classList.add('sn-loading');
691
692				var fd = new FormData();
693				fd.append('action', 'sn_cart_remove_item');
694				fd.append('nonce', window.snCart.nonce);
695				fd.append('key', key);
696
697				fetch(window.snCart.ajaxUrl, { method: 'POST', body: fd })
698					.then(function (r) { return r.json(); })
699					.then(function (data) {
700						if (!data.success) return;
701						var tmp = document.createElement('div');
702						tmp.innerHTML = data.data.cart_html;
703						var newBody = tmp.firstElementChild;
704						if (drawerBody && newBody) drawerBody.replaceWith(newBody);
705
706						var badge = document.querySelector('.cart-count');
707						if (badge && data.data.badge_html) {
708							var tmpBadge = document.createElement('div');
709							tmpBadge.innerHTML = data.data.badge_html;
710							badge.replaceWith(tmpBadge.firstElementChild);
711						}
712
713						var footer = document.getElementById('cart-drawer-footer');
714						if (footer && data.data.footer_html) {
715							var tmpFooter = document.createElement('div');
716							tmpFooter.innerHTML = data.data.footer_html;
717							footer.replaceWith(tmpFooter.firstElementChild);
718						}
719						// Notifica Block Cart del cambio (se presente)
720						invalidateBlocksCart();
721						// Se siamo in checkout, aggiorna il riepilogo ordine
722						if (document.body.classList.contains('woocommerce-checkout') && typeof jQuery !== 'undefined') {
723							jQuery(document.body).trigger('update_checkout');
724						}
725					})
726					.catch(function () {
727						if (drawerBody) drawerBody.classList.remove('sn-loading');
728					});
729			});
730		}
731
732		/* ── Cart page: AJAX rimozione e aggiornamento qty ──────────────────── */
733		(function initCartPageAjax() {
734			var cartLayout = document.querySelector('.sn-cart-layout');
735			if (!cartLayout || !window.snCart) return;
736
737			function updateCartSidebar(totalsHtml) {
738				var sidebar = document.querySelector('.sn-cart-sidebar');
739				if (sidebar && totalsHtml) {
740					var tmp = document.createElement('div');
741					tmp.innerHTML = totalsHtml;
742					// Sostituisce il contenuto mantenendo il wrapper .sn-cart-sidebar
743					sidebar.innerHTML = tmp.innerHTML;
744				}
745			}
746
747			function updateCartBadgePage(badgeHtml) {
748				var badge = document.querySelector('.cart-count');
749				if (badge && badgeHtml) {
750					var tmp = document.createElement('div');
751					tmp.innerHTML = badgeHtml;
752					if (tmp.firstElementChild) badge.replaceWith(tmp.firstElementChild);
753				}
754				// Aggiorna anche il drawer badge
755				var drawerBadge = document.querySelector('.cart-count');
756				if (drawerBadge && badgeHtml) {
757					var tmp2 = document.createElement('div');
758					tmp2.innerHTML = badgeHtml;
759					if (tmp2.firstElementChild) drawerBadge.replaceWith(tmp2.firstElementChild);
760				}
761			}
762
763			// ── Rimozione AJAX ──────────────────────────────────────────────
764			cartLayout.addEventListener('click', function (e) {
765				var removeLink = e.target.closest('.sn-cart-remove');
766				if (!removeLink) return;
767				e.preventDefault();
768
769				var row = removeLink.closest('tr[data-cart-item-key]');
770				var key = removeLink.dataset.cart_item_key ||
771				          (row && row.dataset.cartItemKey);
772				if (!key) return;
773
774				removeLink.style.pointerEvents = 'none';
775				removeLink.style.opacity = '0.4';
776
777				var fd = new FormData();
778				fd.append('action', 'sn_cart_remove_item');
779				fd.append('nonce', window.snCart.nonce);
780				fd.append('key', key);
781
782				fetch(window.snCart.ajaxUrl, { method: 'POST', body: fd })
783					.then(function (r) { return r.json(); })
784					.then(function (data) {
785						if (!data.success) {
786							removeLink.style.pointerEvents = '';
787							removeLink.style.opacity = '';
788							return;
789						}
790						// Rimuove la riga con animazione
791						if (row) {
792							row.style.transition = 'opacity .25s, transform .25s';
793							row.style.opacity = '0';
794							row.style.transform = 'translateX(-16px)';
795							setTimeout(function () { row.remove(); }, 260);
796						}
797						updateCartSidebar(data.data.cart_totals_html);
798						updateCartBadgePage(data.data.badge_html);
799						// Aggiorna anche il mini-cart drawer
800						var drawerBody = document.getElementById('cart-drawer-body');
801						if (drawerBody && data.data.cart_html) {
802							var tmpD = document.createElement('div');
803							tmpD.innerHTML = data.data.cart_html;
804							if (tmpD.firstElementChild) drawerBody.replaceWith(tmpD.firstElementChild);
805						}
806						var drawerFooter = document.getElementById('cart-drawer-footer');
807						if (drawerFooter && data.data.footer_html) {
808							var tmpF = document.createElement('div');
809							tmpF.innerHTML = data.data.footer_html;
810							if (tmpF.firstElementChild) drawerFooter.replaceWith(tmpF.firstElementChild);
811						}
812					})
813					.catch(function () {
814						removeLink.style.pointerEvents = '';
815						removeLink.style.opacity = '';
816					});
817			});
818
819			// ── Aggiornamento qty AJAX (debounced) ───────────────────────────
820			var qtyTimer = null;
821
822			cartLayout.addEventListener('change', function (e) {
823				var qtyInput = e.target.closest('.sn-cart-table input.qty');
824				if (!qtyInput) return;
825
826				var row = qtyInput.closest('tr[data-cart-item-key]');
827				if (!row) return;
828				var key = row.dataset.cartItemKey;
829				if (!key) return;
830
831				clearTimeout(qtyTimer);
832				var qty = parseInt(qtyInput.value, 10);
833				if (isNaN(qty) || qty < 0) return;
834
835				qtyTimer = setTimeout(function () {
836					qtyInput.disabled = true;
837
838					var fd = new FormData();
839					fd.append('action', 'sn_cart_update_qty');
840					fd.append('nonce', window.snCart.nonce);
841					fd.append('key', key);
842					fd.append('qty', qty);
843
844					fetch(window.snCart.ajaxUrl, { method: 'POST', body: fd })
845						.then(function (r) { return r.json(); })
846						.then(function (data) {
847							qtyInput.disabled = false;
848							if (!data.success) return;
849
850							// Aggiorna subtotale riga
851							if (data.data.item_subtotals && data.data.item_subtotals[key]) {
852								var subtotalCell = row.querySelector('.product-subtotal');
853								if (subtotalCell) subtotalCell.innerHTML = data.data.item_subtotals[key];
854							}
855
856							// Se qty è 0, rimuovi la riga
857							if (qty === 0 && row) {
858								row.style.transition = 'opacity .25s';
859								row.style.opacity = '0';
860								setTimeout(function () { row.remove(); }, 260);
861							}
862
863							updateCartSidebar(data.data.cart_totals_html);
864							updateCartBadgePage(data.data.badge_html);
865
866							// Aggiorna drawer
867							var drawerBody = document.getElementById('cart-drawer-body');
868							if (drawerBody && data.data.cart_html) {
869								var tmpD = document.createElement('div');
870								tmpD.innerHTML = data.data.cart_html;
871								if (tmpD.firstElementChild) drawerBody.replaceWith(tmpD.firstElementChild);
872							}
873							var drawerFooter = document.getElementById('cart-drawer-footer');
874							if (drawerFooter && data.data.footer_html) {
875								var tmpF = document.createElement('div');
876								tmpF.innerHTML = data.data.footer_html;
877								if (tmpF.firstElementChild) drawerFooter.replaceWith(tmpF.firstElementChild);
878							}
879						})
880						.catch(function () { qtyInput.disabled = false; });
881				}, 500);
882			});
883		})();
884
885		/* ── Block Cart page: sync drawer badge/content quando WC Blocks cambia il carrello ─ */
886		(function syncBlockCartWithDrawer() {
887			// Solo su pagina carrello con Block Cart attivo
888			if (!window.wp || !window.wp.data) return;
889			var cartStore = window.wp.data.select('wc/store/cart');
890			if (!cartStore || !cartStore.getCartData) return;
891
892			var prevCount = null;
893			var refreshTimer = null;
894
895			function getCount() {
896				var cartData = window.wp.data.select('wc/store/cart').getCartData();
897				return cartData ? (cartData.itemsCount || 0) : 0;
898			}
899
900			function refreshDrawerFromServer() {
901				if (!window.snCart) return;
902				var fd = new FormData();
903				fd.append('action', 'sn_cart_get_drawer');
904				fd.append('nonce', window.snCart.nonce);
905				fetch(window.snCart.ajaxUrl, { method: 'POST', body: fd })
906					.then(function (r) { return r.json(); })
907					.then(function (data) {
908						if (!data.success) return;
909						var badge = document.querySelector('.cart-count');
910						if (badge && data.data.badge_html) {
911							var tmp = document.createElement('div');
912							tmp.innerHTML = data.data.badge_html;
913							if (tmp.firstElementChild) badge.replaceWith(tmp.firstElementChild);
914						}
915						var drawerBody = document.getElementById('cart-drawer-body');
916						if (drawerBody && data.data.cart_html) {
917							var tmpD = document.createElement('div');
918							tmpD.innerHTML = data.data.cart_html;
919							if (tmpD.firstElementChild) drawerBody.replaceWith(tmpD.firstElementChild);
920						}
921						var drawerFooter = document.getElementById('cart-drawer-footer');
922						if (drawerFooter && data.data.footer_html) {
923							var tmpF = document.createElement('div');
924							tmpF.innerHTML = data.data.footer_html;
925							if (tmpF.firstElementChild) drawerFooter.replaceWith(tmpF.firstElementChild);
926						}
927					});
928			}
929
930			window.wp.data.subscribe(function () {
931				var count = getCount();
932				if (prevCount !== null && prevCount !== count) {
933					clearTimeout(refreshTimer);
934					refreshTimer = setTimeout(refreshDrawerFromServer, 600);
935				}
936				prevCount = count;
937			});
938		})();
939
940		/* ── Best-selling highlight: sync qty input → data-quantity ATC button ─ */
941		document.querySelectorAll('.best-selling-hl-qty').forEach(function (input) {
942			var actions = input.closest('.best-selling-highlight__actions');
943			if (!actions) return;
944			function syncQty() {
945				var btn = actions.querySelector('.ajax_add_to_cart');
946				if (btn) btn.setAttribute('data-quantity', Math.max(1, parseInt(input.value, 10) || 1));
947			}
948			input.addEventListener('change', syncQty);
949			input.addEventListener('input', syncQty);
950		});
951
952		/* ── Shop view-toggle (grid / row) ──────────────────────────────────── */
953		var shopMain = document.getElementById('shop-main');
954		var viewBtns = document.querySelectorAll('.sn-view-btn');
955
956		if (shopMain && viewBtns.length) {
957			var STORAGE_KEY = 'sn_shop_view';
958
959			function applyView(view) {
960				// Classe sul contenitore per CSS
961				shopMain.classList.toggle('view-row', view === 'row');
962				shopMain.classList.toggle('view-grid', view === 'grid');
963
964				// Stato aria-pressed sui bottoni
965				viewBtns.forEach(function (btn) {
966					var active = btn.dataset.view === view;
967					btn.setAttribute('aria-pressed', String(active));
968					btn.classList.toggle('is-active', active);
969				});
970
971				// Persiste scelta (localStorage + cookie per PHP)
972				try { localStorage.setItem(STORAGE_KEY, view); } catch (e) {}
973				document.cookie = 'sn_shop_view=' + view + '; path=/; max-age=' + (60 * 60 * 24 * 365) + '; SameSite=Lax';
974			}
975
976			// Legge preferenza salvata (default: grid)
977			var savedView = 'grid';
978			try { savedView = localStorage.getItem(STORAGE_KEY) || 'grid'; } catch (e) {}
979			if (savedView !== 'grid' && savedView !== 'row') savedView = 'grid';
980
981			applyView(savedView);
982
983			viewBtns.forEach(function (btn) {
984				btn.addEventListener('click', function () {
985					applyView(btn.dataset.view);
986				});
987			});
988		}
989
990		/* ── Sidebar filter: mostra/nascondi item extra ───────────────────── */
991		var VISIBLE_LIMIT = 6;
992
993		function initSidebarToggles() {
994			document.querySelectorAll('.shop-sidebar .woocommerce-widget-layered-nav-list').forEach(function (ul) {
995				// Evita doppia inizializzazione dopo fragment refresh
996				if (ul.dataset.snToggleInit) return;
997				ul.dataset.snToggleInit = '1';
998
999				var items = Array.from(ul.querySelectorAll(':scope > .woocommerce-widget-layered-nav-list__item'));
1000				if (items.length <= VISIBLE_LIMIT) return;
1001
1002				var hidden = items.slice(VISIBLE_LIMIT);
1003				var extra  = hidden.length;
1004
1005				hidden.forEach(function (li) { li.classList.add('sn-filter-hidden'); });
1006
1007				var btn = document.createElement('button');
1008				btn.type = 'button';
1009				btn.className = 'sn-filter-toggle';
1010				btn.setAttribute('aria-expanded', 'false');
1011				btn.textContent = '+ ' + extra + ' altri';
1012				ul.after(btn);
1013
1014				btn.addEventListener('click', function () {
1015					var expanded = btn.getAttribute('aria-expanded') === 'true';
1016
1017					hidden.forEach(function (li) {
1018						li.classList.toggle('sn-filter-hidden', expanded);
1019					});
1020
1021					btn.setAttribute('aria-expanded', String(!expanded));
1022					btn.textContent = expanded
1023						? '+ ' + extra + ' altri'
1024						: '− Mostra meno';
1025
1026					// Porta il primo item appena rivelato in vista
1027					if (!expanded && hidden[0]) {
1028						setTimeout(function () {
1029							hidden[0].scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1030						}, 16);
1031					}
1032				});
1033			});
1034		}
1035
1036		initSidebarToggles();
1037
1038		// Re-inizializza dopo fragment refresh WooCommerce (filtri AJAX)
1039		document.body.addEventListener('wc_fragments_refreshed', initSidebarToggles);
1040		document.body.addEventListener('wc_fragment_refresh', initSidebarToggles);
1041
1042		/* ── Product variation swatches (sostituisce select con pulsanti) ──── */
1043		(function initVariationSwatches() {
1044			var form = document.querySelector('.variations_form');
1045			if (!form) return;
1046			var selects = Array.from(form.querySelectorAll('table.variations select'));
1047			if (!selects.length) return;
1048
1049			// Legge i dati delle variazioni per determinare la disponibilità reale
1050			var variations = [];
1051			try {
1052				var raw = form.dataset.product_variations;
1053				if (raw && raw !== 'false') variations = JSON.parse(raw);
1054			} catch (e) {}
1055
1056			// Per ogni attributo: set di valori con almeno una variazione in-stock
1057			// '*' = variazione "qualsiasi valore" (attribute value vuoto in WC)
1058			var stockByAttr = {};
1059			variations.forEach(function (v) {
1060				if (!v.is_in_stock) return;
1061				Object.keys(v.attributes).forEach(function (attr) {
1062					if (!stockByAttr[attr]) stockByAttr[attr] = {};
1063					var val = v.attributes[attr];
1064					stockByAttr[attr][val === '' ? '*' : val] = true;
1065				});
1066			});
1067
1068			function isValueInStock(select, val) {
1069				if (!variations.length) return true;
1070				var inStock = stockByAttr[select.name];
1071				if (!inStock) return true;
1072				return inStock['*'] || inStock[val] || false;
1073			}
1074
1075			// Costruisce i pulsanti swatch per ogni select
1076			selects.forEach(function (select) {
1077				var td = select.closest('td.value');
1078				if (!td) return;
1079
1080				var container = document.createElement('div');
1081				container.className = 'sn-swatches';
1082
1083				Array.from(select.options).forEach(function (opt) {
1084					if (!opt.value) return;
1085					var btn = document.createElement('button');
1086					btn.type = 'button';
1087					btn.className = 'sn-swatch';
1088					btn.textContent = opt.text;
1089					btn.dataset.value = opt.value;
1090					if (!isValueInStock(select, opt.value)) {
1091						btn.classList.add('is-unavailable');
1092					}
1093					container.appendChild(btn);
1094				});
1095
1096				// Nasconde il select nativo, inserisce i pulsanti
1097				select.style.display = 'none';
1098				td.insertBefore(container, select);
1099
1100				// Click: attiva/deattiva il valore
1101				container.addEventListener('click', function (e) {
1102					var btn = e.target.closest('.sn-swatch');
1103					if (!btn) return;
1104					select.value = (select.value === btn.dataset.value) ? '' : btn.dataset.value;
1105					if (typeof jQuery !== 'undefined') {
1106						jQuery(select).trigger('change');
1107					} else {
1108						select.dispatchEvent(new Event('change', { bubbles: true }));
1109					}
1110				});
1111			});
1112
1113			// Aggiorna stato visivo (attivo / non disponibile)
1114			function syncSwatches() {
1115				selects.forEach(function (select) {
1116					var td = select.closest('td.value');
1117					if (!td) return;
1118					var container = td.querySelector('.sn-swatches');
1119					if (!container) return;
1120					container.querySelectorAll('.sn-swatch').forEach(function (btn) {
1121						var opt = Array.from(select.options).find(function (o) { return o.value === btn.dataset.value; });
1122						btn.classList.toggle('is-active', select.value === btn.dataset.value);
1123						// Non disponibile: WC ha disabilitato l'option (multi-attr) O nessuna variazione in-stock
1124						var wcDisabled = opt ? (opt.disabled || opt.classList.contains('disabled')) : false;
1125						btn.classList.toggle('is-unavailable', wcDisabled || !isValueInStock(select, btn.dataset.value));
1126					});
1127				});
1128			}
1129
1130			// Ascolta eventi WooCommerce (nativi)
1131			['woocommerce_variation_select_change', 'found_variation', 'reset_data',
1132				'woocommerce_update_variation_values', 'hide_variation'].forEach(function (evt) {
1133				form.addEventListener(evt, syncSwatches);
1134			});
1135			// Ascolta eventi WooCommerce via jQuery se disponibile
1136			if (typeof jQuery !== 'undefined') {
1137				jQuery(form).on('found_variation reset_data hide_variation woocommerce_update_variation_values', syncSwatches);
1138			}
1139
1140			// MutationObserver sulle option: WooCommerce aggiunge/rimuove disabled
1141			selects.forEach(function (select) {
1142				var obs = new MutationObserver(syncSwatches);
1143				Array.from(select.options).forEach(function (opt) {
1144					if (opt.value) obs.observe(opt, { attributes: true, attributeFilter: ['disabled', 'class'] });
1145				});
1146			});
1147
1148			// Auto-seleziona il primo valore DISPONIBILE (non il primo in assoluto)
1149			function autoSelectFirst() {
1150				selects.forEach(function (select) {
1151					if (select.value) return;
1152					// Preferisce prima opzione in-stock; fallback: prima non disabilitata da WC
1153					var first = Array.from(select.options).find(function (o) {
1154						return o.value && !o.disabled && isValueInStock(select, o.value);
1155					}) || Array.from(select.options).find(function (o) { return o.value && !o.disabled; });
1156					if (!first) return;
1157					select.value = first.value;
1158					if (typeof jQuery !== 'undefined') {
1159						jQuery(select).trigger('change');
1160					} else {
1161						select.dispatchEvent(new Event('change', { bubbles: true }));
1162					}
1163				});
1164				syncSwatches();
1165			}
1166			// Attende che WooCommerce completi l'inizializzazione del form
1167			setTimeout(autoSelectFirst, 80);
1168		})();
1169
1170		/* ── Single product gallery fallback ───────────────────────────────── */
1171		(function initSingleProductGalleryFallback() {
1172			function mount() {
1173				var gallery = document.querySelector('.single-product .woocommerce-product-gallery');
1174				if (!gallery || gallery.dataset.snGalleryMounted === '1') return false;
1175
1176				var viewport = gallery.querySelector('.flex-viewport');
1177				var wrapper = gallery.querySelector('.woocommerce-product-gallery__wrapper');
1178				var slides = Array.from(gallery.querySelectorAll('.woocommerce-product-gallery__wrapper .woocommerce-product-gallery__image'));
1179				var thumbs = Array.from(gallery.querySelectorAll('.flex-control-thumbs li'));
1180
1181				if (!viewport || !wrapper || slides.length < 2 || thumbs.length < 2) return false;
1182
1183				gallery.dataset.snGalleryMounted = '1';
1184
1185				var currentIndex = Math.max(0, slides.findIndex(function (slide) {
1186					return slide.classList.contains('flex-active-slide');
1187				}));
1188
1189				function sync(index) {
1190					currentIndex = Math.max(0, Math.min(index, slides.length - 1));
1191					var width = viewport.clientWidth || slides[0].clientWidth || 0;
1192
1193					if (width > 0) {
1194						wrapper.style.transform = 'translate3d(' + (-currentIndex * width) + 'px, 0, 0)';
1195						wrapper.style.transitionDuration = '0.35s';
1196					}
1197
1198					slides.forEach(function (slide, idx) {
1199						slide.classList.toggle('flex-active-slide', idx === currentIndex);
1200					});
1201
1202					thumbs.forEach(function (thumb, idx) {
1203						thumb.classList.toggle('flex-active', idx === currentIndex);
1204					});
1205				}
1206
1207				thumbs.forEach(function (thumb, idx) {
1208					thumb.addEventListener('click', function (e) {
1209						e.preventDefault();
1210						e.stopPropagation();
1211						sync(idx);
1212					});
1213				});
1214
1215				window.addEventListener('resize', function () {
1216					sync(currentIndex);
1217				});
1218
1219				sync(currentIndex);
1220				return true;
1221			}
1222
1223			if (mount()) return;
1224
1225			var attempts = 0;
1226			var timer = setInterval(function () {
1227				attempts += 1;
1228				if (mount() || attempts > 20) {
1229					clearInterval(timer);
1230				}
1231			}, 150);
1232		})();
1233
1234		/* ── Product page: AJAX add-to-cart ────────────────────────────────── */
1235		// Intercetta il submit di form.cart sulla pagina prodotto e lo converte in
1236		// una chiamata AJAX a ?wc-ajax=add_to_cart. Funziona sia per il pulsante
1237		// standard "Aggiungi al carrello" sia per il pulsante FBT "Aggiungi tutti
1238		// e tre al carrello" (che chiama wooAddButton.click() → triggera submit).
1239		(function initSingleProductAjaxAtc() {
1240			var singleForm = document.querySelector('form.cart');
1241			// Presente solo sulla pagina prodotto; non interferiamo con la pagina carrello
1242			if (!singleForm) return;
1243
1244			singleForm.addEventListener('submit', function (e) {
1245				var submitBtn = singleForm.querySelector('button.single_add_to_cart_button');
1246				if (!submitBtn || submitBtn.classList.contains('loading')) return;
1247
1248				// Determina il product_id per l'endpoint wc-ajax:
1249				// - variabile: variation_id aggiornato da wc-add-to-cart-variation.js
1250				// - semplice:  valore del pulsante add-to-cart
1251				var variationInput = singleForm.querySelector('input[name="variation_id"]');
1252				var variationId    = variationInput ? parseInt(variationInput.value, 10) : 0;
1253				var addToCartEl    = singleForm.querySelector('[name="add-to-cart"]');
1254				var productId      = variationId > 0
1255				                   ? variationId
1256				                   : (addToCartEl ? parseInt(addToCartEl.value, 10) : 0);
1257
1258				if (!productId) return; // id non valido: lascia il submit nativo
1259
1260				e.preventDefault();
1261
1262				var fd = new FormData(singleForm);
1263				fd.set('product_id', productId);
1264
1265				submitBtn.classList.add('loading');
1266				submitBtn.setAttribute('disabled', '');
1267
1268				fetch('/?wc-ajax=add_to_cart', { method: 'POST', body: fd })
1269					.then(function (r) { return r.json(); })
1270					.then(function (data) {
1271						submitBtn.classList.remove('loading');
1272						submitBtn.removeAttribute('disabled');
1273
1274						if (data.error) return;
1275
1276						// Applica i fragment WooCommerce al DOM
1277						if (data.fragments) {
1278							Object.keys(data.fragments).forEach(function (selector) {
1279								var target = document.querySelector(selector);
1280								if (!target) return;
1281								var tmp = document.createElement('div');
1282								tmp.innerHTML = data.fragments[selector];
1283								if (tmp.firstElementChild) target.replaceWith(tmp.firstElementChild);
1284							});
1285						}
1286
1287						// Pulisce gli input FBT temporanei (se il submit veniva dal btn FBT)
1288						singleForm.querySelectorAll('input[data-sn-fbt-hidden="1"]').forEach(function (i) { i.remove(); });
1289
1290						// Apre il drawer e anima l'icona carrello (identico al comportamento
1291						// dei pulsanti ATC sullo shop/homepage)
1292						document.body.dispatchEvent(new CustomEvent('added_to_cart', { bubbles: false }));
1293					})
1294					.catch(function () {
1295						submitBtn.classList.remove('loading');
1296						submitBtn.removeAttribute('disabled');
1297					});
1298			});
1299		})();
1300
1301		/* ── Cart: auto-aggiornamento quantità ───────────────────────────── */
1302		var cartForm = document.querySelector('.woocommerce-cart-form');
1303		if (cartForm) {
1304			var checkoutBtn = document.querySelector('.wc-proceed-to-checkout .checkout-button');
1305			var updateBtn   = cartForm.querySelector('[name="update_cart"]');
1306			var qtyTimer    = null;
1307
1308			cartForm.addEventListener('change', function (e) {
1309				if (!e.target.matches('.qty')) return;
1310
1311				// Blocca visivamente il pulsante "Procedi con l'ordine"
1312				if (checkoutBtn) checkoutBtn.classList.add('sn-cart-updating');
1313
1314				clearTimeout(qtyTimer);
1315				qtyTimer = setTimeout(function () {
1316					// WooCommerce abilita il bottone via jQuery sul change; forziamo comunque
1317					if (updateBtn) {
1318						updateBtn.disabled = false;
1319						updateBtn.click();
1320					}
1321				}, 700);
1322			});
1323		}
1324
1325		/* ── FAQ accordion: una sola voce aperta alla volta ─────────────────── */
1326		document.querySelectorAll('.faq-content').forEach(function (container) {
1327			container.addEventListener('toggle', function (e) {
1328				if (!e.target.open) return;
1329				container.querySelectorAll('details.faq-item').forEach(function (d) {
1330					if (d !== e.target) d.removeAttribute('open');
1331				});
1332			}, true);
1333		});
1334
1335	}); // end DOMContentLoaded
1336})();
1337

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.