@@ -268,8 +281,15 @@ function renderShapeList() {
`).join('');
}
-function selectNode(id) {
- selectedNodeId = id;
+function selectNode(id, add = false) {
+ if (add) {
+ const idx = selectedNodeIds.indexOf(id);
+ if (idx >= 0) { selectedNodeIds.splice(idx, 1); if (selectedNodeIds.length === 0) selectedNodeId = null; else selectedNodeId = selectedNodeIds[0]; }
+ else { selectedNodeIds.push(id); selectedNodeId = id; }
+ } else {
+ selectedNodeId = id;
+ selectedNodeIds = [id];
+ }
selectedShapeId = null;
const n = nodes.find(x => x.id == id);
if (n) {
@@ -280,7 +300,8 @@ function selectNode(id) {
Type: ${n.node_type}
Status: ${n.status}
Group: ${n.group_name}
-
+ ${selectedNodeIds.length > 1 ? `
+${selectedNodeIds.length - 1} more selected` : ''}
+
`;
}
@@ -297,12 +318,14 @@ function selectShape(id) {
renderNetwork();
}
-async function deleteSelectedNode(id) {
- if (!id) return;
- const ok = await showConfirm('Delete this node and its connections?');
+async function deleteSelectedNodes() {
+ const ids = [...selectedNodeIds];
+ if (!ids.length) return;
+ const ok = await showConfirm(`Delete ${ids.length} node(s) and their connections?`);
if (!ok) return;
selectedNodeId = null;
- await apiFetch(`nodes/${id}`, { method: 'DELETE' });
+ selectedNodeIds = [];
+ for (const id of ids) await apiFetch(`nodes/${id}`, { method: 'DELETE' });
loadNetworkData();
}
@@ -503,6 +526,18 @@ function renderNetwork() {
});
canvasNodes.forEach(drawCanvasNode);
+
+ // Selection rectangle
+ if (selectRect && (selectRect.w > 0 || selectRect.h > 0)) {
+ ctx.strokeStyle = '#3b82f6';
+ ctx.lineWidth = 1.5;
+ ctx.setLineDash([4, 4]);
+ ctx.strokeRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
+ ctx.fillStyle = 'rgba(59, 130, 246, 0.08)';
+ ctx.fillRect(selectRect.x, selectRect.y, selectRect.w, selectRect.h);
+ ctx.setLineDash([]);
+ }
+
ctx.restore();
}
@@ -547,7 +582,7 @@ function drawShape(s) {
}
function drawCanvasNode(n) {
- const sel = selectedNodeId == n.id;
+ const sel = selectedNodeIds.includes(n.id);
ctx.save();
if (sel) { ctx.shadowColor = n.color; ctx.shadowBlur = 18; }
@@ -639,9 +674,18 @@ function onMouseDown(e) {
const node = getCanvasNodeAt(mx, my);
if (node) {
- dragType = 'node'; dragTarget = node;
- dragOffX = mx - node.x; dragOffY = my - node.y;
- selectNode(node.id);
+ if (e.shiftKey) {
+ selectNode(node.id, true);
+ // Start multi-drag for all selected
+ dragType = 'node'; dragTarget = node;
+ dragOffX = mx - node.x; dragOffY = my - node.y;
+ } else {
+ if (!selectedNodeIds.includes(node.id)) {
+ selectNode(node.id);
+ }
+ dragType = 'node'; dragTarget = node;
+ dragOffX = mx - node.x; dragOffY = my - node.y;
+ }
return;
}
@@ -661,19 +705,23 @@ function onMouseDown(e) {
return;
}
- selectedNodeId = null; selectedShapeId = null;
+ selectedNodeId = null; selectedNodeIds = []; selectedShapeId = null;
renderNodeList(); renderShapeList();
- isPanning = true;
- panStartX = e.clientX - panX; panStartY = e.clientY - panY;
- canvas.style.cursor = 'grabbing';
+ dragType = 'select';
+ selectStartX = mx; selectStartY = my;
+ selectRect = { x: mx, y: my, w: 0, h: 0 };
}
function onMouseMove(e) {
const rect = canvas.getBoundingClientRect();
if (dragType === 'node' && dragTarget) {
- dragTarget.x = e.clientX - rect.left - panX - dragOffX;
- dragTarget.y = e.clientY - rect.top - panY - dragOffY;
+ const dx = e.clientX - rect.left - panX - dragOffX - dragTarget.x;
+ const dy = e.clientY - rect.top - panY - dragOffY - dragTarget.y;
+ // Move all selected nodes by delta
+ for (const c of canvasNodes) {
+ if (selectedNodeIds.includes(c.id)) { c.x += dx; c.y += dy; }
+ }
renderNetwork();
return;
}
@@ -699,6 +747,16 @@ function onMouseMove(e) {
renderNetwork();
return;
}
+ if (dragType === 'select') {
+ const mx = e.clientX - rect.left - panX;
+ const my = e.clientY - rect.top - panY;
+ selectRect.x = Math.min(selectStartX, mx);
+ selectRect.y = Math.min(selectStartY, my);
+ selectRect.w = Math.abs(mx - selectStartX);
+ selectRect.h = Math.abs(my - selectStartY);
+ renderNetwork();
+ return;
+ }
if (isPanning) {
panX = e.clientX - panStartX; panY = e.clientY - panStartY;
renderNetwork();
@@ -713,12 +771,35 @@ function onMouseMove(e) {
}
function onMouseUp(e) {
- if (dragTarget && dragType === 'node') {
- apiFetch(`nodes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y }) });
+ if (dragType === 'node') {
+ for (const c of canvasNodes) {
+ if (selectedNodeIds.includes(c.id)) {
+ apiFetch(`nodes/${c.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: c.x, pos_y: c.y }) });
+ }
+ }
} else if (dragTarget && (dragType === 'shape' || dragType === 'resize')) {
apiFetch(`shapes/${dragTarget.id}`, { method: 'PUT', body: JSON.stringify({ pos_x: dragTarget.x, pos_y: dragTarget.y, width: dragTarget.w, height: dragTarget.h }) });
+ } else if (dragType === 'select') {
+ selectRect = null;
+ const rect = canvas.getBoundingClientRect();
+ const mx = e.clientX - rect.left - panX;
+ const my = e.clientY - rect.top - panY;
+ const rx = Math.min(selectStartX, mx);
+ const ry = Math.min(selectStartY, my);
+ const rw = Math.abs(mx - selectStartX);
+ const rh = Math.abs(my - selectStartY);
+ if (rw > 5 || rh > 5) {
+ const found = canvasNodes.filter(n =>
+ n.x >= rx && n.x <= rx + rw && n.y >= ry && n.y <= ry + rh
+ );
+ selectedNodeIds = found.map(n => n.id);
+ selectedNodeId = found.length > 0 ? found[0].id : null;
+ if (found.length > 0) selectNode(found[0].id);
+ else { selectedNodeId = null; selectedNodeIds = []; renderNodeList(); renderShapeList(); renderNetwork(); }
+ }
}
dragType = null; dragTarget = null; dragOrig = null;
+ selectRect = null;
isPanning = false;
canvas.style.cursor = 'grab';
}