@xyflow/svelte
Version:
Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.
214 lines (213 loc) • 9.3 kB
JavaScript
import { getOverlappingArea, isRectObject, nodeToRect, pointToRendererPoint, getViewportForBounds, getElementsToRemove, rendererPointToPoint, evaluateAbsolutePosition, getNodesBounds } from '@xyflow/system';
import { useStore } from '../store';
import { isEdge, isNode } from '../utils';
import { untrack } from 'svelte';
/**
* Hook for accessing the SvelteFlow instance.
*
* @public
* @returns A set of helper functions
*/
export function useSvelteFlow() {
const store = $derived(useStore());
const getNodeRect = (node) => {
const nodeToUse = isNode(node) ? node : store.nodeLookup.get(node.id);
const position = nodeToUse.parentId
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.measured, nodeToUse.parentId, store.nodeLookup, store.nodeOrigin)
: nodeToUse.position;
const nodeWithPosition = {
...nodeToUse,
position,
width: nodeToUse.measured?.width ?? nodeToUse.width,
height: nodeToUse.measured?.height ?? nodeToUse.height
};
return nodeToRect(nodeWithPosition);
};
function updateNode(id, nodeUpdate, options = { replace: false }) {
store.nodes = untrack(() => store.nodes).map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node) : nodeUpdate;
return options?.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
}
return node;
});
}
function updateEdge(id, edgeUpdate, options = { replace: false }) {
store.edges = untrack(() => store.edges).map((edge) => {
if (edge.id === id) {
const nextEdge = typeof edgeUpdate === 'function' ? edgeUpdate(edge) : edgeUpdate;
return options.replace && isEdge(nextEdge) ? nextEdge : { ...edge, ...nextEdge };
}
return edge;
});
}
const getInternalNode = (id) => store.nodeLookup.get(id);
return {
zoomIn: store.zoomIn,
zoomOut: store.zoomOut,
getInternalNode,
getNode: (id) => getInternalNode(id)?.internals.userNode,
getNodes: (ids) => (ids === undefined ? store.nodes : getElements(store.nodeLookup, ids)),
getEdge: (id) => store.edgeLookup.get(id),
getEdges: (ids) => (ids === undefined ? store.edges : getElements(store.edgeLookup, ids)),
setZoom: async (zoomLevel, options) => {
const panZoom = store.panZoom;
return panZoom ? panZoom.scaleTo(zoomLevel, options) : false;
},
getZoom: () => store.viewport.zoom,
setViewport: async (nextViewport, options) => {
const currentViewport = store.viewport;
if (!store.panZoom) {
return false;
}
await store.panZoom.setViewport({
x: nextViewport.x ?? currentViewport.x,
y: nextViewport.y ?? currentViewport.y,
zoom: nextViewport.zoom ?? currentViewport.zoom
}, options);
return true;
},
getViewport: () => $state.snapshot(store.viewport),
setCenter: async (x, y, options) => store.setCenter(x, y, options),
fitView: (options) => store.fitView(options),
fitBounds: async (bounds, options) => {
if (!store.panZoom) {
return false;
}
const viewport = getViewportForBounds(bounds, store.width, store.height, store.minZoom, store.maxZoom, options?.padding ?? 0.1);
await store.panZoom.setViewport(viewport, {
duration: options?.duration,
ease: options?.ease,
interpolate: options?.interpolate
});
return true;
},
/**
* Partial is defined as "the 2 nodes/areas are intersecting partially".
* If a is contained in b or b is contained in a, they are both
* considered fully intersecting.
*/
getIntersectingNodes: (nodeOrRect, partially = true, nodesToIntersect) => {
const isRect = isRectObject(nodeOrRect);
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
if (!nodeRect) {
return [];
}
return (nodesToIntersect || store.nodes).filter((n) => {
const internalNode = store.nodeLookup.get(n.id);
if (!internalNode || (!isRect && n.id === nodeOrRect.id)) {
return false;
}
const currNodeRect = nodeToRect(internalNode);
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
const partiallyVisible = partially && overlappingArea > 0;
return (partiallyVisible ||
overlappingArea >= currNodeRect.width * currNodeRect.height ||
overlappingArea >= nodeRect.width * nodeRect.height);
});
},
isNodeIntersecting: (nodeOrRect, area, partially = true) => {
const isRect = isRectObject(nodeOrRect);
const nodeRect = isRect ? nodeOrRect : getNodeRect(nodeOrRect);
if (!nodeRect) {
return false;
}
const overlappingArea = getOverlappingArea(nodeRect, area);
const partiallyVisible = partially && overlappingArea > 0;
return (partiallyVisible ||
overlappingArea >= area.width * area.height ||
overlappingArea >= nodeRect.width * nodeRect.height);
},
deleteElements: async ({ nodes: nodesToRemove = [], edges: edgesToRemove = [] }) => {
const { nodes: matchingNodes, edges: matchingEdges } = await getElementsToRemove({
nodesToRemove,
edgesToRemove,
nodes: store.nodes,
edges: store.edges,
onBeforeDelete: store.onbeforedelete
});
if (matchingNodes) {
store.nodes = untrack(() => store.nodes).filter((node) => !matchingNodes.some(({ id }) => id === node.id));
}
if (matchingEdges) {
store.edges = untrack(() => store.edges).filter((edge) => !matchingEdges.some(({ id }) => id === edge.id));
}
if (matchingNodes.length > 0 || matchingEdges.length > 0) {
store.ondelete?.({
nodes: matchingNodes,
edges: matchingEdges
});
}
return {
deletedNodes: matchingNodes,
deletedEdges: matchingEdges
};
},
screenToFlowPosition: (position, options = { snapToGrid: true }) => {
if (!store.domNode) {
return position;
}
const _snapGrid = options.snapToGrid ? store.snapGrid : false;
const { x, y, zoom } = store.viewport;
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const correctedPosition = {
x: position.x - domX,
y: position.y - domY
};
return pointToRendererPoint(correctedPosition, [x, y, zoom], _snapGrid !== null, _snapGrid || [1, 1]);
},
/**
*
* @param position
* @returns
*/
flowToScreenPosition: (position) => {
if (!store.domNode) {
return position;
}
const { x, y, zoom } = store.viewport;
const { x: domX, y: domY } = store.domNode.getBoundingClientRect();
const rendererPosition = rendererPointToPoint(position, [x, y, zoom]);
return {
x: rendererPosition.x + domX,
y: rendererPosition.y + domY
};
},
toObject: () => {
return structuredClone({
nodes: [...store.nodes],
edges: [...store.edges],
viewport: { ...store.viewport }
});
},
updateNode,
updateNodeData: (id, dataUpdate, options) => {
const node = store.nodeLookup.get(id)?.internals.userNode;
if (!node) {
return;
}
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
updateNode(id, (node) => ({
...node,
data: options?.replace ? nextData : { ...node.data, ...nextData }
}));
},
updateEdge,
getNodesBounds: (nodes) => {
return getNodesBounds(nodes, { nodeLookup: store.nodeLookup, nodeOrigin: store.nodeOrigin });
},
getHandleConnections: ({ type, id, nodeId }) => Array.from(store.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)?.values() ?? [])
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function getElements(lookup, ids) {
const result = [];
for (const id of ids) {
const item = lookup.get(id);
if (item) {
const element = 'internals' in item ? item.internals?.userNode : item;
result.push(element);
}
}
return result;
}