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.