1import { escapeHtml, prefersReducedMotion } from '../core/dom.js'; 2 3const INTERVAL = 4500; 4 5export function mountCarousel(root, items) { 6 if (!root || items.length < 2) return; 7 8 const track = root.querySelector('[data-carousel-track]'); 9 const existing = new Set([...track.querySelectorAll('img')].map((img) => img.getAttribute('src'))); 10 11 items.filter((item) => !existing.has(item.image)).forEach((item) => { 12 track.insertAdjacentHTML('beforeend', ` 13 <div class="carousel__slide"> 14 <img src="${item.image}" alt="${escapeHtml(item.alt)}" loading="lazy" decoding="async"> 15 </div>`); 16 }); 17 18 const slides = [...track.children]; 19 const dots = root.querySelector('[data-carousel-dots]'); 20 dots.innerHTML = slides.map((slide, i) => 21 `<button class="carousel__dot" type="button" aria-label="Show image ${i + 1} of ${slides.length}"></button>`, 22 ).join(''); 23 root.style.setProperty('--carousel-interval', `${INTERVAL}ms`); 24 25 const autoplay = !prefersReducedMotion(); 26 let index = 0; 27 let timer = null; 28 let remaining = INTERVAL; 29 let startedAt = 0; 30 let paused = false; 31 32 const show = (next) => { 33 index = (next + slides.length) % slides.length; 34 track.style.transform = `translateX(-${index * 100}%)`; 35 slides.forEach((slide, i) => slide.setAttribute('aria-hidden', String(i !== index))); 36 [...dots.children].forEach((dot, i) => { 37 dot.setAttribute('aria-current', String(i === index)); 38 dot.classList.remove('is-progress'); 39 }); 40 41 if (!autoplay) return; 42 const active = dots.children[index]; 43 void active.offsetWidth; 44 active.classList.add('is-progress'); 45 remaining = INTERVAL; 46 schedule(); 47 }; 48 49 function schedule() { 50 clearTimeout(timer); 51 if (paused) return; 52 startedAt = performance.now(); 53 timer = setTimeout(() => show(index + 1), remaining); 54 } 55 56 const pause = () => { 57 if (!autoplay || paused) return; 58 paused = true; 59 clearTimeout(timer); 60 remaining = Math.max(0, remaining - (performance.now() - startedAt)); 61 root.classList.add('is-paused'); 62 }; 63 64 const resume = () => { 65 if (!autoplay || !paused) return; 66 paused = false; 67 root.classList.remove('is-paused'); 68 schedule(); 69 }; 70 71 dots.addEventListener('click', (event) => { 72 const dot = event.target.closest('.carousel__dot'); 73 if (dot) show([...dots.children].indexOf(dot)); 74 }); 75 76 root.addEventListener('mouseenter', pause); 77 root.addEventListener('mouseleave', resume); 78 root.addEventListener('focusin', pause); 79 root.addEventListener('focusout', resume); 80 document.addEventListener('visibilitychange', () => (document.hidden ? pause() : resume())); 81 82 show(0); 83}
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.