PageSourceSearch

https://montagut.com/modules/mmquickshop/views/js/front-v146.js

js montagut.com collected 2026-10-08 01:33:55 UTC 37,089 bytes, 898 lines download raw bytes

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.