Analytics: Banner-Skript Review-Fixes (R8/R9, Tab-Widerruf)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5930f052d7
commit
da8bba2a1c
6 files changed
+239
-152
No files matched your search
+15
-25
@@ -13,9 +13,12 @@ export function vpClass(width) {
|
||||
|
||||
export function cleanPath(href) {
|
||||
let p = '/';
|
||||
try {
|
||||
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
|
||||
} catch (e) { /* bleibt '/' */ }
|
||||
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);
|
||||
@@ -34,32 +37,19 @@ export function refHost(referrer, ownHost) {
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
|
||||
function looksGenerated(s) {
|
||||
if (/\d{4,}/.test(s)) return true;
|
||||
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
|
||||
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
|
||||
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
|
||||
return s.split(/[-_]/).some((p) => mixed(p, 6));
|
||||
}
|
||||
|
||||
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
|
||||
|
||||
// Nur Tag-Namen mit :nth-of-type, keine IDs/Klassen (koennten personenbezogene Texte enthalten).
|
||||
export function cssPath(el) {
|
||||
const parts = [];
|
||||
let n = el;
|
||||
while (n && n.tagName && parts.length < 5) {
|
||||
const tag = String(n.tagName).toLowerCase();
|
||||
let seg = tag;
|
||||
const id = n.id ? safeName(n.id) : '';
|
||||
if (id && !looksGenerated(id)) {
|
||||
parts.unshift(seg + '#' + id);
|
||||
break;
|
||||
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++;
|
||||
}
|
||||
const raw = n.getAttribute ? n.getAttribute('class') : '';
|
||||
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
|
||||
if (cls.length) seg += '.' + cls.join('.');
|
||||
parts.unshift(seg);
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
|
||||
Vendored
+18
-28
@@ -16,9 +16,12 @@ function vpClass(width) {
|
||||
|
||||
function cleanPath(href) {
|
||||
let p = '/';
|
||||
try {
|
||||
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
|
||||
} catch (e) { /* bleibt '/' */ }
|
||||
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);
|
||||
@@ -37,32 +40,19 @@ function refHost(referrer, ownHost) {
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
|
||||
function looksGenerated(s) {
|
||||
if (/\d{4,}/.test(s)) return true;
|
||||
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
|
||||
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
|
||||
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
|
||||
return s.split(/[-_]/).some((p) => mixed(p, 6));
|
||||
}
|
||||
|
||||
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
|
||||
|
||||
// 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 < 5) {
|
||||
const tag = String(n.tagName).toLowerCase();
|
||||
let seg = tag;
|
||||
const id = n.id ? safeName(n.id) : '';
|
||||
if (id && !looksGenerated(id)) {
|
||||
parts.unshift(seg + '#' + id);
|
||||
break;
|
||||
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++;
|
||||
}
|
||||
const raw = n.getAttribute ? n.getAttribute('class') : '';
|
||||
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
|
||||
if (cls.length) seg += '.' + cls.join('.');
|
||||
parts.unshift(seg);
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
@@ -140,7 +130,7 @@ function main() {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar';
|
||||
const msg = document.createElement('span');
|
||||
msg.innerHTML = escapeHtml('Heatmap wird geladen ...');
|
||||
msg.textContent = 'Heatmap wird geladen ...';
|
||||
const close = document.createElement('button');
|
||||
close.setAttribute('type', 'button');
|
||||
close.textContent = 'Schließen';
|
||||
@@ -212,11 +202,11 @@ function main() {
|
||||
.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; });
|
||||
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)');
|
||||
msg.textContent = 'Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)';
|
||||
draw();
|
||||
timer = setInterval(redrawIfChanged, 1000);
|
||||
})
|
||||
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); });
|
||||
.catch(function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
|
||||
Vendored
+56
-48
@@ -16,9 +16,12 @@ function vpClass(width) {
|
||||
|
||||
function cleanPath(href) {
|
||||
let p = '/';
|
||||
try {
|
||||
p = new URL(String(href == null ? '' : href), 'http://x.invalid/').pathname || '/';
|
||||
} catch (e) { /* bleibt '/' */ }
|
||||
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);
|
||||
@@ -37,32 +40,19 @@ function refHost(referrer, ownHost) {
|
||||
} catch (e) { return ''; }
|
||||
}
|
||||
|
||||
// Erzeugt wirkende Namen (css-1a2b3c, ember12345, 9f8e7d6c5b4a ...) erkennen.
|
||||
function looksGenerated(s) {
|
||||
if (/\d{4,}/.test(s)) return true;
|
||||
const mixed = (t, min) => t.length >= min && /\d/.test(t) && /[a-z]/i.test(t);
|
||||
if (mixed(s, 10) && !/[-_]/.test(s)) return true;
|
||||
if (/^[a-f0-9]{8,}$/i.test(s) && /\d/.test(s)) return true;
|
||||
return s.split(/[-_]/).some((p) => mixed(p, 6));
|
||||
}
|
||||
|
||||
const safeName = (s) => String(s).replace(/[^\w-]/g, '');
|
||||
|
||||
// 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 < 5) {
|
||||
const tag = String(n.tagName).toLowerCase();
|
||||
let seg = tag;
|
||||
const id = n.id ? safeName(n.id) : '';
|
||||
if (id && !looksGenerated(id)) {
|
||||
parts.unshift(seg + '#' + id);
|
||||
break;
|
||||
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++;
|
||||
}
|
||||
const raw = n.getAttribute ? n.getAttribute('class') : '';
|
||||
const cls = String(raw || '').split(/\s+/).map(safeName).filter((c) => c && !looksGenerated(c)).slice(0, 2);
|
||||
if (cls.length) seg += '.' + cls.join('.');
|
||||
parts.unshift(seg);
|
||||
parts.unshift(tag + ':nth-of-type(' + idx + ')');
|
||||
if (tag === 'body' || tag === 'html') break;
|
||||
n = n.parentNode;
|
||||
}
|
||||
let out = parts.join(' > ');
|
||||
@@ -128,8 +118,10 @@ function escapeHtml(s) {
|
||||
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"]');
|
||||
? 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;
|
||||
@@ -184,6 +176,7 @@ function main() {
|
||||
}), 300);
|
||||
}
|
||||
sendPv(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; };
|
||||
@@ -191,6 +184,9 @@ function main() {
|
||||
} catch (e) { /* egal */ }
|
||||
|
||||
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
const hosts = [];
|
||||
const scrollPct = () => {
|
||||
@@ -200,6 +196,7 @@ function main() {
|
||||
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;
|
||||
@@ -218,13 +215,21 @@ function main() {
|
||||
});
|
||||
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
|
||||
const onLeave = safe(function () {
|
||||
if (enSent) return;
|
||||
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 onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
|
||||
|
||||
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
||||
function allowed() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
const ok = c ? c.a === 'accept' : !!(memChoice && memChoice.a === 'accept');
|
||||
if (!ok && tracking) stopTracking();
|
||||
return ok;
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (tracking) return;
|
||||
tracking = true;
|
||||
@@ -242,17 +247,17 @@ function main() {
|
||||
}
|
||||
|
||||
// ---- Einwilligung ----
|
||||
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1';
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const gpc = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
|
||||
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
const stored = readConsent(storage, Date.now());
|
||||
let bannerHost = null;
|
||||
let linkHostEl = null;
|
||||
|
||||
function choose(a) {
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') startTracking(); else stopTracking();
|
||||
closeBanner();
|
||||
@@ -271,12 +276,11 @@ function main() {
|
||||
'.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;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' +
|
||||
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
|
||||
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;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%}}';
|
||||
|
||||
function openBanner() {
|
||||
function openBanner(focus) {
|
||||
if (bannerHost) return;
|
||||
const prev = DOC.activeElement;
|
||||
bannerHost = DOC.createElement('div');
|
||||
@@ -286,12 +290,11 @@ function main() {
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
|
||||
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der ');
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info'));
|
||||
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
|
||||
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details: ');
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz'));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen');
|
||||
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);
|
||||
@@ -299,7 +302,7 @@ function main() {
|
||||
root.appendChild(bar);
|
||||
DOC.body.appendChild(bannerHost);
|
||||
if (linkHostEl) linkHostEl.style.display = 'none';
|
||||
no.focus();
|
||||
if (focus === true) no.focus();
|
||||
}
|
||||
function closeBanner() {
|
||||
if (!bannerHost) return;
|
||||
@@ -318,19 +321,24 @@ function main() {
|
||||
const root = linkHostEl.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
|
||||
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
|
||||
b.addEventListener('click', safe(openBanner));
|
||||
b.addEventListener('click', safe(function () { openBanner(true); }));
|
||||
root.appendChild(b);
|
||||
DOC.body.appendChild(linkHostEl);
|
||||
}
|
||||
|
||||
if (DOC.querySelector('[data-webklar-consent]')) {
|
||||
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(); }
|
||||
}));
|
||||
} else {
|
||||
addSettingsLink();
|
||||
}
|
||||
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());
|
||||
if (c && c.a === 'accept') startTracking(); else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
if (stored) {
|
||||
if (stored.a === 'accept') startTracking();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
// WEBklar Analyse: Heatmap-Overlay (wird von w.js nur mit gueltigem ?__wk_heat=Token geladen).
|
||||
import { escapeHtml } from './core.js';
|
||||
|
||||
function main() {
|
||||
const me = document.currentScript;
|
||||
@@ -17,7 +16,7 @@ function main() {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'bar';
|
||||
const msg = document.createElement('span');
|
||||
msg.innerHTML = escapeHtml('Heatmap wird geladen ...');
|
||||
msg.textContent = 'Heatmap wird geladen ...';
|
||||
const close = document.createElement('button');
|
||||
close.setAttribute('type', 'button');
|
||||
close.textContent = 'Schließen';
|
||||
@@ -89,11 +88,11 @@ function main() {
|
||||
.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; });
|
||||
msg.innerHTML = escapeHtml('Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)');
|
||||
msg.textContent = 'Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)';
|
||||
draw();
|
||||
timer = setInterval(redrawIfChanged, 1000);
|
||||
})
|
||||
.catch(function () { msg.innerHTML = escapeHtml('Heatmap-Link abgelaufen oder ungültig'); });
|
||||
.catch(function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
|
||||
}
|
||||
|
||||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||||
+41
-23
@@ -5,8 +5,10 @@ import { cleanPath, vpClass, refHost, cssPath, contactKind, linkHost, readConsen
|
||||
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"]');
|
||||
? 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;
|
||||
@@ -61,6 +63,7 @@ function main() {
|
||||
}), 300);
|
||||
}
|
||||
sendPv(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; };
|
||||
@@ -68,6 +71,9 @@ function main() {
|
||||
} catch (e) { /* egal */ }
|
||||
|
||||
// ---- Tracking (nur nach Annehmen erreichbar) ----
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
let memChoice = null;
|
||||
let tracking = false;
|
||||
const hosts = [];
|
||||
const scrollPct = () => {
|
||||
@@ -77,6 +83,7 @@ function main() {
|
||||
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;
|
||||
@@ -95,13 +102,21 @@ function main() {
|
||||
});
|
||||
const onScroll = safe(function () { maxScroll = Math.max(maxScroll, scrollPct()); });
|
||||
const onLeave = safe(function () {
|
||||
if (enSent) return;
|
||||
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 onVis = safe(function () { if (DOC.visibilityState === 'hidden') onLeave(); });
|
||||
|
||||
// Einwilligung unmittelbar vor jedem Senden pruefen (z.B. in einem anderen Tab widerrufen)
|
||||
function allowed() {
|
||||
const c = readConsent(storage, Date.now());
|
||||
const ok = c ? c.a === 'accept' : !!(memChoice && memChoice.a === 'accept');
|
||||
if (!ok && tracking) stopTracking();
|
||||
return ok;
|
||||
}
|
||||
|
||||
function startTracking() {
|
||||
if (tracking) return;
|
||||
tracking = true;
|
||||
@@ -119,17 +134,17 @@ function main() {
|
||||
}
|
||||
|
||||
// ---- Einwilligung ----
|
||||
const gpc = navigator.globalPrivacyControl === true || navigator.doNotTrack === '1' || WIN.doNotTrack === '1';
|
||||
const dnt = (v) => v === '1' || v === 'yes';
|
||||
const gpc = navigator.globalPrivacyControl === true || dnt(navigator.doNotTrack) || dnt(WIN.doNotTrack) || dnt(navigator.msDoNotTrack);
|
||||
if (gpc) return; // wie Ablehnen: kein Banner, kein bn, nie Tracking
|
||||
|
||||
let storage = null;
|
||||
try { storage = WIN.localStorage; } catch (e) { /* gesperrt */ }
|
||||
const stored = readConsent(storage, Date.now());
|
||||
let bannerHost = null;
|
||||
let linkHostEl = null;
|
||||
|
||||
function choose(a) {
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
memChoice = null;
|
||||
try { storage.setItem(CONSENT_KEY, JSON.stringify({ v: 1, a, t: Date.now() })); } catch (e) { memChoice = { a }; /* ohne Speicher: gilt nur fuer diese Seite */ }
|
||||
send({ t: 'bn', site, a });
|
||||
if (a === 'accept') startTracking(); else stopTracking();
|
||||
closeBanner();
|
||||
@@ -148,12 +163,11 @@ function main() {
|
||||
'.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;min-width:120px;height:44px;padding:0 20px;border:2px solid #111;border-radius:8px;cursor:pointer;background:#fff;color:#111}' +
|
||||
'button.p{background:' + accent + ';border-color:' + accent + ';color:#fff}' +
|
||||
'button{font:600 15px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;min-width:120px;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%}}';
|
||||
|
||||
function openBanner() {
|
||||
function openBanner(focus) {
|
||||
if (bannerHost) return;
|
||||
const prev = DOC.activeElement;
|
||||
bannerHost = DOC.createElement('div');
|
||||
@@ -163,12 +177,11 @@ function main() {
|
||||
const root = bannerHost.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, CSS));
|
||||
const bar = el('div', { class: 'bar', role: 'dialog', 'aria-label': 'Cookie-Einstellungen' });
|
||||
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details in der ');
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz-Info'));
|
||||
p.appendChild(DOC.createTextNode ? DOC.createTextNode('.') : el('span', {}, '.'));
|
||||
const p = el('p', { class: 't' }, 'Wir möchten mit deiner Zustimmung anonym auswerten, wie unsere Website genutzt wird (keine Cookies, keine Weitergabe). Details: ');
|
||||
p.appendChild(el('a', { href: '/datenschutz-analyse.html' }, 'Datenschutz'));
|
||||
const row = el('div', { class: 'r' });
|
||||
const no = el('button', { type: 'button' }, 'Ablehnen');
|
||||
const yes = el('button', { type: 'button', class: 'p' }, 'Annehmen');
|
||||
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);
|
||||
@@ -176,7 +189,7 @@ function main() {
|
||||
root.appendChild(bar);
|
||||
DOC.body.appendChild(bannerHost);
|
||||
if (linkHostEl) linkHostEl.style.display = 'none';
|
||||
no.focus();
|
||||
if (focus === true) no.focus();
|
||||
}
|
||||
function closeBanner() {
|
||||
if (!bannerHost) return;
|
||||
@@ -195,19 +208,24 @@ function main() {
|
||||
const root = linkHostEl.attachShadow({ mode: 'open' });
|
||||
root.appendChild(el('style', {}, ':host{all:initial}button{font:12px/1 system-ui,-apple-system,Segoe UI,Roboto,sans-serif;padding:6px 10px;border:1px solid #bbb;border-radius:6px;background:#fff;color:#444;cursor:pointer}button:focus-visible{outline:3px solid #1a73e8}'));
|
||||
const b = el('button', { type: 'button' }, 'Cookie-Einstellungen');
|
||||
b.addEventListener('click', safe(openBanner));
|
||||
b.addEventListener('click', safe(function () { openBanner(true); }));
|
||||
root.appendChild(b);
|
||||
DOC.body.appendChild(linkHostEl);
|
||||
}
|
||||
|
||||
if (DOC.querySelector('[data-webklar-consent]')) {
|
||||
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(); }
|
||||
}));
|
||||
} else {
|
||||
addSettingsLink();
|
||||
}
|
||||
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());
|
||||
if (c && c.a === 'accept') startTracking(); else stopTracking();
|
||||
if (c) closeBanner();
|
||||
}));
|
||||
|
||||
if (stored) {
|
||||
if (stored.a === 'accept') startTracking();
|
||||
|
||||
Reference in new issue
Block a user