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:
Braks
2025-01-04 16:56:00 +01:00
parent 041fd871a1
commit b33da47a3a
16 changed files with 385 additions and 312 deletions
+137 -76
View File
@@ -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 {