diff options
| author | tv <tv@krebsco.de> | 2026-11-05 02:05:02 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-05 02:05:02 +0100 |
| commit | df8dba88c5199e8f6822a6d134de5892ed011e60 (patch) | |
| tree | 389874cb5f0cd98a8e512c3460df11296b755c20 /src | |
| parent | 88aa2d13139d304e425177acaaa3b778a1e5d4fc (diff) | |
dashboard: don't update tooltips
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 73 |
1 files changed, 66 insertions, 7 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index a2c862a..b2a5255 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -377,6 +377,58 @@ function el(tag, attrs, ...children) { } return node; } +// Sets `node`'s tooltip (its `title`; none if null), unless the pointer is on it: then +// once the pointer leaves, so an open tooltip doesn't close and open again with every +// update (as `tooltip` does on the map). +const titlesDue = new Map(); +function retitle(node, title) { + if (node.getAttribute("title") === title) { titlesDue.delete(node); return; } + const set = value => value === null ? node.removeAttribute("title") : node.setAttribute("title", value); + if (!node.matches(":hover")) { titlesDue.delete(node); set(title); return; } + if (!titlesDue.has(node)) { + node.addEventListener("mouseleave", () => { + if (!titlesDue.has(node)) return; + const due = titlesDue.get(node); + titlesDue.delete(node); + set(due); + }, { once: true }); + } + titlesDue.set(node, title); +} +// Makes `old` (an element) like `fresh`, one of the same tag, keeping its nodes where +// they are alike, changing only what differs, and tooltips as `retitle` does: drawn +// anew with every update, a table would close the tooltip the pointer is on, and open +// it again. Listeners on `fresh` are not kept: put them on what stays. +function morph(old, fresh) { + for (const attr of old.getAttributeNames()) { + if (!fresh.hasAttribute(attr)) { + if (attr === "title") retitle(old, null); else old.removeAttribute(attr); + } + } + for (const attr of fresh.getAttributeNames()) { + const value = fresh.getAttribute(attr); + if (attr === "title") retitle(old, value); + else if (old.getAttribute(attr) !== value) old.setAttribute(attr, value); + } + morphChildren(old, [...fresh.childNodes]); +} +// `parent`'s children made like `fresh` (`morph`). +function morphChildren(parent, fresh) { + const old = [...parent.childNodes]; + fresh.forEach((node, i) => { + const was = old[i]; + if (was && was.nodeType === Node.TEXT_NODE && node.nodeType === Node.TEXT_NODE) { + if (was.nodeValue !== node.nodeValue) was.nodeValue = node.nodeValue; + } else if (was && was.nodeType === Node.ELEMENT_NODE && was.nodeName === node.nodeName) { + morph(was, node); + } else if (was) { + parent.replaceChild(node, was); + } else { + parent.append(node); + } + }); + for (const extra of old.slice(fresh.length)) extra.remove(); +} const SVG = "http://www.w3.org/2000/svg"; function svg(tag, attrs) { const node = document.createElementNS(SVG, tag); @@ -429,6 +481,15 @@ function focus(id) { } // --- Peers --- +// On press (the table may be drawn anew, with ages, before a click would complete): the +// row's peer in focus, or none if it was. On the table, as its rows are kept and +// changed (`morph`), whoever they show. +document.getElementById("peers").addEventListener("pointerdown", event => { + const row = event.target.closest("tr[data-peer]"); + if (!row) return; + const id = row.getAttribute("data-peer"); + focus(id === focused ? null : id); +}); function renderPeers() { const table = document.getElementById("peers"); const headers = { reaches: "How many members besides this node it reaches (an active " + @@ -465,11 +526,9 @@ function renderPeers() { reachesCell(peer.reaches, ours, id), el("td", {}, peer.sameEpoch === null || peer.sameEpoch === undefined ? "" : peer.sameEpoch ? "yes" : "no")); - // On press: the table may be drawn anew (ages) before a click would complete. - row.addEventListener("pointerdown", () => focus(id === focused ? null : id)); rows.push(row); } - table.replaceChildren(...rows); + morphChildren(table, rows); // As tall as the rows, up to 8 of them (and the header); then it scrolls. const scroll = document.getElementById("peers-scroll"); scroll.style.maxHeight = rows.length > 9 @@ -572,7 +631,7 @@ function renderStats() { const field = (key, value) => el("div", { class: "leaf" }, el("span", { class: "key" }, key + ": "), value); title.replaceChildren("Message: ", what); - title.title = title.textContent; + retitle(title, title.textContent); document.getElementById("stats").replaceChildren( field("at", clockTime(new Date(message.at))), field(message.way === "received" ? "from" : "to", message.peer), @@ -586,7 +645,7 @@ function renderStats() { if (isPair(focused)) { const [a, b] = focused.split(" "); what.textContent = name(a) + " – " + name(b); - what.title = focused; + retitle(what, focused); const listed = (x, y) => (state.graph.intended[x] || []).includes(y); shown = [el("div", { class: "leaf" }, el("span", { class: "key" }, "intended: "), listed(a, b) || listed(b, a) ? "yes (member list)" : "no")]; @@ -611,7 +670,7 @@ function renderStats() { destination: peer.destination, addresses: peer.addresses }, "paths")); } title.replaceChildren("Link: ", what); - title.title = title.textContent; + retitle(title, title.textContent); document.getElementById("stats").replaceChildren(...shown); return; } @@ -631,7 +690,7 @@ function renderStats() { if (report) shown.push(tree("links", report, "links")); } title.replaceChildren(focused === null ? "Figures: " : "Peer: ", what); - title.title = title.textContent; + retitle(title, title.textContent); document.getElementById("stats").replaceChildren(...shown); } |
