1// Site-wide product-card painter (favoris + cart). 2 3const operationsInProgress = new Set(); 4 5class UserSessionContext { 6 static getCookie(name) { 7 const value = `; ${document.cookie}`; 8 const parts = value.split(`; ${name}=`); 9 if (parts.length === 2) { 10 return parts.pop().split(';').shift(); 11 } 12 return null; 13 } 14 15 static getUserToken() { 16 const userCookie = this.getCookie('lbe_user'); 17 if (userCookie && userCookie !== 'None') { 18 return userCookie; 19 } 20 return 'anonymous'; 21 } 22 23 static getSessionId() { 24 return this.getCookie('sessionid') || null; 25 } 26} 27 28class CartStateCache { 29 static get storageKey() { 30 return 'cart_state_cache_v1'; 31 } 32 33 static get storageVersion() { 34 return 3; 35 } 36 37 static get ttl() { 38 return 24 * 60 * 60 * 1000; 39 } 40 41 static read() { 42 try { 43 const raw = localStorage.getItem(this.storageKey); 44 if (!raw) { 45 return null; 46 } 47 const parsed = JSON.parse(raw); 48 if (parsed.version !== this.storageVersion) { 49 return null; 50 } 51 if (parsed.userToken !== UserSessionContext.getUserToken()) { 52 return null; 53 } 54 if (parsed.sessionId !== UserSessionContext.getSessionId()) { 55 return null; 56 } 57 if (!parsed.timestamp || Date.now() - parsed.timestamp > this.ttl) { 58 this.clear(); 59 return null; 60 } 61 return parsed; 62 } catch (error) { 63 console.error('Unable to read cart state cache:', error); 64 return null; 65 } 66 } 67 68 static write(cartItems, selectionProducts) { 69 try { 70 const payload = { 71 version: this.storageVersion, 72 userToken: UserSessionContext.getUserToken(), 73 sessionId: UserSessionContext.getSessionId(), 74 cart_items: cartItems, 75 selection_products: Array.isArray(selectionProducts) ? selectionProducts : [], 76 timestamp: Date.now(), 77 }; 78 localStorage.setItem(this.storageKey, JSON.stringify(payload)); 79 } catch (error) { 80 console.error('Unable to write cart state cache:', error); 81 } 82 } 83 84 static clear() { 85 try { 86 localStorage.removeItem(this.storageKey); 87 } catch (error) { 88 console.error('Unable to clear cart state cache:', error); 89 } 90 } 91 92 static normalize(cartItems) { 93 return Array.isArray(cartItems) ? cartItems.map((item) => this.normalizeItem(item)).filter((item) => item !== null) : []; 94 } 95 96 static normalizeItem(item) { 97 if (!item) { 98 return null; 99 } 100 const bestOfferId = Number(item.best_offer_id ?? item.bestOfferId ?? item.offerId ?? item.id); 101 if (!bestOfferId) { 102 return null; 103 } 104 return { 105 id: item.id ?? item.cart_item_id ?? null, 106 best_offer_id: bestOfferId, 107 quantity: Math.max(1, Number(item.quantity ?? item.qty ?? 1) || 1), 108 }; 109 } 110} 111 112class ProductCardManager { 113 constructor() { 114 this.baseUrl = '/fr'; 115 this.selectors = { 116 productCard: '.product-card', 117 favorisHeart: '.favoris-heart-button', 118 addToCartButton: '.js-product-card-addtocart-button', 119 }; 120 this.cachedCart = []; 121 this.cachedFavoris = new Set(); 122 this.handleCartChange = this.handleCartChange.bind(this); 123 this.handleCartCleared = this.handleCartCleared.bind(this); 124 this.handleForceRefresh = this.handleForceRefresh.bind(this); 125 this.handleSelectionChange = this.handleSelectionChange.bind(this); 126 this.handleFavorisRemove = this.handleFavorisRemove.bind(this); 127 this.handleCheckoutLineAdded = this.handleCheckoutLineAdded.bind(this); 128 this.handleCheckoutLineRemoved = this.handleCheckoutLineRemoved.bind(this); 129 this.handleCartCountChanged = this.handleCartCountChanged.bind(this); 130 } 131 132 init() { 133 this.attachGlobalListeners(); 134 if (!this.bootstrapFromIncomingData() && !this.bootstrapFromCache()) { 135 this.fetchCartState({ force: true }); 136 } 137 this.setupEventListeners(); 138 } 139 140 setCachedFavoris(productIds) { 141 this.cachedFavoris = new Set((Array.isArray(productIds) ? productIds : []).map((id) => Number(id)).filter((id) => Number.isFinite(id) && id > 0)); 142 } 143 144 applyFavorisToCards() { 145 document.querySelectorAll(this.selectors.favorisHeart).forEach((btn) => { 146 this._paintHeart(btn); 147 }); 148 } 149 150 applyFavorisToProduct(productId) { 151 const selector = `${this.selectors.favorisHeart}[data-product-id="${productId}"]`; 152 document.querySelectorAll(selector).forEach((btn) => { 153 this._paintHeart(btn); 154 }); 155 CartStateCache.write(this.cachedCart, Array.from(this.cachedFavoris)); 156 } 157 158 _paintHeart(btn) { 159 const productId = Number(btn.dataset.productId); 160 if (!Number.isFinite(productId) || productId <= 0) { 161 return; 162 } 163 const isFav = this.cachedFavoris.has(productId); 164 btn.classList.toggle('active', isFav); 165 // Anonymous hearts use aria-haspopup="dialog"; skip aria-pressed. 166 if (btn.hasAttribute('aria-pressed')) { 167 btn.setAttribute('aria-pressed', isFav ? 'true' : 'false'); 168 btn.setAttribute('aria-label', isFav ? 'Retirer des favoris' : 'Ajouter aux favoris'); 169 } 170 } 171 172 handleSelectionChange(event) { 173 const detail = event?.detail || {}; 174 const productId = Number(detail.productId); 175 if (!Number.isFinite(productId) || productId <= 0) { 176 return; 177 } 178 if (detail.action === 'add') { 179 this.cachedFavoris.add(productId); 180 } else if (detail.action === 'remove') { 181 this.cachedFavoris.delete(productId); 182 } 183 this.applyFavorisToProduct(productId); 184 } 185 186 bootstrapFromIncomingData() { 187 const hasCart = Array.isArray(window.INCOMING_CART_ITEMS); 188 const hasFavoris = Array.isArray(window.INCOMING_SELECTION_PRODUCTS); 189 if (!hasCart && !hasFavoris) { 190 return false;
191 } 192 // Hydrate favoris first so setCachedCart's localStorage write captures the 193 // correct selection_products payload (otherwise it serializes an empty Set). 194 if (hasFavoris) { 195 this.setCachedFavoris(window.INCOMING_SELECTION_PRODUCTS); 196 delete window.INCOMING_SELECTION_PRODUCTS; 197 } 198 if (hasCart) { 199 this.setCachedCart(window.INCOMING_CART_ITEMS); 200 delete window.INCOMING_CART_ITEMS; 201 } else { 202 CartStateCache.write(this.cachedCart, Array.from(this.cachedFavoris)); 203 } 204 this.applyCartToCards(); 205 this.applyFavorisToCards(); 206 this._dispatchHydrated(); 207 return true; 208 } 209 210 attachGlobalListeners() { 211 window.addEventListener('product-state:cart-change', this.handleCartChange); 212 window.addEventListener('product-state:cart-cleared', this.handleCartCleared); 213 window.addEventListener('product-state:refresh', this.handleForceRefresh); 214 window.addEventListener('product-state:selection-change', this.handleSelectionChange); 215 window.addEventListener('favoris:remove', this.handleFavorisRemove); 216 // Les événements HX-Trigger sont dépêchés sur l'élément déclencheur â le bouton de la 217 // carte â et remontent jusqu'ici. 218 window.addEventListener('checkout:line-added', this.handleCheckoutLineAdded); 219 window.addEventListener('checkout:line-removed', this.handleCheckoutLineRemoved); 220 window.addEventListener('cart-count-changed', this.handleCartCountChanged); 221 } 222 223 handleCheckoutLineAdded(event) { 224 const button = this._cardButtonOf(event); 225 if (!button) { 226 return; 227 } 228 const detail = (event && event.detail) || {}; 229 const lineId = detail.line_id; 230 const offerId = Number(detail.offer_id); 231 if (lineId == null || !Number.isFinite(offerId) || offerId <= 0) { 232 return; 233 } 234 this._paintAddedButton(button, lineId); 235 // Une ligne du tunnel vaut un exemplaire, et cart-count-changed pose déjà le compteur 236 // absolu : laisser le delta d'en-tête ici le compterait deux fois. 237 this.applyCartChange('add', { id: lineId, best_offer_id: offerId, quantity: 1 }, { updateHeaderDelta: false }); 238 this._pushGa4AddToCart(button); 239 } 240 241 // Le retrait ne vient d'aucune carte : la poubelle cliquée vit dans le tunnel, et la carte à 242 // dépeindre est celle du catalogue derrière la modale. Le cache est donc corrigé puis TOUTES 243 // les cartes sont repeintes depuis lui â sans quoi la carte reste « Ajouté » 24h durant. 244 handleCheckoutLineRemoved(event) { 245 const detail = (event && event.detail) || {}; 246 const offerId = Number(detail.offer_id); 247 if (!Number.isFinite(offerId) || offerId <= 0) { 248 return; 249 } 250 // cart-count-changed pose déjà le compteur absolu : un delta ici le décompterait deux fois. 251 this.applyCartChange('remove', { id: detail.line_id ?? null, best_offer_id: offerId, quantity: 1 }, { updateHeaderDelta: false }); 252 this.applyCartToCards(); 253 } 254 255 handleCartCountChanged(event) { 256 const detail = (event && event.detail) || {}; 257 const count = Number(detail.count); 258 if (!Number.isFinite(count)) { 259 return; 260 } 261 const cartCountElement = document.querySelector('.js-cart-count'); 262 const btnCartHeader = document.querySelector('#cart-link-btn-header'); 263 if (cartCountElement && btnCartHeader) { 264 this.updateCartUI(cartCountElement, btnCartHeader, count); 265 } 266 } 267 268 _cardButtonOf(event) { 269 const target = event && event.target; 270 if (!target || typeof target.closest !== 'function') { 271 return null; 272 } 273 return target.closest(this.selectors.addToCartButton); 274 } 275 276 handleFavorisRemove(event) { 277 const detail = event?.detail || {}; 278 const productId = Number(detail.productId); 279 280 if (Number.isFinite(productId) && productId > 0) { 281 this.cachedFavoris.delete(productId); 282 this.applyFavorisToProduct(productId); 283 } 284 285 // The grid surfaces are now refreshed server-side via HTMX swap on the 286 // trash button itself; only the rail/PDP path needs DOM-removal here. 287 const grid = document.getElementById('favoris-grid') || document.getElementById('favoris-boutique-grid'); 288 if (grid) return; 289 290 const card = detail.cardElement; 291 if (card && card.parentNode) { 292 card.parentNode.removeChild(card); 293 } 294 } 295 296 bootstrapFromCache() { 297 const cached = CartStateCache.read(); 298 if (!cached) { 299 return false;
300 } 301 this.setCachedCart(cached.cart_items || [], false); 302 this.applyCartToCards(); 303 this.setCachedFavoris(cached.selection_products || []); 304 this.applyFavorisToCards(); 305 this._dispatchHydrated(); 306 // 24h TTL can drift across tabs/devices â revalidate in background. 307 this.fetchCartState({ force: true }); 308 return true; 309 } 310 311 _dispatchHydrated() { 312 window.dispatchEvent(new CustomEvent('product-state:hydrated')); 313 } 314 315 setCachedCart(cartItems, persist = true) { 316 this.cachedCart = CartStateCache.normalize(cartItems); 317 if (persist) { 318 CartStateCache.write(this.cachedCart, Array.from(this.cachedFavoris)); 319 } 320 } 321 322 applyCartToCards() { 323 document.querySelectorAll(this.selectors.addToCartButton).forEach((button) => { 324 button.classList.remove('active'); 325 button.classList.remove('active-animation'); 326 button.classList.remove('waiting'); 327 button.removeAttribute('data-cart_item_id'); 328 }); 329 this.cachedCart.forEach((cartItem) => { 330 const id = cartItem.best_offer_id; 331 const cartItemId = cartItem.id; 332 document.querySelectorAll(`${this.selectors.productCard}[data-id="${id}"]`).forEach((card) => { 333 const btn = card.querySelector(this.selectors.addToCartButton); 334 if (btn) { 335 btn.classList.add('active'); 336 btn.classList.add('active-animation'); 337 btn.dataset.cart_item_id = cartItemId; 338 } 339 }); 340 }); 341 this.updateCartCount(this.cachedCart); 342 } 343 344 fetchCartState(options = {}) { 345 const { force = false } = options; 346 if (typeof IS_BOT !== 'undefined' && IS_BOT) { 347 return; 348 } 349 if (!force) { 350 const cached = CartStateCache.read(); 351 if (cached) { 352 this.setCachedCart(cached.cart_items || [], false); 353 this.applyCartToCards(); 354 return; 355 } 356 } 357 if (typeof axios === 'undefined') { 358 return; 359 } 360 axios 361 .get(`${this.baseUrl}/ajax/get_products_state/`) 362 .then((response) => { 363 const { cart_items, selection_products } = response.data || {}; 364 this.setCachedFavoris(selection_products || []); 365 this.setCachedCart(cart_items || []); 366 this.applyCartToCards(); 367 this.applyFavorisToCards(); 368 this._dispatchHydrated(); 369 }) 370 .catch((error) => { 371 console.error('Error fetching cart state:', error); 372 }); 373 } 374 375 refreshState() { 376 this.fetchCartState({ force: true }); 377 } 378 379 setupEventListeners() { 380 document.querySelectorAll(this.selectors.addToCartButton).forEach((button) => { 381 if (!button.dataset.bound) { 382 button.addEventListener('click', () => this.handleAddToCart(button)); 383 button.dataset.bound = true; 384 } 385 }); 386 } 387 388 initEventListeners() { 389 return this.setupEventListeners(); 390 } 391 392 updateCartCount(cartItems) { 393 const cartCountElement = document.querySelector('.js-cart-count'); 394 const btnCartHeader = document.querySelector('#cart-link-btn-header'); 395 if (cartCountElement && btnCartHeader) { 396 const cartCount = cartItems.reduce((total, item) => total + item.quantity, 0); 397 this.updateCartUI(cartCountElement, btnCartHeader, cartCount); 398 } 399 } 400 401 incrementCartCount(amount = 1) { 402 const cartCountElement = document.querySelector('.js-cart-count'); 403 const btnCartHeader = document.querySelector('#cart-link-btn-header'); 404 if (cartCountElement && btnCartHeader) { 405 let currentCount = parseInt(cartCountElement.textContent) || 0; 406 currentCount += amount; 407 this.updateCartUI(cartCountElement, btnCartHeader, currentCount); 408 } 409 } 410 411 decrementCartCount(amount = 1) { 412 const cartCountElement = document.querySelector('.js-cart-count'); 413 const btnCartHeader = document.querySelector('#cart-link-btn-header'); 414 if (cartCountElement && btnCartHeader) { 415 let currentCount = parseInt(cartCountElement.textContent) || 0; 416 currentCount = Math.max(0, currentCount - amount); 417 this.updateCartUI(cartCountElement, btnCartHeader, currentCount); 418 } 419 } 420 421 updateCartUI(cartCountElement, btnCartHeader, cartCount) { 422 cartCountElement.textContent = cartCount; 423 cartCountElement.style.display = cartCount > 0 ? 'inline' : 'none'; 424 btnCartHeader.classList.toggle('active', cartCount > 0); 425 } 426 427 handleCartChange(event) { 428 if (!event || !event.detail) { 429 return; 430 } 431 const { action, item, options } = event.detail; 432 this.applyCartChange(action, item, options); 433 } 434 435 applyCartChange(action, rawItem, options = {}) { 436 if (action !== 'add' && action !== 'remove') { 437 return; 438 } 439 const normalizedItem = CartStateCache.normalizeItem(rawItem); 440 if (!normalizedItem) { 441 return; 442 } 443 const { updateHeaderDelta = true } = options; 444 const cartItems = [...this.cachedCart]; 445 const existingIndex = cartItems.findIndex((cartItem) => cartItem.best_offer_id === normalizedItem.best_offer_id); 446 if (action === 'add') { 447 let quantityDelta = normalizedItem.quantity; 448 if (existingIndex === -1) { 449 cartItems.push(normalizedItem); 450 } else { 451 const previousItem = cartItems[existingIndex]; 452 quantityDelta = normalizedItem.quantity - previousItem.quantity; 453 cartItems[existingIndex] = normalizedItem; 454 } 455 if (updateHeaderDelta && quantityDelta !== 0) { 456 if (quantityDelta > 0) { 457 this.incrementCartCount(quantityDelta); 458 } else { 459 this.decrementCartCount(Math.abs(quantityDelta)); 460 } 461 } 462 } else if (action === 'remove') { 463 if (existingIndex !== -1) { 464 const removedItem = cartItems.splice(existingIndex, 1)[0]; 465 if (updateHeaderDelta) { 466 this.decrementCartCount(removedItem.quantity); 467 } 468 } 469 } 470 this.setCachedCart(cartItems); 471 this.updateCartCount(cartItems); 472 } 473 474 handleCartCleared() { 475 CartStateCache.clear(); 476 this.setCachedCart([], false); 477 this.updateCartCount([]); 478 this.fetchCartState({ force: true }); 479 } 480 481 handleForceRefresh() { 482 CartStateCache.clear(); 483 this.setCachedCart([], false); 484 this.applyCartToCards(); 485 this.fetchCartState({ force: true }); 486 } 487 488 handleAddToCart(button) { 489 const offerId = button.dataset.offer_id; 490 491 if (operationsInProgress.has(offerId)) { 492 return; 493 } 494 operationsInProgress.add(offerId); 495 496 if (button.dataset.cart_item_id) { 497 this.removeFromCart(button); 498 return; 499 } 500 501 this.addToCart(button); 502 } 503 504 addToCart(button) { 505 const mount = document.getElementById('checkout-add-modal-mount'); 506 const addUrl = mount && mount.dataset.addUrl; 507 if (addUrl && typeof htmx !== 'undefined') { 508 this.addToCartThroughModal(button, addUrl); 509 return; 510 } 511 512 const offerId = button.dataset.offer_id; 513 const data = new FormData(); 514 data.append('offer_id', offerId); 515 data.append('quantity', 1); 516 517 button.classList.add('waiting'); 518 519 axios 520 .post(button.dataset.url, data, { withCredentials: true }) 521 .then((response) => { 522 // Endpoint returns 200 with success=false on business-rule rejection. 523 const payload = response.data || {}; 524 if (response.status === 200 && payload.success === true) { 525 const { data_for_data_layer, cart_item_id, cart_item_quantity } = payload; 526 const quantity = Number(cart_item_quantity) || 1; 527 this.handleAddToCartSuccess(button, data_for_data_layer, cart_item_id, quantity); 528 } else { 529 this.handleAddToCartError(button, response); 530 } 531 }) 532 .catch((error) => { 533 console.error(error); 534 this.handleAddToCartError(button, error); 535 }); 536 } 537 538 // Pas de hx-post déclaratif sur le bouton : les cartes Netwave sont des chaînes JS jamais 539 // passées par htmx.process, les clones de boucle Swiper sont dupliqués hors du cycle htmx, 540 // et le bouton est un toggle, ce qu'aucune expression hx-* statique n'exprime. 541 addToCartThroughModal(button, addUrl) { 542 const offerId = button.dataset.offer_id; 543 // Le bouton est à la fois .robert-button et .product-button-action : htmx poserait 544 // htmx-request dessus, qui porte déjà son ::before rotatif, pendant que .waiting en pose 545 // un second en :after. Dérouter l'indicateur vers l'ancre ne laisse qu'un spinner. 546 button.setAttribute('hx-indicator', '#checkout-add-modal-mount'); 547 button.classList.add('waiting'); 548 // Le refus sort par le chemin legacy (messages / toast), sans échange : sans relâche sur les 549 // deux issues, un ajout raté gèle le bouton. 550 const release = () => { 551 button.classList.remove('waiting'); 552 operationsInProgress.delete(offerId); 553 }; 554 // Elle se prend sur l'événement du bouton et non sur la promesse : checkoutAddModal.js retient 555 // les ajouts d'une rafale pour les passer un par un, et htmx résout la promesse dès qu'il 556 // retient la requête. Relâcher là rendrait le bouton cliquable avant son propre ajout, et un 557 // deuxième clic doublerait la ligne. 558 button.addEventListener('htmx:afterRequest', release, { once: true }); 559 // Un ajout déclenché depuis un rail DE LA MODALE ne rejoue que son intérieur : viser l'ancre 560 // détruirait le panneau ouvert, rejouerait sa transition, remonterait le défilement et 561 // volerait le focus. La réponse porte la coquille entière, `select` en extrait le fragment. 562 const inModal = button.closest('.checkout-add-modal') !== null; 563 // La page panier montre déjà le ticket qu'ouvrirait la modale : `swap: 'none'` fait l'ajout 564 // et laisse passer les en-têtes HX-Trigger (compteur, ligne ajoutée) sans jamais peindre la 565 // modale. cart/_body.html#checkout-cart-refresh écoute déjà `cart-count-changed` pour 566 // rejouer le contenu du panier. 567 const onCartPage = !inModal && button.closest('#checkout-cart-body') !== null; 568 const swapTarget = inModal 569 ? { target: '#checkout-modal-ticket', swap: 'outerHTML', select: '#checkout-modal-ticket' } 570 : onCartPage 571 ? { target: '#checkout-cart-body', swap: 'none' } 572 : { target: '#checkout-add-modal-mount', swap: 'innerHTML' }; 573 htmx 574 .ajax('POST', addUrl, { 575 ...swapTarget, 576 source: button, 577 values: { offer_id: offerId }, 578 }) 579 // Une requête qui n'est jamais partie â ancre absente â n'émettra pas afterRequest. 580 .catch(release); 581 } 582 583 removeFromCart(button) { 584 const cartItemId = button.dataset.cart_item_id; 585 const offerId = button.dataset.offer_id; 586 const data = new FormData(); 587 data.append('item_id', cartItemId); 588 data.append('item_offer_id', offerId); 589 590 button.classList.add('waiting'); 591 592 axios 593 .post(`${this.baseUrl}/commander/supprimer/`, data, { withCredentials: true }) 594 .then((response) => { 595 button.classList.remove('waiting'); 596 button.classList.remove('active-animation'); 597 button.dataset.cart_item_id = null; 598 button.removeAttribute('data-cart_item_id'); 599 600 setTimeout(() => { 601 button.classList.remove('active'); 602 }, 50); 603 604 const cachedItem = this.cachedCart.find((item) => item.best_offer_id === Number(offerId)); 605 const quantity = cachedItem ? cachedItem.quantity : 1; 606 607 this.applyCartChange( 608 'remove', 609 { 610 id: cartItemId, 611 best_offer_id: Number(offerId), 612 quantity, 613 }, 614 { updateHeaderDelta: true } 615 ); 616 617 operationsInProgress.delete(offerId); 618 619 const item = response.data && response.data.data_for_data_layer && response.data.data_for_data_layer.item; 620 if (typeof ga4 !== 'undefined' && item) { 621 ga4.removeFromCart({ ...item, quantity }); 622 } 623 if (typeof data_layer_push !== 'undefined' && data_layer_push.remove_cart_attribution) { 624 data_layer_push.remove_cart_attribution(cartItemId); 625 } 626 }) 627 .catch((error) => { 628 console.error('Error removing item from cart:', error); 629 button.classList.remove('waiting'); 630 operationsInProgress.delete(offerId); 631 }); 632 } 633 634 _paintAddedButton(button, cartItemId) { 635 button.classList.remove('waiting'); 636 button.classList.add('active'); 637 638 setTimeout(() => { 639 button.classList.add('active-animation'); 640 }, 50); 641 642 button.dataset.cart_item_id = cartItemId; 643 } 644 645 _ga4CardExtras(button) { 646 const card = button.closest('[data-ga4-item]'); 647 const peers = card 648 ? Array.from(document.querySelectorAll('[data-ga4-item]')).filter((el) => el.getAttribute('data-ga4-source') === card.getAttribute('data-ga4-source')) 649 : []; 650 return { 651 list_position: card ? peers.indexOf(card) + 1 : null, 652 click_source: card ? card.getAttribute('data-ga4-source') : null, 653 }; 654 } 655 656 // La réponse de checkout:add_modal est du HTML : l'item GA4 ne peut venir que des attributs 657 // déjà posés sur la carte par ga4_item_attrs. 658 _pushGa4AddToCart(button) { 659 if (typeof ga4 === 'undefined') { 660 return; 661 } 662 const card = button.closest('[data-ga4-item]'); 663 if (!card) { 664 return; 665 } 666 let item = null; 667 try { 668 item = JSON.parse(card.getAttribute('data-ga4-item')); 669 } catch (error) { 670 return; 671 } 672 if (!item) { 673 return; 674 } 675 ga4.addToCart({ ...item, quantity: 1 }, this._ga4CardExtras(button)); 676 } 677 678 handleAddToCartSuccess(button, dataForDataLayer, cartItemId, cartItemQuantity = 1) { 679 this._paintAddedButton(button, cartItemId); 680 681 this.applyCartChange( 682 'add', 683 { 684 id: cartItemId, 685 best_offer_id: Number(button.dataset.offer_id), 686 quantity: cartItemQuantity, 687 }, 688 { updateHeaderDelta: true } 689 ); 690 691 const item = dataForDataLayer && dataForDataLayer.item; 692 if (typeof ga4 !== 'undefined' && item) { 693 ga4.addToCart(item, this._ga4CardExtras(button)); 694 } 695 if (typeof data_layer_push !== 'undefined' && data_layer_push.record_cart_attribution) { 696 data_layer_push.record_cart_attribution(Number(button.dataset.offer_id), cartItemId); 697 } 698 699 operationsInProgress.delete(button.dataset.offer_id); 700 } 701 702 handleAddToCartError(button, responseOrError) { 703 const data = (responseOrError && responseOrError.data) || (responseOrError && responseOrError.response && responseOrError.response.data) || {}; 704 const message = data.error_message || "Une erreur est survenue lors de l'ajout de l'article au panier"; 705 706 if (typeof openMessageDialog === 'function') { 707 openMessageDialog(message, 'error'); 708 } 709 710 button.classList.remove('waiting'); 711 operationsInProgress.delete(button.dataset.offer_id); 712 } 713} 714 715document.addEventListener('DOMContentLoaded', () => { 716 window.productCardManager = new ProductCardManager(); 717 window.productCardManager.init(); 718});
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.