/** * Lets a slider advance by itself - and stop as soon as somebody takes over. * * Without this script a slider is a scroll container with snap points and, where the browser can * draw them, dots: complete, swipeable and keyboard operable. The script only adds the automatic * step, and it is loaded exclusively on pages that carry a slider which asked for it. * * What it refuses to do: * - it does not run when the system asks for reduced motion, * - it stops for good at the first touch, click, key press or manual scroll, * - it pauses while the pointer is inside, while something in it has focus, and while the tab is * in the background or the slider is off screen. * * A carousel that keeps moving while somebody reads it is a nuisance, and one that cannot be * stopped is a barrier. */ (() => { if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const setup = slider => { const seconds = Number(slider.dataset.autoplay); if (!Number.isFinite(seconds) || seconds < 3) return; let paused = false; let stopped = false; let visible = true; let ownScroll = 0; const slides = [...slider.children]; if (slides.length < 2) return; const step = () => { if (stopped || paused || !visible || document.hidden) return; const last = slider.scrollLeft + slider.clientWidth >= slider.scrollWidth - 2; // Measured, not computed from offsetLeft: whether a slide's offset parent is the slider or // something further up decides the number, and getting that wrong means scrolling nowhere. const left = slide => slide.getBoundingClientRect().left - slider.getBoundingClientRect().left + slider.scrollLeft; const next = last ? slides[0] : slides.find(slide => left(slide) > slider.scrollLeft + 2); if (!next) return; // Remember that the next scroll event is ours, so it does not look like a person. ownScroll = Date.now(); slider.scrollTo({ left: last ? 0 : left(next), behavior: 'smooth' }); }; const stop = () => { stopped = true; clearInterval(timer); }; const timer = setInterval(step, seconds * 1000); slider.addEventListener('pointerenter', () => { paused = true; }); slider.addEventListener('pointerleave', () => { paused = false; }); slider.addEventListener('focusin', () => { paused = true; }); slider.addEventListener('focusout', () => { paused = false; }); for (const event of ['pointerdown', 'keydown', 'touchstart', 'wheel']) { slider.addEventListener(event, stop, { passive: true }); } slider.addEventListener('scroll', () => { // A scroll that is not the one we just started belongs to a person; then we are done. if (Date.now() - ownScroll > 1200) stop(); }, { passive: true }); if ('IntersectionObserver' in window) { new IntersectionObserver(entries => { visible = entries[0].isIntersecting; }, { threshold: 0.35 }) .observe(slider); } }; document.querySelectorAll('.block-slider[data-autoplay]').forEach(setup); })();