feat(nodes, edges, store): Add updateNodeDimensions actions

* Returns new changes for updating node dimensions
* Add expandParent prop to nodes when parents should be expanded to fit the new child in them
*
This commit is contained in:
Braks
2022-04-04 21:42:48 +02:00
parent 5cc0da29c0
commit 0093df43a9
11 changed files with 244 additions and 96 deletions
+3 -13
View File
@@ -94,25 +94,15 @@ const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } })
store.hooks.nodeDragStop.trigger({ event, node: node.value })
}
const updatePosition = (width: number, height: number) => {
const handleBounds = getHandleBounds(nodeElement.value, scale.value, id)
node.value.dimensions = {
width,
height,
}
node.value.handleBounds = handleBounds
}
const removeEmpty = (obj: Record<string, any>) => Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null))
store.updateNodePosition({ id: node.value.id, diff: { x: 0, y: 0 } })
onMounted(() => {
const dimensions = ref(getDimensions(nodeElement.value))
useResizeObserver(nodeElement, () => (dimensions.value = getDimensions(nodeElement.value)))
useResizeObserver(nodeElement, () => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]))
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () =>
nextTick(() => updatePosition(dimensions.value.width, dimensions.value.height)),
nextTick(() => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])),
)
watch(dimensions, ({ width: w, height: h }) => nextTick(() => w > 0 && h > 0 && updatePosition(w, h)), { immediate: true })
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])
})
</script>
<script lang="ts">
+1 -1
View File
@@ -22,7 +22,7 @@ const props = withDefaults(defineProps<FlowProps>(), {
applyDefault: undefined,
fitViewOnInit: undefined,
})
const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue', 'update:elements', 'update:edges', 'update:nodes'])
const emit = defineEmits([...Object.keys(createHooks()), 'update:modelValue', 'update:edges', 'update:nodes'])
const {
id,
+35 -1
View File
@@ -12,13 +12,16 @@ import {
Edge,
Connection,
GraphEdge,
NodePositionChange,
} from '~/types'
import {
applyChanges,
connectionExists,
createPositionChange,
createSelectionChange,
getDimensions,
getEdgeId,
getHandleBounds,
getSelectionChanges,
isEdge,
isGraphEdge,
@@ -110,7 +113,7 @@ export const parseChildren = (
export default (state: State, getters: ComputedGetters): Actions => {
const updateNodePosition: Actions['updateNodePosition'] = ({ id, diff = { x: 0, y: 0 }, dragging }) => {
const changes: NodeDimensionChange[] = []
const changes: NodePositionChange[] = []
state.nodes.forEach((node) => {
if (node.selected) {
@@ -126,6 +129,36 @@ export default (state: State, getters: ComputedGetters): Actions => {
if (changes.length) state.hooks.nodesChange.trigger(changes)
}
const updateNodeDimensions: Actions['updateNodeDimensions'] = (updates) => {
const changes: NodeDimensionChange[] = updates.reduce<NodeDimensionChange[]>((res, update) => {
const node = getters.getNode.value(update.id)
if (node) {
const dimensions = getDimensions(update.nodeElement)
const doUpdate = !!(
dimensions.width &&
dimensions.height &&
(node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height || update.forceUpdate)
)
if (doUpdate) {
const handleBounds = getHandleBounds(update.nodeElement, state.transform[2])
node.dimensions = dimensions
node.handleBounds = handleBounds
res.push({
id: node.id,
type: 'dimensions',
dimensions,
})
}
}
return res
}, [])
if (changes.length) state.hooks.nodesChange.trigger(changes)
}
const addSelectedNodes: Actions['addSelectedNodes'] = (nodes) => {
const selectedNodesIds = nodes.map((n) => n.id)
@@ -296,6 +329,7 @@ export default (state: State, getters: ComputedGetters): Actions => {
}
return {
updateNodePosition,
updateNodeDimensions,
setElements,
setNodes,
setEdges,
+66
View File
@@ -0,0 +1,66 @@
import { XYPosition, Dimensions } from './flow'
import { NodeHandleBounds, Node, GraphNode, CoordinateExtent } from './node'
import { Edge } from './edge'
export type CreatePositionChangeParams = {
node: GraphNode
nodeExtent: CoordinateExtent
diff?: XYPosition
dragging?: boolean
}
export type NodeDimensionChange = {
id: string
type: 'dimensions'
dimensions: Dimensions
handleBounds?: NodeHandleBounds
}
export type NodePositionChange = {
id: string
type: 'position'
position?: XYPosition
dragging?: boolean
}
export type NodeSelectionChange = {
id: string
type: 'select'
selected: boolean
}
export type NodeRemoveChange = {
id: string
type: 'remove'
}
export type NodeAddChange<NodeData = any> = {
item: Node<NodeData>
type: 'add'
}
export type NodeResetChange<NodeData = any> = {
item: Node<NodeData>
type: 'reset'
}
export type NodeChange =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange
| NodeResetChange
export type EdgeSelectionChange = NodeSelectionChange
export type EdgeRemoveChange = NodeRemoveChange
export type EdgeAddChange<EdgeData = any> = {
item: Edge<EdgeData>
type: 'add'
}
export type EdgeResetChange<EdgeData = any> = {
item: Edge<EdgeData>
type: 'reset'
}
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange | EdgeAddChange | EdgeResetChange
export type ElementChange = NodeChange | EdgeChange
+23
View File
@@ -76,6 +76,11 @@ export interface Edge<T = any> extends Element<T> {
selectable?: boolean
}
export type DefaultEdgeOptions = Omit<
Edge,
'id' | 'source' | 'target' | 'sourceHandle' | 'targetHandle' | 'sourceNode' | 'targetNode'
>
export interface EdgePositions {
sourceX: number
sourceY: number
@@ -125,6 +130,24 @@ export interface EdgeProps<Data = any> {
updatable?: boolean
markerStart?: string
markerEnd?: string
curvature?: number
}
export type BaseEdgeProps = Pick<
EdgeProps,
| 'label'
| 'labelStyle'
| 'labelShowBg'
| 'labelBgStyle'
| 'labelBgPadding'
| 'labelBgBorderRadius'
| 'style'
| 'markerStart'
| 'markerEnd'
> & {
centerX: number
centerY: number
path: string
}
export interface SmoothStepEdgeProps<Data = any> extends EdgeProps<Data> {
+1 -1
View File
@@ -5,7 +5,7 @@ import { ConnectionLineType, ConnectionMode } from './connection'
import { KeyCode, PanOnScrollMode, UseZoomPanHelper } from './zoom'
import { Actions, ComputedGetters, State, FlowStore } from './store'
import { FlowHooksOn } from './hooks'
import { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from "~/types/components";
import { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components'
export type FlowElement<N = any, E = any> = GraphNode<N> | GraphEdge<E>
export type FlowElements<N = any, E = any> = FlowElement<N, E>[]
+2 -34
View File
@@ -2,42 +2,10 @@ import { EventHook, EventHookOn } from '@vueuse/core'
import { MouseTouchEvent } from '@braks/revue-draggable'
import { FlowInstance, Dimensions, XYPosition } from './flow'
import { GraphEdge } from './edge'
import { CoordinateExtent, GraphNode, HandleBounds } from './node'
import { GraphNode } from './node'
import { Connection, OnConnectStartParams } from './connection'
import { FlowTransform } from './zoom'
export type NodeDimensionChange = {
id: string
type: 'dimensions'
dimensions?: Dimensions
position?: XYPosition
handleBounds?: HandleBounds
dragging?: boolean
}
export type NodeSelectionChange = {
id: string
type: 'select'
selected: boolean
}
export type NodeRemoveChange = {
id: string
type: 'remove'
}
export type NodeChange = NodeDimensionChange | NodeSelectionChange | NodeRemoveChange
export type EdgeSelectionChange = NodeSelectionChange
export type EdgeRemoveChange = NodeRemoveChange
export type EdgeChange = EdgeSelectionChange | EdgeRemoveChange
export type SelectionChange = EdgeSelectionChange | NodeSelectionChange
export type RemoveChange = EdgeRemoveChange | NodeRemoveChange
export type CreatePositionChangeParams = {
node: GraphNode
nodeExtent: CoordinateExtent
diff?: XYPosition
dragging?: boolean
}
import { EdgeChange, NodeChange } from './changes'
export interface FlowEvents<N = any, E = N> {
nodesChange: NodeChange[]
+1
View File
@@ -6,3 +6,4 @@ export * from './node'
export * from './zoom'
export * from './store'
export * from './hooks'
export * from './changes'
+28 -18
View File
@@ -1,16 +1,17 @@
import { CSSProperties } from 'vue'
import { XYPosition, Position, SnapGrid, Element, XYZPosition, Dimensions } from './flow'
import { HandleElement, ValidConnectionFunc } from './components'
import { DefaultNodeTypes, HandleElement, ValidConnectionFunc } from './components'
export type CoordinateExtent = [[number, number], [number, number]]
export type HandleBounds = {
export type NodeHandleBounds = {
source?: HandleElement[]
target?: HandleElement[]
}
export interface Node<T = any> extends Element<T> {
position: XYPosition
type?: keyof DefaultNodeTypes | string
targetPosition?: Position
sourcePosition?: Position
draggable?: boolean
@@ -21,29 +22,36 @@ export interface Node<T = any> extends Element<T> {
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
extent?: 'parent' | CoordinateExtent
expandParent?: boolean
children?: Node<T>[]
dimensions?: Dimensions
}
export interface GraphNode<T = any> extends Node<T> {
export interface GraphNode<T = any, P = any> extends Node<T> {
computedPosition: XYZPosition
handleBounds: HandleBounds
handleBounds: NodeHandleBounds
dimensions: Dimensions
parentNode?: GraphNode<T>
isParent?: boolean
selected?: boolean
dragging?: boolean
isParent: boolean
selected: boolean
dragging: boolean
parentNode?: GraphNode<P extends infer U ? U : never>
}
export interface NodeProps<T = any> {
id: string
dimensions: Dimensions
handleBounds: HandleBounds
handleBounds: NodeHandleBounds
/** computed position is the position relative to the node's extent (i.e. parent or CoordinateExtent) */
computedPosition: XYZPosition
/** x and y position on the graph */
position: XYPosition
draggable?: boolean
selectable?: boolean
connectable?: boolean
draggable: boolean
selectable: boolean
connectable: boolean
selected: boolean
dragging: boolean
hidden: boolean
nodeElement: HTMLDivElement
zIndex: number
label?:
| string
| {
@@ -52,17 +60,19 @@ export interface NodeProps<T = any> {
}
class?: string
style?: CSSProperties
hidden?: boolean
type?: string
data?: T
selected?: boolean
targetPosition?: Position
sourcePosition?: Position
dragging?: boolean
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
parentNode?: any
parentNode?: string
isParent?: boolean
children?: Node<T>[]
nodeElement: HTMLDivElement
dragHandle?: string
}
export type NodeBounds = XYPosition & {
width: number | null
height: number | null
}
+9 -1
View File
@@ -5,7 +5,8 @@ import { Connection, ConnectionLineType, ConnectionMode, SetConnectionId } from
import { Edge, GraphEdge } from './edge'
import { GraphNode, CoordinateExtent, Node } from './node'
import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode } from './zoom'
import { EdgeChange, FlowHooks, FlowHooksOn, NodeChange } from './hooks'
import { FlowHooks, FlowHooksOn } from './hooks'
import { NodeChange, EdgeChange } from './changes'
export interface State<N = any, E = N> extends Omit<FlowOptions<N, E>, 'id' | 'modelValue'> {
hooks: FlowHooks<N, E>
@@ -91,6 +92,13 @@ export interface Actions<N = any, E = N> {
setInteractive: (isInteractive: boolean) => void
setState: (state: Partial<FlowOptions<N, E>>) => void
updateNodePosition: ({ id, diff, dragging }: { id?: string; diff?: XYPosition; dragging?: boolean }) => void
updateNodeDimensions: (
update: {
id: string
nodeElement: HTMLDivElement
forceUpdate?: boolean
}[],
) => void
$reset: () => void
}
+75 -27
View File
@@ -1,61 +1,109 @@
import {
CreatePositionChangeParams,
EdgeChange,
EdgeSelectionChange,
ElementChange,
FlowElement,
FlowElements,
GraphNode,
NodeChange,
NodeDimensionChange,
SelectionChange,
NodeSelectionChange,
NodePositionChange,
} from '~/types'
import { clampPosition, isGraphNode } from '~/utils/graph'
function handleParentExpand(parent: GraphNode, updateItem: GraphNode) {
if (parent) {
const extendWidth = updateItem.position.x + updateItem.dimensions.width - parent.dimensions.width
const extendHeight = updateItem.position.y + updateItem.dimensions.height - parent.dimensions.height
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
parent.style = { ...parent.style } || {}
if (extendWidth > 0) {
if (!parent.style.width) {
parent.style.width = parent.dimensions.width
}
;(<number>parent.style.width) += extendWidth
}
if (extendHeight > 0) {
if (!parent.style.height) {
parent.style.height = parent.dimensions.height
}
;(<number>parent.style.height) += extendHeight
}
if (updateItem.position.x < 0) {
const xDiff = Math.abs(updateItem.position.x)
parent.position.x = parent.position.x - xDiff
;(<number>parent.style.width) += xDiff
updateItem.position.x = 0
}
if (updateItem.position.y < 0) {
const yDiff = Math.abs(updateItem.position.y)
parent.position.y = parent.position.y - yDiff
;(<number>parent.style.height) += yDiff
updateItem.position.y = 0
}
parent.dimensions.width = <number>parent.style.width
parent.dimensions.height = <number>parent.style.height
}
}
}
export const applyChanges = <
T extends FlowElement = GraphNode,
C extends NodeChange = T extends GraphNode ? NodeChange : EdgeChange,
C extends ElementChange = T extends GraphNode ? NodeChange : EdgeChange,
>(
changes: C[],
elements: T[],
): T[] => {
let elementIds = elements.map((el) => el.id)
changes.forEach((change) => {
const i = elementIds.indexOf(change.id)
if (change.type === 'add') return elements.push(change.item as any)
const i = elementIds.indexOf((<any>change).id)
const el = elements[i]
if (el) {
switch (change.type) {
case 'select':
el.selected = change.selected
break
case 'dimensions':
if (isGraphNode(el)) {
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
if (typeof change.position !== 'undefined') el.position = change.position
if (typeof change.dragging !== 'undefined') el.dragging = change.dragging
}
break
case 'remove':
elements.splice(i, 1)
elementIds = elements.map((el) => el.id)
break
}
switch (change.type) {
case 'select':
el.selected = change.selected
break
case 'position':
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, el.parentNode)
}
break
case 'dimensions':
if (isGraphNode(el)) {
if (typeof change.dimensions !== 'undefined') el.dimensions = change.dimensions
if (el.expandParent && el.parentNode) handleParentExpand(el, el.parentNode)
}
break
case 'remove':
elements.splice(i, 1)
elementIds = elements.map((el) => el.id)
break
}
})
return elements
}
export const createSelectionChange = (id: string, selected: boolean): SelectionChange => ({
export const createSelectionChange = (id: string, selected: boolean): NodeSelectionChange | EdgeSelectionChange => ({
id,
type: 'select',
selected,
})
export const createPositionChange = ({ node, diff, dragging, nodeExtent }: CreatePositionChangeParams): NodeDimensionChange => {
const change: NodeDimensionChange = {
export const createPositionChange = ({ node, diff, dragging, nodeExtent }: CreatePositionChangeParams): NodePositionChange => {
const change: NodePositionChange = {
id: node.id,
type: 'dimensions',
type: 'position',
dragging: !!dragging,
handleBounds: node.handleBounds,
}
if (diff) {
@@ -99,5 +147,5 @@ export const getSelectionChanges = (items: FlowElements, selectedIds: string[])
}
return res
}, [] as SelectionChange[])
}, [] as (NodeSelectionChange | EdgeSelectionChange)[])
}