Merge pull request #4400 from xyflow/next

React Flow 12.0.0-next.25 & Svelte Flow 0.1.8
This commit is contained in:
Moritz Klack
2024-07-01 12:40:11 +02:00
committed by GitHub
68 changed files with 902 additions and 952 deletions
@@ -37,6 +37,7 @@ const initialNodes: Node[] = [
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'default resizer' }, data: { label: 'default resizer' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
origin: [1, 1],
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{ {
@@ -127,7 +128,7 @@ const initialNodes: Node[] = [
data: { label: 'Parent', keepAspectRatio: true }, data: { label: 'Parent', keepAspectRatio: true },
position: { x: 700, y: 0 }, position: { x: 700, y: 0 },
width: 300, width: 300,
height: 400, height: 300,
style: { ...nodeStyle }, style: { ...nodeStyle },
}, },
{ {
@@ -147,7 +148,9 @@ const initialNodes: Node[] = [
id: '5b', id: '5b',
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent' }, data: { label: 'Child with expandParent' },
position: { x: 150, y: 100 }, position: { x: 100, y: 100 },
width: 100,
height: 100,
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
@@ -155,8 +158,10 @@ const initialNodes: Node[] = [
{ {
id: '5c', id: '5c',
type: 'defaultResizer', type: 'defaultResizer',
data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true }, data: { label: 'Child with expandParent & keepAspectRatio' },
position: { x: 25, y: 200 }, position: { x: 250, y: 200 },
height: 100,
width: 100,
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
style: { ...nodeStyle }, style: { ...nodeStyle },
@@ -1,6 +1,6 @@
import React, { memo, FC, CSSProperties } from 'react'; import React, { memo, FC, CSSProperties } from 'react';
import { Handle, NodeProps, Position } from '@xyflow/react'; import { Handle, NodeProps, Position, useInternalNode } from '@xyflow/react';
const infoStyle: CSSProperties = { fontSize: 11 }; const infoStyle: CSSProperties = { fontSize: 11 };
const idStyle: CSSProperties = { const idStyle: CSSProperties = {
@@ -12,6 +12,8 @@ const idStyle: CSSProperties = {
}; };
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => { const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
const node = useInternalNode(id)!;
return ( return (
<> <>
<Handle type="target" position={Position.Top} /> <Handle type="target" position={Position.Top} />
@@ -19,6 +21,9 @@ const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY
<div style={infoStyle}> <div style={infoStyle}>
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex} x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
</div> </div>
<div style={infoStyle}>
x:{Math.round(node.position.x)} y:{Math.round(node.position.y)}
</div>
<Handle type="source" position={Position.Bottom} /> <Handle type="source" position={Position.Bottom} />
</> </>
); );
+11 -2
View File
@@ -14,6 +14,8 @@ import {
Background, Background,
Panel, Panel,
NodeOrigin, NodeOrigin,
useUpdateNodeInternals,
ReactFlowProvider,
} from '@xyflow/react'; } from '@xyflow/react';
import DebugNode from './DebugNode'; import DebugNode from './DebugNode';
@@ -104,7 +106,7 @@ const initialNodes: Node[] = [
{ {
id: '5b', id: '5b',
data: { label: 'Node 5b' }, data: { label: 'Node 5b' },
position: { x: 225, y: 50 }, position: { x: 200, y: 200 },
className: 'light', className: 'light',
parentId: '5', parentId: '5',
expandParent: true, expandParent: true,
@@ -151,6 +153,7 @@ const nodeTypes = {
const Subflow = () => { const Subflow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null); const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const updateNodeInternals = useUpdateNodeInternals();
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -216,6 +219,7 @@ const Subflow = () => {
onlyRenderVisibleElements={false} onlyRenderVisibleElements={false}
nodeTypes={nodeTypes} nodeTypes={nodeTypes}
fitView fitView
nodeOrigin={[0, 0]}
> >
<MiniMap /> <MiniMap />
<Controls /> <Controls />
@@ -228,9 +232,14 @@ const Subflow = () => {
<button onClick={toggleChildNodes}>toggleChildNodes</button> <button onClick={toggleChildNodes}>toggleChildNodes</button>
<button onClick={logToObject}>toObject</button> <button onClick={logToObject}>toObject</button>
<button onClick={() => setNodes(initialNodes)}>setNodes</button> <button onClick={() => setNodes(initialNodes)}>setNodes</button>
<button onClick={() => updateNodeInternals(nodes.map((node) => node.id))}>updateNodeInternals</button>
</Panel> </Panel>
</ReactFlow> </ReactFlow>
); );
}; };
export default Subflow; export default () => (
<ReactFlowProvider>
<Subflow />
</ReactFlowProvider>
);
@@ -1,9 +1,24 @@
<script lang="ts"> <script lang="ts">
import { useConnection } from '@xyflow/svelte'; import { getBezierPath, useConnection } from '@xyflow/svelte';
const connection = useConnection(); const connection = useConnection();
let path: string | null = null;
$: if ($connection.inProgress) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
[path] = getBezierPath(pathParams);
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<path d={$connection.path} fill="none" stroke={$connection.startHandle?.handleId} /> <path d={path} fill="none" stroke={$connection.fromHandle.id} />
{/if} {/if}
+7
View File
@@ -1,5 +1,12 @@
# @xyflow/react # @xyflow/react
## 12.0.0-next.25
- `useConnection` returns `ConnectionState` with `toNode` and `toHandle`
- add `toNode` and `toHandle` to custom connection line props
- node origin is part of position absolute
- refactor connection handling
## 12.0.0-next.24 ## 12.0.0-next.24
- fix `window` bug for SSR for real - fix `window` bug for SSR for real
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/react", "name": "@xyflow/react",
"version": "12.0.0-next.24", "version": "12.0.0-next.25",
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.", "description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
"keywords": [ "keywords": [
"react", "react",
@@ -25,12 +25,8 @@ const selector = (s: ReactFlowState) => {
return { return {
viewBB, viewBB,
boundingRect: boundingRect: s.nodeLookup.size > 0 ? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup), viewBB) : viewBB,
s.nodeLookup.size > 0
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
: viewBB,
rfId: s.rfId, rfId: s.rfId,
nodeOrigin: s.nodeOrigin,
panZoom: s.panZoom, panZoom: s.panZoom,
translateExtent: s.translateExtent, translateExtent: s.translateExtent,
flowWidth: s.width, flowWidth: s.width,
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
import { ComponentType, memo } from 'react'; import { ComponentType, memo } from 'react';
import { NodeOrigin, getNodeDimensions, getNodePositionWithOrigin, nodeHasDimensions } from '@xyflow/system'; import { getNodeDimensions, nodeHasDimensions } from '@xyflow/system';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
@@ -11,7 +11,6 @@ import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMa
declare const window: any; declare const window: any;
const selector = (s: ReactFlowState) => s.nodeOrigin;
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id); const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
const getAttrFunction = <NodeType extends Node>(func: any): GetMiniMapNodeAttribute<NodeType> => const getAttrFunction = <NodeType extends Node>(func: any): GetMiniMapNodeAttribute<NodeType> =>
func instanceof Function ? func : () => func; func instanceof Function ? func : () => func;
@@ -28,7 +27,6 @@ function MiniMapNodes<NodeType extends Node>({
onClick, onClick,
}: MiniMapNodesProps<NodeType>) { }: MiniMapNodesProps<NodeType>) {
const nodeIds = useStore(selectorNodeIds, shallow); const nodeIds = useStore(selectorNodeIds, shallow);
const nodeOrigin = useStore(selector);
const nodeColorFunc = getAttrFunction<NodeType>(nodeColor); const nodeColorFunc = getAttrFunction<NodeType>(nodeColor);
const nodeStrokeColorFunc = getAttrFunction<NodeType>(nodeStrokeColor); const nodeStrokeColorFunc = getAttrFunction<NodeType>(nodeStrokeColor);
const nodeClassNameFunc = getAttrFunction<NodeType>(nodeClassName); const nodeClassNameFunc = getAttrFunction<NodeType>(nodeClassName);
@@ -46,7 +44,6 @@ function MiniMapNodes<NodeType extends Node>({
<NodeComponentWrapper<NodeType> <NodeComponentWrapper<NodeType>
key={nodeId} key={nodeId}
id={nodeId} id={nodeId}
nodeOrigin={nodeOrigin}
nodeColorFunc={nodeColorFunc} nodeColorFunc={nodeColorFunc}
nodeStrokeColorFunc={nodeStrokeColorFunc} nodeStrokeColorFunc={nodeStrokeColorFunc}
nodeClassNameFunc={nodeClassNameFunc} nodeClassNameFunc={nodeClassNameFunc}
@@ -63,7 +60,6 @@ function MiniMapNodes<NodeType extends Node>({
function NodeComponentWrapperInner<NodeType extends Node>({ function NodeComponentWrapperInner<NodeType extends Node>({
id, id,
nodeOrigin,
nodeColorFunc, nodeColorFunc,
nodeStrokeColorFunc, nodeStrokeColorFunc,
nodeClassNameFunc, nodeClassNameFunc,
@@ -74,7 +70,6 @@ function NodeComponentWrapperInner<NodeType extends Node>({
onClick, onClick,
}: { }: {
id: string; id: string;
nodeOrigin: NodeOrigin;
nodeColorFunc: GetMiniMapNodeAttribute<NodeType>; nodeColorFunc: GetMiniMapNodeAttribute<NodeType>;
nodeStrokeColorFunc: GetMiniMapNodeAttribute<NodeType>; nodeStrokeColorFunc: GetMiniMapNodeAttribute<NodeType>;
nodeClassNameFunc: GetMiniMapNodeAttribute<NodeType>; nodeClassNameFunc: GetMiniMapNodeAttribute<NodeType>;
@@ -86,7 +81,7 @@ function NodeComponentWrapperInner<NodeType extends Node>({
}) { }) {
const { node, x, y } = useStore((s) => { const { node, x, y } = useStore((s) => {
const node = s.nodeLookup.get(id) as InternalNode<NodeType>; const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = node.internals.positionAbsolute;
return { return {
node, node,
@@ -67,26 +67,30 @@ function ResizeControl({
}, },
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState(); const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState();
const changes: NodeChange[] = []; const changes: NodeChange[] = [];
const nextPosition = { x: change.x, y: change.y }; const nextPosition = { x: change.x, y: change.y };
const node = nodeLookup.get(id); const node = nodeLookup.get(id);
if (node && node.expandParent && node.parentId) { if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width!;
const height = change.height ?? node.measured.height!;
const child: ParentExpandChild = { const child: ParentExpandChild = {
id: node.id, id: node.id,
parentId: node.parentId, parentId: node.parentId,
rect: { rect: {
width: change.width ?? node.measured.width!, width,
height: change.height ?? node.measured.height!, height,
...evaluateAbsolutePosition( ...evaluateAbsolutePosition(
{ {
x: change.x ?? node.position.x, x: change.x ?? node.position.x,
y: change.y ?? node.position.y, y: change.y ?? node.position.y,
}, },
{ width, height },
node.parentId, node.parentId,
nodeLookup, nodeLookup,
node.origin ?? nodeOrigin origin
), ),
}, },
}; };
@@ -94,9 +98,10 @@ function ResizeControl({
const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
// when the parent was expanded by the child node, its position will be clamped at 0,0 // when the parent was expanded by the child node, its position will be clamped at
nextPosition.x = change.x ? Math.max(0, change.x) : undefined; // 0,0 when node origin is 0,0 and to width, height if it's 1,1
nextPosition.y = change.y ? Math.max(0, change.y) : undefined; nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, change.y) : undefined;
} }
if (nextPosition.x !== undefined && nextPosition.y !== undefined) { if (nextPosition.x !== undefined && nextPosition.y !== undefined) {
@@ -1,7 +1,7 @@
import { useCallback, CSSProperties } from 'react'; import { useCallback, CSSProperties } from 'react';
import cc from 'classcat'; import cc from 'classcat';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system'; import { Position, getNodeToolbarTransform, getInternalNodesBounds, NodeLookup } from '@xyflow/system';
import { InternalNode, ReactFlowState } from '../../types'; import { InternalNode, ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
@@ -17,21 +17,24 @@ const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
a?.selected !== b?.selected || a?.selected !== b?.selected ||
a?.internals.z !== b?.internals.z; a?.internals.z !== b?.internals.z;
const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => { const nodesEqualityFn = (a: NodeLookup, b: NodeLookup) => {
if (a.length !== b.length) { if (a.size !== b.size) {
return false; return false;
} }
return !a.some((node, i) => nodeEqualityFn(node, b[i])); for (const [key, node] of a) {
if (nodeEqualityFn(node, b.get(key))) {
return false;
}
}
return true;
}; };
const storeSelector = (state: ReactFlowState) => ({ const storeSelector = (state: ReactFlowState) => ({
viewport: { x: state.transform[0],
x: state.transform[0], y: state.transform[1],
y: state.transform[1], zoom: state.transform[2],
zoom: state.transform[2],
},
nodeOrigin: state.nodeOrigin,
selectedNodesCount: state.nodes.filter((node) => node.selected).length, selectedNodesCount: state.nodes.filter((node) => node.selected).length,
}); });
@@ -49,36 +52,41 @@ export function NodeToolbar({
const contextNodeId = useNodeId(); const contextNodeId = useNodeId();
const nodesSelector = useCallback( const nodesSelector = useCallback(
(state: ReactFlowState): InternalNode[] => { (state: ReactFlowState): NodeLookup => {
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || '']; const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
const internalNodes = nodeIds.reduce<NodeLookup>((res, id) => {
return nodeIds.reduce<InternalNode[]>((acc, id) => {
const node = state.nodeLookup.get(id); const node = state.nodeLookup.get(id);
if (node) { if (node) {
acc.push(node); res.set(node.id, node);
} }
return acc;
}, []); return res;
}, new Map());
return internalNodes;
}, },
[nodeId, contextNodeId] [nodeId, contextNodeId]
); );
const nodes = useStore(nodesSelector, nodesEqualityFn); const nodes = useStore(nodesSelector, nodesEqualityFn);
const { viewport, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow); const { x, y, zoom, selectedNodesCount } = useStore(storeSelector, shallow);
// if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected // if isVisible is not set, we show the toolbar only if its node is selected and no other node is selected
const isActive = const isActive =
typeof isVisible === 'boolean' ? isVisible : nodes.length === 1 && nodes[0].selected && selectedNodesCount === 1; typeof isVisible === 'boolean'
? isVisible
: nodes.size === 1 && nodes.values().next().value.selected && selectedNodesCount === 1;
if (!isActive || !nodes.length) { if (!isActive || !nodes.size) {
return null; return null;
} }
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin }); const nodeRect = getInternalNodesBounds(nodes);
const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1)); const nodesArray = Array.from(nodes.values());
const zIndex = Math.max(...nodesArray.map((node) => node.internals.z + 1));
const wrapperStyle: CSSProperties = { const wrapperStyle: CSSProperties = {
position: 'absolute', position: 'absolute',
transform: getNodeToolbarTransform(nodeRect, viewport, position, offset, align), transform: getNodeToolbarTransform(nodeRect, { x, y, zoom }, position, offset, align),
zIndex, zIndex,
...style, ...style,
}; };
@@ -89,7 +97,8 @@ export function NodeToolbar({
style={wrapperStyle} style={wrapperStyle}
className={cc(['react-flow__node-toolbar', className])} className={cc(['react-flow__node-toolbar', className])}
{...rest} {...rest}
data-id={nodes.reduce((acc, node) => `${acc}${node.id} `, '').trim()} // @todo: check if we could only do this for non-prod envs
data-id={nodesArray.reduce((acc, node) => `${acc}${node.id} `, '').trim()}
> >
{children} {children}
</div> </div>
@@ -1,132 +1,18 @@
import { CSSProperties, useCallback } from 'react'; import { CSSProperties } from 'react';
import { shallow } from 'zustand/shallow'; import { shallow } from 'zustand/shallow';
import cc from 'classcat'; import cc from 'classcat';
import { import {
Position,
ConnectionLineType, ConnectionLineType,
ConnectionMode,
getBezierPath, getBezierPath,
getSmoothStepPath, getSmoothStepPath,
type ConnectionStatus, getConnectionStatus,
type HandleType, getStraightPath,
} from '@xyflow/system'; } from '@xyflow/system';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge'; import { getSimpleBezierPath } from '../Edges/SimpleBezierEdge';
import type { ConnectionLineComponent, ReactFlowState, ReactFlowStore } from '../../types'; import type { ConnectionLineComponent, ReactFlowState } from '../../types';
import { useConnection } from '../../hooks/useConnection';
type ConnectionLineProps = {
nodeId: string;
handleType: HandleType;
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
connectionStatus: ConnectionStatus | null;
};
const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
const ConnectionLine = ({
nodeId,
handleType,
style,
type = ConnectionLineType.Bezier,
CustomComponent,
connectionStatus,
}: ConnectionLineProps) => {
const { fromNode, handleId, toX, toY, connectionMode, endPosition, isValid } = useStore(
useCallback(
(s: ReactFlowStore) => ({
fromNode: s.nodeLookup.get(nodeId),
handleId: s.connectionStartHandle?.handleId,
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
connectionMode: s.connectionMode,
endPosition: s.connectionEndHandle?.position,
isValid: s.connectionStatus === 'valid',
}),
[nodeId]
),
shallow
);
const fromHandleBounds = fromNode?.internals.handleBounds;
let handleBounds = fromHandleBounds?.[handleType];
if (connectionMode === ConnectionMode.Loose) {
handleBounds = handleBounds ? handleBounds : fromHandleBounds?.[handleType === 'source' ? 'target' : 'source'];
}
if (!fromNode || !handleBounds) {
return null;
}
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.measured.width ?? 0) / 2;
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.measured.height ?? 0;
const fromX = fromNode.internals.positionAbsolute.x + fromHandleX;
const fromY = fromNode.internals.positionAbsolute.y + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition = isValid && endPosition ? endPosition : fromPosition ? oppositePosition[fromPosition] : null;
if (!fromPosition || !toPosition) {
return null;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={fromX}
fromY={fromY}
toX={toX}
toY={toY}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={connectionStatus}
/>
);
}
let dAttr = '';
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
};
if (type === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[dAttr] = getBezierPath(pathParams);
} else if (type === ConnectionLineType.Step) {
[dAttr] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
} else if (type === ConnectionLineType.SmoothStep) {
[dAttr] = getSmoothStepPath(pathParams);
} else if (type === ConnectionLineType.SimpleBezier) {
[dAttr] = getSimpleBezierPath(pathParams);
} else {
dAttr = `M${fromX},${fromY} ${toX},${toY}`;
}
return <path d={dAttr} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
type ConnectionLineWrapperProps = { type ConnectionLineWrapperProps = {
type: ConnectionLineType; type: ConnectionLineType;
@@ -136,19 +22,18 @@ type ConnectionLineWrapperProps = {
}; };
const selector = (s: ReactFlowState) => ({ const selector = (s: ReactFlowState) => ({
nodeId: s.connectionStartHandle?.nodeId,
handleType: s.connectionStartHandle?.type,
nodesConnectable: s.nodesConnectable, nodesConnectable: s.nodesConnectable,
connectionStatus: s.connectionStatus, isValid: s.connection.isValid,
inProgress: s.connection.inProgress,
width: s.width, width: s.width,
height: s.height, height: s.height,
}); });
export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) { export function ConnectionLineWrapper({ containerStyle, style, type, component }: ConnectionLineWrapperProps) {
const { nodeId, handleType, nodesConnectable, width, height, connectionStatus } = useStore(selector, shallow); const { nodesConnectable, width, height, isValid, inProgress } = useStore(selector, shallow);
const isValid = !!(nodeId && handleType && width && nodesConnectable); const renderConnection = !!(width && nodesConnectable && inProgress);
if (!isValid) { if (!renderConnection) {
return null; return null;
} }
@@ -159,16 +44,79 @@ export function ConnectionLineWrapper({ containerStyle, style, type, component }
height={height} height={height}
className="react-flow__connectionline react-flow__container" className="react-flow__connectionline react-flow__container"
> >
<g className={cc(['react-flow__connection', connectionStatus])}> <g className={cc(['react-flow__connection', getConnectionStatus(isValid)])}>
<ConnectionLine <ConnectionLine style={style} type={type} CustomComponent={component} isValid={isValid} />
nodeId={nodeId}
handleType={handleType}
style={style}
type={type}
CustomComponent={component}
connectionStatus={connectionStatus}
/>
</g> </g>
</svg> </svg>
); );
} }
type ConnectionLineProps = {
type: ConnectionLineType;
style?: CSSProperties;
CustomComponent?: ConnectionLineComponent;
isValid: boolean | null;
};
const ConnectionLine = ({ style, type = ConnectionLineType.Bezier, CustomComponent, isValid }: ConnectionLineProps) => {
const { inProgress, from, fromNode, fromHandle, fromPosition, to, toNode, toHandle, toPosition } = useConnection();
if (!inProgress) {
return;
}
if (CustomComponent) {
return (
<CustomComponent
connectionLineType={type}
connectionLineStyle={style}
fromNode={fromNode}
fromHandle={fromHandle}
fromX={from.x}
fromY={from.y}
toX={to.x}
toY={to.y}
fromPosition={fromPosition}
toPosition={toPosition}
connectionStatus={getConnectionStatus(isValid)}
toNode={toNode}
toHandle={toHandle}
/>
);
}
let path = '';
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition,
};
switch (type) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.SimpleBezier:
[path] = getSimpleBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0,
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
return <path d={path} fill="none" className="react-flow__connection-path" style={style} />;
};
ConnectionLine.displayName = 'ConnectionLine';
@@ -96,7 +96,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
onReconnectEnd: _onReconnectEnd, onReconnectEnd: _onReconnectEnd,
updateConnection, updateConnection,
getTransform: () => store.getState().transform, getTransform: () => store.getState().transform,
getConnectionStartHandle: () => store.getState().connectionStartHandle, getFromHandle: () => store.getState().connection.fromHandle,
}); });
}; };
+13 -19
View File
@@ -39,28 +39,22 @@ const selector = (s: ReactFlowState) => ({
const connectingSelector = const connectingSelector =
(nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => { (nodeId: string | null, handleId: string | null, type: HandleType) => (state: ReactFlowState) => {
const { const { connectionClickStartHandle: clickHandle, connectionMode, connection } = state;
connectionStartHandle: startHandle,
connectionEndHandle: endHandle,
connectionClickStartHandle: clickHandle,
connectionMode,
connectionStatus,
} = state;
const connectingTo = endHandle?.nodeId === nodeId && endHandle?.handleId === handleId && endHandle?.type === type; const { fromHandle, toHandle, isValid } = connection;
const connectingTo = toHandle?.nodeId === nodeId && toHandle?.id === handleId && toHandle?.type === type;
return { return {
connectingFrom: connectingFrom: fromHandle?.nodeId === nodeId && fromHandle?.id === handleId && fromHandle?.type === type,
startHandle?.nodeId === nodeId && startHandle?.handleId === handleId && startHandle?.type === type,
connectingTo, connectingTo,
clickConnecting: clickConnecting: clickHandle?.nodeId === nodeId && clickHandle?.id === handleId && clickHandle?.type === type,
clickHandle?.nodeId === nodeId && clickHandle?.handleId === handleId && clickHandle?.type === type,
isPossibleEndHandle: isPossibleEndHandle:
connectionMode === ConnectionMode.Strict connectionMode === ConnectionMode.Strict
? startHandle?.type !== type ? fromHandle?.type !== type
: nodeId !== startHandle?.nodeId || handleId !== startHandle?.handleId, : nodeId !== fromHandle?.nodeId || handleId !== fromHandle?.id,
connectionInProcess: !!startHandle, connectionInProcess: !!fromHandle,
valid: connectingTo && connectionStatus === 'valid', valid: connectingTo && isValid,
}; };
}; };
@@ -144,7 +138,7 @@ function HandleComponent(
onConnect: onConnectExtended, onConnect: onConnectExtended,
isValidConnection: isValidConnection || currentStore.isValidConnection, isValidConnection: isValidConnection || currentStore.isValidConnection,
getTransform: () => store.getState().transform, getTransform: () => store.getState().transform,
getConnectionStartHandle: () => store.getState().connectionStartHandle, getFromHandle: () => store.getState().connection.fromHandle,
}); });
} }
@@ -172,7 +166,7 @@ function HandleComponent(
if (!connectionClickStartHandle) { if (!connectionClickStartHandle) {
onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type }); onClickConnectStart?.(event.nativeEvent, { nodeId, handleId, handleType: type });
store.setState({ connectionClickStartHandle: { nodeId, type, handleId } }); store.setState({ connectionClickStartHandle: { nodeId, type, id: handleId } });
return; return;
} }
@@ -186,7 +180,7 @@ function HandleComponent(
}, },
connectionMode, connectionMode,
fromNodeId: connectionClickStartHandle.nodeId, fromNodeId: connectionClickStartHandle.nodeId,
fromHandleId: connectionClickStartHandle.handleId || null, fromHandleId: connectionClickStartHandle.id || null,
fromType: connectionClickStartHandle.type, fromType: connectionClickStartHandle.type,
isValidConnection: isValidConnectionHandler, isValidConnection: isValidConnectionHandler,
flowId, flowId,
@@ -6,7 +6,6 @@ import {
elementSelectionKeys, elementSelectionKeys,
errorMessages, errorMessages,
getNodeDimensions, getNodeDimensions,
getPositionWithOrigin,
isInputDOMNode, isInputDOMNode,
nodeHasDimensions, nodeHasDimensions,
} from '@xyflow/system'; } from '@xyflow/system';
@@ -40,7 +39,6 @@ export function NodeWrapper<NodeType extends Node>({
rfId, rfId,
nodeTypes, nodeTypes,
nodeExtent, nodeExtent,
nodeOrigin,
onError, onError,
}: NodeWrapperProps<NodeType>) { }: NodeWrapperProps<NodeType>) {
const { node, internals, isParent } = useStore((s) => { const { node, internals, isParent } = useStore((s) => {
@@ -87,15 +85,11 @@ export function NodeWrapper<NodeType extends Node>({
const nodeDimensions = getNodeDimensions(node); const nodeDimensions = getNodeDimensions(node);
const inlineDimensions = getNodeInlineStyleDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node);
// TODO: clamping should happen earlier
const clampedPosition = nodeExtent const clampedPosition = nodeExtent
? clampPosition(internals.positionAbsolute, nodeExtent) ? clampPosition(internals.positionAbsolute, nodeExtent)
: internals.positionAbsolute; : internals.positionAbsolute;
const positionWithOrigin = getPositionWithOrigin({
...clampedPosition,
...nodeDimensions,
origin: node.origin || nodeOrigin,
});
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler = onMouseEnter const onMouseEnterHandler = onMouseEnter
@@ -181,7 +175,7 @@ export function NodeWrapper<NodeType extends Node>({
ref={nodeRef} ref={nodeRef}
style={{ style={{
zIndex: internals.z, zIndex: internals.z,
transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, transform: `translate(${clampedPosition.x}px,${clampedPosition.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none', pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: hasDimensions ? 'visible' : 'hidden', visibility: hasDimensions ? 'visible' : 'hidden',
...node.style, ...node.style,
@@ -21,7 +21,6 @@ export type NodesSelectionProps<NodeType> = {
const selector = (s: ReactFlowState) => { const selector = (s: ReactFlowState) => {
const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, { const { width, height, x, y } = getInternalNodesBounds(s.nodeLookup, {
nodeOrigin: s.nodeOrigin,
filter: (node) => !!node.selected, filter: (node) => !!node.selected,
}); });
@@ -4,6 +4,7 @@ import { Provider } from '../../contexts/StoreContext';
import { createStore } from '../../store'; import { createStore } from '../../store';
import { BatchProvider } from '../BatchProvider'; import { BatchProvider } from '../BatchProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge } from '../../types';
import { NodeOrigin } from '@xyflow/system';
export type ReactFlowProviderProps = { export type ReactFlowProviderProps = {
initialNodes?: Node[]; initialNodes?: Node[];
@@ -13,6 +14,7 @@ export type ReactFlowProviderProps = {
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
children: ReactNode; children: ReactNode;
}; };
@@ -24,6 +26,7 @@ export function ReactFlowProvider({
initialWidth: width, initialWidth: width,
initialHeight: height, initialHeight: height,
fitView, fitView,
nodeOrigin,
children, children,
}: ReactFlowProviderProps) { }: ReactFlowProviderProps) {
const [store] = useState(() => const [store] = useState(() =>
@@ -35,6 +38,7 @@ export function ReactFlowProvider({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}) })
); );
@@ -22,7 +22,6 @@ export type FlowRendererProps<NodeType extends Node = Node> = Omit<
| 'selectNodesOnDrag' | 'selectNodesOnDrag'
| 'defaultMarkerColor' | 'defaultMarkerColor'
| 'rfId' | 'rfId'
| 'nodeOrigin'
> & { > & {
isControlledViewport: boolean; isControlledViewport: boolean;
children: ReactNode; children: ReactNode;
@@ -32,7 +32,6 @@ export type GraphViewProps<NodeType extends Node = Node, EdgeType extends Edge =
| 'noPanClassName' | 'noPanClassName'
| 'defaultViewport' | 'defaultViewport'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'nodeOrigin'
> >
> & { > & {
rfId: string; rfId: string;
@@ -97,7 +96,6 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
noWheelClassName, noWheelClassName,
noPanClassName, noPanClassName,
disableKeyboardA11y, disableKeyboardA11y,
nodeOrigin,
nodeExtent, nodeExtent,
rfId, rfId,
viewport, viewport,
@@ -186,7 +184,6 @@ function GraphViewComponent<NodeType extends Node = Node, EdgeType extends Edge
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
noDragClassName={noDragClassName} noDragClassName={noDragClassName}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
rfId={rfId} rfId={rfId}
/> />
@@ -22,7 +22,6 @@ export type NodeRendererProps<NodeType extends Node> = Pick<
| 'noDragClassName' | 'noDragClassName'
| 'rfId' | 'rfId'
| 'disableKeyboardA11y' | 'disableKeyboardA11y'
| 'nodeOrigin'
| 'nodeExtent' | 'nodeExtent'
| 'nodeTypes' | 'nodeTypes'
>; >;
@@ -72,7 +71,6 @@ function NodeRendererComponent<NodeType extends Node>(props: NodeRendererProps<N
id={nodeId} id={nodeId}
nodeTypes={props.nodeTypes} nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent} nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
onClick={props.onNodeClick} onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter} onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove} onMouseMove={props.onNodeMouseMove}
+2 -3
View File
@@ -155,7 +155,7 @@ export function Pane({
}; };
const onPointerMove = (event: ReactPointerEvent): void => { const onPointerMove = (event: ReactPointerEvent): void => {
const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = const { userSelectionRect, edgeLookup, transform, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
store.getState(); store.getState();
if (!containerBounds.current || !userSelectionRect) { if (!containerBounds.current || !userSelectionRect) {
@@ -181,8 +181,7 @@ export function Pane({
nextUserSelectRect, nextUserSelectRect,
transform, transform,
selectionMode === SelectionMode.Partial, selectionMode === SelectionMode.Partial,
true, true
nodeOrigin
); );
const selectedEdgeIds = new Set<string>(); const selectedEdgeIds = new Set<string>();
@@ -3,6 +3,7 @@ import { useContext, type ReactNode } from 'react';
import StoreContext from '../../contexts/StoreContext'; import StoreContext from '../../contexts/StoreContext';
import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import { ReactFlowProvider } from '../../components/ReactFlowProvider';
import type { Node, Edge } from '../../types'; import type { Node, Edge } from '../../types';
import { NodeOrigin } from '@xyflow/system';
export function Wrapper({ export function Wrapper({
children, children,
@@ -13,6 +14,7 @@ export function Wrapper({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
children: ReactNode; children: ReactNode;
nodes?: Node[]; nodes?: Node[];
@@ -22,6 +24,7 @@ export function Wrapper({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) { }) {
const isWrapped = useContext(StoreContext); const isWrapped = useContext(StoreContext);
@@ -40,6 +43,7 @@ export function Wrapper({
initialWidth={width} initialWidth={width}
initialHeight={height} initialHeight={height}
fitView={fitView} fitView={fitView}
nodeOrigin={nodeOrigin}
> >
{children} {children}
</ReactFlowProvider> </ReactFlowProvider>
@@ -157,7 +157,7 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
data-testid="rf__wrapper" data-testid="rf__wrapper"
id={id} id={id}
> >
<Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView}> <Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView} nodeOrigin={nodeOrigin}>
<GraphView<NodeType, EdgeType> <GraphView<NodeType, EdgeType>
onInit={onInit} onInit={onInit}
onNodeClick={onNodeClick} onNodeClick={onNodeClick}
@@ -217,7 +217,6 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
noPanClassName={noPanClassName} noPanClassName={noPanClassName}
rfId={rfId} rfId={rfId}
disableKeyboardA11y={disableKeyboardA11y} disableKeyboardA11y={disableKeyboardA11y}
nodeOrigin={nodeOrigin}
nodeExtent={nodeExtent} nodeExtent={nodeExtent}
viewport={viewport} viewport={viewport}
onViewportChange={onViewportChange} onViewportChange={onViewportChange}
+9 -23
View File
@@ -2,33 +2,19 @@ import { shallow } from 'zustand/shallow';
import { useStore } from './useStore'; import { useStore } from './useStore';
import type { ReactFlowStore } from '../types/store'; import type { ReactFlowStore } from '../types/store';
import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
const selector = (s: ReactFlowStore) => ({ const selector = (s: ReactFlowStore): ConnectionState => {
startHandle: s.connectionStartHandle, return s.connection.inProgress
endHandle: s.connectionEndHandle, ? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
status: s.connectionStatus, : { ...s.connection };
position: s.connectionStartHandle ? s.connectionPosition : null,
});
type UseConnectionResult = {
/** The start handle where the user interaction started or null */
startHandle: ReactFlowStore['connectionStartHandle'];
/** The target handle that's inside the connection radius or null */
endHandle: ReactFlowStore['connectionEndHandle'];
/** The current connection status 'valid', 'invalid' or null*/
status: ReactFlowStore['connectionStatus'];
/** The current connection position or null */
position: ReactFlowStore['connectionPosition'] | null;
}; };
/** /**
* Hook for accessing the ongoing connection. * Hook for accessing the connection state.
* *
* @public * @public
* @returns ongoing connection * @returns ConnectionState
*/ */
export function useConnection(): UseConnectionResult { export function useConnection(): ConnectionState {
const ongoingConnection = useStore(selector, shallow); return useStore(selector, shallow);
return ongoingConnection;
} }
+1 -1
View File
@@ -50,7 +50,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!; const nodeToUse = isNode<NodeType>(node) ? node : nodeLookup.get(node.id)!;
const position = nodeToUse.parentId const position = nodeToUse.parentId
? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.measured, nodeToUse.parentId, nodeLookup, nodeOrigin)
: nodeToUse.position; : nodeToUse.position;
const nodeWithPosition = { const nodeWithPosition = {
+16 -15
View File
@@ -45,22 +45,23 @@ const useViewportHelper = (): ViewportHelperFunctions => {
return { x, y, zoom }; return { x, y, zoom };
}, },
fitView: (options) => { fitView: (options) => {
const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState(); const { nodeLookup, width, height, minZoom, maxZoom, panZoom } = store.getState();
return panZoom if (!panZoom) {
? fitView( return false;
{ }
nodeLookup,
width, return fitView(
height, {
nodeOrigin, nodeLookup,
minZoom, width,
maxZoom, height,
panZoom, minZoom,
}, maxZoom,
options panZoom,
) },
: false; options
);
}, },
setCenter: (x, y, options) => { setCenter: (x, y, options) => {
const { width, height, maxZoom, panZoom } = store.getState(); const { width, height, maxZoom, panZoom } = store.getState();
-1
View File
@@ -49,7 +49,6 @@ export {
type OnMoveStart, type OnMoveStart,
type OnMoveEnd, type OnMoveEnd,
type Connection, type Connection,
type ConnectionStatus,
ConnectionMode, ConnectionMode,
type OnConnectStartParams, type OnConnectStartParams,
type OnConnectStart, type OnConnectStart,
+15 -20
View File
@@ -12,6 +12,8 @@ import {
EdgeSelectionChange, EdgeSelectionChange,
NodeSelectionChange, NodeSelectionChange,
ParentExpandChild, ParentExpandChild,
initialConnection,
NodeOrigin,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -26,6 +28,7 @@ const createStore = ({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -34,10 +37,11 @@ const createStore = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) => }) =>
createWithEqualityFn<ReactFlowState>( createWithEqualityFn<ReactFlowState>(
(set, get) => ({ (set, get) => ({
...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, defaultNodes, defaultEdges }),
setNodes: (nodes: Node[]) => { setNodes: (nodes: Node[]) => {
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
// setNodes() is called exclusively in response to user actions: // setNodes() is called exclusively in response to user actions:
@@ -98,7 +102,7 @@ const createStore = ({
return; return;
} }
updateAbsolutePositions(nodeLookup, { nodeOrigin }); updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set // we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone; let nextFitViewDone = fitViewDone;
@@ -155,8 +159,8 @@ const createStore = ({
} }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const { nodeLookup, parentLookup } = get(); const { nodeLookup, parentLookup, nodeOrigin } = get();
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
changes.push(...parentExpandChanges); changes.push(...parentExpandChanges);
} }
@@ -288,7 +292,7 @@ const createStore = ({
return panBySystem({ delta, panZoom, transform, translateExtent, width, height }); return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
}, },
fitView: (options?: FitViewOptions): boolean => { fitView: (options?: FitViewOptions): boolean => {
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get(); const { panZoom, width, height, minZoom, maxZoom, nodeLookup } = get();
if (!panZoom) { if (!panZoom) {
return false; return false;
@@ -302,26 +306,17 @@ const createStore = ({
panZoom, panZoom,
minZoom, minZoom,
maxZoom, maxZoom,
nodeOrigin,
}, },
options options
); );
}, },
cancelConnection: () => cancelConnection: () => {
set({ set({
connectionStatus: null, connection: { ...initialConnection },
connectionStartHandle: null, });
connectionEndHandle: null, },
}), updateConnection: (connection) => {
updateConnection: (params) => { set({ connection });
const { connectionPosition } = get();
const currentConnection = {
...params,
connectionPosition: params.connectionPosition ?? connectionPosition,
};
set(currentConnection);
}, },
reset: () => set({ ...getInitialState() }), reset: () => set({ ...getInitialState() }),
+9 -8
View File
@@ -7,6 +7,8 @@ import {
updateConnectionLookup, updateConnectionLookup,
devWarn, devWarn,
getInternalNodesBounds, getInternalNodesBounds,
NodeOrigin,
initialConnection,
} from '@xyflow/system'; } from '@xyflow/system';
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
@@ -19,6 +21,7 @@ const getInitialState = ({
width, width,
height, height,
fitView, fitView,
nodeOrigin,
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
@@ -27,6 +30,7 @@ const getInitialState = ({
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
} = {}): ReactFlowStore => { } = {}): ReactFlowStore => {
const nodeLookup = new Map<string, InternalNode>(); const nodeLookup = new Map<string, InternalNode>();
const parentLookup = new Map(); const parentLookup = new Map();
@@ -34,21 +38,21 @@ const getInitialState = ({
const edgeLookup = new Map(); const edgeLookup = new Map();
const storeEdges = defaultEdges ?? edges ?? []; const storeEdges = defaultEdges ?? edges ?? [];
const storeNodes = defaultNodes ?? nodes ?? []; const storeNodes = defaultNodes ?? nodes ?? [];
const storeNodeOrigin = nodeOrigin ?? [0, 0];
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
adoptUserNodes(storeNodes, nodeLookup, parentLookup, { adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
}); });
let transform: Transform = [0, 0, 1]; let transform: Transform = [0, 0, 1];
if (fitView && width && height) { if (fitView && width && height) {
// @todo users nodeOrigin should be used here
const bounds = getInternalNodesBounds(nodeLookup, { const bounds = getInternalNodesBounds(nodeLookup, {
nodeOrigin: [0, 0],
filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)), filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight)),
}); });
const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); const { x, y, zoom } = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
transform = [x, y, zoom]; transform = [x, y, zoom];
} }
@@ -76,13 +80,11 @@ const getInitialState = ({
nodesSelectionActive: false, nodesSelectionActive: false,
userSelectionActive: false, userSelectionActive: false,
userSelectionRect: null, userSelectionRect: null,
connectionPosition: { x: 0, y: 0 },
connectionStatus: null,
connectionMode: ConnectionMode.Strict, connectionMode: ConnectionMode.Strict,
domNode: null, domNode: null,
paneDragging: false, paneDragging: false,
noPanClassName: 'nopan', noPanClassName: 'nopan',
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
nodeDragThreshold: 1, nodeDragThreshold: 1,
snapGrid: [15, 15], snapGrid: [15, 15],
@@ -103,8 +105,7 @@ const getInitialState = ({
multiSelectionActive: false, multiSelectionActive: false,
connectionStartHandle: null, connection: { ...initialConnection },
connectionEndHandle: null,
connectionClickStartHandle: null, connectionClickStartHandle: null,
connectOnClick: true, connectOnClick: true,
+6 -5
View File
@@ -10,8 +10,7 @@ import type {
HandleType, HandleType,
Connection, Connection,
ConnectionLineType, ConnectionLineType,
HandleElement, Handle,
ConnectionStatus,
EdgePosition, EdgePosition,
StepPathOptions, StepPathOptions,
OnError, OnError,
@@ -194,15 +193,17 @@ export type OnReconnect<EdgeType extends Edge = Edge> = (oldEdge: EdgeType, newC
export type ConnectionLineComponentProps = { export type ConnectionLineComponentProps = {
connectionLineStyle?: CSSProperties; connectionLineStyle?: CSSProperties;
connectionLineType: ConnectionLineType; connectionLineType: ConnectionLineType;
fromNode?: Node; fromNode: Node;
fromHandle?: HandleElement; fromHandle: Handle;
fromX: number; fromX: number;
fromY: number; fromY: number;
toX: number; toX: number;
toY: number; toY: number;
fromPosition: Position; fromPosition: Position;
toPosition: Position; toPosition: Position;
connectionStatus: ConnectionStatus | null; connectionStatus: 'valid' | 'invalid' | null;
toNode: Node | null;
toHandle: Handle | null;
}; };
export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>; export type ConnectionLineComponent = ComponentType<ConnectionLineComponentProps>;
+1 -9
View File
@@ -1,12 +1,5 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { import type { CoordinateExtent, NodeBase, OnError, NodeProps as NodePropsBase, InternalNodeBase } from '@xyflow/system';
CoordinateExtent,
NodeBase,
NodeOrigin,
OnError,
NodeProps as NodePropsBase,
InternalNodeBase,
} from '@xyflow/system';
import { NodeTypes } from './general'; import { NodeTypes } from './general';
@@ -59,7 +52,6 @@ export type NodeWrapperProps<NodeType extends Node> = {
disableKeyboardA11y: boolean; disableKeyboardA11y: boolean;
nodeTypes?: NodeTypes; nodeTypes?: NodeTypes;
nodeExtent?: CoordinateExtent; nodeExtent?: CoordinateExtent;
nodeOrigin: NodeOrigin;
onError?: OnError; onError?: OnError;
}; };
+8 -12
View File
@@ -1,6 +1,6 @@
import { import {
ConnectionMode, ConnectionMode,
type ConnectionStatus, type ConnectionState,
type CoordinateExtent, type CoordinateExtent,
type InternalNodeUpdate, type InternalNodeUpdate,
type UpdateNodePositions, type UpdateNodePositions,
@@ -10,9 +10,8 @@ import {
type OnViewportChange, type OnViewportChange,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type ConnectingHandle, type Handle,
type Transform, type Transform,
type XYPosition,
type PanZoomInstance, type PanZoomInstance,
type PanBy, type PanBy,
type OnConnectStart, type OnConnectStart,
@@ -25,8 +24,9 @@ import {
type EdgeLookup, type EdgeLookup,
type ConnectionLookup, type ConnectionLookup,
type NodeLookup, type NodeLookup,
NodeChange, type NodeChange,
EdgeChange, type EdgeChange,
type ParentLookup,
} from '@xyflow/system'; } from '@xyflow/system';
import type { import type {
@@ -54,7 +54,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
transform: Transform; transform: Transform;
nodes: NodeType[]; nodes: NodeType[];
nodeLookup: NodeLookup<InternalNode<NodeType>>; nodeLookup: NodeLookup<InternalNode<NodeType>>;
parentLookup: Map<string, InternalNode<NodeType>[]>; parentLookup: ParentLookup<InternalNode<NodeType>>;
edges: Edge[]; edges: Edge[];
edgeLookup: EdgeLookup<EdgeType>; edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup; connectionLookup: ConnectionLookup;
@@ -78,9 +78,9 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
userSelectionActive: boolean; userSelectionActive: boolean;
userSelectionRect: SelectionRect | null; userSelectionRect: SelectionRect | null;
connectionPosition: XYPosition; connection: ConnectionState;
connectionStatus: ConnectionStatus | null;
connectionMode: ConnectionMode; connectionMode: ConnectionMode;
connectionClickStartHandle: (Pick<Handle, 'nodeId' | 'id'> & Required<Pick<Handle, 'type'>>) | null;
snapToGrid: boolean; snapToGrid: boolean;
snapGrid: SnapGrid; snapGrid: SnapGrid;
@@ -97,10 +97,6 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
multiSelectionActive: boolean; multiSelectionActive: boolean;
connectionStartHandle: ConnectingHandle | null;
connectionEndHandle: ConnectingHandle | null;
connectionClickStartHandle: ConnectingHandle | null;
onNodeDragStart?: OnNodeDrag<NodeType>; onNodeDragStart?: OnNodeDrag<NodeType>;
onNodeDrag?: OnNodeDrag<NodeType>; onNodeDrag?: OnNodeDrag<NodeType>;
onNodeDragStop?: OnNodeDrag<NodeType>; onNodeDragStop?: OnNodeDrag<NodeType>;
+6
View File
@@ -1,5 +1,11 @@
# @xyflow/svelte # @xyflow/svelte
## 0.1.8
- `useConnection` returns `ConnectionState` with `toNode` and `toHandle`
- node origin is part of position absolute
- refactor connection handling
## 0.1.7 ## 0.1.7
- revise selection usability (capture while dragging out of the flow) - revise selection usability (capture while dragging out of the flow)
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/svelte", "name": "@xyflow/svelte",
"version": "0.1.7", "version": "0.1.8",
"description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.", "description": "Svelte Flow - A highly customizable Svelte library for building node-based editors, workflow systems, diagrams and more.",
"keywords": [ "keywords": [
"svelte", "svelte",
@@ -34,7 +34,7 @@ export default function drag(domNode: Element, params: UseDragParams) {
nodeExtent: get(store.nodeExtent), nodeExtent: get(store.nodeExtent),
snapGrid: snapGrid ? snapGrid : [0, 0], snapGrid: snapGrid ? snapGrid : [0, 0],
snapToGrid: !!snapGrid, snapToGrid: !!snapGrid,
nodeOrigin: [0, 0], nodeOrigin: get(store.nodeOrigin),
multiSelectionActive: get(store.multiselectionKeyPressed), multiSelectionActive: get(store.multiselectionKeyPressed),
domNode: get(store.domNode), domNode: get(store.domNode),
transform: [vp.x, vp.y, vp.zoom], transform: [vp.x, vp.y, vp.zoom],
@@ -2,21 +2,59 @@
import cc from 'classcat'; import cc from 'classcat';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import {
ConnectionLineType,
getBezierPath,
getConnectionStatus,
getSmoothStepPath,
getStraightPath
} from '@xyflow/system';
export let containerStyle: string = ''; export let containerStyle: string = '';
export let style: string = ''; export let style: string = '';
export let isCustomComponent: boolean = false; export let isCustomComponent: boolean = false;
const { width, height, connection } = useStore(); const { width, height, connection, connectionLineType } = useStore();
let path: string | null = null;
$: if ($connection.inProgress && !isCustomComponent) {
const { from, to, fromPosition, toPosition } = $connection;
const pathParams = {
sourceX: from.x,
sourceY: from.y,
sourcePosition: fromPosition,
targetX: to.x,
targetY: to.y,
targetPosition: toPosition
};
switch ($connectionLineType) {
case ConnectionLineType.Bezier:
[path] = getBezierPath(pathParams);
break;
case ConnectionLineType.Step:
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
break;
case ConnectionLineType.SmoothStep:
[path] = getSmoothStepPath(pathParams);
break;
default:
[path] = getStraightPath(pathParams);
}
}
</script> </script>
{#if $connection.path} {#if $connection.inProgress}
<svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}> <svg width={$width} height={$height} class="svelte-flow__connectionline" style={containerStyle}>
<g class={cc(['svelte-flow__connection', $connection.status])}> <g class={cc(['svelte-flow__connection', getConnectionStatus($connection.isValid)])}>
<slot name="connectionLine" /> <slot name="connectionLine" />
<!-- slot fallbacks do not work if slots are forwarded in parent --> <!-- slot fallbacks do not work if slots are forwarded in parent -->
{#if !isCustomComponent} {#if !isCustomComponent}
<path d={$connection.path} {style} fill="none" class="svelte-flow__connection-path" /> <path d={path} {style} fill="none" class="svelte-flow__connection-path" />
{/if} {/if}
</g> </g>
</svg> </svg>
@@ -103,7 +103,7 @@
$onConnectEndAction?.(event); $onConnectEndAction?.(event);
}, },
getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom], getTransform: () => [$viewport.x, $viewport.y, $viewport.zoom],
getConnectionStartHandle: () => $connection.startHandle getFromHandle: () => $connection.fromHandle
}); });
} }
} }
@@ -128,21 +128,20 @@
prevConnections = connections ?? new Map(); prevConnections = connections ?? new Map();
} }
$: connectionInProcess = !!$connection.startHandle; $: connectionInProcess = !!$connection.fromHandle;
$: connectingFrom = $: connectingFrom =
$connection.startHandle?.nodeId === nodeId && $connection.fromHandle?.nodeId === nodeId &&
$connection.startHandle?.type === type && $connection.fromHandle?.type === type &&
$connection.startHandle?.handleId === handleId; $connection.fromHandle?.id === handleId;
$: connectingTo = $: connectingTo =
$connection.endHandle?.nodeId === nodeId && $connection.toHandle?.nodeId === nodeId &&
$connection.endHandle?.type === type && $connection.toHandle?.type === type &&
$connection.endHandle?.handleId === handleId; $connection.toHandle?.id === handleId;
$: isPossibleEndHandle = $: isPossibleEndHandle =
$connectionMode === ConnectionMode.Strict $connectionMode === ConnectionMode.Strict
? $connection.startHandle?.type !== type ? $connection.fromHandle?.type !== type
: nodeId !== $connection.startHandle?.nodeId || : nodeId !== $connection.fromHandle?.nodeId || handleId !== $connection.fromHandle?.id;
handleId !== $connection.startHandle?.handleId; $: valid = connectingTo && $connection.isValid;
$: valid = connectingTo && $connection.status === 'valid';
</script> </script>
<!-- <!--
@@ -31,8 +31,6 @@
export let isParent: $$Props['isParent'] = false; export let isParent: $$Props['isParent'] = false;
export let positionX: $$Props['positionX']; export let positionX: $$Props['positionX'];
export let positionY: $$Props['positionY']; export let positionY: $$Props['positionY'];
export let positionOriginX: $$Props['positionOriginX'];
export let positionOriginY: $$Props['positionOriginY'];
export let sourcePosition: $$Props['sourcePosition'] = undefined; export let sourcePosition: $$Props['sourcePosition'] = undefined;
export let targetPosition: $$Props['targetPosition'] = undefined; export let targetPosition: $$Props['targetPosition'] = undefined;
export let zIndex: $$Props['zIndex']; export let zIndex: $$Props['zIndex'];
@@ -181,7 +179,7 @@
class:nopan={draggable} class:nopan={draggable}
class:parent={isParent} class:parent={isParent}
style:z-index={zIndex} style:z-index={zIndex}
style:transform="translate({positionOriginX}px, {positionOriginY}px)" style:transform="translate({positionX}px, {positionY}px)"
style:visibility={initialized ? 'visible' : 'hidden'} style:visibility={initialized ? 'visible' : 'hidden'}
style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}" style="{style ?? ''};{inlineStyleDimensions.width}{inlineStyleDimensions.height}"
on:click={onSelectNodeHandler} on:click={onSelectNodeHandler}
@@ -28,8 +28,6 @@ export type NodeWrapperProps = Pick<
type: string; type: string;
positionX: number; positionX: number;
positionY: number; positionY: number;
positionOriginX: number;
positionOriginY: number;
'on:nodeclick'?: (event: MouseEvent) => void; 'on:nodeclick'?: (event: MouseEvent) => void;
resizeObserver?: ResizeObserver | null; resizeObserver?: ResizeObserver | null;
isParent?: boolean; isParent?: boolean;
@@ -11,12 +11,14 @@
export let initialWidth: $$Props['initialWidth'] = undefined; export let initialWidth: $$Props['initialWidth'] = undefined;
export let initialHeight: $$Props['initialHeight'] = undefined; export let initialHeight: $$Props['initialHeight'] = undefined;
export let fitView: $$Props['fitView'] = undefined; export let fitView: $$Props['fitView'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
const store = createStore({ const store = createStore({
nodes: initialNodes, nodes: initialNodes,
edges: initialEdges, edges: initialEdges,
width: initialWidth, width: initialWidth,
height: initialHeight, height: initialHeight,
nodeOrigin,
fitView fitView
}); });
@@ -1,4 +1,5 @@
import type { Edge, Node } from '$lib/types'; import type { Edge, Node } from '$lib/types';
import type { NodeOrigin } from '@xyflow/system';
export type SvelteFlowProviderProps = { export type SvelteFlowProviderProps = {
initialNodes?: Node[]; initialNodes?: Node[];
@@ -6,4 +7,5 @@ export type SvelteFlowProviderProps = {
initialWidth?: number; initialWidth?: number;
initialHeight?: number; initialHeight?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}; };
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { getPositionWithOrigin, getNodeDimensions, nodeHasDimensions } from '@xyflow/system'; import { nodeHasDimensions } from '@xyflow/system';
import { NodeWrapper } from '$lib/components/NodeWrapper'; import { NodeWrapper } from '$lib/components/NodeWrapper';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
@@ -40,13 +40,6 @@
<div class="svelte-flow__nodes"> <div class="svelte-flow__nodes">
{#each $visibleNodes as node (node.id)} {#each $visibleNodes as node (node.id)}
{@const nodeDimesions = getNodeDimensions(node)}
{@const posOrigin = getPositionWithOrigin({
x: node.internals.positionAbsolute.x,
y: node.internals.positionAbsolute.y,
...nodeDimesions,
origin: node.origin
})}
<NodeWrapper <NodeWrapper
{node} {node}
id={node.id} id={node.id}
@@ -65,8 +58,6 @@
deletable={node.deletable ?? true} deletable={node.deletable ?? true}
positionX={node.internals.positionAbsolute.x} positionX={node.internals.positionAbsolute.x}
positionY={node.internals.positionAbsolute.y} positionY={node.internals.positionAbsolute.y}
positionOriginX={posOrigin.x ?? 0}
positionOriginY={posOrigin.y ?? 0}
isParent={$parentLookup.has(node.id)} isParent={$parentLookup.has(node.id)}
style={node.style} style={node.style}
class={node.class} class={node.class}
@@ -78,6 +78,7 @@
export let onconnectend: $$Props['onconnectend'] = undefined; export let onconnectend: $$Props['onconnectend'] = undefined;
export let onbeforedelete: $$Props['onbeforedelete'] = undefined; export let onbeforedelete: $$Props['onbeforedelete'] = undefined;
export let oninit: $$Props['oninit'] = undefined; export let oninit: $$Props['oninit'] = undefined;
export let nodeOrigin: $$Props['nodeOrigin'] = undefined;
export let defaultMarkerColor = '#b1b1b7'; export let defaultMarkerColor = '#b1b1b7';
@@ -93,7 +94,14 @@
const store = hasContext(key) const store = hasContext(key)
? useStore() ? useStore()
: createStoreContext({ nodes: get(nodes), edges: get(edges), width, height, fitView }); : createStoreContext({
nodes: get(nodes),
edges: get(edges),
width,
height,
fitView,
nodeOrigin
});
onMount(() => { onMount(() => {
store.width.set(clientWidth); store.width.set(clientWidth);
@@ -168,7 +176,8 @@
onconnect, onconnect,
onconnectstart, onconnectstart,
onconnectend, onconnectend,
onbeforedelete onbeforedelete,
nodeOrigin
}; };
updateStoreByKeys(store, updatableProps); updateStoreByKeys(store, updatableProps);
@@ -70,6 +70,7 @@ export type UpdatableStoreProps = {
onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>; onconnectstart?: UnwrapWritable<SvelteFlowStore['onconnectstart']>;
onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>; onconnectend?: UnwrapWritable<SvelteFlowStore['onconnectend']>;
onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>; onbeforedelete?: UnwrapWritable<SvelteFlowStore['onbeforedelete']>;
nodeOrigin?: UnwrapWritable<SvelteFlowStore['nodeOrigin']>;
}; };
export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) { export function updateStoreByKeys(store: SvelteFlowStore, keys: UpdatableStoreProps) {
@@ -1,7 +1,7 @@
import type { Readable } from 'svelte/store'; import type { Readable } from 'svelte/store';
import { useStore } from '$lib/store'; import { useStore } from '$lib/store';
import type { ConnectionProps } from '$lib/store/derived-connection-props';
import type { ConnectionState } from '@xyflow/system';
/** /**
* Hook for receiving the current connection. * Hook for receiving the current connection.
@@ -9,7 +9,7 @@ import type { ConnectionProps } from '$lib/store/derived-connection-props';
* @public * @public
* @returns current connection as a readable store * @returns current connection as a readable store
*/ */
export function useConnection(): Readable<ConnectionProps> { export function useConnection(): Readable<ConnectionState> {
const { connection } = useStore(); const { connection } = useStore();
return connection; return connection;
-1
View File
@@ -70,7 +70,6 @@ export {
type OnMoveStart, type OnMoveStart,
type OnMoveEnd, type OnMoveEnd,
type Connection, type Connection,
type ConnectionStatus,
ConnectionMode, ConnectionMode,
type OnConnectStartParams, type OnConnectStartParams,
type OnConnectStart, type OnConnectStart,
@@ -11,7 +11,6 @@
getBoundsOfRects, getBoundsOfRects,
getInternalNodesBounds, getInternalNodesBounds,
getNodeDimensions, getNodeDimensions,
getNodePositionWithOrigin,
nodeHasDimensions, nodeHasDimensions,
type Rect type Rect
} from '@xyflow/system'; } from '@xyflow/system';
@@ -134,11 +133,10 @@
{#each $nodes as userNode (userNode.id)} {#each $nodes as userNode (userNode.id)}
{@const node = $nodeLookup.get(userNode.id)} {@const node = $nodeLookup.get(userNode.id)}
{#if node && nodeHasDimensions(node)} {#if node && nodeHasDimensions(node)}
{@const pos = getNodePositionWithOrigin(node).positionAbsolute}
{@const nodeDimesions = getNodeDimensions(node)} {@const nodeDimesions = getNodeDimensions(node)}
<MinimapNode <MinimapNode
x={pos.x} x={node.internals.positionAbsolute.x}
y={pos.y} y={node.internals.positionAbsolute.y}
{...nodeDimesions} {...nodeDimesions}
selected={node.selected} selected={node.selected}
color={nodeColorFunc?.(node)} color={nodeColorFunc?.(node)}
@@ -1,130 +0,0 @@
import { derived, type Writable } from 'svelte/store';
import {
getBezierPath,
getSmoothStepPath,
getStraightPath,
ConnectionLineType,
ConnectionMode,
Position,
type HandleElement
} 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,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top
};
export function getDerivedConnectionProps(
store: SvelteFlowStoreState,
currentConnection: Writable<ConnectionData>
) {
return derived(
[
currentConnection,
store.connectionLineType,
store.connectionMode,
store.nodeLookup,
store.viewport
],
([connection, connectionLineType, connectionMode, nodeLookup, viewport]) => {
if (!connection.connectionStartHandle?.nodeId) {
return initConnectionProps;
}
// TODO: it should bail out if the node is not found
const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId);
const fromHandleBounds = fromNode?.internals.handleBounds;
const handleBoundsStrict =
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict
? handleBoundsStrict
: fromHandleBounds?.[
connection?.connectionStartHandle?.type === 'source' ? 'target' : 'source'
];
const handleBounds =
connectionMode === ConnectionMode.Strict ? handleBoundsStrict : handleBoundsLoose;
const fromHandle = connection.connectionStartHandle?.handleId
? handleBounds?.find((d) => d.id === connection.connectionStartHandle?.handleId)
: handleBounds?.[0];
const fromHandleX = fromHandle
? fromHandle.x + fromHandle.width / 2
: (fromNode?.measured.width ?? 0) / 2;
const fromHandleY = fromHandle
? fromHandle.y + fromHandle.height / 2
: fromNode?.measured.height ?? 0;
const fromX = (fromNode?.internals.positionAbsolute.x ?? 0) + fromHandleX;
const fromY = (fromNode?.internals.positionAbsolute.y ?? 0) + fromHandleY;
const fromPosition = fromHandle?.position;
const toPosition =
connection.connectionEndHandle?.position ??
(fromPosition ? oppositePosition[fromPosition] : undefined);
const pathParams = {
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: ((connection.connectionPosition?.x ?? 0) - viewport.x) / viewport.zoom,
targetY: ((connection.connectionPosition?.y ?? 0) - viewport.y) / viewport.zoom,
targetPosition: toPosition
};
let path = '';
if (connectionLineType === ConnectionLineType.Bezier) {
// we assume the destination position is opposite to the source position
[path] = getBezierPath(pathParams);
} else if (connectionLineType === ConnectionLineType.Step) {
[path] = getSmoothStepPath({
...pathParams,
borderRadius: 0
});
} else if (connectionLineType === ConnectionLineType.SmoothStep) {
[path] = getSmoothStepPath(pathParams);
} else {
[path] = getStraightPath(pathParams);
}
return {
path,
...pathParams,
pointerPosition: connection.connectionPosition,
startHandle: connection.connectionStartHandle,
endHandle: connection.connectionEndHandle,
status: connection.connectionStatus
};
}
);
}
+35 -23
View File
@@ -7,6 +7,9 @@ import {
panBy as panBySystem, panBy as panBySystem,
updateNodeInternals as updateNodeInternalsSystem, updateNodeInternals as updateNodeInternalsSystem,
addEdge as addEdgeUtil, addEdge as addEdgeUtil,
initialConnection,
errorMessages,
pointToRendererPoint,
type UpdateNodePositions, type UpdateNodePositions,
type InternalNodeUpdate, type InternalNodeUpdate,
type ViewportHelperFunctionOptions, type ViewportHelperFunctionOptions,
@@ -14,16 +17,16 @@ import {
type XYPosition, type XYPosition,
type CoordinateExtent, type CoordinateExtent,
type UpdateConnection, type UpdateConnection,
errorMessages type ConnectionState,
type NodeOrigin
} from '@xyflow/system'; } from '@xyflow/system';
import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions, ConnectionData } from '$lib/types'; import type { EdgeTypes, NodeTypes, Node, Edge, FitViewOptions } from '$lib/types';
import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store'; import { initialEdgeTypes, initialNodeTypes, getInitialStore } from './initial-store';
import type { SvelteFlowStore } from './types'; import type { SvelteFlowStore } from './types';
import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils'; import { syncNodeStores, syncEdgeStores, syncViewportStores } from './utils';
import { getVisibleEdges } from './visible-edges'; import { getVisibleEdges } from './visible-edges';
import { getVisibleNodes } from './visible-nodes'; import { getVisibleNodes } from './visible-nodes';
import { getDerivedConnectionProps } from './derived-connection-props';
export const key = Symbol(); export const key = Symbol();
@@ -32,15 +35,24 @@ export function createStore({
edges, edges,
width, width,
height, height,
fitView: fitViewOnCreate fitView: fitViewOnCreate,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}): SvelteFlowStore { }): SvelteFlowStore {
const store = getInitialStore({ nodes, edges, width, height, fitView: fitViewOnCreate }); const store = getInitialStore({
nodes,
edges,
width,
height,
fitView: fitViewOnCreate,
nodeOrigin
});
function setNodeTypes(nodeTypes: NodeTypes) { function setNodeTypes(nodeTypes: NodeTypes) {
store.nodeTypes.set({ store.nodeTypes.set({
@@ -122,6 +134,7 @@ export function createStore({
} }
case 'position': case 'position':
node.position = change.position ?? node.position; node.position = change.position ?? node.position;
break; break;
} }
} }
@@ -147,8 +160,7 @@ export function createStore({
height: get(store.height), height: get(store.height),
minZoom: get(store.minZoom), minZoom: get(store.minZoom),
maxZoom: get(store.maxZoom), maxZoom: get(store.maxZoom),
panZoom, panZoom
nodeOrigin: get(store.nodeOrigin)
}, },
options options
); );
@@ -330,22 +342,13 @@ export function createStore({
}); });
} }
const initConnectionUpdateData = { const _connection = writable<ConnectionState>(initialConnection);
connectionStartHandle: null, const updateConnection: UpdateConnection = (newConnection: ConnectionState) => {
connectionEndHandle: null, _connection.set({ ...newConnection });
connectionPosition: null,
connectionStatus: null
};
// by creating an internal, unexposed store and using a derived store
// we prevent using slow get() calls
const currentConnection = writable<ConnectionData>(initConnectionUpdateData);
const updateConnection: UpdateConnection = (newConnection: ConnectionData) => {
currentConnection.set(newConnection);
}; };
function cancelConnection() { function cancelConnection() {
updateConnection(initConnectionUpdateData); _connection.set(initialConnection);
} }
function reset() { function reset() {
@@ -364,9 +367,16 @@ export function createStore({
...store, ...store,
// derived state // derived state
connection: getDerivedConnectionProps(store, currentConnection),
visibleEdges: getVisibleEdges(store), visibleEdges: getVisibleEdges(store),
visibleNodes: getVisibleNodes(store), visibleNodes: getVisibleNodes(store),
connection: derived([_connection, store.viewport], ([connection, viewport]) => {
return connection.inProgress
? {
...connection,
to: pointToRendererPoint(connection.to, [viewport.x, viewport.y, viewport.zoom])
}
: { ...connection };
}),
markers: derived( markers: derived(
[store.edges, store.defaultMarkerColor, store.flowId], [store.edges, store.defaultMarkerColor, store.flowId],
([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id }) ([edges, defaultColor, id]) => createMarkerIds(edges, { defaultColor, id })
@@ -438,15 +448,17 @@ export function createStoreContext({
edges, edges,
width, width,
height, height,
fitView fitView,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) { }) {
const store = createStore({ nodes, edges, width, height, fitView }); const store = createStore({ nodes, edges, width, height, fitView, nodeOrigin });
setContext(key, { setContext(key, {
getStore: () => store getStore: () => store
+17 -15
View File
@@ -6,9 +6,9 @@ import {
ConnectionLineType, ConnectionLineType,
devWarn, devWarn,
adoptUserNodes, adoptUserNodes,
getNodesBounds,
getViewportForBounds, getViewportForBounds,
updateConnectionLookup, updateConnectionLookup,
initialConnection,
type SelectionRect, type SelectionRect,
type SnapGrid, type SnapGrid,
type MarkerProps, type MarkerProps,
@@ -22,7 +22,10 @@ import {
type OnConnectStart, type OnConnectStart,
type OnConnectEnd, type OnConnectEnd,
type NodeLookup, type NodeLookup,
type EdgeLookup type EdgeLookup,
type ConnectionState,
type ParentLookup,
getInternalNodesBounds
} from '@xyflow/system'; } from '@xyflow/system';
import DefaultNode from '$lib/components/nodes/DefaultNode.svelte'; import DefaultNode from '$lib/components/nodes/DefaultNode.svelte';
@@ -51,7 +54,6 @@ import type {
InternalNode InternalNode
} from '$lib/types'; } from '$lib/types';
import { createNodesStore, createEdgesStore } from './utils'; import { createNodesStore, createEdgesStore } from './utils';
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
export const initialNodeTypes = { export const initialNodeTypes = {
input: InputNode, input: InputNode,
@@ -72,18 +74,21 @@ export const getInitialStore = ({
edges = [], edges = [],
width, width,
height, height,
fitView fitView,
nodeOrigin
}: { }: {
nodes?: Node[]; nodes?: Node[];
edges?: Edge[]; edges?: Edge[];
width?: number; width?: number;
height?: number; height?: number;
fitView?: boolean; fitView?: boolean;
nodeOrigin?: NodeOrigin;
}) => { }) => {
const nodeLookup: NodeLookup = new Map(); const nodeLookup: NodeLookup = new Map();
const parentLookup = new Map(); const parentLookup = new Map();
const storeNodeOrigin = nodeOrigin ?? [0, 0];
adoptUserNodes(nodes, nodeLookup, parentLookup, { adoptUserNodes(nodes, nodeLookup, parentLookup, {
nodeOrigin: [0, 0], nodeOrigin: storeNodeOrigin,
elevateNodesOnSelect: false, elevateNodesOnSelect: false,
checkEquality: false checkEquality: false
}); });
@@ -94,20 +99,17 @@ export const getInitialStore = ({
let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
if (fitView && width && height) { if (fitView && width && height) {
const nodesWithDimensions = nodes.filter( const bounds = getInternalNodesBounds(nodeLookup, {
(node) => (node.width && node.height) || (node.initialWidth && node.initialHeight) filter: (node) => !!((node.width || node.initialWidth) && (node.height || node.initialHeight))
); });
// @todo users nodeOrigin should be used here
const bounds = getNodesBounds(nodesWithDimensions, { nodeOrigin: [0, 0] });
viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1); viewport = getViewportForBounds(bounds, width, height, 0.5, 2, 0.1);
} }
return { return {
flowId: writable<string | null>(null), flowId: writable<string | null>(null),
nodes: createNodesStore(nodes, nodeLookup, parentLookup), nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin),
nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup), nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
parentLookup: readable<Map<string, InternalNode[]>>(parentLookup), parentLookup: readable<ParentLookup<InternalNode>>(parentLookup),
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup), edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
visibleNodes: readable<InternalNode[]>([]), visibleNodes: readable<InternalNode[]>([]),
edges: createEdgesStore(edges, connectionLookup, edgeLookup), edges: createEdgesStore(edges, connectionLookup, edgeLookup),
@@ -117,7 +119,7 @@ export const getInitialStore = ({
width: writable<number>(500), width: writable<number>(500),
minZoom: writable<number>(0.5), minZoom: writable<number>(0.5),
maxZoom: writable<number>(2), maxZoom: writable<number>(2),
nodeOrigin: writable<NodeOrigin>([0, 0]), nodeOrigin: writable<NodeOrigin>(storeNodeOrigin),
nodeDragThreshold: writable<number>(1), nodeDragThreshold: writable<number>(1),
nodeExtent: writable<CoordinateExtent>(infiniteExtent), nodeExtent: writable<CoordinateExtent>(infiniteExtent),
translateExtent: writable<CoordinateExtent>(infiniteExtent), translateExtent: writable<CoordinateExtent>(infiniteExtent),
@@ -142,7 +144,7 @@ export const getInitialStore = ({
viewport: writable<Viewport>(viewport), viewport: writable<Viewport>(viewport),
connectionMode: writable<ConnectionMode>(ConnectionMode.Strict), connectionMode: writable<ConnectionMode>(ConnectionMode.Strict),
domNode: writable<HTMLDivElement | null>(null), domNode: writable<HTMLDivElement | null>(null),
connection: readable<ConnectionProps>(initConnectionProps), connection: readable<ConnectionState>(initialConnection),
connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier), connectionLineType: writable<ConnectionLineType>(ConnectionLineType.Bezier),
connectionRadius: writable<number>(20), connectionRadius: writable<number>(20),
isValidConnection: writable<IsValidConnection>(() => true), isValidConnection: writable<IsValidConnection>(() => true),
+6 -2
View File
@@ -13,7 +13,9 @@ import {
type PanZoomInstance, type PanZoomInstance,
type ConnectionLookup, type ConnectionLookup,
type EdgeLookup, type EdgeLookup,
type NodeLookup type NodeLookup,
type ParentLookup,
type NodeOrigin
} from '@xyflow/system'; } from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types'; import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
@@ -128,7 +130,8 @@ export type NodeStoreOptions = {
export const createNodesStore = ( export const createNodesStore = (
nodes: Node[], nodes: Node[],
nodeLookup: NodeLookup<InternalNode>, nodeLookup: NodeLookup<InternalNode>,
parentLookup: Map<string, InternalNode[]> parentLookup: ParentLookup<InternalNode>,
nodeOrigin: NodeOrigin = [0, 0]
): { ): {
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber; subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
update: (this: void, updater: Updater<Node[]>) => void; update: (this: void, updater: Updater<Node[]>) => void;
@@ -144,6 +147,7 @@ export const createNodesStore = (
const _set = (nds: Node[]): Node[] => { const _set = (nds: Node[]): Node[] => {
adoptUserNodes(nds, nodeLookup, parentLookup, { adoptUserNodes(nds, nodeLookup, parentLookup, {
elevateNodesOnSelect, elevateNodesOnSelect,
nodeOrigin,
defaults, defaults,
checkEquality: false checkEquality: false
}); });
+3 -3
View File
@@ -2,7 +2,7 @@ import type { ShortcutModifierDefinition } from '@svelte-put/shortcut';
import type { import type {
FitViewOptionsBase, FitViewOptionsBase,
XYPosition, XYPosition,
ConnectingHandle, Handle,
Connection, Connection,
OnBeforeDeleteBase, OnBeforeDeleteBase,
HandleProps as HandlePropsSystem HandleProps as HandlePropsSystem
@@ -17,8 +17,8 @@ export type KeyDefinition = string | KeyDefinitionObject;
export type ConnectionData = { export type ConnectionData = {
connectionPosition: XYPosition | null; connectionPosition: XYPosition | null;
connectionStartHandle: ConnectingHandle | null; connectionStartHandle: Handle | null;
connectionEndHandle: ConnectingHandle | null; connectionEndHandle: Handle | null;
connectionStatus: string | null; connectionStatus: string | null;
}; };
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xyflow/system", "name": "@xyflow/system",
"version": "0.0.30", "version": "0.0.31",
"description": "xyflow core system that powers React Flow and Svelte Flow.", "description": "xyflow core system that powers React Flow and Svelte Flow.",
"keywords": [ "keywords": [
"node-based UI", "node-based UI",
+49 -12
View File
@@ -6,9 +6,9 @@ import type { ZoomBehavior } from 'd3-zoom';
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { Transition } from 'd3-transition'; import type { Transition } from 'd3-transition';
import type { XYPosition, Rect } from './utils'; import type { XYPosition, Rect, Position } from './utils';
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes'; import type { InternalNodeBase, NodeBase, NodeDragItem } from './nodes';
import type { ConnectingHandle, HandleType } from './handles'; import type { Handle, HandleType } from './handles';
import { PanZoomInstance } from './panzoom'; import { PanZoomInstance } from './panzoom';
import { EdgeBase } from '..'; import { EdgeBase } from '..';
@@ -37,8 +37,6 @@ export type HandleConnection = Connection & {
edgeId: string; edgeId: string;
}; };
export type ConnectionStatus = 'valid' | 'invalid';
export enum ConnectionMode { export enum ConnectionMode {
Strict = 'strict', Strict = 'strict',
Loose = 'loose', Loose = 'loose',
@@ -63,7 +61,6 @@ export type FitViewParamsBase<NodeType extends NodeBase> = {
panZoom: PanZoomInstance; panZoom: PanZoomInstance;
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
nodeOrigin?: NodeOrigin;
}; };
export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = { export type FitViewOptionsBase<NodeType extends NodeBase = NodeBase> = {
@@ -135,12 +132,52 @@ export type OnError = (id: string, message: string) => void;
export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void; export type UpdateNodePositions = (dragItems: Map<string, NodeDragItem | InternalNodeBase>, dragging?: boolean) => void;
export type PanBy = (delta: XYPosition) => boolean; export type PanBy = (delta: XYPosition) => boolean;
export type UpdateConnection = (params: { export const initialConnection: NoConnection = {
connectionPosition: XYPosition | null; inProgress: false,
connectionStatus: ConnectionStatus | null; isValid: null,
connectionStartHandle: ConnectingHandle | null; from: null,
connectionEndHandle: ConnectingHandle | null; fromHandle: null,
}) => void; fromPosition: null,
fromNode: null,
to: null,
toHandle: null,
toPosition: null,
toNode: null,
};
export type NoConnection = {
inProgress: false;
isValid: null;
from: null;
fromHandle: null;
fromPosition: null;
fromNode: null;
to: null;
toHandle: null;
toPosition: null;
toNode: null;
};
export type ConnectionInProgress = {
inProgress: true;
isValid: boolean | null;
from: XYPosition;
fromHandle: Handle;
fromPosition: Position;
fromNode: NodeBase;
to: XYPosition;
toHandle: Handle | null;
toPosition: Position;
toNode: NodeBase | null;
};
export type ConnectionState = ConnectionInProgress | NoConnection;
export type UpdateConnection = (params: ConnectionState) => void;
export type ColorModeClass = 'light' | 'dark'; export type ColorModeClass = 'light' | 'dark';
export type ColorMode = ColorModeClass | 'system'; export type ColorMode = ColorModeClass | 'system';
+4 -18
View File
@@ -2,29 +2,15 @@ import type { Position, IsValidConnection } from '.';
export type HandleType = 'source' | 'target'; export type HandleType = 'source' | 'target';
export type HandleElement = { export type Handle = {
id?: string | null; id?: string | null;
nodeId: string;
x: number; x: number;
y: number; y: number;
position: Position;
type: HandleType;
width: number; width: number;
height: number; height: number;
position: Position;
type?: HandleType;
};
export type ConnectingHandle = {
nodeId: string;
type: HandleType;
handleId?: string | null;
position?: Position | null;
};
export type ConnectionHandle = {
id: string | null;
type: HandleType;
nodeId: string;
x: number;
y: number;
}; };
export type HandleProps = { export type HandleProps = {
+6 -5
View File
@@ -1,4 +1,4 @@
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.'; import type { XYPosition, Position, CoordinateExtent, Handle } from '.';
import { Optional } from '../utils/types'; import { Optional } from '../utils/types';
/** /**
@@ -77,6 +77,7 @@ export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType &
* Used as an optimization to avoid certain operations. */ * Used as an optimization to avoid certain operations. */
userNode: NodeType; userNode: NodeType;
handleBounds?: NodeHandleBounds; handleBounds?: NodeHandleBounds;
bounds?: NodeBounds;
}; };
}; };
@@ -110,8 +111,8 @@ export type NodeProps<NodeType extends NodeBase> = Pick<
}; };
export type NodeHandleBounds = { export type NodeHandleBounds = {
source: HandleElement[] | null; source: Handle[] | null;
target: HandleElement[] | null; target: Handle[] | null;
}; };
export type InternalNodeUpdate = { export type InternalNodeUpdate = {
@@ -148,9 +149,9 @@ export type NodeOrigin = [number, number];
export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void; export type OnSelectionDrag = (event: MouseEvent, nodes: NodeBase[]) => void;
export type NodeHandle = Optional<HandleElement, 'width' | 'height'>; export type NodeHandle = Omit<Optional<Handle, 'width' | 'height'>, 'nodeId'>;
export type Align = 'center' | 'start' | 'end'; export type Align = 'center' | 'start' | 'end';
export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>; export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
export type ParentLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType[]>; export type ParentLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, Map<string, NodeType>>;
+7
View File
@@ -5,6 +5,13 @@ export enum Position {
Bottom = 'bottom', Bottom = 'bottom',
} }
export const oppositePosition = {
[Position.Left]: Position.Right,
[Position.Right]: Position.Left,
[Position.Top]: Position.Bottom,
[Position.Bottom]: Position.Top,
};
export type XYPosition = { export type XYPosition = {
x: number; x: number;
y: number; y: number;
+4
View File
@@ -51,3 +51,7 @@ export function handleConnectionChange(
cb(diff); cb(diff);
} }
} }
export function getConnectionStatus(isValid: boolean | null) {
return isValid === null ? null : isValid ? 'valid' : 'invalid';
}
+11 -16
View File
@@ -1,4 +1,4 @@
import type { Transform, XYPosition, SnapGrid, Dimensions, NodeOrigin, HandleElement, Position } from '../types'; import type { Transform, XYPosition, SnapGrid, Dimensions, Position, Handle } from '../types';
import { snapPosition, pointToRendererPoint } from './general'; import { snapPosition, pointToRendererPoint } from './general';
export type GetPointerPositionParams = { export type GetPointerPositionParams = {
@@ -59,34 +59,29 @@ export const getEventPosition = (event: MouseEvent | TouchEvent, bounds?: DOMRec
// We store them in the internals object of the node in order to avoid // We store them in the internals object of the node in order to avoid
// unnecessary recalculations. // unnecessary recalculations.
export const getHandleBounds = ( export const getHandleBounds = (
selector: string, type: 'source' | 'target',
nodeElement: HTMLDivElement, nodeElement: HTMLDivElement,
nodeBounds: DOMRect, nodeBounds: DOMRect,
zoom: number, zoom: number,
nodeOrigin: NodeOrigin = [0, 0] nodeId: string
): HandleElement[] | null => { ): Handle[] | null => {
const handles = nodeElement.querySelectorAll(selector); const handles = nodeElement.querySelectorAll(`.${type}`);
if (!handles || !handles.length) { if (!handles || !handles.length) {
return null; return null;
} }
const handlesArray = Array.from(handles) as HTMLDivElement[]; return Array.from(handles).map((handle): Handle => {
const nodeOffset = {
x: nodeBounds.left + nodeBounds.width * nodeOrigin[0],
y: nodeBounds.top + nodeBounds.height * nodeOrigin[1],
};
return handlesArray.map((handle): HandleElement => {
const handleBounds = handle.getBoundingClientRect(); const handleBounds = handle.getBoundingClientRect();
return { return {
id: handle.getAttribute('data-handleid'), id: handle.getAttribute('data-handleid'),
type,
nodeId,
position: handle.getAttribute('data-handlepos') as unknown as Position, position: handle.getAttribute('data-handlepos') as unknown as Position,
x: (handleBounds.left - nodeOffset.x) / zoom, x: (handleBounds.left - nodeBounds.left) / zoom,
y: (handleBounds.top - nodeOffset.y) / zoom, y: (handleBounds.top - nodeBounds.top) / zoom,
...getDimensions(handle), ...getDimensions(handle as HTMLDivElement),
}; };
}); });
}; };
+24 -18
View File
@@ -1,9 +1,9 @@
import { EdgePosition } from '../../types/edges'; import { EdgePosition } from '../../types/edges';
import { ConnectionMode, OnError } from '../../types/general'; import { ConnectionMode, OnError } from '../../types/general';
import { InternalNodeBase, NodeHandle } from '../../types/nodes'; import { InternalNodeBase, NodeHandle } from '../../types/nodes';
import { Position } from '../../types/utils'; import { Position, XYPosition } from '../../types/utils';
import { errorMessages } from '../../constants'; import { errorMessages } from '../../constants';
import { HandleElement } from '../../types'; import { Handle } from '../../types';
import { getNodeDimensions } from '../general'; import { getNodeDimensions } from '../general';
export type GetEdgePositionParams = { export type GetEdgePositionParams = {
@@ -58,14 +58,14 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
const sourcePosition = sourceHandle?.position || Position.Bottom; const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top; const targetPosition = targetHandle?.position || Position.Top;
const [sourceX, sourceY] = getHandlePosition(sourceNode, sourceHandle, sourcePosition); const source = getHandlePosition(sourceNode, sourceHandle, sourcePosition);
const [targetX, targetY] = getHandlePosition(targetNode, targetHandle, targetPosition); const target = getHandlePosition(targetNode, targetHandle, targetPosition);
return { return {
sourceX, sourceX: source.x,
sourceY, sourceY: source.y,
targetX, targetX: target.x,
targetY, targetY: target.y,
sourcePosition, sourcePosition,
targetPosition, targetPosition,
}; };
@@ -84,9 +84,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
handle.height = handle.height ?? 1; handle.height = handle.height ?? 1;
if (handle.type === 'source') { if (handle.type === 'source') {
source.push(handle as HandleElement); source.push(handle as Handle);
} else if (handle.type === 'target') { } else if (handle.type === 'target') {
target.push(handle as HandleElement); target.push(handle as Handle);
} }
} }
@@ -98,27 +98,33 @@ function toHandleBounds(handles?: NodeHandle[]) {
export function getHandlePosition( export function getHandlePosition(
node: InternalNodeBase, node: InternalNodeBase,
handle: HandleElement | null, handle: Handle | null,
fallbackPosition: Position = Position.Left fallbackPosition: Position = Position.Left,
): number[] { center = false
): XYPosition {
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x; const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y; const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
const { width, height } = handle ?? getNodeDimensions(node); const { width, height } = handle ?? getNodeDimensions(node);
if (center) {
return { x: x + width / 2, y: y + height / 2 };
}
const position = handle?.position ?? fallbackPosition; const position = handle?.position ?? fallbackPosition;
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return [x + width / 2, y]; return { x: x + width / 2, y };
case Position.Right: case Position.Right:
return [x + width, y + height / 2]; return { x: x + width, y: y + height / 2 };
case Position.Bottom: case Position.Bottom:
return [x + width / 2, y + height]; return { x: x + width / 2, y: y + height };
case Position.Left: case Position.Left:
return [x, y + height / 2]; return { x, y: y + height / 2 };
} }
} }
function getHandle(bounds: HandleElement[], handleId?: string | null): HandleElement | null { function getHandle(bounds: Handle[], handleId?: string | null): Handle | null {
if (!bounds) { if (!bounds) {
return null; return null;
} }
+15 -35
View File
@@ -12,7 +12,7 @@ import type {
NodeLookup, NodeLookup,
} from '../types'; } from '../types';
import { type Viewport } from '../types'; import { type Viewport } from '../types';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max); export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
@@ -68,24 +68,28 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
}); });
export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => { export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = isInternalNodeBase(node)
? node.internals.positionAbsolute
: getNodePositionWithOrigin(node, nodeOrigin);
return { return {
x, x,
y, y,
width: node.measured?.width ?? node.width ?? 0, width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
height: node.measured?.height ?? node.height ?? 0, height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
}; };
}; };
export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => { export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
const { x, y } = getNodePositionWithOrigin(node, nodeOrigin).positionAbsolute; const { x, y } = isInternalNodeBase(node)
? node.internals.positionAbsolute
: getNodePositionWithOrigin(node, nodeOrigin);
return { return {
x, x,
y, y,
x2: x + (node.measured?.width ?? node.width ?? 0), x2: x + (node.measured?.width ?? node.width ?? node.initialWidth ?? 0),
y2: y + (node.measured?.height ?? node.height ?? 0), y2: y + (node.measured?.height ?? node.height ?? node.initialHeight ?? 0),
}; };
}; };
@@ -114,29 +118,6 @@ export const devWarn = (id: string, message: string) => {
} }
}; };
export const getPositionWithOrigin = ({
x,
y,
width,
height,
origin = [0, 0],
}: {
x: number;
y: number;
width: number;
height: number;
origin?: NodeOrigin;
}): XYPosition => {
if (!width || !height || origin[0] < 0 || origin[1] < 0 || origin[0] > 1 || origin[1] > 1) {
return { x, y };
}
return {
x: x - width * origin[0],
y: y - height * origin[1],
};
};
export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => { export const snapPosition = (position: XYPosition, snapGrid: SnapGrid = [1, 1]): XYPosition => {
return { return {
x: snapGrid[0] * Math.round(position.x / snapGrid[0]), x: snapGrid[0] * Math.round(position.x / snapGrid[0]),
@@ -239,9 +220,10 @@ export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: No
*/ */
export function evaluateAbsolutePosition( export function evaluateAbsolutePosition(
position: XYPosition, position: XYPosition,
dimensions: { width?: number; height?: number } = { width: 0, height: 0 },
parentId: string, parentId: string,
nodeLookup: NodeLookup, nodeLookup: NodeLookup,
nodeOrigin: NodeOrigin = [0, 0] nodeOrigin: NodeOrigin
): XYPosition { ): XYPosition {
let nextParentId: string | undefined = parentId; let nextParentId: string | undefined = parentId;
const positionAbsolute = { ...position }; const positionAbsolute = { ...position };
@@ -252,10 +234,8 @@ export function evaluateAbsolutePosition(
if (parent) { if (parent) {
const origin = parent.origin || nodeOrigin; const origin = parent.origin || nodeOrigin;
const xOffset = (parent.measured.width ?? 0) * origin[0]; positionAbsolute.x += parent.internals.positionAbsolute.x - (dimensions.width ?? 0) * origin[0];
const yOffset = (parent.measured.height ?? 0) * origin[1]; positionAbsolute.y += parent.internals.positionAbsolute.y - (dimensions.height ?? 0) * origin[1];
positionAbsolute.x += parent.position.x - xOffset;
positionAbsolute.y += parent.position.y - yOffset;
} }
} }
+21 -39
View File
@@ -106,25 +106,15 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
return nodes.filter((n) => incomersIds.has(n.id)); return nodes.filter((n) => incomersIds.has(n.id));
}; };
export const getNodePositionWithOrigin = ( export const getNodePositionWithOrigin = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): XYPosition => {
node: InternalNodeBase | NodeBase,
nodeOrigin: NodeOrigin = [0, 0]
): { position: XYPosition; positionAbsolute: XYPosition } => {
const { width, height } = getNodeDimensions(node); const { width, height } = getNodeDimensions(node);
const positionAbsolute = 'internals' in node ? node.internals.positionAbsolute : node.position; const origin = node.origin ?? nodeOrigin;
const origin = node.origin || nodeOrigin;
const offsetX = width * origin[0]; const offsetX = width * origin[0];
const offsetY = height * origin[1]; const offsetY = height * origin[1];
return { return {
position: { x: node.position.x - offsetX,
x: node.position.x - offsetX, y: node.position.y - offsetY,
y: node.position.y - offsetY,
},
positionAbsolute: {
x: positionAbsolute.x - offsetX,
y: positionAbsolute.y - offsetY,
},
}; };
}; };
@@ -157,7 +147,6 @@ export const getNodesBounds = (nodes: NodeBase[], params: GetNodesBoundsParams =
}; };
export type GetInternalNodesBoundsParams<NodeType> = { export type GetInternalNodesBoundsParams<NodeType> = {
nodeOrigin?: NodeOrigin;
useRelativePosition?: boolean; useRelativePosition?: boolean;
filter?: (node: NodeType) => boolean; filter?: (node: NodeType) => boolean;
}; };
@@ -168,9 +157,7 @@ export type GetInternalNodesBoundsParams<NodeType> = {
*/ */
export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeDragItem>( export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeDragItem>(
nodeLookup: Map<string, NodeType>, nodeLookup: Map<string, NodeType>,
params: GetInternalNodesBoundsParams<NodeType> = { params: GetInternalNodesBoundsParams<NodeType> = {}
nodeOrigin: [0, 0],
}
): Rect => { ): Rect => {
if (nodeLookup.size === 0) { if (nodeLookup.size === 0) {
return { x: 0, y: 0, width: 0, height: 0 }; return { x: 0, y: 0, width: 0, height: 0 };
@@ -179,8 +166,8 @@ export const getInternalNodesBounds = <NodeType extends InternalNodeBase | NodeD
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }; let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
nodeLookup.forEach((node) => { nodeLookup.forEach((node) => {
if (params.filter == undefined || params.filter(node)) { if (params.filter === undefined || params.filter(node)) {
const nodeBox = nodeToBox(node as InternalNodeBase, params.nodeOrigin); const nodeBox = nodeToBox(node as InternalNodeBase);
box = getBoundsOfBoxes(box, nodeBox); box = getBoundsOfBoxes(box, nodeBox);
} }
}); });
@@ -194,8 +181,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
[tx, ty, tScale]: Transform = [0, 0, 1], [tx, ty, tScale]: Transform = [0, 0, 1],
partially = false, partially = false,
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
excludeNonSelectableNodes = false, excludeNonSelectableNodes = false
nodeOrigin: NodeOrigin = [0, 0]
): InternalNodeBase<NodeType>[] => { ): InternalNodeBase<NodeType>[] => {
const paneRect = { const paneRect = {
...pointToRendererPoint(rect, [tx, ty, tScale]), ...pointToRendererPoint(rect, [tx, ty, tScale]),
@@ -214,7 +200,7 @@ export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
continue; continue;
} }
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin)); const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node));
const notInitialized = width === null || height === null; const notInitialized = width === null || height === null;
const partiallyVisible = partially && overlappingArea > 0; const partiallyVisible = partially && overlappingArea > 0;
@@ -248,22 +234,22 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
}; };
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>( export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
{ nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params, { nodeLookup, width, height, panZoom, minZoom, maxZoom }: Params,
options?: Options options?: Options
) { ) {
const filteredNodes: InternalNodeBase[] = []; const filteredNodes: Map<string, InternalNodeBase> = new Map();
const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null; const optionNodeIds = options?.nodes ? new Set(options.nodes.map((node) => node.id)) : null;
nodeLookup.forEach((n) => { nodeLookup.forEach((n) => {
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden); const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) { if (isVisible && (!optionNodeIds || optionNodeIds.has(n.id))) {
filteredNodes.push(n); filteredNodes.set(n.id, n);
} }
}); });
if (filteredNodes.length > 0) { if (filteredNodes.size > 0) {
const bounds = getNodesBounds(filteredNodes, { nodeOrigin }); const bounds = getInternalNodesBounds(filteredNodes);
const viewport = getViewportForBounds( const viewport = getViewportForBounds(
bounds, bounds,
@@ -323,9 +309,8 @@ export function calculateNodePosition<NodeType extends NodeBase>({
}): { position: XYPosition; positionAbsolute: XYPosition } { }): { position: XYPosition; positionAbsolute: XYPosition } {
const node = nodeLookup.get(nodeId)!; const node = nodeLookup.get(nodeId)!;
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined; const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
const { x: parentX, y: parentY } = parentNode const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute const origin = node.origin ?? nodeOrigin;
: { x: 0, y: 0 };
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent); let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
@@ -339,13 +324,9 @@ export function calculateNodePosition<NodeType extends NodeBase>({
const parentHeight = parentNode.measured.height; const parentHeight = parentNode.measured.height;
if (nodeWidth && nodeHeight && parentWidth && parentHeight) { if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
const currNodeOrigin = node.origin || nodeOrigin;
const extentX = parentX + nodeWidth * currNodeOrigin[0];
const extentY = parentY + nodeHeight * currNodeOrigin[1];
currentExtent = [ currentExtent = [
[extentX, extentY], [parentX, parentY],
[extentX + parentWidth - nodeWidth, extentY + parentHeight - nodeHeight], [parentX + parentWidth - nodeWidth, parentY + parentHeight - nodeHeight],
]; ];
} }
} }
@@ -362,8 +343,9 @@ export function calculateNodePosition<NodeType extends NodeBase>({
return { return {
position: { position: {
x: positionAbsolute.x - parentX, // TODO: is there a better way to do this?
y: positionAbsolute.y - parentY, x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
}, },
positionAbsolute, positionAbsolute,
}; };
+120 -103
View File
@@ -6,7 +6,6 @@ import {
PanZoomInstance, PanZoomInstance,
Transform, Transform,
XYPosition, XYPosition,
XYZPosition,
ConnectionLookup, ConnectionLookup,
EdgeBase, EdgeBase,
EdgeLookup, EdgeLookup,
@@ -22,48 +21,28 @@ import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './ge
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
import { ParentExpandChild } from './types'; import { ParentExpandChild } from './types';
const defaultOptions = {
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
};
const adoptUserNodesDefaultOptions = {
...defaultOptions,
checkEquality: true,
};
export function updateAbsolutePositions<NodeType extends NodeBase>( export function updateAbsolutePositions<NodeType extends NodeBase>(
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
nodeOrigin: [0, 0] as NodeOrigin, options?: UpdateNodesOptions<NodeType>
elevateNodesOnSelect: true,
defaults: {},
}
) { ) {
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const _options = { ...defaultOptions, ...options };
for (const node of nodeLookup.values()) {
for (const [, node] of nodeLookup) { if (!node.parentId) {
const parentId = node.parentId;
if (!parentId) {
continue; continue;
} }
if (!nodeLookup.has(parentId)) { updateChildPosition(node, nodeLookup, parentLookup, _options);
throw new Error(`Parent node ${parentId} not found`);
}
const parentNode = nodeLookup.get(parentId);
const { x, y, z } = calculateXYZPosition(
node,
nodeLookup,
{
...node.position,
z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0),
},
parentNode?.origin ?? options.nodeOrigin
);
const currPosition = node.internals.positionAbsolute;
const positionChanged = x !== currPosition.x || y !== currPosition.y;
if (positionChanged || z !== node.internals.z) {
node.internals = {
...node.internals,
positionAbsolute: positionChanged ? { x, y } : currPosition,
z,
};
}
} }
} }
@@ -76,38 +55,33 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
export function adoptUserNodes<NodeType extends NodeBase>( export function adoptUserNodes<NodeType extends NodeBase>(
nodes: NodeType[], nodes: NodeType[],
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
parentLookup: Map<string, InternalNodeBase<NodeType>[]>, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
options: UpdateNodesOptions<NodeType> = { options?: UpdateNodesOptions<NodeType>
nodeOrigin: [0, 0] as NodeOrigin,
elevateNodesOnSelect: true,
defaults: {},
checkEquality: true,
}
) { ) {
const _options = { ...adoptUserNodesDefaultOptions, ...options };
const tmpLookup = new Map(nodeLookup); const tmpLookup = new Map(nodeLookup);
nodeLookup.clear(); nodeLookup.clear();
parentLookup.clear(); parentLookup.clear();
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
nodes.forEach((userNode) => { for (const userNode of nodes) {
let internalNode = tmpLookup.get(userNode.id); let internalNode = tmpLookup.get(userNode.id);
if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
if (options.checkEquality && userNode === internalNode?.internals.userNode) {
nodeLookup.set(userNode.id, internalNode); nodeLookup.set(userNode.id, internalNode);
} else { } else {
internalNode = { internalNode = {
...options.defaults, ..._options.defaults,
...userNode, ...userNode,
measured: { measured: {
width: userNode.measured?.width, width: userNode.measured?.width,
height: userNode.measured?.height, height: userNode.measured?.height,
}, },
internals: { internals: {
positionAbsolute: userNode.position, positionAbsolute: getNodePositionWithOrigin(userNode, _options.nodeOrigin),
handleBounds: internalNode?.internals.handleBounds, handleBounds: internalNode?.internals.handleBounds,
z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), z: calculateZ(userNode, selectedNodeZ),
userNode, userNode,
}, },
}; };
@@ -115,50 +89,75 @@ export function adoptUserNodes<NodeType extends NodeBase>(
} }
if (userNode.parentId) { if (userNode.parentId) {
const childNodes = parentLookup.get(userNode.parentId); updateChildPosition(internalNode, nodeLookup, parentLookup, options);
if (childNodes) {
childNodes.push(internalNode);
} else {
parentLookup.set(userNode.parentId, [internalNode]);
}
} }
});
if (parentLookup.size > 0) {
updateAbsolutePositions(nodeLookup, options);
} }
} }
function calculateXYZPosition<NodeType extends NodeBase>( function updateChildPosition<NodeType extends NodeBase>(
node: NodeType, node: InternalNodeBase<NodeType>,
nodeLookup: Map<string, InternalNodeBase<NodeType>>, nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
result: XYZPosition, parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
nodeOrigin: NodeOrigin = [0, 0] options?: UpdateNodesOptions<NodeType>
): XYZPosition { ) {
if (!node.parentId) { const _options = { ...defaultOptions, ...options };
return result;
const parentId = node.parentId!;
const parentNode = nodeLookup.get(parentId);
if (!parentNode) {
throw new Error(`Parent node ${parentId} not found`);
} }
const parent = nodeLookup.get(node.parentId)!; // update the parentLookup
const parentPosition = getNodePositionWithOrigin(parent, nodeOrigin).position; const childNodes = parentLookup.get(parentId);
if (childNodes) {
childNodes.set(node.id, node);
} else {
parentLookup.set(parentId, new Map([[node.id, node]]));
}
return calculateXYZPosition( const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
parent,
nodeLookup, const { x, y, z } = calculateChildXYZ(node, parentNode, _options.nodeOrigin!, selectedNodeZ);
{
x: (result.x ?? 0) + parentPosition.x, const currPosition = node.internals.positionAbsolute;
y: (result.y ?? 0) + parentPosition.y, const positionChanged = x !== currPosition.x || y !== currPosition.y;
z: (parent.internals.z ?? 0) > (result.z ?? 0) ? parent.internals.z ?? 0 : result.z ?? 0,
}, if (positionChanged || z !== node.internals.z) {
parent.origin || nodeOrigin node.internals = {
); ...node.internals,
positionAbsolute: positionChanged ? { x, y } : currPosition,
z,
};
}
}
function calculateZ(node: NodeBase, selectedNodeZ: number) {
return (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0);
}
function calculateChildXYZ<NodeType extends NodeBase>(
childNode: InternalNodeBase<NodeType>,
parentNode: InternalNodeBase<NodeType>,
nodeOrigin: NodeOrigin,
selectedNodeZ: number
) {
const position = getNodePositionWithOrigin(childNode, nodeOrigin);
const childZ = calculateZ(childNode, selectedNodeZ);
const parentZ = parentNode.internals.z ?? 0;
return {
x: parentNode.internals.positionAbsolute.x + position.x,
y: parentNode.internals.positionAbsolute.y + position.y,
z: parentZ > childZ ? parentZ : childZ,
};
} }
export function handleExpandParent( export function handleExpandParent(
children: ParentExpandChild[], children: ParentExpandChild[],
nodeLookup: NodeLookup, nodeLookup: NodeLookup,
parentLookup: ParentLookup, parentLookup: ParentLookup,
nodeOrigin?: NodeOrigin nodeOrigin: NodeOrigin = [0, 0]
): (NodeDimensionChange | NodePositionChange)[] { ): (NodeDimensionChange | NodePositionChange)[] {
const changes: (NodeDimensionChange | NodePositionChange)[] = []; const changes: (NodeDimensionChange | NodePositionChange)[] = [];
const parentExpansions = new Map<string, { expandedRect: Rect; parent: InternalNodeBase }>(); const parentExpansions = new Map<string, { expandedRect: Rect; parent: InternalNodeBase }>();
@@ -170,36 +169,45 @@ export function handleExpandParent(
continue; continue;
} }
const parentRect = const parentRect = parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent);
parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent, parent.origin ?? nodeOrigin);
const expandedRect = getBoundsOfRects(parentRect, child.rect); const expandedRect = getBoundsOfRects(parentRect, child.rect);
parentExpansions.set(child.parentId, { expandedRect, parent }); parentExpansions.set(child.parentId, { expandedRect, parent });
} }
if (parentExpansions.size > 0) { if (parentExpansions.size > 0) {
parentExpansions.forEach(({ expandedRect, parent }, parentId) => { parentExpansions.forEach(({ expandedRect, parent }, parentId) => {
// determine the position & dimensions of the parent // determine the position & dimensions of the parent
const { position } = getNodePositionWithOrigin(parent, parent.origin); const positionAbsolute = parent.internals.positionAbsolute;
const dimensions = getNodeDimensions(parent); const dimensions = getNodeDimensions(parent);
const origin = parent.origin ?? nodeOrigin;
// determine how much the parent expands by moving the position // determine how much the parent expands in width and position
const xChange = expandedRect.x < position.x ? Math.round(Math.abs(position.x - expandedRect.x)) : 0; const xChange =
const yChange = expandedRect.y < position.y ? Math.round(Math.abs(position.y - expandedRect.y)) : 0; expandedRect.x < positionAbsolute.x ? Math.round(Math.abs(positionAbsolute.x - expandedRect.x)) : 0;
const yChange =
expandedRect.y < positionAbsolute.y ? Math.round(Math.abs(positionAbsolute.y - expandedRect.y)) : 0;
if (xChange > 0 || yChange > 0) { const newWidth = Math.max(dimensions.width, Math.round(expandedRect.width));
const newHeight = Math.max(dimensions.height, Math.round(expandedRect.height));
const widthChange = (newWidth - dimensions.width) * origin[0];
const heightChange = (newHeight - dimensions.height) * origin[1];
// We need to correct the position of the parent node if the origin is not [0,0]
if (xChange > 0 || yChange > 0 || widthChange || heightChange) {
changes.push({ changes.push({
id: parentId, id: parentId,
type: 'position', type: 'position',
position: { position: {
x: position.x - xChange, x: parent.position.x - xChange + widthChange,
y: position.y - yChange, y: parent.position.y - yChange + heightChange,
}, },
}); });
// We move all child nodes in the oppsite direction // We move all child nodes in the oppsite direction
// so the x,y changes of the parent do not move the children // so the x,y changes of the parent do not move the children
const childNodes = parentLookup.get(parentId); parentLookup.get(parentId)?.forEach((childNode) => {
childNodes?.forEach((childNode) => {
if (!children.some((child) => child.id === childNode.id)) { if (!children.some((child) => child.id === childNode.id)) {
changes.push({ changes.push({
id: childNode.id, id: childNode.id,
@@ -213,14 +221,15 @@ export function handleExpandParent(
}); });
} }
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height) { // We need to correct the dimensions of the parent node if the origin is not [0,0]
if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height || xChange || yChange) {
changes.push({ changes.push({
id: parentId, id: parentId,
type: 'dimensions', type: 'dimensions',
setAttributes: true, setAttributes: true,
dimensions: { dimensions: {
width: Math.max(dimensions.width, Math.round(expandedRect.width)), width: newWidth + (xChange ? origin[0] * xChange - widthChange : 0),
height: Math.max(dimensions.height, Math.round(expandedRect.height)), height: newHeight + (yChange ? origin[1] * yChange - heightChange : 0),
}, },
}); });
} }
@@ -250,16 +259,19 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
// in this array we collect nodes, that might trigger changes (like expanding parent) // in this array we collect nodes, that might trigger changes (like expanding parent)
const parentExpandChildren: ParentExpandChild[] = []; const parentExpandChildren: ParentExpandChild[] = [];
updates.forEach((update) => { for (const update of updates.values()) {
const node = nodeLookup.get(update.id); const node = nodeLookup.get(update.id);
if (!node) {
continue;
}
if (node?.hidden) { if (node.hidden) {
node.internals = { node.internals = {
...node.internals, ...node.internals,
handleBounds: undefined, handleBounds: undefined,
}; };
updatedInternals = true; updatedInternals = true;
} else if (node) { } else {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height; const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
const doUpdate = !!( const doUpdate = !!(
@@ -270,14 +282,19 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
if (doUpdate) { if (doUpdate) {
const nodeBounds = update.nodeElement.getBoundingClientRect(); const nodeBounds = update.nodeElement.getBoundingClientRect();
node.measured = dimensions; node.measured = dimensions;
node.internals = { node.internals = {
...node.internals, ...node.internals,
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
handleBounds: { handleBounds: {
source: getHandleBounds('.source', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin), source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
target: getHandleBounds('.target', update.nodeElement, nodeBounds, zoom, node.origin || nodeOrigin), target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
}, },
}; };
if (node.parentId) {
updateChildPosition(node, nodeLookup, parentLookup, { nodeOrigin });
}
updatedInternals = true; updatedInternals = true;
@@ -298,7 +315,7 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
} }
} }
} }
}); }
if (parentExpandChildren.length > 0) { if (parentExpandChildren.length > 0) {
const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin);
+1 -2
View File
@@ -124,13 +124,12 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 }; let nodesBox: Box = { x: 0, y: 0, x2: 0, y2: 0 };
if (dragItems.size > 1 && nodeExtent) { if (dragItems.size > 1 && nodeExtent) {
const rect = getInternalNodesBounds(dragItems, { nodeOrigin }); const rect = getInternalNodesBounds(dragItems);
nodesBox = rectToBox(rect); nodesBox = rectToBox(rect);
} }
for (const [id, dragItem] of dragItems) { for (const [id, dragItem] of dragItems) {
let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y }; let nextPosition = { x: x - dragItem.distance.x, y: y - dragItem.distance.y };
if (snapToGrid) { if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid); nextPosition = snapPosition(nextPosition, snapGrid);
} }
+91 -105
View File
@@ -1,75 +1,25 @@
import { pointToRendererPoint, rendererPointToPoint, getHostForElement, calcAutoPan, getEventPosition } from '../utils'; import {
pointToRendererPoint,
getHostForElement,
calcAutoPan,
getEventPosition,
getHandlePosition,
rendererPointToPoint,
} from '../utils';
import { import {
ConnectionMode, ConnectionMode,
type OnConnect,
type OnConnectStart,
type HandleType,
type Connection,
type PanBy,
type Transform,
type ConnectingHandle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
type ConnectionHandle,
NodeLookup,
Position, Position,
oppositePosition,
ConnectionInProgress,
type Handle,
type Connection,
} from '../types'; } from '../types';
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils'; import { getClosestHandle, isConnectionValid, getHandleLookup, getHandleType } from './utils';
import { IsValidParams, OnPointerDownParams, Result, XYHandleInstance } from './types';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform;
getConnectionStartHandle: () => ConnectingHandle | null;
};
export type IsValidParams = {
handle: Pick<ConnectionHandle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode;
fromNodeId: string;
fromHandleId: string | null;
fromType: HandleType;
isValidConnection?: IsValidConnection;
doc: Document | ShadowRoot;
lib: string;
flowId: string | null;
};
export type XYHandleInstance = {
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
};
type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection | null;
endHandle: ConnectingHandle | null;
};
const alwaysValid = () => true; const alwaysValid = () => true;
let connectionStartHandle: ConnectingHandle | null = null;
function onPointerDown( function onPointerDown(
event: MouseEvent | TouchEvent, event: MouseEvent | TouchEvent,
{ {
@@ -93,13 +43,13 @@ function onPointerDown(
onReconnectEnd, onReconnectEnd,
updateConnection, updateConnection,
getTransform, getTransform,
getConnectionStartHandle, getFromHandle,
}: OnPointerDownParams }: OnPointerDownParams
) { ) {
// when xyflow is used inside a shadow root we can't use document // when xyflow is used inside a shadow root we can't use document
const doc = getHostForElement(event.target as HTMLElement); const doc = getHostForElement(event.target as HTMLElement);
let autoPanId = 0; let autoPanId = 0;
let closestHandle: ConnectionHandle | null; let closestHandle: Handle | null;
const { x, y } = getEventPosition(event); const { x, y } = getEventPosition(event);
const clickedHandle = doc?.elementFromPoint(x, y); const clickedHandle = doc?.elementFromPoint(x, y);
@@ -110,13 +60,13 @@ function onPointerDown(
return; return;
} }
let connectionPosition = getEventPosition(event, containerBounds); let position = getEventPosition(event, containerBounds);
let autoPanStarted = false; let autoPanStarted = false;
let connection: Connection | null = null; let connection: Connection | null = null;
let isValid = false; let isValid: boolean | null = false;
let handleDomNode: Element | null = null; let handleDomNode: Element | null = null;
const handleLookup = getHandleLookup({ const [handleLookup, fromHandleInternal] = getHandleLookup({
nodeLookup, nodeLookup,
nodeId, nodeId,
handleId, handleId,
@@ -128,38 +78,54 @@ function onPointerDown(
if (!autoPanOnConnect || !containerBounds) { if (!autoPanOnConnect || !containerBounds) {
return; return;
} }
const [x, y] = calcAutoPan(connectionPosition, containerBounds); const [x, y] = calcAutoPan(position, containerBounds);
panBy({ x, y }); panBy({ x, y });
autoPanId = requestAnimationFrame(autoPan); autoPanId = requestAnimationFrame(autoPan);
} }
// Stays the same for all consecutive pointermove events // Stays the same for all consecutive pointermove events
connectionStartHandle = { const fromHandle: Handle = {
...fromHandleInternal,
nodeId, nodeId,
handleId,
type: handleType, type: handleType,
position: (clickedHandle?.getAttribute('data-handlepos') as Position) || Position.Top, position: fromHandleInternal.position,
}; };
updateConnection({ const fromNodeInternal = nodeLookup.get(nodeId)!;
connectionPosition,
connectionStatus: null, const from = getHandlePosition(fromNodeInternal, fromHandle, Position.Left, true);
connectionStartHandle,
connectionEndHandle: null, const newConnection: ConnectionInProgress = {
}); inProgress: true,
isValid: null,
from,
fromHandle,
fromPosition: fromHandle.position,
fromNode: fromNodeInternal.internals.userNode,
to: position,
toHandle: null,
toPosition: oppositePosition[fromHandle.position],
toNode: null,
};
updateConnection(newConnection);
let previousConnection: ConnectionInProgress = newConnection;
onConnectStart?.(event, { nodeId, handleId, handleType }); onConnectStart?.(event, { nodeId, handleId, handleType });
function onPointerMove(event: MouseEvent | TouchEvent) { function onPointerMove(event: MouseEvent | TouchEvent) {
if (!getConnectionStartHandle()) { if (!getFromHandle() || !fromHandle) {
onPointerUp(event); onPointerUp(event);
return;
} }
const transform = getTransform(); const transform = getTransform();
connectionPosition = getEventPosition(event, containerBounds); position = getEventPosition(event, containerBounds);
closestHandle = getClosestHandle( closestHandle = getClosestHandle(
pointToRendererPoint(connectionPosition, transform, false, [1, 1]), pointToRendererPoint(position, transform, false, [1, 1]),
connectionRadius, connectionRadius,
handleLookup handleLookup
); );
@@ -179,27 +145,42 @@ function onPointerDown(
doc, doc,
lib, lib,
flowId, flowId,
handleLookup,
}); });
handleDomNode = result.handleDomNode; handleDomNode = result.handleDomNode;
connection = result.connection; connection = result.connection;
isValid = result.isValid; isValid = isConnectionValid(!!closestHandle, result.isValid);
updateConnection({ const newConnection: ConnectionInProgress = {
connectionStartHandle, // from stays the same
connectionPosition: ...previousConnection,
isValid,
to:
closestHandle && isValid closestHandle && isValid
? rendererPointToPoint( ? rendererPointToPoint({ x: closestHandle.x, y: closestHandle.y }, transform)
{ : position,
x: closestHandle.x, toHandle: result.toHandle,
y: closestHandle.y, toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position],
}, toNode: result.toHandle ? nodeLookup.get(result.toHandle.nodeId)!.internals.userNode : null,
transform };
)
: connectionPosition, // we don't want to trigger an update when the connection
connectionStatus: getConnectionStatus(!!closestHandle, isValid), // is snapped to the same handle as before
connectionEndHandle: result.endHandle, if (
}); isValid &&
closestHandle &&
previousConnection.toHandle &&
newConnection.toHandle &&
previousConnection.toHandle.type === newConnection.toHandle.type &&
previousConnection.toHandle.nodeId === newConnection.toHandle.nodeId &&
previousConnection.toHandle.id === newConnection.toHandle.id
) {
return;
}
updateConnection(newConnection);
previousConnection = newConnection;
} }
function onPointerUp(event: MouseEvent | TouchEvent) { function onPointerUp(event: MouseEvent | TouchEvent) {
@@ -221,7 +202,6 @@ function onPointerDown(
isValid = false; isValid = false;
connection = null; connection = null;
handleDomNode = null; handleDomNode = null;
connectionStartHandle = null;
doc.removeEventListener('mousemove', onPointerMove as EventListener); doc.removeEventListener('mousemove', onPointerMove as EventListener);
doc.removeEventListener('mouseup', onPointerUp as EventListener); doc.removeEventListener('mouseup', onPointerUp as EventListener);
@@ -250,6 +230,7 @@ function isValidHandle(
lib, lib,
flowId, flowId,
isValidConnection = alwaysValid, isValidConnection = alwaysValid,
handleLookup,
}: IsValidParams }: IsValidParams
) { ) {
const isTarget = fromType === 'target'; const isTarget = fromType === 'target';
@@ -267,7 +248,7 @@ function isValidHandle(
handleDomNode: handleToCheck, handleDomNode: handleToCheck,
isValid: false, isValid: false,
connection: null, connection: null,
endHandle: null, toHandle: null,
}; };
if (handleToCheck) { if (handleToCheck) {
@@ -300,12 +281,17 @@ function isValidHandle(
result.isValid = isValid && isValidConnection(connection); result.isValid = isValid && isValidConnection(connection);
result.endHandle = { if (handleLookup) {
nodeId: handleNodeId as string, const toHandle = handleLookup.find(
handleId, (h) => h.id === handleId && h.nodeId === handleNodeId && h.type === handleType
type: handleType as HandleType, );
position: handleToCheck.getAttribute('data-handlepos') as Position,
}; if (toHandle) {
result.toHandle = {
...toHandle,
};
}
}
} }
return result; return result;
+63
View File
@@ -0,0 +1,63 @@
import {
ConnectionMode,
type Connection,
type OnConnect,
type OnConnectStart,
type HandleType,
type PanBy,
type Transform,
type Handle,
type OnConnectEnd,
type UpdateConnection,
type IsValidConnection,
NodeLookup,
} from '../types';
export type OnPointerDownParams = {
autoPanOnConnect: boolean;
connectionMode: ConnectionMode;
connectionRadius: number;
domNode: HTMLDivElement | null;
handleId: string | null;
nodeId: string;
isTarget: boolean;
nodeLookup: NodeLookup;
lib: string;
flowId: string | null;
edgeUpdaterType?: HandleType;
updateConnection: UpdateConnection;
panBy: PanBy;
cancelConnection: () => void;
onConnectStart?: OnConnectStart;
onConnect?: OnConnect;
onConnectEnd?: OnConnectEnd;
isValidConnection?: IsValidConnection;
onReconnectEnd?: (evt: MouseEvent | TouchEvent) => void;
getTransform: () => Transform;
getFromHandle: () => Handle | null;
};
export type IsValidParams = {
handle: Pick<Handle, 'nodeId' | 'id' | 'type'> | null;
connectionMode: ConnectionMode;
fromNodeId: string;
fromHandleId: string | null;
fromType: HandleType;
isValidConnection?: IsValidConnection;
doc: Document | ShadowRoot;
lib: string;
flowId: string | null;
handleLookup?: Handle[];
};
export type XYHandleInstance = {
onPointerDown: (event: MouseEvent | TouchEvent, params: OnPointerDownParams) => void;
isValid: (event: MouseEvent | TouchEvent, params: IsValidParams) => Result;
};
export type Result = {
handleDomNode: Element | null;
isValid: boolean;
connection: Connection | null;
toHandle: Handle | null;
};
+29 -34
View File
@@ -1,43 +1,36 @@
import { getHandlePosition } from '../utils'; import { getHandlePosition } from '../utils';
import { import {
ConnectionStatus,
type HandleType, type HandleType,
type NodeHandleBounds, type NodeHandleBounds,
type XYPosition, type XYPosition,
type ConnectionHandle, type Handle,
InternalNodeBase, InternalNodeBase,
NodeLookup, NodeLookup,
} from '../types'; } from '../types';
// this functions collects all handles and adds an absolute position // this functions collects all handles and adds an absolute position
// so that we can later find the closest handle to the mouse position // so that we can later find the closest handle to the mouse position
export function getHandles( function getHandles(
node: InternalNodeBase, node: InternalNodeBase,
handleBounds: NodeHandleBounds, handleBounds: NodeHandleBounds,
type: HandleType, type: HandleType,
currentHandle: string currentHandle: { nodeId: string; handleId: string | null; handleType: HandleType }
): ConnectionHandle[] { ): [Handle[], Handle | null] {
return (handleBounds[type] || []).reduce<ConnectionHandle[]>((res, handle) => { let excludedHandle = null;
if (`${node.id}-${handle.id}-${type}` !== currentHandle) { const handles = (handleBounds[type] || []).reduce<Handle[]>((res, handle) => {
const [x, y] = getHandlePosition(node, handle); if (node.id === currentHandle.nodeId && type === currentHandle.handleType && handle.id === currentHandle.handleId) {
res.push({ excludedHandle = handle;
id: handle.id || null, } else {
type, const handleXY = getHandlePosition(node, handle, handle.position, true);
nodeId: node.id, res.push({ ...handle, ...handleXY });
x,
y,
});
} }
return res; return res;
}, []); }, []);
return [handles, excludedHandle];
} }
export function getClosestHandle( export function getClosestHandle(pos: XYPosition, connectionRadius: number, handles: Handle[]): Handle | null {
pos: XYPosition, let closestHandles: Handle[] = [];
connectionRadius: number,
handles: ConnectionHandle[]
): ConnectionHandle | null {
let closestHandles: ConnectionHandle[] = [];
let minDistance = Infinity; let minDistance = Infinity;
for (const handle of handles) { for (const handle of handles) {
@@ -67,7 +60,7 @@ type GetHandleLookupParams = {
nodeLookup: NodeLookup; nodeLookup: NodeLookup;
nodeId: string; nodeId: string;
handleId: string | null; handleId: string | null;
handleType: string; handleType: HandleType;
}; };
export function getHandleLookup({ export function getHandleLookup({
@@ -75,19 +68,21 @@ export function getHandleLookup({
nodeId, nodeId,
handleId, handleId,
handleType, handleType,
}: GetHandleLookupParams): ConnectionHandle[] { }: GetHandleLookupParams): [Handle[], Handle] {
const connectionHandles: ConnectionHandle[] = []; const connectionHandles: Handle[] = [];
const currentHandle = { nodeId, handleId, handleType };
let excludedHandle: Handle | null = null;
for (const [, node] of nodeLookup) { for (const node of nodeLookup.values()) {
if (node.internals.handleBounds) { if (node.internals.handleBounds) {
const id = `${nodeId}-${handleId}-${handleType}`; const [sourceHandles, excludedSource] = getHandles(node, node.internals.handleBounds, 'source', currentHandle);
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id); const [targetHandles, excludedTarget] = getHandles(node, node.internals.handleBounds, 'target', currentHandle);
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id); excludedHandle = excludedHandle ? excludedHandle : excludedSource ?? excludedTarget;
connectionHandles.push(...sourceHandles, ...targetHandles); connectionHandles.push(...sourceHandles, ...targetHandles);
} }
} }
return connectionHandles; return [connectionHandles, excludedHandle!];
} }
export function getHandleType( export function getHandleType(
@@ -105,14 +100,14 @@ export function getHandleType(
return null; return null;
} }
export function getConnectionStatus(isInsideConnectionRadius: boolean, isHandleValid: boolean) { export function isConnectionValid(isInsideConnectionRadius: boolean, isHandleValid: boolean) {
let connectionStatus = null; let isValid: boolean | null = null;
if (isHandleValid) { if (isHandleValid) {
connectionStatus = 'valid'; isValid = true;
} else if (isInsideConnectionRadius && !isHandleValid) { } else if (isInsideConnectionRadius && !isHandleValid) {
connectionStatus = 'invalid'; isValid = false;
} }
return connectionStatus as ConnectionStatus; return isValid;
} }
+36 -25
View File
@@ -3,7 +3,15 @@ import { select } from 'd3-selection';
import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils'; import { getControlDirection, getDimensionsAfterResize, getResizeDirection } from './utils';
import { getPointerPosition } from '../utils'; import { getPointerPosition } from '../utils';
import type { CoordinateExtent, NodeBase, NodeLookup, NodeOrigin, Transform, XYPosition } from '../types'; import type {
CoordinateExtent,
InternalNodeBase,
NodeBase,
NodeLookup,
NodeOrigin,
Transform,
XYPosition,
} from '../types';
import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types'; import type { OnResize, OnResizeEnd, OnResizeStart, ResizeDragEvent, ShouldResize, ControlPosition } from './types';
const initPrevValues = { width: 0, height: 0, x: 0, y: 0 }; const initPrevValues = { width: 0, height: 0, x: 0, y: 0 };
@@ -100,9 +108,9 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
const controlDirection = getControlDirection(controlPosition); const controlDirection = getControlDirection(controlPosition);
let node: NodeBase | undefined = undefined; let node: InternalNodeBase | undefined = undefined;
let childNodes: XYResizerChildChange[] = []; let childNodes: XYResizerChildChange[] = [];
let parentNode: NodeBase | undefined = undefined; // Needed to fix expandParent let parentNode: InternalNodeBase | undefined = undefined; // Needed to fix expandParent
let parentExtent: CoordinateExtent | undefined = undefined; let parentExtent: CoordinateExtent | undefined = undefined;
let childExtent: CoordinateExtent | undefined = undefined; let childExtent: CoordinateExtent | undefined = undefined;
@@ -118,8 +126,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
prevValues = { prevValues = {
width: node.measured?.width ?? 0, width: node.measured.width ?? 0,
height: node.measured?.height ?? 0, height: node.measured.height ?? 0,
x: node.position.x ?? 0, x: node.position.x ?? 0,
y: node.position.y ?? 0, y: node.position.y ?? 0,
}; };
@@ -132,11 +140,10 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
}; };
parentNode = undefined; parentNode = undefined;
if (node.extent === 'parent' || node.expandParent) {
parentNode = nodeLookup.get(node.parentId!); if (node.parentId && (node.extent === 'parent' || node.expandParent)) {
if (parentNode && node.extent === 'parent') { parentNode = nodeLookup.get(node.parentId);
parentExtent = nodeToParentExtent(parentNode); parentExtent = parentNode && node.extent === 'parent' ? nodeToParentExtent(parentNode) : undefined;
}
} }
// Collect all child nodes to correct their relative positions when top/left changes // Collect all child nodes to correct their relative positions when top/left changes
@@ -153,7 +160,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
}); });
if (child.extent === 'parent' || child.expandParent) { if (child.extent === 'parent' || child.expandParent) {
const extent = nodeToChildExtent(child, node!, child.origin ?? nodeOrigin); const extent = nodeToChildExtent(child, node, child.origin ?? nodeOrigin);
if (childExtent) { if (childExtent) {
childExtent = [ childExtent = [
@@ -202,26 +209,15 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
return; return;
} }
if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] == 1) { if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] === 1) {
change.x = isXPosChange ? x : prevValues.x; change.x = isXPosChange ? x : prevValues.x;
change.y = isYPosChange ? y : prevValues.y; change.y = isYPosChange ? y : prevValues.y;
prevValues.x = change.x; prevValues.x = change.x;
prevValues.y = change.y; prevValues.y = change.y;
// Fix expandParent when resizing from top/left // when top/left changes, correct the relative positions of child nodes
if (parentNode && node.expandParent) { // so that they stay in the same position
if (change.x && change.x < 0) {
prevValues.x = 0;
startValues.x = startValues.x - change.x;
}
if (change.y && change.y < 0) {
prevValues.y = 0;
startValues.y = startValues.y - change.y;
}
}
if (childNodes.length > 0) { if (childNodes.length > 0) {
const xChange = x - prevX; const xChange = x - prevX;
const yChange = y - prevY; const yChange = y - prevY;
@@ -243,6 +239,21 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange, onEnd }: X
prevValues.height = change.height; prevValues.height = change.height;
} }
// Fix expandParent when resizing from top/left
if (parentNode && node.expandParent) {
const xLimit = nodeOrigin[0] * (change.width ?? 0);
if (change.x && change.x < xLimit) {
prevValues.x = xLimit;
startValues.x = startValues.x - (change.x - xLimit);
}
const yLimit = nodeOrigin[1] * (change.height ?? 0);
if (change.y && change.y < yLimit) {
prevValues.y = yLimit;
startValues.y = startValues.y - (change.y - yLimit);
}
}
const direction = getResizeDirection({ const direction = getResizeDirection({
width: prevValues.width, width: prevValues.width,
prevWidth, prevWidth,
+1 -1
View File
@@ -10,7 +10,7 @@
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"inlineSources": false, "inlineSources": false,
"isolatedModules": true, "isolatedModules": true,
"moduleResolution": "node", "moduleResolution": "bundler",
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noImplicitAny": true, "noImplicitAny": true,
"noImplicitReturns": false, "noImplicitReturns": false,