fix(core): use center position of handle as snapping point for connection lines (#1625)
* fix(core): calculate handle positions correctly Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * fix(core): remove handleId check Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(core): cleanup Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(core): cleanup click connect handlers Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * fix(core): add flow id to handle ids Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * tests: correct updateEdge test Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * fix(core): use center position of handle Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(examples): cleanup easy connect example Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * refactor(tests): run actions on chrome Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> * chore(workflows): correctly hash lock file Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> --------- Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
@@ -26,7 +26,7 @@ runs:
|
|||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
with:
|
with:
|
||||||
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
path: ${{ steps.pnpm-cache.outputs.STORE_PATH }}
|
||||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('pnpm-lock.yaml') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-pnpm-store-
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/build-and-test.yml
vendored
2
.github/workflows/build-and-test.yml
vendored
@@ -54,5 +54,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
install-command: pnpm cypress install
|
install-command: pnpm cypress install
|
||||||
command: pnpm run test --cache-dir=.turbo
|
command: pnpm run test --cache-dir=.turbo
|
||||||
browser: chrome
|
|
||||||
component: true
|
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Background } from '@vue-flow/background'
|
import { Background } from '@vue-flow/background'
|
||||||
import { Controls } from '@vue-flow/controls'
|
import type { Node } from '@vue-flow/core'
|
||||||
import { MiniMap } from '@vue-flow/minimap'
|
|
||||||
import type { Elements } from '@vue-flow/core'
|
|
||||||
import { MarkerType, VueFlow, useVueFlow } from '@vue-flow/core'
|
import { MarkerType, VueFlow, useVueFlow } from '@vue-flow/core'
|
||||||
import CustomNode from './CustomNode.vue'
|
import CustomNode from './CustomNode.vue'
|
||||||
import CustomConnectionLine from './CustomConnectionLine.vue'
|
import FloatingConnectionLine from './FloatingConnectionLine.vue'
|
||||||
import FloatingEdge from './FloatingEdge.vue'
|
import FloatingEdge from './FloatingEdge.vue'
|
||||||
|
|
||||||
const { onConnect, addEdges } = useVueFlow()
|
const { onConnect, addEdges } = useVueFlow()
|
||||||
@@ -20,7 +18,7 @@ const defaultEdgeOptions = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = ref<Elements>([
|
const nodes = ref<Node[]>([
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'custom',
|
type: 'custom',
|
||||||
@@ -43,26 +41,21 @@ const elements = ref<Elements>([
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const edges = ref([])
|
||||||
|
|
||||||
onConnect(addEdges)
|
onConnect(addEdges)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div style="height: 100vh">
|
|
||||||
<VueFlow
|
<VueFlow
|
||||||
v-model="elements"
|
v-model:nodes="nodes"
|
||||||
|
v-model:edges="edges"
|
||||||
:elevate-nodes-on-select="false"
|
:elevate-nodes-on-select="false"
|
||||||
class="vue-flow-basic-example"
|
|
||||||
:default-zoom="1.5"
|
|
||||||
:default-edge-options="defaultEdgeOptions"
|
:default-edge-options="defaultEdgeOptions"
|
||||||
:min-zoom="0.2"
|
fit-view-on-init
|
||||||
:max-zoom="4"
|
|
||||||
>
|
>
|
||||||
<Background pattern-color="#aaa" :gap="8" />
|
<Background pattern-color="#aaa" :gap="8" />
|
||||||
|
|
||||||
<MiniMap />
|
|
||||||
|
|
||||||
<Controls />
|
|
||||||
|
|
||||||
<template #node-custom="props">
|
<template #node-custom="props">
|
||||||
<CustomNode :id="props.id" />
|
<CustomNode :id="props.id" />
|
||||||
</template>
|
</template>
|
||||||
@@ -72,8 +65,7 @@ onConnect(addEdges)
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #connection-line="cProps">
|
<template #connection-line="cProps">
|
||||||
<CustomConnectionLine v-bind="cProps" />
|
<FloatingConnectionLine v-bind="cProps" />
|
||||||
</template>
|
</template>
|
||||||
</VueFlow>
|
</VueFlow>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GraphNode } from '@vue-flow/core'
|
import type { ConnectionLineProps } from '@vue-flow/core'
|
||||||
import { getStraightPath } from '@vue-flow/core'
|
import { BaseEdge, getStraightPath } from '@vue-flow/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{ sourceNode: GraphNode; sourceX: number; sourceY: number; targetX: number; targetY: number }>()
|
const props = defineProps<ConnectionLineProps>()
|
||||||
|
|
||||||
const edgePath = computed(() =>
|
const edgePath = computed(() =>
|
||||||
getStraightPath({
|
getStraightPath({
|
||||||
@@ -15,13 +15,12 @@ const edgePath = computed(() =>
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<g>
|
<g>
|
||||||
<path
|
<BaseEdge
|
||||||
:style="{
|
:style="{
|
||||||
strokeWidth: 3,
|
strokeWidth: 3,
|
||||||
stroke: 'black',
|
stroke: 'black',
|
||||||
}"
|
}"
|
||||||
fill="none"
|
:path="edgePath[0]"
|
||||||
:d="edgePath[0]"
|
|
||||||
/>
|
/>
|
||||||
<circle :cx="targetX" :cy="targetY" fill="black" :r="3" stroke="black" :stroke-width="1.5" />
|
<circle :cx="targetX" :cy="targetY" fill="black" :r="3" stroke="black" :stroke-width="1.5" />
|
||||||
</g>
|
</g>
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { GraphNode } from '@vue-flow/core'
|
import type { EdgeProps } from '@vue-flow/core'
|
||||||
import { getStraightPath } from '@vue-flow/core'
|
import { BaseEdge, getStraightPath } from '@vue-flow/core'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
|
||||||
import { getEdgeParams } from './utils'
|
import { getEdgeParams } from './utils'
|
||||||
|
|
||||||
const props = defineProps<{ id: string; sourceNode: GraphNode; targetNode: GraphNode; markerEnd: string; style: any }>()
|
const props = defineProps<EdgeProps>()
|
||||||
|
|
||||||
const edgeParams = computed(() => getEdgeParams(props.sourceNode, props.targetNode))
|
const edgeParams = computed(() => getEdgeParams(props.sourceNode, props.targetNode))
|
||||||
|
|
||||||
@@ -20,5 +20,5 @@ const edgePath = computed(() =>
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<path :id="id" class="vue-flow__edge-path" :d="edgePath[0]" :marker-end="markerEnd" :style="style" />
|
<BaseEdge :id="id" :path="edgePath[0]" :marker-end="markerEnd" :style="style" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
import { computed, defineComponent, h, inject } from 'vue'
|
import { computed, defineComponent, h, inject } from 'vue'
|
||||||
import type { HandleElement } from '../../types'
|
import type { HandleElement } from '../../types'
|
||||||
import { ConnectionLineType, ConnectionMode, Position } from '../../types'
|
import { ConnectionLineType, ConnectionMode, Position } from '../../types'
|
||||||
import { getHandlePosition, getMarkerId } from '../../utils'
|
import { getHandlePosition, getMarkerId, oppositePosition } from '../../utils'
|
||||||
import { useVueFlow } from '../../composables'
|
import { useVueFlow } from '../../composables'
|
||||||
import { Slots } from '../../context'
|
import { Slots } from '../../context'
|
||||||
import { getBezierPath, getSimpleBezierPath, getSmoothStepPath } from '../Edges/utils'
|
import { getBezierPath, getSimpleBezierPath, getSmoothStepPath } from '../Edges/utils'
|
||||||
|
|
||||||
const oppositePosition = {
|
|
||||||
[Position.Left]: Position.Right,
|
|
||||||
[Position.Right]: Position.Left,
|
|
||||||
[Position.Top]: Position.Bottom,
|
|
||||||
[Position.Bottom]: Position.Top,
|
|
||||||
}
|
|
||||||
|
|
||||||
const ConnectionLine = defineComponent({
|
const ConnectionLine = defineComponent({
|
||||||
name: 'ConnectionLine',
|
name: 'ConnectionLine',
|
||||||
compatConfig: { MODE: 3 },
|
compatConfig: { MODE: 3 },
|
||||||
@@ -57,7 +50,7 @@ const ConnectionLine = defineComponent({
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const startHandleId = connectionStartHandle.value.handleId
|
const startHandleId = connectionStartHandle.value.id
|
||||||
|
|
||||||
const handleType = connectionStartHandle.value.type
|
const handleType = connectionStartHandle.value.type
|
||||||
|
|
||||||
@@ -78,18 +71,18 @@ const ConnectionLine = defineComponent({
|
|||||||
const { x: fromX, y: fromY } = getHandlePosition(fromNode.value, fromHandle, fromPosition)
|
const { x: fromX, y: fromY } = getHandlePosition(fromNode.value, fromHandle, fromPosition)
|
||||||
|
|
||||||
let toHandle: HandleElement | null = null
|
let toHandle: HandleElement | null = null
|
||||||
if (toNode.value && connectionEndHandle.value?.handleId) {
|
if (toNode.value) {
|
||||||
// if connection mode is strict, we only look for handles of the opposite type
|
// if connection mode is strict, we only look for handles of the opposite type
|
||||||
if (connectionMode.value === ConnectionMode.Strict) {
|
if (connectionMode.value === ConnectionMode.Strict) {
|
||||||
toHandle =
|
toHandle =
|
||||||
toNode.value.handleBounds[handleType === 'source' ? 'target' : 'source']?.find(
|
toNode.value.handleBounds[handleType === 'source' ? 'target' : 'source']?.find(
|
||||||
(d) => d.id === connectionEndHandle.value?.handleId,
|
(d) => d.id === connectionEndHandle.value?.id,
|
||||||
) || null
|
) || null
|
||||||
} else {
|
} else {
|
||||||
// if connection mode is loose, look for the handle in both source and target bounds
|
// if connection mode is loose, look for the handle in both source and target bounds
|
||||||
toHandle =
|
toHandle =
|
||||||
[...(toNode.value.handleBounds.source || []), ...(toNode.value.handleBounds.target || [])]?.find(
|
[...(toNode.value.handleBounds.source || []), ...(toNode.value.handleBounds.target || [])]?.find(
|
||||||
(d) => d.id === connectionEndHandle.value?.handleId,
|
(d) => d.id === connectionEndHandle.value?.id,
|
||||||
) || null
|
) || null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
ErrorCode,
|
ErrorCode,
|
||||||
VueFlowError,
|
VueFlowError,
|
||||||
elementSelectionKeys,
|
elementSelectionKeys,
|
||||||
getHandle,
|
getEdgeHandle,
|
||||||
getHandlePosition,
|
getHandlePosition,
|
||||||
getMarkerId,
|
getMarkerId,
|
||||||
} from '../../utils'
|
} from '../../utils'
|
||||||
@@ -150,7 +150,7 @@ const EdgeWrapper = defineComponent({
|
|||||||
sourceNodeHandles = [...(sourceNode.handleBounds.source || []), ...(sourceNode.handleBounds.target || [])]
|
sourceNodeHandles = [...(sourceNode.handleBounds.source || []), ...(sourceNode.handleBounds.target || [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
const sourceHandle = getHandle(sourceNodeHandles, edge.value.sourceHandle)
|
const sourceHandle = getEdgeHandle(sourceNodeHandles, edge.value.sourceHandle)
|
||||||
|
|
||||||
let targetNodeHandles
|
let targetNodeHandles
|
||||||
if (connectionMode.value === ConnectionMode.Strict) {
|
if (connectionMode.value === ConnectionMode.Strict) {
|
||||||
@@ -159,7 +159,7 @@ const EdgeWrapper = defineComponent({
|
|||||||
targetNodeHandles = [...(targetNode.handleBounds.target || []), ...(targetNode.handleBounds.source || [])]
|
targetNodeHandles = [...(targetNode.handleBounds.target || []), ...(targetNode.handleBounds.source || [])]
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetHandle = getHandle(targetNodeHandles, edge.value.targetHandle)
|
const targetHandle = getEdgeHandle(targetNodeHandles, edge.value.targetHandle)
|
||||||
|
|
||||||
const sourcePosition = sourceHandle?.position || Position.Bottom
|
const sourcePosition = sourceHandle?.position || Position.Bottom
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const type = toRef(() => props.type ?? 'source')
|
|||||||
const isValidConnection = toRef(() => props.isValidConnection ?? null)
|
const isValidConnection = toRef(() => props.isValidConnection ?? null)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
id: flowId,
|
||||||
connectionStartHandle,
|
connectionStartHandle,
|
||||||
connectionClickStartHandle,
|
connectionClickStartHandle,
|
||||||
connectionEndHandle,
|
connectionEndHandle,
|
||||||
@@ -39,17 +40,17 @@ const isConnectableEnd = toRef(() => (typeof connectableEnd !== 'undefined' ? co
|
|||||||
const isConnecting = toRef(
|
const isConnecting = toRef(
|
||||||
() =>
|
() =>
|
||||||
(connectionStartHandle.value?.nodeId === nodeId &&
|
(connectionStartHandle.value?.nodeId === nodeId &&
|
||||||
connectionStartHandle.value?.handleId === handleId &&
|
connectionStartHandle.value?.id === handleId &&
|
||||||
connectionStartHandle.value?.type === type.value) ||
|
connectionStartHandle.value?.type === type.value) ||
|
||||||
(connectionEndHandle.value?.nodeId === nodeId &&
|
(connectionEndHandle.value?.nodeId === nodeId &&
|
||||||
connectionEndHandle.value?.handleId === handleId &&
|
connectionEndHandle.value?.id === handleId &&
|
||||||
connectionEndHandle.value?.type === type.value),
|
connectionEndHandle.value?.type === type.value),
|
||||||
)
|
)
|
||||||
|
|
||||||
const isClickConnecting = toRef(
|
const isClickConnecting = toRef(
|
||||||
() =>
|
() =>
|
||||||
connectionClickStartHandle.value?.nodeId === nodeId &&
|
connectionClickStartHandle.value?.nodeId === nodeId &&
|
||||||
connectionClickStartHandle.value?.handleId === handleId &&
|
connectionClickStartHandle.value?.id === handleId &&
|
||||||
connectionClickStartHandle.value?.type === type.value,
|
connectionClickStartHandle.value?.type === type.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -127,6 +128,8 @@ onMounted(() => {
|
|||||||
position,
|
position,
|
||||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||||
|
type: type.value,
|
||||||
|
nodeId,
|
||||||
...getDimensions(handle.value),
|
...getDimensions(handle.value),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +180,7 @@ export default {
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="handle"
|
ref="handle"
|
||||||
:data-id="`${nodeId}-${handleId}-${type}`"
|
:data-id="`${flowId}-${nodeId}-${handleId}-${type}`"
|
||||||
:data-handleid="handleId"
|
:data-handleid="handleId"
|
||||||
:data-nodeid="nodeId"
|
:data-nodeid="nodeId"
|
||||||
:data-handlepos="position"
|
:data-handlepos="position"
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
import type { MaybeRefOrGetter } from 'vue'
|
import type { MaybeRefOrGetter } from 'vue'
|
||||||
import { toValue } from 'vue'
|
import { toValue } from 'vue'
|
||||||
import type { Connection, ConnectionHandle, HandleType, MouseTouchEvent, ValidConnectionFunc, ValidHandleResult } from '../types'
|
import type { Connection, ConnectionInProgress, HandleElement, HandleType, MouseTouchEvent, ValidConnectionFunc } from '../types'
|
||||||
import {
|
import {
|
||||||
calcAutoPan,
|
calcAutoPan,
|
||||||
getClosestHandle,
|
getClosestHandle,
|
||||||
getConnectionStatus,
|
getConnectionStatus,
|
||||||
getEventPosition,
|
getEventPosition,
|
||||||
getHandleLookup,
|
getHandle,
|
||||||
|
getHandlePosition,
|
||||||
getHandleType,
|
getHandleType,
|
||||||
getHostForElement,
|
getHostForElement,
|
||||||
|
isConnectionValid,
|
||||||
isMouseEvent,
|
isMouseEvent,
|
||||||
isValidHandle,
|
isValidHandle,
|
||||||
|
oppositePosition,
|
||||||
pointToRendererPoint,
|
pointToRendererPoint,
|
||||||
rendererPointToPoint,
|
rendererPointToPoint,
|
||||||
resetRecentHandle,
|
resetRecentHandle,
|
||||||
@@ -49,6 +52,7 @@ export function useHandle({
|
|||||||
onEdgeUpdateEnd,
|
onEdgeUpdateEnd,
|
||||||
}: UseHandleProps) {
|
}: UseHandleProps) {
|
||||||
const {
|
const {
|
||||||
|
id: flowId,
|
||||||
vueFlowRef,
|
vueFlowRef,
|
||||||
connectionMode,
|
connectionMode,
|
||||||
connectionRadius,
|
connectionRadius,
|
||||||
@@ -67,12 +71,12 @@ export function useHandle({
|
|||||||
edges,
|
edges,
|
||||||
nodes,
|
nodes,
|
||||||
isValidConnection: isValidConnectionProp,
|
isValidConnection: isValidConnectionProp,
|
||||||
|
nodeLookup,
|
||||||
} = useVueFlow()
|
} = useVueFlow()
|
||||||
|
|
||||||
let connection: Connection | null = null
|
let connection: Connection | null = null
|
||||||
let isValid = false
|
let isValid: boolean | null = false
|
||||||
let handleDomNode: Element | null = null
|
let handleDomNode: Element | null = null
|
||||||
let previousConnection: ValidHandleResult | null = null
|
|
||||||
|
|
||||||
function handlePointerDown(event: MouseTouchEvent) {
|
function handlePointerDown(event: MouseTouchEvent) {
|
||||||
const isTarget = toValue(type) === 'target'
|
const isTarget = toValue(type) === 'target'
|
||||||
@@ -91,7 +95,7 @@ export function useHandle({
|
|||||||
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
|
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
|
||||||
}
|
}
|
||||||
|
|
||||||
let closestHandle: ConnectionHandle | null
|
let closestHandle: HandleElement | null
|
||||||
|
|
||||||
let autoPanId = 0
|
let autoPanId = 0
|
||||||
|
|
||||||
@@ -104,17 +108,16 @@ export function useHandle({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fromHandleInternal = getHandle(toValue(nodeId), handleType, toValue(handleId), nodeLookup.value, connectionMode.value)
|
||||||
|
|
||||||
|
if (!fromHandleInternal) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let prevActiveHandle: Element
|
let prevActiveHandle: Element
|
||||||
let connectionPosition = getEventPosition(event, containerBounds)
|
let connectionPosition = getEventPosition(event, containerBounds)
|
||||||
let autoPanStarted = false
|
let autoPanStarted = false
|
||||||
|
|
||||||
const handleLookup = getHandleLookup({
|
|
||||||
nodes: nodes.value,
|
|
||||||
nodeId: toValue(nodeId),
|
|
||||||
handleId: toValue(handleId),
|
|
||||||
handleType,
|
|
||||||
})
|
|
||||||
|
|
||||||
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
|
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
|
||||||
const autoPan = () => {
|
const autoPan = () => {
|
||||||
if (!autoPanOnConnect.value) {
|
if (!autoPanOnConnect.value) {
|
||||||
@@ -127,10 +130,37 @@ export function useHandle({
|
|||||||
autoPanId = requestAnimationFrame(autoPan)
|
autoPanId = requestAnimationFrame(autoPan)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stays the same for all consecutive pointermove events
|
||||||
|
const fromHandle: HandleElement = {
|
||||||
|
...fromHandleInternal,
|
||||||
|
nodeId: toValue(nodeId),
|
||||||
|
type: handleType,
|
||||||
|
position: fromHandleInternal.position,
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromNodeInternal = nodeLookup.value.get(toValue(nodeId))!
|
||||||
|
|
||||||
|
const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true)
|
||||||
|
|
||||||
|
const newConnection: ConnectionInProgress = {
|
||||||
|
inProgress: true,
|
||||||
|
isValid: null,
|
||||||
|
|
||||||
|
from,
|
||||||
|
fromHandle,
|
||||||
|
fromPosition: fromHandle.position,
|
||||||
|
fromNode: fromNodeInternal,
|
||||||
|
|
||||||
|
to: connectionPosition,
|
||||||
|
toHandle: null,
|
||||||
|
toPosition: oppositePosition[fromHandle.position],
|
||||||
|
toNode: null,
|
||||||
|
}
|
||||||
|
|
||||||
startConnection(
|
startConnection(
|
||||||
{
|
{
|
||||||
nodeId: toValue(nodeId),
|
nodeId: toValue(nodeId),
|
||||||
handleId: toValue(handleId),
|
id: toValue(handleId),
|
||||||
type: handleType,
|
type: handleType,
|
||||||
position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top,
|
position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top,
|
||||||
},
|
},
|
||||||
@@ -142,52 +172,71 @@ export function useHandle({
|
|||||||
|
|
||||||
emits.connectStart({ event, nodeId: toValue(nodeId), handleId: toValue(handleId), handleType })
|
emits.connectStart({ event, nodeId: toValue(nodeId), handleId: toValue(handleId), handleType })
|
||||||
|
|
||||||
|
let previousConnection: ConnectionInProgress = newConnection
|
||||||
|
|
||||||
function onPointerMove(event: MouseTouchEvent) {
|
function onPointerMove(event: MouseTouchEvent) {
|
||||||
connectionPosition = getEventPosition(event, containerBounds)
|
connectionPosition = getEventPosition(event, containerBounds)
|
||||||
|
|
||||||
const { handle, validHandleResult } = getClosestHandle(
|
closestHandle = getClosestHandle(
|
||||||
event,
|
|
||||||
doc,
|
|
||||||
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
|
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
|
||||||
connectionRadius.value,
|
connectionRadius.value,
|
||||||
handleLookup,
|
nodeLookup.value,
|
||||||
(handle) =>
|
fromHandle,
|
||||||
isValidHandle(
|
|
||||||
event,
|
|
||||||
handle,
|
|
||||||
connectionMode.value,
|
|
||||||
toValue(nodeId),
|
|
||||||
toValue(handleId),
|
|
||||||
isTarget ? 'target' : 'source',
|
|
||||||
isValidConnectionHandler,
|
|
||||||
doc,
|
|
||||||
edges.value,
|
|
||||||
nodes.value,
|
|
||||||
findNode,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
closestHandle = handle
|
|
||||||
|
|
||||||
if (!autoPanStarted) {
|
if (!autoPanStarted) {
|
||||||
autoPan()
|
autoPan()
|
||||||
autoPanStarted = true
|
autoPanStarted = true
|
||||||
}
|
}
|
||||||
|
|
||||||
connection = validHandleResult.connection
|
const result = isValidHandle(
|
||||||
isValid = validHandleResult.isValid
|
event,
|
||||||
handleDomNode = validHandleResult.handleDomNode
|
{
|
||||||
|
handle: closestHandle,
|
||||||
|
connectionMode: connectionMode.value,
|
||||||
|
fromNodeId: toValue(nodeId),
|
||||||
|
fromHandleId: toValue(handleId),
|
||||||
|
fromType: isTarget ? 'target' : 'source',
|
||||||
|
isValidConnection: isValidConnectionHandler,
|
||||||
|
doc,
|
||||||
|
lib: 'vue',
|
||||||
|
flowId,
|
||||||
|
nodeLookup: nodeLookup.value,
|
||||||
|
},
|
||||||
|
edges.value,
|
||||||
|
nodes.value,
|
||||||
|
findNode,
|
||||||
|
)
|
||||||
|
|
||||||
|
handleDomNode = result.handleDomNode
|
||||||
|
connection = result.connection
|
||||||
|
isValid = isConnectionValid(!!closestHandle, result.isValid)
|
||||||
|
|
||||||
|
const newConnection: ConnectionInProgress = {
|
||||||
|
// from stays the same
|
||||||
|
...previousConnection,
|
||||||
|
isValid,
|
||||||
|
to:
|
||||||
|
closestHandle && isValid
|
||||||
|
? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, viewport.value)
|
||||||
|
: connectionPosition,
|
||||||
|
toHandle: result.toHandle,
|
||||||
|
toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
|
||||||
|
toNode: result.toHandle ? nodeLookup.value.get(result.toHandle.nodeId)! : null,
|
||||||
|
}
|
||||||
|
|
||||||
// we don't want to trigger an update when the connection
|
// we don't want to trigger an update when the connection
|
||||||
// is snapped to the same handle as before
|
// is snapped to the same handle as before
|
||||||
if (
|
if (
|
||||||
isValid &&
|
isValid &&
|
||||||
closestHandle &&
|
closestHandle &&
|
||||||
previousConnection?.endHandle &&
|
previousConnection?.toHandle &&
|
||||||
validHandleResult.endHandle &&
|
newConnection.toHandle &&
|
||||||
previousConnection.endHandle.type === validHandleResult.endHandle.type &&
|
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
||||||
previousConnection.endHandle.nodeId === validHandleResult.endHandle.nodeId &&
|
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
||||||
previousConnection.endHandle.handleId === validHandleResult.endHandle.handleId
|
previousConnection.toHandle.id === newConnection.toHandle.id &&
|
||||||
|
previousConnection.to.x === newConnection.to.x &&
|
||||||
|
previousConnection.to.y === newConnection.to.y
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -202,11 +251,11 @@ export function useHandle({
|
|||||||
viewport.value,
|
viewport.value,
|
||||||
)
|
)
|
||||||
: connectionPosition,
|
: connectionPosition,
|
||||||
validHandleResult.endHandle,
|
result.toHandle,
|
||||||
getConnectionStatus(!!closestHandle, isValid),
|
getConnectionStatus(!!closestHandle, isValid),
|
||||||
)
|
)
|
||||||
|
|
||||||
previousConnection = validHandleResult
|
previousConnection = newConnection
|
||||||
|
|
||||||
if (!closestHandle && !isValid && !handleDomNode) {
|
if (!closestHandle && !isValid && !handleDomNode) {
|
||||||
return resetRecentHandle(prevActiveHandle)
|
return resetRecentHandle(prevActiveHandle)
|
||||||
@@ -219,9 +268,9 @@ export function useHandle({
|
|||||||
|
|
||||||
// todo: remove `vue-flow__handle-connecting` in next major version
|
// todo: remove `vue-flow__handle-connecting` in next major version
|
||||||
handleDomNode.classList.add('connecting', 'vue-flow__handle-connecting')
|
handleDomNode.classList.add('connecting', 'vue-flow__handle-connecting')
|
||||||
handleDomNode.classList.toggle('valid', isValid)
|
handleDomNode.classList.toggle('valid', !!isValid)
|
||||||
// todo: remove this in next major version
|
// todo: remove this in next major version
|
||||||
handleDomNode.classList.toggle('vue-flow__handle-valid', isValid)
|
handleDomNode.classList.toggle('vue-flow__handle-valid', !!isValid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,8 +324,15 @@ export function useHandle({
|
|||||||
if (!connectionClickStartHandle.value) {
|
if (!connectionClickStartHandle.value) {
|
||||||
emits.clickConnectStart({ event, nodeId: toValue(nodeId), handleId: toValue(handleId) })
|
emits.clickConnectStart({ event, nodeId: toValue(nodeId), handleId: toValue(handleId) })
|
||||||
|
|
||||||
startConnection({ nodeId: toValue(nodeId), type: toValue(type), handleId: toValue(handleId) }, undefined, true)
|
startConnection(
|
||||||
} else {
|
{ nodeId: toValue(nodeId), type: toValue(type), id: toValue(handleId), position: Position.Top },
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let isValidConnectionHandler = toValue(isValidConnection) || isValidConnectionProp.value || alwaysValid
|
let isValidConnectionHandler = toValue(isValidConnection) || isValidConnectionProp.value || alwaysValid
|
||||||
|
|
||||||
const node = findNode(toValue(nodeId))
|
const node = findNode(toValue(nodeId))
|
||||||
@@ -291,35 +347,40 @@ export function useHandle({
|
|||||||
|
|
||||||
const doc = getHostForElement(event.target as HTMLElement)
|
const doc = getHostForElement(event.target as HTMLElement)
|
||||||
|
|
||||||
const { connection, isValid } = isValidHandle(
|
const result = isValidHandle(
|
||||||
event,
|
event,
|
||||||
{
|
{
|
||||||
|
handle: {
|
||||||
nodeId: toValue(nodeId),
|
nodeId: toValue(nodeId),
|
||||||
id: toValue(handleId),
|
id: toValue(handleId),
|
||||||
type: toValue(type),
|
type: toValue(type),
|
||||||
|
position: Position.Top,
|
||||||
},
|
},
|
||||||
connectionMode.value,
|
connectionMode: connectionMode.value,
|
||||||
connectionClickStartHandle.value.nodeId,
|
fromNodeId: connectionClickStartHandle.value.nodeId,
|
||||||
connectionClickStartHandle.value.handleId || null,
|
fromHandleId: connectionClickStartHandle.value.id || null,
|
||||||
connectionClickStartHandle.value.type,
|
fromType: connectionClickStartHandle.value.type,
|
||||||
isValidConnectionHandler,
|
isValidConnection: isValidConnectionHandler,
|
||||||
doc,
|
doc,
|
||||||
|
lib: 'vue',
|
||||||
|
flowId,
|
||||||
|
nodeLookup: nodeLookup.value,
|
||||||
|
},
|
||||||
edges.value,
|
edges.value,
|
||||||
nodes.value,
|
nodes.value,
|
||||||
findNode,
|
findNode,
|
||||||
)
|
)
|
||||||
|
|
||||||
const isOwnHandle = connection.source === connection.target
|
const isOwnHandle = result.connection?.source === result.connection?.target
|
||||||
|
|
||||||
if (isValid && !isOwnHandle) {
|
if (result.isValid && result.connection && !isOwnHandle) {
|
||||||
emits.connect(connection)
|
emits.connect(result.connection)
|
||||||
}
|
}
|
||||||
|
|
||||||
emits.clickConnectEnd(event)
|
emits.clickConnectEnd(event)
|
||||||
|
|
||||||
endConnection(event, true)
|
endConnection(event, true)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handlePointerDown,
|
handlePointerDown,
|
||||||
|
|||||||
@@ -157,8 +157,8 @@ export function useActions(state: State, nodeLookup: ComputedRef<NodeLookup>, ed
|
|||||||
if (doUpdate) {
|
if (doUpdate) {
|
||||||
const nodeBounds = update.nodeElement.getBoundingClientRect()
|
const nodeBounds = update.nodeElement.getBoundingClientRect()
|
||||||
node.dimensions = dimensions
|
node.dimensions = dimensions
|
||||||
node.handleBounds.source = getHandleBounds('.source', update.nodeElement, nodeBounds, zoom)
|
node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom)
|
||||||
node.handleBounds.target = getHandleBounds('.target', update.nodeElement, nodeBounds, zoom)
|
node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom)
|
||||||
|
|
||||||
changes.push({
|
changes.push({
|
||||||
id: node.id,
|
id: node.id,
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import type { Dimensions, Position, XYPosition } from './flow'
|
import type { Dimensions, Position, XYPosition } from './flow'
|
||||||
import type { Connection } from './connection'
|
import type { Connection, ConnectionMode } from './connection'
|
||||||
import type { GraphEdge } from './edge'
|
import type { GraphEdge } from './edge'
|
||||||
import type { GraphNode } from './node'
|
import type { GraphNode } from './node'
|
||||||
|
import type { NodeLookup } from './store'
|
||||||
|
|
||||||
export type HandleType = 'source' | 'target'
|
export type HandleType = 'source' | 'target'
|
||||||
|
|
||||||
export interface HandleElement extends XYPosition, Dimensions {
|
export interface HandleElement extends XYPosition, Dimensions {
|
||||||
id?: string | null
|
id?: string | null
|
||||||
position: Position
|
position: Position
|
||||||
|
type: HandleType
|
||||||
|
nodeId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ConnectionHandle extends XYPosition {
|
export interface ConnectionHandle extends XYPosition {
|
||||||
@@ -16,18 +19,11 @@ export interface ConnectionHandle extends XYPosition {
|
|||||||
nodeId: string
|
nodeId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ValidHandleResult {
|
|
||||||
endHandle: ConnectingHandle | null
|
|
||||||
handleDomNode: Element | null
|
|
||||||
isValid: boolean
|
|
||||||
connection: Connection
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ConnectingHandle {
|
export interface ConnectingHandle {
|
||||||
nodeId: string
|
nodeId: string
|
||||||
type: HandleType
|
type: HandleType
|
||||||
handleId?: string | null
|
id?: string | null
|
||||||
position?: Position | null
|
position: Position
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A valid connection function can determine if an attempted connection is valid or not, i.e. abort creating a new edge */
|
/** A valid connection function can determine if an attempted connection is valid or not, i.e. abort creating a new edge */
|
||||||
@@ -63,3 +59,36 @@ export interface HandleProps {
|
|||||||
/** Can this handle be used to *end* a connection */
|
/** Can this handle be used to *end* a connection */
|
||||||
connectableEnd?: boolean
|
connectableEnd?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IsValidParams {
|
||||||
|
handle: ConnectingHandle | null
|
||||||
|
connectionMode: ConnectionMode
|
||||||
|
fromNodeId: string
|
||||||
|
fromHandleId: string | null
|
||||||
|
fromType: HandleType
|
||||||
|
isValidConnection?: ValidConnectionFunc
|
||||||
|
doc: Document | ShadowRoot
|
||||||
|
lib: string
|
||||||
|
flowId: string | null
|
||||||
|
nodeLookup: NodeLookup
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Result {
|
||||||
|
handleDomNode: Element | null
|
||||||
|
isValid: boolean
|
||||||
|
connection: Connection | null
|
||||||
|
toHandle: ConnectingHandle | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConnectionInProgress<NodeType extends GraphNode = GraphNode> {
|
||||||
|
inProgress: true
|
||||||
|
isValid: boolean | null
|
||||||
|
from: XYPosition
|
||||||
|
fromHandle: HandleElement
|
||||||
|
fromPosition: Position
|
||||||
|
fromNode: NodeType
|
||||||
|
to: XYPosition
|
||||||
|
toHandle: ConnectingHandle | null
|
||||||
|
toPosition: Position
|
||||||
|
toNode: NodeType | null
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,41 +6,36 @@ export function getHandlePosition(
|
|||||||
node: GraphNode,
|
node: GraphNode,
|
||||||
handle: HandleElement | null,
|
handle: HandleElement | null,
|
||||||
fallbackPosition: Position = Position.Left,
|
fallbackPosition: Position = Position.Left,
|
||||||
|
center = false,
|
||||||
): XYPosition {
|
): XYPosition {
|
||||||
const x = (handle?.x ?? 0) + node.computedPosition.x
|
const x = (handle?.x ?? 0) + node.computedPosition.x
|
||||||
const y = (handle?.y ?? 0) + node.computedPosition.y
|
const y = (handle?.y ?? 0) + node.computedPosition.y
|
||||||
const { width, height } = handle ?? getNodeDimensions(node)
|
const { width, height } = handle ?? getNodeDimensions(node)
|
||||||
|
|
||||||
|
if (center) {
|
||||||
|
return { x: x + width / 2, y: y + height / 2 }
|
||||||
|
}
|
||||||
|
|
||||||
const position = handle?.position ?? fallbackPosition
|
const position = handle?.position ?? fallbackPosition
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
case Position.Top:
|
case Position.Top:
|
||||||
return {
|
return { x: x + width / 2, y }
|
||||||
x: x + width / 2,
|
|
||||||
y,
|
|
||||||
}
|
|
||||||
case Position.Right:
|
case Position.Right:
|
||||||
return {
|
return { x: x + width, y: y + height / 2 }
|
||||||
x: x + width,
|
|
||||||
y: y + height / 2,
|
|
||||||
}
|
|
||||||
case Position.Bottom:
|
case Position.Bottom:
|
||||||
return {
|
return { x: x + width / 2, y: y + height }
|
||||||
x: x + width / 2,
|
|
||||||
y: y + height,
|
|
||||||
}
|
|
||||||
case Position.Left:
|
case Position.Left:
|
||||||
return {
|
return { x, y: y + height / 2 }
|
||||||
x,
|
|
||||||
y: y + height / 2,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHandle(bounds: HandleElement[] = [], handleId?: string | null): HandleElement | null {
|
export function getEdgeHandle(bounds: HandleElement[] | undefined, handleId?: string | null): HandleElement | null {
|
||||||
if (!bounds.length) {
|
if (!bounds) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if no handleId is given, we use the first handle, otherwise we check for the id
|
||||||
return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null
|
return (!handleId ? bounds[0] : bounds.find((d) => d.id === handleId)) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ConnectionMode } from '../types'
|
import { ConnectionMode, Position } from '../types'
|
||||||
import type {
|
import type {
|
||||||
Actions,
|
Actions,
|
||||||
Connection,
|
Connection,
|
||||||
@@ -6,23 +6,17 @@ import type {
|
|||||||
ConnectionStatus,
|
ConnectionStatus,
|
||||||
GraphEdge,
|
GraphEdge,
|
||||||
GraphNode,
|
GraphNode,
|
||||||
|
HandleElement,
|
||||||
HandleType,
|
HandleType,
|
||||||
|
IsValidParams,
|
||||||
NodeHandleBounds,
|
NodeHandleBounds,
|
||||||
Position,
|
NodeLookup,
|
||||||
ValidConnectionFunc,
|
Result,
|
||||||
ValidHandleResult,
|
|
||||||
XYPosition,
|
XYPosition,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { getEventPosition, getHandlePosition } from '.'
|
import { getEventPosition, getHandlePosition, getOverlappingArea, nodeToRect } from '.'
|
||||||
|
|
||||||
function defaultValidHandleResult(): ValidHandleResult {
|
const alwaysValid = () => true
|
||||||
return {
|
|
||||||
handleDomNode: null,
|
|
||||||
isValid: false,
|
|
||||||
connection: { source: '', target: '', sourceHandle: null, targetHandle: null },
|
|
||||||
endHandle: null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function resetRecentHandle(handleDomNode: Element): void {
|
export function resetRecentHandle(handleDomNode: Element): void {
|
||||||
handleDomNode?.classList.remove('valid', 'connecting', 'vue-flow__handle-valid', 'vue-flow__handle-connecting')
|
handleDomNode?.classList.remove('valid', 'connecting', 'vue-flow__handle-valid', 'vue-flow__handle-connecting')
|
||||||
@@ -55,126 +49,126 @@ export function getHandles(
|
|||||||
return connectionHandles
|
return connectionHandles
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getNodesWithinDistance(position: XYPosition, nodeLookup: NodeLookup, distance: number): GraphNode[] {
|
||||||
|
const nodes: GraphNode[] = []
|
||||||
|
const rect = {
|
||||||
|
x: position.x - distance,
|
||||||
|
y: position.y - distance,
|
||||||
|
width: distance * 2,
|
||||||
|
height: distance * 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of nodeLookup.values()) {
|
||||||
|
if (getOverlappingArea(rect, nodeToRect(node)) > 0) {
|
||||||
|
nodes.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
// this distance is used for the area around the user pointer
|
||||||
|
// while doing a connection for finding the closest nodes
|
||||||
|
const ADDITIONAL_DISTANCE = 250
|
||||||
|
|
||||||
export function getClosestHandle(
|
export function getClosestHandle(
|
||||||
event: MouseEvent | TouchEvent,
|
position: XYPosition,
|
||||||
doc: Document | ShadowRoot,
|
|
||||||
pos: XYPosition,
|
|
||||||
connectionRadius: number,
|
connectionRadius: number,
|
||||||
handles: ConnectionHandle[],
|
nodeLookup: NodeLookup,
|
||||||
validator: (handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'>) => ValidHandleResult,
|
fromHandle: { nodeId: string; type: HandleType; id?: string | null },
|
||||||
) {
|
): HandleElement | null {
|
||||||
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
let closestHandles: HandleElement[] = []
|
||||||
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
|
||||||
const { x, y } = getEventPosition(event)
|
|
||||||
const domNodes = doc.elementsFromPoint(x, y)
|
|
||||||
|
|
||||||
const handleBelow = domNodes.find((el) => el.classList.contains('vue-flow__handle'))
|
|
||||||
|
|
||||||
if (handleBelow) {
|
|
||||||
const handleNodeId = handleBelow.getAttribute('data-nodeid')
|
|
||||||
|
|
||||||
if (handleNodeId) {
|
|
||||||
const handleType = getHandleType(undefined, handleBelow)
|
|
||||||
const handleId = handleBelow.getAttribute('data-handleid')
|
|
||||||
const validHandleResult = validator({ nodeId: handleNodeId, id: handleId, type: handleType })
|
|
||||||
|
|
||||||
if (validHandleResult) {
|
|
||||||
const handle = handles.find((h) => h.nodeId === handleNodeId && h.type === handleType && h.id === handleId)
|
|
||||||
|
|
||||||
return {
|
|
||||||
handle: {
|
|
||||||
id: handleId,
|
|
||||||
type: handleType,
|
|
||||||
nodeId: handleNodeId,
|
|
||||||
x: handle?.x || pos.x,
|
|
||||||
y: handle?.y || pos.y,
|
|
||||||
},
|
|
||||||
validHandleResult,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if we couldn't find a handle below the mouse cursor we look for the closest distance based on the connectionRadius
|
|
||||||
let closestHandles: { handle: ConnectionHandle; validHandleResult: ValidHandleResult }[] = []
|
|
||||||
let minDistance = Number.POSITIVE_INFINITY
|
let minDistance = Number.POSITIVE_INFINITY
|
||||||
|
|
||||||
for (const handle of handles) {
|
const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE)
|
||||||
const distance = Math.sqrt((handle.x - pos.x) ** 2 + (handle.y - pos.y) ** 2)
|
|
||||||
|
|
||||||
if (distance <= connectionRadius) {
|
for (const node of closeNodes) {
|
||||||
const validHandleResult = validator(handle)
|
const allHandles = [...(node.handleBounds?.source ?? []), ...(node.handleBounds?.target ?? [])]
|
||||||
|
|
||||||
if (distance <= minDistance) {
|
for (const handle of allHandles) {
|
||||||
if (distance < minDistance) {
|
// if the handle is the same as the fromHandle we skip it
|
||||||
closestHandles = [{ handle, validHandleResult }]
|
if (fromHandle.nodeId === handle.nodeId && fromHandle.type === handle.type && fromHandle.id === handle.id) {
|
||||||
} else if (distance === minDistance) {
|
continue
|
||||||
// when multiple handles are on the same distance we collect all of them
|
|
||||||
closestHandles.push({
|
|
||||||
handle,
|
|
||||||
validHandleResult,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// determine absolute position of the handle
|
||||||
|
const { x, y } = getHandlePosition(node, handle, handle.position, true)
|
||||||
|
|
||||||
|
const distance = Math.sqrt((x - position.x) ** 2 + (y - position.y) ** 2)
|
||||||
|
if (distance > connectionRadius) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distance < minDistance) {
|
||||||
|
closestHandles = [{ ...handle, x, y }]
|
||||||
minDistance = distance
|
minDistance = distance
|
||||||
|
} else if (distance === minDistance) {
|
||||||
|
// when multiple handles are on the same distance we collect all of them
|
||||||
|
closestHandles.push({ ...handle, x, y })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!closestHandles.length) {
|
if (!closestHandles.length) {
|
||||||
return { handle: null, validHandleResult: defaultValidHandleResult() }
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// when multiple handles overlay each other we prefer the opposite handle
|
||||||
|
if (closestHandles.length > 1) {
|
||||||
|
const oppositeHandleType = fromHandle.type === 'source' ? 'target' : 'source'
|
||||||
|
return closestHandles.find((handle) => handle.type === oppositeHandleType) ?? closestHandles[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (closestHandles.length === 1) {
|
|
||||||
return closestHandles[0]
|
return closestHandles[0]
|
||||||
}
|
|
||||||
|
|
||||||
const hasValidHandle = closestHandles.some(({ validHandleResult }) => validHandleResult.isValid)
|
|
||||||
const hasTargetHandle = closestHandles.some(({ handle }) => handle.type === 'target')
|
|
||||||
|
|
||||||
// if multiple handles are layout on top of each other we prefer the one with type = target and the one that is valid
|
|
||||||
return (
|
|
||||||
closestHandles.find(({ handle, validHandleResult }) =>
|
|
||||||
hasTargetHandle ? handle.type === 'target' : hasValidHandle ? validHandleResult.isValid : true,
|
|
||||||
) || closestHandles[0]
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// checks if and returns connection in fom of an object { source: 123, target: 312 }
|
// checks if and returns connection in form of an object { source: 123, target: 312 }
|
||||||
export function isValidHandle(
|
export function isValidHandle(
|
||||||
event: MouseEvent | TouchEvent,
|
event: MouseEvent | TouchEvent,
|
||||||
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
|
{
|
||||||
connectionMode: ConnectionMode,
|
handle,
|
||||||
fromNodeId: string,
|
connectionMode,
|
||||||
fromHandleId: string | null,
|
fromNodeId,
|
||||||
fromType: HandleType,
|
fromHandleId,
|
||||||
isValidConnection: ValidConnectionFunc,
|
fromType,
|
||||||
doc: Document | ShadowRoot,
|
doc,
|
||||||
|
lib,
|
||||||
|
flowId,
|
||||||
|
isValidConnection = alwaysValid,
|
||||||
|
}: IsValidParams,
|
||||||
edges: GraphEdge[],
|
edges: GraphEdge[],
|
||||||
nodes: GraphNode[],
|
nodes: GraphNode[],
|
||||||
findNode: Actions['findNode'],
|
findNode: Actions['findNode'],
|
||||||
) {
|
) {
|
||||||
const isTarget = fromType === 'target'
|
const isTarget = fromType === 'target'
|
||||||
|
const handleDomNode = handle
|
||||||
|
? doc.querySelector(`.${lib}-flow__handle[data-id="${flowId}-${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
|
||||||
|
: null
|
||||||
|
|
||||||
const handleDomNode = doc.querySelector(`.vue-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`)
|
|
||||||
const { x, y } = getEventPosition(event)
|
const { x, y } = getEventPosition(event)
|
||||||
const elementBelow = doc.elementFromPoint(x, y)
|
const handleBelow = doc.elementFromPoint(x, y)
|
||||||
|
|
||||||
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
// we always want to prioritize the handle below the mouse cursor over the closest distance handle,
|
||||||
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
// because it could be that the center of another handle is closer to the mouse pointer than the handle below the cursor
|
||||||
const handleToCheck = elementBelow?.classList.contains('vue-flow__handle') ? elementBelow : handleDomNode
|
const handleToCheck = handleBelow?.classList.contains(`${lib}-flow__handle`) ? handleBelow : handleDomNode
|
||||||
|
|
||||||
const result = defaultValidHandleResult()
|
const result: Result = {
|
||||||
|
handleDomNode: handleToCheck,
|
||||||
|
isValid: false,
|
||||||
|
connection: null,
|
||||||
|
toHandle: null,
|
||||||
|
}
|
||||||
|
|
||||||
if (handleToCheck) {
|
if (handleToCheck) {
|
||||||
result.handleDomNode = handleToCheck
|
|
||||||
|
|
||||||
const handleType = getHandleType(undefined, handleToCheck)
|
const handleType = getHandleType(undefined, handleToCheck)
|
||||||
const handleNodeId = handleToCheck.getAttribute('data-nodeid')!
|
const handleNodeId = handleToCheck.getAttribute('data-nodeid')
|
||||||
const handleId = handleToCheck.getAttribute('data-handleid')
|
const handleId = handleToCheck.getAttribute('data-handleid')
|
||||||
const connectable = handleToCheck.classList.contains('connectable')
|
const connectable = handleToCheck.classList.contains('connectable')
|
||||||
const connectableEnd = handleToCheck.classList.contains('connectableend')
|
const connectableEnd = handleToCheck.classList.contains('connectableend')
|
||||||
|
|
||||||
|
if (!handleNodeId || !handleType) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
const connection: Connection = {
|
const connection: Connection = {
|
||||||
source: isTarget ? handleNodeId : fromNodeId,
|
source: isTarget ? handleNodeId : fromNodeId,
|
||||||
sourceHandle: isTarget ? handleId : fromHandleId,
|
sourceHandle: isTarget ? handleId : fromHandleId,
|
||||||
@@ -185,7 +179,6 @@ export function isValidHandle(
|
|||||||
result.connection = connection
|
result.connection = connection
|
||||||
|
|
||||||
const isConnectable = connectable && connectableEnd
|
const isConnectable = connectable && connectableEnd
|
||||||
|
|
||||||
// in strict mode we don't allow target to target or source to source connections
|
// in strict mode we don't allow target to target or source to source connections
|
||||||
const isValid =
|
const isValid =
|
||||||
isConnectable &&
|
isConnectable &&
|
||||||
@@ -193,54 +186,21 @@ export function isValidHandle(
|
|||||||
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
||||||
: handleNodeId !== fromNodeId || handleId !== fromHandleId)
|
: handleNodeId !== fromNodeId || handleId !== fromHandleId)
|
||||||
|
|
||||||
if (isValid) {
|
result.isValid =
|
||||||
result.isValid = isValidConnection(connection, {
|
isValid &&
|
||||||
edges,
|
isValidConnection(connection, {
|
||||||
nodes,
|
nodes,
|
||||||
sourceNode: findNode(connection.source)!,
|
edges,
|
||||||
targetNode: findNode(connection.target)!,
|
sourceNode: findNode(fromNodeId)!,
|
||||||
|
targetNode: findNode(handleNodeId)!,
|
||||||
})
|
})
|
||||||
|
|
||||||
result.endHandle = {
|
result.toHandle = handle
|
||||||
nodeId: handleNodeId,
|
|
||||||
handleId,
|
|
||||||
type: handleType as HandleType,
|
|
||||||
position: result.isValid ? (handleToCheck.getAttribute('data-handlepos') as Position) : null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GetHandleLookupParams {
|
|
||||||
nodes: GraphNode[]
|
|
||||||
nodeId: string
|
|
||||||
handleId: string | null
|
|
||||||
handleType: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
|
|
||||||
const handleLookup: ConnectionHandle[] = []
|
|
||||||
for (let i = 0; i < nodes.length; i++) {
|
|
||||||
const node = nodes[i]
|
|
||||||
|
|
||||||
const { handleBounds } = node
|
|
||||||
|
|
||||||
let sourceHandles: ConnectionHandle[] = []
|
|
||||||
let targetHandles: ConnectionHandle[] = []
|
|
||||||
|
|
||||||
if (handleBounds) {
|
|
||||||
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`)
|
|
||||||
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLookup.push(...sourceHandles, ...targetHandles)
|
|
||||||
}
|
|
||||||
|
|
||||||
return handleLookup
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDomNode: Element | null): HandleType | null {
|
export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDomNode: Element | null): HandleType | null {
|
||||||
if (edgeUpdaterType) {
|
if (edgeUpdaterType) {
|
||||||
return edgeUpdaterType
|
return edgeUpdaterType
|
||||||
@@ -253,7 +213,7 @@ export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDom
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean | null) {
|
||||||
let connectionStatus: ConnectionStatus | null = null
|
let connectionStatus: ConnectionStatus | null = null
|
||||||
|
|
||||||
if (isHandleValid) {
|
if (isHandleValid) {
|
||||||
@@ -264,3 +224,44 @@ export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleV
|
|||||||
|
|
||||||
return connectionStatus
|
return connectionStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isConnectionValid(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
||||||
|
let isValid: boolean | null = null
|
||||||
|
|
||||||
|
if (isHandleValid) {
|
||||||
|
isValid = true
|
||||||
|
} else if (isInsideConnectionRadius && !isHandleValid) {
|
||||||
|
isValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHandle(
|
||||||
|
nodeId: string,
|
||||||
|
handleType: HandleType,
|
||||||
|
handleId: string | null,
|
||||||
|
nodeLookup: NodeLookup,
|
||||||
|
connectionMode: ConnectionMode,
|
||||||
|
withAbsolutePosition = false,
|
||||||
|
): HandleElement | null {
|
||||||
|
const node = nodeLookup.get(nodeId)
|
||||||
|
if (!node) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const handles =
|
||||||
|
connectionMode === ConnectionMode.Strict
|
||||||
|
? node.handleBounds?.[handleType]
|
||||||
|
: [...(node.handleBounds?.source ?? []), ...(node.handleBounds?.target ?? [])]
|
||||||
|
const handle = (handleId ? handles?.find((h) => h.id === handleId) : handles?.[0]) ?? null
|
||||||
|
|
||||||
|
return handle && withAbsolutePosition ? { ...handle, ...getHandlePosition(node, handle, handle.position, true) } : handle
|
||||||
|
}
|
||||||
|
|
||||||
|
export const oppositePosition = {
|
||||||
|
[Position.Left]: Position.Right,
|
||||||
|
[Position.Right]: Position.Left,
|
||||||
|
[Position.Top]: Position.Bottom,
|
||||||
|
[Position.Bottom]: Position.Top,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
import { nextTick } from 'vue'
|
import { nextTick } from 'vue'
|
||||||
import type { Actions, GraphNode, HandleElement, Position } from '../types'
|
import type { Actions, GraphNode, HandleElement, HandleType, Position } from '../types'
|
||||||
import { getDimensions } from '.'
|
import { getDimensions } from '.'
|
||||||
|
|
||||||
export function getHandleBounds(
|
export function getHandleBounds(
|
||||||
selector: string,
|
type: HandleType,
|
||||||
nodeElement: HTMLDivElement,
|
nodeElement: HTMLDivElement,
|
||||||
nodeBounds: DOMRect,
|
nodeBounds: DOMRect,
|
||||||
zoom: number,
|
zoom: number,
|
||||||
): HandleElement[] {
|
): HandleElement[] {
|
||||||
const handles = nodeElement.querySelectorAll(`.vue-flow__handle${selector}`)
|
const handles = nodeElement.querySelectorAll(`.vue-flow__handle.${type}`)
|
||||||
|
|
||||||
const handlesArray = Array.from(handles) as HTMLDivElement[]
|
const handlesArray = Array.from(handles) as HTMLDivElement[]
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ export function getHandleBounds(
|
|||||||
return {
|
return {
|
||||||
id: handle.getAttribute('data-handleid'),
|
id: handle.getAttribute('data-handleid'),
|
||||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||||
|
nodeId: handle.getAttribute('data-nodeid') as string,
|
||||||
|
type,
|
||||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||||
...getDimensions(handle),
|
...getDimensions(handle),
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ describe('Check if edges are updatable', () => {
|
|||||||
view: win,
|
view: win,
|
||||||
})
|
})
|
||||||
.trigger('mousemove', {
|
.trigger('mousemove', {
|
||||||
clientX: x + 5,
|
clientX: x,
|
||||||
clientY: y + 5,
|
clientY: y,
|
||||||
force: true,
|
force: true,
|
||||||
})
|
})
|
||||||
.trigger('mouseup', {
|
.trigger('mouseup', {
|
||||||
clientX: x + 5,
|
clientX: x,
|
||||||
clientY: y + 5,
|
clientY: y,
|
||||||
force: true,
|
force: true,
|
||||||
view: win,
|
view: win,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "cypress run --component",
|
"test": "cypress run --component --browser chrome",
|
||||||
"open": "cypress open",
|
"open": "cypress open",
|
||||||
"lint": "eslint --ext .js,.ts ./"
|
"lint": "eslint --ext .js,.ts ./"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user