Files
vue-flow/packages/core/src/utils/store.ts
T
Braks 6f3ed67463 refactor(core): use connection lookup to check for selected elements (#1737)
* refactor(core): use connection lookup to check for selected elements

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>

* chore(changeset): add

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>

---------

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
2025-01-12 10:05:01 +01:00

320 lines
7.8 KiB
TypeScript

import { unref } from 'vue'
import type {
Actions,
Connection,
ConnectionLookup,
DefaultEdgeOptions,
Edge,
EdgeLookup,
GraphEdge,
GraphNode,
Node,
NodeConnection,
State,
ValidConnectionFunc,
VueFlowStore,
} from '../types'
import { ErrorCode, VueFlowError, connectionExists, getEdgeId, isEdge, isNode, parseEdge, parseNode } from '.'
type NonUndefined<T> = T extends undefined ? never : T
export function isDef<T>(val: T): val is NonUndefined<T> {
const unrefVal = unref(val)
return typeof unrefVal !== 'undefined'
}
export function addEdgeToStore(
edgeParams: Edge | Connection,
edges: Edge[],
triggerError: State['hooks']['error']['trigger'],
defaultEdgeOptions?: DefaultEdgeOptions,
): GraphEdge | false {
if (!edgeParams || !edgeParams.source || !edgeParams.target) {
triggerError(new VueFlowError(ErrorCode.EDGE_INVALID, (edgeParams as undefined | Edge)?.id ?? `[ID UNKNOWN]`))
return false
}
let edge
if (isEdge(edgeParams)) {
edge = edgeParams
} else {
edge = {
...edgeParams,
id: getEdgeId(edgeParams),
} as Edge
}
edge = parseEdge(edge, undefined, defaultEdgeOptions)
if (connectionExists(edge, edges)) {
return false
}
return edge
}
export function updateEdgeAction(
edge: GraphEdge,
newConnection: Connection,
prevEdge: GraphEdge | undefined,
shouldReplaceId: boolean,
triggerError: State['hooks']['error']['trigger'],
) {
if (!newConnection.source || !newConnection.target) {
triggerError(new VueFlowError(ErrorCode.EDGE_INVALID, edge.id))
return false
}
if (!prevEdge) {
triggerError(new VueFlowError(ErrorCode.EDGE_NOT_FOUND, edge.id))
return false
}
const { id, ...rest } = edge
return {
...rest,
id: shouldReplaceId ? getEdgeId(newConnection) : id,
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle,
}
}
export function createGraphNodes(nodes: Node[], findNode: Actions['findNode'], triggerError: State['hooks']['error']['trigger']) {
const parentNodes: Record<string, true> = {}
const nextNodes: GraphNode[] = []
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i]
if (!isNode(node)) {
triggerError(
new VueFlowError(ErrorCode.NODE_INVALID, (node as undefined | Record<any, any>)?.id) || `[ID UNKNOWN|INDEX ${i}]`,
)
continue
}
const parsed = parseNode(node, findNode(node.id), node.parentNode)
if (node.parentNode) {
parentNodes[node.parentNode] = true
}
nextNodes[i] = parsed
}
for (const node of nextNodes) {
const parentNode = findNode(node.parentNode) || nextNodes.find((n) => n.id === node.parentNode)
if (node.parentNode && !parentNode) {
triggerError(new VueFlowError(ErrorCode.NODE_MISSING_PARENT, node.id, node.parentNode))
}
if (node.parentNode || parentNodes[node.id]) {
if (parentNodes[node.id]) {
node.isParent = true
}
if (parentNode) {
parentNode.isParent = true
}
}
}
return nextNodes
}
/**
* 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: NodeConnection,
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: GraphEdge[]) {
connectionLookup.clear()
edgeLookup.clear()
for (const edge of edges) {
const { source: sourceNode, target: targetNode, sourceHandle = null, targetHandle = null } = edge
const connection = { edgeId: edge.id, source: sourceNode, target: targetNode, sourceHandle, targetHandle }
const sourceKey = `${sourceNode}-${sourceHandle}`
const targetKey = `${targetNode}-${targetHandle}`
addConnectionToLookup('source', connection, targetKey, connectionLookup, sourceNode, sourceHandle)
addConnectionToLookup('target', connection, sourceKey, connectionLookup, targetNode, targetHandle)
edgeLookup.set(edge.id, edge)
}
}
/**
* We call the callback for all connections in a that are not in b
*
* @internal
*/
export function handleConnectionChange(
a: Map<string, NodeConnection>,
b: Map<string, NodeConnection>,
cb?: (diff: NodeConnection[]) => void,
) {
if (!cb) {
return
}
const diff: NodeConnection[] = []
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
}
/**
* @internal
*/
export function areSetsEqual(a: Set<string>, b: Set<string>) {
if (a.size !== b.size) {
return false
}
for (const item of a) {
if (!b.has(item)) {
return false
}
}
return true
}
/**
* @internal
*/
export function createGraphEdges(
nextEdges: (Edge | Connection)[],
isValidConnection: ValidConnectionFunc | null,
findNode: Actions['findNode'],
findEdge: Actions['findEdge'],
onError: VueFlowStore['emits']['error'],
defaultEdgeOptions: DefaultEdgeOptions | undefined,
nodes: GraphNode[],
edges: GraphEdge[],
) {
const validEdges: GraphEdge[] = []
for (const edgeOrConnection of nextEdges) {
const edge = isEdge(edgeOrConnection)
? edgeOrConnection
: addEdgeToStore(edgeOrConnection, edges, onError, defaultEdgeOptions)
if (!edge) {
continue
}
const sourceNode = findNode(edge.source)
const targetNode = findNode(edge.target)
if (!sourceNode || !targetNode) {
onError(new VueFlowError(ErrorCode.EDGE_SOURCE_TARGET_MISSING, edge.id, edge.source, edge.target))
continue
}
if (!sourceNode) {
onError(new VueFlowError(ErrorCode.EDGE_SOURCE_MISSING, edge.id, edge.source))
continue
}
if (!targetNode) {
onError(new VueFlowError(ErrorCode.EDGE_TARGET_MISSING, edge.id, edge.target))
continue
}
if (isValidConnection) {
const isValid = isValidConnection(edge, {
edges,
nodes,
sourceNode,
targetNode,
})
if (!isValid) {
onError(new VueFlowError(ErrorCode.EDGE_INVALID, edge.id))
continue
}
}
const existingEdge = findEdge(edge.id)
validEdges.push({
...parseEdge(edge, existingEdge, defaultEdgeOptions),
sourceNode,
targetNode,
})
}
return validEdges
}