Files
Webklar-Kundenbereich/analytics/client/h.js
T
ClaudeandClaude Opus 5.5 0a113c2983 Heatmap in Geraeterahmen (Computer/Tablet + Handy) und Keks statt "Datenschutz-Einstellungen"
Heatmap im Portal (Funktionen -> Analyse): die echte Website laeuft jetzt in realistischen
Geraeterahmen, gross MacBook oder iPad (Umschalter oben), daneben das iPhone, jeweils in echter
Breite (1512 / 820 / 402 px), verkleinert und im Geraet scrollbar, mit den Klicks der
passenden Geraeteklasse als Overlay. MacBook und iPhone stammen aus den Rahmen der neuen
Website (device-mockups), das iPad ist im gleichen Stil nachgebaut (public/geraete.css/.js).

- heatmap-link: embed: true haengt __wk_embed=1 an; w.js reicht das als data-embed an h.js.
- h.js eingebettet (nur in einem Rahmen): keine Leiste, Links und Formulare gesperrt (kein
  Seitenwechsel, also keine Zaehlung und kein Banner), Scrollleisten aus, Klickzahl per
  postMessage nur an die Herkunft von h.js (project.webklar.com).
- iframe mit Sandbox allow-scripts allow-same-origin (keine Formulare, Pop-ups, keine
  Navigation des Portals); Laden erst, wenn die Heatmap in Sicht kommt. Laesst sich eine
  Website nicht einbetten, kommt nach 20 s ein Hinweis mit "In neuem Tab oeffnen" (alter Weg).

Banner-Skript: der Einstellungsknopf nach der Wahl ist jetzt ein angeknabberter Keks unten
rechts (Inline-SVG, 44 px). Er dreht sich langsam, haelt beim Darueberfahren/Fokus an und
zeigt dann "Datenschutz-Einstellungen" (auch sein Name fuer Screenreader, passend zum
Bannertext); bei reduzierter Bewegung dreht er sich nicht.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 15:09:17 +00:00

130 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 */ }