1const SLIDE_GAP_PX = 24; 2 3document.addEventListener('alpine:init', () => { 4 Alpine.data('merchantRailPdp', () => ({ 5 swiper: null, 6 needsSwiper: false, 7 8 init() { 9 // Conteneur de largeur nulle â rail replié, modale fermée : toute diapositive « déborde » 10 // et `setupSwiper` détruirait le carrousel vivant pour le rebâtir sur 0px, avec des flèches 11 // qui ne bougent rien. On ne remesure pas, l'observateur rejouera au retour de la largeur. 12 this._remeasure = () => { 13 const container = this.$refs.swiperContainer; 14 if (container && container.offsetWidth === 0) return; 15 this.checkSwiperNeeded(); 16 this.setupSwiper(); 17 }; 18 this.$nextTick(() => { 19 this._remeasure(); 20 this._watchWidth(); 21 }); 22 this._onFavorisRemove = () => { 23 // product-card.js detaches the card first; wait for it before pruning rail slides. 24 this.$nextTick(() => this._pruneSlides((slide) => !slide.querySelector('.product-card'))); 25 }; 26 window.addEventListener('favoris:remove', this._onFavorisRemove); 27 // Le rail du tunnel arme le cÅur du storefront et non la poubelle : son retrait passe par 28 // `product-state:selection-change`, qui ne détache aucune carte. Sans cette jambe, un rail 29 // qui ne contient QUE des favoris garde une carte au cÅur vide. 30 this._onSelectionChange = (event) => { 31 const detail = (event && event.detail) || {}; 32 if (detail.action !== 'remove') return; 33 const productId = Number(detail.productId) || null; 34 const offerId = Number(detail.bestOfferId) || null; 35 if (!productId && !offerId) return; 36 this._pruneSlides((slide) => { 37 const card = slide.querySelector('.product-card'); 38 if (!card) return false; 39 if (productId) return Number(card.dataset.productId) === productId; 40 return Number(card.dataset.id) === offerId; 41 }); 42 }; 43 window.addEventListener('product-state:selection-change', this._onSelectionChange); 44 }, 45 46 destroy() { 47 window.removeEventListener('resize', this._remeasure); 48 window.removeEventListener('favoris:remove', this._onFavorisRemove); 49 window.removeEventListener('product-state:selection-change', this._onSelectionChange); 50 if (this._widthObserver) this._widthObserver.disconnect(); 51 this.destroySwiper(); 52 }, 53 54 _pruneSlides(shouldDrop) { 55 const wrapper = this.$refs.swiperWrapper; 56 if (!wrapper) return; 57 let removed = 0; 58 wrapper.querySelectorAll('.favoris-merchant-rail-pdp-slide').forEach((slide) => { 59 if (!shouldDrop(slide)) return; 60 slide.remove(); 61 removed += 1; 62 }); 63 if (!removed) return; 64 this._decrementCaption(removed); 65 if (wrapper.querySelector('.favoris-merchant-rail-pdp-slide')) { 66 this._remeasure(); 67 return; 68 } 69 // Le rail ne survit pas à sa dernière carte : titre, repli et « Voir tout » resteraient 70 // au-dessus d'une bande vide. Swiper part AVANT le détachement â il lit encore ses nÅuds. 71 this.destroySwiper(); 72 const section = this.$el.closest('section'); 73 if (section) section.remove(); 74 }, 75 76 // Le rail du panier vit sous un repli et celui du tiroir hérité sous une modale fermée : la 77 // largeur revient sans qu'aucun `resize` de fenêtre ne le dise, et le garde de `_remeasure` 78 // aurait laissé le rail sur sa dernière mesure jusqu'au prochain redimensionnement de fenêtre. 79 _watchWidth() { 80 const container = this.$refs.swiperContainer; 81 if (!container) return; 82 // La diapositive fait 174px en dur, sans point de rupture : la largeur du conteneur décide 83 // seule du carrousel, et l'observateur voit tout ce qu'un `resize` de fenêtre aurait dit. 84 if (typeof ResizeObserver === 'undefined') { 85 window.addEventListener('resize', this._remeasure); 86 return; 87 } 88 this._measuredWidth = container.offsetWidth; 89 this._widthObserver = new ResizeObserver(() => { 90 if (container.offsetWidth === this._measuredWidth) return; 91 this._measuredWidth = container.offsetWidth; 92 this._remeasure(); 93 }); 94 this._widthObserver.observe(container); 95 }, 96 97 _decrementCaption(by) { 98 const caption = document.querySelector('[data-favoris-merchant-count]'); 99 if (!caption) return; 100 const current = parseInt(caption.dataset.count, 10) || 0; 101 const next = Math.max(0, current - by); 102 if (next === 0) { 103 caption.remove(); 104 return; 105 } 106 caption.dataset.count = String(next); 107 const link = caption.querySelector('a'); 108 if (!link) return; 109 const tpl = next > 1 ? caption.dataset.tplPlural : caption.dataset.tplSingular; 110 if (tpl) link.textContent = tpl.replace('{count}', String(next)); 111 }, 112 113 checkSwiperNeeded() { 114 const container = this.$refs.swiperContainer; 115 const wrapper = this.$refs.swiperWrapper; 116 if (!container || !wrapper) { 117 this.needsSwiper = false;
118 return; 119 } 120 const slides = wrapper.querySelectorAll('.favoris-merchant-rail-pdp-slide'); 121 if (slides.length === 0) { 122 this.needsSwiper = false; 123 return; 124 } 125 let totalWidth = 0; 126 slides.forEach((slide, idx) => { 127 totalWidth += slide.offsetWidth; 128 if (idx < slides.length - 1) totalWidth += SLIDE_GAP_PX; 129 }); 130 this.needsSwiper = totalWidth > container.offsetWidth; 131 }, 132 133 setupSwiper() { 134 this.destroySwiper(); 135 if (this.needsSwiper) this.initSwiper(); 136 }, 137 138 initSwiper() { 139 const container = this.$refs.swiperContainer; 140 const wrapper = this.$refs.swiperWrapper; 141 if (!container || !wrapper) return; 142 143 container.classList.add('swiper'); 144 wrapper.classList.add('swiper-wrapper'); 145 wrapper.querySelectorAll('.favoris-merchant-rail-pdp-slide').forEach((slide) => { 146 slide.classList.add('swiper-slide'); 147 }); 148 149 const options = { 150 slidesPerView: 'auto', 151 slidesPerGroupAuto: true, 152 spaceBetween: SLIDE_GAP_PX, 153 watchOverflow: true, 154 preventClicks: true, 155 a11y: false, 156 navigation: { 157 nextEl: this.$el.querySelector('.favoris-merchant-rail-pdp-next'), 158 prevEl: this.$el.querySelector('.favoris-merchant-rail-pdp-prev'), 159 disabledClass: 'disabled', 160 }, 161 pagination: { 162 el: this.$el.querySelector('.favoris-merchant-rail-pdp-pagination'), 163 clickable: true, 164 bulletClass: 'swiper-pagination-bullet swiper-pagination-bullet-custom', 165 bulletActiveClass: 'swiper-pagination-bullet-active bullet-active-custom', 166 }, 167 }; 168 169 this.swiper = new Swiper(container, options); 170 }, 171 172 destroySwiper() { 173 if (!this.swiper) return; 174 const container = this.$refs.swiperContainer; 175 const wrapper = this.$refs.swiperWrapper; 176 if (container) container.classList.remove('swiper'); 177 if (wrapper) wrapper.classList.remove('swiper-wrapper'); 178 wrapper?.querySelectorAll('.favoris-merchant-rail-pdp-slide').forEach((slide) => { 179 slide.classList.remove('swiper-slide'); 180 }); 181 this.swiper.destroy(true, true); 182 this.swiper = null; 183 }, 184 })); 185});
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.