1/** 2 * Maison Montagut â Quick-Shop + Luxury Drawer v1.4.6 3 * - Carousel + dual buttons + colorâproduct link (PDP overlay) [TOUJOURS ACTIF] 4 * - Quick add / Quick buy + Toast [TOUJOURS ACTIF] 5 * - Site-wide cart drawer (stepper luxe, hover-to-open, etc.) [GATED MMQS_SIDEBAR_ON] 6 * 7 * v1.4.6 â Sidebar panier latéral désactivée par défaut : 8 * - BUG CLIENT : le drawer apparaissait à tort, couvrant le bouton Acheter 9 * sur la PDP, et s'ouvrait au hover sur l'icône panier de la home en 10 * masquant la barre d'icônes du header. 11 * - FIX : window.mmquickshop_sidebar_enabled (int 0/1) définit MMQS_SIDEBAR_ON. 12 * Tous les blocs liés au drawer (rendu, hover-to-open, MutationObserver 13 * killNativeDropdowns, listener add-to-cart natif optimiste, listeners 14 * PS updateCart, API publique window.MMQS) sont wrappés par 15 * `if (MMQS_SIDEBAR_ON) { ... }`. Quand off : le code drawer ne s'exécute 16 * PAS du tout, le panier natif PrestaShop reprend ses droits. 17 * - PRÃSERVà INTACT : carousel, size/color/fav, quickadd/quickbuy, toast. 18 * - API publique window.MMQS reste exposée mais avec des stubs no-op qui 19 * redirigent vers /panier (pour ne pas casser mmcategoryshop & co qui 20 * pourraient l'appeler). 21 * 22 * v1.4.5 â HOTFIX conflit "Voir la dispo en magasin". 23 * NATIVE_ADD_SELECTORS resserré (+ filtre popups + libellés). 24 * [Conservé en v1.4.6 â bloc n'est plus activé que sidebar ON] 25 * 26 * v1.4.4 â HOTFIX dernière pièce + hover collé. [Conservé] 27 * v1.4.3 â Drawer épuré. [Conservé] 28 * v1.4.2 â HOTFIX install. [Conservé] 29 * v1.4.1 â Zéro-flash. [Conservé] 30 * v1.4.0 â Copy livraison point relais. [Conservé] 31 * v1.3.9 â UX OPEN OPTIMISTIC sur PDP native. [Conservé] 32 * v1.3.8 â HOTFIX conteneur icône `.cart-preview`. [Conservé] 33 * v1.3.6 â fix copy Club Privilège + lien CMS 130. [Conservé] 34 * v1.3.5 â HOTFIX icône panier disparue. [Conservé] 35 * v1.3.4 â snappy + zero-flash + cross-browser. [Conservé] 36 * v1.3.3 â fix rendu items + entités. [Conservé] 37 * v1.3.2 â multi-hook render + per-shop config. [Conservé] 38 * v1.3.1 â drawer en <template> lazy-cloné. [Conservé] 39 */ 40(function () { 41 'use strict'; 42 43 // v1.4.6 â Flag sidebar : tout le code drawer est gated par cette var. 44 // Quand 0 (défaut depuis v1.4.6) â le panier natif PrestaShop reprend la main. 45 var MMQS_SIDEBAR_ON = !!parseInt(window.mmquickshop_sidebar_enabled || 0, 10); 46 47 // v1.4.6 â log d'init immédiat pour diagnostic. 48 try { 49 console.log( 50 '%c[MMQS] front.js v1.4.6 loaded (sidebar=' + (MMQS_SIDEBAR_ON ? 'ON' : 'OFF') + ')', 51 'color:#a88048;font-weight:bold' 52 ); 53 } catch(e) {} 54 55 function $(sel, root) { return (root || document).querySelector(sel); } 56 function $$(sel, root) { return Array.prototype.slice.call((root || document).querySelectorAll(sel)); } 57 58 function escHtml(s) { 59 if (s === null || typeof s === 'undefined') return ''; 60 var div = document.createElement('div'); 61 div.appendChild(document.createTextNode(String(s))); 62 return div.innerHTML; 63 } 64 65 /* ââ Toast ââ (TOUJOURS ACTIF) */ 66 function showToast(msg) { 67 var t = $('#mmqs-toast'); 68 if (!t) return; 69 var m = $('.mmqs-toast-msg', t); 70 if (m && msg) m.textContent = msg; 71 t.classList.add('mmqs-show'); 72 setTimeout(function () { t.classList.remove('mmqs-show'); }, 1800); 73 } 74 75 /* ââ Combo map resolver ââ */ 76 function resolveIPA(card, sizeAttr) { 77 var colorBtn = $('.mmqs-color.mmqs-active', card); 78 var colorAttr = colorBtn ? parseInt(colorBtn.getAttribute('data-id-attribute'), 10) || 0 : 0; 79 var mapStr = card.getAttribute('data-combo-map'); 80 if (mapStr) { 81 try { 82 var map = JSON.parse(mapStr); 83 var key = sizeAttr + '-' + colorAttr; 84 if (map[key]) return map[key]; 85 key = sizeAttr + '-0'; 86 if (map[key]) return map[key]; 87 } catch (e) { /* ignore */ } 88 } 89 return 0; 90 } 91 92 /* âââââââââââââââââââââââââââââââââââââââââââ 93 CAROUSEL (TOUJOURS ACTIF) 94 âââââââââââââââââââââââââââââââââââââââââââ */ 95 function carouselGoTo(carousel, index) { 96 var slides = $$('.mmqs-slide', carousel); 97 var dots = $$('.mmqs-dot', carousel); 98 if (!slides.length) return; 99 index = ((index % slides.length) + slides.length) % slides.length;
100 slides.forEach(function (s, i) { s.classList.toggle('mmqs-slide-active', i === index); }); 101 dots.forEach(function (d, i) { d.classList.toggle('mmqs-dot-active', i === index); }); 102 carousel._currentSlide = index; 103 } 104 function carouselGetIndex(carousel) { return carousel._currentSlide || 0; } 105 106 function carouselSetImages(carousel, urls, productLink, productName) { 107 var track = $('.mmqs-carousel-track', carousel); 108 var dotsContainer = $('.mmqs-dots', carousel); 109 if (!track) return; 110 var html = ''; 111 for (var i = 0; i < urls.length; i++) { 112 html += '<a href="' + escHtml(productLink) + '" class="mmqs-slide' + (i === 0 ? ' mmqs-slide-active' : '') + '" aria-label="' + escHtml(productName) + '">'; 113 html += '<img src="' + escHtml(urls[i]) + '" alt="' + escHtml(productName) + '" loading="lazy" draggable="false">'; 114 html += '</a>'; 115 } 116 track.innerHTML = html; 117 if (dotsContainer) { 118 var dotsHtml = ''; 119 for (var j = 0; j < urls.length; j++) { 120 dotsHtml += '<span class="mmqs-dot' + (j === 0 ? ' mmqs-dot-active' : '') + '"></span>'; 121 } 122 dotsContainer.innerHTML = dotsHtml; 123 dotsContainer.style.display = urls.length > 1 ? '' : 'none'; 124 } 125 var arrows = $$('.mmqs-arrow', carousel); 126 arrows.forEach(function (a) { a.style.display = urls.length > 1 ? '' : 'none'; }); 127 carousel._currentSlide = 0; 128 } 129 130 /* ââ Color â product link ââ */ 131 function buildColorLink(card, colorAttr) { 132 var baseLink = card.getAttribute('data-base-link') || ''; 133 if (!baseLink) { 134 var nameEl = card.querySelector('.mmqs-name'); 135 baseLink = nameEl ? nameEl.href : '#'; 136 } 137 if (!colorAttr) return baseLink; 138 var linksStr = card.getAttribute('data-color-links'); 139 if (linksStr) { 140 try { 141 var linksMap = JSON.parse(linksStr); 142 if (linksMap[colorAttr]) return linksMap[colorAttr]; 143 } catch (e) { /* ignore */ } 144 } 145 return baseLink; 146 } 147 function updateCardLinks(card, newHref) { 148 var nameLink = card.querySelector('.mmqs-name'); 149 if (nameLink) nameLink.href = newHref; 150 $$('.mmqs-slide', card).forEach(function (slide) { if (slide.tagName === 'A') slide.href = newHref; }); 151 } 152 153 /* Arrows / dots / touch */ 154 document.addEventListener('click', function (e) { 155 var arrow = e.target.closest ? e.target.closest('.mmqs-arrow') : null; 156 if (!arrow) return; 157 e.preventDefault(); e.stopPropagation(); 158 var carousel = arrow.closest('.mmqs-carousel'); 159 if (!carousel) return; 160 var idx = carouselGetIndex(carousel); 161 carouselGoTo(carousel, idx + (arrow.classList.contains('mmqs-arrow-left') ? -1 : 1)); 162 }); 163 document.addEventListener('click', function (e) { 164 var dot = e.target.closest ? e.target.closest('.mmqs-dot') : null; 165 if (!dot) return; 166 var dotsContainer = dot.closest('.mmqs-dots'); 167 var carousel = dot.closest('.mmqs-carousel'); 168 if (!dotsContainer || !carousel) return; 169 var dots = $$('.mmqs-dot', dotsContainer); 170 var idx = dots.indexOf(dot); 171 if (idx >= 0) carouselGoTo(carousel, idx); 172 }); 173 var touchStartX = 0, touchStartY = 0, swiping = false; 174 document.addEventListener('touchstart', function (e) { 175 var carousel = e.target.closest ? e.target.closest('.mmqs-carousel') : null; 176 if (!carousel) return; 177 touchStartX = e.touches[0].clientX; touchStartY = e.touches[0].clientY; swiping = true; 178 }, { passive: true }); 179 document.addEventListener('touchend', function (e) { 180 if (!swiping) return; 181 swiping = false; 182 var carousel = e.target.closest ? e.target.closest('.mmqs-carousel') : null; 183 if (!carousel) return; 184 var dx = e.changedTouches[0].clientX - touchStartX; 185 var dy = e.changedTouches[0].clientY - touchStartY; 186 if (Math.abs(dx) > 40 && Math.abs(dx) > Math.abs(dy)) { 187 var idx = carouselGetIndex(carousel); 188 carouselGoTo(carousel, idx + (dx < 0 ? 1 : -1)); 189 } 190 }, { passive: true }); 191 192 /* Size / Color / Fav (TOUJOURS ACTIF) */ 193 document.addEventListener('click', function (e) { 194 var el = e.target.closest ? e.target.closest('.mmqs-size') : null; 195 if (!el || el.classList.contains('mmqs-sold')) return; 196 var sizesBox = el.closest('.mmqs-sizes');
197 $$('.mmqs-size', sizesBox).forEach(function (s) { s.classList.remove('mmqs-active'); }); 198 el.classList.add('mmqs-active'); 199 }); 200 document.addEventListener('click', function (e) { 201 var el = e.target.closest ? e.target.closest('.mmqs-color') : null; 202 if (!el) return; 203 var box = el.closest('.mmqs-colors'); 204 $$('.mmqs-color', box).forEach(function (c) { c.classList.remove('mmqs-active'); }); 205 el.classList.add('mmqs-active'); 206 var card = el.closest('.mmqs-card'); 207 if (!card) return; 208 var colorAttr = el.getAttribute('data-id-attribute'); 209 var colorLink = buildColorLink(card, colorAttr); 210 updateCardLinks(card, colorLink); 211 var allImagesStr = card.getAttribute('data-color-all-images'); 212 if (allImagesStr && colorAttr) { 213 try { 214 var colorAllImgs = JSON.parse(allImagesStr); 215 if (colorAllImgs[colorAttr] && colorAllImgs[colorAttr].length) { 216 var carousel = $('.mmqs-carousel', card); 217 var productName = card.querySelector('.mmqs-name') ? card.querySelector('.mmqs-name').textContent : ''; 218 if (carousel) carouselSetImages(carousel, colorAllImgs[colorAttr], colorLink, productName); 219 } 220 } catch (err) { /* ignore */ } 221 } 222 }); 223 document.addEventListener('click', function (e) { 224 var el = e.target.closest ? e.target.closest('.mmqs-fav') : null; 225 if (!el) return; 226 el.classList.toggle('mmqs-fav-active'); 227 }); 228 229 /* âââââââââââââââââââââââââââââââââââââââââââ 230 CART SIDEBAR â Luxury (GATED depuis v1.4.6) 231 âââââââââââââââââââââââââââââââââââââââââââ */ 232 var sidebar = null; 233 var backdrop = null; 234 var emptyEl = null; 235 var bodyEl = null; 236 var hoverTimer = null; 237 var cartHasLoaded = false; 238 var domInjected = false; 239 var optimisticTimer = null; 240 var skeletonShowing = false; 241 242 /** 243 * Lazy inject du drawer depuis le <template>. 244 */ 245 function ensureSidebarInDOM() { 246 if (domInjected) return true; 247 if (document.getElementById('mmqs-sidebar')) { domInjected = true; return true; } 248 var tpl = document.getElementById('mmqs-sidebar-template'); 249 if (!tpl || !tpl.content) return false; 250 try { 251 document.body.appendChild(tpl.content.cloneNode(true)); 252 domInjected = true; 253 return true; 254 } catch (err) { 255 return false; 256 } 257 } 258 259 function refs() { 260 ensureSidebarInDOM(); 261 if (!sidebar) sidebar = $('#mmqs-sidebar'); 262 if (!backdrop) backdrop = $('#mmqs-sidebar-backdrop'); 263 if (!bodyEl) bodyEl = $('#mmqs-sidebar-body'); 264 if (!emptyEl) emptyEl = $('#mmqs-cart-empty'); 265 } 266 267 /* Skeleton optimiste */ 268 var SKELETON_HTML = '<div class="mmqs-cart-skeleton" aria-hidden="true">' 269 + '<div class="mmqs-skel-item"><div class="mmqs-skel-thumb"></div><div class="mmqs-skel-lines"><div class="mmqs-skel-line mmqs-skel-line-1"></div><div class="mmqs-skel-line mmqs-skel-line-2"></div><div class="mmqs-skel-line mmqs-skel-line-3"></div></div></div>' 270 + '<div class="mmqs-skel-item"><div class="mmqs-skel-thumb"></div><div class="mmqs-skel-lines"><div class="mmqs-skel-line mmqs-skel-line-1"></div><div class="mmqs-skel-line mmqs-skel-line-2"></div><div class="mmqs-skel-line mmqs-skel-line-3"></div></div></div>' 271 + '</div>'; 272 273 function showSkeletonInDrawer() { 274 refs(); 275 if (!bodyEl) return; 276 bodyEl.innerHTML = SKELETON_HTML; 277 skeletonShowing = true; 278 } 279 280 function clearOptimisticWatchdog() { 281 if (optimisticTimer) { clearTimeout(optimisticTimer); optimisticTimer = null; } 282 } 283 284 function startOptimisticWatchdog() { 285 clearOptimisticWatchdog(); 286 optimisticTimer = setTimeout(function () { 287 if (skeletonShowing) { 288 skeletonShowing = false; 289 closeSidebar(); 290 } 291 optimisticTimer = null; 292 }, 3000); 293 } 294 295 function openSidebar() { 296 refs(); 297 if (!sidebar) return; 298 sidebar.classList.add('mmqs-open'); 299 sidebar.setAttribute('aria-hidden', 'false'); 300 if (backdrop) { backdrop.classList.add('mmqs-open'); backdrop.setAttribute('aria-hidden', 'false'); } 301 document.body.classList.add('mmqs-drawer-open'); 302 if (!skeletonShowing) fetchCart(); 303 } 304 function closeSidebar() { 305 refs(); 306 if (sidebar) { sidebar.classList.remove('mmqs-open'); sidebar.setAttribute('aria-hidden', 'true'); } 307 if (backdrop) { backdrop.classList.remove('mmqs-open'); backdrop.setAttribute('aria-hidden', 'true'); } 308 document.body.classList.remove('mmqs-drawer-open'); 309 } 310 311 function formatMoney(amount) { 312 try { 313 var sign = window.mmquickshop_currency_sign || 'â¬'; 314 var n = Number(amount) || 0; 315 var s = n.toFixed(2).replace('.', ','); 316 return s + ' ' + sign; 317 } catch (e) { 318 return amount + ''; 319 } 320 } 321 322 function updateProgressBar(totalRaw) { 323 var threshold = parseFloat(window.mmquickshop_free_ship_threshold || 0); 324 var wrap = $('#mmqs-progress'); 325 var fill = $('#mmqs-progress-fill'); 326 var label = $('#mmqs-progress-label'); 327 if (!wrap || !fill || threshold <= 0) return; 328 var total = parseFloat(totalRaw) || 0; 329 var pct = Math.min(100, Math.max(0, (total / threshold) * 100)); 330 fill.style.width = pct + '%'; 331 var t = window.mmquickshop_t || {}; 332 if (total >= threshold) { 333 wrap.classList.add('mmqs-reached'); 334 if (label) label.innerHTML = (t.progress_reached || 'ð Livraison offerte !'); 335 } else { 336 wrap.classList.remove('mmqs-reached'); 337 var remain = Math.max(0, threshold - total); 338 if (label) { 339 var tmpl = t.progress_left || 'Plus que %s pour la livraison offerte'; 340 label.innerHTML = tmpl.replace('%s', '<strong>' + formatMoney(remain) + '</strong>'); 341 } 342 } 343 } 344 345 function updateCount(n) { 346 var el = $('#mmqs-sidebar-count'); 347 if (!el) return; 348 var t = window.mmquickshop_t || {}; 349 if (n > 0) { 350 el.textContent = ' · ' + n + ' ' + (n > 1 ? (t.articles || 'articles') : (t.article || 'article')); 351 } else { 352 el.textContent = ''; 353 } 354 } 355
356 function renderSidebarCart(cartData) { 357 refs(); 358 if (!bodyEl) return; 359 skeletonShowing = false; 360 clearOptimisticWatchdog(); 361 var t = window.mmquickshop_t || {}; 362 var total = $('#mmqs-sidebar-total'); 363 var checkoutBtn = $('#mmqs-sidebar-checkout'); 364 365 if (!cartData || !cartData.items || !cartData.items.length) { 366 bodyEl.innerHTML = '' 367 + '<div class="mmqs-cart-empty" id="mmqs-cart-empty">' 368 + '<div class="mmqs-empty-icon" aria-hidden="true">' 369 + '<svg viewBox="0 0 40 40" width="38" height="38" fill="none" stroke="currentColor" stroke-width="1.2">' 370 + '<path d="M8 12h24l-2.2 18a2 2 0 0 1-2 1.8H12.2a2 2 0 0 1-2-1.8L8 12z"/>' 371 + '<path d="M14 12V9a6 6 0 0 1 12 0v3"/>' 372 + '</svg>' 373 + '</div>' 374 + '<div class="mmqs-empty-title">' + escHtml(t.sidebar_empty || 'Votre panier est vide') + '</div>' 375 + '<div class="mmqs-empty-sub">' + escHtml(t.sidebar_empty_sub || 'Découvrez nos créations et laissez-vous séduire.') + '</div>' 376 + '</div>'; 377 emptyEl = $('#mmqs-cart-empty'); 378 if (total) total.textContent = formatMoney(0); 379 updateCount(0); 380 updateProgressBar(0); 381 return; 382 } 383 384 var html = '<ul class="mmqs-cart-list">'; 385 for (var i = 0; i < cartData.items.length; i++) { 386 var it = cartData.items[i]; 387 var qtyNum = parseInt(it.qty, 10) || 1; 388 var isLast = !!it.is_last_piece; 389 var plusAttrs = isLast 390 ? ' disabled aria-disabled="true"' 391 : ''; 392 var plusExtraClass = isLast ? ' mmqs-qty-disabled' : ''; 393 var itemClass = 'mmqs-cart-item' + (isLast ? ' mmqs-item-last-piece' : ''); 394 395 html += '<li class="' + itemClass + '" ' 396 + 'data-id-product="' + (parseInt(it.id_product, 10) || 0) + '" ' 397 + 'data-id-product-attribute="' + (parseInt(it.id_product_attribute, 10) || 0) + '">'; 398 399 html += '<div class="mmqs-cart-thumb">'; 400 if (it.image_url) { 401 html += '<img class="mmqs-cart-img" src="' + escHtml(it.image_url) + '" alt="' + escHtml(it.name) + '" loading="lazy">'; 402 } 403 html += '</div>'; 404 405 html += '<div class="mmqs-cart-info">'; 406 html += '<div class="mmqs-cart-name">' + escHtml(it.name) + '</div>'; 407 if (it.variant) html += '<div class="mmqs-cart-variant">' + escHtml(it.variant) + '</div>'; 408 html += '<div class="mmqs-cart-qty-row">'; 409 html += '<div class="mmqs-qty-pills">'; 410 html += '<button type="button" class="mmqs-qty-btn mmqs-qty-minus" aria-label="-">â</button>'; 411 html += '<span class="mmqs-qty-val">' + qtyNum + '</span>'; 412 html += '<button type="button" class="mmqs-qty-btn mmqs-qty-plus' + plusExtraClass + '"' + plusAttrs + ' aria-label="+">+</button>'; 413 html += '</div>'; 414 html += '<button type="button" class="mmqs-cart-remove" aria-label="' + escHtml(t.remove || t.remove_label || 'Retirer') + '">' + escHtml(t.remove || t.remove_label || 'Retirer') + '</button>'; 415 html += '</div>'; 416 if (isLast) { 417 html += '<div class="mmqs-last-piece-hint" role="status">' + escHtml(t.last_piece || 'Dernière pièce en stock') + '</div>'; 418 } 419 html += '</div>'; 420 421 html += '<div class="mmqs-cart-price-line">' + (it.price || '') + '</div>'; 422 html += '</li>'; 423 } 424 html += '</ul>'; 425 bodyEl.innerHTML = html; 426 427 if (total) total.textContent = cartData.total || formatMoney(cartData.total_raw || 0); 428 if (checkoutBtn && cartData.checkout_url) checkoutBtn.href = cartData.checkout_url; 429 430 updateCount(parseInt(cartData.nb_products, 10) || 0); 431 updateProgressBar(cartData.total_raw || 0); 432 } 433 434 /* ââ AJAX : charge le panier courant ââ */ 435 function fetchCart(cb) { 436 if (!window.mmquickshop_ajax_url) return; 437 var fd = new FormData(); 438 fd.append('action', 'get_cart'); 439 fd.append('ajax', 1); 440 fetch(window.mmquickshop_ajax_url, { 441 method: 'POST', 442 credentials: 'same-origin', 443 body: fd, 444 headers: { 'X-Requested-With': 'XMLHttpRequest' } 445 }) 446 .then(function (r) { return r.json().catch(function () { return {}; }); }) 447 .then(function (data) { 448 if (data && data.cart_details) renderSidebarCart(data.cart_details); 449 cartHasLoaded = true; 450 if (typeof cb === 'function') cb(data); 451 }) 452 .catch(function () { /* silent */ }); 453 } 454 455 /* ââ AJAX : update qty + remove ââ */ 456 function ajaxCartAction(payload, onDone) { 457 if (!window.mmquickshop_ajax_url) return; 458 var fd = new FormData();
459 Object.keys(payload).forEach(function (k) { fd.append(k, payload[k]); }); 460 fd.append('ajax', 1); 461 fetch(window.mmquickshop_ajax_url, { 462 method: 'POST', 463 credentials: 'same-origin', 464 body: fd, 465 headers: { 'X-Requested-With': 'XMLHttpRequest' } 466 }) 467 .then(function (r) { return r.json().catch(function () { return {}; }); }) 468 .then(function (data) { 469 if (data && data.cart_details) renderSidebarCart(data.cart_details); 470 if (data && data.success === false && data.out_of_stock) { 471 showToast(data.message || 'Dernière pièce en stock'); 472 } 473 if (typeof onDone === 'function') onDone(data); 474 try { 475 if (window.prestashop && typeof window.prestashop.emit === 'function') { 476 window.prestashop.emit('updateCart', { reason: {}, resp: data }); 477 } 478 } catch (e) { /* ignore */ } 479 }) 480 .catch(function () { showToast('Erreur réseau â veuillez réessayer'); }); 481 } 482 483 /* âââââââââââââââââââââââââââââââââââââââââââ 484 v1.4.6 â Tous les listeners drawer-related ne sont attachés que si 485 MMQS_SIDEBAR_ON. Quand off, l'utilisateur voit le panier natif PS. 486 âââââââââââââââââââââââââââââââââââââââââââ */ 487 if (MMQS_SIDEBAR_ON) { 488 489 /* Qty +/- click */ 490 document.addEventListener('click', function (e) { 491 var btn = e.target.closest ? e.target.closest('.mmqs-qty-btn') : null; 492 if (!btn) return; 493 494 if (btn.hasAttribute('disabled') || btn.getAttribute('aria-disabled') === 'true' 495 || btn.classList.contains('mmqs-qty-disabled')) { 496 var tNow = (window.mmquickshop_t || {}); 497 showToast(tNow.last_piece || 'Dernière pièce en stock'); 498 try { btn.blur(); } catch(_){ } 499 return; 500 } 501 502 var item = btn.closest('.mmqs-cart-item'); 503 if (!item) return; 504 var idp = parseInt(item.getAttribute('data-id-product'), 10) || 0; 505 var ipa = parseInt(item.getAttribute('data-id-product-attribute'), 10) || 0; 506 if (!idp) return; 507 var op = btn.classList.contains('mmqs-qty-plus') ? 'up' : 'down'; 508 509 try { btn.blur(); } catch(_){ } 510 511 ajaxCartAction({ 512 action: 'update_qty', 513 id_product: idp, 514 id_product_attribute: ipa, 515 op: op, 516 qty: 1 517 }); 518 }); 519 520 /* Remove click */ 521 document.addEventListener('click', function (e) { 522 var btn = e.target.closest ? e.target.closest('.mmqs-cart-remove') : null; 523 if (!btn) return; 524 var item = btn.closest('.mmqs-cart-item'); 525 if (!item) return; 526 var idp = parseInt(item.getAttribute('data-id-product'), 10) || 0; 527 var ipa = parseInt(item.getAttribute('data-id-product-attribute'), 10) || 0; 528 if (!idp) return; 529 ajaxCartAction({ 530 action: 'remove', 531 id_product: idp, 532 id_product_attribute: ipa 533 }); 534 }); 535 536 /* Close buttons / backdrop / ESC */ 537 document.addEventListener('click', function (e) { 538 if (e.target.closest('#mmqs-sidebar-close')) { e.preventDefault(); closeSidebar(); return; } 539 if (e.target.id === 'mmqs-sidebar-backdrop') { closeSidebar(); return; } 540 if (e.target.closest('#mmqs-sidebar-continue')) { e.preventDefault(); closeSidebar(); return; } 541 }); 542 document.addEventListener('keydown', function (e) { 543 if (e.key === 'Escape') closeSidebar(); 544 }); 545 546 /* âââââââââââââââââââââââââââââââââââââââââââ 547 HOVER-TO-OPEN on header cart button 548 âââââââââââââââââââââââââââââââââââââââââââ */ 549 var HOVER_SELECTORS = [ 550 '.blockcart', 551 '#_desktop_cart', 552 '#_mobile_cart', 553 '.header-cart', 554 '.header__cart', 555 '.shopping-cart', 556 'a[href*="/panier"]', 557 'a[href*="controller=cart"]' 558 ].join(','); 559 560 var NATIVE_DROPDOWN_SELECTORS = [ 561 '.cart-dropdown.active', 562 '.cart-dropdown.show', 563 '.cart-dropdown.open', 564 '.blockcart__dropdown.active', 565 '.blockcart__dropdown.show', 566 '.blockcart__dropdown.open', 567 '.header-cart__dropdown.active', 568 '.header-cart__dropdown.show', 569 '.header-cart__dropdown.open' 570 ].join(','); 571 572 function isHoverEnabled() { 573 return typeof window.mmquickshop_hover_open === 'undefined' 574 ? true 575 : !!parseInt(window.mmquickshop_hover_open, 10); 576 } 577 578 function findCartTrigger(target) { 579 if (!target || !target.closest) return null; 580 if (target.closest('#mmqs-sidebar')) return null; 581 return target.closest(HOVER_SELECTORS); 582 } 583 584 function isSafeToKill(el) { 585 if (!el || !el.querySelector) return false;
586 try { if (el.matches && el.matches(HOVER_SELECTORS)) return false; } catch (e) {} 587 if (el.querySelector('a[href*="/panier"], a[href*="/cart"], a[href*="/order"], a[href*="controller=cart"], a[href*="controller=order"]')) { 588 return false; 589 } 590 if (el.querySelector('.cart-products-count, .header-cart__count, .blockcart__count')) { 591 return false; 592 } 593 return true; 594 } 595 596 function killNativeDropdowns() { 597 try { 598 var nodes = document.querySelectorAll(NATIVE_DROPDOWN_SELECTORS); 599 for (var i = 0; i < nodes.length; i++) { 600 var n = nodes[i]; 601 if (!isSafeToKill(n)) continue; 602 n.classList.remove('active', 'show', 'open'); 603 n.style.display = 'none'; 604 n.style.visibility = 'hidden'; 605 n.style.opacity = '0'; 606 n.setAttribute('aria-hidden', 'true'); 607 } 608 } catch (e) { /* ignore */ } 609 } 610 611 document.addEventListener('mouseover', function (e) { 612 if (!isHoverEnabled()) return; 613 var trigger = findCartTrigger(e.target); 614 if (!trigger) return; 615 if (e.relatedTarget && trigger.contains(e.relatedTarget)) return; 616 killNativeDropdowns(); 617 if (hoverTimer) clearTimeout(hoverTimer); 618 hoverTimer = setTimeout(function () { 619 killNativeDropdowns(); 620 openSidebar(); 621 }, 60); 622 }); 623 624 document.addEventListener('mouseout', function (e) { 625 if (!isHoverEnabled()) return; 626 var trigger = findCartTrigger(e.target); 627 if (!trigger) return; 628 if (e.relatedTarget && trigger.contains(e.relatedTarget)) return; 629 if (hoverTimer) { clearTimeout(hoverTimer); hoverTimer = null; } 630 }); 631 632 /* PRE-HOVER INTERCEPTOR (capture phase) */ 633 document.addEventListener('mouseenter', function (e) { 634 if (!isHoverEnabled()) return; 635 if (!e.target || !e.target.closest) return; 636 var trigger = e.target.closest(HOVER_SELECTORS); 637 if (!trigger) return; 638 if (e.target.closest('#mmqs-sidebar')) return; 639 try { killNativeDropdowns(); } catch (_) {} 640 try { e.stopImmediatePropagation(); } catch (_) {} 641 }, true); 642 643 /* sur chaque ouverture du drawer, tuer une dernière fois les natifs */ 644 var _origOpenSidebar = openSidebar; 645 openSidebar = function () { 646 killNativeDropdowns(); 647 _origOpenSidebar(); 648 var killTimer = 0, killCount = 0; 649 killTimer = setInterval(function () { 650 killNativeDropdowns(); 651 if (++killCount > 8) clearInterval(killTimer); 652 }, 100); 653 }; 654 655 /* MutationObserver kill native dropdowns */ 656 try { 657 var mutationObs = new MutationObserver(function (mutations) { 658 for (var i = 0; i < mutations.length; i++) { 659 var m = mutations[i]; 660 if (m.type !== 'attributes') continue; 661 var el = m.target; 662 if (!el || !el.classList) continue; 663 var isDropdown = el.matches &&
664 el.matches('.cart-dropdown, .blockcart__dropdown, .header-cart__dropdown'); 665 if (!isDropdown) continue; 666 if (!isSafeToKill(el)) continue; 667 if (el.classList.contains('active') || el.classList.contains('show') || el.classList.contains('open')) { 668 el.classList.remove('active', 'show', 'open'); 669 el.style.display = 'none'; 670 el.style.visibility = 'hidden'; 671 el.style.opacity = '0'; 672 } 673 } 674 }); 675 mutationObs.observe(document.body, { 676 subtree: true, attributes: true, attributeFilter: ['class'] 677 }); 678 } catch (e) { /* observer non supporté */ } 679 680 } /* end if (MMQS_SIDEBAR_ON) â block 1 */ 681 682 /* âââââââââââââââââââââââââââââââââââââââââââ 683 Quick add / Quick buy (PDP overlay) (TOUJOURS ACTIF) 684 v1.4.6 â Si sidebar OFF : pas de drawer ouvert, on montre juste un toast 685 "Ajouté au panier". Le panier natif PS reflète l'ajout. 686 âââââââââââââââââââââââââââââââââââââââââââ */ 687 document.addEventListener('click', function (e) { 688 var btn = e.target.closest ? e.target.closest('[data-action="quickadd"],[data-action="quickbuy"]') : null; 689 if (!btn) return; 690 691 var action = btn.getAttribute('data-action'); 692 var card = btn.closest('.mmqs-card'); 693 if (!card) return; 694 695 var id_product = parseInt(card.getAttribute('data-id-product'), 10); 696 var activeSize = $('.mmqs-size.mmqs-active', card); 697 var sizesExist = !!$('.mmqs-size', card); 698 699 if (sizesExist && !activeSize) { 700 showToast(window.mmquickshop_select_size_msg || 'Sélectionnez une taille'); 701 var firstSize = $('.mmqs-size:not(.mmqs-sold)', card); 702 if (firstSize) firstSize.focus(); 703 return; 704 } 705 706 var id_product_attribute = 0; 707 if (activeSize) { 708 var sizeAttr = parseInt(activeSize.getAttribute('data-id-attribute'), 10) || 0; 709 id_product_attribute = resolveIPA(card, sizeAttr); 710 if (!id_product_attribute) { 711 id_product_attribute = parseInt(activeSize.getAttribute('data-id-product-attribute'), 10) || 0; 712 } 713 } 714 715 var btns = $$('[data-action="quickadd"],[data-action="quickbuy"]', card); 716 btns.forEach(function (b) { b.classList.add('mmqs-loading'); b.disabled = true; }); 717 718 var formData = new FormData(); 719 formData.append('action', 'add'); 720 formData.append('id_product', id_product); 721 formData.append('id_product_attribute', id_product_attribute); 722 formData.append('qty', 1); 723 formData.append('ajax', 1); 724 formData.append('return_cart_details', 1); 725 726 fetch(window.mmquickshop_ajax_url, { 727 method: 'POST', 728 credentials: 'same-origin', 729 body: formData, 730 headers: { 'X-Requested-With': 'XMLHttpRequest' } 731 }) 732 .then(function (r) { return r.json().catch(function () { return {}; }); }) 733 .then(function (data) { 734 btns.forEach(function (b) { b.classList.remove('mmqs-loading'); b.disabled = false; }); 735 736 if (data && data.success) { 737 var original = btn.innerHTML; 738 btn.classList.add('mmqs-added'); 739 btn.innerHTML = '<span class="mmqs-ic">â</span>' + (data.label_added || 'Ajouté'); 740 741 try { 742 if (window.prestashop && typeof window.prestashop.emit === 'function') { 743 window.prestashop.emit('updateCart', { reason: { cart: data.cart || null }, resp: data }); 744 } 745 } catch (err) { /* ignore */ } 746 747 if (action === 'quickbuy') { 748 var cartUrl = window.mmquickshop_cart_show_url 749 || (window.mmquickshop_cart_url ? window.mmquickshop_cart_url + '?action=show' : '#'); 750 window.location.href = cartUrl; 751 return; 752 } 753 754 // v1.4.6 â sidebar OFF par défaut : on remplace l'ouverture du drawer 755 // par un simple toast. Le panier natif PS prend en compte l'ajout. 756 showToast(window.mmquickshop_added_msg || 'Ajouté au panier'); 757 758 setTimeout(function () { 759 btn.classList.remove('mmqs-added'); 760 btn.innerHTML = original; 761 }, 2200); 762 763 } else { 764 showToast((data && data.message) || 'Erreur â veuillez réessayer'); 765 } 766 }) 767 .catch(function () { 768 btns.forEach(function (b) { b.classList.remove('mmqs-loading'); b.disabled = false; }); 769 showToast('Erreur réseau â veuillez réessayer'); 770 }); 771 }); 772 773 /* âââââââââââââââââââââââââââââââââââââââââââ 774 Listen to PrestaShop's own updatedCart events 775 (GATED â utile uniquement pour rafraîchir le drawer) 776 âââââââââââââââââââââââââââââââââââââââââââ */ 777 function bindPsUpdatedCart() { 778 try { 779 if (window.prestashop && typeof window.prestashop.on === 'function') { 780 window.prestashop.on('updatedCart', function () { clearOptimisticWatchdog(); fetchCart(); }); 781 window.prestashop.on('updateCart', function () { clearOptimisticWatchdog(); fetchCart(); }); 782 } else { 783 setTimeout(bindPsUpdatedCart, 400); 784 } 785 } catch (e) { /* ignore */ } 786 } 787 788 if (MMQS_SIDEBAR_ON) { 789 /* âââââââââââââââââââââââââââââââââââââââââââ 790 OPEN OPTIMISTIC sur add-to-cart natif PDP 791 âââââââââââââââââââââââââââââââââââââââââââ */ 792 var NATIVE_ADD_SELECTORS = [ 793 '[data-button-action="add-to-cart"]', 794 '.js-add-to-cart' 795 ].join(','); 796 797 var POPUP_TRIGGER_SELECTORS = [ 798 '[data-toggle]', 799 '[data-bs-toggle]', 800 '[data-target]', 801 '[data-bs-target]', 802 '[aria-haspopup="true"]', 803 '[aria-haspopup="dialog"]', 804 '[data-stocks-popup]', 805 '[data-store-popup]', 806 '.open-popup', 807 '.stocks-popup', 808 '.store-locator-btn', 809 '.product-instore', 810 '.btn-stocks', 811 '.btn-availability' 812 ].join(','); 813 814 function isRealNativeAdd(target) { 815 if (!target) return false;
816 if (!target.matches || !target.matches(NATIVE_ADD_SELECTORS)) return false; 817 try { if (target.matches(POPUP_TRIGGER_SELECTORS)) return false; } catch (_) {} 818 if (target.tagName === 'A') { 819 var href = (target.getAttribute('href') || '').trim(); 820 if (!href || href === '#' || href.charAt(0) === '#' || href.indexOf('javascript:') === 0) return false; 821 } 822 var label = (target.textContent || target.getAttribute('aria-label') || '').toLowerCase(); 823 if (label) { 824 if (label.indexOf('dispo') !== -1 825 || label.indexOf('magasin') !== -1 826 || label.indexOf('boutique') !== -1 827 || label.indexOf('en stock en') !== -1 828 || label.indexOf('in store') !== -1 829 || label.indexOf('store availability') !== -1 830 || label.indexOf('check stores') !== -1) { 831 return false; 832 } 833 } 834 return true; 835 } 836 837 document.addEventListener('click', function (e) { 838 var target = e.target && e.target.closest ? e.target.closest(NATIVE_ADD_SELECTORS) : null; 839 if (!target) return; 840 if (!isRealNativeAdd(target)) return; 841 if (target.closest('[data-action="quickadd"],[data-action="quickbuy"]')) return; 842 if (target.closest('#mmqs-sidebar')) return; 843 if (target.disabled || (target.classList && target.classList.contains('mmqs-loading'))) return; 844 try { 845 showSkeletonInDrawer(); 846 openSidebar(); 847 startOptimisticWatchdog(); 848 } catch (err) { /* ignore */ } 849 }, true); 850 851 } /* end if (MMQS_SIDEBAR_ON) â block 2 */ 852 853 /* âââââââââââââââââââââââââââââââââââââââââââ 854 Public API â window.MMQS 855 v1.4.6 â Quand sidebar OFF : stubs no-op qui redirigent vers /panier 856 pour ne pas casser les modules tiers (mmcategoryshop) qui 857 pourraient appeler MMQS.open / addedExternal. 858 âââââââââââââââââââââââââââââââââââââââââââ */ 859 if (MMQS_SIDEBAR_ON) { 860 window.MMQS = { 861 open: function () { openSidebar(); }, 862 close: function () { closeSidebar(); }, 863 refresh: function (cb) { fetchCart(cb); }, 864 addedExternal: function (cartDetails) { 865 if (cartDetails) { 866 renderSidebarCart(cartDetails); 867 openSidebar(); 868 } else { 869 fetchCart(function () { openSidebar(); }); 870 } 871 } 872 }; 873 } else { 874 // Stubs no-op : redirige vers /panier au lieu de crasher. 875 var __cartUrl = function () { 876 return window.mmquickshop_cart_show_url 877 || window.mmquickshop_cart_url 878 || '/panier'; 879 }; 880 window.MMQS = { 881 open: function () { try { window.location = __cartUrl(); } catch(_){} }, 882 close: function () { /* no-op */ }, 883 refresh: function (cb) { if (typeof cb === 'function') { try { cb({}); } catch(_){} } }, 884 addedExternal: function () { try { window.location = __cartUrl(); } catch(_){} } 885 }; 886 } 887 888 /* ââ Boot ââ */ 889 document.addEventListener('DOMContentLoaded', function () { 890 if (MMQS_SIDEBAR_ON) { 891 // Marqueur body pour que le CSS sidebar (préfixé body.mmqs-sidebar-on) s'applique.
892 try { document.body.classList.add('mmqs-sidebar-on'); } catch(_){} 893 refs(); 894 bindPsUpdatedCart(); 895 fetchCart(); 896 } 897 }); 898})();
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.