feat(core,composables): add useHandleConnections composable
This commit is contained in:
@@ -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 { VueFlow as VueFlowInjection, NodeId as NodeIdInjection } from './context'
|
||||||
|
|
||||||
export {
|
export { useZoomPanHelper } from './composables/useZoomPanHelper'
|
||||||
useZoomPanHelper,
|
export { useVueFlow } from './composables/useVueFlow'
|
||||||
useVueFlow,
|
export { useHandle } from './composables/useHandle'
|
||||||
Storage as GlobalVueFlowStorage,
|
export { useNode } from './composables/useNode'
|
||||||
useHandle,
|
export { useEdge } from './composables/useEdge'
|
||||||
useNode,
|
export { useGetPointerPosition } from './composables/useGetPointerPosition'
|
||||||
useEdge,
|
export { useNodeId } from './composables/useNodeId'
|
||||||
useGetPointerPosition,
|
export { useConnection } from './composables/useConnection'
|
||||||
} from './composables'
|
export { useHandleConnections } from './composables/useHandleConnections'
|
||||||
|
|
||||||
export { VueFlowError, ErrorCode } from './utils/errors'
|
export { VueFlowError, ErrorCode } from './utils/errors'
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {
|
|||||||
isRect,
|
isRect,
|
||||||
nodeToRect,
|
nodeToRect,
|
||||||
parseEdge,
|
parseEdge,
|
||||||
|
updateConnectionLookup,
|
||||||
updateEdgeAction,
|
updateEdgeAction,
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { useState } from './state'
|
import { useState } from './state'
|
||||||
@@ -376,6 +377,8 @@ export function useActions(
|
|||||||
)
|
)
|
||||||
: nextEdges
|
: nextEdges
|
||||||
|
|
||||||
|
updateConnectionLookup(state.connectionLookup, validEdges)
|
||||||
|
|
||||||
state.edges = validEdges.reduce<GraphEdge[]>((res, edge) => {
|
state.edges = validEdges.reduce<GraphEdge[]>((res, edge) => {
|
||||||
const sourceNode = findNode(edge.source)
|
const sourceNode = findNode(edge.source)
|
||||||
const targetNode = findNode(edge.target)
|
const targetNode = findNode(edge.target)
|
||||||
@@ -587,9 +590,17 @@ export function useActions(
|
|||||||
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) =>
|
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection, shouldReplaceId = true) =>
|
||||||
updateEdgeAction(oldEdge, newConnection, state.edges, findEdge, shouldReplaceId, state.hooks.error.trigger)
|
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) => {
|
const startConnection: Actions['startConnection'] = (startHandle, position, event, isClick = false) => {
|
||||||
if (isClick) {
|
if (isClick) {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ function defaultState(): State {
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
// todo: change this to a Set
|
// todo: change this to a Set
|
||||||
edges: [],
|
edges: [],
|
||||||
|
connectionLookup: new Map(),
|
||||||
nodeTypes: {},
|
nodeTypes: {},
|
||||||
edgeTypes: {},
|
edgeTypes: {},
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks
|
|||||||
import type { EdgeChange, NodeChange, NodeDragItem } from './changes'
|
import type { EdgeChange, NodeChange, NodeDragItem } from './changes'
|
||||||
import type { ConnectingHandle, ValidConnectionFunc } from './handle'
|
import type { ConnectingHandle, ValidConnectionFunc } from './handle'
|
||||||
|
|
||||||
|
export type ConnectionLookup = Map<string, Map<string, Connection>>
|
||||||
|
|
||||||
export interface UpdateNodeDimensionsParams {
|
export interface UpdateNodeDimensionsParams {
|
||||||
id: string
|
id: string
|
||||||
nodeElement: HTMLDivElement
|
nodeElement: HTMLDivElement
|
||||||
@@ -49,6 +51,8 @@ export interface State extends Omit<FlowOptions, 'id' | 'modelValue'> {
|
|||||||
/** all stored edges */
|
/** all stored edges */
|
||||||
edges: GraphEdge[]
|
edges: GraphEdge[]
|
||||||
|
|
||||||
|
connectionLookup: ConnectionLookup
|
||||||
|
|
||||||
readonly d3Zoom: D3Zoom | null
|
readonly d3Zoom: D3Zoom | null
|
||||||
readonly d3Selection: D3Selection | null
|
readonly d3Selection: D3Selection | null
|
||||||
readonly d3ZoomHandler: D3ZoomHandler | null
|
readonly d3ZoomHandler: D3ZoomHandler | null
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { unref } from 'vue'
|
import { markRaw, unref } from 'vue'
|
||||||
import type { Actions, Connection, Edge, GraphEdge, GraphNode, Node, State } from '../types'
|
import type { Actions, Connection, ConnectionLookup, Edge, GraphEdge, GraphNode, Node, State } from '../types'
|
||||||
import { ErrorCode, VueFlowError, connectionExists, getEdgeId, isEdge, isNode, parseEdge, parseNode } from '.'
|
import { ErrorCode, VueFlowError, connectionExists, getEdgeId, isEdge, isNode, parseEdge, parseNode } from '.'
|
||||||
|
|
||||||
type NonUndefined<T> = T extends undefined ? never : T
|
type NonUndefined<T> = T extends undefined ? never : T
|
||||||
@@ -120,3 +120,73 @@ export function createGraphNodes(
|
|||||||
|
|
||||||
return graphNodes
|
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