implement click connection
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user