// SvxPortal — 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('