Files
reflector-dashboard/hoverinfo.js
T
bjorneandClaude Fable 5.1 25b481e2ae Public web-only release: SvxPortal front-end + auth backend, install templates and INSTALL.md
Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr),
deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles
live in install/ with CHANGEME placeholders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 23:07:05 +02:00

83 lines
2.9 KiB
JavaScript

'use strict';
// Shared hover info box: any element carrying a data-info attribute (URI-encoded
// HTML) shows a floating details box while the mouse is over it. Wired once via
// delegation, so it works for rows rendered at any time. CSP-safe (no inline JS).
(function(){
let box = null;
function ensureBox(){
if (box) return box;
box = document.createElement('div');
box.id = 'rowInfo';
box.style.cssText =
'position:fixed;z-index:2000;display:none;max-width:340px;padding:8px 11px;'
+ 'background:#0a1628;border:1px solid #2d5a9e;border-radius:8px;font-size:12px;'
+ 'line-height:1.5;color:#e2e8f0;box-shadow:0 6px 20px rgba(0,0,0,.6);'
+ 'pointer-events:none;white-space:normal;';
document.body.appendChild(box);
return box;
}
function place(x, y){
if (!box) return;
const pad = 14, w = box.offsetWidth, h = box.offsetHeight;
let left = x + pad, top = y + pad;
if (left + w > window.innerWidth - 8) left = x - w - pad;
if (top + h > window.innerHeight - 8) top = y - h - pad;
if (left < 4) left = 4;
if (top < 4) top = 4;
box.style.left = left + 'px';
box.style.top = top + 'px';
}
function show(html, x, y){
const b = ensureBox();
b.innerHTML = html;
b.style.display = 'block';
place(x, y);
}
function hide(){ if (box) box.style.display = 'none'; }
function infoEl(t){ return (t && t.closest) ? t.closest('[data-info]') : null; }
document.addEventListener('mouseover', function(e){
const el = infoEl(e.target);
if (!el) return;
const raw = el.getAttribute('data-info');
if (!raw) { hide(); return; }
let html = '';
try { html = decodeURIComponent(raw); } catch (_) { html = raw; }
show(html, e.clientX, e.clientY);
});
document.addEventListener('mousemove', function(e){
if (!box || box.style.display !== 'block') return;
if (infoEl(e.target)) place(e.clientX, e.clientY);
else hide();
});
document.addEventListener('mouseout', function(e){
if (!infoEl(e.target)) return;
const to = e.relatedTarget;
if (!to || !to.closest || !to.closest('[data-info]')) hide();
});
// Build a URI-encoded data-info payload from label/value pairs.
// rows: array of [label, value]; a value that is empty/null/undefined is skipped.
// Pass {talking:true} to prepend a red "Talking" line.
window.buildRowInfo = function(rows, opts){
opts = opts || {};
const parts = [];
if (opts.talking) parts.push('<div style="color:#ff5252;font-weight:bold">&#9679; Talking</div>');
for (const r of (rows || [])){
if (!r) continue;
const v = r[1];
if (v === undefined || v === null || v === '') continue;
parts.push('<div><span style="color:#7dd3fc">' + escHtml(r[0]) + ':</span> ' + escHtml(v) + '</div>');
}
if (!parts.length) return '';
return encodeURIComponent(parts.join(''));
};
})();