feat(core,composables): add useHandleConnections composable

This commit is contained in:
braks
2024-02-01 19:45:47 +01:00
committed by Braks
parent a9bd82d28a
commit 064cfd84b3
6 changed files with 166 additions and 13 deletions

View 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() ?? []))
}

View File

@@ -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'

View File

@@ -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) {

View File

@@ -35,6 +35,7 @@ function defaultState(): State {
nodes: [],
// todo: change this to a Set
edges: [],
connectionLookup: new Map(),
nodeTypes: {},
edgeTypes: {},

View File

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

View File

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