Slider mit Autorotation, Beitragsliste mit Blaettern
CMS quality / php (8.2) (push) Canceled after 0s
CMS quality / php (8.4) (push) Canceled after 0s
CMS quality / browser (push) Canceled after 0s
CMS quality / mysql (push) Canceled after 0s

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>
This commit is contained in:
kantarciandClaude Opus 5 committed 2026-09-13 10:40:50 +02:00
1 parent b2c4f45474
commit 5a5933f8cd
12 files changed
+229 -9

No files matched your search

+1 -1
View File
@@ -52,7 +52,7 @@ export function createBlock(type) {
if (type === 'embed') block.props = { group: 'external', title: '', provider: '', url: '', hint: '', height: 400 };
if (type === 'icon') block.props = { icon: 'star', size: 48, decorative: true, label: '', href: '' };
if (type === 'progress') block.props = { label: 'Fortschritt', value: 70, showValue: true };
if (type === 'slider') block.props = { desktopVisible: 3, tabletVisible: 2, mobileVisible: 1, snap: true, dots: true };
if (type === 'slider') block.props = { desktopVisible: 3, tabletVisible: 2, mobileVisible: 1, snap: true, dots: true, autoplay: 0 };
if (type === 'image') block.props = { src: '/assets/image-placeholder.svg', alt: 'Abstrakte Landschaft als Platzhalter', decorative: false, width: 1000, height: 600 };
return block;
}
+2
View File
@@ -260,6 +260,7 @@ function renderProperties() {
field('Wert anzeigen', 'showValue', block.props.showValue, { checkbox: true });
}
if (block.type === 'slider') {
field('Automatisch weiterblättern (Sekunden, 0 = aus)', 'autoplay', block.props.autoplay || 0, { number: true, help: 'Ab 3 Sekunden. Lädt ein kleines Skript, nur auf Seiten mit einem solchen Slider. Bei „weniger Bewegung“ im Betriebssystem bleibt er stehen, und die erste Berührung stoppt ihn endgültig.' });
field('Punkte und Pfeile', 'dots', block.props.dots === true, { checkbox: true, help: 'Zeichnet der Browser selbst, ohne Skript. Wo er es noch nicht kann, bleibt ein Schiebefeld.' });
for (const [key, label] of [['desktopVisible', 'Sichtbar auf Desktop'], ['tabletVisible', 'Sichtbar auf Tablet'], ['mobileVisible', 'Sichtbar auf Mobil']]) {
field(label, key, block.props[key], { number: true, help: key === 'desktopVisible' ? 'Wie viele Folien gleichzeitig zu sehen sind, 1 bis 6.' : undefined });
@@ -284,6 +285,7 @@ function renderProperties() {
field('Anzahl Beiträge', 'count', block.props.count, { number: true, help: 'Zwischen 1 und 24. Die Beiträge selbst kommen beim Ausliefern dazu, nicht aus dem gespeicherten Inhalt.' });
field('Nur Kategorie', 'category', block.props.category || '', { help: 'Adresse der Kategorie, zum Beispiel projekte. Leer lassen für alle Beiträge.' });
field('Nur Schlagwort', 'tag', block.props.tag || '', { help: 'Adresse des Schlagworts. Leer lassen für alle Beiträge.' });
field('Blättern statt abschneiden', 'paginate', block.props.paginate === true, { checkbox: true, help: 'Für eine Übersichtsseite. Eine Auswahl auf der Startseite lässt man aus.' });
field('Datum und Autor zeigen', 'showMeta', block.props.showMeta !== false, { checkbox: true });
field('Auszug zeigen', 'showExcerpt', block.props.showExcerpt !== false, { checkbox: true });
field('Kategorie zeigen', 'showTerms', block.props.showTerms !== false, { checkbox: true, help: 'Alle drei aus: ein Raster aus Bild und Titel, wie es zu Projekten passt.' });
+1 -1
View File
@@ -31,7 +31,7 @@ p{hyphens:auto}
.post-meta,.post-header{display:flex;flex-wrap:wrap;gap:6px 16px;align-items:baseline;font-size:13px;margin:0}
.post-header{padding-block:22px 0}
.post-excerpt{margin:0;line-height:1.65}
.post-terms{display:flex;flex-wrap:wrap;gap:8px;margin:0;font-size:13px}
.post-pager{display:flex;flex-wrap:wrap;gap:10px 22px;align-items:center;justify-content:center;margin-top:34px;font-size:14px}.post-pager span{color:var(--muted)}.post-terms{display:flex;flex-wrap:wrap;gap:8px;margin:0;font-size:13px}
.post-empty{padding:26px;border:1px dashed currentColor;border-radius:10px}
.pagination{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:center;margin-top:38px;padding-top:22px;border-top:1px solid currentColor;font-size:14px}
.post-terms-label{font-size:11px;font-weight:700;letter-spacing:1.6px;text-transform:uppercase;margin-bottom:10px}
+67
View File
@@ -0,0 +1,67 @@
/**
* 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);
})();