fix(nodes,edges): try to resolve component before falling back to default

This commit is contained in:
Braks
2022-04-04 21:42:48 +02:00
parent 897f662c29
commit 0d6bc39e49
4 changed files with 104 additions and 101 deletions
+50 -36
View File
@@ -1,47 +1,46 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useHandle, useVueFlow } from '../../composables' import { useHandle, useVueFlow } from '../../composables'
import { ConnectionMode, Position, GraphEdge, EdgeComponent } from '../../types' import { ConnectionMode, Position, EdgeComponent } from '../../types'
import { getEdgePositions, getHandle, getMarkerId } from '../../utils' import { getEdgePositions, getHandle, getMarkerId } from '../../utils'
import EdgeAnchor from './EdgeAnchor.vue' import EdgeAnchor from './EdgeAnchor.vue'
interface EdgeWrapper { interface EdgeWrapper {
edge: GraphEdge id: string
component?: EdgeComponent
selectable?: boolean selectable?: boolean
updatable?: boolean updatable?: boolean
} }
const props = defineProps<EdgeWrapper>() const props = defineProps<EdgeWrapper>()
const emit = defineEmits(['update:edge'])
const edge = useVModel(props, 'edge', emit)
const { store } = useVueFlow() const { store } = useVueFlow()
const edge = store.getEdge(props.id)
if (!edge) throw new Error(`Edge with ${props.id} not found!`)
const updating = ref(false) const updating = ref(false)
const { onMouseDown } = useHandle() const { onMouseDown } = useHandle()
const onEdgeClick = (event: MouseEvent) => { const onEdgeClick = (event: MouseEvent) => {
const data = { event, edge: edge.value } const data = { event, edge }
if (props.selectable) { if (props.selectable) {
store.nodesSelectionActive = false store.nodesSelectionActive = false
store.addSelectedEdges([edge.value]) store.addSelectedEdges([edge])
} }
store.hooks.edgeClick.trigger(data) store.hooks.edgeClick.trigger(data)
} }
const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge: edge.value }) const onEdgeContextMenu = (event: MouseEvent) => store.hooks.edgeContextMenu.trigger({ event, edge })
const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge: edge.value }) const onDoubleClick = (event: MouseEvent) => store.hooks.edgeDoubleClick.trigger({ event, edge })
const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge: edge.value }) const onEdgeMouseEnter = (event: MouseEvent) => store.hooks.edgeMouseEnter.trigger({ event, edge })
const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge: edge.value }) const onEdgeMouseMove = (event: MouseEvent) => store.hooks.edgeMouseMove.trigger({ event, edge })
const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge: edge.value }) const onEdgeMouseLeave = (event: MouseEvent) => store.hooks.edgeMouseLeave.trigger({ event, edge })
const onEdgeUpdaterMouseEnter = () => (updating.value = true) const onEdgeUpdaterMouseEnter = () => (updating.value = true)
const onEdgeUpdaterMouseOut = () => (updating.value = false) const onEdgeUpdaterMouseOut = () => (updating.value = false)
const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true) const onEdgeUpdaterSourceMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, true)
const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false) const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(event, false)
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => { const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? edge.value.target : edge.value.source const nodeId = isSourceHandle ? edge.target : edge.source
const handleId = (isSourceHandle ? edge.value.targetHandle : edge.value.sourceHandle) ?? '' const handleId = (isSourceHandle ? edge.targetHandle : edge.sourceHandle) ?? ''
store.hooks.edgeUpdateStart.trigger({ event, edge: edge.value }) store.hooks.edgeUpdateStart.trigger({ event, edge })
onMouseDown( onMouseDown(
event, event,
handleId, handleId,
@@ -49,22 +48,22 @@ const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
isSourceHandle, isSourceHandle,
undefined, undefined,
isSourceHandle ? 'target' : 'source', isSourceHandle ? 'target' : 'source',
(connection) => store.hooks.edgeUpdate.trigger({ edge: edge.value, connection }), (connection) => store.hooks.edgeUpdate.trigger({ edge, connection }),
() => store.hooks.edgeUpdateEnd.trigger({ event, edge: edge.value }), () => store.hooks.edgeUpdateEnd.trigger({ event, edge }),
) )
} }
// when connection type is loose we can define all handles as sources // when connection type is loose we can define all handles as sources
const targetNodeHandles = computed(() => const targetNodeHandles = computed(() =>
store.connectionMode === ConnectionMode.Strict store.connectionMode === ConnectionMode.Strict
? edge.value.targetNode.handleBounds.target ? edge.targetNode.handleBounds.target
: edge.value.targetNode.handleBounds.target ?? edge.value.targetNode.handleBounds.source, : edge.targetNode.handleBounds.target ?? edge.targetNode.handleBounds.source,
) )
const sourceHandle = controlledComputed( const sourceHandle = controlledComputed(
() => edge.value.sourceNode.handleBounds, () => edge.sourceNode.handleBounds,
() => getHandle(edge.value.sourceNode.handleBounds.source, edge.value.sourceHandle), () => getHandle(edge.sourceNode.handleBounds.source, edge.sourceHandle),
) )
const targetHandle = computed(() => getHandle(targetNodeHandles.value, edge.value.targetHandle)) const targetHandle = computed(() => getHandle(targetNodeHandles.value, edge.targetHandle))
const sourcePosition = computed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom)) const sourcePosition = computed(() => (sourceHandle.value ? sourceHandle.value.position : Position.Bottom))
const targetPosition = computed(() => (targetHandle.value ? targetHandle.value.position : Position.Top)) const targetPosition = computed(() => (targetHandle.value ? targetHandle.value.position : Position.Top))
const edgeUpdaterRadius = computed(() => store.edgeUpdaterRadius) const edgeUpdaterRadius = computed(() => store.edgeUpdaterRadius)
@@ -74,30 +73,44 @@ onMounted(() => {
[ [
sourcePosition, sourcePosition,
targetPosition, targetPosition,
() => edge.value.sourceNode.position, () => edge.sourceNode.position,
() => edge.value.targetNode.position, () => edge.targetNode.position,
() => edge.value.sourceNode.computedPosition, () => edge.sourceNode.computedPosition,
() => edge.value.targetNode.computedPosition, () => edge.targetNode.computedPosition,
() => edge.value.sourceNode.dimensions, () => edge.sourceNode.dimensions,
() => edge.value.targetNode.dimensions, () => edge.targetNode.dimensions,
], ],
() => { () => {
const { sourceX, sourceY, targetY, targetX } = getEdgePositions( const { sourceX, sourceY, targetY, targetX } = getEdgePositions(
edge.value.sourceNode, edge.sourceNode,
sourceHandle.value, sourceHandle.value,
sourcePosition.value, sourcePosition.value,
edge.value.targetNode, edge.targetNode,
targetHandle.value, targetHandle.value,
targetPosition.value, targetPosition.value,
) )
if (edge.value.sourceX !== sourceX) edge.value.sourceX = sourceX if (edge.sourceX !== sourceX) edge.sourceX = sourceX
if (edge.value.sourceY !== sourceY) edge.value.sourceY = sourceY if (edge.sourceY !== sourceY) edge.sourceY = sourceY
if (edge.value.targetX !== targetX) edge.value.targetX = targetX if (edge.targetX !== targetX) edge.targetX = targetX
if (edge.value.targetY !== targetY) edge.value.targetY = targetY if (edge.targetY !== targetY) edge.targetY = targetY
}, },
{ immediate: true, deep: true }, { immediate: true, deep: true },
) )
}) })
const name = ref(edge.type ?? 'default')
const type = computed(() => {
let edgeType = store.getEdgeTypes[name.value]
if (typeof edgeType === 'string') edgeType = resolveComponent(name.value, false) as EdgeComponent
if (typeof edgeType !== 'string') return edgeType
const slot = useSlots()?.[name.value]?.({})
if (!slot || !slot[0].key?.toString().includes(name.value)) {
console.warn(`Node type "${edge.type}" not found and no slot detected. Using fallback type "default".`)
name.value = 'default'
return store.getEdgeTypes.default
}
})
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
@@ -109,7 +122,7 @@ export default {
:key="`edge-${edge.id}`" :key="`edge-${edge.id}`"
:class="[ :class="[
'vue-flow__edge', 'vue-flow__edge',
`vue-flow__edge-${edge.type || 'default'}`, `vue-flow__edge-${name}`,
store.noPanClassName, store.noPanClassName,
{ {
selected: edge.selected, selected: edge.selected,
@@ -157,7 +170,8 @@ export default {
}" }"
> >
<component <component
:is="props.component ?? edge.type" :is="type"
v-if="type"
v-bind="{ v-bind="{
id: edge.id, id: edge.id,
sourceNode: edge.sourceNode, sourceNode: edge.sourceNode,
+49 -37
View File
@@ -1,13 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable' import { DraggableEventListener, DraggableCore } from '@braks/revue-draggable'
import { useVueFlow } from '../../composables' import { useVueFlow } from '../../composables'
import { GraphNode, NodeComponent, SnapGrid } from '../../types' import { NodeComponent, SnapGrid } from '../../types'
import { NodeId } from '../../context' import { NodeId } from '../../context'
import { getXYZPos } from '../../utils' import { getXYZPos } from '../../utils'
interface NodeWrapperProps { interface NodeWrapperProps {
node: GraphNode id: string
type: NodeComponent
draggable: boolean draggable: boolean
selectable: boolean selectable: boolean
connectable: boolean connectable: boolean
@@ -15,74 +14,72 @@ interface NodeWrapperProps {
} }
const props = defineProps<NodeWrapperProps>() const props = defineProps<NodeWrapperProps>()
const node = useVModel(props, 'node')
const { store } = useVueFlow() const { store } = useVueFlow()
provide(NodeId, props.node.id) const node = store.getNode(props.id)
if (!node) throw new Error(`Node with ${props.id} not found!`)
provide(NodeId, props.id)
const nodeElement = templateRef<HTMLDivElement>('node-element', null) const nodeElement = templateRef<HTMLDivElement>('node-element', null)
const onMouseEnterHandler = () => const onMouseEnterHandler = () => node.dragging && ((event: MouseEvent) => store.hooks.nodeMouseEnter.trigger({ event, node }))
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseEnter.trigger({ event, node: node.value })) const onMouseMoveHandler = () => node.dragging && ((event: MouseEvent) => store.hooks.nodeMouseMove.trigger({ event, node }))
const onMouseMoveHandler = () => const onMouseLeaveHandler = () => node.dragging && ((event: MouseEvent) => store.hooks.nodeMouseLeave.trigger({ event, node }))
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseMove.trigger({ event, node: node.value }))
const onMouseLeaveHandler = () =>
node.value.dragging && ((event: MouseEvent) => store.hooks.nodeMouseLeave.trigger({ event, node: node.value }))
const onContextMenuHandler = () => (event: MouseEvent) => const onContextMenuHandler = () => (event: MouseEvent) =>
store.hooks.nodeContextMenu.trigger({ store.hooks.nodeContextMenu.trigger({
event, event,
node: node.value, node,
}) })
const onDoubleClick = () => (event: MouseEvent) => store.hooks.nodeDoubleClick.trigger({ event, node: node.value }) const onDoubleClick = () => (event: MouseEvent) => store.hooks.nodeDoubleClick.trigger({ event, node })
const onSelectNodeHandler = (event: MouseEvent) => { const onSelectNodeHandler = (event: MouseEvent) => {
if (!props.draggable) { if (!props.draggable) {
if (props.selectable) { if (props.selectable) {
store.nodesSelectionActive = false store.nodesSelectionActive = false
if (!node.value.selected) store.addSelectedNodes([node.value]) if (!node.selected) store.addSelectedNodes([node])
} }
store.hooks.nodeClick.trigger({ event, node: node.value }) store.hooks.nodeClick.trigger({ event, node })
} }
} }
const onDragStart: DraggableEventListener = ({ event }) => { const onDragStart: DraggableEventListener = ({ event }) => {
store.addSelectedNodes([]) store.addSelectedNodes([])
store.hooks.nodeDragStart.trigger({ event, node: node.value }) store.hooks.nodeDragStart.trigger({ event, node })
if (store.selectNodesOnDrag && props.selectable) { if (store.selectNodesOnDrag && props.selectable) {
store.nodesSelectionActive = false store.nodesSelectionActive = false
if (!node.value.selected) store.addSelectedNodes([node.value]) if (!node.selected) store.addSelectedNodes([node])
} else if (!store.selectNodesOnDrag && !node.value.selected && props.selectable) { } else if (!store.selectNodesOnDrag && !node.selected && props.selectable) {
store.nodesSelectionActive = false store.nodesSelectionActive = false
store.addSelectedNodes([]) store.addSelectedNodes([])
} }
} }
const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => { const onDrag: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
nextTick(() => store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: true })) nextTick(() => store.updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: true }))
store.hooks.nodeDrag.trigger({ event, node: node.value }) store.hooks.nodeDrag.trigger({ event, node })
} }
const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => { const onDragStop: DraggableEventListener = ({ event, data: { deltaX, deltaY } }) => {
// onDragStop also gets called when user just clicks on a node. // onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here // Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!node.value.dragging) { if (!node.dragging) {
if (props.selectable && !store.selectNodesOnDrag && !node.value.selected) { if (props.selectable && !store.selectNodesOnDrag && !node.selected) {
store.addSelectedNodes([node.value]) store.addSelectedNodes([node])
} }
store.hooks.nodeClick.trigger({ event, node: node.value }) store.hooks.nodeClick.trigger({ event, node })
return return
} }
store.updateNodePosition({ id: node.value.id, diff: { x: deltaX, y: deltaY }, dragging: false }) store.updateNodePosition({ id: node.id, diff: { x: deltaX, y: deltaY }, dragging: false })
store.hooks.nodeDragStop.trigger({ event, node: node.value }) store.hooks.nodeDragStop.trigger({ event, node })
} }
onMounted(() => { onMounted(() => {
useResizeObserver(nodeElement, () => useResizeObserver(nodeElement, () =>
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value, forceUpdate: true }]), store.updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value, forceUpdate: true }]),
) )
watch([() => node.value.type, () => node.value.sourcePosition, () => node.value.targetPosition], () => watch([() => node.type, () => node.sourcePosition, () => node.targetPosition], () =>
nextTick(() => store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }])), nextTick(() => store.updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value }])),
) )
store.updateNodeDimensions([{ id: node.value.id, nodeElement: nodeElement.value }]) store.updateNodeDimensions([{ id: node.id, nodeElement: nodeElement.value }])
}) })
const scale = controlledComputed( const scale = controlledComputed(
@@ -91,22 +88,36 @@ const scale = controlledComputed(
) )
watch( watch(
[() => node.value.position, () => store.getNode(node.value.parentNode!)], [() => node.position, () => store.getNode(node.parentNode!)],
([pos, parent]) => { ([pos, parent]) => {
const xyzPos = { const xyzPos = {
...pos, ...pos,
z: node.value.dragging || node.value.selected ? 1000 : node.value.computedPosition.z, z: node.dragging || node.selected ? 1000 : node.computedPosition.z,
} }
if (parent) { if (parent) {
node.value.computedPosition = getXYZPos(parent, xyzPos) node.computedPosition = getXYZPos(parent, xyzPos)
} else { } else {
node.value.computedPosition = xyzPos node.computedPosition = xyzPos
} }
}, },
{ deep: true }, { deep: true },
) )
store.updateNodePosition({ id: node.value.id, diff: { x: 0, y: 0 } }) store.updateNodePosition({ id: node.id, diff: { x: 0, y: 0 } })
const name = ref(node.type ?? 'default')
const type = computed(() => {
let nodeType = store.getNodeTypes[name.value]
if (typeof nodeType === 'string') nodeType = resolveComponent(name.value, false) as NodeComponent
if (typeof nodeType !== 'string') return nodeType
const slot = useSlots()?.[name.value]?.({})
if (!slot || !slot[0].key?.toString().includes(name.value)) {
console.warn(`Node type "${node.type}" not found and no slot detected. Using fallback type "default".`)
name.value = 'default'
return store.getNodeTypes.default
}
})
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
@@ -130,7 +141,7 @@ export default {
:key="`node-${node.id}`" :key="`node-${node.id}`"
:class="[ :class="[
'vue-flow__node', 'vue-flow__node',
`vue-flow__node-${node.type}`, `vue-flow__node-${name}`,
store.noPanClassName, store.noPanClassName,
{ {
dragging: node.dragging, dragging: node.dragging,
@@ -176,7 +187,8 @@ export default {
}" }"
> >
<component <component
:is="props.type ?? node.type" :is="type"
v-if="type"
v-bind="{ v-bind="{
nodeElement, nodeElement,
id: node.id, id: node.id,
+2 -12
View File
@@ -6,15 +6,6 @@ import { groupEdgesByZLevel } from '../../utils'
import MarkerDefinitions from './MarkerDefinitions.vue' import MarkerDefinitions from './MarkerDefinitions.vue'
const { store } = useVueFlow() const { store } = useVueFlow()
const getType = (type?: string) => {
const t = type ?? 'default'
let edgeType = store.getEdgeTypes[t]
if (!edgeType) {
edgeType = store.getEdgeTypes.default
console.warn(`Edge type "${type}" not found. Using fallback type "default".`)
}
return edgeType
}
const sourceNode = controlledComputed( const sourceNode = controlledComputed(
() => store.connectionNodeId, () => store.connectionNodeId,
@@ -52,14 +43,13 @@ export default {
<g> <g>
<EdgeWrapper <EdgeWrapper
v-for="edge of group.edges" v-for="edge of group.edges"
:id="edge.id"
:key="edge.id" :key="edge.id"
:edge="edge"
:component="getType(edge.type)"
:selectable="typeof edge.selectable === 'undefined' ? store.elementsSelectable : edge.selectable" :selectable="typeof edge.selectable === 'undefined' ? store.elementsSelectable : edge.selectable"
:updatable="typeof edge.updatable === 'undefined' ? store.edgesUpdatable : edge.updatable" :updatable="typeof edge.updatable === 'undefined' ? store.edgesUpdatable : edge.updatable"
> >
<template #default="edgeProps"> <template #default="edgeProps">
<slot :name="`edge-${edge.type}`" v-bind="edgeProps"></slot> <slot v-if="edge.type" :name="`edge-${edge.type}`" v-bind="edgeProps"></slot>
</template> </template>
</EdgeWrapper> </EdgeWrapper>
<ConnectionLine v-if="connectionLineVisible && sourceNode" :source-node="sourceNode"> <ConnectionLine v-if="connectionLineVisible && sourceNode" :source-node="sourceNode">
+3 -16
View File
@@ -1,20 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { GraphNode } from '../../types'
import NodeWrapper from '../../components/Nodes/NodeWrapper.vue' import NodeWrapper from '../../components/Nodes/NodeWrapper.vue'
import { useVueFlow } from '../../composables' import { useVueFlow } from '../../composables'
const { store } = useVueFlow() const { store } = useVueFlow()
const getType = (type?: string) => {
const t = type ?? 'default'
let nodeType = store.getNodeTypes[t]
if (!nodeType) {
nodeType = store.getNodeTypes.default
console.warn(`Node type "${type}" not found. Using fallback type "default".`)
}
return nodeType
}
const snapGrid = (node: GraphNode) => node.snapGrid ?? (store.snapToGrid ? store.snapGrid : undefined)
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
@@ -25,16 +13,15 @@ export default {
<div class="vue-flow__nodes vue-flow__container"> <div class="vue-flow__nodes vue-flow__container">
<NodeWrapper <NodeWrapper
v-for="node of store.getNodes" v-for="node of store.getNodes"
:id="node.id"
:key="node.id" :key="node.id"
:node="node"
:type="getType(node.type)"
:draggable="typeof node.draggable === 'undefined' ? store.nodesDraggable : !!node.draggable" :draggable="typeof node.draggable === 'undefined' ? store.nodesDraggable : !!node.draggable"
:selectable="typeof node.selectable === 'undefined' ? store.elementsSelectable : !!node.selectable" :selectable="typeof node.selectable === 'undefined' ? store.elementsSelectable : !!node.selectable"
:connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : !!node.connectable" :connectable="typeof node.connectable === 'undefined' ? store.nodesConnectable : !!node.connectable"
:snap-grid="snapGrid(node)" :snap-grid="node.snapGrid ?? (store.snapToGrid ? store.snapGrid : undefined)"
> >
<template #default="nodeProps"> <template #default="nodeProps">
<slot :name="`node-${node.type}`" v-bind="nodeProps"></slot> <slot v-if="node.type" :name="`node-${node.type}`" v-bind="nodeProps"></slot>
</template> </template>
</NodeWrapper> </NodeWrapper>
</div> </div>