/* WEBklar Analyse h.js - generiert aus client/core.js + client/h.js (node client/build.js), nicht von Hand aendern */ (function () { 'use strict'; // Reine Funktionen des Analyse-Skripts (ohne DOM-Zugriff ausser an uebergebenen Objekten). // Wird von w.js/h.js importiert; build.js fuegt alles zu je einer Datei ohne Import zusammen. const CONSENT_KEY = 'wk-consent'; const CONSENT_MAX_AGE = 183 * 86400000; const BANNER_VERSION = '2026-10-02'; const BANNER_TEXT = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Deiner Zustimmung messen wir zusätzlich, von welcher Website Du kommst, wie lange Du bleibst, wie weit Du scrollst und wo Du klickst (Heatmap), mit einer täglich wechselnden Kennung. Die Messung betreibt für uns die WEBklar GbR in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Du abgelehnt hast. Deine Zustimmung kannst Du jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: '; // Eigene Websites der WEBklar GbR (webklar.com, Skript-Attribut data-variant="eigen"): Sie-Form, WEBklar misst selbst // (keine Auftragsverarbeitung). Eigene Fassung, damit eine Textaenderung nur dort neu fragt. const BANNER_VERSION_EIGEN = '2026-10-17'; const BANNER_TEXT_EIGEN = 'Um unsere Website zu verbessern, zählen wir Seitenaufrufe mit Land und Gerätetyp, ohne Kennung. Mit Ihrer Zustimmung messen wir zusätzlich, von welcher Website Sie kommen, wie lange Sie bleiben, wie weit Sie scrollen und wo Sie klicken (Heatmap), mit einer täglich wechselnden Kennung. Wir messen selbst auf unserem Server in Deutschland, ohne Cookies und ohne Werbung. Nach „Ablehnen“ zählen wir nur noch, dass Sie abgelehnt haben. Ihre Zustimmung können Sie jederzeit über „Datenschutz-Einstellungen“ mit Wirkung für die Zukunft widerrufen. Details: '; const BANNER_LINK_LABEL = 'Datenschutz'; const BANNER_ARIA = 'Einwilligung zur Reichweitenmessung'; const SETTINGS_LABEL = 'Datenschutz-Einstellungen'; function vpClass(width) { const w = Number(width); if (w < 768) return 'm'; if (w < 1024) return 't'; return 'd'; } function cleanPath(href) { let p = '/'; const h = String(href == null ? '' : href); if (h.charAt(0) === '/') { p = h.replace(/[?#].*$/, '') || '/'; // wie location.pathname, auch '//foo' } else { try { p = new URL(h, 'http://x.invalid/').pathname || '/'; } catch (e) { /* bleibt '/' */ } } try { p = decodeURIComponent(p); } catch (e) { /* kaputtes Encoding: roh lassen */ } if (p.charAt(0) !== '/') p = '/' + p; return p.slice(0, 300); } const stripWww = (h) => String(h).toLowerCase().replace(/^www\./, ''); function refHost(referrer, ownHost) { if (!referrer) return ''; try { const u = new URL(String(referrer)); if (u.protocol !== 'http:' && u.protocol !== 'https:') return ''; const h = u.hostname.toLowerCase(); if (!h || stripWww(h) === stripWww(ownHost || '')) return ''; return h; } catch (e) { return ''; } } // Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten). function cssPath(el) { const parts = []; let n = el; while (n && n.tagName && parts.length < 6) { const tag = String(n.tagName).toLowerCase().replace(/[^a-z0-9-]/g, ''); if (!tag) break; let idx = 1; for (let s = n.previousElementSibling; s; s = s.previousElementSibling) { if (String(s.tagName).toLowerCase() === String(n.tagName).toLowerCase()) idx++; } parts.unshift(tag + ':nth-of-type(' + idx + ')'); if (tag === 'body' || tag === 'html') break; n = n.parentNode; } let out = parts.join(' > '); while (out.length > 200 && parts.length > 1) { parts.shift(); out = parts.join(' > '); } return out.slice(0, 200); } function parseHref(a) { const href = a && (typeof a.href === 'string' && a.href ? a.href : (a.getAttribute && a.getAttribute('href'))) || ''; return String(href).trim(); } function linkHost(a) { try { return new URL(parseHref(a)).hostname.toLowerCase(); } catch (e) { return ''; } } function contactKind(a, ownHost) { const href = parseHref(a); if (!href) return null; const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(href); if (!scheme) return null; // relativ, Anker const s = scheme[1].toLowerCase(); if (s === 'tel') return 'tel'; if (s === 'mailto') return 'mail'; if (s === 'whatsapp') return 'wa'; if (s !== 'http' && s !== 'https') return null; const host = linkHost(a); if (!host) return null; if (host === 'wa.me' || host === 'api.whatsapp.com') return 'wa'; const own = ownHost == null ? (typeof location !== 'undefined' ? location.hostname : '') : ownHost; return stripWww(host) === stripWww(own || '') ? null : 'out'; } // Banner-Fassung je Variante: 'eigen' fuer WEBklars eigene Websites, alles andere ist die Kunden-Fassung. function bannerFor(variant) { return variant === 'eigen' ? { version: BANNER_VERSION_EIGEN, text: BANNER_TEXT_EIGEN } : { version: BANNER_VERSION, text: BANNER_TEXT }; } function readConsent(storage, now, version = BANNER_VERSION) { try { const raw = storage.getItem(CONSENT_KEY); if (!raw) return null; const c = JSON.parse(raw); if (!c || c.v !== 1 || (c.a !== 'accept' && c.a !== 'reject') || typeof c.t !== 'number' || !isFinite(c.t)) return null; if (c.bv !== version) return null; // andere/fehlende Fassung des Banner-Textes: neu fragen const age = now - c.t; if (age < 0 || age >= CONSENT_MAX_AGE) return null; return { v: 1, a: c.a, t: c.t, bv: c.bv }; } catch (e) { return null; } } function contrastOk(hex) { if (typeof hex !== 'string' || !/^#[0-9a-f]{6}$/i.test(hex)) return false; const lin = (i) => { const c = parseInt(hex.slice(1 + i * 2, 3 + i * 2), 16) / 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); }; const L = 0.2126 * lin(0) + 0.7152 * lin(1) + 0.0722 * lin(2); return 1.05 / (L + 0.05) >= 4.5; } function escapeHtml(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); } // WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen). // data-embed="1" (nur in einem Rahmen): Ansicht im Geraeterahmen des Kundenportals. Dann ohne Leiste, // Links und Formulare gesperrt, und die Klickzahl geht per postMessage nur an das Portal (Herkunft von h.js). function main() { const me = document.currentScript; const token = me && me.getAttribute('data-token'); const src = String((me && (me.src || me.getAttribute('src'))) || '').replace(/[?#].*$/, ''); const base = src.replace(/[^/]*$/, ''); if (!token || !base) return; const de = document.documentElement; let portal = ''; try { portal = new URL(base).origin; } catch (e) { /* ohne Portal keine Einbettung */ } const embed = !!portal && me.getAttribute('data-embed') === '1' && window.parent && window.parent !== window; function tell(state, count) { if (!embed) return; try { window.parent.postMessage({ wk: 'heat', state: state, count: count }, portal); } catch (e) { /* egal */ } } const canvas = document.createElement('canvas'); const cs = canvas.style; cs.position = 'absolute'; cs.left = '0'; cs.top = '0'; cs.pointerEvents = 'none'; cs.zIndex = '2147483646'; document.body.appendChild(canvas); let points = []; let lastW = 0, lastH = 0, timer = 0; // Farbverlauf: transparent -> blau -> gruen -> gelb -> rot const ramp = (function () { const c = document.createElement('canvas'); c.width = 256; c.height = 1; const g = c.getContext('2d'); const lg = g.createLinearGradient(0, 0, 256, 0); lg.addColorStop(0.25, 'rgb(0,0,255)'); lg.addColorStop(0.55, 'rgb(0,255,0)'); lg.addColorStop(0.85, 'rgb(255,255,0)'); lg.addColorStop(1, 'rgb(255,0,0)'); g.fillStyle = lg; g.fillRect(0, 0, 256, 1); return g.getImageData(0, 0, 256, 1).data; })(); if (embed) { // Nur ansehen: kein Seitenwechsel (er wuerde zaehlen und das Banner zeigen) const block = function (e) { const t = e.target; if (e.type === 'submit' || (t && t.closest && t.closest('a,area'))) { e.preventDefault(); e.stopPropagation(); } }; window.addEventListener('click', block, true); window.addEventListener('submit', block, true); // Handy und Tablet haben ueberlagernde Scrollleisten; so stimmt die Breite wie auf dem Geraet const st = document.createElement('style'); st.textContent = 'html{scrollbar-width:none}html::-webkit-scrollbar{display:none}'; (document.head || de).appendChild(st); start(function (n) { tell('ready', n); }, function () { tell('error', 0); }); return; } const host = document.createElement('div'); host.style.position = 'fixed'; host.style.left = '0'; host.style.right = '0'; host.style.top = '0'; host.style.zIndex = '2147483647'; const root = host.attachShadow({ mode: 'open' }); const style = document.createElement('style'); style.textContent = ':host{all:initial}.bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:10px 16px;background:#111;color:#fff;font:14px/1.4 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}button{font:600 14px/1 system-ui,sans-serif;padding:8px 14px;border:2px solid #fff;border-radius:6px;background:#fff;color:#111;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}'; const bar = document.createElement('div'); bar.className = 'bar'; const msg = document.createElement('span'); msg.textContent = 'Heatmap wird geladen ...'; const close = document.createElement('button'); close.setAttribute('type', 'button'); close.textContent = 'Schließen'; bar.appendChild(msg); bar.appendChild(close); root.appendChild(style); root.appendChild(bar); document.body.appendChild(host); function draw() { const w = Math.max(de.scrollWidth, de.clientWidth, 1); const h = Math.max(de.scrollHeight, de.clientHeight, 1); lastW = w; lastH = h; // Zeichenflaeche begrenzen (Speicher): hoechstens ca. 4 Mio. Pixel const scale = Math.min(1, Math.sqrt(4e6 / (w * h))); const cw = Math.max(1, Math.floor(w * scale)), ch = Math.max(1, Math.floor(h * scale)); canvas.width = cw; canvas.height = ch; cs.width = w + 'px'; cs.height = h + 'px'; const g = canvas.getContext('2d'); g.clearRect(0, 0, cw, ch); const r = Math.max(6, 30 * scale); for (const p of points) { const x = p.x * cw, y = p.y * ch; const rg = g.createRadialGradient(x, y, 0, x, y, r); rg.addColorStop(0, 'rgba(0,0,0,0.25)'); rg.addColorStop(1, 'rgba(0,0,0,0)'); g.fillStyle = rg; g.fillRect(x - r, y - r, r * 2, r * 2); } // Deckkraft-Stufen in Farben umsetzen const img = g.getImageData(0, 0, cw, ch); const d = img.data; for (let i = 3; i < d.length; i += 4) { const a = d[i]; if (!a) continue; const k = Math.min(255, a * 3) * 4; d[i - 3] = ramp[k]; d[i - 2] = ramp[k + 1]; d[i - 1] = ramp[k + 2]; d[i] = Math.min(200, 60 + a); } g.putImageData(img, 0, 0); } function redrawIfChanged() { if (Math.max(de.scrollWidth, de.clientWidth) !== lastW || Math.max(de.scrollHeight, de.clientHeight) !== lastH) draw(); } function stop() { clearInterval(timer); window.removeEventListener('resize', draw); host.remove(); canvas.remove(); } close.addEventListener('click', stop); start(function (n) { msg.textContent = 'Heatmap – ' + n + ' Klicks (letzte 30 Tage)'; }, function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; }); // Punkte laden, zeichnen und bei Groessenaenderung neu zeichnen function start(onReady, onError) { window.addEventListener('resize', draw); fetch(base + 'heat?token=' + encodeURIComponent(token), { credentials: 'omit' }) .then(function (r) { if (!r.ok) throw new Error('heat'); return r.json(); }) .then(function (j) { points = (j && Array.isArray(j.points) ? j.points : []).filter(function (p) { return p && p.x >= 0 && p.x <= 1 && p.y >= 0 && p.y <= 1; }); draw(); timer = setInterval(redrawIfChanged, 1000); onReady(points.length); }) .catch(function () { onError(); }); } } try { main(); } catch (e) { /* nie die Seite stoeren */ } })();