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
|
||||
with:
|
||||
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: |
|
||||
${{ 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:
|
||||
install-command: pnpm cypress install
|
||||
command: pnpm run test --cache-dir=.turbo
|
||||
browser: chrome
|
||||
component: true
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { Background } from '@vue-flow/background'
|
||||
import { Controls } from '@vue-flow/controls'
|
||||
import { MiniMap } from '@vue-flow/minimap'
|
||||
import type { Elements } from '@vue-flow/core'
|
||||
import type { Node } from '@vue-flow/core'
|
||||
import { MarkerType, VueFlow, useVueFlow } from '@vue-flow/core'
|
||||
import CustomNode from './CustomNode.vue'
|
||||
import CustomConnectionLine from './CustomConnectionLine.vue'
|
||||
import FloatingConnectionLine from './FloatingConnectionLine.vue'
|
||||
import FloatingEdge from './FloatingEdge.vue'
|
||||
|
||||
const { onConnect, addEdges } = useVueFlow()
|
||||
@@ -20,7 +18,7 @@ const defaultEdgeOptions = {
|
||||
},
|
||||
}
|
||||
|
||||
const elements = ref<Elements>([
|
||||
const nodes = ref<Node[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'custom',
|
||||
@@ -43,37 +41,31 @@ const elements = ref<Elements>([
|
||||
},
|
||||
])
|
||||
|
||||
const edges = ref([])
|
||||
|
||||
onConnect(addEdges)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div style="height: 100vh">
|
||||
<VueFlow
|
||||
v-model="elements"
|
||||
:elevate-nodes-on-select="false"
|
||||
class="vue-flow-basic-example"
|
||||
:default-zoom="1.5"
|
||||
:default-edge-options="defaultEdgeOptions"
|
||||
:min-zoom="0.2"
|
||||
:max-zoom="4"
|
||||
>
|
||||
<Background pattern-color="#aaa" :gap="8" />
|
||||
<VueFlow
|
||||
v-model:nodes="nodes"
|
||||
v-model:edges="edges"
|
||||
:elevate-nodes-on-select="false"
|
||||
:default-edge-options="defaultEdgeOptions"
|
||||
fit-view-on-init
|
||||
>
|
||||
<Background pattern-color="#aaa" :gap="8" />
|
||||
|
||||
<MiniMap />
|
||||
<template #node-custom="props">
|
||||
<CustomNode :id="props.id" />
|
||||
</template>
|
||||
|
||||
<Controls />
|
||||
<template #edge-floating="fProps">
|
||||
<FloatingEdge v-bind="fProps" />
|
||||
</template>
|
||||
|
||||
<template #node-custom="props">
|
||||
<CustomNode :id="props.id" />
|
||||
</template>
|
||||
|
||||
<template #edge-floating="fProps">
|
||||
<FloatingEdge v-bind="fProps" />
|
||||
</template>
|
||||
|
||||
<template #connection-line="cProps">
|
||||
<CustomConnectionLine v-bind="cProps" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</div>
|
||||
<template #connection-line="cProps">
|
||||
<FloatingConnectionLine v-bind="cProps" />
|
||||
</template>
|
||||
</VueFlow>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GraphNode } from '@vue-flow/core'
|
||||
import { getStraightPath } from '@vue-flow/core'
|
||||
import type { ConnectionLineProps } from '@vue-flow/core'
|
||||
import { BaseEdge, getStraightPath } from '@vue-flow/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps<{ sourceNode: GraphNode; sourceX: number; sourceY: number; targetX: number; targetY: number }>()
|
||||
const props = defineProps<ConnectionLineProps>()
|
||||
|
||||
const edgePath = computed(() =>
|
||||
getStraightPath({
|
||||
@@ -15,13 +15,12 @@ const edgePath = computed(() =>
|
||||
|
||||
<template>
|
||||
<g>
|
||||
<path
|
||||
<BaseEdge
|
||||
:style="{
|
||||
strokeWidth: 3,
|
||||
stroke: 'black',
|
||||
}"
|
||||
fill="none"
|
||||
:d="edgePath[0]"
|
||||
:path="edgePath[0]"
|
||||
/>
|
||||
<circle :cx="targetX" :cy="targetY" fill="black" :r="3" stroke="black" :stroke-width="1.5" />
|
||||
</g>
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GraphNode } from '@vue-flow/core'
|
||||
import { getStraightPath } from '@vue-flow/core'
|
||||
import type { EdgeProps } from '@vue-flow/core'
|
||||
import { BaseEdge, getStraightPath } from '@vue-flow/core'
|
||||
import { computed } from 'vue'
|
||||
|
||||
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))
|
||||
|
||||
@@ -20,5 +20,5 @@ const edgePath = computed(() =>
|
||||
</script>
|
||||
|
||||
<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>
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { computed, defineComponent, h, inject } from 'vue'
|
||||
import type { HandleElement } from '../../types'
|
||||
import { ConnectionLineType, ConnectionMode, Position } from '../../types'
|
||||
import { getHandlePosition, getMarkerId } from '../../utils'
|
||||
import { getHandlePosition, getMarkerId, oppositePosition } from '../../utils'
|
||||
import { useVueFlow } from '../../composables'
|
||||
import { Slots } from '../../context'
|
||||
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({
|
||||
name: 'ConnectionLine',
|
||||
compatConfig: { MODE: 3 },
|
||||
@@ -57,7 +50,7 @@ const ConnectionLine = defineComponent({
|
||||
return null
|
||||
}
|
||||
|
||||
const startHandleId = connectionStartHandle.value.handleId
|
||||
const startHandleId = connectionStartHandle.value.id
|
||||
|
||||
const handleType = connectionStartHandle.value.type
|
||||
|
||||
@@ -78,18 +71,18 @@ const ConnectionLine = defineComponent({
|
||||
const { x: fromX, y: fromY } = getHandlePosition(fromNode.value, fromHandle, fromPosition)
|
||||
|
||||
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 (connectionMode.value === ConnectionMode.Strict) {
|
||||
toHandle =
|
||||
toNode.value.handleBounds[handleType === 'source' ? 'target' : 'source']?.find(
|
||||
(d) => d.id === connectionEndHandle.value?.handleId,
|
||||
(d) => d.id === connectionEndHandle.value?.id,
|
||||
) || null
|
||||
} else {
|
||||
// if connection mode is loose, look for the handle in both source and target bounds
|
||||
toHandle =
|
||||
[...(toNode.value.handleBounds.source || []), ...(toNode.value.handleBounds.target || [])]?.find(
|
||||
(d) => d.id === connectionEndHandle.value?.handleId,
|
||||
(d) => d.id === connectionEndHandle.value?.id,
|
||||
) || null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
ErrorCode,
|
||||
VueFlowError,
|
||||
elementSelectionKeys,
|
||||
getHandle,
|
||||
getEdgeHandle,
|
||||
getHandlePosition,
|
||||
getMarkerId,
|
||||
} from '../../utils'
|
||||
@@ -150,7 +150,7 @@ const EdgeWrapper = defineComponent({
|
||||
sourceNodeHandles = [...(sourceNode.handleBounds.source || []), ...(sourceNode.handleBounds.target || [])]
|
||||
}
|
||||
|
||||
const sourceHandle = getHandle(sourceNodeHandles, edge.value.sourceHandle)
|
||||
const sourceHandle = getEdgeHandle(sourceNodeHandles, edge.value.sourceHandle)
|
||||
|
||||
let targetNodeHandles
|
||||
if (connectionMode.value === ConnectionMode.Strict) {
|
||||
@@ -159,7 +159,7 @@ const EdgeWrapper = defineComponent({
|
||||
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
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ const type = toRef(() => props.type ?? 'source')
|
||||
const isValidConnection = toRef(() => props.isValidConnection ?? null)
|
||||
|
||||
const {
|
||||
id: flowId,
|
||||
connectionStartHandle,
|
||||
connectionClickStartHandle,
|
||||
connectionEndHandle,
|
||||
@@ -39,17 +40,17 @@ const isConnectableEnd = toRef(() => (typeof connectableEnd !== 'undefined' ? co
|
||||
const isConnecting = toRef(
|
||||
() =>
|
||||
(connectionStartHandle.value?.nodeId === nodeId &&
|
||||
connectionStartHandle.value?.handleId === handleId &&
|
||||
connectionStartHandle.value?.id === handleId &&
|
||||
connectionStartHandle.value?.type === type.value) ||
|
||||
(connectionEndHandle.value?.nodeId === nodeId &&
|
||||
connectionEndHandle.value?.handleId === handleId &&
|
||||
connectionEndHandle.value?.id === handleId &&
|
||||
connectionEndHandle.value?.type === type.value),
|
||||
)
|
||||
|
||||
const isClickConnecting = toRef(
|
||||
() =>
|
||||
connectionClickStartHandle.value?.nodeId === nodeId &&
|
||||
connectionClickStartHandle.value?.handleId === handleId &&
|
||||
connectionClickStartHandle.value?.id === handleId &&
|
||||
connectionClickStartHandle.value?.type === type.value,
|
||||
)
|
||||
|
||||
@@ -127,6 +128,8 @@ onMounted(() => {
|
||||
position,
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
type: type.value,
|
||||
nodeId,
|
||||
...getDimensions(handle.value),
|
||||
}
|
||||
|
||||
@@ -177,7 +180,7 @@ export default {
|
||||
<template>
|
||||
<div
|
||||
ref="handle"
|
||||
:data-id="`${nodeId}-${handleId}-${type}`"
|
||||
:data-id="`${flowId}-${nodeId}-${handleId}-${type}`"
|
||||
:data-handleid="handleId"
|
||||
:data-nodeid="nodeId"
|
||||
:data-handlepos="position"
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
import type { MaybeRefOrGetter } 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 {
|
||||
calcAutoPan,
|
||||
getClosestHandle,
|
||||
getConnectionStatus,
|
||||
getEventPosition,
|
||||
getHandleLookup,
|
||||
getHandle,
|
||||
getHandlePosition,
|
||||
getHandleType,
|
||||
getHostForElement,
|
||||
isConnectionValid,
|
||||
isMouseEvent,
|
||||
isValidHandle,
|
||||
oppositePosition,
|
||||
pointToRendererPoint,
|
||||
rendererPointToPoint,
|
||||
resetRecentHandle,
|
||||
@@ -49,6 +52,7 @@ export function useHandle({
|
||||
onEdgeUpdateEnd,
|
||||
}: UseHandleProps) {
|
||||
const {
|
||||
id: flowId,
|
||||
vueFlowRef,
|
||||
connectionMode,
|
||||
connectionRadius,
|
||||
@@ -67,12 +71,12 @@ export function useHandle({
|
||||
edges,
|
||||
nodes,
|
||||
isValidConnection: isValidConnectionProp,
|
||||
nodeLookup,
|
||||
} = useVueFlow()
|
||||
|
||||
let connection: Connection | null = null
|
||||
let isValid = false
|
||||
let isValid: boolean | null = false
|
||||
let handleDomNode: Element | null = null
|
||||
let previousConnection: ValidHandleResult | null = null
|
||||
|
||||
function handlePointerDown(event: MouseTouchEvent) {
|
||||
const isTarget = toValue(type) === 'target'
|
||||
@@ -91,7 +95,7 @@ export function useHandle({
|
||||
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
|
||||
}
|
||||
|
||||
let closestHandle: ConnectionHandle | null
|
||||
let closestHandle: HandleElement | null
|
||||
|
||||
let autoPanId = 0
|
||||
|
||||
@@ -104,17 +108,16 @@ export function useHandle({
|
||||
return
|
||||
}
|
||||
|
||||
const fromHandleInternal = getHandle(toValue(nodeId), handleType, toValue(handleId), nodeLookup.value, connectionMode.value)
|
||||
|
||||
if (!fromHandleInternal) {
|
||||
return
|
||||
}
|
||||
|
||||
let prevActiveHandle: Element
|
||||
let connectionPosition = getEventPosition(event, containerBounds)
|
||||
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
|
||||
const autoPan = () => {
|
||||
if (!autoPanOnConnect.value) {
|
||||
@@ -127,10 +130,37 @@ export function useHandle({
|
||||
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(
|
||||
{
|
||||
nodeId: toValue(nodeId),
|
||||
handleId: toValue(handleId),
|
||||
id: toValue(handleId),
|
||||
type: handleType,
|
||||
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 })
|
||||
|
||||
let previousConnection: ConnectionInProgress = newConnection
|
||||
|
||||
function onPointerMove(event: MouseTouchEvent) {
|
||||
connectionPosition = getEventPosition(event, containerBounds)
|
||||
|
||||
const { handle, validHandleResult } = getClosestHandle(
|
||||
event,
|
||||
doc,
|
||||
closestHandle = getClosestHandle(
|
||||
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
|
||||
connectionRadius.value,
|
||||
handleLookup,
|
||||
(handle) =>
|
||||
isValidHandle(
|
||||
event,
|
||||
handle,
|
||||
connectionMode.value,
|
||||
toValue(nodeId),
|
||||
toValue(handleId),
|
||||
isTarget ? 'target' : 'source',
|
||||
isValidConnectionHandler,
|
||||
doc,
|
||||
edges.value,
|
||||
nodes.value,
|
||||
findNode,
|
||||
),
|
||||
nodeLookup.value,
|
||||
fromHandle,
|
||||
)
|
||||
|
||||
closestHandle = handle
|
||||
|
||||
if (!autoPanStarted) {
|
||||
autoPan()
|
||||
autoPanStarted = true
|
||||
}
|
||||
|
||||
connection = validHandleResult.connection
|
||||
isValid = validHandleResult.isValid
|
||||
handleDomNode = validHandleResult.handleDomNode
|
||||
const result = isValidHandle(
|
||||
event,
|
||||
{
|
||||
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
|
||||
// is snapped to the same handle as before
|
||||
if (
|
||||
isValid &&
|
||||
closestHandle &&
|
||||
previousConnection?.endHandle &&
|
||||
validHandleResult.endHandle &&
|
||||
previousConnection.endHandle.type === validHandleResult.endHandle.type &&
|
||||
previousConnection.endHandle.nodeId === validHandleResult.endHandle.nodeId &&
|
||||
previousConnection.endHandle.handleId === validHandleResult.endHandle.handleId
|
||||
previousConnection?.toHandle &&
|
||||
newConnection.toHandle &&
|
||||
previousConnection.toHandle.type === newConnection.toHandle.type &&
|
||||
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
|
||||
previousConnection.toHandle.id === newConnection.toHandle.id &&
|
||||
previousConnection.to.x === newConnection.to.x &&
|
||||
previousConnection.to.y === newConnection.to.y
|
||||
) {
|
||||
return
|
||||
}
|
||||
@@ -202,11 +251,11 @@ export function useHandle({
|
||||
viewport.value,
|
||||
)
|
||||
: connectionPosition,
|
||||
validHandleResult.endHandle,
|
||||
result.toHandle,
|
||||
getConnectionStatus(!!closestHandle, isValid),
|
||||
)
|
||||
|
||||
previousConnection = validHandleResult
|
||||
previousConnection = newConnection
|
||||
|
||||
if (!closestHandle && !isValid && !handleDomNode) {
|
||||
return resetRecentHandle(prevActiveHandle)
|
||||
@@ -219,9 +268,9 @@ export function useHandle({
|
||||
|
||||
// todo: remove `vue-flow__handle-connecting` in next major version
|
||||
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
|
||||
handleDomNode.classList.toggle('vue-flow__handle-valid', isValid)
|
||||
handleDomNode.classList.toggle('vue-flow__handle-valid', !!isValid)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,50 +324,62 @@ export function useHandle({
|
||||
if (!connectionClickStartHandle.value) {
|
||||
emits.clickConnectStart({ event, nodeId: toValue(nodeId), handleId: toValue(handleId) })
|
||||
|
||||
startConnection({ nodeId: toValue(nodeId), type: toValue(type), handleId: toValue(handleId) }, undefined, true)
|
||||
} else {
|
||||
let isValidConnectionHandler = toValue(isValidConnection) || isValidConnectionProp.value || alwaysValid
|
||||
startConnection(
|
||||
{ nodeId: toValue(nodeId), type: toValue(type), id: toValue(handleId), position: Position.Top },
|
||||
undefined,
|
||||
true,
|
||||
)
|
||||
|
||||
const node = findNode(toValue(nodeId))
|
||||
return
|
||||
}
|
||||
|
||||
if (!isValidConnectionHandler && node) {
|
||||
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
|
||||
}
|
||||
let isValidConnectionHandler = toValue(isValidConnection) || isValidConnectionProp.value || alwaysValid
|
||||
|
||||
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable.value : node.connectable) === false) {
|
||||
return
|
||||
}
|
||||
const node = findNode(toValue(nodeId))
|
||||
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
if (!isValidConnectionHandler && node) {
|
||||
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
|
||||
}
|
||||
|
||||
const { connection, isValid } = isValidHandle(
|
||||
event,
|
||||
{
|
||||
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable.value : node.connectable) === false) {
|
||||
return
|
||||
}
|
||||
|
||||
const doc = getHostForElement(event.target as HTMLElement)
|
||||
|
||||
const result = isValidHandle(
|
||||
event,
|
||||
{
|
||||
handle: {
|
||||
nodeId: toValue(nodeId),
|
||||
id: toValue(handleId),
|
||||
type: toValue(type),
|
||||
position: Position.Top,
|
||||
},
|
||||
connectionMode.value,
|
||||
connectionClickStartHandle.value.nodeId,
|
||||
connectionClickStartHandle.value.handleId || null,
|
||||
connectionClickStartHandle.value.type,
|
||||
isValidConnectionHandler,
|
||||
connectionMode: connectionMode.value,
|
||||
fromNodeId: connectionClickStartHandle.value.nodeId,
|
||||
fromHandleId: connectionClickStartHandle.value.id || null,
|
||||
fromType: connectionClickStartHandle.value.type,
|
||||
isValidConnection: isValidConnectionHandler,
|
||||
doc,
|
||||
edges.value,
|
||||
nodes.value,
|
||||
findNode,
|
||||
)
|
||||
lib: 'vue',
|
||||
flowId,
|
||||
nodeLookup: nodeLookup.value,
|
||||
},
|
||||
edges.value,
|
||||
nodes.value,
|
||||
findNode,
|
||||
)
|
||||
|
||||
const isOwnHandle = connection.source === connection.target
|
||||
const isOwnHandle = result.connection?.source === result.connection?.target
|
||||
|
||||
if (isValid && !isOwnHandle) {
|
||||
emits.connect(connection)
|
||||
}
|
||||
|
||||
emits.clickConnectEnd(event)
|
||||
|
||||
endConnection(event, true)
|
||||
if (result.isValid && result.connection && !isOwnHandle) {
|
||||
emits.connect(result.connection)
|
||||
}
|
||||
|
||||
emits.clickConnectEnd(event)
|
||||
|
||||
endConnection(event, true)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -157,8 +157,8 @@ export function useActions(state: State, nodeLookup: ComputedRef<NodeLookup>, ed
|
||||
if (doUpdate) {
|
||||
const nodeBounds = update.nodeElement.getBoundingClientRect()
|
||||
node.dimensions = dimensions
|
||||
node.handleBounds.source = getHandleBounds('.source', update.nodeElement, nodeBounds, zoom)
|
||||
node.handleBounds.target = getHandleBounds('.target', update.nodeElement, nodeBounds, zoom)
|
||||
node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom)
|
||||
node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom)
|
||||
|
||||
changes.push({
|
||||
id: node.id,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
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 { GraphNode } from './node'
|
||||
import type { NodeLookup } from './store'
|
||||
|
||||
export type HandleType = 'source' | 'target'
|
||||
|
||||
export interface HandleElement extends XYPosition, Dimensions {
|
||||
id?: string | null
|
||||
position: Position
|
||||
type: HandleType
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
export interface ConnectionHandle extends XYPosition {
|
||||
@@ -16,18 +19,11 @@ export interface ConnectionHandle extends XYPosition {
|
||||
nodeId: string
|
||||
}
|
||||
|
||||
export interface ValidHandleResult {
|
||||
endHandle: ConnectingHandle | null
|
||||
handleDomNode: Element | null
|
||||
isValid: boolean
|
||||
connection: Connection
|
||||
}
|
||||
|
||||
export interface ConnectingHandle {
|
||||
nodeId: string
|
||||
type: HandleType
|
||||
handleId?: string | null
|
||||
position?: Position | null
|
||||
id?: string | null
|
||||
position: Position
|
||||
}
|
||||
|
||||
/** 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 */
|
||||
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,
|
||||
handle: HandleElement | null,
|
||||
fallbackPosition: Position = Position.Left,
|
||||
center = false,
|
||||
): XYPosition {
|
||||
const x = (handle?.x ?? 0) + node.computedPosition.x
|
||||
const y = (handle?.y ?? 0) + node.computedPosition.y
|
||||
const { width, height } = handle ?? getNodeDimensions(node)
|
||||
|
||||
if (center) {
|
||||
return { x: x + width / 2, y: y + height / 2 }
|
||||
}
|
||||
|
||||
const position = handle?.position ?? fallbackPosition
|
||||
|
||||
switch (position) {
|
||||
case Position.Top:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y,
|
||||
}
|
||||
return { x: x + width / 2, y }
|
||||
case Position.Right:
|
||||
return {
|
||||
x: x + width,
|
||||
y: y + height / 2,
|
||||
}
|
||||
return { x: x + width, y: y + height / 2 }
|
||||
case Position.Bottom:
|
||||
return {
|
||||
x: x + width / 2,
|
||||
y: y + height,
|
||||
}
|
||||
return { x: x + width / 2, y: y + height }
|
||||
case Position.Left:
|
||||
return {
|
||||
x,
|
||||
y: y + height / 2,
|
||||
}
|
||||
return { x, y: y + height / 2 }
|
||||
}
|
||||
}
|
||||
|
||||
export function getHandle(bounds: HandleElement[] = [], handleId?: string | null): HandleElement | null {
|
||||
if (!bounds.length) {
|
||||
export function getEdgeHandle(bounds: HandleElement[] | undefined, handleId?: string | null): HandleElement | null {
|
||||
if (!bounds) {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ConnectionMode } from '../types'
|
||||
import { ConnectionMode, Position } from '../types'
|
||||
import type {
|
||||
Actions,
|
||||
Connection,
|
||||
@@ -6,23 +6,17 @@ import type {
|
||||
ConnectionStatus,
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
HandleElement,
|
||||
HandleType,
|
||||
IsValidParams,
|
||||
NodeHandleBounds,
|
||||
Position,
|
||||
ValidConnectionFunc,
|
||||
ValidHandleResult,
|
||||
NodeLookup,
|
||||
Result,
|
||||
XYPosition,
|
||||
} from '../types'
|
||||
import { getEventPosition, getHandlePosition } from '.'
|
||||
import { getEventPosition, getHandlePosition, getOverlappingArea, nodeToRect } from '.'
|
||||
|
||||
function defaultValidHandleResult(): ValidHandleResult {
|
||||
return {
|
||||
handleDomNode: null,
|
||||
isValid: false,
|
||||
connection: { source: '', target: '', sourceHandle: null, targetHandle: null },
|
||||
endHandle: null,
|
||||
}
|
||||
}
|
||||
const alwaysValid = () => true
|
||||
|
||||
export function resetRecentHandle(handleDomNode: Element): void {
|
||||
handleDomNode?.classList.remove('valid', 'connecting', 'vue-flow__handle-valid', 'vue-flow__handle-connecting')
|
||||
@@ -55,126 +49,126 @@ export function getHandles(
|
||||
return connectionHandles
|
||||
}
|
||||
|
||||
export function getClosestHandle(
|
||||
event: MouseEvent | TouchEvent,
|
||||
doc: Document | ShadowRoot,
|
||||
pos: XYPosition,
|
||||
connectionRadius: number,
|
||||
handles: ConnectionHandle[],
|
||||
validator: (handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'>) => ValidHandleResult,
|
||||
) {
|
||||
// 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
|
||||
const { x, y } = getEventPosition(event)
|
||||
const domNodes = doc.elementsFromPoint(x, y)
|
||||
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,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
for (const node of nodeLookup.values()) {
|
||||
if (getOverlappingArea(rect, nodeToRect(node)) > 0) {
|
||||
nodes.push(node)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }[] = []
|
||||
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(
|
||||
position: XYPosition,
|
||||
connectionRadius: number,
|
||||
nodeLookup: NodeLookup,
|
||||
fromHandle: { nodeId: string; type: HandleType; id?: string | null },
|
||||
): HandleElement | null {
|
||||
let closestHandles: HandleElement[] = []
|
||||
let minDistance = Number.POSITIVE_INFINITY
|
||||
|
||||
for (const handle of handles) {
|
||||
const distance = Math.sqrt((handle.x - pos.x) ** 2 + (handle.y - pos.y) ** 2)
|
||||
const closeNodes = getNodesWithinDistance(position, nodeLookup, connectionRadius + ADDITIONAL_DISTANCE)
|
||||
|
||||
if (distance <= connectionRadius) {
|
||||
const validHandleResult = validator(handle)
|
||||
for (const node of closeNodes) {
|
||||
const allHandles = [...(node.handleBounds?.source ?? []), ...(node.handleBounds?.target ?? [])]
|
||||
|
||||
if (distance <= minDistance) {
|
||||
if (distance < minDistance) {
|
||||
closestHandles = [{ handle, validHandleResult }]
|
||||
} else if (distance === minDistance) {
|
||||
// when multiple handles are on the same distance we collect all of them
|
||||
closestHandles.push({
|
||||
handle,
|
||||
validHandleResult,
|
||||
})
|
||||
}
|
||||
for (const handle of allHandles) {
|
||||
// if the handle is the same as the fromHandle we skip it
|
||||
if (fromHandle.nodeId === handle.nodeId && fromHandle.type === handle.type && fromHandle.id === handle.id) {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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
|
||||
} 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) {
|
||||
return { handle: null, validHandleResult: defaultValidHandleResult() }
|
||||
return null
|
||||
}
|
||||
|
||||
if (closestHandles.length === 1) {
|
||||
return closestHandles[0]
|
||||
// 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]
|
||||
}
|
||||
|
||||
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]
|
||||
)
|
||||
return 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(
|
||||
event: MouseEvent | TouchEvent,
|
||||
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
|
||||
connectionMode: ConnectionMode,
|
||||
fromNodeId: string,
|
||||
fromHandleId: string | null,
|
||||
fromType: HandleType,
|
||||
isValidConnection: ValidConnectionFunc,
|
||||
doc: Document | ShadowRoot,
|
||||
{
|
||||
handle,
|
||||
connectionMode,
|
||||
fromNodeId,
|
||||
fromHandleId,
|
||||
fromType,
|
||||
doc,
|
||||
lib,
|
||||
flowId,
|
||||
isValidConnection = alwaysValid,
|
||||
}: IsValidParams,
|
||||
edges: GraphEdge[],
|
||||
nodes: GraphNode[],
|
||||
findNode: Actions['findNode'],
|
||||
) {
|
||||
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 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,
|
||||
// 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) {
|
||||
result.handleDomNode = 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 connectable = handleToCheck.classList.contains('connectable')
|
||||
const connectableEnd = handleToCheck.classList.contains('connectableend')
|
||||
|
||||
if (!handleNodeId || !handleType) {
|
||||
return result
|
||||
}
|
||||
|
||||
const connection: Connection = {
|
||||
source: isTarget ? handleNodeId : fromNodeId,
|
||||
sourceHandle: isTarget ? handleId : fromHandleId,
|
||||
@@ -185,7 +179,6 @@ export function isValidHandle(
|
||||
result.connection = connection
|
||||
|
||||
const isConnectable = connectable && connectableEnd
|
||||
|
||||
// in strict mode we don't allow target to target or source to source connections
|
||||
const isValid =
|
||||
isConnectable &&
|
||||
@@ -193,54 +186,21 @@ export function isValidHandle(
|
||||
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
||||
: handleNodeId !== fromNodeId || handleId !== fromHandleId)
|
||||
|
||||
if (isValid) {
|
||||
result.isValid = isValidConnection(connection, {
|
||||
edges,
|
||||
result.isValid =
|
||||
isValid &&
|
||||
isValidConnection(connection, {
|
||||
nodes,
|
||||
sourceNode: findNode(connection.source)!,
|
||||
targetNode: findNode(connection.target)!,
|
||||
edges,
|
||||
sourceNode: findNode(fromNodeId)!,
|
||||
targetNode: findNode(handleNodeId)!,
|
||||
})
|
||||
|
||||
result.endHandle = {
|
||||
nodeId: handleNodeId,
|
||||
handleId,
|
||||
type: handleType as HandleType,
|
||||
position: result.isValid ? (handleToCheck.getAttribute('data-handlepos') as Position) : null,
|
||||
}
|
||||
}
|
||||
result.toHandle = handle
|
||||
}
|
||||
|
||||
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 {
|
||||
if (edgeUpdaterType) {
|
||||
return edgeUpdaterType
|
||||
@@ -253,7 +213,7 @@ export function getHandleType(edgeUpdaterType: HandleType | undefined, handleDom
|
||||
return null
|
||||
}
|
||||
|
||||
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
||||
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean | null) {
|
||||
let connectionStatus: ConnectionStatus | null = null
|
||||
|
||||
if (isHandleValid) {
|
||||
@@ -264,3 +224,44 @@ export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleV
|
||||
|
||||
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 { nextTick } from 'vue'
|
||||
import type { Actions, GraphNode, HandleElement, Position } from '../types'
|
||||
import type { Actions, GraphNode, HandleElement, HandleType, Position } from '../types'
|
||||
import { getDimensions } from '.'
|
||||
|
||||
export function getHandleBounds(
|
||||
selector: string,
|
||||
type: HandleType,
|
||||
nodeElement: HTMLDivElement,
|
||||
nodeBounds: DOMRect,
|
||||
zoom: number,
|
||||
): HandleElement[] {
|
||||
const handles = nodeElement.querySelectorAll(`.vue-flow__handle${selector}`)
|
||||
const handles = nodeElement.querySelectorAll(`.vue-flow__handle.${type}`)
|
||||
|
||||
const handlesArray = Array.from(handles) as HTMLDivElement[]
|
||||
|
||||
@@ -19,6 +19,8 @@ export function getHandleBounds(
|
||||
return {
|
||||
id: handle.getAttribute('data-handleid'),
|
||||
position: handle.getAttribute('data-handlepos') as unknown as Position,
|
||||
nodeId: handle.getAttribute('data-nodeid') as string,
|
||||
type,
|
||||
x: (handleBounds.left - nodeBounds.left) / zoom,
|
||||
y: (handleBounds.top - nodeBounds.top) / zoom,
|
||||
...getDimensions(handle),
|
||||
|
||||
@@ -50,13 +50,13 @@ describe('Check if edges are updatable', () => {
|
||||
view: win,
|
||||
})
|
||||
.trigger('mousemove', {
|
||||
clientX: x + 5,
|
||||
clientY: y + 5,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
force: true,
|
||||
})
|
||||
.trigger('mouseup', {
|
||||
clientX: x + 5,
|
||||
clientY: y + 5,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
force: true,
|
||||
view: win,
|
||||
})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "cypress run --component",
|
||||
"test": "cypress run --component --browser chrome",
|
||||
"open": "cypress open",
|
||||
"lint": "eslint --ext .js,.ts ./"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user