feat(svelte): add connectionline and connection functionality
This commit is contained in:
@@ -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">
|
||||
import { getContext } from 'svelte';
|
||||
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';
|
||||
export let position: Position = Position.Top;
|
||||
export let id: string | null = null;
|
||||
|
||||
import { handlePointerDown } from './handler';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
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;
|
||||
export { className as class };
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
data-handleid={id}
|
||||
data-handleid={handleId}
|
||||
data-nodeid={nodeId}
|
||||
data-handlepos={position}
|
||||
data-id={`${nodeId}-${id}-${type}`}
|
||||
@@ -23,12 +79,16 @@
|
||||
'react-flow__handle',
|
||||
`react-flow__handle-${position}`,
|
||||
'nodrag',
|
||||
'nopan',
|
||||
className,
|
||||
position
|
||||
])}
|
||||
class:source={!isTarget}
|
||||
class:target={isTarget}
|
||||
>
|
||||
class:connectable={isConnectable}
|
||||
on:mousedown={onPointerDown}
|
||||
on:touchstart={onPointerDown}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -46,6 +106,11 @@
|
||||
border-radius: 100%;
|
||||
}
|
||||
|
||||
.connectable {
|
||||
pointer-events: all;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
top: 100%;
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user