PageSourceSearch

https://www.brico-phone.com/themes/bricophone/assets/js/redesign25.js?v=1791162547

js brico-phone.com collected 2026-10-05 01:10:33 UTC 28,114 bytes, 794 lines download raw bytes

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.