UNPKG

@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
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; }