aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-31 22:52:58 +0100
committertv <tv@krebsco.de>2026-10-31 22:52:58 +0100
commit42a8c78d6321742a2169b6c10270aa15b186c2b6 (patch)
tree748d35bfeec369dda4d610f2555c4dc86eeea2b9
parentdaa05b40d31a3559134ecdcb0d4e1f1b0d3b0017 (diff)
dashboard: double-click pins and unpins
-rw-r--r--src/api/dashboard/dashboard.html35
1 files changed, 25 insertions, 10 deletions
diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html
index ccf4892..5854f61 100644
--- a/src/api/dashboard/dashboard.html
+++ b/src/api/dashboard/dashboard.html
@@ -169,7 +169,7 @@
<div class="legend" id="legend"></div>
<div class="legend">
<span class="hint">drag to pan, wheel or pinch to zoom, turn with two fingers;
- drag a node to pin it, double-click it to let go</span>
+ drag a node to move it, double-click it to pin it there or let it go</span>
</div>
</div>
<svg id="graph"><g id="view"></g></svg>
@@ -496,7 +496,7 @@ function relax(nodes, pairs, width, height, gain, most, only) {
}
let moved = 0;
for (const id of nodes) {
- if (pinned.has(id) || (only && !only.has(id))) continue;
+ if (pinned.has(id) || id === grabbed || (only && !only.has(id))) continue;
const p = positions.get(id), f = force.get(id);
const fx = f.x * gain, fy = f.y * gain;
const x = Math.min(width - 40, Math.max(40, p.x + Math.max(-most, Math.min(most, fx))));
@@ -807,7 +807,7 @@ function renderGraph() {
(dormant ? " (dormant)" : "") +
(clock && clock.off ? " (clock " + seconds(clock.offsetMs) + " off the swarm's, ±" +
clock.withinMs + " ms)" : ""));
- // Pinned (dragged there): a dot in the middle.
+ // Pinned (double-clicked): a dot in the middle.
const pin = pinned.has(id) ? [drawing("pin " + id, "circle", { cx: p.x, cy: p.y, r: 2,
fill: member.self || reported ? "var(--panel)" : "var(--fg)",
"pointer-events": "none" })] : [];
@@ -1308,12 +1308,14 @@ graph.addEventListener("pointerup", event => {
const click = press && !press.moved ? press.click : null;
press = null;
graph.classList.remove("dragging");
+ dropped();
if (click) click();
});
graph.addEventListener("pointercancel", event => {
untouched(event);
press = null;
graph.classList.remove("dragging");
+ dropped();
});
// The map dragged: it pans; a click, `click`.
function pan(event, click) {
@@ -1331,34 +1333,47 @@ graph.addEventListener("pointerdown", event => {
});
// Not the browser's drag and drop of the picture.
graph.addEventListener("dragstart", event => event.preventDefault());
-// A node: dragged, it stays where it is put (`pinned`), and the layout places the others
-// around it; a click, it is in focus; a second one soon after, it's let go (`unpin`).
-// Told apart here: the map holds the pointer while pressed (`pressing`), so the
-// browser's clicks and double-clicks go to the map, not the node.
+// A node: dragged, it moves, held where the pointer is (`grabbed`) until let go; a click,
+// it is in focus; a second one soon after pins it where it is (`pinned`: the layout
+// leaves it there), or lets it go if pinned (`unpin`). Told apart here: the map holds
+// the pointer while pressed (`pressing`), so the browser's clicks and double-clicks
+// go to the map, not the node.
let lastNodeClick = { id: null, at: 0 };
const DOUBLE_CLICK_MS = 400;
+let grabbed = null;
function drag(event, id) {
const p = positions.get(id), from = { x: p.x, y: p.y };
+ grabbed = id;
pressing(event, (dx, dy) => {
// The pointer's move, on the map (unturned and unscaled).
const [x, y] = turned(dx, dy, -view.turn);
p.x = from.x + x / view.k;
p.y = from.y + y / view.k;
- pinned.add(id);
renderGraph();
- // Resized by round trip: the others follow it at once.
+ // Dynamic: the others follow it at once.
if (byRtt) settle();
}, () => {
const now = performance.now();
if (lastNodeClick.id === id && now - lastNodeClick.at < DOUBLE_CLICK_MS) {
lastNodeClick = { id: null, at: now };
- unpin(id);
+ if (pinned.has(id)) unpin(id); else pin(id);
} else {
lastNodeClick = { id, at: now };
focus(id);
}
});
}
+// A dragged node let go of: dynamic, it moves to its place among the others again.
+function dropped() {
+ if (grabbed === null) return;
+ grabbed = null;
+ if (byRtt) settle();
+}
+// A node pinned where it is: the layout leaves it there.
+function pin(id) {
+ pinned.add(id);
+ renderGraph();
+}
// A node let go: it moves to its place among the others, smoothly (`settle`); on a
// static map, only it moves.
function unpin(id) {