refactor(nodes)!: Change children API to parentNode API

* instead of passing a children arr, pass a parentNode id
* allows for easier adding of new child nodes in options api
This commit is contained in:
Braks
2022-04-04 21:42:48 +02:00
parent 0d0ac9d2e6
commit 0a079afcee
7 changed files with 169 additions and 167 deletions
+44 -49
View File
@@ -12,13 +12,12 @@ const { onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
position: { x: 100, y: 100 },
class: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: '200px', height: '200px' },
children: [
{
id: '2a',
label: 'Node 2a',
position: { x: 10, y: 50 },
},
],
},
{
id: '2a',
label: 'Node 2a',
position: { x: 10, y: 50 },
parentNode: '2',
},
{ id: '3', label: 'Node 3', position: { x: 320, y: 100 }, class: 'light' },
{
@@ -27,36 +26,36 @@ const { onConnect, nodes, edges, addEdges, addNodes } = useVueFlow({
position: { x: 320, y: 200 },
class: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: '300px', height: '300px' },
children: [
{
id: '4a',
label: 'Node 4a',
position: { x: 15, y: 65 },
class: 'light',
extent: 'parent',
},
{
id: '4b',
label: 'Node 4b',
position: { x: 15, y: 120 },
class: 'light',
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: '150px', width: '270px' },
children: [
{
id: '4b1',
label: 'Node 4b1',
position: { x: 20, y: 40 },
class: 'light',
},
{
id: '4b2',
label: 'Node 4b2',
position: { x: 100, y: 100 },
class: 'light',
},
],
},
],
},
{
id: '4a',
label: 'Node 4a',
position: { x: 15, y: 65 },
class: 'light',
extent: 'parent',
parentNode: '4',
},
{
id: '4b',
label: 'Node 4b',
position: { x: 15, y: 120 },
class: 'light',
style: { backgroundColor: 'rgba(255, 0, 255, 0.7)', height: '150px', width: '270px' },
parentNode: '4',
},
{
id: '4b1',
label: 'Node 4b1',
position: { x: 20, y: 40 },
class: 'light',
parentNode: '4b',
},
{
id: '4b2',
label: 'Node 4b2',
position: { x: 100, y: 100 },
class: 'light',
parentNode: '4b',
},
],
edges: [
@@ -75,21 +74,17 @@ onConnect((params) => addEdges([params]))
onMounted(() => {
// add nodes to parent
addNodes(
[
{
id: '999',
type: 'input',
label: 'Added after mount',
position: { x: 20, y: 100 },
class: 'light',
expandParent: true,
},
],
addNodes([
{
id: '999',
type: 'input',
label: 'Added after mount',
position: { x: 20, y: 100 },
class: 'light',
expandParent: true,
parentNode: '2',
},
)
])
})
</script>
<template>
+42 -40
View File
@@ -26,11 +26,11 @@ const scale = controlledComputed(
() => store.transform[2],
)
watch(
[() => node.value.position, () => node.value.parentNode],
[() => node.value.position, () => store.getNode(node.value.parentNode!)],
([pos, parent]) => {
const xyzPos = {
...pos,
z: node.value.computedPosition.z,
z: node.value.dragging || node.value.selected ? 1000 : node.value.computedPosition.z,
}
if (parent) {
node.value.computedPosition = getXYZPos(parent, xyzPos)
@@ -95,7 +95,9 @@ const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } })
store.updateNodePosition({ id: node.value.id, diff: { x: 0, y: 0 } })
onMounted(() => {
useResizeObserver(nodeElement, () => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]))
useResizeObserver(nodeElement, () =>
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]),
)
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () =>
nextTick(() => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])),
)
@@ -134,7 +136,7 @@ export default {
node.class,
]"
:style="{
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
zIndex: node.computedPosition.z,
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
...node.style,
@@ -149,47 +151,47 @@ export default {
>
<slot
v-bind="{
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
isConnectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode ? node.parentNode.id : undefined,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
isConnectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
}"
>
<component
:is="props.type ?? node.type"
v-bind="{
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
connectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode ? node.parentNode.id : undefined,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
nodeElement,
id: node.id,
type: node.type,
data: node.data,
selected: !!node.selected,
connectable: props.connectable,
position: node.position,
computedPosition: node.computedPosition,
dimensions: node.dimensions,
isValidTargetPos: node.isValidTargetPos,
isValidSourcePos: node.isValidSourcePos,
parentNode: node.parentNode,
dragging: !!node.dragging,
zIndex: node.dragging || node.selected ? 1000 : node.computedPosition.z,
targetPosition: node.targetPosition,
sourcePosition: node.sourcePosition,
label: node.label,
dragHandle: node.dragHandle,
}"
/>
</slot>
+47 -50
View File
@@ -13,6 +13,7 @@ import {
Connection,
GraphEdge,
NodePositionChange,
Getters,
} from '~/types'
import {
applyChanges,
@@ -34,16 +35,6 @@ import {
const isDef = <T>(val: T): val is NonNullable<T> => typeof val !== 'undefined'
const getParent = (root: Node[], id: string): GraphNode | undefined => {
let node
root.some((n) => {
if (n.id === id) return (node = n)
if (n.children) return (node = getParent(n.children, id))
return false
})
return node
}
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.")
@@ -92,25 +83,41 @@ const updateEdgeAction = (edge: GraphEdge, newConnection: Connection, edges: Gra
return true
}
const applyEdgeChangesAction = (changes: EdgeChange[], edges: GraphEdge[]) => applyChanges(changes, edges)
const applyNodeChangesAction = (changes: NodeChange[], nodes: GraphNode[]) => applyChanges(changes, nodes)
const applyEdgeChangesAction = (changes: EdgeChange[], edges: GraphEdge[], getNode: Getters['getNode']) =>
applyChanges(changes, edges, getNode)
const applyNodeChangesAction = (changes: NodeChange[], nodes: GraphNode[], getNode: Getters['getNode']) =>
applyChanges(changes, nodes, getNode)
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,
const createGraphNodes = (nodes: Node[], getNode: Getters['getNode'], currGraphNodes: GraphNode[], extent: CoordinateExtent) => {
const parentNodes: Record<string, true> = {}
const graphNodes = nodes.map((node) => {
const parsed = parseNode(node, extent, {
...getNode(node.id),
parentNode: node.parentNode,
})
if (node.parentNode) {
parentNodes[node.parentNode] = true
}
return parsed
})
arr.push(parsed)
if (n.children && n.children.length) {
n.children.forEach((c) => parseChildren(c, parsed, arr, extent, getNode))
}
graphNodes.forEach((node) => {
if (node.parentNode && ![...graphNodes, ...currGraphNodes].find((n) => n.id === node.parentNode)) {
throw new Error(`Parent node ${node.parentNode} not found`)
}
if (node.parentNode || parentNodes[node.id]) {
if (parentNodes[node.id]) {
node.isParent = true
}
const parent = node.parentNode ? getNode(node.parentNode) : undefined
if (parent) parent.isParent = true
}
})
return graphNodes
}
export default (state: State, getters: ComputedGetters): Actions => {
@@ -120,12 +127,12 @@ export default (state: State, getters: ComputedGetters): Actions => {
state.nodes.forEach((node) => {
if (node.selected) {
if (!node.parentNode) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }))
} else if (!isParentSelected(node)) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }))
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
} else if (!isParentSelected(node, getters.getNode.value)) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
}
} else if (node.id === id) {
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }))
changes.push(createPositionChange({ node, diff, nodeExtent: state.nodeExtent, dragging }, getters.getNode.value))
}
})
@@ -166,7 +173,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
let changedNodes: NodeChange[]
if (state.multiSelectionActive) changedNodes = selectedNodesIds.map((nodeId) => createSelectionChange(nodeId, true))
else changedNodes = getSelectionChanges(state.nodes, selectedNodesIds)
else changedNodes = getSelectionChanges(state.nodes, selectedNodesIds, getters.getNode.value)
if (changedNodes.length) state.hooks.nodesChange.trigger(changedNodes)
}
@@ -175,7 +182,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
let changedEdges: EdgeChange[]
if (state.multiSelectionActive) changedEdges = selectedEdgesIds.map((nodeId) => createSelectionChange(nodeId, true))
else changedEdges = getSelectionChanges(state.edges, selectedEdgesIds)
else changedEdges = getSelectionChanges(state.edges, selectedEdgesIds, getters.getNode.value)
if (changedEdges.length) state.hooks.edgesChange.trigger(changedEdges)
}
@@ -207,12 +214,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
const setNodes: Actions['setNodes'] = (nodes, extent?: CoordinateExtent) => {
if (!state.initialized && !nodes.length) return
nodes = nodes.flatMap((node) => {
const children: GraphNode[] = []
parseChildren(node, undefined, children, extent ?? state.nodeExtent, getters.getNode.value)
return children
})
state.nodes = <GraphNode[]>nodes
state.nodes = createGraphNodes(nodes, getters.getNode.value, state.nodes, extent ?? state.nodeExtent)
}
const setEdges: Actions['setEdges'] = (edges) => {
if (!state.initialized && !edges.length) return
@@ -241,15 +243,8 @@ export default (state: State, getters: ComputedGetters): Actions => {
setEdges(elements.filter(isEdge))
}
const addNodes: Actions['addNodes'] = (nodes, options) => {
const parsed = nodes.flatMap((node) => {
const children: GraphNode[] = []
const parent =
options && (typeof options.parentNode === 'string' ? getters.getNode.value(options.parentNode) : options.parentNode)
parseChildren(node, parent, children, options?.extent ?? state.nodeExtent, getters.getNode.value)
return children
})
state.nodes.push(...parsed)
const addNodes: Actions['addNodes'] = (nodes, extent) => {
state.nodes.push(...createGraphNodes(nodes, getters.getNode.value, state.nodes, extent ?? state.nodeExtent))
}
const addEdges: Actions['addEdges'] = (params) => {
@@ -275,8 +270,10 @@ export default (state: State, getters: ComputedGetters): Actions => {
const updateEdge: Actions['updateEdge'] = (oldEdge, newConnection) =>
updateEdgeAction(oldEdge, newConnection, state.edges, addEdges)
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) => applyNodeChangesAction(changes, state.nodes)
const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) => applyEdgeChangesAction(changes, state.edges)
const applyNodeChanges: Actions['applyNodeChanges'] = (changes) =>
applyNodeChangesAction(changes, state.nodes, getters.getNode.value)
const applyEdgeChanges: Actions['applyEdgeChanges'] = (changes) =>
applyEdgeChangesAction(changes, state.edges, getters.getNode.value)
const setState: Actions['setState'] = (opts) => {
const skip = ['modelValue', 'nodes', 'edges', 'maxZoom', 'minZoom', 'translateExtent']
+1 -2
View File
@@ -23,7 +23,7 @@ export interface Node<T = any> extends Element<T> {
isValidSourcePos?: ValidConnectionFunc
extent?: 'parent' | CoordinateExtent
expandParent?: boolean
children?: Node<T>[]
parentNode?: string
}
export interface GraphNode<T = any> extends Node<T> {
@@ -33,7 +33,6 @@ export interface GraphNode<T = any> extends Node<T> {
isParent: boolean
selected: boolean
dragging: boolean
parentNode?: GraphNode<T>
}
export interface NodeProps<T = any> {
+1 -1
View File
@@ -85,7 +85,7 @@ export interface Actions<N = any, E = N> {
setElements: (elements: Elements<N, E>, extent?: CoordinateExtent) => void
setNodes: (nodes: Node<N>[], extent?: CoordinateExtent) => void
setEdges: (edges: Edge<E>[]) => void
addNodes: <NA = N>(nodes: Node<NA>[], options?: { extent?: CoordinateExtent; parentNode: GraphNode | string }) => void
addNodes: <NA = N>(nodes: Node<NA>[], extent?: CoordinateExtent) => void
addEdges: <EA = E>(edgesOrConnections: (Edge<EA> | Connection)[]) => void
updateEdge: <EU = E>(oldEdge: GraphEdge<EU>, newConnection: Connection) => boolean
applyEdgeChanges: <ED = E>(changes: EdgeChange[]) => GraphEdge<ED>[]
+27 -19
View File
@@ -10,10 +10,11 @@ import {
NodeChange,
NodeSelectionChange,
NodePositionChange,
Getters,
} from '~/types'
function handleParentExpand(updateItem: GraphNode) {
const parent = updateItem.parentNode
function handleParentExpand(updateItem: GraphNode, getNode: Getters['getNode']) {
const parent = updateItem.parentNode ? getNode(updateItem.parentNode) : undefined
if (parent) {
const extendWidth = updateItem.position.x + updateItem.dimensions.width - parent.dimensions.width
const extendHeight = updateItem.position.y + updateItem.dimensions.height - parent.dimensions.height
@@ -69,8 +70,12 @@ function handleParentExpand(updateItem: GraphNode) {
updateItem.position.y = 0
}
parent.dimensions.width = <number>parent.style.width
parent.dimensions.height = <number>parent.style.height
parent.dimensions.width = (
typeof parent.style.width === 'string' ? parseInt((<string>parent.style.width)!, 10) : parent.style.width
)!
parent.dimensions.height = (
typeof parent.style.height === 'string' ? parseInt((<string>parent.style.height)!, 10) : parent.style.height
)!
}
}
}
@@ -81,6 +86,7 @@ export const applyChanges = <
>(
changes: C[],
elements: T[],
getNode: Getters['getNode'],
): T[] => {
let elementIds = elements.map((el) => el.id)
changes.forEach((change) => {
@@ -95,13 +101,13 @@ export const applyChanges = <
if (isGraphNode(el)) {
if (typeof change.position !== 'undefined') el.position = change.position
if (typeof change.dragging !== 'undefined') el.dragging = change.dragging
if (el.expandParent && el.parentNode) handleParentExpand(el)
if (el.expandParent && el.parentNode) handleParentExpand(el, getNode)
}
break
case 'dimensions':
if (isGraphNode(el)) {
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
if (el.expandParent && el.parentNode) handleParentExpand(el)
if (el.expandParent && el.parentNode) handleParentExpand(el, getNode)
}
break
case 'remove':
@@ -120,7 +126,11 @@ export const createSelectionChange = (id: string, selected: boolean): NodeSelect
selected,
})
export const createPositionChange = ({ node, diff, dragging, nodeExtent }: CreatePositionChangeParams): NodePositionChange => {
export const createPositionChange = (
{ node, diff, dragging, nodeExtent }: CreatePositionChangeParams,
getNode: Getters['getNode'],
): NodePositionChange => {
const parent = node.parentNode ? getNode(node.parentNode) : undefined
const change: NodePositionChange = {
id: node.id,
type: 'position',
@@ -131,15 +141,12 @@ export const createPositionChange = ({ node, diff, dragging, nodeExtent }: Creat
const nextPosition = { x: node.position.x + diff.x, y: node.position.y + diff.y }
let currentExtent = nodeExtent || node.extent
if (node.extent === 'parent' && node.parentNode && node.dimensions.width && node.dimensions.height) {
if (node.extent === 'parent' && parent && node.dimensions.width && node.dimensions.height) {
currentExtent =
node.parentNode?.dimensions.width && node.parentNode?.dimensions.height
parent.dimensions.width && parent.dimensions.height
? [
[0, 0],
[
node.parentNode.dimensions.width - node.dimensions.width,
node.parentNode.dimensions.height - node.dimensions.height,
],
[parent.dimensions.width - node.dimensions.width, parent.dimensions.height - node.dimensions.height],
]
: currentExtent
}
@@ -150,16 +157,17 @@ export const createPositionChange = ({ node, diff, dragging, nodeExtent }: Creat
return change
}
const isParentSelected = (node: GraphNode, selectedIds: string[]): boolean => {
if (!node.parentNode) return false
if (selectedIds.includes(node.parentNode.id)) return true
return isParentSelected(node.parentNode, selectedIds)
const isParentSelected = (node: GraphNode, selectedIds: string[], getNode: Getters['getNode']): boolean => {
const parent = node.parentNode ? getNode(node.parentNode) : undefined
if (!node.parentNode || !parent) return false
if (selectedIds.includes(node.parentNode)) return true
return isParentSelected(parent, selectedIds, getNode)
}
export const getSelectionChanges = (items: FlowElements, selectedIds: string[]) => {
export const getSelectionChanges = (items: FlowElements, selectedIds: string[], getNode: Getters['getNode']) => {
return items.reduce((res, item) => {
const willBeSelected =
selectedIds.includes(item.id) || !!(isGraphNode(item) && item.parentNode && isParentSelected(item, selectedIds))
selectedIds.includes(item.id) || !!(isGraphNode(item) && item.parentNode && isParentSelected(item, selectedIds, getNode))
if (!item.selected && willBeSelected) {
res.push(createSelectionChange(item.id, true))
+7 -6
View File
@@ -16,8 +16,8 @@ import {
Rect,
Transform,
XYPosition,
XYZPosition,
} from '~/types'
XYZPosition, Getters
} from "~/types";
import { useWindow } from '~/composables'
const isHTMLElement = (el: EventTarget): el is HTMLElement => ('nodeName' || 'hasAttribute') in el
@@ -75,7 +75,6 @@ export const parseNode = (node: Node, nodeExtent: CoordinateExtent, defaults?: P
z: typeof node.style?.zIndex === 'string' ? parseInt(node.style?.zIndex as string) : node.style?.zIndex ?? 0,
...clampPosition(node.position, nodeExtent),
},
isParent: !!(node.children && node.children.length),
dragging: false,
draggable: undefined,
selectable: undefined,
@@ -335,10 +334,12 @@ export const getXYZPos = (parentNode: GraphNode, computedPosition: XYZPosition):
}
}
export const isParentSelected = (node: GraphNode): boolean => {
export const isParentSelected = (node: GraphNode, getNode: Getters['getNode']): boolean => {
if (!node.parentNode) return false
if (node.parentNode.selected) return true
return isParentSelected(node.parentNode)
const parent = getNode(node.parentNode)
if (!parent) return false
if (parent.selected) return true
return isParentSelected(parent, getNode)
}
export const getMarkerId = (marker: EdgeMarkerType | undefined): string => {