improve selection
Deploy / deploy (push) Successful in 38s

This commit is contained in:
2026-05-07 19:18:49 +02:00
parent 0f629753f4
commit 658d4d45c5
+102 -21
View File
@@ -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 `
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeId == n.id ? 'active' : ''}" onclick="selectNode(${n.id})">
<div class="list-group-item bg-dark border-secondary node-list-item py-2 ${selectedNodeIds.includes(n.id) ? 'active' : ''}" onclick="selectNode(${n.id}, event.shiftKey)">
<div class="d-flex align-items-center">
<span class="status-dot status-${n.status}"></span>
<i class="fas ${iconMap[n.node_type] || 'fa-circle'} me-2" style="color:${getNodeColorVal(n.node_type)};font-size:.85rem;"></i>
@@ -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) {
<div><span class="text-secondary">Type:</span> ${n.node_type}</div>
<div><span class="text-secondary">Status:</span> ${n.status}</div>
<div><span class="text-secondary">Group:</span> ${n.group_name}</div>
<button class="btn btn-outline-danger btn-sm mt-2" onclick="deleteSelectedNode(${n.id})"><i class="fas fa-trash me-1"></i>Delete</button>
${selectedNodeIds.length > 1 ? `<small class="text-secondary">+${selectedNodeIds.length - 1} more selected</small>` : ''}
<button class="btn btn-outline-danger btn-sm mt-2" onclick="deleteSelectedNodes()"><i class="fas fa-trash me-1"></i>Delete</button>
</div>
`;
}
@@ -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';
}