added Edge Reconnection

This commit is contained in:
peterkogo
2025-04-11 15:32:07 +02:00
parent d94f3f490a
commit a3e3055186
17 changed files with 314 additions and 165 deletions
@@ -1,31 +1 @@
type PortalOptions = {
target?: string;
domNode: Element | null;
};
function tryToMount(node: Element, domNode: Element | null, target: string | undefined) {
if (!domNode) {
return;
}
const targetEl = target ? domNode.querySelector(target) : domNode;
if (targetEl) {
targetEl.appendChild(node);
}
}
export default function (node: Element, { target, domNode }: PortalOptions) {
tryToMount(node, domNode, target);
return {
async update({ target, domNode }: PortalOptions) {
tryToMount(node, domNode, target);
},
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
};
}
export { default as portal } from './portal.svelte';
@@ -0,0 +1,42 @@
import { useStore } from '$lib/store';
type Portal = 'viewport' | 'edgelabel' | 'root';
const selector: Record<Exclude<Portal, 'root'>, string> = {
viewport: '.svelte-flow__viewport-portal',
edgelabel: '.svelte-flow__edgelabel-renderer'
};
function tryToMount(node: Element, domNode: Element | null, target: Portal | undefined) {
if (!target || !domNode) {
return;
}
const targetEl = target === 'root' ? domNode : domNode.querySelector(selector[target]);
if (targetEl) {
targetEl.appendChild(node);
}
}
export default function (node: Element, target: Portal | undefined) {
const { domNode } = $derived(useStore());
let previousTarget: Portal | undefined = target;
tryToMount(node, domNode, target);
return {
async update(target: Portal) {
if (target !== previousTarget) {
node.parentNode?.removeChild(node);
}
tryToMount(node, domNode, target);
},
destroy() {
if (node.parentNode) {
node.parentNode.removeChild(node);
}
}
};
}
@@ -1,13 +1,10 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
const store = useStore();
import { portal } from '$lib/actions/portal';
let { children }: { children?: Snippet } = $props();
</script>
<div use:portal={{ target: '.svelte-flow__edgelabel-renderer', domNode: store.domNode }}>
<div use:portal={'edgelabel'}>
{@render children?.()}
</div>
@@ -0,0 +1,145 @@
<script lang="ts">
import { useStore } from '$lib/store';
import type { Edge } from '$lib/types';
import { XYHandle, type HandleType, type XYPosition } from '@xyflow/system';
import { getContext, type Snippet } from 'svelte';
import type { ClassValue } from 'svelte/elements';
import { portal } from '$lib/actions/portal';
let {
type,
reconnecting = $bindable(false),
position,
class: className,
size = 25,
style,
children,
asDomNode = false
}: {
type: HandleType;
reconnecting?: boolean;
style?: string;
class?: ClassValue;
position?: XYPosition;
asDomNode?: boolean;
size?: number;
children?: Snippet;
} = $props();
const store = useStore();
let edgeId: string | undefined = getContext('svelteflow__edge_id');
if (!edgeId) {
throw new Error('EdgeReconnectAnchor must be used within an Edge component');
}
const onPointerDown = (event: PointerEvent) => {
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
isValidConnection,
connectionMode,
connectionRadius,
onconnectstart,
onconnectend,
onreconnect,
onreconnectstart,
onreconnectend,
onbeforereconnect,
cancelConnection,
nodeLookup,
flowId,
panBy,
updateConnection,
edgeLookup
} = store;
let newEdge: Edge | undefined;
let edge = edgeLookup.get(edgeId)!;
reconnecting = true;
onreconnectstart?.(event, edge, type);
const opposite =
type === 'target'
? { nodeId: edge.source, handleId: edge.sourceHandle ?? null, type: 'source' as HandleType }
: {
nodeId: edge.target,
handleId: edge.targetHandle ?? null,
type: 'target' as HandleType
};
XYHandle.onPointerDown(event, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId: opposite.handleId,
nodeId: opposite.nodeId,
nodeLookup,
isTarget: opposite.type === 'target',
edgeUpdaterType: opposite.type,
lib: 'svelte',
flowId,
cancelConnection,
panBy,
isValidConnection,
onConnectStart: onconnectstart,
onConnectEnd: onconnectend,
onConnect: (connection) => {
newEdge = { ...edge, ...connection };
newEdge = onbeforereconnect ? (onbeforereconnect(newEdge, edge) ?? undefined) : newEdge;
if (newEdge) {
store.edges = store.edges.map((e) => (e.id === edge.id ? (newEdge as Edge) : e));
}
onreconnect?.(edge, connection);
},
onReconnectEnd: (event, connectionState) => {
reconnecting = false;
onreconnectend?.(event, edge, opposite.type, connectionState);
},
updateConnection,
getTransform: () => [store.viewport.x, store.viewport.y, store.viewport.zoom],
getFromHandle: () => store.connection.fromHandle
});
};
let offset = $derived(asDomNode ? '-50%' : `-${size * 0.5}px`);
</script>
<svelte:element
this={asDomNode ? 'div' : 'g'}
use:portal={asDomNode ? 'edgelabel' : undefined}
xmlns={asDomNode ? undefined : 'http://www.w3.org/2000/svg'}
class={['svelte-flow__edgeupdater nopan', `svelte-flow__edgeupdater-${type}`, className]}
style:position="absolute"
style:width={`${size}px`}
style:height={`${size}px`}
style:transform={position
? `translate(calc(${offset} + ${position.x}px), calc(${offset} + ${position.y}px))`
: `translate(${offset}, ${offset})`}
{style}
onpointerdown={onPointerDown}
>
{#if !reconnecting}
{#if children}
{@render children()}
{:else if !asDomNode}
<rect
width={size}
height={size}
x={0}
fill="transparent"
stroke="transparent"
pointer-events="all"
/>
{/if}
{/if}
</svelte:element>
@@ -0,0 +1 @@
export { default as EdgeReconnectAnchor } from './EdgeReconnectAnchor.svelte';
@@ -196,10 +196,10 @@
The Handle component is the part of a node that can be used to connect nodes.
-->
<div
data-handleid={handleId}
data-handleid={handleId ?? 'null'}
data-nodeid={nodeId}
data-handlepos={position}
data-id="{store.flowId}-{nodeId}-{handleId}-{type}"
data-id="{store.flowId}-{nodeId}-{handleId ?? 'null'}-{type}"
class={[
'svelte-flow__handle',
`svelte-flow__handle-${position}`,
@@ -1,14 +1,11 @@
<script lang="ts">
import type { Snippet } from 'svelte';
import portal from '$lib/actions/portal';
import { useStore } from '$lib/store';
import { portal } from '$lib/actions/portal';
let { children }: { children?: Snippet } = $props();
const store = useStore();
</script>
<div use:portal={{ target: '.svelte-flow__viewport-portal', domNode: store.domNode }}>
<div use:portal={'viewport'}>
{@render children?.()}
</div>
@@ -66,9 +66,6 @@
nodes = $bindable([]),
edges = $bindable([]),
viewport = $bindable(undefined),
nodesDraggable = $bindable(true),
nodesConnectable = $bindable(true),
elementsSelectable = $bindable(true),
...props
}: SvelteFlowProps & HTMLAttributes<HTMLDivElement> = $props();
@@ -32,6 +32,9 @@
edgeTypes,
colorMode: _colorMode,
isValidConnection,
onmove,
onmovestart,
onmoveend,
onerror,
ondelete,
onbeforedelete,
@@ -39,6 +42,10 @@
onconnect,
onconnectstart,
onconnectend,
onbeforereconnect,
onreconnect,
onreconnectstart,
onreconnectend,
onclickconnectstart,
onclickconnectend,
clickConnect,
@@ -18,7 +18,10 @@ import type {
OnConnect,
OnConnectStart,
OnConnectEnd,
ColorModeClass
ColorModeClass,
OnReconnect,
OnRecoonnectStart,
OnReconnectEnd
} from '@xyflow/system';
import type {
@@ -32,7 +35,8 @@ import type {
OnDelete,
OnBeforeConnect,
OnBeforeDelete,
IsValidConnection
IsValidConnection,
OnBeforeReconnect
} from '$lib/types';
import type { Component } from 'svelte';
@@ -356,16 +360,22 @@ export type SvelteFlowProps = NodeEvents &
ondelete?: OnDelete;
/** This handler gets called before the user deletes nodes or edges and provides a way to abort the deletion by returning false. */
onbeforedelete?: OnBeforeDelete;
/** This handler gets called when a new edge is created. You can use it to modify the newly created edge. */
onbeforeconnect?: OnBeforeConnect;
/** This event gets fired when a connection successfully completes and an edge is created. */
onconnect?: OnConnect;
/** When a user starts to drag a connection line, this event gets fired. */
onconnectstart?: OnConnectStart;
/** When a user stops dragging a connection line, this event gets fired. */
onconnectend?: OnConnectEnd;
/** This event gets fired when after an edge was reconnected*/
onreconnect?: OnReconnect;
/** This event gets fired when a user starts to reconnect an edge */
onreconnectstart?: OnRecoonnectStart;
/** This event gets fired when a user stops reconnecting an edge */
onreconnectend?: OnReconnectEnd;
/** This handler gets called when an edge is reconnected. You can use it to modify the edge before the update is applied. */
onbeforereconnect?: OnBeforeReconnect;
/** A connection is started by clicking on a handle */
onclickconnectstart?: OnConnectStart;
/** A connection is finished by clicking on a handle */
+4
View File
@@ -16,6 +16,7 @@ export {
} from '$lib/components/edges';
export * from '$lib/components/Handle';
export * from '$lib/components/EdgeLabel';
export * from '$lib/components/EdgeReconnectAnchor';
// plugins
export * from '$lib/plugins/Controls';
@@ -40,6 +41,9 @@ export * from '$lib/hooks/useNodesData.svelte';
export * from '$lib/hooks/useInternalNode.svelte';
export * from '$lib/hooks/useInitialized.svelte';
//actions
export * from '$lib/actions/portal';
// types
export type {
Edge,
@@ -2,7 +2,7 @@
import { getContext } from 'svelte';
import { Position, getNodeToolbarTransform } from '@xyflow/system';
import portal from '$lib/actions/portal';
import { portal } from '$lib/actions/portal';
import { useStore } from '$lib/store';
import { useSvelteFlow } from '$lib/hooks/useSvelteFlow.svelte';
@@ -68,7 +68,7 @@
<div
data-id={toolbarNodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
class="svelte-flow__node-toolbar"
use:portal={{ domNode: store.domNode }}
use:portal={'root'}
style:position="absolute"
style:transform
style:z-index={zIndex}
@@ -29,7 +29,10 @@ import {
type ColorModeClass,
type Transform,
fitViewport,
type Handle
type Handle,
type OnReconnect,
type OnRecoonnectStart,
type OnReconnectEnd
} from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -54,7 +57,8 @@ import type {
IsValidConnection,
Edge,
EdgeLayouted,
InternalNode
InternalNode,
OnBeforeReconnect
} from '$lib/types';
import type { StoreSignals } from './types';
@@ -270,16 +274,22 @@ export const getInitialStore = (signals: StoreSignals) => {
onlyRenderVisibleElements: boolean = $derived(signals.props.onlyRenderVisibleElements ?? false);
onerror: OnError = $derived(signals.props.onerror ?? devWarn);
ondelete?: OnDelete = $derived(signals.props.ondelete ?? undefined);
ondelete?: OnDelete = $derived(signals.props.ondelete);
onbeforedelete?: OnBeforeDelete = $derived(signals.props.onbeforedelete);
onbeforeconnect?: OnBeforeConnect = $derived(signals.props.onbeforeconnect);
onconnect?: OnConnect = $derived(signals.props.onconnect);
onconnectstart?: OnConnectStart = $derived(signals.props.onconnectstart);
onconnectend?: OnConnectEnd = $derived(signals.props.onconnectend);
onbeforereconnect?: OnBeforeReconnect = $derived(signals.props.onbeforereconnect);
onreconnect?: OnReconnect = $derived(signals.props.onreconnect);
onreconnectstart?: OnRecoonnectStart = $derived(signals.props.onreconnectstart);
onreconnectend?: OnReconnectEnd = $derived(signals.props.onreconnectend);
clickConnect?: boolean = $derived(signals.props.clickConnect ?? true);
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart ?? undefined);
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend ?? undefined);
onclickconnectstart?: OnConnectStart = $derived(signals.props.onclickconnectstart);
onclickconnectend?: OnConnectEnd = $derived(signals.props.onclickconnectend);
clickConnectStartHandle: Pick<Handle, 'id' | 'nodeId' | 'type'> | null = $state(null);
resolveFitView = async () => {
+4
View File
@@ -36,6 +36,10 @@ export type FitViewOptions<NodeType extends Node = Node> = FitViewOptionsBase<No
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
export type OnBeforeConnect = (connection: Connection) => Edge | Connection | void;
export type OnBeforeReconnect<EdgeType extends Edge = Edge> = (
newEdge: EdgeType,
oldEdge: EdgeType
) => EdgeType | void;
export type OnBeforeDelete<
NodeType extends Node = Node,
EdgeType extends Edge = Edge
+13
View File
@@ -80,6 +80,19 @@ export type OnConnectStart = (event: MouseEvent | TouchEvent, params: OnConnectS
export type OnConnect = (connection: Connection) => void;
export type OnConnectEnd = (event: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => void;
export type OnReconnect<EdgeType extends EdgeBase = EdgeBase> = (oldEdge: EdgeType, newConnection: Connection) => void;
export type OnRecoonnectStart<EdgeType extends EdgeBase = EdgeBase> = (
event: MouseEvent | TouchEvent,
edge: EdgeType,
handleType: HandleType
) => void;
export type OnReconnectEnd<EdgeType extends EdgeBase = EdgeBase> = (
event: MouseEvent | TouchEvent,
edge: EdgeType,
handleType: HandleType,
connectionState: FinalConnectionState
) => void;
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
/**