节点编辑三项增强

- 删除中间节点自动缝合:线性节点删除后把前驱接到其后继
- 撤销/重做:Ctrl+Z / Ctrl+Y(含连线、删除、移动、改字段,防抖快照)
- 开头节点(nodes[0]):绿色边框+「▶开头」标识,选中事件时自动定位到它
This commit is contained in:
bia
2026-06-08 18:39:04 +08:00
parent 2de308c1e1
commit 188bfbbf7c
4 changed files with 104 additions and 17 deletions

View File

@ -61,10 +61,10 @@
}
// ---------- 节点 HTML ----------
function nodeInner(ir, node) {
function nodeInner(ir, node, isStart) {
const names = roleNames(ir), end = isEnding(node);
const sm = summary(ir, names, end ? Object.assign({ kind: "ending" }, node) : node);
let h = '<div class="nid">#' + esc(node.id) + '</div>'
let h = '<div class="nid">' + (isStart ? '<span class="startflag">▶ 开头</span> ' : '') + '#' + esc(node.id) + '</div>'
+ '<div class="k">' + esc(sm[0]) + '</div>'
+ '<div class="t">' + esc(sm[1] || "") + '</div>';
if (end) {
@ -173,11 +173,15 @@
editor.on("connectionCreated", info => { if (building) return; handleConnect(info); });
editor.on("connectionRemoved", info => { if (building) return; handleDisconnect(info); });
editor.on("nodeRemoved", id => { if (building) return; const ir = dfId2ir[id]; if (ir && cb.onRemove) cb.onRemove(ir); });
// 防误删:焦点在右栏输入框时按 Del/Backspace 不应删画布选中节点
// 键盘 Del删画布选中节点 → 交给 app 做「缝合删除」;焦点在输入框时放行文本编辑;选中连线时交给 Drawflow
document.addEventListener("keydown", e => {
if (e.key === "Delete" || e.key === "Backspace") {
const a = document.activeElement;
if (a && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.isContentEditable)) e.stopPropagation();
if (e.key !== "Delete" && e.key !== "Backspace") return;
const a = document.activeElement;
if (a && (a.tagName === "INPUT" || a.tagName === "TEXTAREA" || a.isContentEditable)) return;
const sel = document.querySelector("#drawflow .drawflow-node.selected");
if (sel) {
const irId = dfId2ir[sel.id.replace("node-", "")];
if (irId && cb.onDeleteSelected) { e.stopPropagation(); e.preventDefault(); cb.onDeleteSelected(irId); }
}
}, true);
},
@ -186,13 +190,14 @@
editor.clear();
dfId2ir = {}; ir2dfId = {};
const layout = ensureLayout(ir);
const startId = (ir.nodes && ir.nodes[0]) ? ir.nodes[0].id : null;
const all = (ir.nodes || []).map(n => ({ node: n, end: false }))
.concat((ir.endings || []).map(e => ({ node: e, end: true })));
all.forEach(({ node, end }) => {
const outN = end ? 0 : getOutlets(node).length;
const pos = layout[node.id] || { x: 40, y: 30 };
const dfId = editor.addNode(node.id, 1, outN, pos.x, pos.y,
"irnode kind-" + (end ? "ending" : node.kind), { irId: node.id }, nodeInner(ir, node));
const cls = "irnode kind-" + (end ? "ending" : node.kind) + (node.id === startId ? " isstart" : "");
const dfId = editor.addNode(node.id, 1, outN, pos.x, pos.y, cls, { irId: node.id }, nodeInner(ir, node, node.id === startId));
dfId2ir[dfId] = node.id; ir2dfId[node.id] = dfId;
});
all.forEach(({ node, end }) => {
@ -210,8 +215,22 @@
_ir = ir;
const dfId = ir2dfId[irId]; if (!dfId) return;
const node = findNode(irId); if (!node) return;
const startId = (ir.nodes && ir.nodes[0]) ? ir.nodes[0].id : null;
const box = document.querySelector("#node-" + dfId + " .drawflow_content_node");
if (box) box.innerHTML = nodeInner(ir, node);
if (box) box.innerHTML = nodeInner(ir, node, irId === startId);
},
// 把画布平移到开头节点ir.nodes[0])附近
focusStart(ir) {
const startId = (ir.nodes && ir.nodes[0]) ? ir.nodes[0].id : null;
if (!startId) return;
const p = (ir._layout || {})[startId]; if (!p) return;
const z = editor.zoom || 1;
const tx = 70 - p.x * z, ty = 90 - p.y * z;
try {
editor.canvas_x = tx; editor.canvas_y = ty;
const pc = editor.precanvas || document.querySelector("#drawflow .drawflow");
if (pc) pc.style.transform = "translate(" + tx + "px, " + ty + "px) scale(" + z + ")";
} catch (e) {}
},
select(irId) {
document.querySelectorAll("#drawflow .drawflow-node.selected").forEach(n => n.classList.remove("selected"));