1/** 2 * Montagut - Cart Unified Handler 3 * Gère le sélecteur de quantité intelligent sur desktop ET mobile 4 * + Titre panier dynamique 5 * + Message livraison dynamique 6 * + Sticky sidebar CSS (position: sticky) 7 */ 8 9(function() { 10 'use strict'; 11 12 // Configuration 13 var FREE_SHIPPING_THRESHOLD = 250; 14 var isUpdating = false; 15 var STICKY_TOP_OFFSET = 80; 16 17 // Initialisation au chargement 18 document.addEventListener('DOMContentLoaded', function() { 19 disableTouchSpin(); 20 initQuantityButtons(); 21 cleanColorAttributes(); 22 updateCartTitle(); 23 updateShippingMessage(); 24 initPromoCodeToggle(); 25 initStickySidebar(); 26 }); 27 28 // Réinitialiser après refresh AJAX du panier 29 if (typeof prestashop !== 'undefined') { 30 prestashop.on('updatedCart', function() { 31 isUpdating = false; 32 setTimeout(function() { 33 disableTouchSpin(); 34 initQuantityButtons(); 35 cleanColorAttributes(); 36 hideAllLoaders(); 37 updateCartTitle(); 38 updateShippingMessage(); 39 initPromoCodeToggle(); 40 initStickySidebar(); 41 }, 100); 42 }); 43 } 44 45 /** 46 * Sticky Sidebar â CSS position:sticky 47 * 48 * Pour que sticky fonctionne, le parent (.cart-summary-column) doit être 49 * PLUS GRAND que le sticky. On force align-self:stretch pour que la colonne 50 * s'étire sur toute la hauteur du grid (celle de la colonne produits). 51 * Le sticky "voyage" alors dans cet espace. 52 */ 53 function initStickySidebar() { 54 var sidebar = document.querySelector('.cart-summary-sticky'); 55 var sidebarColumn = document.querySelector('.cart-summary-column'); 56 57 if (!sidebar || !sidebarColumn) return; 58 59 // Injecter le CSS une seule fois 60 if (!document.getElementById('sticky-sidebar-styles')) { 61 var style = document.createElement('style'); 62 style.id = 'sticky-sidebar-styles'; 63 style.textContent = 64 '@media (min-width: 992px) {' + 65 '.cart-summary-column.sticky-active {' + 66 'align-self: stretch !important;' + 67 '}' + 68 '.cart-summary-sticky.is-sticky-enabled {' + 69 'position: -webkit-sticky !important;' + 70 'position: sticky !important;' + 71 'top: ' + STICKY_TOP_OFFSET + 'px !important;' + 72 '}' + 73 '}'; 74 document.head.appendChild(style); 75 } 76 77 // Nettoyer tout style inline résiduel (ancienne logique JS) 78 sidebar.style.position = ''; 79 sidebar.style.top = ''; 80 sidebar.style.width = ''; 81 82 // Appliquer ou retirer selon les conditions 83 applyStickyState(sidebar, sidebarColumn); 84 85 // Nettoyer les anciens listeners 86 if (window._stickyResizeHandler) { 87 window.removeEventListener('resize', window._stickyResizeHandler); 88 } 89 if (window._stickyScrollHandler) { 90 window.removeEventListener('scroll', window._stickyScrollHandler); 91 window._stickyScrollHandler = null; 92 } 93 94 // Resize : activer/désactiver selon viewport 95 window._stickyResizeHandler = function() { 96 applyStickyState(sidebar, sidebarColumn); 97 }; 98 window.addEventListener('resize', window._stickyResizeHandler); 99 } 100 101 /** 102 * Active ou désactive le sticky selon viewport et nombre de produits 103 */ 104 function applyStickyState(sidebar, sidebarColumn) { 105 var productCount = document.querySelectorAll('.cart-items > .cart-item').length; 106 107 if (window.innerWidth <= 991 || productCount < 3) { 108 sidebar.classList.remove('is-sticky-enabled'); 109 sidebarColumn.classList.remove('sticky-active'); 110 } else { 111 sidebar.classList.add('is-sticky-enabled'); 112 sidebarColumn.classList.add('sticky-active'); 113 } 114 } 115 116 /** 117 * Désactive le plugin TouchSpin de PrestaShop 118 */ 119 function disableTouchSpin() { 120 if (typeof jQuery !== 'undefined' && jQuery.fn.TouchSpin) {
121 jQuery('.cart-quantity-selector input').each(function() { 122 var $input = jQuery(this); 123 if ($input.data('bts.touchspin')) { 124 try { 125 $input.TouchSpin('destroy'); 126 } catch(e) {} 127 } 128 }); 129 } 130 131 var elementsToRemove = [ 132 '.cart-quantity-selector .input-group-btn-vertical', 133 '.cart-quantity-selector .bootstrap-touchspin-prefix', 134 '.cart-quantity-selector .bootstrap-touchspin-postfix', 135 '.cart-quantity-selector .touchspin-up', 136 '.cart-quantity-selector .touchspin-down', 137 '.cart-quantity-selector .btn-touchspin' 138 ]; 139 140 elementsToRemove.forEach(function(selector) { 141 document.querySelectorAll(selector).forEach(function(el) { 142 el.remove(); 143 }); 144 }); 145 146 document.querySelectorAll('.cart-quantity-selector .bootstrap-touchspin').forEach(function(wrapper) { 147 var input = wrapper.querySelector('input'); 148 if (input && wrapper.parentNode) { 149 input.style.display = 'inline-flex'; 150 input.style.visibility = 'visible'; 151 input.style.opacity = '1'; 152 } 153 }); 154 } 155 156 /** 157 * Met à jour le titre "Mon Panier (X)" 158 */ 159 function updateCartTitle() { 160 var titleElement = document.querySelector('.cart-title .cart-items-count'); 161 if (!titleElement) return; 162 163 var totalItems = 0; 164 165 if (typeof prestashop !== 'undefined' && prestashop.cart && prestashop.cart.products_count !== undefined) { 166 totalItems = parseInt(prestashop.cart.products_count) || 0; 167 } else { 168 var productLines = document.querySelectorAll('.cart-items > .cart-item > .product-line-unified'); 169 170 if (productLines.length === 0) { 171 productLines = document.querySelectorAll('.cart-item .product-line-unified'); 172 } 173 174 var countedProducts = new Set(); 175 176 productLines.forEach(function(item) { 177 var productId = item.dataset.idProduct || item.closest('[data-id-product]')?.dataset.idProduct; 178 var productAttrId = item.dataset.idProductAttribute || item.closest('[data-id-product-attribute]')?.dataset.idProductAttribute || '0'; 179 var uniqueKey = productId + '-' + productAttrId; 180 181 if (productId && !countedProducts.has(uniqueKey)) { 182 countedProducts.add(uniqueKey); 183 184 var input = item.querySelector('.qty-input'); 185 if (input && input.value) { 186 totalItems += parseInt(input.value) || 0; 187 } else { 188 var display = item.querySelector('.qty-display'); 189 if (display) { 190 totalItems += parseInt(display.textContent) || 1; 191 } else { 192 var giftQty = item.querySelector('.gift-quantity'); 193 if (giftQty) { 194 totalItems += parseInt(giftQty.textContent) || 1; 195 } else { 196 totalItems += 1; 197 } 198 } 199 } 200 } 201 }); 202 } 203 204 titleElement.textContent = '(' + totalItems + ')'; 205 } 206 207 /** 208 * Met à jour le message de livraison 209 */ 210 function updateShippingMessage() { 211 var messageContainer = document.querySelector('.js-shipping-message'); 212 if (!messageContainer) return; 213 214 var cartTotal = 0; 215 216 if (typeof prestashop !== 'undefined' && prestashop.cart) { 217 cartTotal = parseFloat(prestashop.cart.totals.total.amount) || 0; 218 } 219 220 if (cartTotal === 0) { 221 var totalElement = document.querySelector('.cart-total .value, .cart-summary-line.cart-total .value'); 222 if (totalElement) { 223 var totalText = totalElement.textContent.replace(/[^\d,\.]/g, '').replace(',', '.'); 224 cartTotal = parseFloat(totalText) || 0; 225 } 226 } 227 228 var freeShippingMsg = messageContainer.querySelector('.free-shipping-message'); 229 var nextStepMsg = messageContainer.querySelector('.shipping-next-step-message'); 230 231 if (freeShippingMsg && nextStepMsg) { 232 if (cartTotal >= FREE_SHIPPING_THRESHOLD) { 233 freeShippingMsg.style.display = 'inline'; 234 nextStepMsg.style.display = 'none'; 235 } else { 236 freeShippingMsg.style.display = 'none'; 237 nextStepMsg.style.display = 'inline'; 238 } 239 } 240 } 241 242 /** 243 * Nettoie les noms de couleurs (supprime préfixe numérique) 244 */ 245 function cleanColorAttributes() { 246 var selectors = [ 247 '.product-attribute .attr-value', 248 '.cart-product-attribute .attr-value', 249 '.product-line-info.couleur .value' 250 ]; 251
252 selectors.forEach(function(selector) { 253 document.querySelectorAll(selector).forEach(function(el) { 254 var text = el.textContent.trim(); 255 var cleaned = text.replace(/^\d{1,3}-/, ''); 256 if (cleaned !== text) { 257 el.textContent = cleaned; 258 } 259 }); 260 }); 261 } 262 263 /** 264 * Initialise tous les boutons de quantité via délégation d'événements 265 */ 266 function initQuantityButtons() { 267 if (window.cartQuantityInitialized) return; 268 window.cartQuantityInitialized = true; 269 270 document.addEventListener('click', function(e) { 271 var target = e.target.closest('.qty-btn'); 272 if (!target) return; 273 274 if (isUpdating) { 275 e.preventDefault(); 276 e.stopPropagation(); 277 return; 278 } 279 280 if (target.classList.contains('qty-increase')) { 281 e.preventDefault(); 282 e.stopPropagation(); 283 284 var upUrl = target.getAttribute('data-up-url'); 285 if (upUrl) { 286 var selector = target.closest('.cart-quantity-selector'); 287 showSpinner(selector); 288 isUpdating = true; 289 updateCart(upUrl); 290 } 291 return; 292 } 293 294 if (target.classList.contains('qty-decrease')) { 295 e.preventDefault(); 296 e.stopPropagation(); 297 298 var downUrl = target.getAttribute('data-down-url'); 299 if (downUrl) { 300 var selector = target.closest('.cart-quantity-selector'); 301 showSpinner(selector); 302 isUpdating = true; 303 updateCart(downUrl); 304 } 305 return; 306 } 307 308 if (target.classList.contains('qty-trash')) { 309 var selector = target.closest('.cart-quantity-selector'); 310 showSpinner(selector); 311 isUpdating = true; 312 return; 313 } 314 }); 315 316 document.addEventListener('change', function(e) { 317 var input = e.target; 318 if (!input.classList.contains('qty-input') && !input.classList.contains('js-cart-line-product-quantity')) { 319 return; 320 } 321 322 if (isUpdating) return; 323 324 var newQty = parseInt(input.value) || 1; 325 if (newQty < 1) newQty = 1; 326 327 var updateUrl = input.getAttribute('data-update-url'); 328 if (updateUrl) { 329 var selector = input.closest('.cart-quantity-selector'); 330 showSpinner(selector); 331 isUpdating = true; 332 updateCart(updateUrl + '&qty=' + newQty); 333 } 334 }); 335 } 336 337 /** 338 * Affiche le spinner sur un sélecteur de quantité 339 */ 340 function showSpinner(selector) { 341 if (!selector) return; 342 343 selector.classList.add('is-loading'); 344 345 selector.querySelectorAll('.qty-btn').forEach(function(btn) { 346 btn.disabled = true; 347 btn.classList.add('disabled'); 348 }); 349 350 var input = selector.querySelector('.qty-input'); 351 if (input) { 352 input.disabled = true; 353 } 354 355 if (!selector.querySelector('.qty-spinner')) { 356 var spinner = document.createElement('div'); 357 spinner.className = 'qty-spinner'; 358 spinner.innerHTML = '<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-dasharray="31.4 31.4" transform="rotate(-90 12 12)"><animateTransform attributeName="transform" type="rotate" from="0 12 12" to="360 12 12" dur="0.6s" repeatCount="indefinite"/></circle></svg>'; 359 selector.appendChild(spinner); 360 } 361 } 362 363 /** 364 * Cache tous les loaders 365 */ 366 function hideAllLoaders() { 367 isUpdating = false; 368 369 document.querySelectorAll('.cart-quantity-selector').forEach(function(selector) { 370 selector.classList.remove('is-loading'); 371 372 selector.querySelectorAll('.qty-btn').forEach(function(btn) { 373 btn.disabled = false; 374 btn.classList.remove('disabled'); 375 }); 376 377 var input = selector.querySelector('.qty-input'); 378 if (input) { 379 input.disabled = false; 380 } 381 382 var spinner = selector.querySelector('.qty-spinner'); 383 if (spinner) { 384 spinner.remove(); 385 } 386 }); 387 } 388 389 /** 390 * Met à jour le panier via AJAX 391 */ 392 function updateCart(url) { 393 var ajaxUrl = url + (url.indexOf('?') > -1 ? '&' : '?') + 'ajax=1&action=update'; 394 395 fetch(ajaxUrl, { 396 method: 'POST', 397 headers: { 398 'Accept': 'application/json, text/javascript, */*; q=0.01', 399 'X-Requested-With': 'XMLHttpRequest' 400 } 401 }) 402 .then(function(response) { 403 return response.json(); 404 }) 405 .then(function(data) { 406 if (typeof prestashop !== 'undefined') { 407 prestashop.emit('updateCart', { 408 reason: data.id_product ? { idProduct: data.id_product } : {}, 409 resp: data 410 }); 411 } else { 412 window.location.reload(); 413 } 414 }) 415 .catch(function(error) { 416 console.error('Cart: Erreur', error); 417 isUpdating = false;
418 hideAllLoaders(); 419 window.location.reload(); 420 }); 421 } 422 423 /** 424 * Toggle du champ code promo 425 */ 426 function initPromoCodeToggle() { 427 var toggleBtns = document.querySelectorAll('.js-promo-toggle'); 428 429 toggleBtns.forEach(function(btn) { 430 if (btn.dataset.initialized) return; 431 btn.dataset.initialized = 'true'; 432 433 btn.addEventListener('click', function(e) { 434 e.preventDefault(); 435 436 var form = this.nextElementSibling; 437 if (!form || !form.classList.contains('js-promo-form')) { 438 form = this.parentElement.querySelector('.js-promo-form'); 439 } 440 441 if (form) { 442 this.classList.toggle('is-open'); 443 form.classList.toggle('is-open'); 444 445 if (form.classList.contains('is-open')) { 446 var input = form.querySelector('input[type="text"], input[name="discount_name"], .promo-input'); 447 if (input) { 448 setTimeout(function() { input.focus(); }, 100); 449 } 450 } 451 } 452 }); 453 }); 454 } 455 456})();
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.