215 lines
8.6 KiB
JavaScript
215 lines
8.6 KiB
JavaScript
/* 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;
|
||
|
||
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';
|
||
}
|
||
|
||
function readConsent(storage, now) {
|
||
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;
|
||
const age = now - c.t;
|
||
if (age < 0 || age >= CONSENT_MAX_AGE) return null;
|
||
return { v: 1, a: c.a, t: c.t };
|
||
} 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).
|
||
|
||
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;
|
||
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);
|
||
|
||
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;
|
||
})();
|
||
|
||
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);
|
||
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; });
|
||
msg.textContent = 'Heatmap – ' + points.length + ' Klicks (letzte 30 Tage)';
|
||
draw();
|
||
timer = setInterval(redrawIfChanged, 1000);
|
||
})
|
||
.catch(function () { msg.textContent = 'Heatmap-Link abgelaufen oder ungültig'; });
|
||
}
|
||
|
||
try { main(); } catch (e) { /* nie die Seite stoeren */ }
|
||
|
||
})();
|