diff --git a/packages/core/src/composables/useHandleConnections.ts b/packages/core/src/composables/useHandleConnections.ts new file mode 100644 index 00000000..704e3e32 --- /dev/null +++ b/packages/core/src/composables/useHandleConnections.ts @@ -0,0 +1,67 @@ +import type { ComputedRef } from 'vue' +import { computed, ref, watch } from 'vue' +import type { MaybeRefOrGetter } from '@vueuse/core' +import { toRef, toValue } from '@vueuse/core' +import type { Connection, HandleType } from '../types' +import { areConnectionMapsEqual, handleConnectionChange } from '../utils' +import { useNodeId } from './useNodeId' +import { useVueFlow } from './useVueFlow' + +interface UseHandleConnectionsParams { + type: MaybeRefOrGetter + id?: MaybeRefOrGetter + nodeId?: MaybeRefOrGetter + onConnect?: (connections: Connection[]) => void + onDisconnect?: (connections: Connection[]) => void +} + +/** + * Hook to check if a is connected to another and get the connections. + * + * @public + * @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) + * @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 useHandleConnections({ + type, + id = null, + nodeId, + onConnect, + onDisconnect, +}: UseHandleConnectionsParams): ComputedRef { + const { connectionLookup } = useVueFlow() + const _nodeId = useNodeId() + const currentNodeId = toRef(() => toValue(nodeId) || _nodeId) + + const connections = ref>() + + watch( + () => connectionLookup.value.get(`${currentNodeId.value}-${toValue(type)}-${toValue(id)}`), + (nextConnections) => { + if (areConnectionMapsEqual(connections.value, nextConnections)) { + return + } + + connections.value = nextConnections + }, + { immediate: true }, + ) + + watch( + [connections, () => typeof onConnect !== 'undefined', () => typeof onDisconnect !== 'undefined'], + ([currentConnections], [prevConnections]) => { + if (prevConnections && prevConnections !== currentConnections) { + const _connections = currentConnections ?? new Map() + handleConnectionChange(prevConnections, _connections, onDisconnect) + handleConnectionChange(_connections, prevConnections, onConnect) + } + }, + { immediate: true }, + ) + + return computed(() => Array.from(connections.value?.values() ?? [])) +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d05a199e..bafb658a 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -62,15 +62,15 @@ export { defaultEdgeTypes, defaultNodeTypes } from './store/state' export { VueFlow as VueFlowInjection, NodeId as NodeIdInjection } from './context' -export { - useZoomPanHelper, - useVueFlow, - Storage as GlobalVueFlowStorage, - useHandle, - useNode, - useEdge, - useGetPointerPosition, -} from './composables' +export { useZoomPanHelper } from './composables/useZoomPanHelper' +export { useVueFlow } from './composables/useVueFlow' +export { useHandle } from './composables/useHandle' +export { useNode } from './composables/useNode' +export { useEdge } from './composables/useEdge' +export { useGetPointerPosition } from './composables/useGetPointerPosition' +export { useNodeId } from './composables/useNodeId' +export { useConnection } from './composables/useConnection' +export { useHandleConnections } from './composables/useHandleConnections' export { VueFlowError, ErrorCode } from './utils/errors' diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index b0b4beda..cc2986a1 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -47,6 +47,7 @@ import { isRect, nodeToRect, parseEdge, + updateConnectionLookup, updateEdgeAction, } from '../utils' import { useState } from './state' @@ -376,6 +377,8 @@ export function useActions( ) : nextEdges + updateConnectionLookup(state.connectionLookup, validEdges) + state.edges = validEdges.reduce((res, edge) => { const sourceNode = findNode(edge.source) const targetNode = findNode(edge.target) @@ -587,9 +590,17 @@ export function useActions( const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) => updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId, state.hooks.error.trigger) - const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyChanges(changes, state.nodes) + const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => { + return applyChanges(changes, state.nodes) + } - const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) => applyChanges(changes, state.edges) + const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) => { + const changedEdges = applyChanges(changes, state.edges) + + updateConnectionLookup(state.connectionLookup, changedEdges) + + return changedEdges + } const startConnection: Actions['startConnection'] = (startHandle, position, event, isClick = false) => { if (isClick) { diff --git a/packages/core/src/store/state.ts b/packages/core/src/store/state.ts index 39e20bd5..d0a40e74 100644 --- a/packages/core/src/store/state.ts +++ b/packages/core/src/store/state.ts @@ -35,6 +35,7 @@ function defaultState(): State { nodes: [], // todo: change this to a Set edges: [], + connectionLookup: new Map(), nodeTypes: {}, edgeTypes: {}, diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts index c703299c..2646171a 100644 --- a/packages/core/src/types/store.ts +++ b/packages/core/src/types/store.ts @@ -29,6 +29,8 @@ import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks import type { EdgeChange, NodeChange, NodeDragItem } from './changes' import type { ConnectingHandle, ValidConnectionFunc } from './handle' +export type ConnectionLookup = Map> + export interface UpdateNodeDimensionsParams { id: string nodeElement: HTMLDivElement @@ -49,6 +51,8 @@ export interface State extends Omit { /** all stored edges */ edges: GraphEdge[] + connectionLookup: ConnectionLookup + readonly d3Zoom: D3Zoom | null readonly d3Selection: D3Selection | null readonly d3ZoomHandler: D3ZoomHandler | null diff --git a/packages/core/src/utils/store.ts b/packages/core/src/utils/store.ts index 24c84f75..4fb0c09a 100644 --- a/packages/core/src/utils/store.ts +++ b/packages/core/src/utils/store.ts @@ -1,5 +1,5 @@ -import { unref } from 'vue' -import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node, State } from '../types' +import { markRaw, unref } from 'vue' +import type { Actions, Connection, ConnectionLookup, Edge, GraphEdge, GraphNode, Node, State } from '../types' import { ErrorCode, VueFlowError, connectionExists, getEdgeId, isEdge, isNode, parseEdge, parseNode } from '.' type NonUndefined = T extends undefined ? never : T @@ -120,3 +120,73 @@ export function createGraphNodes( return graphNodes } + +export function updateConnectionLookup(connectionLookup: ConnectionLookup, edges: Edge[]) { + connectionLookup.clear() + + for (const edge of edges) { + const { source, target, sourceHandle = null, targetHandle = null } = edge + + const sourceKey = `${source}-source-${sourceHandle}` + const targetKey = `${target}-target-${targetHandle}` + + const prevSource = connectionLookup.get(sourceKey) || new Map() + const prevTarget = connectionLookup.get(targetKey) || new Map() + const connection = markRaw({ source, target, sourceHandle, targetHandle }) + + connectionLookup.set(sourceKey, prevSource.set(`${target}-${targetHandle}`, connection)) + connectionLookup.set(targetKey, prevTarget.set(`${source}-${sourceHandle}`, connection)) + } +} + +/** + * We call the callback for all connections in a that are not in b + * + * @internal + */ +export function handleConnectionChange( + a: Map, + b: Map, + cb?: (diff: Connection[]) => void, +) { + if (!cb) { + return + } + + const diff: Connection[] = [] + + for (const key of a.keys()) { + if (!b.has(key)) { + diff.push(a.get(key)!) + } + } + + if (diff.length) { + cb(diff) + } +} + +/** + * @internal + */ +export function areConnectionMapsEqual(a?: Map, b?: Map) { + if (!a && !b) { + return true + } + + if (!a || !b || a.size !== b.size) { + return false + } + + if (!a.size && !b.size) { + return true + } + + for (const key of a.keys()) { + if (!b.has(key)) { + return false + } + } + + return true +}