diff --git a/src/components/Edges/EdgeWrapper.vue b/src/components/Edges/EdgeWrapper.vue index 8e5c86a4..c5947001 100644 --- a/src/components/Edges/EdgeWrapper.vue +++ b/src/components/Edges/EdgeWrapper.vue @@ -25,7 +25,6 @@ const onEdgeClick = (event: MouseEvent) => { store.addSelectedEdges([props.edge]) } store.hooks.edgeClick.trigger(data) - store.hooks.elementClick.trigger({ event, element: props.edge }) } const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: props.edge }) const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: props.edge }) diff --git a/src/composables/useElementState.ts b/src/composables/useElementState.ts new file mode 100644 index 00000000..f934c45a --- /dev/null +++ b/src/composables/useElementState.ts @@ -0,0 +1,117 @@ +import { + FlowStore, + GraphEdge, + NodeChange, + EdgeChange, + UseEdgesStateOptions, + UseEdgeState, + Edge, + Connection, + GraphNode, + UseNodesStateOptions, + UseNodesState, +} from '~/types' +import { connectionExists, getEdgeId, isEdge, isGraphEdge, parseEdge } from '~/utils' +import { parseChildren } from '~/store' + +const addEdge = (edgeParams: Edge | Connection, edges: Edge[]) => { + if (!edgeParams.source || !edgeParams.target) { + console.warn("Can't create edge. An edge needs a source and a target.") + return false + } + + let edge + if (isEdge(edgeParams)) { + edge = { ...edgeParams } + } else { + edge = { + ...edgeParams, + id: getEdgeId(edgeParams), + } as Edge + } + edge = parseEdge(edge) + if (connectionExists(edge, edges)) return false + return edge +} + +const updateEdge = (edge: GraphEdge, newConnection: Connection, edges: GraphEdge[]) => { + if (!newConnection.source || !newConnection.target) { + console.warn("Can't create new edge. An edge needs a source and a target.") + return false + } + + const foundEdge = edges.find((e) => isGraphEdge(e) && e.id === edge.id) + + if (!foundEdge) { + console.warn(`The old edge with id=${edge.id} does not exist.`) + return false + } + + edge.id = getEdgeId(newConnection) + edge.source = newConnection.source + edge.target = newConnection.target + edge.sourceHandle = newConnection.sourceHandle + edge.targetHandle = newConnection.targetHandle + + return edge +} + +export const useEdgesState = ( + store: FlowStore, + apply: (changes: EdgeChange[]) => GraphEdge[], +): ((options?: UseEdgesStateOptions) => UseEdgeState) => { + return ({ edges, applyDefault } = { applyDefault: false }) => { + if (edges && edges.length) store.setEdges(edges) + if (applyDefault) store.hooksOn.OnEdgesChange((e) => apply(e)) + return { + edges: store.edges, + setEdges: store.setEdges, + addEdges: (params) => { + params.forEach((param) => { + const edge = addEdge(param, store.edges) + if (edge) { + const sourceNode = store.getNode(edge.source)! + const targetNode = store.getNode(edge.target)! + if (!sourceNode || typeof sourceNode === 'undefined') + console.warn(`couldn't create edge for source id: ${edge.source}; edge id: ${edge.id}`) + if (!targetNode || typeof targetNode === 'undefined') + console.warn(`couldn't create edge for target id: ${edge.target}; edge id: ${edge.id}`) + + store.edges.push({ + ...edge, + sourceNode, + targetNode, + }) + } + }) + return store.edges + }, + updateEdge: (oldEdge, newConnection) => updateEdge(oldEdge, newConnection, store.edges), + OnEdgesChange: store.hooksOn.OnEdgesChange, + } + } +} + +export const useNodesState = ( + store: FlowStore, + apply: (changes: NodeChange[]) => GraphNode[], +): ((options?: UseNodesStateOptions) => UseNodesState) => { + return ({ nodes, applyDefault } = { applyDefault: false }) => { + if (nodes && nodes.length) store.setNodes(nodes) + if (applyDefault) store.hooksOn.OnNodesChange((e) => apply(e)) + return { + nodes: store.nodes, + setNodes: store.setNodes, + addNodes: (nodes, extent) => { + const parsed = nodes.flatMap((node) => { + const children: GraphNode[] = [] + parseChildren(node, undefined, children, extent ?? store.nodeExtent, store.getNode) + return children + }) + store.nodes.concat(parsed) + return store.nodes + }, + OnNodesChange: store.hooksOn.OnNodesChange, + } + } +} diff --git a/src/composables/useVueFlow.ts b/src/composables/useVueFlow.ts index 5ff384f7..ad0c3bd0 100644 --- a/src/composables/useVueFlow.ts +++ b/src/composables/useVueFlow.ts @@ -1,4 +1,5 @@ import { getCurrentInstance } from 'vue' +import { useEdgesState, useNodesState } from './useElementState' import { EdgeChange, FlowOptions, GraphEdge, GraphNode, NodeChange, UseVueFlow } from '~/types' import { applyChanges } from '~/utils' import { VueFlow } from '~/context' @@ -17,28 +18,8 @@ export default (options?: FlowOptions): UseVueFlow => { vueFlow = { id: store.id, store, - useNodesState: (nodes) => { - store.setNodes(nodes) - return { - nodes: store.nodes, - setNodes: store.setNodes, - OnNodesChange: (applyDefault = false) => { - if (applyDefault) store.hooksOn.OnNodesChange((e) => applyNodes(e)) - return store.hooksOn.OnNodesChange - }, - } - }, - useEdgesState: (edges) => { - store.setEdges(edges) - return { - edges: store.edges, - setEdges: store.setEdges, - OnEdgesChange: (applyDefault = false) => { - if (applyDefault) store.hooksOn.OnEdgesChange((e) => applyEdges(e)) - return store.hooksOn.OnEdgesChange - }, - } - }, + useNodesState: (nodes, applyDefault) => useNodesState(store, applyNodes)({ nodes, applyDefault }), + useEdgesState: (edges, applyDefault) => useEdgesState(store, applyEdges)({ edges, applyDefault }), applyNodeChanges: applyNodes, applyEdgeChanges: applyEdges, ...store.hooksOn, diff --git a/src/store/actions.ts b/src/store/actions.ts index b53b73d5..8f4979b4 100644 --- a/src/store/actions.ts +++ b/src/store/actions.ts @@ -32,6 +32,24 @@ const getParent = (root: Node[], id: string): GraphNode | undefined => { return node } +export const parseChildren = ( + n: Node, + p: GraphNode | undefined, + arr: GraphNode[], + extent: CoordinateExtent, + getNode: (id: string) => GraphNode | undefined, +) => { + const parent = typeof p === 'undefined' || typeof p !== 'object' ? getParent(arr, n.id) : p + const parsed = parseNode(n, extent, { + ...getNode(n.id), + parentNode: parent, + }) + arr.push(parsed) + if (n.children && n.children.length) { + n.children.forEach((c) => parseChildren(c, parsed, arr, extent, getNode)) + } +} + export default (state: FlowState, getters: FlowGetters): FlowActions => { const updateNodePosition: FlowActions['updateNodePosition'] = ({ id, diff = { x: 0, y: 0 }, dragging }) => { const changes: NodeDimensionChange[] = [] @@ -103,28 +121,16 @@ export default (state: FlowState, getters: FlowGetters): FlowActions => { state.elementsSelectable = isInteractive } - const parseChildren = (n: Node, p: GraphNode | undefined, arr: GraphNode[], extent: CoordinateExtent) => { - const parent = typeof p === 'undefined' || typeof p !== 'object' ? getParent(arr, n.id) : p - const parsed = parseNode(n, extent, { - ...getters.getNode.value(n.id), - parentNode: parent, - }) - arr.push(parsed) - if (n.children && n.children.length) { - n.children.forEach((c) => parseChildren(c, parsed, arr, extent)) - } - } - const setNodes: FlowActions['setNodes'] = (nodes, extent?: CoordinateExtent) => { nodes = nodes.flatMap((node) => { const children: GraphNode[] = [] - parseChildren(node, undefined, children, extent ?? state.nodeExtent) + parseChildren(node, undefined, children, extent ?? state.nodeExtent, getters.getNode.value) return children }) state.nodes = nodes } const setEdges: FlowActions['setEdges'] = (edges) => { - state.edges = edges.flatMap((edge) => { + state.edges = edges.map((edge) => { const sourceNode = getters.getNode.value(edge.source)! const targetNode = getters.getNode.value(edge.target)! if (!sourceNode || typeof sourceNode === 'undefined') diff --git a/src/store/index.ts b/src/store/index.ts index 0115ebc5..f1aa12bd 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -1,3 +1,4 @@ export { default as useHooks, createHooks } from './hooks' export { default as useStore } from './store' +export * from './actions' export * from './state' diff --git a/src/types/flow.ts b/src/types/flow.ts index 9cd182cf..761904e4 100644 --- a/src/types/flow.ts +++ b/src/types/flow.ts @@ -1,7 +1,7 @@ import { CSSProperties } from 'vue' import { GraphEdge, Edge } from './edge' import { GraphNode, CoordinateExtent, Node } from './node' -import { ConnectionLineType, ConnectionMode } from './connection' +import { Connection, ConnectionLineType, ConnectionMode } from './connection' import { KeyCode, PanOnScrollMode, UseZoomPanHelper } from './zoom' import { FlowActions, FlowStore } from './store' import { EdgeChange, FlowHooksOn, NodeChange } from './hooks' @@ -119,21 +119,33 @@ export interface FlowProps { export type FlowOptions = FlowProps +type UseStateOptions = { + applyDefault?: boolean +} export type UseNodesState = { nodes: GraphNode[] setNodes: FlowActions['setNodes'] - OnNodesChange: (applyDefault?: boolean) => FlowHooksOn['OnNodesChange'] + addNodes: (nodes: Node[], extent?: CoordinateExtent) => GraphNode[] + OnNodesChange: FlowHooksOn['OnNodesChange'] +} +export interface UseNodesStateOptions extends UseStateOptions { + nodes?: Node[] +} +export interface UseEdgesStateOptions extends UseStateOptions { + edges?: Edge[] } export type UseEdgeState = { edges: GraphEdge[] setEdges: FlowActions['setEdges'] - OnEdgesChange: (applyDefault: boolean) => FlowHooksOn['OnEdgesChange'] + addEdges: (params: (Edge | Connection)[]) => GraphEdge[] + updateEdge: (oldEdge: GraphEdge, newConnection: Connection) => GraphEdge | false + OnEdgesChange: FlowHooksOn['OnEdgesChange'] } export type UseVueFlow = { id: string store: FlowStore - useNodesState: (nodes: Node[]) => UseNodesState - useEdgesState: (edges: Edge[]) => UseEdgeState + useNodesState: (nodes?: Node[], applyDefault?: boolean) => UseNodesState + useEdgesState: (edges?: Edge[], applyDefault?: boolean) => UseEdgeState applyNodeChanges: (changes: NodeChange[]) => void applyEdgeChanges: (changes: EdgeChange[]) => void } & FlowHooksOn diff --git a/src/utils/graph.ts b/src/utils/graph.ts index dbde524f..2e428d53 100644 --- a/src/utils/graph.ts +++ b/src/utils/graph.ts @@ -59,6 +59,55 @@ export const isNode = (element: Node | Edge | Connection): element is Node => 'i export const isGraphNode = (element: Node | Edge | Connection): element is GraphNode => isNode(element) && 'computedPosition' in element +export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial): GraphNode => { + defaults = !isGraphNode(node) + ? { + type: node.type ?? 'default', + dimensions: { + width: 0, + height: 0, + }, + handleBounds: { + source: [], + target: [], + }, + computedPosition: { + z: typeof node.style?.zIndex === 'string' ? parseInt(node.style?.zIndex) : node.style?.zIndex ?? 0, + ...clampPosition(node.position, nodeExtent), + }, + isParent: !!(node.children && node.children.length), + dragging: false, + ...defaults, + } + : defaults + + return { + ...node, + ...(defaults as GraphNode), + id: node.id.toString(), + } +} + +export const parseEdge = (edge: Edge, defaults?: Partial): GraphEdge => { + defaults = !isGraphEdge(edge) + ? { + sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : undefined, + targetHandle: edge.targetHandle ? edge.targetHandle.toString() : undefined, + type: edge.type ?? 'default', + source: edge.source.toString(), + target: edge.target.toString(), + z: typeof edge.style?.zIndex === 'string' ? parseInt(edge.style?.zIndex) : edge.style?.zIndex ?? 0, + ...defaults, + } + : defaults + + return { + ...edge, + ...(defaults as GraphEdge), + id: edge.id.toString(), + } +} + const getConnectedElements = (node: GraphNode, elements: Elements, dir: 'source' | 'target') => { if (!isNode(node)) return [] const ids = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => isEdge(e) && e[dir]) @@ -68,11 +117,11 @@ export const getOutgoers = (node: GraphNode, elements: Elements) => getConnected export const getIncomers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'source') -const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection) => +export const getEdgeId = ({ source, sourceHandle, target, targetHandle }: Connection) => `vueflow__edge-${source}${sourceHandle}-${target}${targetHandle}` -const connectionExists = (edge: Edge, elements: Elements) => { - return elements.some( +export const connectionExists = (edge: Edge, elements: Elements) => + elements.some( (el) => isEdge(el) && el.source === edge.source && @@ -80,7 +129,6 @@ const connectionExists = (edge: Edge, elements: Elements) => { (el.sourceHandle === edge.sourceHandle || (!el.sourceHandle && !edge.sourceHandle)) && (el.targetHandle === edge.targetHandle || (!el.targetHandle && !edge.targetHandle)), ) -} export const addEdge = (edgeParams: Edge | Connection, elements: Elements) => { if (!edgeParams.source || !edgeParams.target) { @@ -97,6 +145,7 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements) => { id: getEdgeId(edgeParams), } as Edge } + edge = parseEdge(edge) if (connectionExists(edge, elements)) return elements elements.push(edge) return [...elements, edge] @@ -152,58 +201,6 @@ export const pointToRendererPoint = ( export const onLoadProject = (currentStore: FlowStore) => (position: XYPosition) => pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid) -export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: Partial): GraphNode => { - defaults = !isGraphNode(node) - ? { - type: node.type ?? 'default', - dimensions: { - width: 0, - height: 0, - }, - handleBounds: { - source: [], - target: [], - }, - computedPosition: { - z: typeof node.style?.zIndex === 'string' ? parseInt(node.style?.zIndex) : node.style?.zIndex ?? 0, - ...clampPosition(node.position, nodeExtent), - }, - isParent: !!(node.children && node.children.length), - dragging: false, - ...defaults, - } - : defaults - - return { - ...node, - ...(defaults as GraphNode), - id: node.id.toString(), - } -} - -export const parseEdge = ( - edge: Edge, - defaults: Partial & { sourceNode: GraphNode; targetNode: GraphNode }, -): GraphEdge => { - defaults = !isGraphEdge(edge) - ? { - sourceHandle: edge.sourceHandle ? edge.sourceHandle.toString() : undefined, - targetHandle: edge.targetHandle ? edge.targetHandle.toString() : undefined, - type: edge.type ?? 'default', - source: edge.source.toString(), - target: edge.target.toString(), - z: typeof edge.style?.zIndex === 'string' ? parseInt(edge.style?.zIndex) : edge.style?.zIndex ?? 0, - ...defaults, - } - : defaults - - return { - ...edge, - ...defaults, - id: edge.id.toString(), - } -} - const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ x: Math.min(box1.x, box2.x), y: Math.min(box1.y, box2.y),