From 4beb2e1715e57ee93b81e30b4ef7391a5be8791a Mon Sep 17 00:00:00 2001 From: janis steiner Date: Thu, 7 May 2026 19:04:23 +0200 Subject: [PATCH] improve copy & paste --- frontend/assets/js/app.js | 69 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 67 insertions(+), 2 deletions(-) diff --git a/frontend/assets/js/app.js b/frontend/assets/js/app.js index b827e01..b08b082 100644 --- a/frontend/assets/js/app.js +++ b/frontend/assets/js/app.js @@ -22,6 +22,7 @@ let dragHandle = null; let dragOrig = null; let nextShapeZ = 0; +let copyBuffer = null; document.addEventListener('DOMContentLoaded', () => { canvas = document.getElementById('networkCanvas'); @@ -49,6 +50,19 @@ document.addEventListener('DOMContentLoaded', () => { window.addEventListener('resize', () => { resizeCanvas(); renderNetwork(); }); document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'c') { + if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { + if (selectedNodeId) copyNode(selectedNodeId); + else if (selectedShapeId) copyShape(selectedShapeId); + } + return; + } + if ((e.ctrlKey || e.metaKey) && e.key === 'v') { + if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { + if (copyBuffer) pasteItem(); + } + return; + } if (e.key === 'Delete') { if (!document.activeElement || document.activeElement.tagName !== 'INPUT') { if (selectedNodeId) deleteSelectedNode(selectedNodeId); @@ -301,6 +315,57 @@ async function deleteSelectedShape(id) { loadNetworkData(); } +function copyNode(id) { + const n = nodes.find(x => x.id == id); + if (!n) return; + copyBuffer = { type: 'node', data: { ...n } }; +} + +function copyShape(id) { + const s = shapes.find(x => x.id == id); + if (!s) return; + copyBuffer = { type: 'shape', data: { ...s } }; +} + +async function pasteItem() { + if (!copyBuffer) return; + const offset = 30; + if (copyBuffer.type === 'node') { + const d = copyBuffer.data; + await apiFetch('nodes', { + method: 'POST', + body: JSON.stringify({ + label: d.label + ' (copy)', + ip_address: d.ip_address, + node_type: d.node_type, + status: d.status, + group_name: d.group_name, + pos_x: (parseFloat(d.pos_x) || 100) + offset, + pos_y: (parseFloat(d.pos_y) || 100) + offset + }) + }); + loadNetworkData(); + } else if (copyBuffer.type === 'shape') { + const d = copyBuffer.data; + await apiFetch('shapes', { + method: 'POST', + body: JSON.stringify({ + label: d.label + ' (copy)', + shape_type: d.shape_type, + pos_x: (parseFloat(d.pos_x) || 100) + offset, + pos_y: (parseFloat(d.pos_y) || 100) + offset, + width: d.width || 200, + height: d.height || 150, + color: d.color || '#1e3a5f', + border_color: d.border_color || '#3b82f6', + opacity: parseFloat(d.opacity) || 0.15, + z_index: nextShapeZ++ + }) + }); + loadNetworkData(); + } +} + function showConfirm(msg) { return new Promise((resolve) => { const modalEl = document.getElementById('confirmModal'); @@ -381,7 +446,7 @@ const NODE_FA_ICONS = { server: { icon: '\uf233', color: '#8b5cf6' }, router: { icon: '\uf4d8', color: '#f59e0b' }, firewall: { icon: '\uf6ed', color: '#ef4444' }, - switch: { icon: '\uf2d1', color: '#06b6d4' }, + switch: { icon: '\uf0e8', color: '#06b6d4' }, cloud: { icon: '\uf0c2', color: '#22c55e' }, endpoint: { icon: '\uf109', color: '#ec4899' }, other: { icon: '\uf111', color: '#6b7280' } @@ -501,7 +566,7 @@ function drawCanvasNode(n) { // Draw icon using Font Awesome ctx.save(); - ctx.font = '500 22px "Font Awesome 6 Free", "FontAwesome", "Font Awesome 5 Free", sans-serif'; + ctx.font = '900 22px "Font Awesome 6 Free", "FontAwesome", "Font Awesome 5 Free"'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = n.color;