refactor(types): Remove internal __vf field

* fields are merged into the GraphNode / Node interface
* Remove passing props from VueFlow container, use injected state

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-12-11 09:45:54 +01:00
parent 3f0b0dbd70
commit f28e10b05a
17 changed files with 219 additions and 478 deletions

View File

@@ -8,8 +8,8 @@ store.hooks.nodeDragStop.on(({ node }) => {
const nodes = getNodesInside(
store.getNodes,
{
height: props.__vf.height,
width: props.__vf.width,
height: props.height,
width: props.width,
x: props.position.x,
y: props.position.y,
},

View File

@@ -24,7 +24,7 @@ const onLoad = (flowInstance: FlowInstance) => {
console.log(flowInstance.getElements())
}
const initialElements: Elements = getElements(10, 10)
const initialElements: Elements = getElements(50, 20)
const elements = ref(initialElements)
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value))

View File

@@ -93,10 +93,10 @@ export default {
>
<template v-for="node of store.getNodes" :key="`mini-map-node-${node.id}`">
<slot
:x="node.position.x"
:y="node.position.y"
:width="node.__vf.width"
:height="node.__vf.height"
:x="node.computedPosition.x"
:y="node.computedPosition.y"
:width="node.width"
:height="node.height"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"
@@ -106,10 +106,10 @@ export default {
:shape-rendering="shapeRendering"
>
<MiniMapNode
:x="node.position.x"
:y="node.position.y"
:width="node.__vf.width"
:height="node.__vf.height"
:x="node.computedPosition.x"
:y="node.computedPosition.y"
:width="node.width"
:height="node.height"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"

View File

@@ -1,56 +1,31 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import { getBezierPath, getSmoothStepPath } from '../Edges/utils'
import {
ConnectionLineType,
HandleElement,
Position,
HandleType,
XYPosition,
ConnectionMode,
Transform,
GraphNode,
} from '../../types'
import { ConnectionLineType, GraphNode, HandleElement, Position } from '../../types'
import { useStore } from '../../composables'
interface ConnectionLineProps {
sourceNode: GraphNode
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
connectionHandleId?: string
connectionNodeId?: string
connectionHandleType?: HandleType
connectionPosition?: XYPosition
connectionMode: ConnectionMode
nodes: GraphNode[]
transform: Transform
}
const props = withDefaults(defineProps<ConnectionLineProps>(), {
connectionLineType: ConnectionLineType.Bezier,
connectionLineStyle: () => ({}),
connectionPosition: () => ({ x: 0, y: 0 }),
})
const props = defineProps<ConnectionLineProps>()
const store = useStore()
const sourceHandle =
props.connectionHandleId && props.connectionHandleType
? props.sourceNode.__vf.handleBounds[props.connectionHandleType]?.find(
(d: HandleElement) => d.id === props.connectionHandleId,
)
: props.connectionHandleType && props.sourceNode.__vf.handleBounds[props.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
store.connectionHandleId && store.connectionHandleType
? props.sourceNode.handleBounds[store.connectionHandleType]?.find((d: HandleElement) => d.id === store.connectionHandleId)
: store.connectionHandleType && props.sourceNode.handleBounds[store.connectionHandleType ?? 'source']?.[0]
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : (props.sourceNode.width as number) / 2
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : props.sourceNode.height
const sourceX = props.sourceNode.position.x + sourceHandleX
const sourceY = props.sourceNode.position.y + sourceHandleY
const isRightOrLeft = sourceHandle?.position === Position.Left || sourceHandle?.position === Position.Right
const targetPosition = isRightOrLeft ? Position.Left : Position.Top
const targetX = computed(() => (props.connectionPosition.x - props.transform[0]) / props.transform[2])
const targetY = computed(() => (props.connectionPosition.y - props.transform[1]) / props.transform[2])
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},${sourceY} ${targetX.value},${targetY.value}`
switch (props.connectionLineType) {
switch (store.connectionLineType) {
case ConnectionLineType.Bezier:
path = getBezierPath({
sourceX,
@@ -101,14 +76,14 @@ export default {
targetX,
targetY,
targetPosition,
connectionLineType: props.connectionLineType,
connectionLineStyle: props.connectionLineStyle,
nodes: props.nodes,
connectionLineType: store.connectionLineType,
connectionLineStyle: store.connectionLineStyle,
nodes: store.getNodes,
sourceNode: props.sourceNode,
sourceHandle,
}"
>
<path :d="dAttr" class="vue-flow__connection-path" :style="props.connectionLineStyle" />
<path :d="dAttr" class="vue-flow__connection-path" :style="store.connectionLineStyle" />
</slot>
</g>
</template>

View File

@@ -61,15 +61,15 @@ const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
// when connection type is loose we can define all handles as sources
const targetNodeHandles = computed(() =>
store.connectionMode === ConnectionMode.Strict
? props.edge.targetNode.__vf.handleBounds.target
: props.edge.targetNode.__vf.handleBounds.target ?? props.edge.targetNode.__vf.handleBounds.source,
? props.edge.targetNode.handleBounds.target
: props.edge.targetNode.handleBounds.target ?? props.edge.targetNode.handleBounds.source,
)
const sourceHandle = controlledComputed(
() => props.edge,
() => {
if (props.edge.sourceNode && props.edge.sourceNode.__vf.handleBounds.source)
return getHandle(props.edge.sourceNode.__vf.handleBounds.source, props.edge.sourceHandle)
if (props.edge.sourceNode && props.edge.sourceNode.handleBounds.source)
return getHandle(props.edge.sourceNode.handleBounds.source, props.edge.sourceHandle)
else return undefined
},
)

View File

@@ -84,24 +84,24 @@ const updateNodePosition = (node: GraphNode, { x, y }: XYPosition) => {
const position = {
x: node.position.x + x,
y: node.position.y + y,
z: node.__vf.position.z,
z: node.computedPosition.z,
}
let extent = store.nodeExtent
if (node.extent === 'parent' && node.parentNode && node.__vf.width && node.__vf.height) {
const parent = node.__vf.parentNode
if (node.extent === 'parent' && node.parentNode && node.width && node.height) {
const parent = node.parentNode
extent =
parent.__vf.width && parent.__vf.height
parent.width && parent.height
? [
[0, 0],
[parent.__vf.width - node.__vf.width, parent.__vf.height - node.__vf.height],
[parent.width - node.width, parent.height - node.height],
]
: extent
}
node.position = clampPosition(position, extent)
if (node.parentNode || node.__vf.isParent) {
node.__vf.position = calculateXYZPosition(node, { z: position.z, ...node.position })
} else node.__vf.position = { z: position.z, ...node.position }
if (node.parentNode || node.isParent) {
node.computedPosition = calculateXYZPosition(node, { z: position.z, ...node.position })
} else node.computedPosition = { ...node.position, z: position.z }
}
const updateNodeDimensions = ({ nodeElement, forceUpdate }: NodeDimensionUpdate) => {
@@ -110,42 +110,36 @@ const updateNodeDimensions = ({ nodeElement, forceUpdate }: NodeDimensionUpdate)
const doUpdate =
dimensions.width &&
dimensions.height &&
(node.value.__vf.width !== dimensions.width || node.value.__vf.height !== dimensions.height || forceUpdate)
(node.value.width !== dimensions.width || node.value.height !== dimensions.height || forceUpdate)
if (doUpdate) {
const handleBounds = getHandleBounds(nodeElement, store.transform[2], store.id)
node.value.__vf = {
...node.value.__vf,
node.value = {
...node.value,
...dimensions,
handleBounds,
}
}
}
tryOnMounted(() => {
watch([() => node.value.__vf.width, () => node.value.__vf.height], () => {
onRenderTriggered(() => console.log(node.value.id))
onMounted(() => {
nextTick(() => {
updateNodeDimensions({
id: node.value.id,
id: props.node.id,
nodeElement: nodeElement.value,
forceUpdate: true,
})
})
useResizeObserver(nodeElement, (entries) =>
entries.forEach((entry) => {
updateNodeDimensions({
id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
})
}),
)
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () => {
updateNodeDimensions({
id: node.value.id,
nodeElement: nodeElement.value,
forceUpdate: true,
})
useResizeObserver(nodeElement, (entries) =>
entries.forEach((entry) => {
updateNodeDimensions({
id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
})
}),
)
})
})
</script>
@@ -180,10 +174,10 @@ export default {
node.class,
]"
:style="{
zIndex: node.dragging || selected ? 1000 : node.__vf.position.z,
transform: `translate(${node.__vf.position.x}px,${node.__vf.position.y}px)`,
zIndex: node.dragging || selected ? 1000 : node.computedPosition.z,
transform: `translate(${node.computedPosition.x}px,${node.computedPosition.y}px)`,
pointerEvents: props.selectable || props.draggable ? 'all' : 'none',
opacity: node.__vf.width !== null && node.__vf.height !== null ? 1 : 0,
opacity: node.width !== null && node.height !== null ? 1 : 0,
...node.style,
}"
:data-id="node.id"
@@ -200,7 +194,6 @@ export default {
data: node.data,
type: node.type,
position: node.position,
__vf: node.__vf,
selected,
connectable: props.connectable,
sourcePosition: node.sourcePosition,
@@ -217,7 +210,6 @@ export default {
data: node.data,
type: node.type,
position: node.position,
__vf: node.__vf,
selected,
connectable: props.connectable,
sourcePosition: node.sourcePosition,

View File

@@ -1,16 +1,9 @@
<script lang="ts" setup>
import { Draggable, DraggableEventListener } from '@braks/revue-draggable'
import { useStore } from '../../composables'
import { FlowElements } from '../../types'
import { getRectOfNodes } from '../../utils'
interface NodesSelectionProps {
selectedElements: FlowElements
}
const store = useStore()
const props = defineProps<NodesSelectionProps>()
const selectedNodesBBox = getRectOfNodes(store.getSelectedNodes)
const innerStyle = {
width: `${selectedNodesBBox.width}px`,
@@ -27,12 +20,12 @@ const onDrag: DraggableEventListener = ({ event, data }) => {
x: node.position.x + data.deltaX,
y: node.position.y + data.deltaY,
}
node.__vf.isDragging = true
node.dragging = true
})
}
const onStop: DraggableEventListener = ({ event }) => {
store.hooks.selectionDragStop.trigger({ event, nodes: store.getSelectedNodes })
store.getSelectedNodes.forEach((node) => (node.__vf.isDragging = false))
store.getSelectedNodes.forEach((node) => (node.dragging = false))
}
const onContextMenu = (event: MouseEvent) => store.hooks.selectionContextMenu.trigger({ event, nodes: store.getSelectedNodes })

View File

@@ -1,75 +1,37 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import {
ConnectionLineType,
ConnectionMode,
Dimensions,
EdgeComponent,
GraphEdge,
GraphNode,
HandleType,
Transform,
XYPosition,
} from '../../types'
import EdgeWrapper from '../../components/Edges/EdgeWrapper.vue'
import ConnectionLine from '../../components/ConnectionLine/ConnectionLine.vue'
import { useStore } from '../../composables'
import MarkerDefinitions from './MarkerDefinitions.vue'
interface EdgeRendererProps {
edges: GraphEdge[]
nodes: GraphNode[]
edgeTypes: Record<string, EdgeComponent>
transform: Transform
dimensions: Dimensions
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
arrowHeadColor?: string
markerEndId?: string
elementsSelectable?: boolean
edgeUpdaterRadius?: number
connectionNodeId?: string
connectionHandleType?: HandleType
connectionHandleId?: string
connectionPosition?: XYPosition
connectionMode: ConnectionMode
nodesConnectable?: boolean
edgesUpdatable?: boolean
}
const props = withDefaults(defineProps<EdgeRendererProps>(), {
arrowHeadColor: '#b1b1b7',
connectionLineType: ConnectionLineType.Bezier,
edgeUpdaterRadius: 10,
handleEdgeUpdate: false,
})
const store = useStore()
const getType = (type?: string) => {
const t = type ?? 'default'
let edgeType = props.edgeTypes[t]
let edgeType = store.getEdgeTypes[t]
if (!edgeType) {
edgeType = props.edgeTypes.default
edgeType = store.getEdgeTypes.default
console.warn(`Edge type "${type}" not found. Using fallback type "default".`)
}
return edgeType
}
const sourceNode = controlledComputed(
() => props.connectionNodeId,
() => store.connectionNodeId,
() => {
if (props.connectionNodeId) return props.nodes[props.nodes.map((n) => n.id).indexOf(props.connectionNodeId)]
if (store.connectionNodeId) return store.getNodes[store.getNodes.map((n) => n.id).indexOf(store.connectionNodeId)]
},
)
const connectionLineVisible = controlledComputed(
() => props.connectionNodeId,
() => store.connectionNodeId,
() =>
!!(
sourceNode.value &&
(typeof sourceNode.value.connectable === 'undefined' ? props.nodesConnectable : sourceNode.value.connectable) &&
props.connectionNodeId &&
props.connectionHandleType
(typeof sourceNode.value.connectable === 'undefined' ? store.nodesConnectable : sourceNode.value.connectable) &&
store.connectionNodeId &&
store.connectionHandleType
),
)
const transform = computed(() => `translate(${props.transform[0]},${props.transform[1]}) scale(${props.transform[2]})`)
const transform = computed(() => `translate(${store.transform[0]},${store.transform[1]}) scale(${store.transform[2]})`)
</script>
<script lang="ts">
export default {
@@ -77,37 +39,25 @@ export default {
}
</script>
<template>
<svg :width="props.dimensions.width" :height="props.dimensions.height" class="vue-flow__edges">
<MarkerDefinitions :color="props.arrowHeadColor" />
<svg :width="store.dimensions.width" :height="store.dimensions.height" class="vue-flow__edges">
<MarkerDefinitions :color="store.arrowHeadColor" />
<g :transform="transform">
<EdgeWrapper
v-for="(edge, i) of props.edges"
v-for="(edge, i) of store.getEdges"
:key="`${edge.id}-${i}`"
:edge="edge"
:component="getType(edge.type)"
:selectable="props.elementsSelectable"
:updatable="typeof edge.updatable === 'undefined' ? props.edgesUpdatable : edge.updatable"
:edge-updater-radius="props.edgeUpdaterRadius"
:dimensions="props.dimensions"
:transform="props.transform"
:selectable="store.elementsSelectable"
:updatable="typeof edge.updatable === 'undefined' ? store.edgesUpdatable : edge.updatable"
:edge-updater-radius="store.edgeUpdaterRadius"
:dimensions="store.dimensions"
:transform="store.transform"
>
<template #default="edgeProps">
<slot :name="`edge-${edge.type}`" v-bind="edgeProps"></slot>
</template>
</EdgeWrapper>
<ConnectionLine
v-if="connectionLineVisible && sourceNode"
:source-node="sourceNode"
:connection-line-style="props.connectionLineStyle"
:connection-line-type="props.connectionLineType"
:connection-handle-id="props.connectionHandleId"
:connection-node-id="props.connectionNodeId"
:connection-handle-type="props.connectionHandleType"
:connection-position="props.connectionPosition"
:connection-mode="props.connectionMode"
:nodes="props.nodes"
:transform="props.transform"
>
<ConnectionLine v-if="connectionLineVisible && sourceNode" :source-node="sourceNode">
<template #default="customConnectionLineProps">
<slot name="custom-connection-line" v-bind="customConnectionLineProps"></slot>
</template>

View File

@@ -1,40 +1,21 @@
<script lang="ts" setup>
import { GraphNode, NodeComponent, SnapGrid, Transform } from '../../types'
import { GraphNode } from '../../types'
import NodeWrapper from '../../components/Nodes/NodeWrapper.vue'
import { useStore } from '../../composables'
interface NodeRendererProps {
nodes?: GraphNode[]
nodeTypes: Record<string, NodeComponent>
selectNodesOnDrag?: boolean
snapToGrid?: boolean
snapGrid?: SnapGrid
transform: Transform
elementsSelectable?: boolean
nodesDraggable?: boolean
nodesConnectable?: boolean
}
const props = withDefaults(defineProps<NodeRendererProps>(), {
selectNodesOnDrag: true,
nodesConnectable: true,
nodesDraggable: true,
elementsSelectable: true,
snapToGrid: false,
snapGrid: () => [15, 15],
})
const store = useStore()
const getType = (type?: string) => {
const t = type ?? 'default'
let nodeType = props.nodeTypes[t]
let nodeType = store.getNodeTypes[t]
if (!nodeType) {
nodeType = props.nodeTypes.default
nodeType = store.getNodeTypes.default
console.warn(`Node type "${type}" not found. Using fallback type "default".`)
}
return nodeType
}
const transform = computed(() => `translate(${props.transform[0]}px,${props.transform[1]}px) scale(${props.transform[2]})`)
const snapGrid = (node: GraphNode) => node.snapGrid ?? (props.snapToGrid ? props.snapGrid : undefined)
const transform = computed(() => `translate(${store.transform[0]}px,${store.transform[1]}px) scale(${store.transform[2]})`)
const snapGrid = (node: GraphNode) => node.snapGrid ?? (store.snapToGrid ? store.snapGrid : undefined)
</script>
<script lang="ts">
export default {
@@ -44,16 +25,16 @@ export default {
<template>
<div class="vue-flow__nodes" :style="{ transform }">
<NodeWrapper
v-for="(node, i) of props.nodes"
v-for="(node, i) of store.getNodes"
:key="`${node.id}-${i}`"
:node="node"
:component="getType(node.type)"
:draggable="typeof node.draggable === 'undefined' ? props.nodesDraggable : node.draggable"
:selectable="typeof node.selectable === 'undefined' ? props.elementsSelectable : node.selectable"
:connectable="typeof node.connectable === 'undefined' ? props.nodesConnectable : node.connectable"
:draggable="typeof node.draggable === 'undefined' ? store.nodesDraggable : node.draggable"
:selectable="typeof node.selectable === 'undefined' ? store.elementsSelectable : node.selectable"
:connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : node.connectable"
:snap-grid="snapGrid(node)"
:select-nodes-on-drag="props.selectNodesOnDrag"
:scale="props.transform[2]"
:select-nodes-on-drag="store.selectNodesOnDrag"
:scale="store.transform[2]"
>
<template #default="nodeProps">
<slot :name="`node-${node.type}`" v-bind="nodeProps"></slot>

View File

@@ -5,24 +5,6 @@ import { getConnectedEdges, isGraphNode } from '../../utils'
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
import UserSelection from '../../components/UserSelection/UserSelection.vue'
interface SelectionPaneProps {
edges: GraphEdge[]
selectedElements?: FlowElements
selectionKeyCode?: KeyCode
deleteKeyCode?: KeyCode
multiSelectionKeyCode?: KeyCode
elementsSelectable?: boolean
nodesSelectionActive?: boolean
selectionActive?: boolean
}
const props = withDefaults(defineProps<SelectionPaneProps>(), {
selectionKeyCode: 'Shift',
deleteKeyCode: 'Backspace',
multiSelectionKeyCode: 'Meta',
elementsSelectable: true,
selectedElements: () => [],
})
const store = useStore()
const onClick = (event: MouseEvent) => {
@@ -38,11 +20,11 @@ const onWheel = (event: WheelEvent) => store.hooks.paneScroll.trigger(event)
const userSelection = ref(false)
tryOnMounted(() => {
useKeyPress(props.deleteKeyCode, (keyPressed) => {
if (keyPressed && props.selectedElements) {
const selectedNodes = props.selectedElements.filter(isGraphNode)
const connectedEdges = getConnectedEdges(selectedNodes, props.edges)
const elementsToRemove = [...props.selectedElements, ...connectedEdges].reduce(
useKeyPress(store.deleteKeyCode, (keyPressed) => {
if (keyPressed && store.selectedElements) {
const selectedNodes = store.selectedElements.filter(isGraphNode)
const connectedEdges = getConnectedEdges(selectedNodes, store.getEdges)
const elementsToRemove = [...store.selectedElements, ...connectedEdges].reduce(
(res, item) => res.set(item.id, item),
new Map<string, FlowElement>(),
)
@@ -53,12 +35,12 @@ tryOnMounted(() => {
}
})
useKeyPress(props.multiSelectionKeyCode, (keyPressed) => {
useKeyPress(store.multiSelectionKeyCode, (keyPressed) => {
store.multiSelectionActive = keyPressed
})
useKeyPress(props.selectionKeyCode, (keyPressed) => {
userSelection.value = keyPressed && (props.selectionActive || props.elementsSelectable)
useKeyPress(store.selectionKeyCode, (keyPressed) => {
userSelection.value = keyPressed && (store.selectionActive || store.elementsSelectable)
})
})
</script>
@@ -70,12 +52,7 @@ export default {
</script>
<template>
<slot></slot>
<UserSelection v-if="userSelection" id="user-selection" :key="`user-selection-${store.id}`" />
<NodesSelection
v-if="props.nodesSelectionActive"
id="nodes-selection"
:key="`nodes-selction-${store.id}`"
:selected-elements="props.selectedElements"
/>
<UserSelection v-if="userSelection" :key="`user-selection-${store.id}`" />
<NodesSelection v-if="store.nodesSelectionActive" :key="`nodes-selction-${store.id}`" />
<div :key="`flow-pane-${store.id}`" class="vue-flow__pane" @click="onClick" @contextmenu="onContextMenu" @wheel="onWheel" />
</template>

View File

@@ -1,16 +1,4 @@
<script lang="ts" setup>
import { CSSProperties } from 'vue'
import {
ConnectionLineType,
ConnectionMode,
Elements,
PanOnScrollMode,
KeyCode,
CoordinateExtent,
NodeTypes,
EdgeTypes,
FlowOptions,
} from '../../types'
import Renderer from '../Renderer/Renderer.vue'
import ZoomPane from '../ZoomPane/ZoomPane.vue'
import SelectionPane from '../SelectionPane/SelectionPane.vue'
@@ -18,47 +6,7 @@ import NodeRenderer from '../NodeRenderer/NodeRenderer.vue'
import EdgeRenderer from '../EdgeRenderer/EdgeRenderer.vue'
import LoadingIndicator from '../../components/Loading/LoadingIndicator.vue'
import { createHooks, initFlow } from '../../composables'
interface FlowProps extends FlowOptions {
id?: string
modelValue?: Elements
nodeTypes?: NodeTypes
edgeTypes?: EdgeTypes
connectionMode?: ConnectionMode
connectionLineType?: ConnectionLineType
connectionLineStyle?: CSSProperties
deleteKeyCode?: KeyCode
selectionKeyCode?: KeyCode
multiSelectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
preventScrolling?: boolean
snapToGrid?: boolean
snapGrid?: [number, number]
onlyRenderVisibleElements?: boolean
edgesUpdatable?: boolean
nodesDraggable?: boolean
nodesConnectable?: boolean
elementsSelectable?: boolean
selectNodesOnDrag?: boolean
paneMoveable?: boolean
minZoom?: number
maxZoom?: number
defaultZoom?: number
defaultPosition?: [number, number]
translateExtent?: CoordinateExtent
nodeExtent?: CoordinateExtent
arrowHeadColor?: string
markerEndId?: string
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
panOnScrollSpeed?: number
panOnScrollMode?: PanOnScrollMode
zoomOnDoubleClick?: boolean
edgeUpdaterRadius?: number
storageKey?: string
loading?: string
}
import type { FlowProps } from '../../types/flow'
const props = withDefaults(defineProps<FlowProps>(), {
modelValue: () => [],
@@ -119,45 +67,10 @@ export default {
<Suspense timeout="0">
<template #default>
<Renderer :key="`renderer-${store.id}`" :elements="elements">
<ZoomPane
:key="`zoom-pane-${store.id}`"
:prevent-scrolling="store.preventScrolling"
:selection-key-code="store.selectionKeyCode"
:zoom-activation-key-code="store.zoomActivationKeyCode"
:default-zoom="store.defaultZoom"
:default-position="store.defaultPosition"
:zoom-on-scroll="store.zoomOnScroll"
:zoom-on-pinch="store.zoomOnPinch"
:zoom-on-double-click="store.zoomOnDoubleClick"
:pan-on-scroll="store.panOnScroll"
:pan-on-scroll-speed="store.panOnScrollSpeed"
:pan-on-scroll-mode="store.panOnScrollMode"
:pane-moveable="store.paneMoveable"
>
<ZoomPane :key="`zoom-pane-${store.id}`">
<template #default="zoomPaneProps">
<SelectionPane
:key="`selection-pane-${store.id}`"
:edges="store.getEdges"
:selected-elements="store.selectedElements"
:selection-active="store.selectionActive"
:nodes-selection-active="store.nodesSelectionActive"
:elements-selectable="store.elementsSelectable"
:delete-key-code="store.deleteKeyCode"
:multi-selection-key-code="store.multiSelectionKeyCode"
:selection-key-code="store.selectionKeyCode"
>
<NodeRenderer
:key="`node-renderer-${store.id}`"
:nodes="store.getNodes"
:node-types="store.getNodeTypes"
:snap-to-grid="store.snapToGrid"
:snap-grid="store.snapGrid"
:select-nodes-on-drag="store.selectNodesOnDrag"
:transform="store.transform"
:elements-selectable="store.elementsSelectable"
:nodes-draggable="store.nodesDraggable"
:nodes-connectable="store.nodesConnectable"
>
<SelectionPane :key="`selection-pane-${store.id}`">
<NodeRenderer :key="`node-renderer-${store.id}`">
<template
v-for="nodeName of Object.keys(store.getNodeTypes)"
#[`node-${nodeName}`]="nodeProps"
@@ -166,28 +79,7 @@ export default {
<slot :name="`node-${nodeName}`" v-bind="nodeProps" />
</template>
</NodeRenderer>
<EdgeRenderer
:key="`edge-renderer-${store.id}`"
:transform="store.transform"
:dimensions="store.dimensions"
:edges="store.getEdges"
:edge-types="store.getEdgeTypes"
:nodes="store.getNodes"
:selected-elements="store.selectedElements"
:elements-selectable="store.elementsSelectable"
:connection-node-id="store.connectionNodeId"
:connction-handle-id="store.connectionHandleId"
:connection-handle-type="store.connectionHandleType"
:connection-position="store.connectionPosition"
:connection-mode="store.connectionMode"
:nodes-connectable="store.nodesConnectable"
:connection-line-type="store.connectionLineType"
:connection-line-style="store.connectionLineStyle"
:arrow-head-color="store.arrowHeadColor"
:marker-end-id="store.markerEndId"
:only-render-visible-elements="store.onlyRenderVisibleElements"
:edges-updatable="store.edgesUpdatable"
>
<EdgeRenderer :key="`edge-renderer-${store.id}`">
<template
v-for="edgeName of Object.keys(store.getEdgeTypes)"
#[`edge-${edgeName}`]="edgeProps"

View File

@@ -1,39 +1,10 @@
<script lang="ts" setup>
import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
import { pointer, select } from 'd3-selection'
import { FlowTransform, KeyCode, PanOnScrollMode } from '../../types'
import { FlowTransform, PanOnScrollMode } from '../../types'
import { useKeyPress, useStore, useWindow } from '../../composables'
import { clamp } from '../../utils'
import { clamp, clampPosition } from '../../utils'
interface ZoomPaneProps {
selectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
paneMoveable?: boolean
defaultZoom?: number
defaultPosition?: [number, number]
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
panOnScrollSpeed?: number
panOnScrollMode?: PanOnScrollMode
zoomOnDoubleClick?: boolean
preventScrolling?: boolean
}
const props = withDefaults(defineProps<ZoomPaneProps>(), {
selectionKeyCode: 'Shift',
zoomActivationKeyCode: 'Meta',
defaultZoom: 1,
defaultPosition: () => [0, 0],
zoomOnScroll: true,
zoomOnPinch: true,
zoomOnDoubleClick: true,
panOnScroll: false,
panOnScrollSpeed: 0.5,
panOnScrollMode: PanOnScrollMode.Free,
paneMoveable: true,
preventScrolling: true,
})
const store = useStore()
const zoomPaneEl = templateRef<HTMLDivElement>('zoomPane', null)
@@ -50,10 +21,11 @@ const eventToFlowTransform = (eventTransform: ZoomTransform): FlowTransform => (
const noWheel = (event: Event) => (event.target as Element).closest('.nowheel')
const clampedX = clamp(props.defaultPosition[0], store.translateExtent[0][0], store.translateExtent[1][0])
const clampedY = clamp(props.defaultPosition[1], store.translateExtent[0][1], store.translateExtent[1][1])
const clampedZoom = clamp(props.defaultZoom, store.minZoom, store.maxZoom)
const transform = ref({ x: clampedX, y: clampedY, zoom: clampedZoom })
const clampedZoom = clamp(store.defaultZoom, store.minZoom, store.maxZoom)
const transform = ref({
...clampPosition({ x: store.defaultPosition[0], y: store.defaultPosition[1] }, store.translateExtent),
zoom: clampedZoom,
})
const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent))
const d3Selection = ref()
@@ -66,7 +38,7 @@ nextTick(async () => {
const d3s = d3Selection.value!
const d3ZoomHandler = d3s.on('wheel.zoom')
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
const updatedTransform = zoomIdentity.translate(transform.value.x, transform.value.y).scale(transform.value.zoom)
d3z.transform(d3s, updatedTransform)
store.initD3Zoom({ d3Zoom: d3z, d3Selection: d3s, d3ZoomHandler })
store.transform = [updatedTransform.x, updatedTransform.y, updatedTransform.k]
@@ -87,7 +59,7 @@ nextTick(async () => {
}
})
useKeyPress(props.selectionKeyCode, (keyPress) => {
useKeyPress(store.selectionKeyCode, (keyPress) => {
if (keyPress) {
d3z.on('zoom', null)
} else {
@@ -101,8 +73,8 @@ nextTick(async () => {
}
})
useKeyPress(props.zoomActivationKeyCode, (keyPress) => {
if (props.panOnScroll && keyPress) {
useKeyPress(store.zoomActivationKeyCode, (keyPress) => {
if (store.panOnScroll && keyPress) {
d3s
?.on('wheel', (event: WheelEvent) => {
if (noWheel(event)) return
@@ -111,7 +83,7 @@ nextTick(async () => {
const currentZoom = d3s?.property('__zoom').k || 1
if (event.ctrlKey && props.zoomOnPinch) {
if (event.ctrlKey && store.zoomOnPinch) {
const point = pointer(event)
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10
@@ -124,40 +96,40 @@ nextTick(async () => {
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1
const deltaX = props.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
const deltaY = props.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
const deltaX = store.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize
const deltaY = store.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize
if (d3s && props.panOnScrollSpeed)
if (d3s && store.panOnScrollSpeed)
d3z?.translateBy(
d3s,
-(deltaX / currentZoom) * props.panOnScrollSpeed,
-(deltaY / currentZoom) * props.panOnScrollSpeed,
-(deltaX / currentZoom) * store.panOnScrollSpeed,
-(deltaY / currentZoom) * store.panOnScrollSpeed,
)
})
.on('wheel.zoom', null)
} else if (typeof d3ZoomHandler !== 'undefined') {
d3s
?.on('wheel', (event: WheelEvent) => {
if ((!props.zoomOnScroll && props.preventScrolling) || !props.preventScrolling || noWheel(event)) return
if ((!store.zoomOnScroll && store.preventScrolling) || !store.preventScrolling || noWheel(event)) return
event.preventDefault()
})
.on('wheel.zoom', d3ZoomHandler)
}
})
const keyPress = useKeyPress(props.selectionKeyCode)
const keyPress = useKeyPress(store.selectionKeyCode)
d3z.filter((event: MouseEvent) => {
const pinchZoom = props.zoomOnPinch && event.ctrlKey
const pinchZoom = store.zoomOnPinch && event.ctrlKey
// if all interactions are disabled, we prevent all zoom events
if (!props.paneMoveable && !props.zoomOnScroll && !props.panOnScroll && !props.zoomOnDoubleClick && !props.zoomOnPinch)
if (!store.paneMoveable && !store.zoomOnScroll && !store.panOnScroll && !store.zoomOnDoubleClick && !store.zoomOnPinch)
return false
// during a selection we prevent all other interactions
if (keyPress.value) return false
// if zoom on double click is disabled, we prevent the double click event
if (!props.zoomOnDoubleClick && event.type === 'dblclick') return false
if (!store.zoomOnDoubleClick && event.type === 'dblclick') return false
if (noWheel(event) && event.type === 'wheel') return false
@@ -171,13 +143,13 @@ nextTick(async () => {
// when the target element is a node selection, we still allow zooming
if ((event.target as Element).closest('.vue-flow__nodesselection') && event.type !== 'wheel') return false
if (!props.zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
if (!store.zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false
// when there is no scroll handling enabled, we prevent all wheel events
if (!props.zoomOnScroll && !props.panOnScroll && !pinchZoom && event.type === 'wheel') return false
if (!store.zoomOnScroll && !store.panOnScroll && !pinchZoom && event.type === 'wheel') return false
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
if (!props.paneMoveable && (event.type === 'mousedown' || event.type === 'touchstart')) return false
if (!store.paneMoveable && (event.type === 'mousedown' || event.type === 'touchstart')) return false
// default filter for d3-zoom
return (!event.ctrlKey || event.type === 'wheel') && !event.button
@@ -190,24 +162,24 @@ const { width, height } = useElementBounding(zoomPaneEl)
const window = useWindow()
if ('screen' in window) await until(() => store.isReady).toBe(true)
watch(
[width, height],
([newWidth, newHeight]) => {
store.dimensions.width = newWidth
store.dimensions.height = newHeight
},
{ flush: 'sync', immediate: true },
)
watch(
transform,
(val) =>
(store.transform = [
clamp(val.x, store.translateExtent[0][0], store.translateExtent[1][0]),
clamp(val.y, store.translateExtent[0][1], store.translateExtent[1][1]),
clamp(val.zoom, store.minZoom, store.maxZoom),
]),
{ flush: 'sync', immediate: true },
)
nextTick(() => {
watch(
[width, height],
([newWidth, newHeight]) => {
store.dimensions.width = newWidth
store.dimensions.height = newHeight
},
{ flush: 'sync', immediate: true },
)
watch(
transform,
(val) => {
const { x, y } = clampPosition(val, store.translateExtent)
store.transform = [x, y, clamp(val.zoom, store.minZoom, store.maxZoom)]
},
{ flush: 'sync', immediate: true },
)
})
</script>
<script lang="ts">
export default {
@@ -216,11 +188,6 @@ export default {
</script>
<template>
<div ref="zoomPane" class="vue-flow__zoompane">
<slot
v-bind="{
transform: store.transform,
dimensions: { width, height },
}"
/>
<slot />
</div>
</template>

View File

@@ -1,4 +1,4 @@
import { FlowGetters, FlowState, GraphEdge, GraphNode } from '~/types'
import { FlowGetters, FlowState, GraphEdge, GraphNode, Node } from '~/types'
import { defaultEdgeTypes, defaultNodeTypes, getNodesInside, getSourceTargetNodes, isEdge, isGraphNode } from '~/utils'
export default (state: FlowState): FlowGetters => {
@@ -19,8 +19,16 @@ export default (state: FlowState): FlowGetters => {
})
const getNodes = computed<GraphNode[]>(() => {
if (state.isReady) {
const nodes = state.elements.filter((n) => isGraphNode(n) && !n.hidden) as GraphNode[]
if (state.isReady && state.dimensions.width && state.dimensions.height) {
console.log('getter')
const nodes: GraphNode[] = []
const parseNode = (n: Node) => {
nodes.push(<GraphNode>n)
if (n.children && n.children.length) {
n.children.forEach((c) => parseNode(c))
}
}
state.elements.filter((n) => isGraphNode(n) && !n.hidden).forEach((node) => parseNode(<GraphNode>node))
return state.onlyRenderVisibleElements
? nodes &&
getNodesInside(
@@ -41,7 +49,8 @@ export default (state: FlowState): FlowGetters => {
const getEdges = computed<GraphEdge[]>(() => {
const edges = state.elements.filter((e) => isEdge(e) && !e.hidden) as GraphEdge[]
if (state.isReady) {
if (state.isReady && state.dimensions.width && state.dimensions.height) {
console.log('getter2')
return (
edges
.map((edge) => {

View File

@@ -109,9 +109,9 @@ export type FlowInstance<T = any> = {
toObject: ToObjectFunc<T>
}
export interface FlowOptions {
export interface FlowProps {
modelValue?: Elements
id?: string
elements?: Elements
nodeTypes?: NodeTypes
edgeTypes?: EdgeTypes
connectionMode?: ConnectionMode
@@ -149,3 +149,7 @@ export interface FlowOptions {
storageKey?: string
loading?: string
}
export interface FlowOptions extends Omit<FlowProps, 'modelValue'> {
elements?: Elements
}

View File

@@ -1,19 +1,9 @@
import { DraggableOptions } from '@braks/revue-draggable'
import { XYPosition, Position, SnapGrid, Element, Dimensions, XYZPosition } from './flow'
import { XYPosition, Position, SnapGrid, Element, XYZPosition } from './flow'
import { HandleElement, ValidConnectionFunc } from './components'
export type CoordinateExtent = [[number, number], [number, number]]
export interface VFInternals extends Dimensions {
handleBounds: {
source?: HandleElement[]
target?: HandleElement[]
}
parentNode?: any
isParent?: boolean
position: XYZPosition
}
export type Draggable = Omit<DraggableOptions, 'scale' | 'grid' | 'enableUserSelectHack' | 'enableTransformFix'> | boolean
export interface Node<T = any> extends Element<T> {
@@ -28,18 +18,27 @@ export interface Node<T = any> extends Element<T> {
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
extent?: 'parent' | CoordinateExtent
parentNode?: string
children?: Node[]
width?: number
height?: number
}
export interface GraphNode<T = any> extends Node<T> {
__vf: VFInternals
handleBounds: {
source?: HandleElement[]
target?: HandleElement[]
}
parentNode?: GraphNode
isParent?: boolean
computedPosition: XYZPosition
selected?: boolean
dragging?: boolean
width: number
height: number
}
export interface NodeProps<T = any> extends GraphNode {
id: string
position: XYPosition
type?: string
data?: T
selected?: boolean
@@ -49,7 +48,13 @@ export interface NodeProps<T = any> extends GraphNode {
dragging?: boolean
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
__vf: VFInternals
handleBounds: {
source?: HandleElement[]
target?: HandleElement[]
}
parentNode?: GraphNode
isParent?: boolean
position: XYZPosition
}
export type NodeDimensionUpdate = {

View File

@@ -4,8 +4,8 @@ import { Edge, EdgePositions, GraphNode, HandleElement, Position, Transform, XYP
export function getHandlePosition(position: Position, node: GraphNode, handle?: HandleElement): XYPosition {
const x = (handle?.x ?? 0) + node.position.x
const y = (handle?.y ?? 0) + node.position.y
const width = handle?.width ?? node.__vf.width
const height = handle?.height ?? node.__vf.height
const width = handle?.width ?? node.width
const height = handle?.height ?? node.height
switch (position) {
case Position.Top:

View File

@@ -169,20 +169,17 @@ export const parseNode = (node: Node, nodeExtent: CoordinateExtent): GraphNode =
...node,
id: node.id.toString(),
type: node.type ?? 'default',
__vf: {
width: 0,
height: 0,
handleBounds: {
source: [],
target: [],
},
position: {
z: typeof node.style?.zIndex === 'string' ? parseInt(node.style?.zIndex) : node.style?.zIndex ?? 0,
...clampPosition(node.position, nodeExtent),
},
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),
},
dragging: false,
position: clampPosition(node.position, nodeExtent),
})
export const parseEdge = (edge: Edge): GraphEdge => ({
@@ -222,7 +219,7 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect) => boxToRect(getBound
export const getRectOfNodes = (nodes: GraphNode[]) => {
const box = nodes.reduce(
(currBox, { position = { x: 0, y: 0 }, __vf: { width = 0, height = 0 } = {} }) =>
(currBox, { position = { x: 0, y: 0 }, width = 0, height = 0 } = {} as any) =>
getBoundsOfBoxes(
currBox,
rectToBox({
@@ -251,12 +248,8 @@ export const getNodesInside = (nodes: GraphNode[], rect: Rect, [tx, ty, tScale]:
})
return nodes.filter((node) => {
if (!node.__vf || node.selectable === false) return false
const {
position = { x: 0, y: 0 },
__vf: { width = 0, height = 0 },
dragging = false,
} = node
if (!node || node.selectable === false) return false
const { position = { x: 0, y: 0 }, width = 0, height = 0, dragging = false } = node
const nBox = rectToBox({ ...position, width, height } as any)
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x))
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y))
@@ -321,11 +314,11 @@ export const getTransformForBounds = (
export const processElements = (elements: FlowElements, fn: (elements: FlowElements) => void) => {
return new Promise((resolve) => {
const chunk = 50
const chunk = 100
let index = 0
function doChunk() {
const chunkPos = chunk * index
const lastChunk = elements.length - chunkPos < chunkPos
const lastChunk = elements.length - chunkPos < chunk * (index > 0 ? index : 1)
const cnt = !lastChunk ? chunk : elements.length - chunkPos
fn(elements.slice(chunkPos, chunkPos + cnt))
index++
@@ -370,10 +363,13 @@ export const parseElement = (element: Node | Edge, prevElements: FlowElements, n
export function calculateXYZPosition(node: GraphNode, result: XYZPosition): XYZPosition {
if (!node.parentNode) return result
return calculateXYZPosition(node.__vf.parentNode, {
x: result.x + node.__vf.parentNode.position.x,
y: result.y + node.__vf.parentNode.position.y,
z: node.__vf.parentNode.__vf.z > node.__vf.position.z ? node.__vf.parentNode.__vf.z + 1 : node.__vf.position.z,
return calculateXYZPosition(node.parentNode, {
x: result.x + node.parentNode.position.x,
y: result.y + node.parentNode.position.y,
z:
node.parentNode.computedPosition.z > node.computedPosition.z
? node.parentNode.computedPosition.z + 1
: node.computedPosition.z,
})
}
@@ -392,12 +388,12 @@ export const parseElements = (elements: Elements, prevElements: FlowElements, no
nodes.forEach((n) => {
if (n.parentNode) {
const parent = nodes.find((p) => p.id === n.parentNode)
const parent = nodes.find((p) => p.id === n.parentNode?.id)
if (n.parentNode && !parent) {
console.error(`Parent node ${n.parentNode} not found`)
} else if (parent) {
n.__vf.parentNode = parent
parent.__vf.isParent = true
n.parentNode = parent
parent.isParent = true
}
}
})
@@ -418,7 +414,7 @@ export const parseElements = (elements: Elements, prevElements: FlowElements, no
export function isParentSelected(node: GraphNode): boolean {
if (!node.parentNode) return false
if (!node.__vf.parentNode) return false
if (node.__vf.parentNode.selected) return true
return isParentSelected(node.__vf.parentNode)
if (!node.parentNode) return false
if (node.parentNode.selected) return true
return isParentSelected(node.parentNode)
}