Files
xyflow/packages/svelte/src/lib/components/Handle/Handle.svelte
T

238 lines
7.0 KiB
Svelte

<script lang="ts">
import { getContext } from 'svelte';
import {
Position,
XYHandle,
isMouseEvent,
areConnectionMapsEqual,
handleConnectionChange,
ConnectionMode,
getHostForElement,
type HandleConnection,
type Optional,
type ConnectionState,
type Connection
} from '@xyflow/system';
import { useStore } from '$lib/store';
import type { ConnectableContext } from '../NodeWrapper/types';
import type { HandleProps } from './types';
let {
id: handleId = null,
type = 'source',
position = Position.Top,
style,
class: className,
isConnectable: isConnectableProp,
isConnectableStart = true,
isConnectableEnd = true,
isValidConnection,
onconnect,
ondisconnect,
children,
...rest
}: HandleProps = $props();
const nodeId = getContext<string>('svelteflow__node_id');
const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
let isTarget = $derived(type === 'target');
let isConnectable = $derived(
isConnectableProp !== undefined ? isConnectableProp : isConnectableContext.value
);
let store = useStore();
let ariaLabelConfig = $derived(store.ariaLabelConfig);
let prevConnections: Map<string, HandleConnection> | null = null;
$effect.pre(() => {
if (onconnect || ondisconnect) {
// connectionLookup is not reactive, so we use edges to get notified about updates
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.edges;
let connections = store.connectionLookup.get(
`${nodeId}-${type}${handleId ? `-${handleId}` : ''}`
);
if (prevConnections && !areConnectionMapsEqual(connections, prevConnections)) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections, _connections, ondisconnect);
handleConnectionChange(_connections, prevConnections, onconnect);
}
prevConnections = new Map(connections);
}
});
let [connectionInProgress, connectingFrom, connectingTo, isPossibleTargetHandle, valid] =
$derived.by(() => {
if (!store.connection.inProgress) {
return [false, false, false, false, null];
}
const { fromHandle, toHandle, isValid } = store.connection;
const connectingFrom =
fromHandle &&
fromHandle.nodeId === nodeId &&
fromHandle.type === type &&
fromHandle.id === handleId;
const connectingTo =
toHandle &&
toHandle.nodeId === nodeId &&
toHandle.type === type &&
toHandle.id === handleId;
const isPossibleTargetHandle =
store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
const valid = connectingTo && isValid;
return [true, connectingFrom, connectingTo, isPossibleTargetHandle, valid];
});
function onConnectExtended(connection: Connection) {
const edge = store.onbeforeconnect?.(connection) ?? connection;
if (!edge) {
return;
}
store.addEdge(edge);
store.onconnect?.(connection);
}
function onpointerdown(event: MouseEvent | TouchEvent) {
const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
XYHandle.onPointerDown(event, {
handleId,
nodeId,
isTarget,
connectionRadius: store.connectionRadius,
domNode: store.domNode,
nodeLookup: store.nodeLookup,
connectionMode: store.connectionMode,
lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect,
flowId: store.flowId,
isValidConnection: isValidConnection ?? store.isValidConnection,
updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection,
panBy: store.panBy,
onConnect: onConnectExtended,
onConnectStart: (event, startParams) => {
store.onconnectstart?.(event, {
nodeId: startParams.nodeId,
handleId: startParams.handleId,
handleType: startParams.handleType
});
},
onConnectEnd: (event, connectionState) => {
store.onconnectend?.(event, connectionState);
},
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
});
}
}
function onclick(event: MouseEvent) {
if (!nodeId || (!store.clickConnectStartHandle && !isConnectableStart)) {
return;
}
if (!store.clickConnectStartHandle) {
store.onclickconnectstart?.(event, { nodeId, handleId, handleType: type });
store.clickConnectStartHandle = { nodeId, type, id: handleId };
return;
}
const doc = getHostForElement(event.target);
const isValidConnectionHandler = isValidConnection ?? store.isValidConnection;
const { connectionMode, clickConnectStartHandle, flowId, nodeLookup } = store;
const { connection, isValid } = XYHandle.isValid(event, {
handle: {
nodeId,
id: handleId,
type
},
connectionMode,
fromNodeId: clickConnectStartHandle.nodeId,
fromHandleId: clickConnectStartHandle.id ?? null,
fromType: clickConnectStartHandle.type,
isValidConnection: isValidConnectionHandler,
flowId,
doc,
lib: 'svelte',
nodeLookup
});
if (isValid && connection) {
onConnectExtended(connection);
}
const connectionClone = structuredClone($state.snapshot(store.connection)) as Optional<
ConnectionState,
'inProgress'
>;
delete connectionClone.inProgress;
connectionClone.toPosition = connectionClone.toHandle
? connectionClone.toHandle.position
: null;
store.onclickconnectend?.(event, connectionClone);
store.clickConnectStartHandle = null;
}
</script>
<!--
@component
The Handle component is the part of a node that can be used to connect nodes.
-->
<div
data-handleid={handleId}
data-nodeid={nodeId}
data-handlepos={position}
data-id="{store.flowId}-{nodeId}-{handleId ?? 'null'}-{type}"
class={[
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
store.noDragClass,
store.noPanClass,
position,
className
]}
class:valid
class:connectingto={connectingTo}
class:connectingfrom={connectingFrom}
class:source={!isTarget}
class:target={isTarget}
class:connectablestart={isConnectableStart}
class:connectableend={isConnectableEnd}
class:connectable={isConnectable}
class:connectionindicator={isConnectable &&
(!connectionInProgress || isPossibleTargetHandle) &&
(connectionInProgress || store.clickConnectStartHandle ? isConnectableEnd : isConnectableStart)}
onmousedown={onpointerdown}
ontouchstart={onpointerdown}
onclick={store.clickConnect ? onclick : undefined}
onkeypress={() => {}}
{style}
role="button"
aria-label={ariaLabelConfig[`handle.ariaLabel`]}
tabindex="-1"
{...rest}
>
{@render children?.()}
</div>