1/* JW Statistieken: meting in de browser (cache-veilig). */ 2(function () { 3 'use strict'; 4 var C = window.jwstatCtx; 5 if (!C || !C.u || !window.FormData) { return; } 6 7 /* Consent: is er een consent-cookie ingesteld en is die er (nog) niet, dan NIET stoppen maar wachten. 8 Zodra de bezoeker accepteert (zonder herladen) meten we alsnog deze pagina, met de oorspronkelijke 9 verwijzer (Google e.d.). Voorheen stopte het script hier, waardoor de binnenkomstpagina ontbrak en 10 de sessie op pagina 2 begon als 'Direct'. */ 11 function heeftConsent() { return !C.k || document.cookie.indexOf(C.k) !== -1; } 12 if (!heeftConsent()) { 13 var klaar = false, t0 = Date.now(); 14 var probeer = function () { 15 if (klaar) { return; } 16 if (heeftConsent()) { klaar = true; clearInterval(iv); start(); } 17 else if (Date.now() - t0 > 30 * 60 * 1000) { klaar = true; clearInterval(iv); } 18 }; 19 var iv = setInterval(probeer, 500); 20 ['click', 'keydown', 'touchend', 'visibilitychange'].forEach(function (ev) { document.addEventListener(ev, function () { setTimeout(probeer, 50); }, true); }); 21 // Bekende CMP-events (Complianz, Cookiebot, CookieYes, Borlabs, Usercentrics) versnellen de start. 22 ['cmplz_status_change', 'cmplz_fire_categories', 'CookiebotOnAccept', 'cookieyes_consent_update', 'borlabs-cookie-consent-saved', 'ucEvent'].forEach(function (ev) { 23 window.addEventListener(ev, function () { setTimeout(probeer, 50); }); 24 document.addEventListener(ev, function () { setTimeout(probeer, 50); }); 25 }); 26 return; 27 } 28 start(); 29 30 function start() { 31 var NAAM = 'jwstat_vid'; 32 33 // Cookie op het hoofddomein (ook geldig op www.), zie jwstat_cookie_domein(). 34 var CD = C.cd && (location.hostname === C.cd || location.hostname.slice(-C.cd.length - 1) === '.' + C.cd) ? '; domain=' + C.cd : ''; 35 function leesCookie(n) { 36 var re = new RegExp('(?:^|; )' + n + '=([a-f0-9]{32})', 'g'), m, alle = []; 37 while ((m = re.exec(document.cookie))) { alle.push(m[1]); } 38 return alle.length ? alle[alle.length - 1] : ''; 39 } 40 function nieuwId() { 41 var a = new Uint8Array(16), s = '', i; 42 (window.crypto || window.msCrypto).getRandomValues(a); 43 for (i = 0; i < a.length; i++) { s += ('0' + a[i].toString(16)).slice(-2); } 44 return s; 45 } 46 /* Bezoeker-ID: cookie, met localStorage en sessionStorage als reserve. Sommige cookiemeldingen wissen 47 cookies die vóór of tijdens het accepteren zijn gezet; dan werd de volgende pagina een 'nieuwe bezoeker'. */ 48 function opslag(soort) { try { var s = window[soort]; s.setItem('jwstat_t', '1'); s.removeItem('jwstat_t'); return s; } catch (e) { return null; } } 49 var LS = opslag('localStorage'), SS = opslag('sessionStorage'); 50 function leesOpslag(s) { try { var v = s ? s.getItem(NAAM) : ''; return /^[a-f0-9]{32}$/.test(v || '') ? v : ''; } catch (e) { return ''; } } 51 var DEBUG = /[?&]jwstat_debug=1/.test(location.search) || (LS && LS.getItem('jwstat_debug') === '1'); 52 if (DEBUG && LS) { LS.setItem('jwstat_debug', '1'); } 53 function log() { if (DEBUG && window.console) { console.info.apply(console, ['[jw-statistieken]'].concat([].slice.call(arguments))); } } 54 55 var vidBron = 'c'; 56 var vid = leesCookie(NAAM); 57 if (!vid) { vid = leesOpslag(LS); vidBron = 'l'; } 58 if (!vid) { vid = leesOpslag(SS); vidBron = 's'; } 59 if (!vid) { vid = nieuwId(); vidBron = 'n'; } 60 function bewaarVid(v) { 61 vid = v; 62 // Elke paginaweergave de looptijd verlengen (= bewaartermijn). Oude cookie van alleen dit (sub)domein opruimen. 63 if (CD) { document.cookie = NAAM + '=; path=/; max-age=0'; } 64 document.cookie = NAAM + '=' + v + '; path=/; max-age=' + (C.d * 86400) + '; samesite=lax' + CD + (location.protocol === 'https:' ? '; secure' : ''); 65 try { if (LS) { LS.setItem(NAAM, v); } if (SS) { SS.setItem(NAAM, v); } } catch (e) {} 66 } 67 bewaarVid(vid); 68 log('bezoeker', vid, { bron: { c: 'cookie', l: 'localStorage (cookie was weg)', s: 'sessionStorage (cookie was weg)', n: 'nieuw' }[vidBron], cookieNaGezet: !!leesCookie(NAAM), consent: C.k || '(geen)' }); 69 70 /* Vorige pagina in dit tabblad: eigen 'verwijzer', want de browser stuurt soms alleen het domein mee. */ 71 var VORIGE = 'jwstat_prev'; 72 // Met categorie en groep: dan hoeft de server de vorige pagina niet zelf uit de URL te herleiden.
73 function leesVorige() { try { var x = JSON.parse((SS && SS.getItem(VORIGE)) || 'null'); return x && x.p && Date.now() - x.t < 1800000 ? x : null; } catch (e) { return null; } } 74 function zetVorige() { try { if (SS) { SS.setItem(VORIGE, JSON.stringify({ p: location.pathname, t: Date.now(), c: C.c || '', g: C.g || '' })); } } catch (e) {} } 75 76 var pv = 0, cid = '', zichtbaarSinds = document.visibilityState === 'visible' ? Date.now() : 0, actief = 0, maxScroll = 0; 77 78 function fd(data) { 79 var f = new FormData(); 80 f.append('action', 'jwstat_h'); 81 f.append('v', vid); 82 for (var k in data) { if (Object.prototype.hasOwnProperty.call(data, k)) { f.append(k, data[k]); } } 83 return f; 84 } 85 function beacon(data) { 86 var f = fd(data); 87 if (navigator.sendBeacon && navigator.sendBeacon(C.u, f)) { return; } 88 try { fetch(C.u, { method: 'POST', body: f, keepalive: true, credentials: 'same-origin' }); } catch (e) {} 89 } 90 // Kliks en tijd gaan direct mee met de code van deze weergave (cid); de server koppelt ze ook als 91 // het antwoord met het pageview-nummer nog niet binnen is (bv. klik vlak na binnenkomst). 92 function stuur(data) { 93 data.pv = pv || 0; 94 data.cid = cid; 95 beacon(data); 96 } 97 98 /* Weergave die nog niet bevestigd is, bewaren in sessionStorage. Lukt het versturen niet (bv. direct 99 doorgeklikt terwijl het verzoek nog liep), dan stuurt de volgende pagina hem alsnog, met de echte tijd. */ 100 var OPEN = 'jwstat_open'; 101 function leesOpen() { try { return JSON.parse(sessionStorage.getItem(OPEN) || 'null'); } catch (e) { return null; } } 102 function zetOpen(v) { try { if (v) { sessionStorage.setItem(OPEN, JSON.stringify(v)); } else { sessionStorage.removeItem(OPEN); } } catch (e) {} } 103 104 function verstuurPv(data, ago) { 105 var d = {}; for (var k in data) { d[k] = data[k]; } 106 d.t = 'pv'; d.ago = ago || 0; 107 // keepalive: het verzoek loopt door, ook als de bezoeker meteen doorklikt. 108 return fetch(C.u, { method: 'POST', credentials: 'same-origin', keepalive: true, body: fd(d) }) 109 .then(function (r) { return r.json(); }); 110 } 111 112 function nieuwCid() { return nieuwId().slice(0, 16); } 113 114 function paginaweergave() { 115 pv = 0; 116 cid = nieuwCid(); 117 startHartslag(false); 118 var vorige = leesVorige(); 119 var nu = { 120 cid: cid, 121 p: location.pathname, q: location.search, r: document.referrer || '', 122 ti: (document.title || '').slice(0, 250), c: C.c || '', g: C.g || '', id: C.p || 0, l: C.l || '', 123 w: (window.screen && screen.width) || 0, wd: navigator.webdriver ? 1 : 0, 124 pp: vorige ? vorige.p : '', pc: vorige ? (vorige.c || '') : '', pg: vorige ? (vorige.g || '') : '', 125 vb: vidBron, jv: C.v || '?' 126 }; 127 zetVorige(); 128 vidBron = 'c'; 129 var oud = leesOpen(); 130 zetOpen({ d: nu, ts: Date.now() }); 131 var eerst = Promise.resolve(); 132 if (oud && oud.d && oud.ts && Date.now() - oud.ts < 1800000) { 133 eerst = verstuurPv(oud.d, Date.now() - oud.ts).catch(function () {}); 134 } 135 eerst.then(function () { return verstuurPv(nu, 0); }).then(function (d) { 136 if (d && d.pv) { 137 zetOpen(null); 138 pv = d.pv; 139 // De server herkende deze bezoeker aan zijn vorige pagina: oude ID terugzetten. 140 if (d.v && /^[a-f0-9]{32}$/.test(d.v) && d.v !== vid) { log('gekoppeld aan bestaande bezoeker', d.v); bewaarVid(d.v); } 141 log('paginaweergave', pv, d.s ? 'sessie ' + d.s : ''); 142 } 143 }).catch(function () {}); 144 } 145 146 /* Actieve tijd (tabblad zichtbaar) en scrolldiepte. */ 147 function actieveTijd() { 148 return actief + (zichtbaarSinds ? Date.now() - zichtbaarSinds : 0); 149 } 150 function meetScroll() { 151 var h = Math.max(document.documentElement.scrollHeight, document.body ? document.body.scrollHeight : 0); 152 var p = h > 0 ? Math.round(Math.min(1, (window.scrollY + window.innerHeight) / h) * 100) : 0; 153 if (p > maxScroll) { maxScroll = p; } 154 } 155 var sT = false;
156 window.addEventListener('scroll', function () { 157 if (sT) { return; } 158 sT = true; 159 requestAnimationFrame(function () { sT = false; meetScroll(); }); 160 }, { passive: true }); 161 window.addEventListener('load', meetScroll); 162 163 /* lv = live-status: 1 tabblad zichtbaar, 2 op de achtergrond, 0 pagina verlaten. */ 164 function engagement(lv) { 165 if (!cid) { return; } 166 meetScroll(); 167 stuur({ t: 'pe', e: actieveTijd(), s: maxScroll, lv: lv }); 168 } 169 170 /* Hartslag voor de Live-pagina: elke 30 s zolang het tabblad zichtbaar is. Na 15 min zonder 171 scrollen/klikken/typen stopt hij (tabblad laten openstaan is geen bezoek); bij de volgende actie 172 gaat hij direct weer door. Werkt tegelijk de actieve tijd bij. */ 173 var HB = 30000, STIL = 15 * 60 * 1000, laatsteActie = Date.now(), hbT = null; 174 function hartslag() { 175 hbT = null; 176 if (document.visibilityState !== 'visible' || !cid) { return; } 177 if (Date.now() - laatsteActie > STIL) { return; } 178 engagement(1); 179 hbT = setTimeout(hartslag, HB); 180 } 181 function startHartslag(direct) { 182 if (hbT) { clearTimeout(hbT); hbT = null; } 183 if (direct) { hartslag(); } else { hbT = setTimeout(hartslag, HB); } 184 } 185 function stopHartslag() { if (hbT) { clearTimeout(hbT); hbT = null; } } 186 ['click', 'keydown', 'scroll', 'touchstart', 'mousemove'].forEach(function (ev) { 187 window.addEventListener(ev, function () { 188 laatsteActie = Date.now(); 189 if (!hbT && cid && document.visibilityState === 'visible') { startHartslag(true); } 190 }, { passive: true, capture: true }); 191 }); 192 193 document.addEventListener('visibilitychange', function () { 194 if (document.visibilityState === 'hidden') { 195 if (zichtbaarSinds) { actief += Date.now() - zichtbaarSinds; zichtbaarSinds = 0; } 196 stopHartslag(); 197 engagement(2); 198 } else { 199 if (!zichtbaarSinds) { zichtbaarSinds = Date.now(); } 200 laatsteActie = Date.now(); 201 startHartslag(true); 202 } 203 }); 204 window.addEventListener('pagehide', function () { stopHartslag(); engagement(0); }); 205 // Terug via de knop van de browser (bfcache): telt als nieuwe weergave. 206 window.addEventListener('pageshow', function (e) { 207 if (e.persisted) { actief = 0; maxScroll = 0; zichtbaarSinds = Date.now(); laatsteActie = Date.now(); paginaweergave(); } 208 }); 209 210 /* Klikgedrag. */ 211 function blokVan(el) { 212 for (var n = el; n && n !== document.body; n = n.parentElement) { 213 if (!n.classList) { continue; } 214 for (var i = 0; i < n.classList.length; i++) { 215 var c = n.classList[i]; 216 if (c.indexOf('dc-') === 0 && c.indexOf('__') === -1 && c !== 'dc-container' && c !== 'dc-small-container' && c !== 'dc-sec') { return c; } 217 } 218 if (n.tagName === 'SECTION' && n.id) { return '#' + n.id; } 219 } 220 return ''; 221 } 222 function zoneVan(el) { 223 if (el.closest('[role=dialog], .dc-revmodal, .dc-mwmodal, .dc-vk-modal')) { return 'popup'; } 224 if (el.closest('header, #masthead, .site-header')) { return 'header'; } 225 if (el.closest('footer, #colophon, .site-footer')) { return 'footer'; } 226 if (el.closest('nav')) { return 'menu'; } 227 if (el.closest('aside')) { return 'zijbalk'; } 228 return 'inhoud'; 229 } 230 document.addEventListener('click', function (e) { 231 var el = e.target && e.target.closest ? e.target.closest('a, button, [role=button], input[type=submit], summary, .swiper-button-next, .swiper-button-prev') : null; 232 if (!el) { return; } 233 var href = el.getAttribute('href') || ''; 234 var k = 'knop'; 235 // Interactie op de pagina zelf: uitklappen (accordeon/FAQ), tabs, sliderpijlen. 236 if (el.matches('.dc-faq__q, summary, [aria-expanded], [data-bs-toggle=collapse], .dc-toggle')) { k = 'uitklap'; } 237 else if (el.matches('.dc-dtabs__tab, [role=tab]')) { k = 'tab'; } 238 else if (el.matches('.swiper-button-next, .swiper-button-prev, [class*="-prev"], [class*="-next"]') && el.tagName !== 'A') { k = 'slider'; } 239 if (el.tagName === 'A') { 240 if (/^tel:/i.test(href)) { k = 'telefoon'; } 241 else if (/^mailto:/i.test(href)) { k = 'email'; } 242 else if (/wa\.me|whatsapp\.com|whatsapp:/i.test(href)) { k = 'whatsapp'; } 243 else if (/\.(pdf|docx?|xlsx?|zip)(\?|$)/i.test(href)) { k = 'download'; } 244 else if (el.host && el.host !== location.host) { k = 'extern'; } 245 else { k = 'link'; } 246 } 247 var label = (el.getAttribute('aria-label') || el.innerText || el.value || el.getAttribute('title') || '').replace(/\s+/g, ' ').trim().slice(0, 120); 248 if (k === 'link' && el.host === location.host) { href = el.pathname || href; } 249 stuur({ t: 'ev', k: k, lb: label, h: href.slice(0, 250), z: zoneVan(el), b: blokVan(el) }); 250 }, true); 251 252 /* JavaScript-fouten van de eigen site (max. 5 per pagina, geen fouten van extensies of externe scripts). */ 253 var fouten = 0, gezien = {};
254 function jsFout(msg, bron, regel, kol) { 255 if (fouten >= 5 || !msg) { return; } 256 if (bron && bron.indexOf(location.host) === -1 && !/^\//.test(bron)) { return; } 257 var f = (bron ? bron.replace(location.origin, '') : 'inline') + (regel ? ':' + regel + (kol ? ':' + kol : '') : ''); 258 var k = msg + '|' + f; 259 if (gezien[k]) { return; } 260 gezien[k] = true; fouten++; 261 stuur({ t: 'er', m: String(msg).slice(0, 250), f: f.slice(0, 250) }); 262 } 263 window.addEventListener('error', function (e) { 264 if (!e || !e.message || e.message === 'Script error.') { return; } 265 jsFout(e.message, e.filename || '', e.lineno, e.colno); 266 }); 267 window.addEventListener('unhandledrejection', function (e) { 268 var r = e && e.reason; 269 jsFout('Promise: ' + (r && r.message ? r.message : String(r)), (r && r.stack && (r.stack.match(/https?:\/\/[^\s)]+/) || [''])[0]) || '', 0, 0); 270 }); 271 272 /* CF7 houdt een formulier met fouten soms al in de browser tegen (veldcontrole), dan ziet de server niets. 273 Na een verstuurpoging: staat het formulier nog op 'init' met foutmeldingen, dan die velden + meldingen 274 doorgeven (nooit de ingevulde waarden). Keurt de server het af, dan registreert de server het zelf. */ 275 var cfLaatst = {}; 276 function cf7Poging(form) { 277 if (!form || !form.classList || !form.classList.contains('wpcf7-form')) { return; } 278 var idEl = form.querySelector('input[name="_wpcf7"]'); 279 var fid = idEl ? idEl.value : ''; 280 if (cfLaatst[fid] && Date.now() - cfLaatst[fid] < 1500) { return; } 281 cfLaatst[fid] = Date.now(); 282 setTimeout(function () { 283 var status = form.getAttribute('data-status') || ''; 284 if (status !== 'init') { return; } 285 var velden = []; 286 form.querySelectorAll('.wpcf7-not-valid-tip').forEach(function (tip) { 287 var w = tip.closest('[data-name]'); 288 velden.push({ veld: w ? w.getAttribute('data-name') : '', melding: (tip.textContent || '').trim().slice(0, 190) }); 289 }); 290 if (!velden.length) { return; } 291 stuur({ t: 'cf', fid: fid, vf: JSON.stringify(velden.slice(0, 20)) }); 292 }, 1200); 293 } 294 // Ook bij indrukken (mousedown/touchstart): als CF7 bij het verlaten van een veld een foutmelding toont, 295 // schuift de knop weg en valt de klik ernaast. De poging telt dan wel. 296 ['mousedown', 'touchstart', 'click'].forEach(function (ev) { 297 document.addEventListener(ev, function (e) { 298 var btn = e.target && e.target.closest ? e.target.closest('input[type=submit], button[type=submit]') : null; 299 if (btn && btn.form) { cf7Poging(btn.form); } 300 }, true); 301 }); 302 document.addEventListener('submit', function (e) { cf7Poging(e.target); }, true); 303 304 /* Voorgeladen pagina (prerender door Chrome of speculation rules): pas meten als de bezoeker hem echt 305 opent. Anders telt een pagina mee die nooit bekeken is (Direct, 0 s actief). */ 306 if (document.prerendering) { 307 document.addEventListener('prerenderingchange', function () { 308 zichtbaarSinds = document.visibilityState === 'visible' ? Date.now() : 0; 309 laatsteActie = Date.now(); 310 paginaweergave(); 311 }, { once: true }); 312 } else { 313 paginaweergave(); 314 } 315 } 316})();
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.