@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user