diff options
Diffstat (limited to 'src/api/dashboard/dashboard.html')
| -rw-r--r-- | src/api/dashboard/dashboard.html | 67 |
1 files changed, 61 insertions, 6 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index 7125ad9..e645b02 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -252,10 +252,11 @@ class="toggle">messages: on</button></div> <div class="legend"> <span class="hint">drag to pan, wheel or pinch to zoom, turn with two fingers; - drag a node to move it, middle-click it to pin it there or let it go</span> + drag a node to move it, middle-click it to pin it there or let it go, + double-click it for its dashboard</span> </div> </div> - <svg id="graph"><g id="view"><g id="flows" pointer-events="none"></g></g></svg> + <svg id="graph"><g id="view"><g id="flows" pointer-events="none"></g><g id="flashes" pointer-events="none"></g></g></svg> </section> <section id="peers-box"> <h2>Peers</h2> @@ -1613,10 +1614,13 @@ graph.addEventListener("pointerdown", event => { // Not the browser's drag and drop of the picture. graph.addEventListener("dragstart", event => event.preventDefault()); // A node: dragged, it moves, held where the pointer is (`grabbed`) until let go; a click, -// it is in focus. A middle click pins it where it is (`pinned`: the layout leaves it +// it is in focus; a second one soon after opens its dashboard (`openDashboard`). A +// middle click pins it where it is (`pinned`: the layout leaves it // there), or lets it go if pinned (`unpin`); dragged with the middle button, it is // pinned where it's dropped. Told apart here: the map holds the pointer while pressed // (`pressing`), so the browser's clicks go to the map, not the node. +let lastNodeClick = { id: null, at: 0 }; +const DOUBLE_CLICK_MS = 400; let grabbed = null; // Of the press under way on a node: whether it's the middle button's, and whether it // moved the node. @@ -1638,11 +1642,62 @@ function drag(event, id) { // Dynamic: the others follow it at once. if (byRtt) settle(); }, () => { - if (!middlePress) focus(id); - else if (pinned.has(id)) unpin(id); - else pin(id); + if (middlePress) { + if (pinned.has(id)) unpin(id); else pin(id); + return; + } + const now = performance.now(); + if (lastNodeClick.id === id && now - lastNodeClick.at < DOUBLE_CLICK_MS) { + lastNodeClick = { id: null, at: 0 }; + openDashboard(id); + } else { + lastNodeClick = { id, at: now }; + focus(id); + } }); } +// A node's dashboard, where it publishes one (`--dashboard-url`, in the member list), +// opened in this page; else its halo flashes red, larger than usual, and "no dashboard" +// rises from it in the same red, both fading out. This node's own: here already. +const FLASH_MS = 1200; +function openDashboard(id) { + const member = state.graph.members[id]; + if (!member || member.self) return; + if (member.dashboard) { + location.assign(member.dashboard); + return; + } + const layer = document.getElementById("flashes"); + const halo = svg("circle", { r: 17, fill: "var(--bad)", "fill-opacity": .5 }); + const words = svg("text", { "text-anchor": "middle", "font-size": 12, "font-weight": 600, + fill: "var(--bad)", "fill-opacity": .5 }); + words.textContent = "no dashboard"; + layer.append(halo, words); + const started = performance.now(); + const step = now => { + const p = positions.get(id); + const t = Math.min(1, Math.max(0, (now - started) / FLASH_MS)); + if (!p || t >= 1) { + halo.remove(); + words.remove(); + return; + } + const opacity = String(1 - t); + halo.setAttribute("cx", p.x); + halo.setAttribute("cy", p.y); + halo.setAttribute("opacity", opacity); + // Up on the screen, however the map is turned or zoomed: from above the node, slowly. + const [x, y] = turned(0, -(22 + (calm.matches ? 0 : 30 * t)), -view.turn); + const attrs = upright({ x: p.x + x / view.k, y: p.y + y / view.k }); + words.setAttribute("x", attrs.x); + words.setAttribute("y", attrs.y); + if (attrs.transform) words.setAttribute("transform", attrs.transform); + else words.removeAttribute("transform"); + words.setAttribute("opacity", opacity); + requestAnimationFrame(step); + }; + requestAnimationFrame(step); +} // A dragged node let go of: dynamic, it moves to its place among the others again; // dragged with the middle button, it is pinned there. function dropped() { |
