feat(nodes): add option to pass a valid target pos / valid source pos func to nodes

* edge updater cannot discern a valid connection by itself so we allow an option to pass a valid connection func to nodes which will then be used to determine if updating connection is valid
* it is optional, by default all connections are valid

Signed-off-by: Braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
Braks
2021-11-25 15:21:36 +01:00
parent 95fc013613
commit 9e941966a4
13 changed files with 83 additions and 42 deletions
+6 -3
View File
@@ -1,9 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Position, Handle, Connection } from '~/index' import { Position, Handle, ValidConnectionFunc } from '~/index'
const isValidConnection = (connection: Connection) => connection.target === 'B' interface CustomInputProps {
isValidTargetPos: ValidConnectionFunc
}
const props = defineProps<CustomInputProps>()
</script> </script>
<template> <template>
<div>Only connectable with B</div> <div>Only connectable with B</div>
<Handle type="source" :position="Position.Right" :is-valid-connection="isValidConnection" /> <Handle type="source" :position="Position.Right" :is-valid-connection="props.isValidTargetPos" />
</template> </template>
+3 -5
View File
@@ -1,16 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import { Position, Handle, Connection, NodeProps } from '~/index' import { Position, Handle, NodeProps, ValidConnectionFunc } from '~/index'
interface CustomNodeProps extends NodeProps { interface CustomNodeProps extends NodeProps {
id: string id: string
isValidSourcePos: ValidConnectionFunc
} }
const props = defineProps<CustomNodeProps>() const props = defineProps<CustomNodeProps>()
const isValidConnection = (connection: Connection) => connection.target === 'B'
</script> </script>
<template> <template>
<Handle type="target" :position="Position.Left" :is-valid-connection="isValidConnection" /> <Handle type="target" :position="Position.Left" :is-valid-connection="props.isValidSourcePos" />
<div>{{ props.id }}</div> <div>{{ props.id }}</div>
<Handle type="source" :position="Position.Right" :is-valid-connection="isValidConnection" />
</template> </template>
+15 -7
View File
@@ -1,24 +1,32 @@
<script lang="ts" setup> <script lang="ts" setup>
import CustomInput from './CustomInput.vue' import CustomInput from './CustomInput.vue'
import CustomNode from './CustomNode.vue' import CustomNode from './CustomNode.vue'
import { VueFlow, addEdge, Connection, Elements, Edge, OnConnectStartParams, FlowInstance } from '~/index' import { VueFlow, addEdge, Connection, Elements, OnConnectStartParams, FlowInstance } from '~/index'
import './validation.css' import './validation.css'
const initialElements: Elements = [ const initialElements: Elements = [
{ id: '0', type: 'custominput', position: { x: 0, y: 150 } }, { id: '0', type: 'custominput', position: { x: 0, y: 150 }, isValidTargetPos: (connection) => connection.target === 'B' },
{ id: 'A', type: 'customnode', position: { x: 250, y: 0 } }, {
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 } }, id: 'A',
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 } }, type: 'customnode',
position: { x: 250, y: 0 },
isValidSourcePos: (connection) => {
console.log(connection)
return false
},
},
{ id: 'B', type: 'customnode', position: { x: 250, y: 150 }, isValidSourcePos: (connection) => connection.target === 'B' },
{ id: 'C', type: 'customnode', position: { x: 250, y: 300 }, isValidSourcePos: (connection) => connection.target === 'B' },
] ]
const onLoad = (reactFlowInstance: FlowInstance) => reactFlowInstance.fitView() const onLoad = (flowInstance: FlowInstance) => flowInstance.fitView()
const onConnectStart = ({ nodeId, handleType }: OnConnectStartParams) => console.log('on connect start', { nodeId, handleType }) const onConnectStart = ({ nodeId, handleType }: OnConnectStartParams) => console.log('on connect start', { nodeId, handleType })
const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event) const onConnectStop = (event: MouseEvent) => console.log('on connect stop', event)
const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event) const onConnectEnd = (event: MouseEvent) => console.log('on connect end', event)
const elements = ref<Elements>(initialElements) const elements = ref<Elements>(initialElements)
const onConnect = (params: Connection | Edge) => { const onConnect = (params: Connection) => {
console.log('on connect', params) console.log('on connect', params)
elements.value = addEdge(params, elements.value) elements.value = addEdge(params, elements.value)
} }
+2 -4
View File
@@ -43,16 +43,14 @@ const onEdgeUpdaterTargetMouseDown = (event: MouseEvent) => handleEdgeUpdater(ev
const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => { const handleEdgeUpdater = (event: MouseEvent, isSourceHandle: boolean) => {
const nodeId = isSourceHandle ? props.edge.target : props.edge.source const nodeId = isSourceHandle ? props.edge.target : props.edge.source
const handleId = (isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle) ?? '' const handleId = (isSourceHandle ? props.edge.targetHandle : props.edge.sourceHandle) ?? ''
const isValidConnection = () => true
const isTarget = isSourceHandle
store.hooks.edgeUpdateStart.trigger({ event, edge: props.edge }) store.hooks.edgeUpdateStart.trigger({ event, edge: props.edge })
handler( handler(
event, event,
handleId, handleId,
nodeId, nodeId,
isTarget, isSourceHandle,
isValidConnection, undefined,
isSourceHandle ? 'target' : 'source', isSourceHandle ? 'target' : 'source',
(connection) => store.hooks.edgeUpdate.trigger({ edge: props.edge, connection }), (connection) => store.hooks.edgeUpdate.trigger({ edge: props.edge, connection }),
() => store.hooks.edgeUpdateEnd.trigger({ event, edge: props.edge }), () => store.hooks.edgeUpdateEnd.trigger({ event, edge: props.edge }),
+1 -1
View File
@@ -22,7 +22,7 @@ const nodeId = inject(NodeId, '')
const handler = useHandle() const handler = useHandle()
const onMouseDownHandler = (event: MouseEvent) => const onMouseDownHandler = (event: MouseEvent) =>
handler(event, props.id, nodeId, props.type === 'target', props.isValidConnection ?? (() => true)) handler(event, props.id, nodeId, props.type === 'target', props.isValidConnection)
</script> </script>
<script lang="ts"> <script lang="ts">
export default { export default {
+15 -3
View File
@@ -1,12 +1,14 @@
<script lang="ts" setup> <script lang="ts" setup>
import Handle from '../Handle/Handle.vue' import Handle from '../Handle/Handle.vue'
import { NodeProps, Position } from '../../types' import { NodeProps, Position, ValidConnectionFunc } from '../../types'
interface DefaultNodeProps extends NodeProps { interface DefaultNodeProps extends NodeProps {
data?: NodeProps['data'] data?: NodeProps['data']
connectable?: NodeProps['connectable'] connectable?: NodeProps['connectable']
targetPosition?: NodeProps['targetPosition'] targetPosition?: NodeProps['targetPosition']
sourcePosition?: NodeProps['sourcePosition'] sourcePosition?: NodeProps['sourcePosition']
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
} }
const props = withDefaults(defineProps<DefaultNodeProps>(), { const props = withDefaults(defineProps<DefaultNodeProps>(), {
@@ -23,10 +25,20 @@ export default {
} }
</script> </script>
<template> <template>
<Handle type="target" :position="props.targetPosition" :is-connectable="props.connectable" /> <Handle
type="target"
:position="props.targetPosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidTargetPos"
/>
<slot v-bind="props"> <slot v-bind="props">
<component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" /> <component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" />
<span v-else v-html="props.data?.label"></span> <span v-else v-html="props.data?.label"></span>
</slot> </slot>
<Handle type="source" :position="props.sourcePosition" :is-connectable="props.connectable" /> <Handle
type="source"
:position="props.sourcePosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidSourcePos"
/>
</template> </template>
+8 -2
View File
@@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import Handle from '../Handle/Handle.vue' import Handle from '../Handle/Handle.vue'
import { NodeProps, Position } from '../../types' import { NodeProps, Position, ValidConnectionFunc } from '../../types'
interface InputNodeProps extends NodeProps { interface InputNodeProps extends NodeProps {
data?: NodeProps['data'] data?: NodeProps['data']
connectable?: NodeProps['connectable'] connectable?: NodeProps['connectable']
sourcePosition?: NodeProps['sourcePosition'] sourcePosition?: NodeProps['sourcePosition']
isValidSourcePos?: ValidConnectionFunc
} }
const props = withDefaults(defineProps<InputNodeProps>(), { const props = withDefaults(defineProps<InputNodeProps>(), {
@@ -25,5 +26,10 @@ export default {
<component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" /> <component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" />
<span v-else v-html="props.data?.label"></span> <span v-else v-html="props.data?.label"></span>
</slot> </slot>
<Handle type="source" :position="props.sourcePosition" :connectable="props.connectable" /> <Handle
type="source"
:position="props.sourcePosition"
:connectable="props.connectable"
:is-valid-connection="props.isValidSourcePos"
/>
</template> </template>
+4
View File
@@ -180,6 +180,8 @@ export default {
sourcePosition: props.node.sourcePosition, sourcePosition: props.node.sourcePosition,
targetPosition: props.node.targetPosition, targetPosition: props.node.targetPosition,
dragging: props.node.__vf.isDragging, dragging: props.node.__vf.isDragging,
isValidTargetPos: props.node.isValidTargetPos,
isValidSourcePos: props.node.isValidSourcePos,
}" }"
> >
<component <component
@@ -195,6 +197,8 @@ export default {
sourcePosition: props.node.sourcePosition, sourcePosition: props.node.sourcePosition,
targetPosition: props.node.targetPosition, targetPosition: props.node.targetPosition,
dragging: props.node.__vf.isDragging, dragging: props.node.__vf.isDragging,
isValidTargetPos: props.node.isValidTargetPos,
isValidSourcePos: props.node.isValidSourcePos,
}" }"
/> />
</slot> </slot>
+8 -2
View File
@@ -1,11 +1,12 @@
<script lang="ts" setup> <script lang="ts" setup>
import Handle from '../Handle/Handle.vue' import Handle from '../Handle/Handle.vue'
import { NodeProps, Position } from '../../types' import { NodeProps, Position, ValidConnectionFunc } from '../../types'
interface OutputNodeProps extends NodeProps { interface OutputNodeProps extends NodeProps {
data?: NodeProps['data'] data?: NodeProps['data']
connectable?: NodeProps['connectable'] connectable?: NodeProps['connectable']
targetPosition?: NodeProps['targetPosition'] targetPosition?: NodeProps['targetPosition']
isValidTargetPos?: ValidConnectionFunc
} }
const props = withDefaults(defineProps<OutputNodeProps>(), { const props = withDefaults(defineProps<OutputNodeProps>(), {
@@ -25,5 +26,10 @@ export default {
<component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" /> <component :is="props.data?.label" v-if="typeof props.data?.label !== 'string'" />
<span v-else v-html="props.data?.label"></span> <span v-else v-html="props.data?.label"></span>
</slot> </slot>
<Handle type="source" :position="props.targetPosition" :is-connectable="props.connectable" /> <Handle
type="source"
:position="props.targetPosition"
:is-connectable="props.connectable"
:is-valid-connection="props.isValidTargetPos"
/>
</template> </template>
+10 -7
View File
@@ -73,9 +73,7 @@ export default (store = useStore()) =>
handleId: ElementId, handleId: ElementId,
nodeId: ElementId, nodeId: ElementId,
isTarget: boolean, isTarget: boolean,
isValidConnection: ValidConnectionFunc = () => { isValidConnection?: ValidConnectionFunc,
return true
},
elementEdgeUpdaterType?: HandleType, elementEdgeUpdaterType?: HandleType,
onEdgeUpdate?: (connection: Connection) => void, onEdgeUpdate?: (connection: Connection) => void,
onEdgeUpdateEnd?: () => void, onEdgeUpdateEnd?: () => void,
@@ -85,7 +83,11 @@ export default (store = useStore()) =>
const doc = getHostForElement(event.target as HTMLElement) const doc = getHostForElement(event.target as HTMLElement)
if (!doc) return if (!doc) return
let validConnectFunc: ValidConnectionFunc = isValidConnection ?? (() => true)
if (!isValidConnection) {
const node = store.getNodes.find((n) => n.id === nodeId)
if (node) validConnectFunc = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) ?? (() => true)
}
const elementBelow = doc.elementFromPoint(event.clientX, event.clientY) const elementBelow = doc.elementFromPoint(event.clientX, event.clientY)
const elementBelowIsTarget = elementBelow?.classList.contains('target') const elementBelowIsTarget = elementBelow?.classList.contains('target')
const elementBelowIsSource = elementBelow?.classList.contains('source') const elementBelowIsSource = elementBelow?.classList.contains('source')
@@ -104,7 +106,7 @@ export default (store = useStore()) =>
connectionHandleId: handleId, connectionHandleId: handleId,
connectionHandleType: handleType, connectionHandleType: handleType,
}) })
store.hooks.connectStart.trigger({ event, params: { nodeId, handleId, handleType } }) store.hooks.connectStart.trigger({ event, nodeId, handleId, handleType })
function onMouseMove(event: MouseEvent) { function onMouseMove(event: MouseEvent) {
store.connectionPosition.x = event.clientX - containerBounds.left store.connectionPosition.x = event.clientX - containerBounds.left
@@ -116,7 +118,7 @@ export default (store = useStore()) =>
isTarget, isTarget,
nodeId, nodeId,
handleId, handleId,
isValidConnection, validConnectFunc,
doc, doc,
) )
@@ -140,9 +142,10 @@ export default (store = useStore()) =>
isTarget, isTarget,
nodeId, nodeId,
handleId, handleId,
isValidConnection, validConnectFunc,
doc, doc,
) )
console.log(isValidConnection)
store.hooks.connectStop.trigger(event) store.hooks.connectStop.trigger(event)
+5 -5
View File
@@ -11,8 +11,8 @@ export enum ConnectionLineType {
export interface Connection { export interface Connection {
source: ElementId source: ElementId
target: ElementId target: ElementId
sourceHandle: ElementId sourceHandle?: ElementId
targetHandle: ElementId targetHandle?: ElementId
} }
export type ConnectionLineProps = { export type ConnectionLineProps = {
@@ -28,9 +28,9 @@ export type ConnectionLineProps = {
export type OnConnectFunc = (connection: Connection) => void export type OnConnectFunc = (connection: Connection) => void
export type OnConnectStartParams = { export type OnConnectStartParams = {
nodeId: ElementId | undefined nodeId?: ElementId
handleId: ElementId | undefined handleId?: ElementId
handleType: HandleType | undefined handleType?: HandleType
} }
export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void export type OnConnectStartFunc = (event: MouseEvent, params: OnConnectStartParams) => void
export type OnConnectStopFunc = (event: MouseEvent) => void export type OnConnectStopFunc = (event: MouseEvent) => void
+1 -2
View File
@@ -21,8 +21,7 @@ export interface FlowEvents {
connect: Connection connect: Connection
connectStart: { connectStart: {
event: MouseEvent event: MouseEvent
params: OnConnectStartParams } & { [key in keyof OnConnectStartParams]: OnConnectStartParams[key] }
}
connectStop: MouseEvent connectStop: MouseEvent
connectEnd: MouseEvent connectEnd: MouseEvent
load: FlowInstance load: FlowInstance
+5 -1
View File
@@ -1,7 +1,7 @@
import { Component, CSSProperties, DefineComponent } from 'vue' import { Component, CSSProperties, DefineComponent } from 'vue'
import { DraggableOptions } from '@braks/revue-draggable' import { DraggableOptions } from '@braks/revue-draggable'
import { XYPosition, ElementId, Position, SnapGrid } from './flow' import { XYPosition, ElementId, Position, SnapGrid } from './flow'
import { HandleElement } from './components' import { HandleElement, ValidConnectionFunc } from './components'
export interface VFInternals { export interface VFInternals {
isDragging?: boolean isDragging?: boolean
@@ -24,6 +24,8 @@ export interface NodeProps<T = any> {
targetPosition?: Position targetPosition?: Position
sourcePosition?: Position sourcePosition?: Position
dragging?: boolean dragging?: boolean
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
} }
export type NodeComponent = Component<NodeProps> | DefineComponent<NodeProps, any, any, any, any> | string export type NodeComponent = Component<NodeProps> | DefineComponent<NodeProps, any, any, any, any> | string
@@ -46,6 +48,8 @@ export interface Node<T = any> {
connectable?: boolean connectable?: boolean
dragHandle?: string dragHandle?: string
snapGrid?: SnapGrid snapGrid?: SnapGrid
isValidTargetPos?: ValidConnectionFunc
isValidSourcePos?: ValidConnectionFunc
} }
export interface GraphNode<T = any> extends Node<T> { export interface GraphNode<T = any> extends Node<T> {