92 lines
3.3 KiB
JavaScript
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">● 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(''));
|
|
};
|
|
})();
|