feat(core,composables): add useHandleConnections composable
This commit is contained in:
67
packages/core/src/composables/useHandleConnections.ts
Normal file
67
packages/core/src/composables/useHandleConnections.ts
Normal file
@@ -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<HandleType>
|
||||
id?: MaybeRefOrGetter<string | null>
|
||||
nodeId?: MaybeRefOrGetter<string>
|
||||
onConnect?: (connections: Connection[]) => void
|
||||
onDisconnect?: (connections: Connection[]) => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to check if a <Handle /> is connected to another <Handle /> 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<Connection[]> {
|
||||
const { connectionLookup } = useVueFlow()
|
||||
const _nodeId = useNodeId()
|
||||
const currentNodeId = toRef(() => toValue(nodeId) || _nodeId)
|
||||
|
||||
const connections = ref<Map<string, Connection>>()
|
||||
|
||||
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() ?? []))
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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<GraphEdge[]>((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) {
|
||||
|
||||
@@ -35,6 +35,7 @@ function defaultState(): State {
|
||||
nodes: [],
|
||||
// todo: change this to a Set
|
||||
edges: [],
|
||||
connectionLookup: new Map(),
|
||||
nodeTypes: {},
|
||||
edgeTypes: {},
|
||||
|
||||
|
||||
@@ -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<string, Map<string, Connection>>
|
||||
|
||||
export interface UpdateNodeDimensionsParams {
|
||||
id: string
|
||||
nodeElement: HTMLDivElement
|
||||
@@ -49,6 +51,8 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
||||
/** all stored edges */
|
||||
edges: GraphEdge[]
|
||||
|
||||
connectionLookup: ConnectionLookup
|
||||
|
||||
readonly d3Zoom: D3Zoom | null
|
||||
readonly d3Selection: D3Selection | null
|
||||
readonly d3ZoomHandler: D3ZoomHandler | null
|
||||
|
||||
@@ -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> = 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<string, Connection>,
|
||||
b: Map<string, Connection>,
|
||||
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<string, Connection>, b?: Map<string, Connection>) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user