1// GENERAL 2 3var position = $(window).scrollTop(); 4jQuery(window).scroll(function () { 5 6 if (jQuery(this).scrollTop() > 115) { 7 $('.bbs_submenu__mobile').addClass('fixed'); 8 $('#products').css('padding-top', '50px'); 9 } else { 10 $('.bbs_submenu__mobile').removeClass('fixed'); 11 $('#products').css('padding-top', '0'); 12 } 13 14}); 15 16// PRODUCT PAGE 17 18$(document).ready(function () { 19 20 $(document).on('click', '#arrow_scroll_to_top', function (e) { 21 e.preventDefault(); // empêche un comportement par défaut, si câest un lien 22 23 $('html, body').animate({ 24 scrollTop: 0 25 }, 600); // durée du scroll en ms (ajuste à ta convenance) 26 }); 27 28 29 $(document).on('click', '#go_to_description', function (e) { 30 e.preventDefault(); 31 const $target = $('.section.description'); 32 if ($target.length) { 33 const offset = $target.offset().top - 60; 34 $('html, body').animate({ scrollTop: offset }, 1000); 35 } 36 }); 37 38 39 $(document).on('dblclick', '.titles_ref_location .reference', function() { 40 var sku = $(this).data('sku'); 41 copyTextToClipboard(sku); 42 }); 43 44 45 46 function scrollToSkeepers() { 47 var target = $('.skeepers_product__reviews'); 48 if (target.length) { 49 $('html, body').animate({ 50 scrollTop: target.offset().top - 250 51 }, 800); 52 } 53 } 54 55 // --- PARTIE A : Interception du clic (Catégorie OU Fiche produit) --- 56 document.addEventListener('click', function(e) { 57 var $link = $(e.target).closest('a[href*="#skeepers_product_widget"]'); 58 59 if ($link.length > 0) { 60 // On enregistre dans la session qu'on veut scroller après le chargement 61 sessionStorage.setItem('pendingSkeepersScroll', 'true'); 62 63 // Si on est déjà sur la même page (clic depuis le haut de la fiche produit) 64 if (window.location.pathname === $link[0].pathname || $link[0].pathname === "") { 65 e.preventDefault(); 66 scrollToSkeepers(); 67 sessionStorage.removeItem('pendingSkeepersScroll'); // Déjà fait, on nettoie 68 } 69 } 70 }, true); 71 72 // --- PARTIE B : Au chargement de la nouvelle page --- 73 // On vérifie soit le Hash, soit notre "mémoire" sessionStorage 74 if (window.location.hash === '#skeepers_product_widget' || sessionStorage.getItem('pendingSkeepersScroll') === 'true') { 75 76 // On attend que Skeepers injecte ses éléments (parfois long) 77 var checkSkeepersExist = setInterval(function() { 78 if ($('.skeepers_product__reviews').length) { 79 clearInterval(checkSkeepersExist); 80 scrollToSkeepers(); 81 sessionStorage.removeItem('pendingSkeepersScroll'); 82 } 83 }, 400); 84 85 // Sécurité : on arrête de chercher après 5 secondes si le widget ne vient pas 86 setTimeout(function() { 87 clearInterval(checkSkeepersExist); 88 sessionStorage.removeItem('pendingSkeepersScroll'); 89 }, 5000); 90 } 91 92 93 94 95 var $thumbs = $('.thumbs'); 96 var $arrowUp = $('.arrow_up'); 97 var $arrowDown = $('.arrow_down'); 98 99 function checkArrowsVisibility() { 100 var scrollTop = $thumbs.scrollTop(); 101 var scrollHeight = $thumbs.prop('scrollHeight'); 102 var containerHeight = $thumbs.outerHeight(); 103 104 // Affiche ou masque la flèche du haut 105 if (scrollTop > 10) { 106 $arrowUp.fadeIn(); 107 } else { 108 $arrowUp.fadeOut(); 109 } 110 111 // Affiche ou masque la flèche du bas 112 if (scrollTop + containerHeight >= scrollHeight - 1) { 113 $arrowDown.fadeOut(); 114 } else { 115 $arrowDown.fadeIn(); 116 } 117 } 118 119 $thumbs.on('scroll', checkArrowsVisibility); 120 121 // Gérer les clics 122 $arrowUp.on('click', function () { 123 $thumbs.animate({ scrollTop: 0 }, 300, checkArrowsVisibility); 124 }); 125 126 $arrowDown.on('click', function () { 127 $thumbs.animate({ scrollTop: $thumbs.scrollTop() + 200 }, 300, checkArrowsVisibility); 128 }); 129 130 // Appel initial pour afficher correctement les flèches 131 checkArrowsVisibility(); 132 133 134 let zoomX = 50, zoomY = 50; 135 let rafId = null; 136 137 // Gestion fluide du zoom avec requestAnimationFrame 138 function updateZoomPosition($zoomContainer) { 139 $zoomContainer.css('background-position', `${zoomX}% ${zoomY}%`); 140 rafId = null; 141 } 142 143 // MOUVEMENT DE SOURIS SUR ZOOM 144 $(document).on('mousemove', '.zoom_on_hover', function (e) { 145 const $zoomContainer = $(this); 146 const offset = $zoomContainer.offset(); 147 const width = $zoomContainer.width(); 148 const height = $zoomContainer.height(); 149 150 zoomX = ((e.pageX - offset.left) / width) * 100; 151 zoomY = ((e.pageY - offset.top) / height) * 100; 152 153 $('.custom_cursor').css({ 154 left: e.pageX - offset.left + 'px', 155 top: e.pageY - offset.top + 'px', 156 display: 'block' 157 }); 158 159 $zoomContainer.addClass('zoom-active'); 160 161 if (!rafId) { 162 rafId = requestAnimationFrame(() => updateZoomPosition($zoomContainer)); 163 } 164 }); 165 166 // SORTIE DE ZOOM 167 $(document).on('mouseleave', '.zoom_on_hover', function () { 168 $('.custom_cursor').hide(); 169 $(this).removeClass('zoom-active'); 170 }); 171 172 // CHANGEMENT DâIMAGE PRINCIPALE 173 $(document).on('click', '.thumbs .thumb', function () {
174 const newZoom = $(this).find('img').data('zoom'); 175 176 const $zoomContainer = $('.zoom_on_hover'); 177 178 // Met à jour l'image visible et celle du zoom 179 // (jQuery 1.11 camélise les noms de propriété dans .css(), ce qui casse 180 // l'écriture d'une custom property CSS : on passe par setProperty natif) 181 $zoomContainer.each(function () { 182 this.style.setProperty('--zoom-image', `url('${newZoom}')`); 183 }); 184 $zoomContainer.find('img').attr('src', newZoom); 185 }); 186 187 188 189 if (typeof Swiper !== 'undefined' && $('.swiper').length) { 190 var swiper = new Swiper('.swiper', { 191 direction: 'horizontal', 192 slidesPerView: 1, 193 spaceBetween: 10, 194 loop: false, 195 pagination: { 196 el: '.swiper-pagination', 197 clickable: true, 198 }, 199 }); 200 swiper.on('slideChange', function () { 201 const bullets = document.querySelectorAll('.swiper-pagination-bullet'); 202 bullets.forEach(b => b.classList.remove('swiper-pagination-bullet-active')); 203 if (bullets[swiper.realIndex]) { 204 bullets[swiper.realIndex].classList.add('swiper-pagination-bullet-active'); 205 } 206 }); 207 } 208 209 210 if (prestashop.page.page_name == 'product') { 211 212 let columnInitialTop; 213 214 function updateColumnTop() { 215 columnInitialTop = $('.product_page_layout').offset().top; 216 } 217 218 $(window).on('scroll', function () { 219 const column = $('.product_page_layout .right_column .lift_container'); 220 const scrollTop = $(window).scrollTop(); 221 222 if (scrollTop >= columnInitialTop - 130) { 223 column.addClass('no_move'); 224 } else { 225 column.removeClass('no_move'); 226 } 227 }); 228 229 $(window).on('resize', updateColumnTop); 230 $(document).ready(updateColumnTop); 231 232 } 233 234 235 236 $(function () { 237 const $sections = $(".section"); 238 const animSpeed = 500; // durée en ms 239 240 // Masquer tout au départ 241 $sections.find(".content").hide(); 242 243 // Ouvrir par défaut how_to et description 244 $(".section.how_to, .section.description").each(function () { 245 $(this).addClass("open").find(".content").show(); 246 }); 247 248 // ð Ouvrir toutes les sections par défaut sur desktop 249 if (window.innerWidth > 768) { 250 $sections.each(function () { 251 if (!$(this).hasClass("open")) { 252 $(this).addClass("open").find(".content").show(); 253 } 254 }); 255 } 256 257 $(".section .name").on("click", function () { 258 const $section = $(this).closest(".section"); 259 const $name = $section.find(".name"); 260 261 if ($section.hasClass("open")) { 262 // Fermer si déjà ouverte 263 $section.removeClass("open") 264 .find(".content").slideUp(animSpeed, "swing"); 265 } else { 266 // ð Exception : on ne ferme PAS how_to ni description 267 if (!$section.hasClass("how_to") && !$section.hasClass("description")) { 268 $sections.not(".how_to, .description").removeClass("open") 269 .find(".content").slideUp(animSpeed, "swing"); 270 } 271 272 // Ouvrir celle cliquée 273 $section.addClass("open") 274 .find(".content").slideDown(animSpeed, "swing", function () { 275 const isMobile = window.innerWidth <= 768; 276 const topOffset = isMobile ? 60 : 120; 277 278 const scrollTarget = $name.offset().top - topOffset; 279 $('html, body').animate({ scrollTop: scrollTarget }, 400); 280 }); 281 } 282 }); 283 }); 284 285 286 287 288 289 290 291 292 293 $(function() { 294 const $container = $('.tab_comp_container'); 295 const $arrow = $('.tab_comp_container .arrow_right'); 296 297 $container.on('scroll', function() { 298 if ($(this).scrollLeft() > 0) { 299 $arrow.fadeOut(200); 300 } else { 301 $arrow.fadeIn(200); 302 } 303 }); 304 }); 305 306 307 $(function() { 308 if ($(window).width() <= 768) { // Mobile uniquement 309 const $addToCart = $('.add_to_cart_btn'); 310 const $mobileBtn = $('.add_to_cart_mobile_bottom'); 311 const headerHeight = 56; // hauteur du header fixed 312 313 if (!$addToCart.length || !$mobileBtn.length) return; 314 315 $(window).on('scroll', function() { 316 const cartRect = $addToCart[0].getBoundingClientRect(); 317 const inViewport = 318 cartRect.top < window.innerHeight && // une partie en bas 319 cartRect.bottom > headerHeight; // une partie au-dessus du header 320 321 if (!inViewport) { 322 $mobileBtn.removeClass('go_down'); // monte pour être visible 323 $('#launcher, #arrow_scroll_to_top, #bpfelix-root, #bpfelix-root .bpf-widget').addClass('go_up'); 324 $('#64300f8f-5e89-4a0d-b6d4-dc4953270a92').hide(); 325 } else { 326 $mobileBtn.addClass('go_down');
326 // descend pour disparaître 327 $('#launcher, #arrow_scroll_to_top, #bpfelix-root, #bpfelix-root .bpf-widget').removeClass('go_up'); 328 $('#64300f8f-5e89-4a0d-b6d4-dc4953270a92').show(); 329 } 330 }); 331 } 332 }); 333 334 335 /* 336 function checkHeight() { 337 let windowWidth = $(window).width(); 338 let windowHeight = $(window).height(); 339 if (windowWidth > 768 && windowHeight < 920) { 340 $('.lift_container').addClass('small_screen'); 341 } else { 342 $('.lift_container').removeClass('small_screen'); 343 } 344 } 345 // Vérification au chargement 346 checkHeight(); 347 // Vérification au redimensionnement 348 $(window).on('resize', function() { 349 checkHeight(); 350 }); 351 */ 352 353 354 /* CATEGORIES */ 355 356 document.addEventListener('click', function(e) { 357 var $target = $(e.target).closest('#category .pic_pdt_price_add_to_cart .rating'); 358 if ($target.length > 0) { 359 var url = $target.attr('href'); 360 if (url) { 361 window.location.href = url; 362 } 363 } 364 }, true); 365 366 367 368 369 /* CREATE ACCOUNT SIRET */ 370 371 if (prestashop.page.page_name == 'registration' || prestashop.page.page_name == 'checkout') { 372 373 function updateSiretRequirement() { 374 375 const selectedGender = $('input[name="id_gender"]:checked').val(); 376 const companyFilled = $.trim($('input[name="company"]').val()).length > 0; 377 378 const mustBeRequired = (selectedGender === "3") || companyFilled; 379 380 const siretInput = $('input[name="siret"]'); 381 const siretLabel = siretInput.closest('.form-group').find('label'); 382 const siretComment = siretInput.closest('.form-group').find('.form-control-comment'); 383 384 const companyInput = $('input[name="company"]'); 385 const companyLabel = companyInput.closest('.form-group').find('label'); 386 const companyComment = companyInput.closest('.form-group').find('.form-control-comment'); 387 388 if (mustBeRequired) { 389 // Ajouter required 390 companyInput.attr('required', true); 391 siretInput.attr('required', true); 392 393 // Forcer 9 ou 14 chiffres 394 siretInput.attr('pattern', '^(\\d{9}|\\d{14})$'); 395 siretInput.attr('title', 'Le SIRET/SIREN doit contenir 9 ou 14 chiffres.'); 396 397 companyLabel.addClass('required'); 398 siretLabel.addClass('required'); 399 400 // Enlever "Optionnel" 401 companyComment.text(''); 402 siretComment.text(''); 403 } else { 404 // Enlever required 405 companyInput.removeAttr('required'); 406 companyLabel.removeClass('required'); 407 siretInput.removeAttr('required'); 408 siretInput.removeAttr('pattern'); 409 siretInput.removeAttr('title'); 410 siretLabel.removeClass('required'); 411 412 // Remettre le texte "Optionnel" 413 companyComment.text('Optionnel'); 414 siretComment.text('Optionnel'); 415 } 416 } 417 418 // Déclencheur : changement de genre 419 $('input[name="id_gender"]').on('change', updateSiretRequirement); 420 421 // Déclencheur : saisie du champ Société 422 $('input[name="company"]').on('keyup change', updateSiretRequirement); 423 424 // Initialisation si le formulaire est pré-rempli 425 updateSiretRequirement(); 426 427 } 428 429}); 430 431function copyTextToClipboard(text) { 432 if (!navigator.clipboard) { 433 fallbackCopyTextToClipboard(text); 434 return; 435 } 436 navigator.clipboard.writeText(text).then(function() { 437 }, function(err) { 438 }); 439} 440 441 442// NEW ADDRESS 443if (prestashop.page.page_name === 'address' || prestashop.page.page_name === 'checkout') { 444 let firstname = $('input[name=firstname]'); 445 let lastname = $('input[name=lastname]'); 446 447 function sanitizeName(input) { 448 input.on('keyup blur paste', function () { 449 let val = $(this).val(); 450 451 // 1. Remplace les majuscules accentuées par leur équivalent non accentué 452 const accentedUpperMap = { 453 'Ã': 'A', 'Ã': 'A', 'Ã': 'A', 'Ã': 'A', 'Ã': 'A', 'à ': 'A', 454 'Ã': 'E', 'Ã': 'E', 'Ã': 'E', 'Ã': 'E', 455 'Ã': 'I', 'Ã': 'I', 'Ã': 'I', 'Ã': 'I', 456 'Ã': 'O', 'Ã': 'O', 'Ã': 'O', 'Ã': 'O', 'Ã': 'O', 457 'Ã': 'U', 'Ã': 'U', 'Ã': 'U', 'Ã': 'U', 458 'Ã': 'Y', 'Ÿ': 'Y', 459 'Ã': 'N', 'Ã': 'C', 'Å': 'OE', 'Ã': 'AE' 460 }; 461 val = val.replace(/./g, function (char) { 462 return accentedUpperMap[char] || char; 463 }); 464 465 // 2. Ne garde que : lettres simples A-Z, lettres minuscules accentuées,
466 // espaces, tirets et apostrophes (pour les noms composés) 467 val = val.replace(/[^A-Za-zà áâãäåèéêëìÃîïòóôõöùúûüýÿñçÅæ\s\-']/g, ''); 468 469 // 3. Empêche les doubles espaces / tirets 470 val = val.replace(/[\s]{2,}/g, ' '); 471 val = val.replace(/[-]{2,}/g, '-'); 472 473 $(this).val(val); 474 }); 475 } 476 477 sanitizeName(firstname); 478 sanitizeName(lastname); 479} 480 481// POSTCODE NO FRANCE 482function checkPostcodeFranceOutreMer(e, $scope) { 483 // 1. On récupère les valeurs au moment du clic sur "Enregistrer" (dans le bloc adresse concerné uniquement) 484 let $postcode = $scope.find('input[name=postcode]').first(); 485 let $country = $scope.find('select[name=id_country]').first(); 486 let postcode = $postcode.val() || ''; 487 let countryId = $country.val(); 488 489 // 2. On vérifie la condition (97 ou 98 + France) 490 if ((postcode.startsWith('97') || postcode.startsWith('98')) && countryId === "8") { 491 492 // 3. On bloque l'envoi du formulaire 493 e.preventDefault(); 494 e.stopPropagation(); 495 if (typeof e.stopImmediatePropagation === 'function') { 496 e.stopImmediatePropagation(); 497 } 498 499 // 4. Alerte utilisateur 500 alert("Erreur : Un code postal commençant par 97 ou 98 n'est pas compatible avec le pays 'France'. Veuillez sélectionner le département/territoire correspondant (Guadeloupe, Martinique, Nouvelle-Calédonie, etc.) ou modifier votre code postal."); 501 502 // 5. Optionnel : On met en évidence les champs en erreur 503 $postcode.add($country).css('border', '2px solid red'); 504 505 return false; 506 } 507} 508 509// Note : dans le tunnel de commande (module brico_checkout_custom), le bloc adresse est 510// entouré d'un <form> supplémentaire alors que le partial address-form.tpl rendu à l'intérieur 511// contient déjà son propre <form> (et un 3e <form> imbriqué pour le bouton "Continuer"). 512// Ce HTML invalide (formulaires imbriqués) rend l'événement natif 'submit' peu fiable selon 513// le navigateur. On se base donc sur le clic du bouton d'envoi, et on cherche les champs via 514// l'ancêtre '.js-address-form' (une simple div, non affectée par ce problème d'imbrication) 515// plutôt que via closest('form'). 516$(document).on('submit', '.js-address-form form', function(e) { 517 return checkPostcodeFranceOutreMer(e, $(this).closest('.js-address-form')); 518}); 519$(document).on('click', '.js-address-form button[type=submit]', function(e) { 520 return checkPostcodeFranceOutreMer(e, $(this).closest('.js-address-form')); 521}); 522 523 524function initCaptchaProtection(formSelector, verifyUrl) { 525 var captchaVerified = false; 526 var form = document.querySelector(formSelector); 527 528 if (!form) return; 529 530 window.onRecaptchaSuccess = function() { 531 captchaVerified = false; // On force quand même la vérif serveur 532 var captchaDiv = form.querySelector('.g-recaptcha'); 533 var errorDiv = form.querySelector('.captcha-error'); 534 if (errorDiv) errorDiv.style.display = 'none'; 535 if (captchaDiv) captchaDiv.style.border = 'none'; 536 }; 537 538 window.onRecaptchaExpired = function() { 539 captchaVerified = false; 540 }; 541 542 form.addEventListener('submit', function(e) { 543 if (captchaVerified) return true; 544 545 var recaptchaResponse = form.querySelector('[name="g-recaptcha-response"]').value; 546 var errorDiv = form.querySelector('.captcha-error'); 547 var captchaDiv = form.querySelector('.g-recaptcha'); 548 549 if (!recaptchaResponse) { 550 e.preventDefault(); 551 e.stopImmediatePropagation(); 552 553 if (errorDiv) errorDiv.style.display = 'block'; 554 if (captchaDiv) { 555 captchaDiv.style.border = '2px solid #ff4444'; 556 captchaDiv.style.borderRadius = '4px'; 557 captchaDiv.scrollIntoView({ behavior: 'smooth', block: 'center' }); 558 } 559 return false; 560 } 561 562 e.preventDefault(); 563 e.stopImmediatePropagation(); 564 565 var honeypot = form.querySelector('[name="url"]') ? form.querySelector('[name="url"]').value : ''; 566 var xhr = new XMLHttpRequest(); 567 xhr.open('POST', verifyUrl, true); 568 xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); 569 xhr.onreadystatechange = function() { 570 if (xhr.readyState === 4 && xhr.status === 200) { 571 var result = JSON.parse(xhr.responseText); 572 if (result.success) { 573 captchaVerified = true; 574 form.submit(); 575 } else { 576 if (errorDiv) errorDiv.style.display = 'block'; 577 grecaptcha.reset(); 578 } 579 } 580 }; 581 xhr.send('g-recaptcha-response=' + encodeURIComponent(recaptchaResponse) + '&url=' + encodeURIComponent(honeypot)); 582 583 return false;
584 }, true); 585} 586 587 588 589/* BBS SUBMENU */ 590$(function () { 591 592 /* desktop: tab switches visible chip rail on hover */ 593 $(document).on('mouseenter', '.bbs_tab[data-group]', function () { 594 var $root = $(this).closest('.bbs_submenu'); 595 var g = $(this).data('group'); 596 $root.find('.bbs_tab').removeClass('is-active'); 597 $(this).addClass('is-active'); 598 $root.find('.bbs_rail').removeClass('is-active'); 599 $root.find('.bbs_rail[data-group="' + g + '"]').addClass('is-active'); 600 }); 601 602 /* mobile: open/close drawer */ 603 $(document).on('click', '.bbs_mbar', function (e) { 604 e.stopPropagation(); 605 $(this).closest('.bbs_msheet').toggleClass('is-open'); 606 }); 607 $(document).on('click', function (e) { 608 if (!$(e.target).closest('.bbs_msheet').length) { 609 $('.bbs_msheet.is-open').removeClass('is-open'); 610 } 611 }); 612 613 /* mobile: accordion groups */ 614 $(document).on('click', '.bbs_macc__head', function () { 615 $(this).parent().toggleClass('is-open'); 616 }); 617 618 /* BBS BREADCRUMB â collapse first 3 items into [â¦] if breadcrumb > 80% width */ 619 function bbsBreadcrumbCollapse() { 620 if ($(window).width() <= 768) { return; } 621 622 var $ol = $('.bbs_breadcrumbs__nav ol'); 623 if (!$ol.length) { return; } 624 625 // Reset any previous state (called on resize) 626 $ol.find('.bbs-crumb-ellipsis').remove(); 627 $ol.find('[data-bbs-hidden]').removeAttr('data-bbs-hidden').show(); 628 629 var $items = $ol.children('li'); 630 if ($items.length < 5) { return; } 631 632 // Temporarily lift overflow to measure true (unclipped) positions 633 $ol.css('overflow', 'visible'); 634 var olLeft = $ol[0].getBoundingClientRect().left; 635 var olWidth = $ol[0].getBoundingClientRect().width; 636 var lastRight = $items.last()[0].getBoundingClientRect().right; 637 $ol.css('overflow', 'hidden'); 638 639 if (!olWidth || (lastRight - olLeft) / olWidth <= 0.9) { return; } 640 641 // Hide first 3 items (Accueil, Marque, Famille) 642 $items.slice(0, 3).attr('data-bbs-hidden', '1').hide(); 643 644 // Insert [â¦] button before item 3 (le Modèle), avec séparateur 645 var $ell = $('<li class="bbs-crumb-ellipsis">' 646 + '<button type="button" title="Afficher le fil d\'Ariane complet">â¦</button>' 647 + '<i class="fa fa-angle-right" aria-hidden="true"></i>' 648 + '</li>'); 649 $ell.insertBefore($items.eq(3)); 650 651 $ell.find('button').on('click', function () { 652 $ol.find('[data-bbs-hidden]').removeAttr('data-bbs-hidden').show(); 653 $ell.remove(); 654 // overflow:hidden reste â items dépassant la droite clippés, pas de retour à la ligne 655 }); 656 } 657 658 bbsBreadcrumbCollapse(); 659 660 var bbsBcResizeTimer; 661 $(window).on('resize', function () { 662 clearTimeout(bbsBcResizeTimer); 663 bbsBcResizeTimer = setTimeout(bbsBreadcrumbCollapse, 150); 664 }); 665 666}); 667 668// FOOTER 669 670$(document).on('click', '.footer_center .links .column .title', function () { 671 $(this).closest('.column').toggleClass('open'); 672}); 673 674// STOP FLOATING WIDGETS ABOVE FOOTER (desktop only) 675 676(function () { 677 678 var GAP = 10; 679 var PIN_Z_INDEX = 1; 680 var active = false; 681 var rafId = null; 682 683 // #bpfelix-root (bulle Felix) et .bpf-widget (fenêtre de chat ouverte) sont 684 // épinglés séparément : .bpf-widget a son propre "position: fixed" dans 685 // felix.css, indépendant de son parent #bpfelix-root â sans les cibler tous 686 // les deux, la fenêtre ouverte resterait plaquée sur le footer. 687 function $widgets() { 688 return $('#launcher, .skeepers-widget-wrapper-64300f8f-5e89-4a0d-b6d4-dc4953270a92, #bpfelix-root, #bpfelix-root .bpf-widget'); 689 } 690 691 // On reste volontairement en position:fixed (jamais absolute) et on ne 692 // touche qu'à "bottom", en coordonnées VIEWPORT â cohérent avec le fixed 693 // déjà posé par chaque widget dans son propre CSS. #bpfelix-root est 694 // TOUJOURS position:fixed (jamais modifié ici) : passer .bpf-widget en 695 // position:absolute avec un top en coordonnées DOCUMENT (feuille de calcul 696 // façon .offset()) le ferait interpréter relativement à ce parent fixed
697 // (donc en coordonnées viewport) â les deux systèmes ne correspondent pas 698 // et le widget se retrouve projeté hors écran (essayé, régression : le 699 // widget ouvert disparaissait complètement au lieu de juste sauter). 700 function update() { 701 rafId = null; 702 var footer = document.querySelector('.footer_top'); 703 if (!footer) { 704 return; 705 } 706 var footerTop = footer.getBoundingClientRect().top; 707 var bottom = Math.max(20, window.innerHeight - footerTop + GAP); 708 709 $widgets().each(function () { 710 var el = this; 711 712 // #bpfelix-root n'a plus de contenu visible en flux normal quand la 713 // conversation Felix est ouverte (le .bpf-fab est caché, .bpf-widget 714 // est hors flux) : son offsetHeight tombe à 0. On ignore ces 715 // éléments â .bpf-widget est de toute façon ciblé séparément. 716 if (!el.offsetHeight) { 717 return; 718 } 719 720 if (el.dataset.origCssText === undefined) { 721 el.dataset.origCssText = el.style.cssText; 722 } 723 el.style.setProperty('bottom', bottom + 'px', 'important'); 724 el.style.setProperty('z-index', PIN_Z_INDEX, 'important'); 725 el.dataset.pinnedAboveFooter = '1'; 726 }); 727 } 728 729 function reset() { 730 $widgets().each(function () { 731 var el = this; 732 if (el.dataset.pinnedAboveFooter) { 733 el.style.cssText = el.dataset.origCssText || ''; 734 delete el.dataset.pinnedAboveFooter; 735 } 736 }); 737 } 738 739 function onScroll() { 740 // Recalcul en continu (throttlé par rAF) tant qu'on est près du footer : 741 // la position exacte du footer dans le viewport change à chaque pixel de 742 // scroll, contrairement à un simple événement d'entrée/sortie. 743 if (!active || rafId) { 744 return; 745 } 746 rafId = requestAnimationFrame(update); 747 } 748 749 function init() { 750 if ($(window).width() <= 768) { 751 return; 752 } 753 754 var $anchor = $('.footer_top'); 755 if (!$anchor.length || !('IntersectionObserver' in window)) { 756 return; 757 } 758 759 var observer = new IntersectionObserver(function (entries) { 760 entries.forEach(function (entry) { 761 active = entry.isIntersecting; 762 if (active) { 763 update(); 764 } else { 765 reset(); 766 } 767 }); 768 }, { threshold: 0 }); 769 770 observer.observe($anchor[0]); 771 window.addEventListener('scroll', onScroll, { passive: true }); 772 773 // widgets Skeepers/Felix se chargent souvent après coup (script defer, 774 // fab/widget créés en JS) : si on est déjà dans la zone du footer à ce 775 // moment-là , on réapplique une fois. 776 setTimeout(function () { 777 if (active) { 778 update(); 779 } 780 }, 3000); 781 782 $(window).on('resize', function () { 783 if ($(window).width() <= 768) { 784 reset(); 785 active = false; 786 } else if (active) { 787 update(); 788 } 789 }); 790 } 791 792 $(document).ready(init); 793 794})();
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.