@cnvx/nodal
Version:
A Svelte 5 library for creating interactive node diagrams with customizable connections and layouts
122 lines (121 loc) • 4.5 kB
JavaScript
import { mount } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import { writable } from "svelte/store";
import Surface from "./Surface.svelte";
let elementIdCounter = 0;
function getElementId(el) {
if (el.id)
return `#${el.id}`;
if (!el.dataset.nodalUid) {
el.dataset.nodalUid = `nodal-${elementIdCounter++}`;
}
return `[data-nodal-uid="${el.dataset.nodalUid}"]`;
}
function getEdgeEndpointKey(endpoint) {
if (typeof endpoint === "string")
return endpoint;
if (endpoint instanceof HTMLElement)
return getElementId(endpoint);
return endpoint.map(e => typeof e === "string" ? e : getElementId(e)).join(",");
}
function drawCall(surface) {
// console.debug("Drawing nodal surface edges...");
const connections = surface.querySelectorAll("[data-nodal-connected='true']");
connections.forEach((conn) => {
// console.debug("Processing connection for:", conn);
const host = conn;
if (!conn._nodalEdgeConnections ||
conn._nodalEdgeConnections.length === 0) {
// console.debug("No edges found for host:", host);
return;
}
conn._nodalEdgeConnections.forEach((edge) => {
let edgeDef;
if (typeof edge == "string") {
edgeDef = {
source: host,
target: edge,
};
}
else {
edgeDef = {
source: "source" in edge ? edge.source : host,
target: "target" in edge ? edge.target : host,
...edge,
};
}
const sourceKey = getEdgeEndpointKey(edgeDef.source);
const targetKey = getEdgeEndpointKey(edgeDef.target);
surface._nodalSurface.internals.edges.set(`${sourceKey}->${targetKey}`, edgeDef);
});
});
}
export function createNodal({ svgAttributes = {}, getNodeAnchor, fadeInDuration, onMount, } = {}) {
return (element) => {
console.debug("Creating nodal sruface..");
if (!Object.hasOwn(element, "_nodalSurface")) {
const edges = new SvelteMap();
const exports = mount(Surface, {
target: element,
props: {
hostElement: element,
edges,
width: writable(element.clientWidth),
height: writable(element.clientHeight),
svgAttributes,
getNodeAnchor,
fadeInDuration,
},
});
const engine = {
_isScheduled: false,
draw: () => drawCall(element),
schedule() {
if (engine._isScheduled)
return;
engine._isScheduled = true;
requestAnimationFrame(() => {
this._isScheduled = false;
drawCall(element);
});
},
internals: exports
};
Object.defineProperty(element, "_nodalSurface", {
value: engine,
});
const resizeObserver = new ResizeObserver((entries) => {
requestAnimationFrame(() => {
for (let entry of entries) {
const { width, height } = entry.contentRect;
// console.debug(
// "Resizing nodal surface to:",
// width,
// height,
// );
exports.width?.set(width);
exports.height?.set(height);
// redraw edges
// drawCall(element as NodalSurfaceElement);
engine.schedule();
}
});
});
try {
onMount?.(engine);
}
catch (e) {
console.error("Error in onMount callback for nodal surface:", e);
}
resizeObserver.observe(element);
engine.schedule();
// drawCall(element as NodalSurfaceElement);
}
};
}
export function connectTo(...edges) {
return (host) => {
host.setAttribute("data-nodal-connected", "true");
host._nodalEdgeConnections = edges;
};
}