implement click connection

This commit is contained in:
peterkogo
2025-04-09 18:48:50 +02:00
parent 89b86a5ae7
commit 80a809cc68
4 changed files with 146 additions and 65 deletions
@@ -7,7 +7,11 @@
type HandleConnection, type HandleConnection,
areConnectionMapsEqual, areConnectionMapsEqual,
handleConnectionChange, handleConnectionChange,
ConnectionMode ConnectionMode,
getHostForElement,
type Optional,
type ConnectionState,
type Connection
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -20,15 +24,14 @@
position = Position.Top, position = Position.Top,
style, style,
class: className, class: className,
isConnectable: isConnectableProp, isConnectable: isConnectableProp = true,
isValidConnection: isValidConnectionProp, isConnectableStart = true,
isConnectableEnd = true,
isValidConnection,
onconnect, onconnect,
ondisconnect, ondisconnect,
children children
}: HandleProps = $props(); }: HandleProps = $props();
// @todo implement connectablestart, connectableend
// export let isConnectableStart: $$Props['isConnectableStart'] = undefined;
// export let isConnectableEnd: $$Props['isConnectableEnd'] = undefined;
const nodeId = getContext<string>('svelteflow__node_id'); const nodeId = getContext<string>('svelteflow__node_id');
const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable'); const isConnectableContext = getContext<ConnectableContext>('svelteflow__node_connectable');
@@ -40,7 +43,69 @@
let store = useStore(); let store = useStore();
function onPointerDown(event: MouseEvent | TouchEvent) { 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); const isMouseTriggered = isMouseEvent(event);
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) { if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
@@ -55,20 +120,11 @@
lib: 'svelte', lib: 'svelte',
autoPanOnConnect: store.autoPanOnConnect, autoPanOnConnect: store.autoPanOnConnect,
flowId: store.flowId, flowId: store.flowId,
isValidConnection: isValidConnectionProp ?? store.isValidConnection, isValidConnection: isValidConnection ?? store.isValidConnection,
updateConnection: store.updateConnection, updateConnection: store.updateConnection,
cancelConnection: store.cancelConnection, cancelConnection: store.cancelConnection,
panBy: store.panBy, panBy: store.panBy,
onConnect: (connection) => { onConnect: onConnectExtended,
const edge = store.onbeforeconnect?.(connection) ?? connection;
if (!edge) {
return;
}
store.addEdge(edge);
store.onconnect?.(connection);
},
onConnectStart: (event, startParams) => { onConnectStart: (event, startParams) => {
store.onconnectstart?.(event, { store.onconnectstart?.(event, {
nodeId: startParams.nodeId, nodeId: startParams.nodeId,
@@ -85,49 +141,54 @@
} }
} }
let prevConnections: Map<string, HandleConnection> | null = null; function onclick(event: MouseEvent) {
$effect.pre(() => { if (!nodeId || (!store.clickConnectStartHandle && !isConnectableStart)) {
// connectionLookup is not reactive, so we use edges to get notified about updates return;
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
store.edges;
if (onconnect || ondisconnect) {
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 [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid] = $derived.by( if (!store.clickConnectStartHandle) {
() => { store.onclickconnectstart?.(event, { nodeId, handleId, handleType: type });
const { fromHandle, toHandle, isValid } = store.connection; store.clickConnectStartHandle = { nodeId, type, id: handleId };
return;
const connectionInProcess = !!fromHandle;
const connectingFrom =
fromHandle?.nodeId === nodeId && fromHandle?.type === type && fromHandle?.id === handleId;
const connectingTo =
toHandle?.nodeId === nodeId && toHandle?.type === type && toHandle?.id === handleId;
const isPossibleEndHandle =
store.connectionMode === ConnectionMode.Strict
? fromHandle?.type !== type
: nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id;
const valid = connectingTo && isValid;
return [connectionInProcess, connectingFrom, connectingTo, isPossibleEndHandle, valid];
} }
);
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> </script>
<!-- <!--
@@ -152,12 +213,16 @@ The Handle component is the part of a node that can be used to connect nodes.
class:connectingfrom={connectingFrom} class:connectingfrom={connectingFrom}
class:source={!isTarget} class:source={!isTarget}
class:target={isTarget} class:target={isTarget}
class:connectablestart={isConnectable} class:connectablestart={isConnectableStart}
class:connectableend={isConnectable} class:connectableend={isConnectableEnd}
class:connectable={isConnectable} class:connectable={isConnectable}
class:connectionindicator={isConnectable && (!connectionInProcess || isPossibleEndHandle)} class:connectionindicator={isConnectable &&
onmousedown={onPointerDown} (!connectionInProgress || isPossibleTargetHandle) &&
ontouchstart={onPointerDown} (connectionInProgress || store.clickConnectStartHandle ? isConnectableEnd : isConnectableStart)}
onmousedown={onpointerdown}
ontouchstart={onpointerdown}
onclick={store.clickConnect ? onclick : undefined}
onkeypress={() => {}}
{style} {style}
role="button" role="button"
tabindex="-1" tabindex="-1"
@@ -39,6 +39,9 @@
onconnect, onconnect,
onconnectstart, onconnectstart,
onconnectend, onconnectend,
onclickconnectstart,
onclickconnectend,
clickConnect,
oninit, oninit,
fitView, fitView,
fitViewOptions, fitViewOptions,
@@ -366,6 +366,12 @@ export type SvelteFlowProps = NodeEvents &
onconnectstart?: OnConnectStart; onconnectstart?: OnConnectStart;
/** When a user stops dragging a connection line, this event gets fired. */ /** When a user stops dragging a connection line, this event gets fired. */
onconnectend?: OnConnectEnd; onconnectend?: OnConnectEnd;
/** A connection is started by clicking on a handle */
onclickconnectstart?: OnConnectStart;
/** A connection is finished by clicking on a handle */
onclickconnectend?: OnConnectEnd;
/** Toggles ability to make connections via clicking the handles */
clickConnect?: boolean;
/** This handler gets called when the flow is finished initializing */ /** This handler gets called when the flow is finished initializing */
oninit?: () => void; oninit?: () => void;
}; };
@@ -28,7 +28,8 @@ import {
pointToRendererPoint, pointToRendererPoint,
type ColorModeClass, type ColorModeClass,
type Transform, type Transform,
fitViewport fitViewport,
type Handle
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -283,12 +284,18 @@ export const getInitialStore = (signals: StoreSignals) => {
}); });
onlyRenderVisibleElements: boolean = $derived(signals.props.onlyRenderVisibleElements ?? false); onlyRenderVisibleElements: boolean = $derived(signals.props.onlyRenderVisibleElements ?? false);
onerror: OnError = $derived(signals.props.onerror ?? devWarn); onerror: OnError = $derived(signals.props.onerror ?? devWarn);
ondelete?: OnDelete = $derived(signals.props.ondelete ?? undefined); ondelete?: OnDelete = $derived(signals.props.ondelete ?? undefined);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
onbeforeconnect?: OnBeforeConnect = $derived(signals.props.onbeforeconnect); onbeforeconnect?: OnBeforeConnect = $derived(signals.props.onbeforeconnect);
onconnect?: OnConnect = $derived(signals.props.onconnect); onconnect?: OnConnect = $derived(signals.props.onconnect);
onconnectstart?: OnConnectStart = $derived(signals.props.onconnectstart); onconnectstart?: OnConnectStart = $derived(signals.props.onconnectstart);
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend); onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete); clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart ?? undefined);
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend ?? undefined);
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state(null);
resolveFitView = async () => { resolveFitView = async () => {
if (!this.panZoom) { if (!this.panZoom) {