PageSourceSearch

https://tri-mag.de/wp-content/plugins/tri-mag-newsticker/assets/js/liveband.js?ver=2.51.0

js tri-mag.de collected 2026-09-24 09:36:22 UTC 7,277 bytes, 202 lines download raw bytes

1/**
2 * tri-mag Newsticker – Liveband
3 * Marquee-Animation, Hover/Button-Pause, REST-Polling alle 90 Sekunden.
4 * Kein jQuery, kein Build-Step – vanilla JS, ES2020+.
5 */
6( function () {
7    'use strict';
8
9    const band   = document.getElementById( 'tmn-liveband' );
10    if ( ! band ) return;
11
12    const strip      = document.getElementById( 'tmn-lb-strip' );
13    const pauseBtn   = document.getElementById( 'tmn-lb-pause' );
14    const apiUrl     = band.dataset.api     || '';
15    const intervalMs = ( parseInt( band.dataset.interval, 10 ) || 90 ) * 1000;
16
17    // ── Zustandsvariablen ─────────────────────────────────────────────────────
18    let manualPaused = false;
19    let newestId     = getNewestId();
20    let animFrame    = null;
21    let position     = 0;
22    let lastTime     = null;
23    const SPEED      = 70; // Pixel pro Sekunde
24
25    // ── Animation ─────────────────────────────────────────────────────────────
26
27    function tick( timestamp ) {
28        if ( lastTime === null ) lastTime = timestamp;
29        const delta = timestamp - lastTime;
30        lastTime = timestamp;
31
32        position += ( SPEED * delta ) / 1000;
33
34        // Nahtloser Loop: wenn erste Hälfte komplett durch → zurücksetzen
35        const halfWidth = strip.scrollWidth / 2;
36        if ( position >= halfWidth ) {
37            position -= halfWidth;
38        }
39
40        strip.style.transform = `translateX(-${ position }px)`;
41        animFrame = requestAnimationFrame( tick );
42    }
43
44    function startAnimation() {
45        if ( animFrame ) return;
46        lastTime  = null;
47        animFrame = requestAnimationFrame( tick );
48    }
49
50    function stopAnimation() {
51        if ( animFrame ) {
52            cancelAnimationFrame( animFrame );
53            animFrame = null;
54            lastTime  = null;
55        }
56    }
57
58    // ── Pause-Logik ───────────────────────────────────────────────────────────
59
60    function pause() {
61        stopAnimation();
62        pauseBtn.textContent = '▶';
63        pauseBtn.setAttribute( 'aria-label', 'Ticker fortsetzen' );
64    }
65
66    function resume() {
67        startAnimation();
68        pauseBtn.textContent = '⏸';
69        pauseBtn.setAttribute( 'aria-label', 'Ticker pausieren' );
70    }
71
72    // Hover pausiert – aber nur wenn nicht manuell pausiert
73    band.addEventListener( 'mouseenter', () => {
74        if ( ! manualPaused ) stopAnimation();
75    } );
76    band.addEventListener( 'mouseleave', () => {
77        if ( ! manualPaused ) startAnimation();
78    } );
79
80    // Touch-Geräte: Tap auf Laufband pausiert / setzt fort
81    band.addEventListener( 'touchstart', ( e ) => {
82        // Tap auf Pause-Button → Button-Handler übernimmt
83        if ( e.target.closest( '#tmn-lb-pause' ) ) return;
84        manualPaused = ! manualPaused;
85        manualPaused ? pause() : resume();
86    }, { passive: true } );
87
88    // Pause-Button
89    pauseBtn.addEventListener( 'click', () => {
90        manualPaused = ! manualPaused;
91        manualPaused ? pause() : resume();
92    } );
93
94    // Accessibility: Space-Taste auf dem Button
95    pauseBtn.addEventListener( 'keydown', ( e ) => {
96        if ( e.key === ' ' || e.key === 'Enter' ) {
97            e.preventDefault();
98            pauseBtn.click();
99        }
100    } );
101
102    // ── REST-Polling ──────────────────────────────────────────────────────────
103
104    function getNewestId() {
105        const first = strip.querySelector( '.tmn-lb-item' );
106        return first ? parseInt( first.dataset.id || '0', 10 ) : 0;
107    }
108
109    async function fetchNewItems() {
110        if ( ! apiUrl ) return;
111        try {
112            const url  = `${ apiUrl }&limit=5`;
113            const resp = await fetch( url, { cache: 'no-store' } );
114            if ( ! resp.ok ) return;
115            const items = await resp.json();
116            const fresh = items.filter( i => i.id > newestId );
117            if ( fresh.length === 0 ) return;
118
119            fresh.reverse().forEach( prependItem );
120            newestId = fresh[ fresh.length - 1 ].id;
121
122            // Duplikat-Hälfte (für seamless loop) ebenfalls aktualisieren
123            rebuildDuplicate();
124        } catch {
125            // Netzwerkfehler still ignorieren – kein UX-Fehler anzeigen
126        }
127    }
128
129    function prependItem( item ) {
130        const li = buildItem( item );
131        strip.prepend( li );
132        // Position leicht korrigieren damit kein Sprung entsteht
133        position += li.getBoundingClientRect().width;
134    }
135
136    function buildItem( item ) {
137        const li     = document.createElement( 'li' );
138        li.className = 'tmn-lb-item tmn-lb-item--new';
139        li.dataset.id = item.id;
140
141        const a       = document.createElement( 'a' );
142        a.className   = 'tmn-lb-link';
143        a.href        = item.article_url || ( archiveUrl() + '?meldung=' + item.id );
144
145        const cat     = document.createElement( 'span' );
146        cat.className = 'tmn-lb-cat';
147        cat.textContent = item.category || '';
148
149        const text     = document.createElement( 'span' );
150        text.className = 'tmn-lb-text';
151        text.textContent = item.laufband_text || item.headline;
152
153        a.append( cat, text );
154        li.appendChild( a );
155        return li;
156    }
157
158    function rebuildDuplicate() {
159        // Alle Duplikat-Elemente entfernen und neu erstellen
160        strip.querySelectorAll( '.tmn-lb-item--dup' ).forEach( el => el.remove() );
161        const originals = Array.from( strip.querySelectorAll( '.tmn-lb-item:not(.tmn-lb-item--dup)' ) );
162        originals.forEach( orig => {
163            const clone       = orig.cloneNode( true );
164            clone.className  += ' tmn-lb-item--dup';
165            clone.setAttribute( 'aria-hidden', 'true' );
166            strip.appendChild( clone );
167        } );
168    }
169
170    function archiveUrl() {
171        // Aus dem ersten Link im Strip ableiten
172        const first = strip.querySelector( '.tmn-lb-link' );
173        if ( ! first ) return '/news';
174        const url = new URL( first.href );
175        return url.origin + '/news';
176    }
177
178    // ── Start ─────────────────────────────────────────────────────────────────
179
180    // Duplikat für seamless loop sicherstellen
181    rebuildDuplicate();
182
183    // Animation starten
184    startAnimation();
185
186    // Polling
187    if ( apiUrl ) {
188        setInterval( fetchNewItems, intervalMs );
189    }
190
191    // Reduced-Motion respektieren
192    const motionQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' );
193    function handleMotionPref() {
194        if ( motionQuery.matches ) {
195            pause();
196            band.classList.add( 'tmn-liveband--reduced-motion' );
197        }
198    }
199    motionQuery.addEventListener( 'change', handleMotionPref );
200    handleMotionPref();
201
202} )();

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.