update!: graphnode and node typing

* graphnode is a node containing internal Vue Flow data
* move util files to util directory
* move flow actions type into store file
* rename panel type file to zoom
* rename types file to flow
* Rename Node to NodeWrapper
* Rename Edge to EdgeWrapper

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-11-21 16:25:27 +01:00
parent ba24f3ca62
commit 062aee45b2
36 changed files with 280 additions and 275 deletions

View File

@@ -35,7 +35,7 @@ const onLoad = (flowInstance: FlowInstance) => {
}
const updatePos = () => {
elements.value = elements.value.map((el: FlowElement) => {
elements.value = elements.value.map((el) => {
if (isNode(el)) {
el.position = {
x: Math.random() * 400,
@@ -50,7 +50,7 @@ const logToObject = () => console.log(rfInstance.value?.toObject())
const resetTransform = () => rfInstance.value?.setTransform({ x: 0, y: 0, zoom: 1 })
const toggleclasss = () => {
elements.value = elements.value.map((el: FlowElement) => {
elements.value = elements.value.map((el) => {
if (isNode(el)) el.class = el.class === 'light' ? 'dark' : 'light'
return el
})

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { getEdgeCenter, getBezierPath, getMarkerEnd, ArrowHeadType, Edge, ElementId, Position, useVueFlow } from '~/index'
import { getEdgeCenter, getBezierPath, getMarkerEnd, ArrowHeadType, ElementId, Position, useVueFlow, EdgeProps } from '~/index'
interface CustomEdgeProps<T = any> extends Edge<T> {
interface CustomEdgeProps<T = any> extends EdgeProps<T> {
id: ElementId
sourceX: number
sourceY: number

View File

@@ -44,8 +44,7 @@ const nodeClassNameFunc = props.nodeClassName instanceof Function ? props.nodeCl
const shapeRendering: ShapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
const nodes = computed(() => store.nodes.filter((node) => !node.isHidden))
const bb = computed(() => getRectOfNodes(nodes.value))
const bb = computed(() => getRectOfNodes(store.getNodes))
const viewBB = computed(() => ({
x: -store.transform[0] / store.transform[2],
y: -store.transform[1] / store.transform[2],
@@ -90,7 +89,7 @@ const d = computed(() => {
:viewBox="`${viewBox.x || 0} ${viewBox.y || 0} ${viewBox.width || 0} ${viewBox.height || 0}`"
class="vue-flow__minimap"
>
<template v-for="node of nodes" :key="`mini-map-node-${node.id}`">
<template v-for="node of store.getNodes" :key="`mini-map-node-${node.id}`">
<slot
:x="node.__vf.position.x"
:y="node.__vf.position.y"

View File

@@ -2,10 +2,10 @@
import { CSSProperties } from 'vue'
import { getBezierPath, getSmoothStepPath } from '../Edges/utils'
import { useStore } from '../../composables'
import { ConnectionLineType, HandleElement, Node, Position } from '../../types'
import { ConnectionLineType, HandleElement, GraphNode, Position } from '../../types'
interface ConnectionLineProps {
sourceNode: Node
sourceNode: GraphNode
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
}
@@ -19,29 +19,29 @@ const store = useStore()
const sourceHandle =
store.connectionHandleId && store.connectionHandleType
? props.sourceNode.__vf?.handleBounds[store.connectionHandleType].find(
? props.sourceNode.__vf.handleBounds[store.connectionHandleType]?.find(
(d: HandleElement) => d.id === store.connectionHandleId,
)
: store.connectionHandleType && props.sourceNode.__vf?.handleBounds[store.connectionHandleType ?? 'source'][0]
: store.connectionHandleType && props.sourceNode.__vf.handleBounds[store.connectionHandleType ?? 'source']?.[0]
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (props.sourceNode.__vf?.width as number) / 2
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.__vf?.height
const sourceX = props.sourceNode.__vf?.position?.x + sourceHandleX
const sourceY = props.sourceNode.__vf?.position?.y + sourceHandleY
const isRightOrLeft = sourceHandle.position === Position.Left || sourceHandle.position === Position.Right
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
const targetX = computed(() => (store.connectionPosition.x - store.transform[0]) / store.transform[2])
const targetY = computed(() => (store.connectionPosition.y - store.transform[1]) / store.transform[2])
const dAttr = computed(() => {
let path = `M${sourceX.value},${sourceY.value} ${targetX.value},${targetY.value}`
let path = `M${sourceX},${sourceY} ${targetX.value},${targetY.value}`
switch (props.connectionLineType) {
case ConnectionLineType.Bezier:
path = getBezierPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
@@ -51,7 +51,7 @@ const dAttr = computed(() => {
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
@@ -62,7 +62,7 @@ const dAttr = computed(() => {
path = getSmoothStepPath({
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
sourcePosition: sourceHandle?.position,
targetX: targetX.value,
targetY: targetY.value,
targetPosition,
@@ -78,7 +78,7 @@ const dAttr = computed(() => {
v-bind="{
sourceX,
sourceY,
sourcePosition: sourceHandle.position,
sourcePosition: sourceHandle?.position,
targetX,
targetY,
targetPosition,

View File

@@ -1,17 +1,16 @@
<script lang="ts" setup>
import { useHandle, useStore } from '../../composables'
import { ConnectionMode, Edge, EdgePositions, Position } from '../../types'
import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '../../container/EdgeRenderer/utils'
import { isEdge } from '../../utils'
import { isEdge, getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from '../../utils'
import EdgeAnchor from './EdgeAnchor.vue'
interface EdgeProps {
interface EdgeWrapper {
edge: Edge
markerEndId?: string
edgeUpdaterRadius?: number
}
const props = withDefaults(defineProps<EdgeProps>(), {})
const props = withDefaults(defineProps<EdgeWrapper>(), {})
const store = useStore()

View File

@@ -4,4 +4,4 @@ export { default as SmoothStepEdge } from './SmoothStepEdge.vue'
export { default as StraightEdge } from './StraightEdge.vue'
export { default as EdgeAnchor } from './EdgeAnchor.vue'
export { default as EdgeText } from './EdgeText.vue'
export { default as EdgeWrapper } from './Edge.vue'
export { default as EdgeWrapper } from './EdgeWrapper.vue'

View File

@@ -1,16 +1,16 @@
<script lang="ts" setup>
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
import { useStore } from '../../composables'
import { Node, SnapGrid } from '../../types'
import { GraphNode, SnapGrid } from '../../types'
import { NodeId } from '../../context'
interface NodeProps {
node: Node
interface NodeWrapperProps {
node: GraphNode
selectNodesOnDrag?: boolean
snapGrid?: SnapGrid
}
const props = withDefaults(defineProps<NodeProps>(), {
const props = withDefaults(defineProps<NodeWrapperProps>(), {
selected: false,
selectNodesOnDrag: true,
})

View File

@@ -1,4 +1,4 @@
export { default as DefaultNode } from './DefaultNode.vue'
export { default as InputNode } from './InputNode.vue'
export { default as OutputNode } from './OutputNode.vue'
export { default as NodeWrapper } from './Node.vue'
export { default as NodeWrapper } from './NodeWrapper.vue'

View File

@@ -1,19 +1,19 @@
<script lang="ts" setup>
import { Draggable, DraggableEventListener } from '@braks/revue-draggable'
import { useStore } from '../../composables'
import { Node } from '../../types'
import { getRectOfNodes, isNode } from '../../utils'
import { GraphNode } from '../../types'
import { getRectOfNodes, isGraphNode } from '../../utils'
const store = useStore()
const selectedNodes = store.selectedElements
? store.selectedElements.filter(isNode).map((selectedNode) => {
const selectedNodes: GraphNode[] = store.selectedElements
? store.selectedElements.filter(isGraphNode).map((selectedNode) => {
const matchingNode = store.nodes.find((node) => node.id === selectedNode.id)
return {
...matchingNode,
position: matchingNode?.__vf?.position,
} as Node
} as GraphNode
})
: []

7
src/components/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export * from './Nodes'
export * from './Edges'
export * from './ConnectionLine/ConnectionLine.vue'
export * from './Handle/Handle.vue'
export * from './Loading/LoadingIndicator.vue'
export * from './NodesSelection/NodesSelection.vue'
export * from './UserSelection/UserSelection.vue'

View File

@@ -1,61 +1,44 @@
import {
Connection,
Edge,
Elements,
EmitFunc,
FlowHooks,
FlowTransform,
Node,
OnConnectStartParams,
FlowInstance,
FlowEvents,
FlowStore,
} from '~/types'
import { EmitFunc, FlowHooks, FlowEvents, FlowStore } from '~/types'
// flow event hooks
export const createHooks = (): FlowHooks => {
return {
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
elementsRemove: createEventHook<Elements>(),
nodeDoubleClick: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeClick: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeMouseEnter: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeMouseMove: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeMouseLeave: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeContextMenu: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeDragStart: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeDrag: createEventHook<{ event: MouseEvent; node: Node }>(),
nodeDragStop: createEventHook<{ event: MouseEvent; node: Node }>(),
connect: createEventHook<Connection>(),
connectStart: createEventHook<{
event: MouseEvent
params: OnConnectStartParams
}>(),
connectStop: createEventHook<MouseEvent>(),
connectEnd: createEventHook<MouseEvent>(),
load: createEventHook<FlowInstance>(),
move: createEventHook<FlowTransform | undefined>(),
moveStart: createEventHook<FlowTransform | undefined>(),
moveEnd: createEventHook<FlowTransform | undefined>(),
selectionChange: createEventHook<Elements | null>(),
selectionDragStart: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
selectionDrag: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
selectionDragStop: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
selectionContextMenu: createEventHook<{ event: MouseEvent; nodes: Node[] }>(),
paneScroll: createEventHook<WheelEvent | undefined>(),
paneClick: createEventHook<MouseEvent>(),
paneContextMenu: createEventHook<MouseEvent>(),
edgeUpdate: createEventHook<{ edge: Edge; connection: Connection }>(),
edgeContextMenu: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeMouseEnter: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeMouseMove: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeMouseLeave: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeDoubleClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeClick: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeUpdateStart: createEventHook<{ event: MouseEvent; edge: Edge }>(),
edgeUpdateEnd: createEventHook<MouseEvent>(),
}
}
export const createHooks = (): FlowHooks => ({
elementClick: createEventHook(),
elementsRemove: createEventHook(),
nodeDoubleClick: createEventHook(),
nodeClick: createEventHook(),
nodeMouseEnter: createEventHook(),
nodeMouseMove: createEventHook(),
nodeMouseLeave: createEventHook(),
nodeContextMenu: createEventHook(),
nodeDragStart: createEventHook(),
nodeDrag: createEventHook(),
nodeDragStop: createEventHook(),
connect: createEventHook(),
connectStart: createEventHook(),
connectStop: createEventHook(),
connectEnd: createEventHook(),
load: createEventHook(),
move: createEventHook(),
moveStart: createEventHook(),
moveEnd: createEventHook(),
selectionChange: createEventHook(),
selectionDragStart: createEventHook(),
selectionDrag: createEventHook(),
selectionDragStop: createEventHook(),
selectionContextMenu: createEventHook(),
paneScroll: createEventHook(),
paneClick: createEventHook(),
paneContextMenu: createEventHook(),
edgeUpdate: createEventHook(),
edgeContextMenu: createEventHook(),
edgeMouseEnter: createEventHook(),
edgeMouseMove: createEventHook(),
edgeMouseLeave: createEventHook(),
edgeDoubleClick: createEventHook(),
edgeClick: createEventHook(),
edgeUpdateStart: createEventHook(),
edgeUpdateEnd: createEventHook(),
})
const bind = (emit: EmitFunc, hooks: FlowHooks) => {
for (const [key, value] of Object.entries(hooks)) {

View File

@@ -1,7 +1,7 @@
import { FlowExportObject, FlowOptions } from '~/types'
import { useStateStore, initialState } from '~/store'
import { FlowExportObject, FlowOptions, FlowState } from '~/types'
import { useStateStore } from '~/store'
import { Store } from '~/context'
import { onLoadToObject } from '~/utils'
import { onLoadToObject, initialState } from '~/utils'
let id = 0
export default (options?: Partial<FlowOptions>, key?: string) => {
@@ -15,10 +15,10 @@ export default (options?: Partial<FlowOptions>, key?: string) => {
const storageKey = key ?? `vue-flow-${id++}`
const preloadedState = {
...initial,
...options,
...(options as FlowState),
}
if (withStorage) {
storedState = useStorage(storageKey, {} as FlowExportObject)
storedState = useStorage<FlowExportObject>(storageKey, { elements: [], position: [0, 0], zoom: 0 })
if (storedState.value) {
preloadedState.elements = storedState.value.elements ?? options?.elements ?? []
if (storedState.value.position && storedState.value.zoom)

View File

@@ -1,4 +1,3 @@
// @ts-nocheck
import { zoomIdentity } from 'd3-zoom'
import useStore from './useStore'
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils'

View File

@@ -2,7 +2,7 @@
import { CSSProperties } from 'vue'
import { ConnectionLineType } from '../../types'
import { useStore } from '../../composables'
import Edge from '../../components/Edges/Edge.vue'
import Edge from '../../components/Edges/EdgeWrapper.vue'
import ConnectionLine from '../../components/ConnectionLine/ConnectionLine.vue'
import MarkerDefinitions from './MarkerDefinitions.vue'

View File

@@ -1,9 +1,12 @@
<script lang="ts" setup>
import { useStore } from '../../composables'
import Node from '../../components/Nodes/Node.vue'
import Node from '../../components/Nodes/NodeWrapper.vue'
import { SnapGrid } from '../../types'
interface NodeRendererProps {
selectNodesOnDrag?: boolean
snapToGrid?: boolean
snapGrid?: SnapGrid
}
const props = withDefaults(defineProps<NodeRendererProps>(), {
@@ -13,7 +16,7 @@ const props = withDefaults(defineProps<NodeRendererProps>(), {
const store = useStore()
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
const snapGrid = computed(() => (store.snapToGrid ? store.snapGrid : undefined))
const snapGrid = computed(() => (props.snapToGrid || store.snapToGrid ? props.snapGrid ?? store.snapGrid : undefined))
</script>
<template>
<div class="vue-flow__nodes" :style="{ transform }">

View File

@@ -3,7 +3,7 @@ import { ElementId, FlowElement, KeyCode } from '../../types'
import { useStore, useKeyPress } from '../../composables'
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
import UserSelection from '../../components/UserSelection/UserSelection.vue'
import { getConnectedEdges, isNode } from '../../utils'
import { getConnectedEdges, isGraphNode } from '../../utils'
interface SelectionPaneProps {
selectionKeyCode?: KeyCode
@@ -33,7 +33,7 @@ const selectionKeyPresed = ref(false)
onMounted(() => {
useKeyPress(props.deleteKeyCode, (keyPressed) => {
if (keyPressed && store.selectedElements) {
const selectedNodes = store.selectedElements.filter(isNode)
const selectedNodes = store.selectedElements.filter(isGraphNode)
const connectedEdges = getConnectedEdges(selectedNodes, store.edges)
const elementsToRemove = [...store.selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),

View File

@@ -181,7 +181,6 @@ const transitionName = computed(() => {
<Suspense>
<template #default>
<ZoomPane
v-show="store.isReady"
key="zoom-pane"
:selection-key-code="store.selectionKeyCode"
:zoom-activation-key-code="store.zoomActivationKeyCode"

View File

@@ -20,11 +20,10 @@ export {
isInputDOMNode,
graphPosToZoomedPos,
} from './utils/graph'
export { defaultEdgeTypes, defaultNodeTypes } from './utils/store'
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
export { default as useVueFlow } from './composables/useVueFlow'
export { default as useHandle } from './composables/useHandle'
export { default as useKeyPress } from './composables/useKeyPress'
export { defaultEdgeTypes, defaultNodeTypes } from './store'
export * from './additional-components'
export * from './types'

View File

@@ -1,2 +1 @@
export { default as useStateStore } from './stateStore'
export { defaultEdgeTypes, defaultNodeTypes, initialState } from './utils'

View File

@@ -1,9 +1,20 @@
import { setActivePinia, createPinia, defineStore, StoreDefinition } from 'pinia'
import microDiff from 'microdiff'
import { parseElements, defaultNodeTypes, defaultEdgeTypes, deepUnref, NextElements } from './utils'
import { FlowState, Node, FlowActions, Elements, FlowGetters, Edge } from '~/types'
import { clampPosition, getDimensions, getConnectedEdges, getNodesInside, getRectOfNodes, isNode } from '~/utils'
import { getHandleBounds } from '~/components/Nodes/utils'
import { FlowState, Node, FlowActions, Elements, FlowGetters, Edge, GraphNode, NextElements } from '~/types'
import {
clampPosition,
getDimensions,
getConnectedEdges,
getNodesInside,
getRectOfNodes,
isNode,
parseElements,
defaultNodeTypes,
defaultEdgeTypes,
deepUnref,
getHandleBounds,
isGraphNode,
} from '~/utils'
import parseElementsWorker from '~/workers/parseElements'
const pinia = createPinia()
@@ -81,7 +92,7 @@ export default function flowStore(
} else next = await parseElements(elements, this.nodes, this.edges, this.nodeExtent)
}
if (next) {
this.elements = elements ?? []
this.elements = [...next.nextNodes, ...next.nextEdges]
this.nodes = next?.nextNodes ?? []
this.edges = next?.nextEdges ?? []
}
@@ -102,7 +113,6 @@ export default function flowStore(
this.nodes.splice(i, 1, {
...node,
__vf: {
position: { x: 0, y: 0 },
...node.__vf,
...dimensions,
handleBounds,
@@ -125,21 +135,16 @@ export default function flowStore(
this.nodes.splice(i, 1, {
...node,
__vf: {
width: 0,
height: 0,
...node.__vf,
position: pos,
},
})
},
updateNodePosDiff({ id, diff, isDragging }) {
const update = (node: Node, i: number) => {
const updatedNode: Node = {
const update = (node: GraphNode, i: number) => {
const updatedNode: GraphNode = {
...node,
__vf: {
width: 0,
height: 0,
position: { x: 0, y: 0 },
...node.__vf,
isDragging,
},
@@ -161,8 +166,8 @@ export default function flowStore(
if (!id) {
const selectedNodes = this.nodes.filter((x) => this.selectedElements?.find((sNode) => sNode?.id === x.id))
selectedNodes.forEach((node) => {
const i = this.nodes.map((x) => x.id).indexOf((node as Node).id)
update(node as Node, i)
const i = this.nodes.map((x) => x.id).indexOf(node.id)
update(node, i)
})
} else {
const i = this.nodes.map((x) => x.id).indexOf(id)
@@ -202,7 +207,7 @@ export default function flowStore(
this.selectedElements = nextSelectedElements
},
unsetUserSelection() {
const selectedNodes = this.selectedElements?.filter((node) => node && isNode(node) && node.__vf) as Node[]
const selectedNodes = this.selectedElements?.filter((node) => node && isGraphNode(node) && node.__vf) as GraphNode[]
this.selectionActive = false
this.userSelectionRect.draw = false
@@ -243,8 +248,6 @@ export default function flowStore(
return {
...node,
__vf: {
height: 0,
width: 0,
...node.__vf,
position: node.__vf?.position ? clampPosition(node.__vf.position, nodeExtent) : { x: 0, y: 0 },
},
@@ -272,7 +275,7 @@ export default function flowStore(
},
async addElements(elements: Elements) {
const { nextNodes, nextEdges } = await parseElements(elements, this.nodes, this.edges, this.nodeExtent)
this.elements = [...this.elements, ...elements]
this.elements = [...this.elements, ...nextNodes, ...nextEdges]
this.nodes = [...this.nodes, ...nextNodes]
this.edges = [...this.edges, ...nextEdges]
},

View File

@@ -1,26 +0,0 @@
import { Dimensions, Elements, XYPosition } from './types'
import { NodeDiffUpdate, NodeDimensionUpdate, NodeExtent, NodePosUpdate, TranslateExtent } from './node'
import { InitD3ZoomPayload } from './panel'
import { SetConnectionId } from './connection'
export interface FlowActions {
setElements: (elements: Elements) => void
updateNodeDimensions: (update: NodeDimensionUpdate) => void
updateNodePos: (payload: NodePosUpdate) => void
updateNodePosDiff: (payload: NodeDiffUpdate) => void
setUserSelection: (mousePos: XYPosition) => void
updateUserSelection: (mousePos: XYPosition) => void
unsetUserSelection: () => void
addSelectedElements: (elements: Elements) => void
initD3Zoom: (payload: InitD3ZoomPayload) => void
setMinZoom: (zoom: number) => void
setMaxZoom: (zoom: number) => void
setTranslateExtent: (translateExtent: TranslateExtent) => void
setNodeExtent: (nodeExtent: NodeExtent) => void
resetSelectedElements: () => void
unsetNodesSelection: () => void
updateSize: (size: Dimensions) => void
setConnectionNodeId: (payload: SetConnectionId) => void
setInteractive: (isInteractive: boolean) => void
addElements: (elements: Elements) => void
}

View File

@@ -1,4 +1,4 @@
import { ElementId, Position } from './types'
import { ElementId, Position } from './flow'
import { HandleType } from './handle'
export enum ConnectionLineType {

View File

@@ -1,5 +1,5 @@
import { Component, CSSProperties, DefineComponent } from 'vue'
import { ArrowHeadType, ElementId, Position } from './types'
import { ArrowHeadType, ElementId, Position } from './flow'
export interface Edge<T = any> {
id: ElementId

View File

@@ -1,15 +1,18 @@
import { CSSProperties } from 'vue'
import { Edge, EdgeTypes } from './edge'
import { Node, NodeExtent, NodeTypes, TranslateExtent } from './node'
import { NodeExtent, GraphNode, NodeTypes, TranslateExtent, Node } from './node'
import { ConnectionLineType, ConnectionMode } from './connection'
import { KeyCode, PanOnScrollMode } from './panel'
import { KeyCode, PanOnScrollMode } from './zoom'
import { FlowStore } from './store'
export type ElementId = string
export type FlowElement<T = any> = Node<T> | Edge<T>
export type Elements<T = any> = FlowElement<T>[]
export type FlowElement<T = any> = GraphNode<T> | Edge<T>
export type FlowElements<T = any> = FlowElement<T>[]
export type Elements<T = any> = (Node<T> | Edge<T>)[]
export type NextElements = {
nextNodes: GraphNode[]
nextEdges: Edge[]
}
export type Transform = [number, number, number]
@@ -63,7 +66,7 @@ export type FitViewParams = {
}
export type FlowExportObject<T = any> = {
elements: Elements<T>
elements: FlowElements<T>
position: [number, number]
zoom: number
}
@@ -98,14 +101,13 @@ export type FlowInstance<T = any> = {
zoomTo: (zoomLevel: number) => void
fitView: FitViewFunc
project: ProjectFunc
getElements: () => Elements<T>
getElements: () => FlowElements<T>
setTransform: (transform: FlowTransform) => void
toObject: ToObjectFunc<T>
}
export interface FlowOptions {
id?: string
store?: FlowStore
elements?: Elements
nodeTypes?: NodeTypes
edgeTypes?: EdgeTypes
@@ -142,4 +144,5 @@ export interface FlowOptions {
storageKey?: string
loading?: Loading
worker?: boolean
store?: FlowStore
}

View File

@@ -1,5 +1,5 @@
import { Dimensions, ElementId, Position, XYPosition } from './types'
import { Connection } from '~/types/connection'
import { Dimensions, ElementId, Position, XYPosition } from './flow'
import { Connection } from './connection'
export type HandleType = 'source' | 'target'

View File

@@ -1,5 +1,5 @@
import { EventHook } from '@vueuse/core'
import { Elements, FlowTransform, FlowInstance } from './types'
import { FlowTransform, FlowInstance, FlowElements } from './flow'
import { Edge } from './edge'
import { Node } from './node'
import { Connection, OnConnectStartParams } from './connection'
@@ -8,7 +8,7 @@ export type FlowHook<T = any> = EventHook<T>
export interface FlowEvents {
elementClick: { event: MouseEvent; element: Node | Edge }
elementsRemove: Elements
elementsRemove: FlowElements
nodeDoubleClick: { event: MouseEvent; node: Node }
nodeClick: { event: MouseEvent; node: Node }
nodeMouseEnter: { event: MouseEvent; node: Node }
@@ -29,7 +29,7 @@ export interface FlowEvents {
move: FlowTransform | undefined
moveStart: FlowTransform | undefined
moveEnd: FlowTransform | undefined
selectionChange: Elements | null
selectionChange: FlowElements | null
selectionDragStart: { event: MouseEvent; nodes: Node[] }
selectionDrag: { event: MouseEvent; nodes: Node[] }
selectionDragStop: { event: MouseEvent; nodes: Node[] }

View File

@@ -1,9 +1,8 @@
export * from './types'
export * from './actions'
export * from './flow'
export * from './connection'
export * from './edge'
export * from './handle'
export * from './node'
export * from './panel'
export * from './zoom'
export * from './store'
export * from './hooks'

View File

@@ -1,21 +1,22 @@
import { Component, CSSProperties, DefineComponent } from 'vue'
import { XYPosition, ElementId, Position } from './types'
import { HandleElement } from '~/types/handle'
import { XYPosition, ElementId, Position } from './flow'
import { HandleElement } from './handle'
interface VFInternals {
position: XYPosition
isDragging?: boolean
width: number
height: number
handleBounds: {
source: HandleElement[] | null
target: HandleElement[] | null
}
}
export interface Node<T = any> {
id: ElementId
position: XYPosition
type?: string
__vf?: {
position: XYPosition
isDragging?: boolean
width: number
height: number
handleBounds?: {
source?: HandleElement[] | null
target?: HandleElement[] | null
}
}
class?: string
style?: CSSProperties
data?: T
@@ -27,6 +28,10 @@ export interface Node<T = any> {
connectable?: boolean
}
export interface GraphNode<T = any> extends Node<T> {
__vf: VFInternals
}
export type NodePosUpdate = {
id: ElementId
pos: XYPosition

View File

@@ -3,6 +3,7 @@ import {
Dimensions,
ElementId,
Elements,
FlowElements,
FlowInstance,
FlowOptions,
Rect,
@@ -10,20 +11,26 @@ import {
SnapGrid,
Transform,
XYPosition,
} from './types'
} from './flow'
import { HandleType } from './handle'
import { ConnectionMode, OnConnectEndFunc, OnConnectFunc, OnConnectStartFunc, OnConnectStopFunc } from './connection'
import {
ConnectionMode,
OnConnectEndFunc,
OnConnectFunc,
OnConnectStartFunc,
OnConnectStopFunc,
SetConnectionId,
} from './connection'
import { Edge, EdgeComponent } from './edge'
import { Node, NodeComponent, NodeExtent, TranslateExtent } from './node'
import { FlowActions } from './actions'
import { D3Selection, D3Zoom, D3ZoomHandler } from './panel'
import { NodeComponent, NodeDiffUpdate, NodeDimensionUpdate, NodeExtent, GraphNode, NodePosUpdate, TranslateExtent } from './node'
import { D3Selection, D3Zoom, D3ZoomHandler, InitD3ZoomPayload } from './zoom'
import { FlowHooks } from './hooks'
export interface FlowState extends FlowOptions {
elements: Elements
nodes: Node[]
elements: FlowElements
nodes: GraphNode[]
edges: Edge[]
selectedElements?: Elements
selectedElements?: FlowElements
selectedNodesBbox: Rect
d3Zoom?: D3Zoom
@@ -66,10 +73,32 @@ export interface FlowState extends FlowOptions {
vueFlowVersion: string
}
export interface FlowActions {
setElements: (elements: Elements) => Promise<void>
updateNodeDimensions: (update: NodeDimensionUpdate) => void
updateNodePos: (payload: NodePosUpdate) => void
updateNodePosDiff: (payload: NodeDiffUpdate) => void
setUserSelection: (mousePos: XYPosition) => void
updateUserSelection: (mousePos: XYPosition) => void
unsetUserSelection: () => void
addSelectedElements: (elements: FlowElements) => void
initD3Zoom: (payload: InitD3ZoomPayload) => void
setMinZoom: (zoom: number) => void
setMaxZoom: (zoom: number) => void
setTranslateExtent: (translateExtent: TranslateExtent) => void
setNodeExtent: (nodeExtent: NodeExtent) => void
resetSelectedElements: () => void
unsetNodesSelection: () => void
updateSize: (size: Dimensions) => void
setConnectionNodeId: (payload: SetConnectionId) => void
setInteractive: (isInteractive: boolean) => void
addElements: (elements: Elements) => Promise<void>
}
export interface FlowGetters {
getEdgeTypes: () => Record<string, EdgeComponent>
getNodeTypes: () => Record<string, NodeComponent>
getNodes: () => Node[]
getNodes: () => GraphNode[]
getEdges: () => Edge[]
}

View File

@@ -1,5 +1,5 @@
import { Selection, ZoomBehavior } from 'd3'
import { FitViewFunc, FlowTransform, Rect, XYPosition } from './types'
import { FitViewFunc, FlowTransform, Rect, XYPosition } from './flow'
export type D3Zoom = ZoomBehavior<HTMLDivElement, unknown>
export type D3Selection = Selection<HTMLDivElement, unknown, any, any>

View File

@@ -1,11 +1,11 @@
import { Edge, EdgePositions, ElementId, HandleElement, Node, Position, Transform, XYPosition } from '../../types'
import { rectToBox } from '~/utils'
import { rectToBox } from './graph'
import { Edge, EdgePositions, ElementId, GraphNode, HandleElement, Position, Transform, XYPosition } from '~/types'
export function getHandlePosition(position: Position, node: Node, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__vf?.position?.x
const y = (handle?.y || 0) + node.__vf?.position?.y
const width = handle?.width || node.__vf?.width
const height = handle?.height || node.__vf?.height
export function getHandlePosition(position: Position, node: GraphNode, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + node.__vf.position.x
const y = (handle?.y || 0) + node.__vf.position.y
const width = handle?.width || node.__vf.width
const height = handle?.height || node.__vf.height
switch (position) {
case Position.Top:
@@ -47,10 +47,10 @@ export function getHandle(bounds: HandleElement[], handleId: ElementId | null):
}
export const getEdgePositions = (
sourceNode: Node,
sourceNode: GraphNode,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNode: Node,
targetNode: GraphNode,
targetHandle: HandleElement | unknown,
targetPosition: Position,
): EdgePositions => {
@@ -104,11 +104,11 @@ export function isEdgeVisible({ sourcePos, targetPos, width, height, transform }
}
type SourceTargetNode = {
sourceNode: Node
targetNode: Node
sourceNode: GraphNode
targetNode: GraphNode
}
export const getSourceTargetNodes = (edge: Edge, nodes: Node[]): SourceTargetNode => {
export const getSourceTargetNodes = (edge: Edge, nodes: GraphNode[]): SourceTargetNode => {
return nodes.reduce(
(res, node) => {
if (node.id === edge.source) {

View File

@@ -12,12 +12,20 @@ import {
NodeExtent,
Dimensions,
FlowStore,
GraphNode,
FlowElements,
FlowElement,
} from '~/types'
import { useWindow } from '~/composables'
const isHTMLElement = (el: EventTarget): el is HTMLElement => ('nodeName' || 'hasAttribute') in el
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
const target = e.target as HTMLElement
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
const target = e.target
if (target && isHTMLElement(target)) {
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
}
return false
}
export const getDimensions = (node: HTMLElement): Dimensions => ({
@@ -40,40 +48,30 @@ export const getHostForElement = (element: HTMLElement): Document => {
else return window.document
}
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
export const isEdge = (element: Node | FlowElement | Connection): element is Edge =>
'id' in element && 'source' in element && 'target' in element
export const isNode = (element: Node | Connection | Edge): element is Node =>
export const isNode = (element: Node | FlowElement | Connection): element is Node =>
'id' in element && !('source' in element) && !('target' in element)
export const getOutgoers = (node: Node, elements: Elements): Node[] => {
if (!isNode(node)) {
return []
}
export const isGraphNode = (element: Node | FlowElement | Connection): element is GraphNode =>
isNode(element) && '__vf' in element
const outgoerIds = elements.filter((e) => isEdge(e) && e.source === node.id).map((e) => (e as Edge).target)
return elements.filter((e) => outgoerIds.includes(e.id)) as Node[]
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])
return elements.filter((e) => ids.includes(e.id))
}
export const getOutgoers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'target')
export const getIncomers = (node: Node, elements: Elements): Node[] => {
if (!isNode(node)) {
return []
}
export const getIncomers = (node: GraphNode, elements: Elements) => getConnectedElements(node, elements, 'source')
const incomersIds = elements.filter((e) => isEdge(e) && e.target === node.id).map((e) => (e as Edge).source)
return elements.filter((e) => incomersIds.includes(e.id)) as Node[]
}
export const removeElements = (elementsToRemove: Elements, elements: Elements): Elements => {
export const removeElements = (elementsToRemove: Elements, elements: Elements) => {
const nodeIdsToRemove = elementsToRemove.map((n) => n.id)
return elements.filter((element) => {
const edgeElement = element as Edge
return !(
nodeIdsToRemove.includes(element.id) ||
nodeIdsToRemove.includes(edgeElement.target) ||
nodeIdsToRemove.includes(edgeElement.source)
)
const { target, source } = isEdge(element) ? element : { target: '', source: '' }
return !(nodeIdsToRemove.includes(element.id) || nodeIdsToRemove.includes(target) || nodeIdsToRemove.includes(source))
})
}
@@ -91,13 +89,13 @@ const connectionExists = (edge: Edge, elements: Elements) => {
)
}
export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elements => {
export const addEdge = (edgeParams: Edge | Connection, elements: Elements) => {
if (!edgeParams.source || !edgeParams.target) {
console.log("Can't create edge. An edge needs a source and a target.")
return elements
}
let edge: Edge
let edge
if (isEdge(edgeParams)) {
edge = { ...edgeParams }
} else {
@@ -114,13 +112,13 @@ export const addEdge = (edgeParams: Edge | Connection, elements: Elements): Elem
return elements.concat(edge)
}
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements): Elements => {
export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: Elements) => {
if (!newConnection.source || !newConnection.target) {
console.warn("Can't create new edge. An edge needs a source and a target.")
return elements
}
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id) as Edge
const foundEdge = elements.find((e) => isEdge(e) && e.id === oldEdge.id)
if (!foundEdge) {
console.warn(`The old edge with id=${oldEdge.id} does not exist.`)
@@ -128,14 +126,14 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E
}
// Remove old edge and create the new edge with parameters of old edge.
const edge = {
const edge: Edge = {
...oldEdge,
id: getEdgeId(newConnection),
source: newConnection.source,
target: newConnection.target,
sourceHandle: newConnection.sourceHandle,
targetHandle: newConnection.targetHandle,
} as Edge
}
return elements.filter((e) => e.id !== oldEdge.id).concat(edge)
}
@@ -145,7 +143,7 @@ export const pointToRendererPoint = (
[tx, ty, tScale]: Transform,
snapToGrid: boolean,
[snapX, snapY]: [number, number],
): XYPosition => {
) => {
const position: XYPosition = {
x: (x - tx) / tScale,
y: (y - ty) / tScale,
@@ -161,12 +159,10 @@ export const pointToRendererPoint = (
return position
}
export const onLoadProject =
(currentStore: FlowStore) =>
(position: XYPosition): XYPosition =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
export const onLoadProject = (currentStore: FlowStore) => (position: XYPosition) =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => ({
export const parseNode = (node: Node, nodeExtent: NodeExtent): GraphNode => ({
...node,
id: node.id.toString(),
type: node.type || 'default',
@@ -174,7 +170,10 @@ export const parseNode = (node: Node, nodeExtent: NodeExtent): Node => ({
position: clampPosition(node.position, nodeExtent),
width: 0,
height: 0,
handleBounds: {},
handleBounds: {
source: null,
target: null,
},
isDragging: false,
},
})
@@ -210,10 +209,9 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
height: y2 - y,
})
export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)))
export const getBoundsofRects = (rect1: Rect, rect2: Rect) => boxToRect(getBoundsOfBoxes(rectToBox(rect1), rectToBox(rect2)))
export const getRectOfNodes = (nodes: Node[]): Rect => {
export const getRectOfNodes = (nodes: GraphNode[]) => {
const box = nodes.reduce(
(currBox, { __vf: { position = { x: 0, y: 0 }, width = 0, height = 0 } = {} }) =>
getBoundsOfBoxes(
@@ -235,7 +233,7 @@ export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Tran
y: y * tScale + ty,
})
export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false): Node[] => {
export const getNodesInside = (nodes: GraphNode[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false) => {
const rBox = rectToBox({
x: (rect.x - tx) / tScale,
y: (rect.y - ty) / tScale,
@@ -266,13 +264,12 @@ export const getNodesInside = (nodes: Node[], rect: Rect, [tx, ty, tScale]: Tran
})
}
export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
export const getConnectedEdges = (nodes: GraphNode[], edges: Edge[]) => {
const nodeIds = nodes.map((node) => node.id)
return edges.filter((edge) => nodeIds.includes(edge.source) || nodeIds.includes(edge.target))
}
export const onLoadGetElements = (currentStore: FlowStore) => (): Elements => currentStore.elements
export const onLoadGetElements = (currentStore: FlowStore) => (): FlowElements => currentStore.elements
export const onLoadToObject = (currentStore: FlowStore) => (): FlowExportObject => {
// we have to stringify/parse so objects containing refs (like nodes and edges) can potentially be saved in a storage

View File

@@ -1 +1,4 @@
export * from './edge'
export * from './graph'
export * from './node'
export * from './store'

View File

@@ -1,5 +1,5 @@
import { HandleElement, Position } from '~/types'
import { getDimensions } from '~/utils'
import { getDimensions } from '~/utils/graph'
export const getHandleBoundsByHandleType = (
selector: string,
@@ -19,7 +19,7 @@ export const getHandleBoundsByHandleType = (
const bounds = handle.getBoundingClientRect()
const dimensions = getDimensions(handle)
const handleId = handle.getAttribute('data-handleid')
const handlePosition = handle.getAttribute('data-handlepos') as unknown as Position
const handlePosition = handle.getAttribute('data-handlepos') as Position
return {
id: handleId,

View File

@@ -1,15 +1,20 @@
import { Component } from 'vue'
import { ConnectionMode, Edge, EdgeProps, Elements, FlowState, Node, NodeExtent, NodeProps, PanOnScrollMode } from '~/types'
import { isEdge, isNode, parseEdge, parseNode } from '~/utils'
import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
import { isEdge, isNode, parseEdge, parseNode } from './graph'
import {
ConnectionMode,
Edge,
EdgeProps,
Elements,
FlowState,
NextElements,
NodeExtent,
GraphNode,
NodeProps,
PanOnScrollMode,
} from '~/types'
import { DefaultNode, InputNode, OutputNode, BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components'
import { createHooks } from '~/composables'
export type NextElements = {
nextNodes: Node[]
nextEdges: Edge[]
}
export const defaultNodeTypes: Record<string, Component<NodeProps>> = {
input: InputNode,
default: DefaultNode,
@@ -94,7 +99,7 @@ export const initialState = (): FlowState => ({
vueFlowVersion: typeof __VUE_FLOW_VERSION__ !== 'undefined' ? __VUE_FLOW_VERSION__ : '-',
})
export const parseElements = async (elements: Elements, nodes: Node[], edges: Edge[], nodeExtent: NodeExtent) =>
export const parseElements = async (elements: Elements, nodes: GraphNode[], edges: Edge[], nodeExtent: NodeExtent) =>
new Promise<NextElements>((resolve) => {
const nextElements: NextElements = {
nextNodes: [],
@@ -105,20 +110,20 @@ export const parseElements = async (elements: Elements, nodes: Node[], edges: Ed
const storeNode = nodes[nodes.map((x) => x.id).indexOf(element.id)]
if (storeNode) {
const updatedNode: Node = {
const updatedNode: GraphNode = {
...storeNode,
...element,
}
if (!updatedNode.__vf) updatedNode.__vf = {}
if (!updatedNode.__vf) updatedNode.__vf = {} as any
if (storeNode.position.x !== element.position.x || storeNode.position.y !== element.position.y) {
updatedNode.__vf.position = element.position
updatedNode.__vf!.position = element.position
}
if (typeof element.type !== 'undefined' && element.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__vf.width = undefined
updatedNode.__vf!.width = 0
}
nextElements.nextNodes.push(updatedNode)

View File

@@ -1,7 +1,7 @@
import { Connection, Edge, Elements, Node, NodeExtent, XYPosition } from '~/types'
import { Connection, Edge, Elements, Node, NodeExtent, GraphNode, XYPosition, NextElements, FlowElement } from '~/types'
export default () =>
useWebWorkerFn((elements: Elements, nodes: Node[], edges: Edge[], nodeExtent: NodeExtent) => {
useWebWorkerFn((elements: Elements, nodes: GraphNode[], edges: Edge[], nodeExtent: NodeExtent): NextElements => {
const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
@@ -9,14 +9,14 @@ export default () =>
y: clamp(position.y, extent[0][1], extent[1][1]),
})
const parseNode = (node: Node, nodeExtent: NodeExtent): Node =>
Object.assign(node, {
const parseNode = (node: Node, nodeExtent: NodeExtent): GraphNode =>
Object.assign(node as GraphNode, {
id: node.id.toString(),
type: node.type || 'default',
__vf: {
position: clampPosition(node.position, nodeExtent),
width: undefined,
height: undefined,
width: 0,
height: 0,
handleBounds: {},
isDragging: false,
},
@@ -32,13 +32,13 @@ export default () =>
type: edge.type || 'default',
})
const isEdge = (element: Node | Connection | Edge): element is Edge =>
const isEdge = (element: Node | FlowElement | Connection): element is Edge =>
'id' in element && 'source' in element && 'target' in element
const isNode = (element: Node | Connection | Edge): element is Node =>
const isNode = (element: Node | FlowElement | Connection): element is Node =>
'id' in element && !('source' in element) && !('target' in element)
const nextElements: any = {
const nextElements: NextElements = {
nextNodes: [],
nextEdges: [],
}
@@ -48,17 +48,17 @@ export default () =>
const storeNode = nodes[nodes.map((x) => x.id).indexOf(element.id)]
if (storeNode) {
const updatedNode: Node = Object.assign(storeNode, element)
if (!updatedNode.__vf) updatedNode.__vf = {}
const updatedNode = Object.assign(storeNode, element)
if (!updatedNode.__vf) updatedNode.__vf = {} as any
if (storeNode.position.x !== element.position.x || storeNode.position.y !== element.position.y) {
updatedNode.__vf.position = element.position
updatedNode.__vf!.position = element.position
}
if (typeof element.type !== 'undefined' && element.type !== storeNode.type) {
// we reset the elements dimensions here in order to force a re-calculation of the bounds.
// When the type of a node changes it is possible that the number or positions of handles changes too.
updatedNode.__vf.width = undefined
updatedNode.__vf!.width = 0
}
nextElements.nextNodes.push(updatedNode)