1/** 2 * 2025 4webs FW_SPBAPI 3 * 4 * @author 4webs 5 * @copyright 4webs 2025 6 * @license 4webs 7 */ 8 9var fwSpbapiPendingAddToCart = null; 10var fwSpbapiPendingProductData = null; 11var fwSpbapiProcessing = false; 12var fwSpbapiEventBlocked = false; 13var fwSpbapiIsAddToCartFlow = false; 14var fwSpbapiExecutingClick = false; 15// True only when the user explicitly clicks "add with insurance" or "without insurance" 16var fwSpbapiInsuranceDecided = false; 17 18function isBannerCheckboxChecked() { 19 var checkbox = document.getElementById('fw-spbapi-banner-checkbox'); 20 return checkbox ? checkbox.checked : false; 21} 22 23function setBannerCheckboxChecked(checked) { 24 var checkbox = document.getElementById('fw-spbapi-banner-checkbox'); 25 if (checkbox) { 26 checkbox.checked = !!checked; 27 } 28} 29 30function extractInsurancePrice(text) { 31 var priceMatch = text.match(/([\d]+[,\.][\d]{2})\s*â¬/) || 32 text.match(/â¬\s*([\d]+[,\.][\d]{2})/) || 33 text.match(/([\d]+[,\.][\d]{2})/); 34 35 if (priceMatch) { 36 return priceMatch[1].replace(',', '.'); 37 } 38 39 var fallbackMatch = text.match(/([\d]+\.[\d]{2})/); 40 return fallbackMatch ? fallbackMatch[1] : '0.00'; 41} 42 43function openModalFromInsuranceIcon(infoIcon) { 44 var openModal = function(retryCount) { 45 retryCount = retryCount || 0; 46 var maxRetries = 20; 47 48 if (typeof $ === 'undefined' || typeof window.fwSpbapiOpenFullModal !== 'function') { 49 if (retryCount < maxRetries) { 50 setTimeout(function() { openModal(retryCount + 1); }, 50); 51 } 52 return; 53 } 54 55 var modalOverlay = document.getElementById('fw-spbapi-modal-overlay'); 56 if (!modalOverlay) { 57 if (retryCount < maxRetries) { 58 setTimeout(function() { openModal(retryCount + 1); }, 50); 59 } 60 return; 61 } 62 63 var $iconElement = $(infoIcon); 64 var $insuranceProduct = $iconElement.closest('.insurance-product'); 65 66 // Only update price if we're on cart/checkout page (has .insurance-product element) 67 // On product page, the price is already correct from PHP, so we don't need to update it 68 if ($insuranceProduct.length > 0) { 69 // Try to get price from data attribute first (already formatted from cart) 70 var insurancePrice = $insuranceProduct.attr('data-insurance-price'); 71 72 // If not available in data attribute, try to extract from text 73 if (!insurancePrice || insurancePrice === '0' || insurancePrice === '0.00' || insurancePrice === '') { 74 var insuranceText = $insuranceProduct.find('.insurance-text').text(); 75 var extractedPrice = extractInsurancePrice(insuranceText); 76 // Format the price if we extracted a numeric value 77 if (extractedPrice && extractedPrice !== '0.00') { 78 // Use PrestaShop's formatCurrency if available, otherwise format manually 79 if (typeof formatCurrency !== 'undefined' && typeof currencyFormat !== 'undefined' && typeof currencySign !== 'undefined') { 80 insurancePrice = formatCurrency(parseFloat(extractedPrice), currencyFormat, currencySign, currencyBlank); 81 } else { 82 // Fallback: simple formatting 83 insurancePrice = extractedPrice.replace('.', ',') + ' â¬'; 84 } 85 } else { 86 insurancePrice = null; 87 } 88 } 89 90 // Update price element only if we have a valid price 91 var $priceElement = $('#fw-spbapi-dynamic-price'); 92 if ($priceElement.length && insurancePrice) { 93 $priceElement.text(insurancePrice); 94 } 95 } 96 97 try { 98 window.fwSpbapiOpenFullModal(); 99 } catch (error) { 100 // Silent fail 101 } 102 }; 103 104 setTimeout(function() { openModal(0); }, 0); 105} 106 107document.addEventListener('click', function(e) { 108 var infoIcon = e.target.closest('.insurance-info-icon'); 109 110 if (infoIcon) { 111 e.preventDefault(); 112 e.stopPropagation(); 113 e.stopImmediatePropagation(); 114 openModalFromInsuranceIcon(infoIcon); 115 } 116}, true); 117 118document.addEventListener('click', function(e) { 119 var banner = document.querySelector('.fw-spbapi-banner-container'); 120 if (!banner) { 121 return; 122 } 123 124 // Without checkbox selected, allow normal add-to-cart (no insurance, no modal) 125 if (!isBannerCheckboxChecked()) { 126 return; 127 } 128 129 var target = e.target.closest('.add-to-cart, [data-button-action="add-to-cart"], .btn-add-to-cart, button[name="add"], button[type="submit"][name="submitAddtocart"], .product-add-to-cart button[type="submit"]'); 130 131 if (!target) { 132 return; 133 } 134 135 // Allow click to proceed if we're executing it programmatically 136 if (fwSpbapiExecutingClick) { 137 return; 138 } 139 140 if (fwSpbapiProcessing) { 141 fwSpbapiEventBlocked = false; 142 return; 143 } 144
145 var urlParams = new URLSearchParams(window.location.search); 146 var productId = urlParams.get('id_product') || 147 target.getAttribute('data-id-product') || 148 document.querySelector('input[name="id_product"]')?.value; 149 150 e.preventDefault(); 151 e.stopPropagation(); 152 e.stopImmediatePropagation(); 153 fwSpbapiEventBlocked = true; 154 155 fwSpbapiPendingAddToCart = target; 156 fwSpbapiPendingProductData = { 157 productId: productId, 158 quantity: document.querySelector('input[name="qty"]')?.value || 1 159 }; 160 161 fwSpbapiProcessing = true; 162 163 document.dispatchEvent(new CustomEvent('fwSpbapiOpenModal', { 164 detail: { 165 button: target, 166 productId: productId 167 } 168 })); 169 170}, true); 171 172// Close modal button handler (native JS - works independently of jQuery) 173document.addEventListener('click', function(e) { 174 var closeBtn = e.target.closest('#fw-spbapi-quick-close-modal'); 175 if (closeBtn) { 176 e.preventDefault(); 177 e.stopPropagation(); 178 e.stopImmediatePropagation(); 179 180 if (typeof window.fwSpbapiCancelModal === 'function') { 181 window.fwSpbapiCancelModal(); 182 } else if (typeof window.fwSpbapiCloseModal === 'function') { 183 window.fwSpbapiCloseModal(); 184 } else { 185 // Fallback if functions don't exist yet 186 var overlay = document.getElementById('fw-spbapi-quick-modal-overlay'); 187 if (overlay) { 188 overlay.classList.remove('active'); 189 document.documentElement.classList.remove('fw-spbapi-modal-open'); 190 document.body.classList.remove('fw-spbapi-modal-open'); 191 } 192 fwSpbapiIsAddToCartFlow = false; 193 fwSpbapiPendingAddToCart = null; 194 fwSpbapiPendingProductData = null; 195 fwSpbapiProcessing = false; 196 fwSpbapiEventBlocked = false; 197 } 198 } 199}, true); 200 201document.addEventListener('fwSpbapiOpenModal', function(e) { 202 fwSpbapiIsAddToCartFlow = true; 203 204 setTimeout(function() { 205 var overlay = document.getElementById('fw-spbapi-quick-modal-overlay'); 206 207 if (overlay) { 208 overlay.classList.add('active'); 209 document.documentElement.classList.add('fw-spbapi-modal-open'); 210 document.body.classList.add('fw-spbapi-modal-open'); 211 212 if (typeof $ !== 'undefined') { 213 $('#fw-spbapi-quick-modal-overlay').addClass('active'); 214 $('html, body').addClass('fw-spbapi-modal-open'); 215 } 216 } 217 218 if (typeof window.fwSpbapiOpenQuickModal === 'function') { 219 window.fwSpbapiOpenQuickModal(); 220 } 221 }, 100); 222}); 223 224(function() { 225 function waitForJQuery(callback) { 226 if (typeof jQuery !== 'undefined' && typeof $ !== 'undefined') { 227 callback(); 228 } else { 229 setTimeout(function() { 230 waitForJQuery(callback); 231 }, 50); 232 } 233 } 234 235 waitForJQuery(function() { 236 $(document).ready(function() { 237 if (typeof $ === 'undefined') { 238 return; 239 } 240 241 function getProductId($button) { 242 var productId = $button.data('id-product') || 243 $button.closest('[data-id-product]').data('id-product') || 244 $button.attr('data-id-product'); 245 246 if (!productId) { 247 var urlParams = new URLSearchParams(window.location.search); 248 productId = urlParams.get('id_product'); 249 } 250 251 if (!productId) { 252 var $productInput = $('input[name="id_product"]'); 253 if ($productInput.length) { 254 productId = $productInput.val(); 255 } 256 } 257 258 return parseInt(productId); 259 } 260 261 function isProductEligible() { 262 return $('.fw-spbapi-banner-container').length > 0;
263 } 264 265 function interceptAddToCart(e, $button) { 266 // Allow click to proceed if we're executing it programmatically 267 if (fwSpbapiExecutingClick) { 268 return true; 269 } 270 271 if (fwSpbapiEventBlocked) { 272 fwSpbapiEventBlocked = false; 273 } 274 275 if (fwSpbapiProcessing) { 276 return true; 277 } 278 279 if (!isProductEligible()) { 280 return true; 281 } 282 283 // Without checkbox selected, allow normal add-to-cart (no insurance, no modal) 284 if (!isBannerCheckboxChecked()) { 285 return true; 286 } 287 288 var productId = getProductId($button); 289 290 if (!productId) { 291 return true; 292 } 293 294 fwSpbapiPendingAddToCart = $button; 295 fwSpbapiPendingProductData = { 296 productId: productId, 297 quantity: $('input[name="qty"]').val() || 1 298 }; 299 300 e.preventDefault(); 301 e.stopPropagation(); 302 e.stopImmediatePropagation(); 303 304 fwSpbapiProcessing = true; 305 window.fwSpbapiOpenQuickModal(); 306 307 return false; 308 } 309 310 var addToCartSelectors = [ 311 '.add-to-cart', 312 '[data-button-action="add-to-cart"]', 313 '.btn-add-to-cart', 314 'button[name="add"]', 315 'button[type="submit"][name="submitAddtocart"]', 316 '.product-add-to-cart button[type="submit"]' 317 ]; 318 319 $(document).on('click', addToCartSelectors.join(', '), function(e) { 320 return interceptAddToCart(e, $(this)); 321 }); 322 323 $('form#add-to-cart-or-refresh, form[action*="cart"]').on('submit', function(e) { 324 if (!isProductEligible()) { 325 return true; 326 } 327 328 if (!isBannerCheckboxChecked()) { 329 return true; 330 } 331 332 // Allow click to proceed if we're executing it programmatically 333 if (fwSpbapiExecutingClick) { 334 return true; 335 } 336 337 if (fwSpbapiProcessing) { 338 return true; 339 } 340 341 var $submitBtn = $(this).find('button[type="submit"]'); 342 return interceptAddToCart(e, $submitBtn); 343 }); 344 345 var observeAddToCartButton = function() { 346 var targetNode = document.querySelector('.product-add-to-cart') || 347 document.querySelector('.product-actions'); 348 349 if (!targetNode) return; 350 351 var observer = new MutationObserver(function(mutations) { 352 mutations.forEach(function(mutation) { 353 if (mutation.addedNodes.length) { 354 $(addToCartSelectors.join(', ')).off('click.fwspbapi').on('click.fwspbapi', function(e) { 355 return interceptAddToCart(e, $(this)); 356 }); 357 } 358 }); 359 }); 360 361 observer.observe(targetNode, { childList: true, subtree: true }); 362 }; 363 364 observeAddToCartButton(); 365 366 var quickModalOverlay = $('#fw-spbapi-quick-modal-overlay'); 367 var quickModal = $('#fw-spbapi-quick-modal'); 368 var quickAddWithInsuranceBtn = $('#fw-spbapi-quick-add-with-insurance'); 369 var quickAddWithoutInsuranceBtn = $('#fw-spbapi-quick-add-without-insurance'); 370 var viewFullDetailsBtn = $('#fw-spbapi-view-full-details'); 371 var fullModalOverlay = $('#fw-spbapi-modal-overlay'); 372 var fullModal = $('#fw-spbapi-modal'); 373 var closeFullModalBtn = $('#fw-spbapi-close-modal'); 374 375 window.fwSpbapiOpenQuickModal = function() { 376 fwSpbapiIsAddToCartFlow = true; 377 fwSpbapiInsuranceDecided = false; 378
379 var overlay = document.getElementById('fw-spbapi-quick-modal-overlay'); 380 if (overlay) { 381 overlay.classList.add('active'); 382 document.documentElement.classList.add('fw-spbapi-modal-open'); 383 document.body.classList.add('fw-spbapi-modal-open'); 384 } 385 386 quickModalOverlay.addClass('active'); 387 $('html, body').addClass('fw-spbapi-modal-open'); 388 }; 389 390 window.fwSpbapiOpenFullModal = function() { 391 fwSpbapiIsAddToCartFlow = false; 392 fullModalOverlay.addClass('active'); 393 $('html, body').addClass('fw-spbapi-modal-open'); 394 }; 395 396 window.fwSpbapiCloseModal = function() { 397 // If the quick modal is being closed without an explicit choice 398 // (X button, click outside, Escape, etc.) and there is a pending 399 // add-to-cart, automatically add the product without insurance. 400 if (fwSpbapiIsAddToCartFlow && 401 !fwSpbapiInsuranceDecided && 402 fwSpbapiPendingAddToCart && 403 fwSpbapiPendingProductData && 404 !fwSpbapiExecutingClick) { 405 406 fwSpbapiInsuranceDecided = true; // prevent recursive trigger 407 setBannerCheckboxChecked(false); 408 proceedWithAddToCart(false); 409 return; // proceedWithAddToCart will call fwSpbapiCloseModal() again 410 } 411 412 var quickOverlay = document.getElementById('fw-spbapi-quick-modal-overlay'); 413 var fullOverlay = document.getElementById('fw-spbapi-modal-overlay'); 414 415 if (quickOverlay) { 416 quickOverlay.classList.remove('active'); 417 } 418 if (fullOverlay) { 419 fullOverlay.classList.remove('active'); 420 } 421 422 document.documentElement.classList.remove('fw-spbapi-modal-open'); 423 document.body.classList.remove('fw-spbapi-modal-open'); 424 425 quickModalOverlay.removeClass('active'); 426 fullModalOverlay.removeClass('active'); 427 $('html, body').removeClass('fw-spbapi-modal-open'); 428 429 fwSpbapiIsAddToCartFlow = false; 430 fwSpbapiInsuranceDecided = false; 431 // Don't clear pending data here if we're about to execute the click 432 if (!fwSpbapiExecutingClick) { 433 fwSpbapiPendingAddToCart = null; 434 fwSpbapiPendingProductData = null; 435 } 436 if (!fwSpbapiExecutingClick) { 437 fwSpbapiProcessing = false; 438 } 439 }; 440 441 function cancelModal() { 442 window.fwSpbapiCloseModal(); 443 fwSpbapiEventBlocked = false; 444 } 445 446 window.fwSpbapiCancelModal = function() { 447 cancelModal(); 448 }; 449 450 function proceedWithAddToCart(withInsurance) { 451 if (!fwSpbapiPendingAddToCart || !fwSpbapiPendingProductData) { 452 setBannerCheckboxChecked(!!withInsurance); 453 window.fwSpbapiCloseModal(); 454 return; 455 } 456 457 setBannerCheckboxChecked(!!withInsurance); 458 459 // Get the form 460 var $form = fwSpbapiPendingAddToCart.jquery ? 461 fwSpbapiPendingAddToCart.closest('form') : 462 $(fwSpbapiPendingAddToCart).closest('form'); 463 464 if (!$form.length) { 465 $form = $('#add-to-cart-or-refresh, form[action*="cart"]').first(); 466 } 467 468 // Always remove any existing insurance input first 469 if ($form.length) { 470 $form.find('input[name="fw_spbapi_add_insurance"]').remove(); 471 472 // Only add the input if user wants insurance 473 if (withInsurance) { 474 $form.append('<input type="hidden" name="fw_spbapi_add_insurance" value="1">'); 475 } 476 } 477 478 var pendingButton = fwSpbapiPendingAddToCart; 479 480 // Clear pending data before executing click 481 fwSpbapiPendingAddToCart = null; 482 fwSpbapiPendingProductData = null; 483 484 window.fwSpbapiCloseModal(); 485 486 setTimeout(function() { 487 if (!pendingButton) { 488 fwSpbapiProcessing = false; 489 fwSpbapiExecutingClick = false; 490 return; 491 } 492 493 // Set flag to allow click to proceed without interception 494 fwSpbapiExecutingClick = true; 495 fwSpbapiProcessing = false; 496 497 // Execute the click 498 if (pendingButton.jquery) { 499 pendingButton.trigger('click'); 500 } else { 501 pendingButton.click(); 502 } 503 504 // Reset flags after a short delay 505 setTimeout(function() { 506 fwSpbapiExecutingClick = false; 507 fwSpbapiProcessing = false; 508 }, 500); 509 }, 100); 510 } 511 512 quickAddWithInsuranceBtn.on('click', function(e) { 513 e.preventDefault(); 514 e.stopPropagation(); 515 fwSpbapiInsuranceDecided = true; 516 proceedWithAddToCart(true); 517 }); 518 519 quickAddWithoutInsuranceBtn.on('click', function(e) { 520 e.preventDefault(); 521 e.stopPropagation(); 522 fwSpbapiInsuranceDecided = true; 523 proceedWithAddToCart(false); 524 }); 525 526 viewFullDetailsBtn.on('click', function(e) { 527 e.preventDefault(); 528 e.stopPropagation(); 529 quickModalOverlay.removeClass('active'); 530 window.fwSpbapiOpenFullModal(); 531 }); 532 533 quickModalOverlay.on('click', function(e) { 534 if ($(e.target).is('#fw-spbapi-quick-modal-overlay')) { 535 cancelModal(); 536 } 537 }); 538 539 quickModal.on('click', function(e) { 540 e.stopPropagation(); 541 }); 542 543 $(document).on('click', '#fw-spbapi-open-modal', function(e) { 544 e.preventDefault(); 545 e.stopPropagation(); 546 window.fwSpbapiOpenFullModal(); 547 }); 548
549 function handleInsuranceInfoIconClick($iconElement) { 550 var $insuranceProduct = $iconElement.closest('.insurance-product'); 551 552 // Only update price if we're on cart/checkout page (has .insurance-product element) 553 // On product page, the price is already correct from PHP, so we don't need to update it 554 if ($insuranceProduct.length > 0) { 555 // Try to get price from data attribute first (already formatted from cart) 556 var insurancePrice = $insuranceProduct.attr('data-insurance-price'); 557 558 // If not available in data attribute, try to extract from text 559 if (!insurancePrice || insurancePrice === '0' || insurancePrice === '0.00' || insurancePrice === '') { 560 var insuranceText = $insuranceProduct.find('.insurance-text').text(); 561 var extractedPrice = extractInsurancePrice(insuranceText); 562 // Format the price if we extracted a numeric value 563 if (extractedPrice && extractedPrice !== '0.00') { 564 // Use PrestaShop's formatCurrency if available, otherwise format manually 565 if (typeof formatCurrency !== 'undefined' && typeof currencyFormat !== 'undefined' && typeof currencySign !== 'undefined') { 566 insurancePrice = formatCurrency(parseFloat(extractedPrice), currencyFormat, currencySign, currencyBlank); 567 } else { 568 // Fallback: simple formatting 569 insurancePrice = extractedPrice.replace('.', ',') + ' â¬'; 570 } 571 } else { 572 insurancePrice = null; 573 } 574 } 575 576 // Update price element only if we have a valid price 577 var $priceElement = $('#fw-spbapi-dynamic-price'); 578 if ($priceElement.length && insurancePrice) { 579 $priceElement.text(insurancePrice); 580 } 581 } 582 583 window.fwSpbapiOpenFullModal(); 584 } 585 586 document.addEventListener('fwSpbapiInfoIconClick', function(e) { 587 if (typeof $ !== 'undefined') { 588 handleInsuranceInfoIconClick($(e.detail.element)); 589 } 590 }); 591 592 $(document).on('click', '.insurance-info-icon', function(e) { 593 e.preventDefault(); 594 e.stopPropagation(); 595 e.stopImmediatePropagation(); 596 handleInsuranceInfoIconClick($(this)); 597 return false; 598 }); 599 600 closeFullModalBtn.on('click', function(e) { 601 e.preventDefault(); 602 e.stopPropagation(); 603 window.fwSpbapiCloseModal(); 604 }); 605 606 fullModalOverlay.on('click', function(e) { 607 if ($(e.target).is('#fw-spbapi-modal-overlay')) { 608 window.fwSpbapiCloseModal(); 609 } 610 }); 611 612 fullModal.on('click', function(e) { 613 e.stopPropagation(); 614 }); 615 616 $(document).on('keydown', function(e) { 617 if (e.key === 'Escape') { 618 if (quickModalOverlay.hasClass('active') && fwSpbapiIsAddToCartFlow) { 619 cancelModal(); 620 } else if (fullModalOverlay.hasClass('active')) { 621 window.fwSpbapiCloseModal(); 622 } 623 } 624 }); 625 626 // Remove insurance from cart/checkout product line checkbox (enable not allowed) 627 $(document).on('change', '.insurance-checkbox', function() { 628 var $checkbox = $(this); 629 var $insuranceProduct = $checkbox.closest('.insurance-product'); 630 var idProduct = parseInt($insuranceProduct.data('product-id'), 10); 631 var idCustomization = parseInt($insuranceProduct.data('customization-id'), 10) || 0; 632 var enable = $checkbox.is(':checked') ? 1 : 0; 633 634 if (!idProduct) { return; } 635 636 // Cart checkbox is only for removing insurance; re-adding is done on the product page 637 if (enable) { 638 $checkbox.prop('checked', false); 639 return; 640 } 641 642 $checkbox.prop('disabled', true); 643 644 var ajaxUrl = (typeof prestashop !== 'undefined' && prestashop.urls && prestashop.urls.base_url) 645 ? prestashop.urls.base_url + 'module/fw_spbapi/ajax' 646 : '/module/fw_spbapi/ajax'; 647 648 $.ajax({
649 url: ajaxUrl, 650 type: 'POST', 651 data: { 652 action: 'toggleInsurance', 653 id_product: idProduct, 654 id_customization: idCustomization, 655 enable: 0 656 }, 657 dataType: 'json', 658 success: function(response) { 659 if (response.success) { 660 if (typeof prestashop !== 'undefined') { 661 prestashop.emit('updateCart', { reason: { action: 'insurance-toggle' } }); 662 } else { 663 window.location.reload(); 664 } 665 } else { 666 $checkbox.prop('checked', true); 667 $checkbox.prop('disabled', false); 668 } 669 }, 670 error: function() { 671 $checkbox.prop('checked', true); 672 $checkbox.prop('disabled', false); 673 } 674 }); 675 }); 676 }); 677 }); 678})(); 679
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.