fix: pin click should not bubble to canvas (was placing waypoints under notes)

Clicking a Note (or any pin) in Route mode used to:
  1. fire the canvas click handler → drop a waypoint at the note's spot
  2. only THEN run the pin's own click/dblclick handlers

So a single click on a note added a stray waypoint, and a double-click added two and may not have reliably reached the dblclick handler. Edit-via-dblclick therefore felt broken.

Fix: every interactive pin (note, text label, route waypoint, pull marker) now stops 'click' propagation explicitly. Click and dblclick on a pin no longer affect the active tool. Drag detection bumped to a 3px threshold and now only persists history when the pin actually moved.
This commit is contained in:
2026-04-25 23:22:04 +02:00
parent 724ae08394
commit 48c401909e
+70 -30
View File
@@ -254,14 +254,20 @@ function makeNotePin(note, idx) {
g.dataset.tooltip = note.text || "(empty)";
// drag-to-move + right-click delete + double-click to edit text
// Click on a pin must NOT bubble to the SVG (which would add a waypoint
// / pull / etc. at the same spot, depending on the active tool).
g.addEventListener("click", (e) => e.stopPropagation());
g.addEventListener("pointerdown", (e) => {
e.stopPropagation();
state.drag = { kind: "note", idx };
state.drag = { kind: "note", idx, moved: false, downX: e.clientX, downY: e.clientY };
g.classList.add("dragging");
g.setPointerCapture(e.pointerId);
});
g.addEventListener("pointermove", (e) => {
if (!state.drag || state.drag.kind !== "note" || state.drag.idx !== idx) return;
if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) {
state.drag.moved = true;
}
const pt = svgPointFromEvent(e);
const arr = state.notes[currentKey()];
arr[idx].x = pt.x; arr[idx].y = pt.y;
@@ -269,29 +275,40 @@ function makeNotePin(note, idx) {
});
g.addEventListener("pointerup", () => {
if (!state.drag) return;
const moved = state.drag.moved;
state.drag = null;
g.classList.remove("dragging");
pushHistory();
updateHash();
});
g.addEventListener("contextmenu", (e) => {
e.preventDefault();
removeNote(idx);
});
g.addEventListener("dblclick", (e) => {
e.preventDefault();
e.stopPropagation();
const txt = prompt("Edit note text", note.text || "");
if (txt !== null) {
state.notes[currentKey()][idx].text = txt;
if (moved) {
pushHistory();
renderOverlay();
updateHash();
}
});
g.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
removeNote(idx);
});
// Double-click → edit. Works regardless of active tool because the
// pin captures click + dblclick before the canvas click handler runs.
g.addEventListener("dblclick", (e) => {
e.preventDefault();
e.stopPropagation();
editNote(idx);
});
return g;
}
function editNote(idx) {
const cur = state.notes[currentKey()]?.[idx];
if (!cur) return;
const txt = prompt("Edit note text:", cur.text || "");
if (txt === null) return;
cur.text = txt;
pushHistory();
renderOverlay();
updateHash();
}
function removeNote(idx) {
const key = currentKey();
const arr = state.notes[key];
@@ -338,15 +355,21 @@ function makeTextLabel(item, idx) {
hit.setAttribute("fill", "transparent");
g.appendChild(hit);
// Drag / right-click delete / dbl-click edit
// Stop click propagating so the canvas's tool handler doesn't fire when
// the user is interacting with this label (otherwise editing in Route
// mode would also drop a waypoint underneath).
g.addEventListener("click", (e) => e.stopPropagation());
g.addEventListener("pointerdown", (e) => {
e.stopPropagation();
state.drag = { kind: "text", idx };
state.drag = { kind: "text", idx, moved: false, downX: e.clientX, downY: e.clientY };
g.classList.add("dragging");
g.setPointerCapture(e.pointerId);
});
g.addEventListener("pointermove", (e) => {
if (!state.drag || state.drag.kind !== "text" || state.drag.idx !== idx) return;
if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) {
state.drag.moved = true;
}
const pt = svgPointFromEvent(e);
const arr = state.texts[currentKey()];
arr[idx].x = pt.x; arr[idx].y = pt.y;
@@ -354,25 +377,23 @@ function makeTextLabel(item, idx) {
});
g.addEventListener("pointerup", () => {
if (!state.drag) return;
const moved = state.drag.moved;
state.drag = null;
g.classList.remove("dragging");
pushHistory();
updateHash();
if (moved) {
pushHistory();
updateHash();
}
});
g.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
removeText(idx);
});
g.addEventListener("dblclick", (e) => {
e.preventDefault();
e.stopPropagation();
const txt = prompt("Edit label", item.text || "");
if (txt !== null) {
state.texts[currentKey()][idx].text = txt;
pushHistory();
renderOverlay();
updateHash();
}
editText(idx);
});
return g;
}
@@ -387,6 +408,17 @@ function removeText(idx) {
updateHash();
}
function editText(idx) {
const cur = state.texts[currentKey()]?.[idx];
if (!cur) return;
const txt = prompt("Edit label:", cur.text || "");
if (txt === null) return;
cur.text = txt;
pushHistory();
renderOverlay();
updateHash();
}
// kg classification IDs:
// 1 = minor (NPCs, ambient mobs)
// 2 = standard trash
@@ -555,14 +587,18 @@ function makeUserPin(x, y, label, cssClass, kind, idx) {
t.textContent = label;
g.appendChild(t);
g.addEventListener("click", (e) => e.stopPropagation());
g.addEventListener("pointerdown", (e) => {
e.stopPropagation();
state.drag = { kind, idx };
state.drag = { kind, idx, moved: false, downX: e.clientX, downY: e.clientY };
g.classList.add("dragging");
g.setPointerCapture(e.pointerId);
});
g.addEventListener("pointermove", (e) => {
if (!state.drag || state.drag.idx !== idx || state.drag.kind !== kind) return;
if (Math.hypot(e.clientX - state.drag.downX, e.clientY - state.drag.downY) > 3) {
state.drag.moved = true;
}
const pt = svgPointFromEvent(e);
const arr = kind === "route" ? state.routes[currentKey()] : state.pulls[currentKey()];
arr[idx] = { x: pt.x, y: pt.y };
@@ -579,14 +615,18 @@ function makeUserPin(x, y, label, cssClass, kind, idx) {
});
g.addEventListener("pointerup", () => {
if (!state.drag) return;
const moved = state.drag.moved;
state.drag = null;
g.classList.remove("dragging");
pushHistory();
renderInfoPane();
updateHash();
if (moved) {
pushHistory();
renderInfoPane();
updateHash();
}
});
g.addEventListener("contextmenu", (e) => {
e.preventDefault();
e.stopPropagation();
removePoint(kind, idx);
});
return g;