diff --git a/.changeset/silly-pots-yell.md b/.changeset/silly-pots-yell.md new file mode 100644 index 00000000..71848488 --- /dev/null +++ b/.changeset/silly-pots-yell.md @@ -0,0 +1,7 @@ +--- +'@xyflow/react': minor +'@xyflow/svelte': patch +'@xyflow/system': patch +--- + +Add useNodeConnections hook to track all connections to a node. Can be filtered by handleType and handleId. diff --git a/examples/react/src/App/routes.ts b/examples/react/src/App/routes.ts index 28c9e8cc..cb184b4a 100644 --- a/examples/react/src/App/routes.ts +++ b/examples/react/src/App/routes.ts @@ -49,7 +49,7 @@ import NodeToolbar from '../examples/NodeToolbar'; import UseConnection from '../examples/UseConnection'; import UseNodesInitialized from '../examples/UseNodesInit'; import UseNodesData from '../examples/UseNodesData'; -import UseHandleConnections from '../examples/UseHandleConnections'; +import UseNodeConnections from '../examples/UseNodeConnections'; import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop'; import DevTools from '../examples/DevTools'; import Redux from '../examples/Redux'; @@ -313,9 +313,9 @@ const routes: IRoute[] = [ component: UseReactFlow, }, { - name: 'useHandleConnections', - path: 'usehandleconnections', - component: UseHandleConnections, + name: 'useNodeConnections', + path: 'usenodeconnections', + component: UseNodeConnections, }, { name: 'useNodesData', diff --git a/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx b/examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx similarity index 87% rename from examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx rename to examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx index 4b8fd2c1..3397242a 100644 --- a/examples/react/src/examples/UseHandleConnections/MultiHandleNode.tsx +++ b/examples/react/src/examples/UseNodeConnections/MultiHandleNode.tsx @@ -1,5 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react'; +import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) { const onConnect = useCallback( @@ -11,9 +11,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string (connections: Connection[]) => console.log('onDisconnect handler, node id:', nodeId, connections), [nodeId] ); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: handleProps.type, - id: handleProps.id, + handleId: handleProps.id, onConnect, onDisconnect, }); diff --git a/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx b/examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx similarity index 86% rename from examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx rename to examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx index 1c13dcb0..070bbf2a 100644 --- a/examples/react/src/examples/UseHandleConnections/SingleHandleNode.tsx +++ b/examples/react/src/examples/UseNodeConnections/SingleHandleNode.tsx @@ -1,5 +1,5 @@ import { memo, FC, useEffect, useCallback } from 'react'; -import { Handle, Position, NodeProps, useHandleConnections, Connection, HandleProps } from '@xyflow/react'; +import { Handle, Position, NodeProps, useNodeConnections, Connection, HandleProps } from '@xyflow/react'; function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }) { const onConnect = useCallback( @@ -14,9 +14,9 @@ function CustomHandle({ nodeId, ...handleProps }: HandleProps & { nodeId: string }, [nodeId] ); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: handleProps.type, - id: handleProps.id, + handleId: handleProps.id, onConnect, onDisconnect, }); diff --git a/examples/react/src/examples/UseHandleConnections/index.tsx b/examples/react/src/examples/UseNodeConnections/index.tsx similarity index 100% rename from examples/react/src/examples/UseHandleConnections/index.tsx rename to examples/react/src/examples/UseNodeConnections/index.tsx diff --git a/examples/react/src/examples/UseNodesData/ResultNode.tsx b/examples/react/src/examples/UseNodesData/ResultNode.tsx index bf8f7a35..45ec4087 100644 --- a/examples/react/src/examples/UseNodesData/ResultNode.tsx +++ b/examples/react/src/examples/UseNodesData/ResultNode.tsx @@ -1,9 +1,9 @@ import { memo } from 'react'; -import { Handle, Position, useHandleConnections, useNodesData } from '@xyflow/react'; +import { Handle, Position, useNodeConnections, useNodesData } from '@xyflow/react'; import { isTextNode, type MyNode } from '.'; function ResultNode() { - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: 'target', }); const nodesData = useNodesData(connections.map((connection) => connection.source)); diff --git a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx index 70c6079d..a6f0b67b 100644 --- a/examples/react/src/examples/UseNodesData/UppercaseNode.tsx +++ b/examples/react/src/examples/UseNodesData/UppercaseNode.tsx @@ -1,10 +1,10 @@ import { memo, useEffect } from 'react'; -import { Position, NodeProps, useReactFlow, Handle, useHandleConnections, useNodesData } from '@xyflow/react'; +import { Position, NodeProps, useReactFlow, Handle, useNodeConnections, useNodesData } from '@xyflow/react'; import { isTextNode, type TextNode, type MyNode } from '.'; function UppercaseNode({ id }: NodeProps) { const { updateNodeData } = useReactFlow(); - const connections = useHandleConnections({ + const connections = useNodeConnections({ type: 'target', }); const nodesData = useNodesData(connections[0]?.source); diff --git a/examples/svelte/src/routes/examples/overview/Flow.svelte b/examples/svelte/src/routes/examples/overview/Flow.svelte index eae13621..2e1138f9 100644 --- a/examples/svelte/src/routes/examples/overview/Flow.svelte +++ b/examples/svelte/src/routes/examples/overview/Flow.svelte @@ -147,6 +147,12 @@ function hideUnhide() { updateNode('hideunhide', (node) => ({ hidden: !node.hidden })); } + + // $inspect(edges); + $effect(() => { + // console.log(edges.map((edge) => ({ id: edge.id, selected: edge.selected }))); + edges.forEach((edge) => console.log({ id: edge.id, selected: edge.selected })); + }); ([ + let nodes = $state.raw([ { id: '1', type: 'input', @@ -95,7 +95,7 @@ } ]); - let edges = $state([ + let edges = $state.raw([ { id: 'e1-2', source: '1', diff --git a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte index 07d9a257..52706a83 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/ResultNode.svelte @@ -2,7 +2,7 @@ import { Handle, Position, - useHandleConnections, + useNodeConnections, useNodesData, type NodeProps } from '@xyflow/svelte'; @@ -10,7 +10,7 @@ let { id }: NodeProps = $props(); - const connections = useHandleConnections({ + const connections = useNodeConnections({ nodeId: id, type: 'target' }); diff --git a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte index 25e540f9..88e2dd02 100644 --- a/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte +++ b/examples/svelte/src/routes/examples/usenodesdata/UppercaseNode.svelte @@ -2,7 +2,7 @@ import { Handle, Position, - useHandleConnections, + useNodeConnections, useNodesData, useSvelteFlow, type Node, @@ -14,7 +14,7 @@ let { id, data }: NodeProps> = $props(); const { updateNodeData } = useSvelteFlow(); - const connections = useHandleConnections({ + const connections = useNodeConnections({ nodeId: id, type: 'target' }); diff --git a/packages/react/src/hooks/useHandleConnections.ts b/packages/react/src/hooks/useHandleConnections.ts index 20b65a2d..cad87715 100644 --- a/packages/react/src/hooks/useHandleConnections.ts +++ b/packages/react/src/hooks/useHandleConnections.ts @@ -22,6 +22,7 @@ type useHandleConnectionsParams = { * Hook to check if a is connected to another and get the connections. * * @public + * @deprecated Use `useNodeConnections` instead. * @param param.type - handle type 'source' or 'target' * @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) @@ -31,18 +32,22 @@ type useHandleConnectionsParams = { */ export function useHandleConnections({ type, - id = null, + id, nodeId, onConnect, onDisconnect, }: useHandleConnectionsParams): HandleConnection[] { + console.warn( + '[DEPRECATED] `useHandleConnections` is deprecated. Instead use `useNodeConnections` https://reactflow.dev/api-reference/hooks/useNodeConnections' + ); + const _nodeId = useNodeId(); const currentNodeId = nodeId ?? _nodeId; const prevConnections = useRef | null>(null); const connections = useStore( - (state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`), + (state) => state.connectionLookup.get(`${currentNodeId}-${type}${id ? `-${id}` : ''}`), areConnectionMapsEqual ); diff --git a/packages/react/src/hooks/useNodeConnections.ts b/packages/react/src/hooks/useNodeConnections.ts new file mode 100644 index 00000000..b0481851 --- /dev/null +++ b/packages/react/src/hooks/useNodeConnections.ts @@ -0,0 +1,69 @@ +import { useEffect, useMemo, useRef } from 'react'; +import { + Connection, + NodeConnection, + HandleType, + areConnectionMapsEqual, + handleConnectionChange, + errorMessages, +} from '@xyflow/system'; + +import { useStore } from './useStore'; +import { useNodeId } from '../contexts/NodeIdContext'; + +const error014 = errorMessages['error014'](); + +type UseNodeConnectionsParams = { + type?: HandleType; + handleId?: string; + nodeId?: string; + onConnect?: (connections: Connection[]) => void; + onDisconnect?: (connections: Connection[]) => void; +}; + +/** + * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * + * @public + * @param param.nodeId - node id - optional if called inside a custom node + * @param param.type - filter by handle type 'source' or 'target' + * @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type) + * @param param.onConnect - gets called when a connection is established + * @param param.onDisconnect - gets called when a connection is removed + * @returns an array with connections + */ +export function useNodeConnections({ + type, + handleId, + nodeId, + onConnect, + onDisconnect, +}: UseNodeConnectionsParams = {}): NodeConnection[] { + const _nodeId = useNodeId(); + const currentNodeId = nodeId ?? _nodeId; + + if (!currentNodeId) { + throw new Error(error014); + } + + const prevConnections = useRef | null>(null); + + const connections = useStore( + (state) => + state.connectionLookup.get(`${currentNodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`), + areConnectionMapsEqual + ); + + useEffect(() => { + // @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts + if (prevConnections.current && prevConnections.current !== connections) { + const _connections = connections ?? new Map(); + handleConnectionChange(prevConnections.current, _connections, onDisconnect); + handleConnectionChange(_connections, prevConnections.current, onConnect); + } + + prevConnections.current = connections ?? new Map(); + }, [connections, onConnect, onDisconnect]); + + return useMemo(() => Array.from(connections?.values() ?? []), [connections]); +} diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 2045761d..3d857cfa 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -238,7 +238,14 @@ export function useReactFlow + Array.from( + store + .getState() + .connectionLookup.get(`${nodeId}${type ? (handleId ? `-${type}-${handleId}` : `-${type}`) : ''}`) ?.values() ?? [] ), }; diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index cb36e305..4cbb393d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -24,6 +24,7 @@ export { useOnViewportChange, type UseOnViewportChangeOptions } from './hooks/us export { useOnSelectionChange, type UseOnSelectionChangeOptions } from './hooks/useOnSelectionChange'; export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/useNodesInitialized'; export { useHandleConnections } from './hooks/useHandleConnections'; +export { useNodeConnections } from './hooks/useNodeConnections'; export { useNodesData } from './hooks/useNodesData'; export { useConnection } from './hooks/useConnection'; export { useInternalNode } from './hooks/useInternalNode'; diff --git a/packages/react/src/types/instance.ts b/packages/react/src/types/instance.ts index fdece4dd..992655a4 100644 --- a/packages/react/src/types/instance.ts +++ b/packages/react/src/types/instance.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-namespace */ -import type { HandleConnection, HandleType, Rect, Viewport } from '@xyflow/system'; +import type { HandleConnection, HandleType, NodeConnection, Rect, Viewport } from '@xyflow/system'; import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.'; export type ReactFlowJsonObject = { @@ -183,7 +183,7 @@ export type GeneralHelpers Rect; /** * Gets all connections for a given handle belonging to a specific node. - * + * @deprecated * @param type - handle type 'source' or 'target' * @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) * @param nodeId - the node id the handle belongs to @@ -198,6 +198,23 @@ export type GeneralHelpers HandleConnection[]; + /** + * Gets all connections to a node. Can be filtered by handle type and id. + * @deprecated use `getNodeConnections` instead + * @param type - handle type 'source' or 'target' + * @param handleId - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle) + * @param nodeId - the node id the handle belongs to + * @returns an array with handle connections + */ + getNodeConnections: ({ + type, + handleId, + nodeId, + }: { + type?: HandleType; + nodeId: string; + handleId?: string | null; + }) => NodeConnection[]; }; export type ReactFlowInstance = GeneralHelpers< diff --git a/packages/svelte/CHANGELOG.md b/packages/svelte/CHANGELOG.md index f25bc805..7758855e 100644 --- a/packages/svelte/CHANGELOG.md +++ b/packages/svelte/CHANGELOG.md @@ -1,5 +1,11 @@ # @xyflow/svelte +## 0.1.26 + +### Patch Changes + +- [#4897](https://github.com/xyflow/xyflow/pull/4897) [`c6890512`](https://github.com/xyflow/xyflow/commit/c6890512a685c8ec87310b3b003e62b0b976eca4) Thanks [@peterkogo](https://github.com/peterkogo)! - Freeze required @svelte-put/shortcut version to 3.1.1 to prevent breaking of key inputs. + ## 0.1.25 ### Patch Changes diff --git a/packages/svelte/package.json b/packages/svelte/package.json index 52dc8996..1e661514 100644 --- a/packages/svelte/package.json +++ b/packages/svelte/package.json @@ -1,6 +1,6 @@ { "name": "@xyflow/svelte", - "version": "0.1.25", + "version": "0.1.26", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "keywords": [ "svelte", diff --git a/packages/svelte/src/lib/components/Handle/Handle.svelte b/packages/svelte/src/lib/components/Handle/Handle.svelte index 5b2954ea..5d9131b9 100644 --- a/packages/svelte/src/lib/components/Handle/Handle.svelte +++ b/packages/svelte/src/lib/components/Handle/Handle.svelte @@ -92,7 +92,7 @@ // eslint-disable-next-line @typescript-eslint/no-unused-expressions store.edges; if (onconnect || ondisconnect) { - let connections = store.connectionLookup.get(`${nodeId}-${type}-${handleId}`); + let connections = store.connectionLookup.get(`${nodeId}-${type}${id ? `-${id}` : ''}`); if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) { const _connections = connections ?? new Map(); diff --git a/packages/svelte/src/lib/container/Pane/Pane.svelte b/packages/svelte/src/lib/container/Pane/Pane.svelte index b7e7491f..3290c631 100644 --- a/packages/svelte/src/lib/container/Pane/Pane.svelte +++ b/packages/svelte/src/lib/container/Pane/Pane.svelte @@ -15,7 +15,7 @@ return (item: Item) => { const isSelected = ids.has(item.id); - if (item.selected !== isSelected) { + if (!!item.selected !== isSelected) { return { ...item, selected: isSelected }; } @@ -23,8 +23,7 @@ }; } - // TODO: maybe replace with set.intersection? - function setEq(a: Set, b: Set) { + function isSetEqual(a: Set, b: Set) { if (a.size !== b.size) { return false; } @@ -57,7 +56,6 @@ // svelte-ignore non_reactive_update let container: HTMLDivElement; let containerBounds: DOMRect | null = null; - // let selectedNodes: InternalNode[] = []; let selectedNodeIds: Set = new Set(); let selectedEdgeIds: Set = new Set(); @@ -149,25 +147,27 @@ ).map((n) => n.id) ); - // TODO: replace with extended connectionLookup let edgesSelectable = store.defaultEdgeOptions.selectable ?? true; selectedEdgeIds = new Set(); - store.edges.forEach((edge) => { - if ( - selectedNodeIds.has(edge.source) && - selectedNodeIds.has(edge.target) && - (edge.selectable ?? edgesSelectable) - ) { - selectedEdgeIds.add(edge.id); + + // We look for all edges connected to the selected nodes + for (let nodeId of selectedNodeIds) { + let connections = store.connectionLookup.get(nodeId); + if (!connections) continue; + for (let { edgeId } of connections.values()) { + let edge = store.edgeLookup.get(edgeId); + if (edge && (edge.selectable ?? edgesSelectable)) { + selectedEdgeIds.add(edgeId); + } } - }); + } // this prevents unnecessary updates while updating the selection rectangle - if (setEq(prevSelectedNodeIds, selectedNodeIds)) { + if (isSetEqual(prevSelectedNodeIds, selectedNodeIds)) { store.nodes = store.nodes.map(toggleSelected(selectedNodeIds)); } - if (setEq(prevSelectedEdgeIds, selectedEdgeIds)) { + if (isSetEqual(prevSelectedEdgeIds, selectedEdgeIds)) { store.edges = store.edges.map(toggleSelected(selectedEdgeIds)); } diff --git a/packages/svelte/src/lib/hooks/derivedWarning.svelte.ts b/packages/svelte/src/lib/hooks/derivedWarning.svelte.ts index ce7226c9..a0ab11fe 100644 --- a/packages/svelte/src/lib/hooks/derivedWarning.svelte.ts +++ b/packages/svelte/src/lib/hooks/derivedWarning.svelte.ts @@ -9,7 +9,7 @@ import { getContext } from 'svelte'; * @param functionName - The name of the function that is being called * @param force - If true, the warning will be shown regardless if child of */ -export function derivedWarning(functionName: string, force?: boolean) { +export function derivedWarning(functionName: string) { const storeContext = getContext(key); if (!storeContext) { @@ -18,7 +18,7 @@ export function derivedWarning(functionName: string, force?: boolean) { ); } - if ((force || storeContext.provider) && !$effect.tracking()) { + if (storeContext.provider && typeof window === 'object' && !$effect.tracking()) { console.warn(`Use $derived(${functionName}()) to receive updates when values change.`); console.trace(functionName); } diff --git a/packages/svelte/src/lib/hooks/useHandleConnections.svelte.ts b/packages/svelte/src/lib/hooks/useHandleConnections.svelte.ts deleted file mode 100644 index 440ed0d8..00000000 --- a/packages/svelte/src/lib/hooks/useHandleConnections.svelte.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { areConnectionMapsEqual, type HandleConnection, type HandleType } from '@xyflow/system'; - -import { useStore } from '$lib/store'; -import { getContext } from 'svelte'; - -export type useHandleConnectionsParams = { - type: HandleType; - nodeId?: string; - id?: string | null; -}; - -const initialConnections: HandleConnection[] = []; - -/** - * Hook to check if a is connected to another and get the connections. - * - * @public - * @param param.nodeId - * @param param.type - handle type 'source' or 'target' - * @param param.id - the handle id (this is only needed if the node has multiple handles of the same type) - * @returns an array with connections - */ -export function useHandleConnections({ - type, - nodeId: _nodeId, - id = null -}: useHandleConnectionsParams) { - const { edges, connectionLookup } = $derived(useStore()); - - const contextNodeId = getContext('svelteflow__node_id'); - const nodeId = _nodeId ?? contextNodeId; - - let prevConnections: Map | undefined = new Map(); - let connectionsArray: HandleConnection[] = initialConnections; - - const connections = $derived.by(() => { - // eslint-disable-next-line @typescript-eslint/no-unused-expressions - edges; - const nextConnections = connectionLookup.get(`${nodeId}-${type}-${id || null}`); - if (!areConnectionMapsEqual(nextConnections, prevConnections)) { - prevConnections = nextConnections; - connectionsArray = Array.from(nextConnections?.values() || initialConnections); - } - return connectionsArray; - }); - - return { - get current() { - return connections; - } - }; -} diff --git a/packages/svelte/src/lib/hooks/useNodeConnections.svelte.ts b/packages/svelte/src/lib/hooks/useNodeConnections.svelte.ts new file mode 100644 index 00000000..35d8019d --- /dev/null +++ b/packages/svelte/src/lib/hooks/useNodeConnections.svelte.ts @@ -0,0 +1,59 @@ +import { areConnectionMapsEqual, type NodeConnection, type HandleType } from '@xyflow/system'; + +import { useStore } from '$lib/store'; +import { getContext } from 'svelte'; + +type UseNodeConnectionsParams = { + type?: HandleType; + handleId?: string; + nodeId?: string; + // TODO: Svelte 5 + // onConnect?: (connections: Connection[]) => void; + // onDisconnect?: (connections: Connection[]) => void; +}; + +const initialConnections: NodeConnection[] = []; + +/** + * Hook to retrieve all edges connected to a node. Can be filtered by handle type and id. + * + * @public + * @param param.nodeId - node id - optional if called inside a custom node + * @param param.type - filter by handle type 'source' or 'target' + * @param param.handleId - filter by handle id (this is only needed if the node has multiple handles of the same type) + * @todo @param param.onConnect - gets called when a connection is established + * @todo @param param.onDisconnect - gets called when a connection is removed + * @returns an array with connections + */ +export function useNodeConnections({ + type, + nodeId: _nodeId, + handleId +}: UseNodeConnectionsParams = {}) { + const { edges, connectionLookup } = $derived(useStore()); + + const contextNodeId = getContext('svelteflow__node_id'); + const nodeId = _nodeId ?? contextNodeId; + + let prevConnections: Map | undefined = new Map(); + let connectionsArray: NodeConnection[] = initialConnections; + + const connections = $derived.by(() => { + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + edges; + const nextConnections = connectionLookup.get( + `${nodeId}-${type}${handleId ? `-${handleId}` : ''}` + ); + if (!areConnectionMapsEqual(nextConnections, prevConnections)) { + prevConnections = nextConnections; + connectionsArray = Array.from(nextConnections?.values() || initialConnections); + } + return connectionsArray; + }); + + return { + get current() { + return connections; + } + }; +} diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index e5ec2743..1e4f3202 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -35,7 +35,7 @@ export * from '$lib/hooks/useSvelteFlow.svelte'; export * from '$lib/hooks/useUpdateNodeInternals.svelte'; export * from '$lib/hooks/useConnection.svelte'; export * from '$lib/hooks/useNodesEdgesViewport.svelte'; -export * from '$lib/hooks/useHandleConnections.svelte'; +export * from '$lib/hooks/useNodeConnections.svelte'; export * from '$lib/hooks/useNodesData.svelte'; export * from '$lib/hooks/useInternalNode.svelte'; export { useInitialized, useNodesInitialized } from '$lib/hooks/useInitialized.svelte'; diff --git a/packages/system/src/constants.ts b/packages/system/src/constants.ts index e108b761..921189fa 100644 --- a/packages/system/src/constants.ts +++ b/packages/system/src/constants.ts @@ -24,6 +24,8 @@ export const errorMessages = { `Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`, error013: (lib: string = 'react') => `It seems that you haven't loaded the styles. Please import '@xyflow/${lib}/dist/style.css' or base.css to make sure everything is working properly.`, + error014: () => + 'useNodeConnections: No node ID found. Call useNodeConnections inside a custom Node or provide a node ID.', }; export const infiniteExtent: CoordinateExtent = [ diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index 98d8afb6..3c6eed52 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -33,10 +33,15 @@ export type Connection = { targetHandle: string | null; }; +// TODO: remove in next version export type HandleConnection = Connection & { edgeId: string; }; +export type NodeConnection = Connection & { + edgeId: string; +}; + export enum ConnectionMode { Strict = 'strict', Loose = 'loose', diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index d62fb534..3076c637 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,4 +1,4 @@ -import { infiniteExtent } from '..'; +import { HandleConnection, infiniteExtent } from '..'; import { NodeBase, CoordinateExtent, @@ -42,7 +42,7 @@ const adoptUserNodesDefaultOptions = { checkEquality: true, }; -function mergeObjects>(base: T, incoming?: Partial): T { +function mergeObjects>(base: T, incoming?: Partial): T { const result = { ...base }; for (const key in incoming) { if (incoming[key] !== undefined) { @@ -450,22 +450,56 @@ export async function panBy({ return Promise.resolve(transformChanged); } +/** + * this function adds the connection to the connectionLookup + * at the following keys: nodeId-type-handleId, nodeId-type and nodeId + * @param type type of the connection + * @param connection connection that should be added to the lookup + * @param connectionKey at which key the connection should be added + * @param connectionLookup reference to the connection lookup + * @param nodeId nodeId of the connection + * @param handleId handleId of the conneciton + */ +function addConnectionToLookup( + type: 'source' | 'target', + connection: HandleConnection, + connectionKey: string, + connectionLookup: ConnectionLookup, + nodeId: string, + handleId: string | null +) { + // We add the connection to the connectionLookup at the following keys + // 1. nodeId, 2. nodeId-type, 3. nodeId-type-handleId + // If the key already exists, we add the connection to the existing map + let key = nodeId; + const nodeMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, nodeMap.set(connectionKey, connection)); + + key = `${nodeId}-${type}`; + const typeMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, typeMap.set(connectionKey, connection)); + + if (handleId) { + key = `${nodeId}-${type}-${handleId}`; + const handleMap = connectionLookup.get(key) || new Map(); + connectionLookup.set(key, handleMap.set(connectionKey, connection)); + } +} + export function updateConnectionLookup(connectionLookup: ConnectionLookup, edgeLookup: EdgeLookup, edges: EdgeBase[]) { connectionLookup.clear(); edgeLookup.clear(); for (const edge of edges) { - const { source, target, sourceHandle = null, targetHandle = null } = edge; + const { source: sourceNode, target: targetNode, sourceHandle = null, targetHandle = null } = edge; - const sourceKey = `${source}-source-${sourceHandle}`; - const targetKey = `${target}-target-${targetHandle}`; + const connection = { edgeId: edge.id, source: sourceNode, target: targetNode, sourceHandle, targetHandle }; + const sourceKey = `${sourceNode}-${sourceHandle}`; + const targetKey = `${targetNode}-${targetHandle}`; - const prevSource = connectionLookup.get(sourceKey) || new Map(); - const prevTarget = connectionLookup.get(targetKey) || new Map(); - const connection = { edgeId: edge.id, source, target, sourceHandle, targetHandle }; + addConnectionToLookup('source', connection, targetKey, connectionLookup, sourceNode, sourceHandle); + addConnectionToLookup('target', connection, sourceKey, connectionLookup, targetNode, targetHandle); edgeLookup.set(edge.id, edge); - connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)); - connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)); } }