- Messskript: data-variant="eigen" zeigt eine eigene Banner-Fassung (Sie-Form, WEBklar misst selbst, BANNER_VERSION_EIGEN 2026-10-17); Kunden-Websites bleiben bei der Fassung 2026-10-02. - config.analyse.eigeneWebsite: fester Schlüssel und Domains von webklar.com; ensureEigeneWebsite zieht den Eintrag im Messdienst täglich nach (aktiv: false schaltet ab). Kein Kunde, kein Abo, keine Tickets. - /api/admin/analyse-eigen (jeder Mitarbeiter): Kennzahlen 7/30/90 Tage und Heatmap-Link auf webklar.com. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
465 lines
22 KiB
JavaScript
465 lines
22 KiB
JavaScript
/* WEBklar Analyse w.js - generiert aus client/core.js + client/w.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: Einwilligungs-Banner + Ereignisse. Laeuft auf oeffentlichen Kunden-Websites.
|
|
// Keine Cookies, keine Kennung im Browser. Ohne Wahl nur pv {t,site,path}; ref/vp/cx/ck/ct/en nur nach Annehmen;
|
|
// nach Ablehnen oder bei GPC/DNT geht nichts mehr raus.
|
|
|
|
function main() {
|
|
const DOC = document;
|
|
const WIN = window;
|
|
if (WIN.__wkAnalytics) return; // doppelt eingebunden
|
|
WIN.__wkAnalytics = true;
|
|
const SELF = DOC.currentScript && DOC.currentScript.getAttribute && DOC.currentScript.getAttribute('data-site')
|
|
? DOC.currentScript : DOC.querySelector('script[src*="/a/w.js"]');
|
|
if (!SELF) return;
|
|
const site = String(SELF.getAttribute('data-site') || '');
|
|
if (!/^[a-z0-9]{16,40}$/.test(site)) return;
|
|
const colorAttr = String(SELF.getAttribute('data-color') || '');
|
|
const accent = contrastOk(colorAttr) ? colorAttr : '#111111';
|
|
// data-variant="eigen": Banner-Fassung fuer WEBklars eigene Websites (Sie-Form, ohne Auftragsverarbeiter)
|
|
const banner = bannerFor(String(SELF.getAttribute('data-variant') || ''));
|
|
const src = String(SELF.src || SELF.getAttribute('src') || '').replace(/[?#].*$/, '');
|
|
const base = src.replace(/[^/]*$/, '');
|
|
const endpoint = base + 'e';
|
|
|
|
// ---- Heatmap-Modus: nichts senden, kein Banner ----
|
|
const qs = new URLSearchParams(WIN.location.search);
|
|
const heatToken = qs.get('__wk_heat');
|
|
if (heatToken) {
|
|
// __wk_embed=1: Ansicht im Geraeterahmen des Kundenportals (h.js ohne Leiste, Links gesperrt)
|
|
const embed = qs.get('__wk_embed') === '1';
|
|
qs.delete('__wk_heat');
|
|
qs.delete('__wk_embed');
|
|
const rest = qs.toString();
|
|
try { WIN.history.replaceState(null, '', WIN.location.pathname + (rest ? '?' + rest : '') + (WIN.location.hash || '')); } catch (e) { /* egal */ }
|
|
const s = DOC.createElement('script');
|
|
s.setAttribute('src', base + 'h.js');
|
|
s.setAttribute('data-token', heatToken);
|
|
if (embed) s.setAttribute('data-embed', '1');
|
|
(DOC.head || DOC.documentElement).appendChild(s);
|
|
return;
|
|
}
|
|
|
|
// ---- Senden: Fehler werden verschluckt ----
|
|
function send(obj) {
|
|
try {
|
|
const body = JSON.stringify(obj);
|
|
fetch(endpoint, { method: 'POST', body, keepalive: true, credentials: 'omit', headers: { 'Content-Type': 'text/plain' } }).catch(function () {});
|
|
} catch (e) { /* egal */ }
|
|
}
|
|
const safe = (fn) => function () { try { return fn.apply(this, arguments); } catch (e) { /* nie die Seite stoeren */ } };
|
|
const vp = () => vpClass(WIN.innerWidth);
|
|
const ownHost = () => WIN.location.hostname;
|
|
|
|
// ---- Widerspruch (GPC/DNT) und Wahl: vor jedem Senden und vor jedem Hook ----
|
|
let storage = null;
|
|
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
|
const dnt = (v) => v === '1' || v === 'yes';
|
|
const optOut = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
|
if (optOut) return; // wie Ablehnen: nichts senden, kein Banner, keine Hooks
|
|
const stored = readConsent(storage, Date.now(), banner.version);
|
|
let memChoice = null;
|
|
let tracking = false;
|
|
// Wahl bei jedem Aufruf frisch lesen (Speicher, sonst nur fuer diese Seite gemerkt)
|
|
function choiceNow() {
|
|
const c = readConsent(storage, Date.now(), banner.version);
|
|
return c ? c.a : (memChoice ? memChoice.a : null);
|
|
}
|
|
const counting = () => choiceNow() !== 'reject';
|
|
|
|
// ---- Seitenaufruf (pv): ohne Wahl nur Seitenadresse, ref/vp nur nach Annehmen ----
|
|
let lastPath = null;
|
|
let pvStart = Date.now();
|
|
let maxScroll = 0;
|
|
let enSent = false;
|
|
let pvSent = false;
|
|
let cxSent = false;
|
|
function sendPv(first) {
|
|
if (!counting()) return;
|
|
lastPath = cleanPath(WIN.location.pathname);
|
|
pvStart = Date.now(); maxScroll = 0; enSent = false;
|
|
const ev = { t: 'pv', site, path: lastPath };
|
|
if (choiceNow() === 'accept') {
|
|
if (first) { ev.ref = refHost(DOC.referrer, ownHost()); cxSent = true; }
|
|
ev.vp = vp();
|
|
}
|
|
pvSent = true;
|
|
send(ev);
|
|
}
|
|
let navTimer = null;
|
|
function scheduleNav() {
|
|
if (navTimer || !counting()) return;
|
|
navTimer = setTimeout(safe(function () {
|
|
navTimer = null;
|
|
if (counting() && cleanPath(WIN.location.pathname) !== lastPath) sendPv(false);
|
|
}), 300);
|
|
}
|
|
let hooksOn = false;
|
|
function ensureHooks() {
|
|
if (hooksOn) return;
|
|
hooksOn = true;
|
|
WIN.addEventListener('pageshow', safe(function (e) { if (e && e.persisted) sendPv(false); }));
|
|
try {
|
|
const orig = WIN.history.pushState;
|
|
WIN.history.pushState = function () { const r = orig.apply(this, arguments); try { scheduleNav(); } catch (e) { /* egal */ } return r; };
|
|
WIN.addEventListener('popstate', safe(scheduleNav));
|
|
} catch (e) { /* egal */ }
|
|
}
|
|
// Nach "Annehmen": Herkunft und Fenstergroesse einmal nachreichen, oder (noch kein pv) pv mit Daten senden
|
|
function onAccept() {
|
|
ensureHooks();
|
|
if (!pvSent) { sendPv(true); return; }
|
|
if (cxSent) return;
|
|
cxSent = true;
|
|
send({ t: 'cx', site, path: lastPath, ref: refHost(DOC.referrer, ownHost()), vp: vp() });
|
|
}
|
|
if (choiceNow() !== 'reject') { sendPv(true); ensureHooks(); }
|
|
|
|
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
|
const hosts = [];
|
|
const scrollPct = () => {
|
|
const h = DOC.documentElement.scrollHeight || 1;
|
|
return Math.max(0, Math.min(100, Math.round(((WIN.scrollY || 0) + WIN.innerHeight) / h * 100)));
|
|
};
|
|
const rel = (v, total) => Math.max(0, Math.min(1, Math.round(v / (total || 1) * 10000) / 10000));
|
|
|
|
const onClick = safe(function (e) {
|
|
if (!allowed()) return;
|
|
const t = e.target;
|
|
if (!t || hosts.indexOf(t) !== -1) return;
|
|
const dw = DOC.documentElement.scrollWidth, dh = DOC.documentElement.scrollHeight;
|
|
const px = typeof e.pageX === 'number' ? e.pageX : (e.clientX || 0) + (WIN.scrollX || 0);
|
|
const py = typeof e.pageY === 'number' ? e.pageY : (e.clientY || 0) + (WIN.scrollY || 0);
|
|
const path = cleanPath(WIN.location.pathname);
|
|
send({ t: 'ck', site, path, vp: vp(), x: rel(px, dw), y: rel(py, dh), sel: cssPath(t) });
|
|
let a = t;
|
|
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
|
const kind = a ? contactKind(a, ownHost()) : null;
|
|
if (kind) {
|
|
const ev = { t: 'ct', site, path, kind };
|
|
if (kind === 'out') ev.host = linkHost(a);
|
|
send(ev);
|
|
}
|
|
});
|
|
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
|
|
function sendEn() {
|
|
if (enSent || !allowed()) return;
|
|
enSent = true;
|
|
send({ t: 'en', site, path: lastPath || cleanPath(WIN.location.pathname), vp: vp(),
|
|
sec: Math.max(0, Math.min(3600, Math.round((Date.now() - pvStart) / 1000))), scroll: Math.max(maxScroll, scrollPct()) });
|
|
}
|
|
const onLeave = safe(sendEn);
|
|
// Interner Seitenwechsel per Link: en sofort senden, der Beacon beim pagehide geht in Chromium oft verloren.
|
|
// Bubbling-Listener am Dokument: laeuft nach den Handlern der Seite, defaultPrevented ist dann bekannt.
|
|
const onLinkClick = safe(function (e) {
|
|
if (e.defaultPrevented || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey || (typeof e.button === 'number' && e.button !== 0)) return;
|
|
let a = e.target;
|
|
while (a && String(a.tagName).toUpperCase() !== 'A') a = a.parentNode;
|
|
if (!a || hosts.indexOf(e.target) !== -1) return;
|
|
const tg = String(a.getAttribute('target') || '').toLowerCase();
|
|
if ((tg && tg !== '_self') || a.getAttribute('download') !== null) return;
|
|
const u = new URL(String(a.href || a.getAttribute('href') || ''), WIN.location.href);
|
|
if (u.protocol !== 'http:' && u.protocol !== 'https:') return;
|
|
if (u.origin !== WIN.location.origin || u.pathname === WIN.location.pathname) return;
|
|
sendEn();
|
|
});
|
|
const onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
|
|
|
|
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
|
function allowed() {
|
|
const ok = choiceNow() === 'accept';
|
|
if (!ok && tracking) stopTracking();
|
|
return ok;
|
|
}
|
|
|
|
function startTracking() {
|
|
if (tracking) return;
|
|
tracking = true;
|
|
DOC.addEventListener('click', onClick, { capture: true, passive: true });
|
|
DOC.addEventListener('click', onLinkClick, { passive: true });
|
|
DOC.addEventListener('visibilitychange', onVis);
|
|
WIN.addEventListener('pagehide', onLeave);
|
|
WIN.addEventListener('scroll', onScroll, { passive: true });
|
|
}
|
|
function stopTracking() {
|
|
tracking = false;
|
|
DOC.removeEventListener('click', onClick, { capture: true });
|
|
DOC.removeEventListener('click', onLinkClick);
|
|
DOC.removeEventListener('visibilitychange', onVis);
|
|
WIN.removeEventListener('pagehide', onLeave);
|
|
WIN.removeEventListener('scroll', onScroll);
|
|
}
|
|
|
|
// ---- Einwilligung ----
|
|
let bannerHost = null;
|
|
let linkHostEl = null;
|
|
|
|
function choose(a) {
|
|
memChoice = null;
|
|
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now(), bv: banner.version })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
|
send({ t: 'bn', site, a });
|
|
if (a === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
|
closeBanner();
|
|
}
|
|
|
|
function el(tag, attrs, text) {
|
|
const n = DOC.createElement(tag);
|
|
for (const k in attrs || {}) n.setAttribute(k, attrs[k]);
|
|
if (text != null) n.textContent = text;
|
|
return n;
|
|
}
|
|
|
|
const CSS = ':host{all:initial}' +
|
|
'*{box-sizing:border-box}' +
|
|
'.bar{display:flex;align-items:center;justify-content:center;gap:16px;padding:14px 20px;background:#fff;color:#111;border-top:1px solid #d0d0d0;box-shadow:0 -2px 12px rgba(0,0,0,.12);font:15px/1.45 system-ui,-apple-system,Segoe UI,Roboto,sans-serif}' +
|
|
'.t{margin:0;max-width:720px}' +
|
|
'.t a{color:#111;text-decoration:underline}' +
|
|
'.r{display:flex;gap:10px;flex:none}' +
|
|
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;width:150px;height:44px;padding:0 20px;border:2px solid ' + accent + ';border-radius:8px;cursor:pointer;background:' + accent + ';color:#fff}' +
|
|
'button:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
|
'@media (max-width:767px){.bar{display:block;margin:8px;padding:16px;border:1px solid #d0d0d0;border-radius:12px}.r{flex-direction:column;margin-top:12px}button{width:100%}}';
|
|
|
|
let prevPad = '';
|
|
function applyPad() {
|
|
if (!bannerHost) return;
|
|
const h = Math.ceil(bannerHost.getBoundingClientRect().height || bannerHost.offsetHeight || 0);
|
|
DOC.body.style.paddingBottom = h + 'px';
|
|
}
|
|
const onResize = safe(applyPad);
|
|
function openBanner(focus) {
|
|
if (bannerHost) return;
|
|
const prev = DOC.activeElement;
|
|
bannerHost = DOC.createElement('div');
|
|
const st = bannerHost.style;
|
|
st.position = 'fixed'; st.left = '0'; st.right = '0'; st.bottom = '0'; st.zIndex = '2147483647';
|
|
hosts.push(bannerHost);
|
|
const root = bannerHost.attachShadow({ mode: 'open' });
|
|
root.appendChild(el('style', {}, CSS));
|
|
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': BANNER_ARIA });
|
|
const p = el('p', { class: 't' }, banner.text);
|
|
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, BANNER_LINK_LABEL));
|
|
const row = el('div', { class: 'r' });
|
|
const no = el('button', { type: 'button' }, 'Ablehnen');
|
|
const yes = el('button', { type: 'button' }, 'Annehmen');
|
|
no.addEventListener('click', safe(function () { choose('reject'); if (prev && prev.focus) prev.focus(); }));
|
|
yes.addEventListener('click', safe(function () { choose('accept'); if (prev && prev.focus) prev.focus(); }));
|
|
row.appendChild(no); row.appendChild(yes);
|
|
bar.appendChild(p); bar.appendChild(row);
|
|
root.appendChild(bar);
|
|
DOC.body.appendChild(bannerHost);
|
|
// Platz unten schaffen, damit Footer-Links (Impressum, Datenschutz) erreichbar bleiben
|
|
prevPad = DOC.body.style.paddingBottom;
|
|
applyPad();
|
|
WIN.addEventListener('resize', onResize);
|
|
if (linkHostEl) linkHostEl.style.display = 'none';
|
|
if (focus === true) no.focus();
|
|
}
|
|
function closeBanner() {
|
|
if (!bannerHost) return;
|
|
const i = hosts.indexOf(bannerHost);
|
|
if (i !== -1) hosts.splice(i, 1);
|
|
bannerHost.remove();
|
|
bannerHost = null;
|
|
WIN.removeEventListener('resize', onResize);
|
|
DOC.body.style.paddingBottom = prevPad;
|
|
if (linkHostEl) linkHostEl.style.display = '';
|
|
}
|
|
|
|
// Einstellungsknopf: angeknabberter Keks unten rechts, dreht sich langsam (nicht bei reduzierter Bewegung).
|
|
// Beim Darueberfahren haelt er an und zeigt "Datenschutz-Einstellungen"; das ist auch sein Name fuer Screenreader.
|
|
// Inline-SVG statt data:-Grafik, damit eine strenge CSP der Website (img-src) ihn nicht blockiert.
|
|
const SETTINGS_CSS = ':host{all:initial}' +
|
|
'.b{position:relative;display:block;width:44px;height:44px;padding:2px;margin:0;border:0;border-radius:50%;background:transparent;cursor:pointer;-webkit-tap-highlight-color:transparent}' +
|
|
'.b:focus-visible{outline:3px solid #1a73e8;outline-offset:2px}' +
|
|
'.c{display:block;width:40px;height:40px;filter:drop-shadow(0 2px 3px rgba(0,0,0,.35));animation:wk-spin 9s linear infinite}' +
|
|
'.b:hover .c,.b:focus-visible .c{animation-play-state:paused}' +
|
|
'.l{position:absolute;right:52px;top:50%;transform:translate(6px,-50%);padding:7px 12px;border-radius:999px;background:#fff;color:#222;box-shadow:0 2px 10px rgba(0,0,0,.18);font:600 13px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;white-space:nowrap;opacity:0;pointer-events:none;transition:opacity .15s,transform .15s}' +
|
|
'.b:hover .l,.b:focus-visible .l{opacity:1;transform:translate(0,-50%)}' +
|
|
'@keyframes wk-spin{to{transform:rotate(360deg)}}' +
|
|
'@media (prefers-reduced-motion:reduce){.c{animation:none}.l{transition:none}}';
|
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
function svgEl(tag, attrs) {
|
|
const n = DOC.createElementNS(SVG_NS, tag);
|
|
for (const k in attrs) n.setAttribute(k, attrs[k]);
|
|
return n;
|
|
}
|
|
function cookieSvg() {
|
|
const svg = svgEl('svg', { class: 'c', viewBox: '0 0 64 64', 'aria-hidden': 'true', focusable: 'false' });
|
|
// Umriss mit Bissspur oben rechts (zwei Bissboegen, dann der grosse Kreis zurueck)
|
|
svg.appendChild(svgEl('path', { d: 'M44.84 6A10 10 0 0 0 57.5 19.37A7.5 7.5 0 0 0 60.96 33.5A29 29 0 1 1 44.84 6Z', fill: '#dfa25b', stroke: '#a5652c', 'stroke-width': '3', 'stroke-linejoin': 'round' }));
|
|
const chips = [[19, 22, 4.2, 3.3, -20], [31, 14, 3.2, 2.6, 15], [33, 33, 4, 3.2, 30], [46, 41, 3.6, 2.8, -35], [21, 45, 4, 3, 10], [36, 52, 3, 2.4, -10], [11, 34, 2.6, 2.1, 40], [47, 31, 2.4, 2, 0]];
|
|
for (const c of chips) svg.appendChild(svgEl('ellipse', { cx: c[0], cy: c[1], rx: c[2], ry: c[3], transform: 'rotate(' + c[4] + ' ' + c[0] + ' ' + c[1] + ')', fill: '#5a3216' }));
|
|
const crumbs = [[26, 28, 1.1], [40, 22, 1], [28, 40, 1.2], [14, 26, 0.9], [40, 46, 1]];
|
|
for (const c of crumbs) svg.appendChild(svgEl('circle', { cx: c[0], cy: c[1], r: c[2], fill: '#f3c98c' }));
|
|
return svg;
|
|
}
|
|
function addSettingsLink() {
|
|
linkHostEl = DOC.createElement('div');
|
|
const st = linkHostEl.style;
|
|
st.position = 'fixed'; st.right = '12px'; st.bottom = '12px'; st.zIndex = '2147483646';
|
|
hosts.push(linkHostEl);
|
|
const root = linkHostEl.attachShadow({ mode: 'open' });
|
|
root.appendChild(el('style', {}, SETTINGS_CSS));
|
|
const b = el('button', { type: 'button', class: 'b', 'aria-label': SETTINGS_LABEL });
|
|
b.appendChild(el('span', { class: 'l', 'aria-hidden': 'true' }, SETTINGS_LABEL));
|
|
b.appendChild(cookieSvg());
|
|
b.addEventListener('click', safe(function () { openBanner(true); }));
|
|
root.appendChild(b);
|
|
DOC.body.appendChild(linkHostEl);
|
|
}
|
|
|
|
DOC.addEventListener('click', safe(function (e) {
|
|
const t = e.target;
|
|
if (t && t.closest && t.closest('[data-webklar-consent]')) { if (e.preventDefault) e.preventDefault(); openBanner(true); }
|
|
}));
|
|
if (!DOC.querySelector('[data-webklar-consent]')) addSettingsLink();
|
|
|
|
// Einwilligung in anderem Tab geaendert
|
|
WIN.addEventListener('storage', safe(function (e) {
|
|
if (e.key !== null && e.key !== CONSENT_KEY) return;
|
|
const c = readConsent(storage, Date.now(), banner.version);
|
|
if (choiceNow() === 'accept') { startTracking(); onAccept(); } else stopTracking();
|
|
if (c) closeBanner();
|
|
}));
|
|
|
|
if (stored) {
|
|
if (stored.a === 'accept') startTracking();
|
|
} else {
|
|
openBanner();
|
|
send({ t: 'bn', site, a: 'shown' });
|
|
}
|
|
}
|
|
|
|
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
|
|
|
})();
|