Zwei Wuensche, zwei Bausteine. Die Autorotation ist Sekunden, kein Haekchen: 0 heisst aus, ab 3 laeuft sie. Sie braucht ein Skript - das zweite auf einer oeffentlichen Seite nach den Tabs - und es kommt nur auf Seiten mit einem solchen Slider, sonst bleibt script-src 'none'. Es laeuft nicht bei "weniger Bewegung", hoert beim ersten Tippen, Klicken, Wischen oder Scrollen endgueltig auf und pausiert bei Zeiger, Fokus, Hintergrundtab und ausserhalb des Bildes. Ohne Skript bleibt der Slider vollstaendig bedienbar. Die Beitragsliste kann blaettern statt abzuschneiden. Eine Auswahl auf der Startseite zeigt die neuesten und hoert auf; eine Uebersichtsseite blaettert und sagt, wie viele es sind. Das war die eigene Regel aus Arbeitspaket M, die fuer diesen Baustein noch nicht galt. Beim Bauen zweimal dieselbe Falle wie vorgestern: eine Variable war in der Schliessung nicht gebunden. Einmal das Sprungziel des Sliders (-78 statt 666), einmal die Seitenzahl - die zweite fiel sofort auf, weil die Seite mit 500 antwortete. Geprueft: 395 Tests unter PHP 8.2 und 8.4, 18 JS-Tests, Editortest, Browserpruefung, axe ohne Verstoss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
68 lines
3.0 KiB
JavaScript
68 lines
3.0 KiB
JavaScript
/**
|
|
* 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);
|
|
})();
|