From 658d4d45c57de45e350553d22445ad1952541f13 Mon Sep 17 00:00:00 2001 From: janis steiner Date: Thu, 7 May 2026 19:18:49 +0200 Subject: [PATCH] improve selection --- frontend/assets/js/app.js | 123 +++++++++++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 21 deletions(-) diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js index 15ddd15..7c9988a 100644 --- a/frontend/assets/js/app.js +++ b/frontend/assets/js/app.js @@ -5,6 +5,7 @@ let nodes = []; let links = []; let shapes = []; let selectedNodeId = null; +let selectedNodeIds = []; let selectedShapeId = null; let canvas, ctx; @@ -20,6 +21,8 @@ let dragType = null; let dragOffX, dragOffY; let dragHandle = null; let dragOrig = null; +let selectStartX, selectStartY; +let selectRect = null; let nextShapeZ = 0; let copyBuffer = null; @@ -63,9 +66,19 @@ document.addEventListener('DOMContentLoaded', () => { } return; } + if ((e.ctrlKey || e.metaKey) && e.key === 'a') { + if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { + e.preventDefault(); + selectedNodeIds = canvasNodes.map(n => n.id); + selectedNodeId = selectedNodeIds.length > 0 ? selectedNodeIds[0] : null; + selectedShapeId = null; + renderNodeList(); renderShapeList(); renderNetwork(); + } + return; + } if (e.key === 'Delete') { if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { - if (selectedNodeId) deleteSelectedNode(selectedNodeId); + if (selectedNodeIds.length > 0) deleteSelectedNodes(); else if (selectedShapeId) deleteSelectedShape(selectedShapeId); } } @@ -240,7 +253,7 @@ function renderNodeList() { list.innerHTML = nodes.map(n => { const iconMap = { host:'fa-desktop', server:'fa-server', router:'fa-route', firewall:'fa-shield-halved', switch:'fa-network-wired', cloud:'fa-cloud', endpoint:'fa-laptop', other:'fa-circle' }; return ` -
+
@@ -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'; }