1/** 2 * tri-mag Newsticker â anonyme Statistik-Beacons. 3 * 4 * (Seit 2.18.3 unter neutralem Datei-/Endpoint-Namen "pulse": Adblocker- 5 * Filterlisten wie EasyPrivacy blockieren URLs mit "track"/"stats" â 6 * dadurch gingen Zählungen von Besuchern mit Adblocker verloren.) 7 * 8 * Views: Karte ausreichend im Viewport (IntersectionObserver), einmal pro 9 * Seitenaufruf, gesammelt und gebündelt per sendBeacon verschickt. Für 10 * hohe Karten (Bild!) gilt statt der 50-%-Regel eine Mindest-Sichthöhe, 11 * da 50 % einer sehr hohen Karte auf kleinen Displays nie erreicht werden. 12 * Klicks: Links in der Karte (Headline, âZum Artikel", Quelle) sowie 13 * Video-Poster-Klick und Podcast-Play. Keine Cookies, keine 14 * personenbezogenen Daten â nur Meldungs-IDs. 15 */ 16( function () { 17 'use strict'; 18 19 var cfg = window.tmnStats || {}; 20 if ( ! cfg.endpoint ) { 21 return; 22 } 23 24 var seen = {}; 25 var queue = []; 26 27 function send( type, ids ) { 28 if ( ! ids.length ) { 29 return; 30 } 31 var payload = JSON.stringify( { type: type, ids: ids } ); 32 if ( navigator.sendBeacon ) { 33 navigator.sendBeacon( cfg.endpoint, new Blob( [ payload ], { type: 'application/json' } ) ); 34 } else { 35 fetch( cfg.endpoint, { 36 method: 'POST', 37 headers: { 'Content-Type': 'application/json' }, 38 body: payload, 39 keepalive: true, 40 credentials: 'omit' 41 } ).catch( function () {} ); 42 } 43 } 44 45 function flushViews() { 46 if ( queue.length ) { 47 send( 'view', queue.splice( 0 ) ); 48 } 49 } 50 51 // ââ Views ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 52 if ( 'IntersectionObserver' in window ) { 53 var observer = new IntersectionObserver( function ( entries ) { 54 entries.forEach( function ( entry ) { 55 if ( ! entry.isIntersecting ) { 56 return; 57 } 58 // Gesehen = mind. 50 % des Elements sichtbar ODER es 59 // füllt mind. 60 % des Viewports (hohe Karten mit Bild 60 // erreichen auf kleinen Displays nie 50 % Sichtbarkeit) 61 var visible = entry.intersectionRect ? entry.intersectionRect.height : 0; 62 if ( entry.intersectionRatio < 0.5 && visible < 0.6 * window.innerHeight ) { 63 return; 64 } 65 var id = entry.target.getAttribute( 'data-stat' ); 66 if ( id && ! seen[ id ] ) { 67 seen[ id ] = true; 68 queue.push( parseInt( id, 10 ) ); 69 observer.unobserve( entry.target ); 70 } 71 } ); 72 }, { threshold: [ 0.1, 0.25, 0.5, 0.75 ] } ); 73 74 document.querySelectorAll( '[data-stat]' ).forEach( function ( el ) { 75 observer.observe( el ); 76 } ); 77 78 // Dynamisch nachgeladene Elemente mitbeobachten â die 79 // Schlagzeilen-Leiste baut ihre Einträge alle 90 s per Poll neu 80 // auf; ohne diesen MutationObserver fielen die neuen Knoten aus 81 // der View-Zählung (bereits gezählte IDs bleiben über `seen` 82 // dedupliziert, es entstehen keine Doppel-Views) 83 if ( 'MutationObserver' in window ) { 84 new MutationObserver( function ( mutations ) { 85 mutations.forEach( function ( m ) { 86 m.addedNodes.forEach( function ( node ) { 87 if ( 1 !== node.nodeType ) { 88 return; 89 } 90 if ( node.hasAttribute && node.hasAttribute( 'data-stat' ) ) { 91 observer.observe( node ); 92 } 93 if ( node.querySelectorAll ) { 94 node.querySelectorAll( '[data-stat]' ).forEach( function ( el ) { 95 observer.observe( el ); 96 } ); 97 } 98 } ); 99 } ); 100 } ).observe( document.body, { childList: true, subtree: true } ); 101 } 102 } 103 104 setInterval( flushViews, 10000 ); 105 document.addEventListener( 'visibilitychange', function () { 106 if ( 'hidden' === document.visibilityState ) { 107 flushViews(); 108 } 109 } ); 110 // pagehide zusätzlich: iOS Safari feuert visibilitychange beim 111 // SchlieÃen/Wegnavigieren nicht zuverlässig â ohne pagehide gingen 112 // dort die Views des letzten Sichtfensters verloren 113 window.addEventListener( 'pagehide', flushViews ); 114 115 // ââ Klicks âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 116 document.addEventListener( 'click', function ( event ) { 117 var wrap = event.target.closest( '[data-stat]' ); 118 if ( ! wrap ) { 119 return; 120 } 121 var isLink = event.target.closest( 'a' ); 122 var isPoster = event.target.closest( '.tmn-yt-poster' ); 123 // Redaktions-Buttons (Inline-Editor) nicht mitzählen 124 if ( event.target.closest( '.tmn-edit-btn, .tmn-edit-form' ) ) { 125 return; 126 } 127 if ( isLink || isPoster ) { 128 send( 'click', [ parseInt( wrap.getAttribute( 'data-stat' ), 10 ) ] ); 129 } 130 }, true ); 131 132 // ââ News-Show-Plays ââââââââââââââââââââââââââââââââââââââââââââââââââ 133 // Player mit data-show="<Attachment-ID>" (Voll- und Kurzfassung sind 134 // getrennte Dateien â getrennte Zählung). Media-Events bubbeln nicht, 135 // deshalb Capture-Listener auf document; einmal pro Element. 136 document.addEventListener( 'play', function ( event ) { 137 var el = event.target; 138 if ( ! el || 'AUDIO' !== el.tagName ) { 139 return; 140 } 141 var id = el.getAttribute( 'data-show' ); 142 if ( ! id || el.dataset.tmnCounted ) { 143 return; 144 } 145 el.dataset.tmnCounted = '1'; 146 send( 'play', [ parseInt( id, 10 ) ] ); 147 }, true ); 148 149 // Podcast-Play (einmal pro Element) 150 document.querySelectorAll( '[data-stat] audio' ).forEach( function ( audio ) { 151 var counted = false;
152 audio.addEventListener( 'play', function () { 153 if ( counted ) { 154 return; 155 } 156 counted = true; 157 var wrap = audio.closest( '[data-stat]' ); 158 if ( wrap ) { 159 send( 'click', [ parseInt( wrap.getAttribute( 'data-stat' ), 10 ) ] ); 160 } 161 } ); 162 } ); 163} )();
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.