Files
vue-flow/packages/core/src/composables/useHandle.ts
T

286 lines
8.0 KiB
TypeScript

import type { MaybeComputedRef } from '@vueuse/core'
import type { Connection, ConnectionHandle, HandleType, MouseTouchEvent, ValidConnectionFunc } from '~/types'
interface UseHandleProps {
handleId: MaybeComputedRef<string | null>
nodeId: MaybeComputedRef<string>
type: MaybeComputedRef<HandleType>
isValidConnection?: ValidConnectionFunc | null
edgeUpdaterType?: MaybeComputedRef<HandleType>
onEdgeUpdate?: (event: MouseTouchEvent, connection: Connection) => void
onEdgeUpdateEnd?: (event: MouseTouchEvent) => void
}
function alwaysValid() {
return true
}
export default function useHandle({
handleId,
nodeId,
type,
isValidConnection,
edgeUpdaterType,
onEdgeUpdate,
onEdgeUpdateEnd,
}: UseHandleProps) {
const {
vueFlowRef,
connectionMode,
connectionRadius,
connectOnClick,
connectionClickStartHandle,
nodesConnectable,
autoPanOnConnect,
findNode,
panBy,
getNodes,
startConnection,
updateConnection,
endConnection,
emits,
viewport,
edges,
isValidConnection: isValidConnectionProp,
} = useVueFlow()
let connection: Connection | null = null
let isValid = false
let handleDomNode: Element | null = null
function handlePointerDown(event: MouseTouchEvent) {
const isTarget = resolveUnref(type) === 'target'
const isMouseTriggered = isMouseEvent(event)
// when vue-flow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement)
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
const node = findNode(resolveUnref(nodeId))
let isValidConnectionHandler = isValidConnection || isValidConnectionProp.value || alwaysValid
if (!isValidConnectionHandler && node) {
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
}
let closestHandle: ConnectionHandle | null
let autoPanId = 0
const { x, y } = getEventPosition(event)
const clickedHandle = doc?.elementFromPoint(x, y)
const handleType = getHandleType(resolveUnref(edgeUpdaterType), clickedHandle)
const containerBounds = vueFlowRef.value?.getBoundingClientRect()
if (!containerBounds || !handleType) {
return
}
let prevActiveHandle: Element
let connectionPosition = getEventPosition(event, containerBounds)
let autoPanStarted = false
const handleLookup = getHandleLookup({
nodes: getNodes.value,
nodeId: resolveUnref(nodeId),
handleId: resolveUnref(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) {
return
}
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds)
panBy({ x: xMovement, y: yMovement })
autoPanId = requestAnimationFrame(autoPan)
}
startConnection(
{
nodeId: resolveUnref(nodeId),
handleId: resolveUnref(handleId),
type: handleType,
},
{
x: x - containerBounds.left,
y: y - containerBounds.top,
},
event,
)
emits.connectStart({ event, nodeId: resolveUnref(nodeId), handleId: resolveUnref(handleId), handleType })
function onPointerMove(event: MouseTouchEvent) {
connectionPosition = getEventPosition(event, containerBounds)
closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]),
connectionRadius.value,
handleLookup,
)
if (!autoPanStarted) {
autoPan()
autoPanStarted = true
}
const result = isValidHandle(
event,
closestHandle,
connectionMode.value,
resolveUnref(nodeId),
resolveUnref(handleId),
isTarget ? 'target' : 'source',
isValidConnectionHandler,
doc,
edges.value,
findNode,
)
connection = result.connection
isValid = result.isValid
handleDomNode = result.handleDomNode
updateConnection(
closestHandle && isValid
? rendererPointToPoint(
{
x: closestHandle.x,
y: closestHandle.y,
},
viewport.value,
)
: connectionPosition,
result.endHandle,
getConnectionStatus(!!closestHandle, isValid),
)
if (!closestHandle && !isValid && !handleDomNode) {
return resetRecentHandle(prevActiveHandle)
}
if (connection && connection.source !== connection.target && handleDomNode) {
resetRecentHandle(prevActiveHandle)
prevActiveHandle = handleDomNode
// todo: remove `vue-flow__handle-connecting` in next major version
handleDomNode.classList.add('connecting', 'vue-flow__handle-connecting')
handleDomNode.classList.toggle('valid', isValid)
// todo: remove this in next major version
handleDomNode.classList.toggle('vue-flow__handle-valid', isValid)
}
}
function onPointerUp(event: MouseTouchEvent) {
if ((closestHandle || handleDomNode) && connection && isValid) {
if (!onEdgeUpdate) {
emits.connect(connection)
} else {
onEdgeUpdate(event, connection)
}
}
emits.connectEnd(event)
if (edgeUpdaterType) {
onEdgeUpdateEnd?.(event)
}
resetRecentHandle(prevActiveHandle)
cancelAnimationFrame(autoPanId)
endConnection(event)
autoPanStarted = false
isValid = false
connection = null
handleDomNode = null
doc.removeEventListener('mousemove', onPointerMove)
doc.removeEventListener('mouseup', onPointerUp)
doc.removeEventListener('touchmove', onPointerMove)
doc.removeEventListener('touchend', onPointerUp)
}
doc.addEventListener('mousemove', onPointerMove)
doc.addEventListener('mouseup', onPointerUp)
doc.addEventListener('touchmove', onPointerMove)
doc.addEventListener('touchend', onPointerUp)
}
}
function handleClick(event: MouseEvent) {
if (!connectOnClick.value) {
return
}
const isTarget = resolveUnref(type) === 'target'
if (!connectionClickStartHandle.value) {
emits.clickConnectStart({ event, nodeId: resolveUnref(nodeId), handleId: resolveUnref(handleId) })
startConnection(
{ nodeId: resolveUnref(nodeId), type: resolveUnref(type), handleId: resolveUnref(handleId) },
undefined,
event,
true,
)
} else {
let isValidConnectionHandler = isValidConnection || isValidConnectionProp.value || alwaysValid
const node = findNode(resolveUnref(nodeId))
if (!isValidConnectionHandler && node) {
isValidConnectionHandler = (!isTarget ? node.isValidTargetPos : node.isValidSourcePos) || alwaysValid
}
if (node && (typeof node.connectable === 'undefined' ? nodesConnectable.value : node.connectable) === false) {
return
}
const doc = getHostForElement(event.target as HTMLElement)
const { connection, isValid } = isValidHandle(
event,
{
nodeId: resolveUnref(nodeId),
id: resolveUnref(handleId),
type: resolveUnref(type),
},
connectionMode.value,
connectionClickStartHandle.value.nodeId,
connectionClickStartHandle.value.handleId || null,
connectionClickStartHandle.value.type,
isValidConnectionHandler,
doc,
edges.value,
findNode,
)
const isOwnHandle = connection.source === connection.target
if (isValid && !isOwnHandle) {
emits.connect(connection)
}
emits.clickConnectEnd(event)
endConnection(event, true)
}
}
return {
handlePointerDown,
handleClick,
}
}