Files

92 lines
3.3 KiB
JavaScript

// Reflector Dashboard — web front-end for SvxReflector
// Copyright (C) 2026 SA7AUX (Björn)
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version. See the LICENSE file for details.
'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(''));
};
})();