App-Netdisco
view release on metacpan or search on metacpan
share/public/javascripts/netdisco-netmap.js view on Meta::CPAN
hit = n;
}
});
return hit;
},
positions: function () {
return fg.graphData().nodes.map(function (n) {
return {
ID: n.ID,
x: Math.round(n.x),
y: Math.round(n.y),
fixed: n.fx !== undefined && n.fx !== null ? 1 : 0
};
});
},
links: function () {
return fg.graphData().links.map(function (l) {
return { source: endpointId(l, 'source'), target: endpointId(l, 'target') };
});
},
screenXY: function (id) {
const n = graph.nodeDataById(id);
if (!n) {
return null;
}
const p = fg.graph2ScreenCoords(n.x, n.y);
const canvas = container.querySelector('canvas');
if (!canvas) {
return null;
}
const r = canvas.getBoundingClientRect();
return { x: r.left + p.x, y: r.top + p.y };
}
};
ndWindow.graph = graph;
// force-graph exposes no simulation find() and no dblclick callback; its
// own hit detection delivers the node to onNodeClick, so a double click is
// two clicks on the same node inside the double-click window
let lastClick = { id: null, at: 0 };
fg.onNodeClick(function (n) {
const now = Date.now();
// 500 ms matches the platform double-click default the old renderer's
// dblclick event inherited
if (n.ID === lastClick.id && now - lastClick.at < 500) {
window.location.assign(n.LINK);
return;
}
lastClick = { id: n.ID, at: now };
});
fg.linkCurvature(function (l) {
const s = endpointId(l, 'source'),
t = endpointId(l, 'target');
return s === t ? 0.6 : 0;
});
// the old template zoomed to the center node 1.5 s after start when
// mapshow=neighbors (a legacy value still reachable from bookmarks)
if (map.dataset.ndMapshow === 'neighbors') {
setTimeout(function () {
const n = graph.nodeDataById(graph.centernode);
if (n) {
fg.centerAt(n.x, n.y, 600);
fg.zoom(4, 600);
}
}, 1500);
}
// box select: shift-drag replaces the old freehand lasso by ruling.
// capture-phase listener so force-graph's own pan never sees the drag.
/** @type {{active: boolean, x0: number, y0: number, el: HTMLElement|null}} */
const box = { active: false, x0: 0, y0: 0, el: null };
container.addEventListener(
'pointerdown',
function (ev) {
if (!ev.shiftKey) {
return;
}
ev.stopPropagation();
ev.preventDefault();
fg.enablePanInteraction(false).enableZoomInteraction(false);
box.active = true;
box.x0 = ev.clientX;
box.y0 = ev.clientY;
box.el = document.createElement('div');
box.el.id = 'nd2_netmap-boxselect';
// document.body sits outside the fullscreen element, so a box drawn
// there would be invisible while fullscreen; append into whichever is
// actually showing
(document.fullscreenElement || document.body).appendChild(box.el);
},
true
);
/**
* Resizes and repositions the box-select rectangle to track the pointer during a
* shift-drag; does nothing when no box-select is active.
* @param {PointerEvent} ev the pointermove event
* @returns {void}
*/
function onBoxPointerMove(ev) {
if (!box.active || !box.el) {
return;
}
const x = Math.min(box.x0, ev.clientX),
y = Math.min(box.y0, ev.clientY);
box.el.style.left = x + 'px';
box.el.style.top = y + 'px';
box.el.style.width = Math.abs(ev.clientX - box.x0) + 'px';
box.el.style.height = Math.abs(ev.clientY - box.y0) + 'px';
}
/**
* Finishes a box-select drag: removes the selection rectangle, restores pan and
* zoom, and marks every node inside the box as selected. Does nothing when no
* box-select is active.
* @param {PointerEvent} ev the pointerup event
* @returns {void}
*/
function onBoxPointerUp(ev) {
if (!box.active || !box.el || !container) {
return;
share/public/javascripts/netdisco-netmap.js view on Meta::CPAN
ndWindow.__ndNetmapPointerHandlers = { move: onBoxPointerMove, up: onBoxPointerUp };
window.addEventListener('pointermove', onBoxPointerMove);
window.addEventListener('pointerup', onBoxPointerUp);
fg.onNodeDrag(function (n, translate) {
if (!n.selected) {
return;
}
if (!dragSnap) {
// hold the node objects themselves, not their IDs: an ID-keyed lookup
// means a linear nodeDataById() scan per node per tick, and re-keying
// by ID risks the numeric-vs-string coercion Object.keys() does
dragSnap = [];
fg.graphData().nodes.forEach(function (o) {
if (o.selected && o.ID !== n.ID) {
dragSnap.push({ node: o, x: o.x, y: o.y });
}
});
}
// translate is the per-tick incremental delta, not cumulative from drag
// start, so the snapshot itself has to accumulate it tick by tick
dragSnap.forEach(function (entry) {
entry.x += translate.x;
entry.y += translate.y;
entry.node.fx = entry.node.x = entry.x;
entry.node.fy = entry.node.y = entry.y;
});
});
// fullscreen: same API dance the old template used, on the pane so the
// sidebar stays outside it
const fullscreenButton = document.getElementById('nd2_netmap-fullscreen');
if (fullscreenButton) {
fullscreenButton.addEventListener('click', function () {
const netmapPane = document.getElementById('netmap_pane');
if (netmapPane) {
requestFullScreen(netmapPane);
}
});
}
const signal = beginRenderListeners();
['webkitfullscreenchange', 'mozfullscreenchange', 'fullscreenchange'].forEach(function (name) {
document.addEventListener(
name,
function () {
resizeGraphContainer();
document.querySelectorAll('#nd2_netmap-fullscreen i').forEach(function (el) {
el.className = isFullScreen() ? 'fas fa-compress fa-lg' : 'fas fa-expand fa-lg';
});
},
{ signal: signal }
);
});
/**
* Resizes the graph canvas to the pane's current width after a short delay,
* letting the sidebar toggle or fullscreen transition finish first.
* @returns {void}
*/
function resizeGraphContainer() {
setTimeout(function () {
const resizePaneEl = document.getElementById('netmap_pane');
const resizePaneParent = resizePaneEl && resizePaneEl.parentElement;
fg.width(parseInt(resizePaneParent ? getComputedStyle(resizePaneParent).width : '0')).height(
window.innerHeight - 100
);
}, 500);
}
const sidebarToggleIn = document.getElementById('nd_sidebar-toggle-img-in');
if (sidebarToggleIn) sidebarToggleIn.addEventListener('click', resizeGraphContainer, { signal: signal });
const sidebarToggleOut = document.getElementById('nd_sidebar-toggle-img-out');
if (sidebarToggleOut) sidebarToggleOut.addEventListener('click', resizeGraphContainer, { signal: signal });
window.addEventListener('resize', resizeGraphContainer, { signal: signal });
// onEngineTick is a setter, not a subscription, so the tick count lives in
// this handler rather than a second one that would replace it
fg.onEngineTick(function () {
ticksSinceStop++;
setSpinnerState('nd_netmap-running');
});
// labels draw above this zoom
const LABEL_ZOOM = +(map.dataset.ndLabelZoom || 0.9);
const LABEL_SIZE = +(map.dataset.ndLabelSize || 8);
// read once, not once per node per frame
const showips = document.getElementById('nd_showips');
fg.nodeCanvasObjectMode(function () {
return 'after';
}).nodeCanvasObject(function (n, ctx, scale) {
if (n.selected) {
ctx.beginPath();
ctx.arc(n.x, n.y, n.radius + 2, 0, 2 * Math.PI);
ctx.strokeStyle = '#0d6efd';
ctx.lineWidth = 1.5 / scale;
ctx.stroke();
}
if (scale < LABEL_ZOOM) {
return;
}
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = '#333';
// Drawn from the two fields rather than splitting LABEL: a device name
// may contain spaces, and a two-word split drops the rest of it.
const gap = LABEL_SIZE * 0.5; // graph units, so it holds as the map zooms
ctx.font = 'bold ' + LABEL_SIZE + 'px sans-serif';
ctx.fillText(n.ORIG_LABEL, n.x, n.y + n.radius + gap);
if (showips instanceof HTMLInputElement && showips.checked && n.ORIG_LABEL !== n.ID) {
ctx.font = LABEL_SIZE + 'px sans-serif';
ctx.fillText(n.ID, n.x, n.y + n.radius + gap + LABEL_SIZE + 1);
}
});
// read once, not once per link per frame
const showspeed = document.getElementById('nd_showspeed');
fg.linkCanvasObjectMode(function () {
return 'after';
}).linkCanvasObject(function (l, ctx) {
( run in 1.040 second using v1.01-cache-2.11-cpan-85d3896f969 )