1/* eslint-disable */ 2class Carousel { 3 constructor(carouselEl, options = { showDots: true, controls: true }) { 4 this._resizeObserver = null; 5 this._mutationObserver = null; 6 this._intersectionObserver = null; 7 this._debouncedSynchronize = null; 8 this.snappedCardIndex = 0; 9 this.paginatedCardIndex = []; 10 this.numberOfCardVisible = 0; 11 this._controlsInitialized = false; 12 this._resizeObserver = new ResizeObserver(debounceCarousel(() => { 13 if (!this.carouselEl.offsetParent) { 14 return; 15 } 16 this._updatePagination(); 17 this._synchronize(); 18 }, 100)); 19 this._mutationObserver = new MutationObserver(debounceCarousel((mutationList) => { 20 for (const mutation of mutationList) { 21 if (mutation.type == 'childList') { 22 const options = { 23 showDots: this.dots.length ? true : false, 24 controls: this.controls ? true : false, 25 }; 26 this._intersectionObserver.disconnect(); 27 this.carouselWrapper.scrollTo({ left: 0, behavior: 'instant' }); 28 this.dots = []; 29 this._init(options); 30 } 31 } 32 }, 100)); 33 this.cards = []; 34 this.dots = []; 35 this._debouncedSynchronize = debounceCarousel(() => { 36 this._synchronize(); 37 }, 100); 38 /** 39 * @param {boolean} reversed - Sens de direction du scroll. 40 */ 41 this.goToNextPage = throttle((reversed = false) => { 42 if (this.paginatedCardIndex.length === 0) { 43 return; 44 } 45 const newIndex = this.paginatedCardIndex.indexOf(this.snappedCardIndex) + 46 (reversed ? -1 : +1); 47 if (!this.paginatedCardIndex[newIndex] == undefined) { 48 return; 49 } 50 this.snappedCardIndex = this.paginatedCardIndex[newIndex]; 51 this._updateDots(); 52 this.goToCard(this.snappedCardIndex); 53 54 }, 100, { leading: true }); 55 if (!carouselEl) { 56 throw new Error('Incorrect given carouselEl'); 57 } 58 this.carouselEl = carouselEl; 59 this.carouselWrapper = carouselEl.querySelector('[carousel-cards]'); 60 if (!this.carouselWrapper) { 61 throw new Error('Given carousel is missing the attribute carousel-cards'); 62 } 63 // entries = only the elements whose visibility changed at this precise moment 64 // cards = all the elements of the carousel 65 this._intersectionObserver = new IntersectionObserver((entries) => { 66 // Updates the is-visible attribute 67 entries.forEach(({ target, isIntersecting }) => { 68 target.toggleAttribute('is-visible', isIntersecting); 69 }); 70 71 const cards = Array.from(this.carouselWrapper.children); 72 const visibles = cards.filter(card => card.hasAttribute('is-visible')); 73 74 if (cards.length === 1 || visibles.length === cards.length) { 75 // Case 1: only one element OR all visible â remove aria-current 76 cards.forEach(card => card.removeAttribute('aria-current')); 77 } else if (visibles.length === 1) { 78 // Case 2: only one visible, others hidden 79 cards.forEach(card => { 80 card.setAttribute('aria-current', card.hasAttribute('is-visible') ? 'true' : 'false'); 81 }); 82 } else { 83 // Case 3: multiple visible but not all â remove aria-current 84 cards.forEach(card => card.removeAttribute('aria-current')); 85 } 86 }, { 87 root: this.carouselWrapper, 88 threshold: 1.0 89 }); 90 if (options.toggleKeyboardNavRef) { 91 this.toggleKeyboardNav = options.toggleKeyboardNavRef; 92 } 93 this._resizeObserver.observe(this.carouselEl); 94 this._mutationObserver.observe(this.carouselWrapper, { 95 childList: true, 96 subtree: false, 97 }); 98 this.carouselWrapper.addEventListener('scroll', this._debouncedSynchronize); 99 this.carouselWrapper.tabIndex = -1; 100 if (options.controls) { 101 this.controls =
102 this.carouselEl.querySelector('carousel-controls'); 103 if (!this.controls) { 104 throw new Error('A <carousel-controls> element is required'); 105 } 106 this.controls.addEventListener('click', this.handleControls.bind(this)); 107 } 108 this._init(options); 109 } 110 _init(options = { showDots: true, controls: true }) { 111 this.cards = Array.from(this.carouselWrapper.children); 112 this.cards.forEach((card, index) => { 113 this._intersectionObserver.observe(card); 114 card.toggleAttribute('is-visible', card.getBoundingClientRect().right < 115 this.carouselWrapper.getBoundingClientRect().right); 116 if (options.showDots) { 117 this.dots.push(document.createElement('mt-carousel-dots')); 118 } 119 if (this.toggleKeyboardNav) { 120 this.toggleKeyboardNav(card, index == 0); 121 } 122 }); 123 124 // Used to simulate :focus-visible behavior in JS. 125 // lastWasKeyboard is true if the last interaction was keyboard navigation (Tab, Shift+Tab, Arrow keys). 126 // This allows us to only scroll cards into view when focus comes from keyboard, not mouse or touch. 127 let lastWasKeyboard = false; 128 129 document.addEventListener('keydown', (e) => { 130 // Tab, Shift+Tab or arrow keys = keyboard navigation 131 if ( 132 e.key === 'Tab' || 133 (e.key.startsWith && e.key.startsWith('Arrow')) 134 ) { 135 lastWasKeyboard = true; 136 } 137 }); 138 document.addEventListener('mousedown', () => { 139 lastWasKeyboard = false; 140 }); 141 document.addEventListener('touchstart', () => { 142 lastWasKeyboard = false; 143 }); 144 145 // Add a single 'focusin' event listener to each card. 146 // 'focusin' triggers when the card or any of its focusable children receives focus. 147 // which can improve scroll performance in some browsers. 148 this.cards.forEach(card => { 149 card.addEventListener('focusin', (event) => { 150 // Simulate :focus-visible: true if navigation was via keyboard (Tab or Shift+Tab) 151 if (!lastWasKeyboard) { 152 return; // Do not scroll if focus was triggered by mouse or touch 153 } 154 card.scrollIntoView({ behavior: 'auto', block: 'nearest', inline: 'nearest' }); 155 lastWasKeyboard = false; // Reset after scroll 156 }, { passive: true }); 157 }); 158 159 if (options.showDots) { 160 this._insertPaginationDots(); 161 } 162 this._updatePagination(); 163 this._synchronize(); 164 } 165 /** 166 * Enlève les observers et eventlisteners 167 */ 168 disconnect() { 169 this._intersectionObserver.disconnect(); 170 this._resizeObserver.disconnect(); 171 this.carouselWrapper.removeEventListener('scroll', this._debouncedSynchronize); 172 if (this.controls) { 173 this.controls.removeEventListener('click', this.handleControls); 174 } 175 } 176 _sendEvent(eventName) { 177 this.carouselEl.dispatchEvent(new Event(eventName)); 178 } 179 /** 180 * Permet de déterminer les decrés de libertés du scroll 181 */ 182 _updateControls() { 183 if (this.cards.length == 0) { 184 return; 185 } 186 187 // cacher les boutons et dots si tout est visible 188 const allVisible = this.cards.every(card => card.hasAttribute('is-visible')); 189 const previousPageBtn = this.carouselEl.querySelector('carousel-controls mds-icon-button:first-of-type'); 190 const nextPageBtn = this.carouselEl.querySelector('carousel-controls mds-icon-button:last-of-type'); 191 if (previousPageBtn && nextPageBtn) { 192 if (allVisible || !this._controlsInitialized) { 193 previousPageBtn.style.display = 'none'; 194 nextPageBtn.style.display = 'none'; 195 this._controlsInitialized = true; 196 // Désactive le scroll horizontal 197 this.carouselWrapper.style.overflowX = 'hidden'; 198 // Désactive les dots
199 this.dots.forEach(dot => { 200 dot.style.display = 'none'; 201 }); 202 } else { 203 previousPageBtn.style.display = ''; 204 nextPageBtn.style.display = ''; 205 // Réactive le scroll horizontal 206 this.carouselWrapper.style.overflowX = 'auto'; 207 // Réactive les dots 208 this.dots.forEach(dot => { 209 dot.style.display = ''; 210 }); 211 // activer/désactiver previous et next btn 212 const lastCard = this.cards.at(-1); 213 const firstCard = this.cards.at(0); 214 const hasReachedEnd = Boolean(lastCard && lastCard.matches('[is-visible]')); 215 const hasReachedStart = Boolean(firstCard && firstCard.matches('[is-visible]')); 216 217 [ 218 { btn: previousPageBtn, shouldDisable: hasReachedStart }, 219 { btn: nextPageBtn, shouldDisable: hasReachedEnd }, 220 ].forEach(({ btn, shouldDisable }) => { 221 if (shouldDisable) { 222 btn.setAttribute("disabled", ""); 223 btn.setAttribute("tabindex", "-1"); 224 } else { 225 btn.removeAttribute("disabled"); 226 btn.setAttribute("tabindex", "0"); 227 } 228 btn.addEventListener("click", (event) => { 229 if (btn.hasAttribute("disabled")) { 230 event.preventDefault(); 231 event.stopPropagation(); 232 } 233 }); 234 }); 235 } 236 } 237 238 if (previousPageBtn) { 239 previousPageBtn.addEventListener('keydown', (e) => { 240 if (e.key === 'Enter') { 241 e.preventDefault(); 242 const lis = this.carouselWrapper.querySelectorAll(':scope > li'); 243 if (lis.length > 0) { 244 lis[0].setAttribute('tabindex', '0'); 245 lis[0].focus(); 246 lis[0].setAttribute('tabindex', '-1'); 247 } 248 } 249 }); 250 } 251 if (nextPageBtn) { 252 nextPageBtn.addEventListener('keydown', (e) => { 253 if (e.key === 'Enter') { 254 e.preventDefault(); 255 const lis = this.carouselWrapper.querySelectorAll(':scope > li'); 256 if (lis.length > 0) { 257 lis[lis.length - 1].setAttribute('tabindex', '0'); 258 lis[lis.length - 1].focus(); 259 lis[lis.length - 1].setAttribute('tabindex', '-1'); 260 } 261 } 262 }); 263 } 264 } 265 handleControls({ target }) { 266 if (target.matches('mds-icon-button')) { 267 this.goToNextPage(target.matches('mds-icon-button:first-of-type')); 268 } 269 } 270 _updatePagination() { 271 if (this.cards.length == 0) { 272 return; 273 } 274 this.numberOfCardVisible = this.cards.filter((card) => card.hasAttribute('is-visible')).length; 275 this.paginatedCardIndex = []; 276 this.cards.forEach((card, index) => { 277 const isFirstCardOfPage = (index % this.numberOfCardVisible === 0 && 278 this.cards.length - index > this.numberOfCardVisible) || 279 index === this.cards.length - this.numberOfCardVisible; 280 card.classList.toggle('snapped', isFirstCardOfPage); 281 if (this.dots[index]) { 282 this.dots[index].classList.toggle('visible', isFirstCardOfPage); 283 } 284 if (isFirstCardOfPage) { 285 this.paginatedCardIndex.push(index); 286 } 287 }); 288 if (!this.paginatedCardIndex.includes(this.snappedCardIndex)) { 289 this.snappedCardIndex = this.paginatedCardIndex[0] || 0; 290 } 291 } 292 _updateDots() { 293 this.cards.forEach((_card, index) => { 294 if (this.dots[index]) { 295 this.dots[index].classList.toggle('active', index === this.snappedCardIndex); 296 } 297 }); 298 } 299 /** 300 * Permet la navigation du carousel à une carte définie 301 * @param {number} customIndex index du carousel concerné 302 */ 303 goToCard(customIndex, inlinePosition = 'start') { 304 const index = customIndex || this.snappedCardIndex; 305 const currentCard = this.cards[index]; 306 // workaround vertical scroll 307 if (currentCard) { 308 309 // Save current vertical scroll position 310 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 311 312 currentCard.scrollIntoView({ 313 behavior: 'auto', 314 block: 'nearest', 315 inline: inlinePosition, 316 }); 317 // Restore vertical scroll position to avoid automatic scrolling ( fix problem items with height > screen height - scroll movement after click on next/previous button ) 318 window.scrollTo(window.pageXOffset, scrollTop); 319 } 320 321 this._sendEvent('carouselpagechange'); 322 } 323 /** 324 * Permet de déterminer la première card affichée, en particulier pour la navigation au clavier/swipe 325 */ 326 _updateSnappedCardIndex() { 327 const currentCard = this.cards.findIndex((card) => card.hasAttribute('is-visible')); 328 if (this.cards.at(-1) && this.cards.at(-1).hasAttribute('is-visible')) { 329 this.snappedCardIndex = this.paginatedCardIndex.at(-1) || 0; 330 return; 331 } 332 this.snappedCardIndex = currentCard ? currentCard : 0; 333 } 334 _synchronize() { 335 this._updateSnappedCardIndex(); 336 this._updateControls(); 337 this._updateDots(); 338 } 339 _insertPaginationDots() { 340 const footer = this.carouselEl.querySelector('footer'); 341 if (this.dots.length === 0 && !footer) { 342 return; 343 } 344 footer.innerHTML = ''; 345 footer.append(...this.dots); 346 } 347} 348function throttle(fn, time, options) { 349 let timeout = 0; 350 let savedArguments = null; 351 return (...args) => { 352 if (timeout) { 353 savedArguments = args; 354 return; 355 } 356 if (options && options.leading) { 357 // @ts-ignore 358 fn.apply(this, args); 359 timeout = window.setTimeout(() => { 360 timeout = 0; 361 savedArguments = null; 362 }, time); 363 return; 364 } 365 timeout = window.setTimeout(() => { 366 // @ts-ignore 367 fn.apply(this, savedArguments || args); 368 savedArguments = null; 369 timeout = 0; 370 }, time); 371 }; 372} 373function debounceCarousel(fn, time = 0) { 374 let timeout; 375 return (...args) => { 376 clearTimeout(timeout); 377 timeout = window.setTimeout(() => { 378 // @ts-ignore 379 fn.apply(this, args); 380 }, time); 381 }; 382} 383/* eslint-enable */ 384//_ sourceMappingURL=carousel.utils.js.map
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.