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.