diff --git a/examples/svelte/src/components/Header/Header.svelte b/examples/svelte/src/components/Header/Header.svelte index 6fa79b68..3d0c9ff5 100644 --- a/examples/svelte/src/components/Header/Header.svelte +++ b/examples/svelte/src/components/Header/Header.svelte @@ -13,7 +13,8 @@ 'subflows', 'usesvelteflow', 'useupdatenodeinternals', - 'validation' + 'validation', + 'custom-connection-line' ]; const onChange = (event: Event) => { diff --git a/examples/svelte/src/routes/custom-connection-line/+page.svelte b/examples/svelte/src/routes/custom-connection-line/+page.svelte new file mode 100644 index 00000000..58552225 --- /dev/null +++ b/examples/svelte/src/routes/custom-connection-line/+page.svelte @@ -0,0 +1,35 @@ + + +
+ + + + +
+ + diff --git a/examples/svelte/src/routes/custom-connection-line/ConnectionLine.svelte b/examples/svelte/src/routes/custom-connection-line/ConnectionLine.svelte new file mode 100644 index 00000000..0a7f87b5 --- /dev/null +++ b/examples/svelte/src/routes/custom-connection-line/ConnectionLine.svelte @@ -0,0 +1,9 @@ + + +{#if $connection.path} + +{/if} diff --git a/examples/svelte/src/routes/custom-connection-line/CustomNode.svelte b/examples/svelte/src/routes/custom-connection-line/CustomNode.svelte new file mode 100644 index 00000000..0f4cc6bd --- /dev/null +++ b/examples/svelte/src/routes/custom-connection-line/CustomNode.svelte @@ -0,0 +1,34 @@ + + +
+
Node
+ + + +
diff --git a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte index f2abe041..4635dad5 100644 --- a/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte +++ b/packages/svelte/src/lib/components/ConnectionLine/ConnectionLine.svelte @@ -3,13 +3,21 @@ import { useStore } from '$lib/store'; - const { connectionPath, width, height, connection } = useStore(); + export let containerStyle: string = ''; + export let style: string = ''; + export let usingCustomLine: boolean = false; + + const { width, height, connection } = useStore(); -{#if $connectionPath} - - - +{#if $connection.path} + + + + + {#if !usingCustomLine} + + {/if} {/if} diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 6c47f37e..bb450236 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -40,6 +40,8 @@ export let connectionRadius: $$Props['connectionRadius'] = undefined; export let connectionLineType: $$Props['connectionLineType'] = undefined; export let connectionMode: $$Props['connectionMode'] = ConnectionMode.Strict; + export let connectionLineStyle: $$Props['connectionLineStyle'] = ''; + export let connectionLineContainerStyle: $$Props['connectionLineContainerStyle'] = ''; export let onMoveStart: $$Props['onMoveStart'] = undefined; export let onMove: $$Props['onMove'] = undefined; export let onMoveEnd: $$Props['onMoveEnd'] = undefined; @@ -160,7 +162,13 @@ - + + +
& { initialViewport?: Viewport; connectionRadius?: number; connectionMode?: ConnectionMode; + connectionLineStyle?: string; + connectionLineContainerStyle?: string; selectionMode?: SelectionMode; snapGrid?: SnapGrid; defaultMarkerColor?: string; diff --git a/packages/svelte/src/lib/hooks/useConnection.ts b/packages/svelte/src/lib/hooks/useConnection.ts new file mode 100644 index 00000000..6db9e735 --- /dev/null +++ b/packages/svelte/src/lib/hooks/useConnection.ts @@ -0,0 +1,10 @@ +import type { Readable } from 'svelte/store'; + +import { useStore } from '$lib/store'; +import type { ConnectionProps } from '$lib/store/derived-connection-props'; + +export function useConnection(): Readable { + const { connection } = useStore(); + + return connection; +} diff --git a/packages/svelte/src/lib/index.ts b/packages/svelte/src/lib/index.ts index 66b1af10..f1af3791 100644 --- a/packages/svelte/src/lib/index.ts +++ b/packages/svelte/src/lib/index.ts @@ -21,6 +21,7 @@ export { useStore } from '$lib/store'; export * from '$lib/utils'; export * from '$lib/hooks/useSvelteFlow'; export * from '$lib/hooks/useUpdateNodeInternals'; +export * from '$lib/hooks/useConnection'; // types export type { Edge, EdgeProps, EdgeTypes, DefaultEdgeOptions } from '$lib/types/edges'; diff --git a/packages/svelte/src/lib/store/connection-path.ts b/packages/svelte/src/lib/store/derived-connection-props.ts similarity index 68% rename from packages/svelte/src/lib/store/connection-path.ts rename to packages/svelte/src/lib/store/derived-connection-props.ts index aea51c3f..f9586885 100644 --- a/packages/svelte/src/lib/store/connection-path.ts +++ b/packages/svelte/src/lib/store/derived-connection-props.ts @@ -1,4 +1,4 @@ -import { derived } from 'svelte/store'; +import { derived, type Writable } from 'svelte/store'; import { getBezierPath, getSmoothStepPath, @@ -10,6 +10,35 @@ import { } from '@xyflow/system'; import type { SvelteFlowStoreState } from './types'; +import type { ConnectionData } from '$lib/types'; + +export type ConnectionProps = { + path: string | null; + sourceX: number | null; + sourceY: number | null; + sourcePosition: Position | undefined | null; + targetX: number | null; + targetY: number | null; + targetPosition: Position | undefined | null; + pointerPosition: ConnectionData['connectionPosition'] | null; + startHandle: ConnectionData['connectionStartHandle'] | null; + endHandle: ConnectionData['connectionEndHandle'] | null; + status: ConnectionData['connectionStatus'] | null; +}; + +export const initConnectionProps = { + path: null, + sourceX: null, + sourceY: null, + sourcePosition: null, + targetX: null, + targetY: null, + targetPosition: null, + pointerPosition: null, + startHandle: null, + endHandle: null, + status: null +}; const oppositePosition = { [Position.Left]: Position.Right, @@ -18,10 +47,13 @@ const oppositePosition = { [Position.Bottom]: Position.Top }; -export function getConnectionPath(store: SvelteFlowStoreState) { +export function getDerivedConnectionProps( + store: SvelteFlowStoreState, + currentConnection: Writable +) { return derived( [ - store.connection, + currentConnection, store.connectionLineType, store.connectionMode, store.nodes, @@ -29,7 +61,7 @@ export function getConnectionPath(store: SvelteFlowStoreState) { ], ([connection, connectionLineType, connectionMode, nodes, transform]) => { if (!connection.connectionStartHandle?.nodeId) { - return null; + return initConnectionProps; } const fromNode = nodes.find((n) => n.id === connection.connectionStartHandle?.nodeId); @@ -80,7 +112,14 @@ export function getConnectionPath(store: SvelteFlowStoreState) { [path] = getStraightPath(pathParams); } - return path; + return { + path, + ...pathParams, + pointerPosition: connection.connectionPosition, + startHandle: connection.connectionStartHandle, + endHandle: connection.connectionEndHandle, + status: connection.connectionStatus + }; } ); } diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 8b7f5156..4d411746 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -1,5 +1,5 @@ import { getContext, setContext } from 'svelte'; -import { derived, get } from 'svelte/store'; +import { derived, get, writable } from 'svelte/store'; import { internalsSymbol, createMarkerIds, @@ -19,18 +19,13 @@ import { } from '@xyflow/system'; import { addEdge as addEdgeUtil } from '$lib/utils'; -import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types'; -import { getConnectionPath } from './connection-path'; -import { - initConnectionData, - initialEdgeTypes, - initialNodeTypes, - getInitialStore -} from './initial-store'; +import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; +import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import type { SvelteFlowStore } from './types'; import { syncNodeStores, syncEdgeStores } from './utils'; import { getEdgeTree } from './edge-tree'; import { getVisibleNodes } from './visible-nodes'; +import { getDerivedConnectionProps } from './derived-connection-props'; export const key = Symbol(); @@ -256,25 +251,22 @@ export function createStore(): SvelteFlowStore { }); } - const updateConnection: UpdateConnection = (update) => { - const currentConnectionData = get(store.connection); + const initConnectionUpdateData = { + connectionStartHandle: null, + connectionEndHandle: null, + connectionPosition: null, + connectionStatus: null + }; - const nextConnectionData = currentConnectionData - ? { - ...initConnectionData, - ...currentConnectionData, - ...update - } - : { - ...initConnectionData, - ...update - }; - - store.connection.set(nextConnectionData); + // by creating an internal, unexposed store and using a derived store + // we prevent using slow get() calls + const currentConnection = writable(initConnectionUpdateData); + const updateConnection: UpdateConnection = (newConnection: ConnectionData) => { + currentConnection.set(newConnection); }; function cancelConnection() { - updateConnection(initConnectionData); + updateConnection(initConnectionUpdateData); } function reset() { @@ -296,7 +288,7 @@ export function createStore(): SvelteFlowStore { // derived state edgeTree: getEdgeTree(store), - connectionPath: getConnectionPath(store), + connection: getDerivedConnectionProps(store, currentConnection), visibleNodes: getVisibleNodes(store), markers: derived( [store.edges, store.defaultMarkerColor, store.flowId], diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 1c82f301..0e76e319 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -24,22 +24,9 @@ import BezierEdge from '$lib/components/edges/BezierEdge.svelte'; import StraightEdge from '$lib/components/edges/StraightEdge.svelte'; import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte'; import StepEdge from '$lib/components/edges/StepEdge.svelte'; -import type { - ConnectionData, - NodeTypes, - EdgeTypes, - EdgeLayouted, - Node, - FitViewOptions -} from '$lib/types'; +import type { NodeTypes, EdgeTypes, EdgeLayouted, Node, FitViewOptions } from '$lib/types'; import { createNodesStore, createEdgesStore } from './utils'; - -export const initConnectionData = { - connectionStartHandle: null, - connectionEndHandle: null, - connectionPosition: null, - connectionStatus: null -}; +import { initConnectionProps, type ConnectionProps } from './derived-connection-props'; export const initialNodeTypes = { input: InputNode, @@ -87,8 +74,7 @@ export const getInitialStore = () => ({ transform: writable([0, 0, 1]), connectionMode: writable(ConnectionMode.Strict), domNode: writable(null), - connectionPath: readable(null), - connection: writable(initConnectionData), + connection: readable(initConnectionProps), connectionLineType: writable(ConnectionLineType.Bezier), connectionRadius: writable(20), isValidConnection: writable(() => true), diff --git a/packages/system/src/types/general.ts b/packages/system/src/types/general.ts index ba1c6e3c..d23dee9b 100644 --- a/packages/system/src/types/general.ts +++ b/packages/system/src/types/general.ts @@ -131,8 +131,8 @@ export type UpdateNodePositions = ( export type PanBy = (delta: XYPosition) => boolean; export type UpdateConnection = (params: { - connectionPosition?: XYPosition | null; - connectionStatus?: ConnectionStatus | null; - connectionStartHandle?: ConnectingHandle | null; - connectionEndHandle?: ConnectingHandle | null; + connectionPosition: XYPosition | null; + connectionStatus: ConnectionStatus | null; + connectionStartHandle: ConnectingHandle | null; + connectionEndHandle: ConnectingHandle | null; }) => void; diff --git a/packages/system/src/xyhandle/XYHandle.ts b/packages/system/src/xyhandle/XYHandle.ts index 732b3e60..6ec86528 100644 --- a/packages/system/src/xyhandle/XYHandle.ts +++ b/packages/system/src/xyhandle/XYHandle.ts @@ -66,6 +66,8 @@ const nullConnection: Connection = { source: null, target: null, sourceHandle: n const alwaysValid = () => true; +let connectionStartHandle: ConnectingHandle | null = null; + function onPointerDown( event: MouseEvent | TouchEvent, { @@ -129,15 +131,18 @@ function onPointerDown( autoPanId = requestAnimationFrame(autoPan); } + // Stays the same for all consecutive pointermove events + connectionStartHandle = { + nodeId, + handleId, + type: handleType, + }; + updateConnection({ connectionPosition, connectionStatus: null, // connectionNodeId etc will be removed in the next major in favor of connectionStartHandle - connectionStartHandle: { - nodeId, - handleId, - type: handleType, - }, + connectionStartHandle, connectionEndHandle: null, }); @@ -173,6 +178,7 @@ function onPointerDown( isValid = result.isValid; updateConnection({ + connectionStartHandle, connectionPosition: closestHandle && isValid ? rendererPointToPoint( @@ -220,6 +226,7 @@ function onPointerDown( isValid = false; connection = null; handleDomNode = null; + connectionStartHandle = null; doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener);