feat(svelte): add connectionline and connection functionality
This commit is contained in:
@@ -22,7 +22,6 @@ export type NodeDragItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UseDragParams = {
|
type UseDragParams = {
|
||||||
noDragClassName?: string;
|
|
||||||
handleSelector?: string;
|
handleSelector?: string;
|
||||||
nodeId?: string;
|
nodeId?: string;
|
||||||
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
|
updateNodePositions: (dragItems: NodeDragItem[], d: boolean, p: boolean) => void;
|
||||||
@@ -32,14 +31,7 @@ type UseDragParams = {
|
|||||||
|
|
||||||
export default function drag(
|
export default function drag(
|
||||||
nodeRef: Element,
|
nodeRef: Element,
|
||||||
{
|
{ handleSelector, nodeId, updateNodePositions, nodesStore, transformStore }: UseDragParams
|
||||||
noDragClassName,
|
|
||||||
handleSelector,
|
|
||||||
nodeId,
|
|
||||||
updateNodePositions,
|
|
||||||
nodesStore,
|
|
||||||
transformStore
|
|
||||||
}: UseDragParams
|
|
||||||
) {
|
) {
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
let dragItems: NodeDragItem[] = [];
|
let dragItems: NodeDragItem[] = [];
|
||||||
@@ -119,7 +111,7 @@ export default function drag(
|
|||||||
const target = event.target as HTMLDivElement;
|
const target = event.target as HTMLDivElement;
|
||||||
const isDraggable =
|
const isDraggable =
|
||||||
!event.button &&
|
!event.button &&
|
||||||
(!noDragClassName || !hasSelector(target, `.${noDragClassName}`, nodeRef)) &&
|
!hasSelector(target, '.nodrag', nodeRef) &&
|
||||||
(!handleSelector || hasSelector(target, handleSelector, nodeRef));
|
(!handleSelector || hasSelector(target, handleSelector, nodeRef));
|
||||||
|
|
||||||
return isDraggable;
|
return isDraggable;
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import cc from 'classcat';
|
||||||
|
|
||||||
|
import { useStore } from '$lib/store';
|
||||||
|
import type { ConnectionLineProps } from '$lib/types';
|
||||||
|
|
||||||
|
type $$Props = ConnectionLineProps;
|
||||||
|
|
||||||
|
const { connectionPathStore, widthStore, heightStore, connectionStore } = useStore();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if $connectionPathStore}
|
||||||
|
<svg
|
||||||
|
width={$widthStore}
|
||||||
|
height={$heightStore}
|
||||||
|
class="react-flow__connectionline"
|
||||||
|
>
|
||||||
|
<g class={cc(['react-flow__connection', $connectionStore.status])}>
|
||||||
|
<path d={$connectionPathStore} fill="none" class="react-flow__connection-path" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.react-flow__connectionline {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
overflow: visible;
|
||||||
|
z-index: 1001;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__connection {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.react-flow__edge-path,
|
||||||
|
.react-flow__connection-path {
|
||||||
|
stroke: #b1b1b7;
|
||||||
|
stroke-width: 1;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as ConnectionLine } from './ConnectionLine.svelte';
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
import {
|
||||||
|
getHostForElement,
|
||||||
|
calcAutoPan,
|
||||||
|
getEventPosition,
|
||||||
|
pointToRendererPoint,
|
||||||
|
rendererPointToPoint
|
||||||
|
} from '@reactflow/utils';
|
||||||
|
import type {
|
||||||
|
OnConnect,
|
||||||
|
HandleType,
|
||||||
|
Connection,
|
||||||
|
ConnectionMode,
|
||||||
|
Node,
|
||||||
|
XYPosition,
|
||||||
|
Transform
|
||||||
|
} from '@reactflow/system';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getClosestHandle,
|
||||||
|
getConnectionStatus,
|
||||||
|
getHandleLookup,
|
||||||
|
getHandleType,
|
||||||
|
isValidHandle,
|
||||||
|
resetRecentHandle,
|
||||||
|
type ConnectionHandle,
|
||||||
|
type ValidConnectionFunc
|
||||||
|
} from './utils';
|
||||||
|
import { get, type Writable } from 'svelte/store';
|
||||||
|
import type { ConnectionData } from '$lib/types';
|
||||||
|
|
||||||
|
export function handlePointerDown({
|
||||||
|
event,
|
||||||
|
handleId,
|
||||||
|
nodeId,
|
||||||
|
onConnect,
|
||||||
|
domNode,
|
||||||
|
nodes,
|
||||||
|
connectionMode,
|
||||||
|
connectionRadius,
|
||||||
|
isTarget,
|
||||||
|
transformStore,
|
||||||
|
panBy,
|
||||||
|
updateConnection,
|
||||||
|
cancelConnection,
|
||||||
|
isValidConnection,
|
||||||
|
edgeUpdaterType,
|
||||||
|
onEdgeUpdateEnd
|
||||||
|
}: {
|
||||||
|
event: MouseEvent | TouchEvent;
|
||||||
|
handleId: string | null;
|
||||||
|
nodeId: string;
|
||||||
|
onConnect: OnConnect;
|
||||||
|
isTarget: boolean;
|
||||||
|
connectionMode: ConnectionMode;
|
||||||
|
domNode: HTMLDivElement | null;
|
||||||
|
nodes: Node[];
|
||||||
|
connectionRadius: number;
|
||||||
|
isValidConnection: ValidConnectionFunc;
|
||||||
|
transformStore: Writable<Transform>;
|
||||||
|
updateConnection: (connection: Partial<ConnectionData>) => void;
|
||||||
|
cancelConnection: () => void;
|
||||||
|
panBy: (delta: XYPosition) => void;
|
||||||
|
edgeUpdaterType?: HandleType;
|
||||||
|
onEdgeUpdateEnd?: (evt: MouseEvent | TouchEvent) => void;
|
||||||
|
}): void {
|
||||||
|
// when react-flow is used inside a shadow root we can't use document
|
||||||
|
const doc = getHostForElement(event.target as HTMLElement);
|
||||||
|
let autoPanId = 0;
|
||||||
|
let prevClosestHandle: ConnectionHandle | null;
|
||||||
|
|
||||||
|
const { x, y } = getEventPosition(event);
|
||||||
|
const clickedHandle = doc?.elementFromPoint(x, y);
|
||||||
|
const handleType = getHandleType(edgeUpdaterType, clickedHandle);
|
||||||
|
const containerBounds = domNode?.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (!containerBounds || !handleType) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let prevActiveHandle: Element;
|
||||||
|
let connectionPosition = getEventPosition(event, containerBounds);
|
||||||
|
let autoPanStarted = false;
|
||||||
|
let connection: Connection | null = null;
|
||||||
|
let isValid = false;
|
||||||
|
let handleDomNode: Element | null = null;
|
||||||
|
|
||||||
|
const autoPanOnConnect = true;
|
||||||
|
|
||||||
|
const handleLookup = getHandleLookup({
|
||||||
|
nodes,
|
||||||
|
nodeId,
|
||||||
|
handleId,
|
||||||
|
handleType
|
||||||
|
});
|
||||||
|
|
||||||
|
// when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas
|
||||||
|
const autoPan = (): void => {
|
||||||
|
// @todd add prop
|
||||||
|
if (!autoPanOnConnect) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [xMovement, yMovement] = calcAutoPan(connectionPosition, containerBounds);
|
||||||
|
|
||||||
|
panBy({ x: xMovement, y: yMovement });
|
||||||
|
autoPanId = requestAnimationFrame(autoPan);
|
||||||
|
};
|
||||||
|
|
||||||
|
updateConnection({
|
||||||
|
position: connectionPosition,
|
||||||
|
nodeId,
|
||||||
|
handleId,
|
||||||
|
handleType,
|
||||||
|
status: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// @todo add prop
|
||||||
|
// onConnectStart?.(event, { nodeId, handleId, handleType });
|
||||||
|
|
||||||
|
function onPointerMove(event: MouseEvent | TouchEvent) {
|
||||||
|
const transform = get(transformStore);
|
||||||
|
connectionPosition = getEventPosition(event, containerBounds);
|
||||||
|
|
||||||
|
prevClosestHandle = getClosestHandle(
|
||||||
|
pointToRendererPoint(connectionPosition, transform, false, [1, 1]),
|
||||||
|
connectionRadius,
|
||||||
|
handleLookup
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!autoPanStarted) {
|
||||||
|
autoPan();
|
||||||
|
autoPanStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = isValidHandle(
|
||||||
|
event,
|
||||||
|
prevClosestHandle,
|
||||||
|
connectionMode,
|
||||||
|
nodeId,
|
||||||
|
handleId,
|
||||||
|
isTarget ? 'target' : 'source',
|
||||||
|
isValidConnection,
|
||||||
|
doc
|
||||||
|
);
|
||||||
|
|
||||||
|
handleDomNode = result.handleDomNode;
|
||||||
|
connection = result.connection;
|
||||||
|
isValid = result.isValid;
|
||||||
|
|
||||||
|
updateConnection({
|
||||||
|
position:
|
||||||
|
prevClosestHandle && isValid
|
||||||
|
? rendererPointToPoint(
|
||||||
|
{
|
||||||
|
x: prevClosestHandle.x,
|
||||||
|
y: prevClosestHandle.y
|
||||||
|
},
|
||||||
|
transform
|
||||||
|
)
|
||||||
|
: connectionPosition,
|
||||||
|
status: getConnectionStatus(!!prevClosestHandle, isValid)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!prevClosestHandle && !isValid && !handleDomNode) {
|
||||||
|
return resetRecentHandle(prevActiveHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connection.source !== connection.target && handleDomNode) {
|
||||||
|
resetRecentHandle(prevActiveHandle);
|
||||||
|
prevActiveHandle = handleDomNode;
|
||||||
|
// @todo: remove the old class names "react-flow__handle-" in the next major version
|
||||||
|
handleDomNode.classList.add('connecting');
|
||||||
|
handleDomNode.classList.toggle('valid', isValid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerUp(event: MouseEvent | TouchEvent) {
|
||||||
|
if ((prevClosestHandle || handleDomNode) && connection && isValid) {
|
||||||
|
onConnect?.(connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// it's important to get a fresh reference from the store here
|
||||||
|
// in order to get the latest state of onConnectEnd
|
||||||
|
// @todo add onConnectEnd prop
|
||||||
|
// getState().onConnectEnd?.(event);
|
||||||
|
|
||||||
|
if (edgeUpdaterType) {
|
||||||
|
onEdgeUpdateEnd?.(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetRecentHandle(prevActiveHandle);
|
||||||
|
cancelConnection();
|
||||||
|
cancelAnimationFrame(autoPanId);
|
||||||
|
autoPanStarted = false;
|
||||||
|
isValid = false;
|
||||||
|
connection = null;
|
||||||
|
handleDomNode = null;
|
||||||
|
|
||||||
|
doc.removeEventListener('mousemove', onPointerMove as EventListener);
|
||||||
|
doc.removeEventListener('mouseup', onPointerUp as EventListener);
|
||||||
|
|
||||||
|
doc.removeEventListener('touchmove', onPointerMove as EventListener);
|
||||||
|
doc.removeEventListener('touchend', onPointerUp as EventListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.addEventListener('mousemove', onPointerMove as EventListener);
|
||||||
|
doc.addEventListener('mouseup', onPointerUp as EventListener);
|
||||||
|
|
||||||
|
doc.addEventListener('touchmove', onPointerMove as EventListener);
|
||||||
|
doc.addEventListener('touchend', onPointerUp as EventListener);
|
||||||
|
}
|
||||||
@@ -1,21 +1,77 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext } from 'svelte';
|
import { getContext } from 'svelte';
|
||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { type HandleType, Position } from '@reactflow/system';
|
import { Position, type Connection, type HandleProps } from '@reactflow/system';
|
||||||
|
import { isMouseEvent } from '@reactflow/utils';
|
||||||
|
|
||||||
export let type: HandleType = 'source';
|
import { handlePointerDown } from './handler';
|
||||||
export let position: Position = Position.Top;
|
import { useStore } from '$lib/store';
|
||||||
export let id: string | null = null;
|
|
||||||
|
type $$Props = HandleProps;
|
||||||
|
|
||||||
|
export let type: $$Props['type'] = 'source';
|
||||||
|
export let position: $$Props['position'] = Position.Top;
|
||||||
|
export let id: $$Props['id'] = undefined;
|
||||||
|
export let isConnectable: $$Props['isConnectable'] = true;
|
||||||
|
export let isValidConnection: $$Props['isValidConnection'] = (_: Connection) => true;
|
||||||
let className: string | null = null;
|
let className: string | null = null;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
const isTarget = type === 'target';
|
const isTarget = type === 'target';
|
||||||
const nodeId = getContext('rf_nodeid');
|
const nodeId = getContext<string>('rf_nodeid');
|
||||||
|
const handleId = id || null;
|
||||||
|
|
||||||
|
const {
|
||||||
|
connectionModeStore,
|
||||||
|
domNodeStore,
|
||||||
|
nodesStore,
|
||||||
|
connectionRadiusStore,
|
||||||
|
transformStore,
|
||||||
|
addEdge,
|
||||||
|
panBy,
|
||||||
|
cancelConnection,
|
||||||
|
updateConnection
|
||||||
|
} = useStore();
|
||||||
|
|
||||||
|
function onConnectExtended(params: Connection) {
|
||||||
|
addEdge(params);
|
||||||
|
// @todo add props
|
||||||
|
// onConnectAction?.(edgeParams);
|
||||||
|
// onConnect?.(edgeParams);
|
||||||
|
};
|
||||||
|
|
||||||
|
function onPointerDown(event: MouseEvent | TouchEvent) {
|
||||||
|
const isMouseTriggered = isMouseEvent(event);
|
||||||
|
|
||||||
|
if ((isMouseTriggered && event.button === 0) || !isMouseTriggered) {
|
||||||
|
handlePointerDown({
|
||||||
|
event,
|
||||||
|
handleId,
|
||||||
|
nodeId,
|
||||||
|
isTarget,
|
||||||
|
connectionRadius: $connectionRadiusStore,
|
||||||
|
domNode: $domNodeStore,
|
||||||
|
nodes: $nodesStore,
|
||||||
|
connectionMode: $connectionModeStore,
|
||||||
|
transformStore,
|
||||||
|
isValidConnection: isValidConnection!,
|
||||||
|
onConnect: onConnectExtended,
|
||||||
|
updateConnection,
|
||||||
|
cancelConnection,
|
||||||
|
panBy,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (isMouseTriggered) {
|
||||||
|
// onMouseDown?.(event);
|
||||||
|
// } else {
|
||||||
|
// onTouchStart?.(event);
|
||||||
|
// }
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
data-handleid={id}
|
data-handleid={handleId}
|
||||||
data-nodeid={nodeId}
|
data-nodeid={nodeId}
|
||||||
data-handlepos={position}
|
data-handlepos={position}
|
||||||
data-id={`${nodeId}-${id}-${type}`}
|
data-id={`${nodeId}-${id}-${type}`}
|
||||||
@@ -23,12 +79,16 @@
|
|||||||
'react-flow__handle',
|
'react-flow__handle',
|
||||||
`react-flow__handle-${position}`,
|
`react-flow__handle-${position}`,
|
||||||
'nodrag',
|
'nodrag',
|
||||||
|
'nopan',
|
||||||
className,
|
className,
|
||||||
position
|
position
|
||||||
])}
|
])}
|
||||||
class:source={!isTarget}
|
class:source={!isTarget}
|
||||||
class:target={isTarget}
|
class:target={isTarget}
|
||||||
>
|
class:connectable={isConnectable}
|
||||||
|
on:mousedown={onPointerDown}
|
||||||
|
on:touchstart={onPointerDown}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -46,6 +106,11 @@
|
|||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connectable {
|
||||||
|
pointer-events: all;
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
import { internalsSymbol, ConnectionMode, type ConnectionStatus } from '@reactflow/system';
|
||||||
|
import type { Connection, HandleType, XYPosition, Node, NodeHandleBounds } from '@reactflow/system';
|
||||||
|
import { getEventPosition } from '@reactflow/utils';
|
||||||
|
|
||||||
|
export type ConnectionHandle = {
|
||||||
|
id: string | null;
|
||||||
|
type: HandleType;
|
||||||
|
nodeId: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidConnectionFunc = (connection: Connection) => boolean;
|
||||||
|
|
||||||
|
// this functions collects all handles and adds an absolute position
|
||||||
|
// so that we can later find the closest handle to the mouse position
|
||||||
|
export function getHandles(
|
||||||
|
node: Node,
|
||||||
|
handleBounds: NodeHandleBounds,
|
||||||
|
type: HandleType,
|
||||||
|
currentHandle: string
|
||||||
|
): ConnectionHandle[] {
|
||||||
|
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, h) => {
|
||||||
|
if (`${node.id}-${h.id}-${type}` !== currentHandle) {
|
||||||
|
res.push({
|
||||||
|
id: h.id || null,
|
||||||
|
type,
|
||||||
|
nodeId: node.id,
|
||||||
|
x: (node.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
|
||||||
|
y: (node.positionAbsolute?.y ?? 0) + h.y + h.height / 2
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getClosestHandle(
|
||||||
|
pos: XYPosition,
|
||||||
|
connectionRadius: number,
|
||||||
|
handles: ConnectionHandle[]
|
||||||
|
): ConnectionHandle | null {
|
||||||
|
let closestHandle: ConnectionHandle | null = null;
|
||||||
|
let minDistance = Infinity;
|
||||||
|
|
||||||
|
handles.forEach((handle) => {
|
||||||
|
const distance = Math.sqrt(Math.pow(handle.x - pos.x, 2) + Math.pow(handle.y - pos.y, 2));
|
||||||
|
if (distance <= connectionRadius && distance < minDistance) {
|
||||||
|
minDistance = distance;
|
||||||
|
closestHandle = handle;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return closestHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result = {
|
||||||
|
handleDomNode: Element | null;
|
||||||
|
isValid: boolean;
|
||||||
|
connection: Connection;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nullConnection: Connection = {
|
||||||
|
source: null,
|
||||||
|
target: null,
|
||||||
|
sourceHandle: null,
|
||||||
|
targetHandle: null
|
||||||
|
};
|
||||||
|
|
||||||
|
// checks if and returns connection in fom of an object { source: 123, target: 312 }
|
||||||
|
export function isValidHandle(
|
||||||
|
event: MouseEvent | TouchEvent,
|
||||||
|
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null,
|
||||||
|
connectionMode: ConnectionMode,
|
||||||
|
fromNodeId: string,
|
||||||
|
fromHandleId: string | null,
|
||||||
|
fromType: string,
|
||||||
|
isValidConnection: ValidConnectionFunc,
|
||||||
|
doc: Document | ShadowRoot
|
||||||
|
) {
|
||||||
|
const isTarget = fromType === 'target';
|
||||||
|
const handleDomNode = doc.querySelector(
|
||||||
|
`.react-flow__handle[data-id="${handle?.nodeId}-${handle?.id}-${handle?.type}"]`
|
||||||
|
);
|
||||||
|
const { x, y } = getEventPosition(event);
|
||||||
|
const handleBelow = doc.elementFromPoint(x, y);
|
||||||
|
const handleToCheck = handleBelow?.classList.contains('react-flow__handle')
|
||||||
|
? handleBelow
|
||||||
|
: handleDomNode;
|
||||||
|
|
||||||
|
const result: Result = {
|
||||||
|
handleDomNode: handleToCheck,
|
||||||
|
isValid: false,
|
||||||
|
connection: nullConnection
|
||||||
|
};
|
||||||
|
|
||||||
|
if (handleToCheck) {
|
||||||
|
const handleType = getHandleType(undefined, handleToCheck);
|
||||||
|
const handleNodeId = handleToCheck.getAttribute('data-nodeid');
|
||||||
|
const handleId = handleToCheck.getAttribute('data-handleid');
|
||||||
|
|
||||||
|
const connection: Connection = {
|
||||||
|
source: isTarget ? handleNodeId : fromNodeId,
|
||||||
|
sourceHandle: isTarget ? handleId : fromHandleId,
|
||||||
|
target: isTarget ? fromNodeId : handleNodeId,
|
||||||
|
targetHandle: isTarget ? fromHandleId : handleId
|
||||||
|
};
|
||||||
|
|
||||||
|
result.connection = connection;
|
||||||
|
|
||||||
|
// in strict mode we don't allow target to target or source to source connections
|
||||||
|
const isValid =
|
||||||
|
connectionMode === ConnectionMode.Strict
|
||||||
|
? (isTarget && handleType === 'source') || (!isTarget && handleType === 'target')
|
||||||
|
: handleNodeId !== fromNodeId || handleId !== fromHandleId;
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
result.isValid = isValidConnection(connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetHandleLookupParams = {
|
||||||
|
nodes: Node[];
|
||||||
|
nodeId: string;
|
||||||
|
handleId: string | null;
|
||||||
|
handleType: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
|
||||||
|
return nodes.reduce<ConnectionHandle[]>((res, node) => {
|
||||||
|
if (node[internalsSymbol]) {
|
||||||
|
const { handleBounds } = node[internalsSymbol];
|
||||||
|
let sourceHandles: ConnectionHandle[] = [];
|
||||||
|
let targetHandles: ConnectionHandle[] = [];
|
||||||
|
|
||||||
|
if (handleBounds) {
|
||||||
|
sourceHandles = getHandles(
|
||||||
|
node,
|
||||||
|
handleBounds,
|
||||||
|
'source',
|
||||||
|
`${nodeId}-${handleId}-${handleType}`
|
||||||
|
);
|
||||||
|
targetHandles = getHandles(
|
||||||
|
node,
|
||||||
|
handleBounds,
|
||||||
|
'target',
|
||||||
|
`${nodeId}-${handleId}-${handleType}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.push(...sourceHandles, ...targetHandles);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHandleType(
|
||||||
|
edgeUpdaterType: HandleType | undefined,
|
||||||
|
handleDomNode: Element | null
|
||||||
|
): HandleType | null {
|
||||||
|
if (edgeUpdaterType) {
|
||||||
|
return edgeUpdaterType;
|
||||||
|
} else if (handleDomNode?.classList.contains('target')) {
|
||||||
|
return 'target';
|
||||||
|
} else if (handleDomNode?.classList.contains('source')) {
|
||||||
|
return 'source';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetRecentHandle(handleDomNode: Element): void {
|
||||||
|
handleDomNode?.classList.remove('valid', 'connecting');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
|
||||||
|
let connectionStatus = null;
|
||||||
|
|
||||||
|
if (isHandleValid) {
|
||||||
|
connectionStatus = 'valid';
|
||||||
|
} else if (isInsideConnectionRadius && !isHandleValid) {
|
||||||
|
connectionStatus = 'invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
return connectionStatus as ConnectionStatus;
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
import UserSelection from '$lib/components/UserSelection/index.svelte';
|
import UserSelection from '$lib/components/UserSelection/index.svelte';
|
||||||
import NodeSelection from '$lib/components/NodeSelection/index.svelte';
|
import NodeSelection from '$lib/components/NodeSelection/index.svelte';
|
||||||
import { KeyHandler } from '$lib/components/KeyHandler';
|
import { KeyHandler } from '$lib/components/KeyHandler';
|
||||||
|
import { ConnectionLine } from '$lib/components/ConnectionLine';
|
||||||
import type { SvelteFlowProps } from '$lib/types';
|
import type { SvelteFlowProps } from '$lib/types';
|
||||||
|
|
||||||
type $$Props = SvelteFlowProps;
|
type $$Props = SvelteFlowProps;
|
||||||
@@ -26,7 +27,7 @@
|
|||||||
let className: $$Props['class'] = undefined;
|
let className: $$Props['class'] = undefined;
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
let domNode: Element;
|
let domNode: HTMLDivElement;
|
||||||
|
|
||||||
const store = createStore({
|
const store = createStore({
|
||||||
fitView,
|
fitView,
|
||||||
@@ -41,6 +42,7 @@
|
|||||||
const { width, height } = domNode.getBoundingClientRect();
|
const { width, height } = domNode.getBoundingClientRect();
|
||||||
store.widthStore.set(width);
|
store.widthStore.set(width);
|
||||||
store.heightStore.set(height);
|
store.heightStore.set(height);
|
||||||
|
store.domNodeStore.set(domNode);
|
||||||
|
|
||||||
// @todo: is this a svelte way for two way binding?
|
// @todo: is this a svelte way for two way binding?
|
||||||
store.nodesStore.subscribe((ns) => {
|
store.nodesStore.subscribe((ns) => {
|
||||||
@@ -69,6 +71,7 @@
|
|||||||
<Pane>
|
<Pane>
|
||||||
<Viewport>
|
<Viewport>
|
||||||
<EdgeRenderer />
|
<EdgeRenderer />
|
||||||
|
<ConnectionLine />
|
||||||
<div class="react-flow__edgelabel-renderer" />
|
<div class="react-flow__edgelabel-renderer" />
|
||||||
<NodeRenderer />
|
<NodeRenderer />
|
||||||
<NodeSelection />
|
<NodeSelection />
|
||||||
|
|||||||
@@ -12,19 +12,32 @@ import {
|
|||||||
type D3SelectionInstance,
|
type D3SelectionInstance,
|
||||||
type ViewportHelperFunctionOptions,
|
type ViewportHelperFunctionOptions,
|
||||||
type SelectionRect,
|
type SelectionRect,
|
||||||
type Node as RFNode
|
type Node as RFNode,
|
||||||
|
type Connection,
|
||||||
|
ConnectionMode,
|
||||||
|
type XYPosition,
|
||||||
|
type CoordinateExtent,
|
||||||
|
ConnectionLineType
|
||||||
} from '@reactflow/system';
|
} from '@reactflow/system';
|
||||||
import { fitView, getConnectedEdges, getD3Transition, getDimensions } from '@reactflow/utils';
|
import {
|
||||||
|
fitView as fitViewUtil,
|
||||||
|
getConnectedEdges,
|
||||||
|
getD3Transition,
|
||||||
|
getDimensions,
|
||||||
|
addEdge as addEdgeUtil
|
||||||
|
} from '@reactflow/utils';
|
||||||
|
import { zoomIdentity } from 'd3-zoom';
|
||||||
|
|
||||||
import { getHandleBounds } from '../../utils';
|
import { getHandleBounds } from '../../utils';
|
||||||
import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils';
|
import { getEdgePositions, getHandle, getNodeData } from '$lib/container/EdgeRenderer/utils';
|
||||||
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
|
||||||
import InputNode from '$lib/components/nodes/InputNode.svelte';
|
import InputNode from '$lib/components/nodes/InputNode.svelte';
|
||||||
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
|
import OutputNode from '$lib/components/nodes/OutputNode.svelte';
|
||||||
import type { EdgeTypes, NodeTypes, Node, Edge, WrapEdgeProps } from '$lib/types';
|
import type { EdgeTypes, NodeTypes, Node, Edge, WrapEdgeProps, ConnectionData } from '$lib/types';
|
||||||
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
|
import BezierEdge from '$lib/components/edges/BezierEdge.svelte';
|
||||||
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
|
import StraightEdge from '$lib/components/edges/StraightEdge.svelte';
|
||||||
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
|
import SmoothStepEdge from '$lib/components/edges/SmoothStepEdge.svelte';
|
||||||
|
import { getBezierPath, getSmoothStepPath, getStraightPath } from '@reactflow/edge-utils';
|
||||||
|
|
||||||
export const key = Symbol();
|
export const key = Symbol();
|
||||||
|
|
||||||
@@ -58,8 +71,14 @@ type SvelteFlowStore = {
|
|||||||
deleteKeyPressedStore: Writable<boolean>;
|
deleteKeyPressedStore: Writable<boolean>;
|
||||||
nodeTypesStore: Writable<NodeTypes>;
|
nodeTypesStore: Writable<NodeTypes>;
|
||||||
edgeTypesStore: Writable<EdgeTypes>;
|
edgeTypesStore: Writable<EdgeTypes>;
|
||||||
|
domNodeStore: Writable<HTMLDivElement | null>;
|
||||||
|
connectionRadiusStore: Writable<number>;
|
||||||
|
connectionModeStore: Writable<ConnectionMode>;
|
||||||
|
connectionStore: Writable<ConnectionData>;
|
||||||
|
connectionPathStore: Readable<string | null>;
|
||||||
setNodes: (nodes: Node[]) => void;
|
setNodes: (nodes: Node[]) => void;
|
||||||
setEdges: (edges: Edge[]) => void;
|
setEdges: (edges: Edge[]) => void;
|
||||||
|
addEdge: (edge: Edge | Connection) => void;
|
||||||
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
|
zoomIn: (options?: ViewportHelperFunctionOptions) => void;
|
||||||
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
|
zoomOut: (options?: ViewportHelperFunctionOptions) => void;
|
||||||
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
|
fitView: (options?: ViewportHelperFunctionOptions) => boolean;
|
||||||
@@ -71,6 +90,17 @@ type SvelteFlowStore = {
|
|||||||
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
updateNodeDimensions: (updates: NodeDimensionUpdate[]) => void;
|
||||||
resetSelectedElements: () => void;
|
resetSelectedElements: () => void;
|
||||||
addSelectedNodes: (ids: string[]) => void;
|
addSelectedNodes: (ids: string[]) => void;
|
||||||
|
panBy: (delta: XYPosition) => void;
|
||||||
|
updateConnection: (connection: Partial<ConnectionData>) => void;
|
||||||
|
cancelConnection: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const initConnectionData = {
|
||||||
|
nodeId: null,
|
||||||
|
handleId: null,
|
||||||
|
handleType: null,
|
||||||
|
position: null,
|
||||||
|
status: null
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createStore({
|
export function createStore({
|
||||||
@@ -112,6 +142,11 @@ export function createStore({
|
|||||||
default: edgeTypes.default || BezierEdge
|
default: edgeTypes.default || BezierEdge
|
||||||
});
|
});
|
||||||
const transformStore = writable(transform);
|
const transformStore = writable(transform);
|
||||||
|
const connectionModeStore = writable(ConnectionMode.Strict);
|
||||||
|
const domNodeStore = writable(null);
|
||||||
|
const connectionStore = writable<ConnectionData>(initConnectionData);
|
||||||
|
const connectionRadiusStore = writable(25);
|
||||||
|
const connectionLineTypeStore = writable(ConnectionLineType.Bezier);
|
||||||
|
|
||||||
let fitViewOnInitDone = false;
|
let fitViewOnInitDone = false;
|
||||||
|
|
||||||
@@ -172,10 +207,84 @@ export function createStore({
|
|||||||
.filter((e) => e !== null) as WrapEdgeProps[];
|
.filter((e) => e !== null) as WrapEdgeProps[];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const oppositePosition = {
|
||||||
|
[Position.Left]: Position.Right,
|
||||||
|
[Position.Right]: Position.Left,
|
||||||
|
[Position.Top]: Position.Bottom,
|
||||||
|
[Position.Bottom]: Position.Top
|
||||||
|
};
|
||||||
|
|
||||||
|
const connectionPathStore = derived(
|
||||||
|
[connectionStore, connectionLineTypeStore, connectionModeStore, nodesStore, transformStore],
|
||||||
|
([
|
||||||
|
$connectionStore,
|
||||||
|
$connectionLineTypeStore,
|
||||||
|
$connectionModeStore,
|
||||||
|
$nodesStore,
|
||||||
|
$transformStore
|
||||||
|
]) => {
|
||||||
|
if (!$connectionStore.nodeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fromNode = $nodesStore.find((n) => n.id === $connectionStore.nodeId);
|
||||||
|
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
|
||||||
|
const handleBoundsStrict = fromHandleBounds?.[$connectionStore.handleType || 'source'] || [];
|
||||||
|
const handleBoundsLoose = handleBoundsStrict
|
||||||
|
? handleBoundsStrict
|
||||||
|
: fromHandleBounds?.[$connectionStore.handleType === 'source' ? 'target' : 'source']!;
|
||||||
|
const handleBounds =
|
||||||
|
$connectionModeStore === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
|
||||||
|
const fromHandle = $connectionStore.handleId
|
||||||
|
? handleBounds.find((d) => d.id === $connectionStore.handleId)
|
||||||
|
: handleBounds[0];
|
||||||
|
const fromHandleX = fromHandle
|
||||||
|
? fromHandle.x + fromHandle.width / 2
|
||||||
|
: (fromNode?.width ?? 0) / 2;
|
||||||
|
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode?.height ?? 0;
|
||||||
|
const fromX = (fromNode?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||||
|
const fromY = (fromNode?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||||
|
const fromPosition = fromHandle?.position;
|
||||||
|
const toPosition = fromPosition ? oppositePosition[fromPosition] : undefined;
|
||||||
|
|
||||||
|
const pathParams = {
|
||||||
|
sourceX: fromX,
|
||||||
|
sourceY: fromY,
|
||||||
|
sourcePosition: fromPosition,
|
||||||
|
targetX: (($connectionStore.position?.x ?? 0) - $transformStore[0]) / $transformStore[2],
|
||||||
|
targetY: (($connectionStore.position?.y ?? 0) - $transformStore[1]) / $transformStore[2],
|
||||||
|
targetPosition: toPosition
|
||||||
|
};
|
||||||
|
|
||||||
|
let path = '';
|
||||||
|
|
||||||
|
if ($connectionLineTypeStore === ConnectionLineType.Bezier) {
|
||||||
|
// we assume the destination position is opposite to the source position
|
||||||
|
[path] = getBezierPath(pathParams);
|
||||||
|
} else if ($connectionLineTypeStore === ConnectionLineType.Step) {
|
||||||
|
[path] = getSmoothStepPath({
|
||||||
|
...pathParams,
|
||||||
|
borderRadius: 0
|
||||||
|
});
|
||||||
|
} else if ($connectionLineTypeStore === ConnectionLineType.SmoothStep) {
|
||||||
|
[path] = getSmoothStepPath(pathParams);
|
||||||
|
} else {
|
||||||
|
[path] = getStraightPath(pathParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
function setEdges(edges: Edge[]) {
|
function setEdges(edges: Edge[]) {
|
||||||
edgesStore.set(edges);
|
edgesStore.set(edges);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addEdge(edgeParams: Edge | Connection) {
|
||||||
|
const edges = get(edgesStore);
|
||||||
|
edgesStore.set(addEdgeUtil(edgeParams, edges));
|
||||||
|
}
|
||||||
|
|
||||||
function setNodes(nodes: Node[]) {
|
function setNodes(nodes: Node[]) {
|
||||||
nodesStore.update((currentNodes) => {
|
nodesStore.update((currentNodes) => {
|
||||||
const nextNodes = nodes.map((n) => {
|
const nextNodes = nodes.map((n) => {
|
||||||
@@ -254,7 +363,7 @@ export function createStore({
|
|||||||
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
|
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
|
||||||
|
|
||||||
fitViewOnInitDone =
|
fitViewOnInitDone =
|
||||||
fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && _fitView());
|
fitViewOnInitDone || (fitViewOnInit && !!d3Zoom && !!d3Selection && fitView());
|
||||||
|
|
||||||
nodesStore.set(nextNodes);
|
nodesStore.set(nextNodes);
|
||||||
}
|
}
|
||||||
@@ -274,14 +383,14 @@ export function createStore({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _fitView() {
|
function fitView() {
|
||||||
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
|
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
|
||||||
|
|
||||||
if (!d3Zoom || !d3Selection) {
|
if (!d3Zoom || !d3Selection) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return fitView(
|
return fitViewUtil(
|
||||||
{
|
{
|
||||||
nodes: get(nodesStore) as RFNode[],
|
nodes: get(nodesStore) as RFNode[],
|
||||||
width: get(widthStore),
|
width: get(widthStore),
|
||||||
@@ -373,6 +482,52 @@ export function createStore({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function panBy(delta: XYPosition) {
|
||||||
|
const { zoom: d3Zoom, selection: d3Selection } = get(d3Store);
|
||||||
|
const transform = get(transformStore);
|
||||||
|
const width = get(widthStore);
|
||||||
|
const height = get(heightStore);
|
||||||
|
|
||||||
|
if (!d3Zoom || !d3Selection || (!delta.x && !delta.y)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nextTransform = zoomIdentity
|
||||||
|
.translate(transform[0] + delta.x, transform[1] + delta.y)
|
||||||
|
.scale(transform[2]);
|
||||||
|
|
||||||
|
const extent: CoordinateExtent = [
|
||||||
|
[0, 0],
|
||||||
|
[width, height]
|
||||||
|
];
|
||||||
|
|
||||||
|
const constrainedTransform = d3Zoom?.constrain()(nextTransform, extent, [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY]
|
||||||
|
]);
|
||||||
|
d3Zoom.transform(d3Selection, constrainedTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateConnection(connectionUpdate: Partial<ConnectionData> | null) {
|
||||||
|
const currentConnectionData = get(connectionStore);
|
||||||
|
const nextConnectionData = currentConnectionData
|
||||||
|
? {
|
||||||
|
...initConnectionData,
|
||||||
|
...currentConnectionData,
|
||||||
|
...connectionUpdate
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
...initConnectionData,
|
||||||
|
...connectionUpdate
|
||||||
|
};
|
||||||
|
|
||||||
|
connectionStore.set(nextConnectionData);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelConnection() {
|
||||||
|
updateConnection(initConnectionData);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodesStore,
|
nodesStore,
|
||||||
edgesStore,
|
edgesStore,
|
||||||
@@ -391,15 +546,24 @@ export function createStore({
|
|||||||
selectionMode,
|
selectionMode,
|
||||||
nodeTypesStore,
|
nodeTypesStore,
|
||||||
edgeTypesStore,
|
edgeTypesStore,
|
||||||
|
connectionModeStore,
|
||||||
|
domNodeStore,
|
||||||
|
connectionStore,
|
||||||
|
connectionRadiusStore,
|
||||||
|
connectionPathStore,
|
||||||
setNodes,
|
setNodes,
|
||||||
setEdges,
|
setEdges,
|
||||||
|
addEdge,
|
||||||
updateNodePositions,
|
updateNodePositions,
|
||||||
updateNodeDimensions,
|
updateNodeDimensions,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
fitView: _fitView,
|
fitView,
|
||||||
resetSelectedElements,
|
resetSelectedElements,
|
||||||
addSelectedNodes
|
addSelectedNodes,
|
||||||
|
panBy,
|
||||||
|
updateConnection,
|
||||||
|
cancelConnection
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import type { Node, NodeTypes } from './nodes';
|
import type { Node, NodeTypes } from './nodes';
|
||||||
import type { Edge } from './edges';
|
|
||||||
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
|
||||||
|
import type { Edge, HandleType, XYPosition } from '@reactflow/system';
|
||||||
|
|
||||||
export type KeyModifier = ShortcutModifierDefinition;
|
export type KeyModifier = ShortcutModifierDefinition;
|
||||||
export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
|
export type KeyDefinitionObject = { key: string; modifier?: KeyModifier };
|
||||||
export type KeyDefinition = string | KeyDefinitionObject;
|
export type KeyDefinition = string | KeyDefinitionObject;
|
||||||
|
|
||||||
|
export type ConnectionData = {
|
||||||
|
position: XYPosition | null;
|
||||||
|
nodeId: string | null;
|
||||||
|
handleId: string | null;
|
||||||
|
handleType: HandleType | null;
|
||||||
|
status: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ConnectionLineProps = {};
|
||||||
|
|
||||||
export type SvelteFlowProps = {
|
export type SvelteFlowProps = {
|
||||||
nodes: Node[];
|
nodes: Node[];
|
||||||
edges: Edge[];
|
edges: Edge[];
|
||||||
|
|||||||
Reference in New Issue
Block a user