@@ -15,8 +15,7 @@ const initialNodes: Node[] = nodes.map((n) => ({
|
||||
|
||||
const expectedNodes: Node[] = initialNodes.map((n) => ({
|
||||
...n,
|
||||
computed: {
|
||||
positionAbsolute: n.position,
|
||||
measured: {
|
||||
...nodeDimensions,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('<ReactFlow />: onNodesChange', () => {
|
||||
id: '1',
|
||||
item: {
|
||||
...nodes[0],
|
||||
computed: { positionAbsolute: nodes[0].position, width: 200, height: 100 },
|
||||
measured: { width: 200, height: 100 },
|
||||
style: { width: 200, height: 100 },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -136,8 +136,8 @@ describe('applyChanges Testing', () => {
|
||||
];
|
||||
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||
|
||||
expect(nodes[0].computed).to.be.undefined;
|
||||
expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||
expect(nodes[0].measured).to.be.undefined;
|
||||
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||
expect(nextNodes[0].width).to.be.undefined;
|
||||
expect(nextNodes[0].height).to.be.undefined;
|
||||
});
|
||||
@@ -153,7 +153,7 @@ describe('applyChanges Testing', () => {
|
||||
const nextNodes = applyNodeChanges(nodeChanges, nodes);
|
||||
|
||||
expect(nextNodes[0].position).to.be.deep.equal(newPosition);
|
||||
expect(nextNodes[0].computed).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||
expect(nextNodes[0].measured).to.be.deep.equal({ width: newWidth, height: newHeight });
|
||||
});
|
||||
|
||||
it('replaces nodes/edges', () => {
|
||||
|
||||
@@ -50,6 +50,7 @@ import UseNodesInitialized from '../examples/UseNodesInit';
|
||||
import UseNodesData from '../examples/UseNodesData';
|
||||
import UseHandleConnections from '../examples/UseHandleConnections';
|
||||
import AddNodeOnEdgeDrop from '../examples/AddNodeOnEdgeDrop';
|
||||
import DevTools from '../examples/DevTools';
|
||||
|
||||
export interface IRoute {
|
||||
name: string;
|
||||
@@ -113,6 +114,11 @@ const routes: IRoute[] = [
|
||||
path: 'default-nodes',
|
||||
component: DefaultNodes,
|
||||
},
|
||||
{
|
||||
name: 'DevTools',
|
||||
path: 'devtools',
|
||||
component: DevTools,
|
||||
},
|
||||
{
|
||||
name: 'Drag Handle',
|
||||
path: 'draghandle',
|
||||
|
||||
@@ -56,18 +56,19 @@ const initialEdges: Edge[] = [
|
||||
const defaultEdgeOptions = {};
|
||||
|
||||
const BasicFlow = () => {
|
||||
const { setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
|
||||
const { addNodes, setNodes, getNodes, setEdges, getEdges, deleteElements, updateNodeData, toObject, setViewport } =
|
||||
useReactFlow();
|
||||
|
||||
const updatePos = () => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.position = {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: Math.random() * 400,
|
||||
y: Math.random() * 400,
|
||||
},
|
||||
};
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -78,9 +79,10 @@ const BasicFlow = () => {
|
||||
const toggleClassnames = () => {
|
||||
setNodes((nodes) =>
|
||||
nodes.map((node) => {
|
||||
node.className = node.className === 'light' ? 'dark' : 'light';
|
||||
|
||||
return node;
|
||||
return {
|
||||
...node,
|
||||
className: node.className === 'light' ? 'dark' : 'light',
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
@@ -108,6 +110,14 @@ const BasicFlow = () => {
|
||||
updateNodeData('1', { label: 'update' });
|
||||
updateNodeData('2', { label: 'update' });
|
||||
};
|
||||
const addNode = () => {
|
||||
addNodes({
|
||||
id: `${Math.random()}`,
|
||||
data: { label: 'Node' },
|
||||
position: { x: Math.random() * 300, y: Math.random() * 300 },
|
||||
className: 'light',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
@@ -144,6 +154,7 @@ const BasicFlow = () => {
|
||||
<button onClick={deleteSomeElements}>deleteSomeElements</button>
|
||||
<button onClick={onSetNodes}>setNodes</button>
|
||||
<button onClick={onUpdateNode}>updateNode</button>
|
||||
<button onClick={addNode}>addNode</button>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
);
|
||||
|
||||
@@ -59,7 +59,7 @@ const CustomMiniMapNodeFlow = () => {
|
||||
onNodeClick={onNodeClick}
|
||||
onConnect={(p) => onConnect(p)}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onlyRenderVisibleElements={false}
|
||||
onlyRenderVisibleElements={true}
|
||||
>
|
||||
<Controls />
|
||||
<Background variant={BackgroundVariant.Lines} />
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { NodeChange, OnNodesChange, useStore, useStoreApi } from '@xyflow/react';
|
||||
|
||||
type ChangeLoggerProps = {
|
||||
color?: string;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
type ChangeInfoProps = {
|
||||
change: NodeChange;
|
||||
};
|
||||
|
||||
function ChangeInfo({ change }: ChangeInfoProps) {
|
||||
const id = 'id' in change ? change.id : '-';
|
||||
const { type } = change;
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<div>node id: {id}</div>
|
||||
<div>
|
||||
{type === 'add' ? JSON.stringify(change.item, null, 2) : null}
|
||||
{type === 'dimensions' ? `${change.dimensions?.width} × ${change.dimensions?.height}` : null}
|
||||
{type === 'position' ? `position: ${change.position?.x.toFixed(1)}, ${change.position?.y.toFixed(1)}` : null}
|
||||
{type === 'remove' ? 'remove' : null}
|
||||
{type === 'replace' ? JSON.stringify(change.item, null, 2) : null}
|
||||
{type === 'select' ? (change.selected ? 'select' : 'unselect') : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ChangeLogger({ limit = 20 }: ChangeLoggerProps) {
|
||||
const [changes, setChanges] = useState<NodeChange[]>([]);
|
||||
const onNodesChangeIntercepted = useRef(false);
|
||||
const onNodesChange = useStore((s) => s.onNodesChange);
|
||||
const store = useStoreApi();
|
||||
|
||||
useEffect(() => {
|
||||
if (!onNodesChange || onNodesChangeIntercepted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
onNodesChangeIntercepted.current = true;
|
||||
const userOnNodesChange = onNodesChange;
|
||||
|
||||
const onNodesChangeLogger: OnNodesChange = (changes) => {
|
||||
userOnNodesChange(changes);
|
||||
|
||||
setChanges((c) => {
|
||||
changes.forEach((change) => {
|
||||
if (c.length >= limit) {
|
||||
c.pop();
|
||||
}
|
||||
|
||||
c = [change, ...c];
|
||||
});
|
||||
return c;
|
||||
});
|
||||
};
|
||||
|
||||
store.setState({ onNodesChange: onNodesChangeLogger });
|
||||
}, [onNodesChange]);
|
||||
|
||||
return (
|
||||
<div className="react-flow__devtools-changelogger">
|
||||
<div className="react-flow__devtools-title">Change Logger</div>
|
||||
{changes.length === 0 ? (
|
||||
<>no changes triggered</>
|
||||
) : (
|
||||
changes.map((change, index) => <ChangeInfo key={index} change={change} />)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useNodes, ViewportPortal } from '@xyflow/react';
|
||||
|
||||
type NodeInfoProps = {
|
||||
id: string;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
data: any;
|
||||
};
|
||||
|
||||
function NodeInfo({ id, type, x, y, width, height, data }: NodeInfoProps) {
|
||||
if (!width || !height) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="react-flow__devtools-nodeinfo"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(${x}px, ${y + height}px)`,
|
||||
width: width * 2,
|
||||
}}
|
||||
>
|
||||
<div>id: {id}</div>
|
||||
<div>type: {type}</div>
|
||||
<div>
|
||||
position: {x.toFixed(1)}, {y.toFixed(1)}
|
||||
</div>
|
||||
<div>
|
||||
dimensions: {width} × {height}
|
||||
</div>
|
||||
<div>data: {JSON.stringify(data, null, 2)}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function NodeInspector() {
|
||||
const nodes = useNodes();
|
||||
|
||||
return (
|
||||
<ViewportPortal>
|
||||
<div className="react-flow__devtools-nodeinspector">
|
||||
{nodes.map((node) => {
|
||||
const x = node.computed?.positionAbsolute?.x || 0;
|
||||
const y = node.computed?.positionAbsolute?.y || 0;
|
||||
const width = node.computed?.width || 0;
|
||||
const height = node.computed?.height || 0;
|
||||
|
||||
return (
|
||||
<NodeInfo
|
||||
key={node.id}
|
||||
id={node.id}
|
||||
type={node.type || 'default'}
|
||||
x={x}
|
||||
y={y}
|
||||
width={width}
|
||||
height={height}
|
||||
data={node.data}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ViewportPortal>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useState, type Dispatch, type SetStateAction, type ReactNode, HTMLAttributes } from 'react';
|
||||
import { Panel, PanelPosition } from '@xyflow/react';
|
||||
|
||||
import NodeInspector from './NodeInspector';
|
||||
import ChangeLogger from './ChangeLogger';
|
||||
|
||||
import './style.css';
|
||||
|
||||
export default function ReactFlowDevTools({ position = 'top-left' }: { position?: PanelPosition }) {
|
||||
const [nodeInspectorActive, setNodeInspectorActive] = useState(false);
|
||||
const [changeLoggerActive, setChangeLoggerActive] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="react-flow__devtools">
|
||||
<Panel position={position}>
|
||||
<DevToolButton setActive={setNodeInspectorActive} active={nodeInspectorActive} title="Toggle Node Inspector">
|
||||
Node Inspector
|
||||
</DevToolButton>
|
||||
<DevToolButton setActive={setChangeLoggerActive} active={changeLoggerActive} title="Toggle Change Logger">
|
||||
Change Logger
|
||||
</DevToolButton>
|
||||
</Panel>
|
||||
{changeLoggerActive && <ChangeLogger />}
|
||||
{nodeInspectorActive && <NodeInspector />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DevToolButton({
|
||||
active,
|
||||
setActive,
|
||||
children,
|
||||
...rest
|
||||
}: {
|
||||
active: boolean;
|
||||
setActive: Dispatch<SetStateAction<boolean>>;
|
||||
children: ReactNode;
|
||||
} & HTMLAttributes<HTMLButtonElement>) {
|
||||
return (
|
||||
<button onClick={() => setActive((a) => !a)} className={active ? 'active' : ''} {...rest}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
.react-flow__devtools {
|
||||
--border-radius: 4px;
|
||||
--highlight-color: rgba(238, 58, 115, 1);
|
||||
--font: monospace, sans-serif;
|
||||
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 11px;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.react-flow__devtools button {
|
||||
background: white;
|
||||
border: none;
|
||||
padding: 5px 15px;
|
||||
color: #222;
|
||||
font-weight: bold;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
font-family: var(--font);
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
|
||||
.react-flow__devtools button:hover {
|
||||
background: var(--highlight-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.react-flow__devtools button.active {
|
||||
background: var(--highlight-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.react-flow__devtools button:first-child {
|
||||
border-radius: var(--border-radius) 0 0 var(--border-radius);
|
||||
border-right: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.react-flow__devtools button:last-child {
|
||||
border-radius: 0 var(--border-radius) var(--border-radius) 0;
|
||||
}
|
||||
|
||||
.react-flow__devtools-changelogger {
|
||||
pointer-events: none;
|
||||
position: relative;
|
||||
top: 50px;
|
||||
left: 20px;
|
||||
font-family: var(--font);
|
||||
}
|
||||
|
||||
.react-flow__devtools-title {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.react-flow__devtools-nodeinspector {
|
||||
pointer-events: none;
|
||||
font-family: monospace, sans-serif;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.react-flow__devtools-nodeinfo {
|
||||
top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { useCallback } from 'react';
|
||||
import { ReactFlow, addEdge, Node, Connection, Edge, useNodesState, useEdgesState } from '@xyflow/react';
|
||||
|
||||
import DevTools from './DevTools';
|
||||
|
||||
const initNodes: Node[] = [
|
||||
{
|
||||
id: '1a',
|
||||
type: 'input',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 250, y: 5 },
|
||||
},
|
||||
{
|
||||
id: '2a',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 100, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '3a',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 400, y: 100 },
|
||||
},
|
||||
{
|
||||
id: '4a',
|
||||
data: { label: 'Node 4' },
|
||||
position: { x: 400, y: 200 },
|
||||
},
|
||||
];
|
||||
|
||||
const initEdges: Edge[] = [
|
||||
{ id: 'e1-2', source: '1a', target: '2a' },
|
||||
{ id: 'e1-3', source: '1a', target: '3a' },
|
||||
];
|
||||
|
||||
const BasicFlow = () => {
|
||||
const [nodes, , onNodesChange] = useNodesState(initNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initEdges);
|
||||
|
||||
const onConnect = useCallback((params: Connection | Edge) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
fitView
|
||||
>
|
||||
<DevTools />
|
||||
</ReactFlow>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicFlow;
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore, getStraightPath, EdgeProps } from '@xyflow/react';
|
||||
import { useStore, getStraightPath, EdgeProps, useInternalNode } from '@xyflow/react';
|
||||
|
||||
import { getEdgeParams } from './utils.js';
|
||||
|
||||
function FloatingEdge({ id, source, target, markerEnd, style }: EdgeProps) {
|
||||
const sourceNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === source), [source]));
|
||||
const targetNode = useStore(useCallback((store) => store.nodes.find((n) => n.id === target), [target]));
|
||||
const sourceNode = useInternalNode(source);
|
||||
const targetNode = useInternalNode(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
|
||||
import { Node, Position, MarkerType, XYPosition, InternalNode } from '@xyflow/react';
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
||||
function getNodeIntersection(intersectionNode: InternalNode, targetNode: InternalNode) {
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
|
||||
const {
|
||||
width: intersectionNodeWidth,
|
||||
height: intersectionNodeHeight,
|
||||
positionAbsolute: intersectionNodePosition,
|
||||
} = intersectionNode.computed || {};
|
||||
const targetPosition = targetNode.computed?.positionAbsolute!;
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured;
|
||||
const intersectionNodePosition = intersectionNode.internals.positionAbsolute;
|
||||
const targetPosition = targetNode.internals.positionAbsolute!;
|
||||
|
||||
const w = intersectionNodeWidth! / 2;
|
||||
const h = intersectionNodeHeight! / 2;
|
||||
|
||||
const x2 = intersectionNodePosition!.x + w;
|
||||
const y2 = intersectionNodePosition!.y + h;
|
||||
const x2 = intersectionNodePosition.x + w;
|
||||
const y2 = intersectionNodePosition.y + h;
|
||||
const x1 = targetPosition.x + w;
|
||||
const y1 = targetPosition.y + h;
|
||||
|
||||
@@ -32,8 +29,8 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
|
||||
}
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to the intersection point
|
||||
function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.computed?.positionAbsolute, ...node };
|
||||
function getEdgePosition(node: InternalNode, intersectionPoint: XYPosition) {
|
||||
const n = { ...node.internals.positionAbsolute, ...node };
|
||||
const nx = Math.round(n.x!);
|
||||
const ny = Math.round(n.y!);
|
||||
const px = Math.round(intersectionPoint.x);
|
||||
@@ -42,13 +39,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + n.computed?.width! - 1) {
|
||||
if (px >= nx + n.measured?.width! - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y! + n.computed?.height! - 1) {
|
||||
if (py >= n.y! + n.measured?.height! - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
@@ -56,7 +53,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import { getEdgeParams } from './utils';
|
||||
|
||||
const FloatingEdge: FC<EdgeProps> = ({ id, source, target, style }) => {
|
||||
const { sourceNode, targetNode } = useStore((s) => {
|
||||
const sourceNode = s.nodes.find((n) => n.id === source);
|
||||
const targetNode = s.nodes.find((n) => n.id === target);
|
||||
const sourceNode = s.nodeLookup.get(source);
|
||||
const targetNode = s.nodeLookup.get(target);
|
||||
|
||||
return { sourceNode, targetNode };
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Position, XYPosition, Node, Edge } from '@xyflow/react';
|
||||
import { Position, XYPosition, Node, Edge, InternalNode } from '@xyflow/react';
|
||||
|
||||
// this helper function returns the intersection point
|
||||
// of the line between the center of the intersectionNode and the target node
|
||||
@@ -6,7 +6,7 @@ function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPositi
|
||||
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
|
||||
|
||||
const { position: intersectionNodePosition } = intersectionNode;
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
|
||||
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.measured ?? {
|
||||
width: 0,
|
||||
height: 0,
|
||||
};
|
||||
@@ -42,13 +42,13 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
if (px <= nx + 1) {
|
||||
return Position.Left;
|
||||
}
|
||||
if (px >= nx + (n.computed?.width ?? 0) - 1) {
|
||||
if (px >= nx + (n.measured?.width ?? 0) - 1) {
|
||||
return Position.Right;
|
||||
}
|
||||
if (py <= ny + 1) {
|
||||
return Position.Top;
|
||||
}
|
||||
if (py >= n.y + (n.computed?.height ?? 0) - 1) {
|
||||
if (py >= n.y + (n.measured?.height ?? 0) - 1) {
|
||||
return Position.Bottom;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ function getEdgePosition(node: Node, intersectionPoint: XYPosition) {
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source: Node, target: Node) {
|
||||
export function getEdgeParams(source: InternalNode, target: InternalNode) {
|
||||
const sourceIntersectionPoint = getNodeIntersection(source, target);
|
||||
const targetIntersectionPoint = getNodeIntersection(target, source);
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
MiniMap,
|
||||
Controls,
|
||||
} from '@xyflow/react';
|
||||
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||
|
||||
const initialNodes: Node[] = [
|
||||
{
|
||||
@@ -82,6 +83,7 @@ const HiddenFlow = () => {
|
||||
>
|
||||
<MiniMap />
|
||||
<Controls />
|
||||
<ReactFlowDevTools position="top-right" />
|
||||
|
||||
<div style={{ position: 'absolute', left: 10, top: 10, zIndex: 4 }}>
|
||||
<div>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
import initialItems from './initial-elements';
|
||||
|
||||
import styles from './layouting.module.css';
|
||||
import ReactFlowDevTools from '../DevTools/DevTools';
|
||||
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
@@ -98,6 +99,7 @@ const LayoutFlow = () => {
|
||||
onEdgesChange={onEdgesChange}
|
||||
>
|
||||
<Controls />
|
||||
<ReactFlowDevTools />
|
||||
</ReactFlow>
|
||||
<Panel position="top-right">
|
||||
<button onClick={() => onLayout('TB')}>vertical layout</button>
|
||||
|
||||
@@ -137,7 +137,7 @@ const initialNodes: Node[] = [
|
||||
label: 'Child with extent: parent',
|
||||
},
|
||||
position: { x: 50, y: 50 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
extent: 'parent',
|
||||
width: 50,
|
||||
height: 100,
|
||||
@@ -148,7 +148,7 @@ const initialNodes: Node[] = [
|
||||
type: 'defaultResizer',
|
||||
data: { label: 'Child with expandParent' },
|
||||
position: { x: 150, y: 100 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
expandParent: true,
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
@@ -157,7 +157,7 @@ const initialNodes: Node[] = [
|
||||
type: 'defaultResizer',
|
||||
data: { label: 'Child with expandParent & keepAspectRatio', keepAspectRatio: true },
|
||||
position: { x: 25, y: 200 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
expandParent: true,
|
||||
style: { ...nodeStyle },
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 4a' },
|
||||
position: { x: 15, y: 15 },
|
||||
className: 'light',
|
||||
parentNode: '4',
|
||||
parentId: '4',
|
||||
origin: [0.5, 0.5],
|
||||
|
||||
extent: [
|
||||
@@ -68,21 +68,21 @@ const initialNodes: Node[] = [
|
||||
height: 200,
|
||||
width: 300,
|
||||
},
|
||||
parentNode: '4',
|
||||
parentId: '4',
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
data: { label: 'Node 4b1' },
|
||||
position: { x: 40, y: 20 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
parentId: '4b',
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
data: { label: 'Node 4b2' },
|
||||
position: { x: 20, y: 100 },
|
||||
className: 'light',
|
||||
parentNode: '4b',
|
||||
parentId: '4b',
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
@@ -98,7 +98,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 5a' },
|
||||
position: { x: 0, y: 0 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
extent: 'parent',
|
||||
},
|
||||
{
|
||||
@@ -106,7 +106,7 @@ const initialNodes: Node[] = [
|
||||
data: { label: 'Node 5b' },
|
||||
position: { x: 225, y: 50 },
|
||||
className: 'light',
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
expandParent: true,
|
||||
},
|
||||
{
|
||||
@@ -160,7 +160,7 @@ const Subflow = () => {
|
||||
const updatePos = () => {
|
||||
setNodes((nds) => {
|
||||
return nds.map((n) => {
|
||||
if (!n.parentNode) {
|
||||
if (!n.parentId) {
|
||||
return {
|
||||
...n,
|
||||
position: {
|
||||
@@ -194,7 +194,7 @@ const Subflow = () => {
|
||||
return nds.map((n) => {
|
||||
return {
|
||||
...n,
|
||||
hidden: !!n.parentNode && !n.hidden,
|
||||
hidden: !!n.parentId && !n.hidden,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
import '@xyflow/svelte/dist/style.css';
|
||||
|
||||
const nodes = writable([
|
||||
const nodes = writable<Node[]>([
|
||||
{
|
||||
id: '1',
|
||||
type: 'input',
|
||||
@@ -55,8 +55,6 @@
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log(event);
|
||||
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'move';
|
||||
}
|
||||
@@ -81,7 +79,8 @@
|
||||
data: { label: `${type} node` }
|
||||
};
|
||||
|
||||
nodes.update((nds) => nds.concat(newNode));
|
||||
$nodes.push(newNode);
|
||||
$nodes = $nodes;
|
||||
};
|
||||
|
||||
$: {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
const { getIntersectingNodes } = useSvelteFlow();
|
||||
|
||||
function onNodeDrag({ detail: { node } }) {
|
||||
const intersections = getIntersectingNodes(node).map((n) => n.id);
|
||||
function onNodeDrag({ detail: { targetNode } }) {
|
||||
const intersections = getIntersectingNodes(targetNode).map((n) => n.id);
|
||||
|
||||
$nodes.forEach((n) => {
|
||||
n.class = intersections.includes(n.id) ? 'highlight' : '';
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
import type { Node, Edge } from '@xyflow/svelte';
|
||||
|
||||
export const initialNodes: Node[] = [
|
||||
{
|
||||
id: '1',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 0, y: 0 },
|
||||
style: 'width: 200px; height: 100px;'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 0, y: 150 }
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 250, y: 0 }
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node' },
|
||||
position: { x: 350, y: 150 },
|
||||
style: 'width: 50px; height: 50px;'
|
||||
}
|
||||
{
|
||||
id: '1',
|
||||
data: { label: 'Node 1' },
|
||||
position: { x: 0, y: 0 },
|
||||
style: 'width: 200px; height: 100px;'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
data: { label: 'Node 2' },
|
||||
position: { x: 0, y: 150 }
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { label: 'Node 3' },
|
||||
position: { x: 250, y: 0 }
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
data: { label: 'Node' },
|
||||
position: { x: 350, y: 150 },
|
||||
style: 'width: 50px; height: 50px;'
|
||||
}
|
||||
];
|
||||
|
||||
export const initialEdges: Edge[] = [];
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
type: 'defaultResizer',
|
||||
data: { label: 'Child with extent parent' },
|
||||
position: { x: 50, y: 50 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
extent: 'parent',
|
||||
style: nodeStyle
|
||||
},
|
||||
@@ -122,7 +122,7 @@
|
||||
type: 'defaultResizer',
|
||||
data: { label: 'Child' },
|
||||
position: { x: 100, y: 100 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
style: nodeStyle
|
||||
}
|
||||
]);
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
id: '4a',
|
||||
data: { label: 'Node 4a' },
|
||||
position: { x: 15, y: 15 },
|
||||
parentNode: '4',
|
||||
parentId: '4',
|
||||
extent: [
|
||||
[0, 0],
|
||||
[100, 100]
|
||||
@@ -47,19 +47,19 @@
|
||||
data: { label: 'Node 4b' },
|
||||
position: { x: 100, y: 60 },
|
||||
style: 'width: 300px; height: 200px;',
|
||||
parentNode: '4'
|
||||
parentId: '4'
|
||||
},
|
||||
{
|
||||
id: '4b1',
|
||||
data: { label: 'Node 4b1' },
|
||||
position: { x: 40, y: 20 },
|
||||
parentNode: '4b'
|
||||
parentId: '4b'
|
||||
},
|
||||
{
|
||||
id: '4b2',
|
||||
data: { label: 'Node 4b2' },
|
||||
position: { x: 20, y: 100 },
|
||||
parentNode: '4b'
|
||||
parentId: '4b'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
@@ -73,14 +73,14 @@
|
||||
id: '5a',
|
||||
data: { label: 'Node 5a' },
|
||||
position: { x: 0, y: 0 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
extent: 'parent'
|
||||
},
|
||||
{
|
||||
id: '5b',
|
||||
data: { label: 'Node 5b' },
|
||||
position: { x: 225, y: 50 },
|
||||
parentNode: '5',
|
||||
parentId: '5',
|
||||
expandParent: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
$$restProps;
|
||||
|
||||
const connections = useHandleConnections({
|
||||
nodeId: id,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
export let data: $$Props['data'];
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
$$restProps;
|
||||
</script>
|
||||
|
||||
<div class="custom">
|
||||
|
||||
@@ -12,6 +12,8 @@
|
||||
type $$Props = NodeProps;
|
||||
|
||||
export let id: $$Props['id'];
|
||||
export let data: $$Props['data'];
|
||||
$$restProps;
|
||||
|
||||
const { updateNodeData } = useSvelteFlow();
|
||||
const connections = useHandleConnections({
|
||||
@@ -22,8 +24,12 @@
|
||||
$: nodeData = useNodesData<MyNode>($connections[0]?.source);
|
||||
$: textNode = isTextNode($nodeData) ? $nodeData : null;
|
||||
|
||||
$: console.log(textNode?.data, data);
|
||||
|
||||
$: {
|
||||
updateNodeData(id, { text: textNode?.data.text.toUpperCase() || '' });
|
||||
const input = textNode?.data.text.toUpperCase() ?? '';
|
||||
updateNodeData(id, { text: input });
|
||||
console.log('updatedNodeData with', input);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,5 +1,33 @@
|
||||
# @xyflow/react
|
||||
|
||||
## 12.0.0-next.14
|
||||
|
||||
## Patch changes
|
||||
|
||||
- fix hidden nodes
|
||||
- use `direction=ltr` for outer wrapper to support rtl sites
|
||||
- allow pinch zoom even if `preventScrolling=false`
|
||||
- export node and edge change related types
|
||||
- only trigger dimensions updates when changes detected
|
||||
|
||||
## 12.0.0-next.13
|
||||
|
||||
## ⚠️ Breaking changes
|
||||
|
||||
- rename `node.parentNode` to `node.parentId`
|
||||
- rename node.computed to node.measured
|
||||
- remove positionAbsolute from `node.computed`
|
||||
|
||||
## Minor Changes
|
||||
|
||||
- new helpers: `useInternalNode` hook, `getInternalNode` function
|
||||
|
||||
## Patch changes
|
||||
|
||||
- remove `internalsSymbol` (now called internals and only available for internal nodes)
|
||||
- handle parentExpand on library side instead of applyChanges
|
||||
- new type `InternalNode`
|
||||
|
||||
## 12.0.0-next.12
|
||||
|
||||
## Patch changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/react",
|
||||
"version": "12.0.0-next.12",
|
||||
"version": "12.0.0-next.14",
|
||||
"description": "React Flow - A highly customizable React library for building node-based editors and interactive flow charts.",
|
||||
"keywords": [
|
||||
"react",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { memo, useEffect, useRef, type MouseEvent, useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||
import { getInternalNodesBounds, getBoundsOfRects, XYMinimap, type Rect, type XYMinimapInstance } from '@xyflow/system';
|
||||
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { Panel } from '../../components/Panel';
|
||||
@@ -26,7 +26,9 @@ const selector = (s: ReactFlowState) => {
|
||||
return {
|
||||
viewBB,
|
||||
boundingRect:
|
||||
s.nodes.length > 0 ? getBoundsOfRects(getNodesBounds(s.nodes, { nodeOrigin: s.nodeOrigin }), viewBB) : viewBB,
|
||||
s.nodeLookup.size > 0
|
||||
? getBoundsOfRects(getInternalNodesBounds(s.nodeLookup, { nodeOrigin: s.nodeOrigin }), viewBB)
|
||||
: viewBB,
|
||||
rfId: s.rfId,
|
||||
nodeOrigin: s.nodeOrigin,
|
||||
panZoom: s.panZoom,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { MiniMapNode } from './MiniMapNode';
|
||||
import type { ReactFlowState, Node } from '../../types';
|
||||
import type { ReactFlowState, Node, InternalNode } from '../../types';
|
||||
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||
|
||||
declare const window: any;
|
||||
@@ -85,7 +85,7 @@ function NodeComponentWrapperInner<NodeType extends Node>({
|
||||
shapeRendering: string;
|
||||
}) {
|
||||
const { node, x, y } = useStore((s) => {
|
||||
const node = s.nodeLookup.get(id) as NodeType;
|
||||
const node = s.nodeLookup.get(id) as InternalNode<NodeType>;
|
||||
const { x, y } = getNodePositionWithOrigin(node, node?.origin || nodeOrigin).positionAbsolute;
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,12 +5,14 @@ import {
|
||||
ResizeControlVariant,
|
||||
type XYResizerInstance,
|
||||
type XYResizerChange,
|
||||
XYResizerChildChange,
|
||||
type XYResizerChildChange,
|
||||
type NodeChange,
|
||||
type NodeDimensionChange,
|
||||
type NodePositionChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import type { NodeChange, NodeDimensionChange, NodePositionChange } from '../../types';
|
||||
import type { ResizeControlProps, ResizeControlLineProps } from './types';
|
||||
|
||||
function ResizeControl({
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
import { useCallback, CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesBounds, Rect, Position, internalsSymbol, getNodeToolbarTransform } from '@xyflow/system';
|
||||
import { Rect, Position, getNodeToolbarTransform, getNodesBounds } from '@xyflow/system';
|
||||
|
||||
import { Node, ReactFlowState } from '../../types';
|
||||
import { InternalNode, ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useNodeId } from '../../contexts/NodeIdContext';
|
||||
import { NodeToolbarPortal } from './NodeToolbarPortal';
|
||||
import type { NodeToolbarProps } from './types';
|
||||
|
||||
const nodeEqualityFn = (a?: Node, b?: Node) =>
|
||||
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
|
||||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
|
||||
a?.computed?.width !== b?.computed?.width ||
|
||||
a?.computed?.height !== b?.computed?.height ||
|
||||
const nodeEqualityFn = (a?: InternalNode, b?: InternalNode) =>
|
||||
a?.internals.positionAbsolute.x !== b?.internals.positionAbsolute.x ||
|
||||
a?.internals.positionAbsolute.y !== b?.internals.positionAbsolute.y ||
|
||||
a?.measured.width !== b?.measured.width ||
|
||||
a?.measured.height !== b?.measured.height ||
|
||||
a?.selected !== b?.selected ||
|
||||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
|
||||
a?.internals.z !== b?.internals.z;
|
||||
|
||||
const nodesEqualityFn = (a: Node[], b: Node[]) => {
|
||||
const nodesEqualityFn = (a: InternalNode[], b: InternalNode[]) => {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
@@ -49,10 +49,10 @@ export function NodeToolbar({
|
||||
const contextNodeId = useNodeId();
|
||||
|
||||
const nodesSelector = useCallback(
|
||||
(state: ReactFlowState): Node[] => {
|
||||
(state: ReactFlowState): InternalNode[] => {
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
|
||||
|
||||
return nodeIds.reduce<Node[]>((acc, id) => {
|
||||
return nodeIds.reduce<InternalNode[]>((acc, id) => {
|
||||
const node = state.nodeLookup.get(id);
|
||||
if (node) {
|
||||
acc.push(node);
|
||||
@@ -74,7 +74,7 @@ export function NodeToolbar({
|
||||
}
|
||||
|
||||
const nodeRect: Rect = getNodesBounds(nodes, { nodeOrigin });
|
||||
const zIndex: number = Math.max(...nodes.map((node) => (node[internalsSymbol]?.z || 1) + 1));
|
||||
const zIndex: number = Math.max(...nodes.map((node) => node.internals.z + 1));
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { CSSProperties, useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
internalsSymbol,
|
||||
Position,
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
@@ -53,7 +52,7 @@ const ConnectionLine = ({
|
||||
),
|
||||
shallow
|
||||
);
|
||||
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
|
||||
const fromHandleBounds = fromNode?.internals.handleBounds;
|
||||
let handleBounds = fromHandleBounds?.[handleType];
|
||||
|
||||
if (connectionMode === ConnectionMode.Loose) {
|
||||
@@ -65,10 +64,10 @@ const ConnectionLine = ({
|
||||
}
|
||||
|
||||
const fromHandle = handleId ? handleBounds.find((d) => d.id === handleId) : handleBounds[0];
|
||||
const fromHandleX = fromHandle ? fromHandle.x + fromHandle.width / 2 : (fromNode.computed?.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle ? fromHandle.y + fromHandle.height / 2 : fromNode.computed?.height ?? 0;
|
||||
const fromX = (fromNode.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
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 = fromPosition ? oppositePosition[fromPosition] : null;
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
cancelConnection,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
rfId: flowId,
|
||||
panBy,
|
||||
updateConnection,
|
||||
@@ -82,7 +82,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
|
||||
domNode,
|
||||
handleId,
|
||||
nodeId,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
isTarget,
|
||||
edgeUpdaterType: handleType,
|
||||
lib,
|
||||
|
||||
@@ -126,7 +126,7 @@ function HandleComponent(
|
||||
connectionMode: currentStore.connectionMode,
|
||||
connectionRadius: currentStore.connectionRadius,
|
||||
domNode: currentStore.domNode,
|
||||
nodes: currentStore.nodes,
|
||||
nodeLookup: currentStore.nodeLookup,
|
||||
lib: currentStore.lib,
|
||||
isTarget,
|
||||
handleId,
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
errorMessages,
|
||||
getNodeDimensions,
|
||||
getPositionWithOrigin,
|
||||
internalsSymbol,
|
||||
isInputDOMNode,
|
||||
nodeHasDimensions,
|
||||
} from '@xyflow/system';
|
||||
@@ -19,7 +18,7 @@ import { useDrag } from '../../hooks/useDrag';
|
||||
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
||||
import { handleNodeClick } from '../Nodes/utils';
|
||||
import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils';
|
||||
import type { Node, NodeWrapperProps } from '../../types';
|
||||
import type { InternalNode, Node, NodeWrapperProps } from '../../types';
|
||||
|
||||
export function NodeWrapper<NodeType extends Node>({
|
||||
id,
|
||||
@@ -44,11 +43,11 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
onError,
|
||||
}: NodeWrapperProps<NodeType>) {
|
||||
const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => {
|
||||
const node = s.nodeLookup.get(id)! as NodeType;
|
||||
const node = s.nodeLookup.get(id)! as InternalNode<NodeType>;
|
||||
|
||||
const positionAbsolute = nodeExtent
|
||||
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
|
||||
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||
? clampPosition(node.internals.positionAbsolute, nodeExtent)
|
||||
: node.internals.positionAbsolute || { x: 0, y: 0 };
|
||||
|
||||
return {
|
||||
node,
|
||||
@@ -56,8 +55,8 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
// so we we need to force a re-render when some change
|
||||
positionAbsoluteX: positionAbsolute.x,
|
||||
positionAbsoluteY: positionAbsolute.y,
|
||||
zIndex: node[internalsSymbol]?.z ?? 0,
|
||||
isParent: !!node[internalsSymbol]?.isParent,
|
||||
zIndex: node.internals.z,
|
||||
isParent: node.internals.isParent,
|
||||
};
|
||||
}, shallow);
|
||||
|
||||
@@ -84,14 +83,16 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
const nodeDimensions = getNodeDimensions(node);
|
||||
const inlineDimensions = getNodeInlineStyleDimensions(node);
|
||||
const initialized = nodeHasDimensions(node);
|
||||
const hasHandleBounds = !!node[internalsSymbol]?.handleBounds;
|
||||
const hasHandleBounds = !!node.internals.handleBounds;
|
||||
|
||||
const moveSelectedNodes = useMoveSelectedNodes();
|
||||
|
||||
useEffect(() => {
|
||||
const currNode = nodeRef.current;
|
||||
|
||||
return () => {
|
||||
if (nodeRef.current) {
|
||||
resizeObserver?.unobserve(nodeRef.current);
|
||||
if (currNode) {
|
||||
resizeObserver?.unobserve(currNode);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
@@ -99,7 +100,6 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
useEffect(() => {
|
||||
if (nodeRef.current && !node.hidden) {
|
||||
const currNode = nodeRef.current;
|
||||
|
||||
if (!initialized || !hasHandleBounds) {
|
||||
resizeObserver?.unobserve(currNode);
|
||||
resizeObserver?.observe(currNode);
|
||||
@@ -123,7 +123,7 @@ export function NodeWrapper<NodeType extends Node>({
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = node.targetPosition;
|
||||
}
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
|
||||
store.getState().updateNodeInternals(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]]));
|
||||
}
|
||||
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InputNode } from '../Nodes/InputNode';
|
||||
import { DefaultNode } from '../Nodes/DefaultNode';
|
||||
import { GroupNode } from '../Nodes/GroupNode';
|
||||
import { OutputNode } from '../Nodes/OutputNode';
|
||||
import type { Node, NodeTypes } from '../../types';
|
||||
import type { InternalNode, Node, NodeTypes } from '../../types';
|
||||
|
||||
export const arrowKeyDiffs: Record<string, XYPosition> = {
|
||||
ArrowUp: { x: 0, y: -1 },
|
||||
@@ -21,12 +21,12 @@ export const builtinNodeTypes: NodeTypes = {
|
||||
};
|
||||
|
||||
export function getNodeInlineStyleDimensions<NodeType extends Node = Node>(
|
||||
node: NodeType
|
||||
node: InternalNode<NodeType>
|
||||
): {
|
||||
width: number | string | undefined;
|
||||
height: number | string | undefined;
|
||||
} {
|
||||
if (!node.computed) {
|
||||
if (node.internals.handleBounds === undefined) {
|
||||
return {
|
||||
width: node.width ?? node.initialWidth ?? node.style?.width,
|
||||
height: node.height ?? node.initialHeight ?? node.style?.height,
|
||||
|
||||
@@ -11,7 +11,7 @@ import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { useDrag } from '../../hooks/useDrag';
|
||||
import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes';
|
||||
import { arrowKeyDiffs } from '../NodeWrapper/utils';
|
||||
import type { Node, ReactFlowState } from '../../types';
|
||||
import type { InternalNode, Node, ReactFlowState } from '../../types';
|
||||
|
||||
export type NodesSelectionProps<NodeType> = {
|
||||
onSelectionContextMenu?: (event: MouseEvent, nodes: NodeType[]) => void;
|
||||
@@ -20,7 +20,13 @@ export type NodesSelectionProps<NodeType> = {
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => {
|
||||
const selectedNodes = s.nodes.filter((n) => n.selected);
|
||||
const selectedNodes: InternalNode[] = [];
|
||||
for (const [, node] of s.nodeLookup) {
|
||||
if (node.selected) {
|
||||
selectedNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
const { width, height, x, y } = getNodesBounds(selectedNodes, { nodeOrigin: s.nodeOrigin });
|
||||
|
||||
return {
|
||||
|
||||
@@ -15,7 +15,7 @@ type SelectionListenerProps = {
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
selectedNodes: s.nodes.filter((n) => n.selected),
|
||||
selectedNodes: Array.from(s.nodeLookup.values()).filter((n) => n.selected),
|
||||
selectedEdges: s.edges.filter((e) => e.selected),
|
||||
});
|
||||
|
||||
|
||||
@@ -138,7 +138,6 @@ export function StoreUpdater<NodeType extends Node = Node, EdgeType extends Edge
|
||||
|
||||
if (fieldValue === previousFieldValue) continue;
|
||||
if (typeof props[fieldName] === 'undefined') continue;
|
||||
|
||||
// Custom handling with dedicated setters for some fields
|
||||
if (fieldName === 'nodes') setNodes(fieldValue as Node[]);
|
||||
else if (fieldName === 'edges') setEdges(fieldValue as Edge[]);
|
||||
|
||||
@@ -2,11 +2,12 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
|
||||
import { ReactFlowState } from '../../types';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { InternalNodeUpdate } from '@xyflow/system';
|
||||
|
||||
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
|
||||
const selector = (s: ReactFlowState) => s.updateNodeInternals;
|
||||
|
||||
export function useResizeObserver() {
|
||||
const updateNodeDimensions = useStore(selector);
|
||||
const updateNodeInternals = useStore(selector);
|
||||
const resizeObserverRef = useRef<ResizeObserver>();
|
||||
|
||||
const resizeObserver = useMemo(() => {
|
||||
@@ -15,18 +16,17 @@ export function useResizeObserver() {
|
||||
}
|
||||
|
||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||
const updates = new Map();
|
||||
const updates = new Map<string, InternalNodeUpdate>();
|
||||
|
||||
entries.forEach((entry: ResizeObserverEntry) => {
|
||||
const id = entry.target.getAttribute('data-id') as string;
|
||||
updates.set(id, {
|
||||
id,
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeDimensions(updates);
|
||||
updateNodeInternals(updates);
|
||||
});
|
||||
|
||||
resizeObserverRef.current = observer;
|
||||
|
||||
@@ -5,13 +5,13 @@
|
||||
import { useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system';
|
||||
import { getNodesInside, getEventPosition, SelectionMode, type NodeChange, type EdgeChange } from '@xyflow/system';
|
||||
|
||||
import { UserSelection } from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles/utils';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { getSelectionChanges } from '../../utils';
|
||||
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange } from '../../types';
|
||||
import type { ReactFlowProps, ReactFlowState } from '../../types';
|
||||
|
||||
type PaneProps = {
|
||||
isSelecting: boolean;
|
||||
@@ -128,7 +128,7 @@ export function Pane({
|
||||
};
|
||||
|
||||
const onMouseMove = (event: ReactMouseEvent): void => {
|
||||
const { userSelectionRect, edges, transform, nodeOrigin, nodes, triggerNodeChanges, triggerEdgeChanges } =
|
||||
const { userSelectionRect, edgeLookup, transform, nodeOrigin, nodeLookup, triggerNodeChanges, triggerEdgeChanges } =
|
||||
store.getState();
|
||||
if (!isSelecting || !containerBounds.current || !userSelectionRect) {
|
||||
return;
|
||||
@@ -149,7 +149,7 @@ export function Pane({
|
||||
};
|
||||
|
||||
const selectedNodes = getNodesInside(
|
||||
nodes,
|
||||
nodeLookup,
|
||||
nextUserSelectRect,
|
||||
transform,
|
||||
selectionMode === SelectionMode.Partial,
|
||||
@@ -163,22 +163,22 @@ export function Pane({
|
||||
for (const selectedNode of selectedNodes) {
|
||||
selectedNodeIds.add(selectedNode.id);
|
||||
|
||||
for (const edge of edges) {
|
||||
for (const [edgeId, edge] of edgeLookup) {
|
||||
if (edge.source === selectedNode.id || edge.target === selectedNode.id) {
|
||||
selectedEdgeIds.add(edge.id);
|
||||
selectedEdgeIds.add(edgeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.size) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.size;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds, true) as NodeChange[];
|
||||
const changes = getSelectionChanges(nodeLookup, selectedNodeIds, true) as NodeChange[];
|
||||
triggerNodeChanges(changes);
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.size) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.size;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
const changes = getSelectionChanges(edgeLookup, selectedEdgeIds) as EdgeChange[];
|
||||
triggerEdgeChanges(changes);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { InternalNode, Node } from '../types';
|
||||
|
||||
/**
|
||||
* Hook for getting an internal node by id
|
||||
*
|
||||
* @public
|
||||
* @param id - id of the node
|
||||
* @returns array with visible node ids
|
||||
*/
|
||||
export function useInternalNode<NodeType extends Node = Node>(id: string): InternalNode<NodeType> | undefined {
|
||||
const node = useStore(
|
||||
useCallback((s) => s.nodeLookup.get(id) as InternalNode<NodeType> | undefined, [id]),
|
||||
shallow
|
||||
);
|
||||
|
||||
return node;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback } from 'react';
|
||||
import { calculateNodePosition, snapPosition, type XYPosition } from '@xyflow/system';
|
||||
|
||||
import { Node } from '../types';
|
||||
import { type Node } from '../types';
|
||||
import { useStoreApi } from './useStore';
|
||||
|
||||
const selectedAndDraggable = (nodesDraggable: boolean) => (n: Node) =>
|
||||
@@ -17,18 +17,11 @@ export function useMoveSelectedNodes() {
|
||||
const store = useStoreApi();
|
||||
|
||||
const moveSelectedNodes = useCallback((params: { direction: XYPosition; factor: number }) => {
|
||||
const {
|
||||
nodeExtent,
|
||||
nodes,
|
||||
snapToGrid,
|
||||
snapGrid,
|
||||
nodesDraggable,
|
||||
onError,
|
||||
updateNodePositions,
|
||||
nodeLookup,
|
||||
nodeOrigin,
|
||||
} = store.getState();
|
||||
const selectedNodes = nodes.filter(selectedAndDraggable(nodesDraggable));
|
||||
const { nodeExtent, snapToGrid, snapGrid, nodesDraggable, onError, updateNodePositions, nodeLookup, nodeOrigin } =
|
||||
store.getState();
|
||||
const nodeUpdates = [];
|
||||
const isSelected = selectedAndDraggable(nodesDraggable);
|
||||
|
||||
// by default a node moves 5px on each key press
|
||||
// if snap grid is enabled, we use that for the velocity
|
||||
const xVelo = snapToGrid ? snapGrid[0] : 5;
|
||||
@@ -37,32 +30,34 @@ export function useMoveSelectedNodes() {
|
||||
const xDiff = params.direction.x * xVelo * params.factor;
|
||||
const yDiff = params.direction.y * yVelo * params.factor;
|
||||
|
||||
const nodeUpdates = selectedNodes.map((node) => {
|
||||
if (node.computed?.positionAbsolute) {
|
||||
let nextPosition = {
|
||||
x: node.computed.positionAbsolute.x + xDiff,
|
||||
y: node.computed.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
nodeId: node.id,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
});
|
||||
|
||||
node.position = position;
|
||||
node.computed.positionAbsolute = positionAbsolute;
|
||||
for (const [, node] of nodeLookup) {
|
||||
if (!isSelected(node)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
let nextPosition = {
|
||||
x: node.internals.positionAbsolute.x + xDiff,
|
||||
y: node.internals.positionAbsolute.y + yDiff,
|
||||
};
|
||||
|
||||
if (snapToGrid) {
|
||||
nextPosition = snapPosition(nextPosition, snapGrid);
|
||||
}
|
||||
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
nodeId: node.id,
|
||||
nextPosition,
|
||||
nodeLookup,
|
||||
nodeExtent,
|
||||
nodeOrigin,
|
||||
onError,
|
||||
});
|
||||
|
||||
node.position = position;
|
||||
node.internals.positionAbsolute = positionAbsolute;
|
||||
|
||||
nodeUpdates.push(node);
|
||||
}
|
||||
|
||||
updateNodePositions(nodeUpdates);
|
||||
}, []);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { internalsSymbol } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { ReactFlowState } from '../types';
|
||||
|
||||
@@ -8,13 +6,13 @@ export type UseNodesInitializedOptions = {
|
||||
};
|
||||
|
||||
const selector = (options: UseNodesInitializedOptions) => (s: ReactFlowState) => {
|
||||
if (s.nodes.length === 0) {
|
||||
if (s.nodeLookup.size === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const node of s.nodes) {
|
||||
for (const [, node] of s.nodeLookup) {
|
||||
if (options.includeHiddenNodes || !node.hidden) {
|
||||
if (node[internalsSymbol]?.handleBounds === undefined) {
|
||||
if (node.internals.handleBounds === undefined) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
getElementsToRemove,
|
||||
getOverlappingArea,
|
||||
isRectObject,
|
||||
nodeHasDimensions,
|
||||
nodeToRect,
|
||||
type Rect,
|
||||
} from '@xyflow/system';
|
||||
import { getElementsToRemove, getOverlappingArea, isRectObject, nodeToRect, type Rect } from '@xyflow/system';
|
||||
|
||||
import useViewportHelper from './useViewportHelper';
|
||||
import { useStoreApi } from './useStore';
|
||||
import type { ReactFlowInstance, Instance, Node, Edge } from '../types';
|
||||
import type { ReactFlowInstance, Instance, Node, Edge, InternalNode } from '../types';
|
||||
import { getElementsDiffChanges, isNode } from '../utils';
|
||||
import { useIsomorphicLayoutEffect } from './useIsomorphicLayoutEffect';
|
||||
|
||||
@@ -27,13 +20,20 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
const viewportHelper = useViewportHelper();
|
||||
const store = useStoreApi();
|
||||
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(() => {
|
||||
return store.getState().nodes.map((n) => ({ ...n })) as NodeType[];
|
||||
}, []);
|
||||
const getNodes = useCallback<Instance.GetNodes<NodeType>>(
|
||||
() => store.getState().nodes.map((n) => ({ ...n })) as NodeType[],
|
||||
[]
|
||||
);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeType>>((id) => {
|
||||
return store.getState().nodeLookup.get(id) as NodeType;
|
||||
}, []);
|
||||
const getInternalNode = useCallback<Instance.GetInternalNode<NodeType>>(
|
||||
(id) => store.getState().nodeLookup.get(id) as InternalNode<NodeType>,
|
||||
[]
|
||||
);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeType>>(
|
||||
(id) => getInternalNode(id)?.internals.userNode as NodeType,
|
||||
[getInternalNode]
|
||||
);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeType>>(() => {
|
||||
const { edges = [] } = store.getState();
|
||||
@@ -223,13 +223,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
[]
|
||||
);
|
||||
|
||||
const getNodeRect = useCallback((nodeOrRect: NodeType | { id: NodeType['id'] }): Rect | null => {
|
||||
const node =
|
||||
isNode(nodeOrRect) && nodeHasDimensions(nodeOrRect)
|
||||
? nodeOrRect
|
||||
: (store.getState().nodeLookup.get(nodeOrRect.id) as NodeType);
|
||||
|
||||
return node ? nodeToRect(node) : null;
|
||||
const getNodeRect = useCallback(({ id }: { id: string }): Rect | null => {
|
||||
const internalNode = store.getState().nodeLookup.get(id);
|
||||
return internalNode ? nodeToRect(internalNode) : null;
|
||||
}, []);
|
||||
|
||||
const getIntersectingNodes = useCallback<Instance.GetIntersectingNodes<NodeType>>(
|
||||
@@ -242,7 +238,9 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
}
|
||||
|
||||
return (nodes || store.getState().nodes).filter((n) => {
|
||||
if (!isRect && (n.id === nodeOrRect!.id || !n.computed?.positionAbsolute)) {
|
||||
const internalNode = store.getState().nodeLookup.get(n.id);
|
||||
|
||||
if (internalNode && !isRect && (n.id === nodeOrRect!.id || !internalNode.internals.positionAbsolute)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -308,6 +306,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
...viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
@@ -325,6 +324,7 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
|
||||
viewportHelper,
|
||||
getNodes,
|
||||
getNode,
|
||||
getInternalNode,
|
||||
getEdges,
|
||||
getEdge,
|
||||
setNodes,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { UpdateNodeInternals, NodeDimensionUpdate } from '@xyflow/system';
|
||||
import type { UpdateNodeInternals, InternalNodeUpdate } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../hooks/useStore';
|
||||
|
||||
@@ -13,18 +13,18 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const store = useStoreApi();
|
||||
|
||||
return useCallback<UpdateNodeInternals>((id: string | string[]) => {
|
||||
const { domNode, updateNodeDimensions } = store.getState();
|
||||
const { domNode, updateNodeInternals } = store.getState();
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = new Map<string, NodeDimensionUpdate>();
|
||||
const updates = new Map<string, InternalNodeUpdate>();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
requestAnimationFrame(() => updateNodeInternals(updates));
|
||||
}, []);
|
||||
}
|
||||
|
||||
@@ -48,12 +48,12 @@ const useViewportHelper = (): ViewportHelperFunctions => {
|
||||
return { x, y, zoom };
|
||||
},
|
||||
fitView: (options) => {
|
||||
const { nodes, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
|
||||
const { nodeLookup, width, height, nodeOrigin, minZoom, maxZoom, panZoom } = store.getState();
|
||||
|
||||
return panZoom
|
||||
? fitView(
|
||||
{
|
||||
nodes,
|
||||
nodeLookup,
|
||||
width,
|
||||
height,
|
||||
nodeOrigin,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
import { useCallback } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { getNodesInside } from '@xyflow/system';
|
||||
|
||||
import { useStore } from './useStore';
|
||||
import type { Node, ReactFlowState } from '../types';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
|
||||
return onlyRenderVisible
|
||||
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||
? getNodesInside<Node>(s.nodeLookup, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||
(node) => node.id
|
||||
)
|
||||
: Array.from(s.nodeLookup.keys());
|
||||
|
||||
@@ -26,9 +26,10 @@ export { useNodesInitialized, type UseNodesInitializedOptions } from './hooks/us
|
||||
export { useHandleConnections } from './hooks/useHandleConnections';
|
||||
export { useNodesData } from './hooks/useNodesData';
|
||||
export { useConnection } from './hooks/useConnection';
|
||||
export { useInternalNode } from './hooks/useInternalNode';
|
||||
export { useNodeId } from './contexts/NodeIdContext';
|
||||
|
||||
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes';
|
||||
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
|
||||
export { isNode, isEdge } from './utils/general';
|
||||
|
||||
export * from './additional-components';
|
||||
@@ -84,6 +85,18 @@ export {
|
||||
type ControlPosition,
|
||||
type ControlLinePosition,
|
||||
type ResizeControlVariant,
|
||||
type NodeChange,
|
||||
type NodeDimensionChange,
|
||||
type NodePositionChange,
|
||||
type NodeSelectionChange,
|
||||
type NodeRemoveChange,
|
||||
type NodeAddChange,
|
||||
type NodeReplaceChange,
|
||||
type EdgeChange,
|
||||
type EdgeSelectionChange,
|
||||
type EdgeRemoveChange,
|
||||
type EdgeAddChange,
|
||||
type EdgeReplaceChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
// system utils
|
||||
@@ -103,5 +116,4 @@ export {
|
||||
addEdge,
|
||||
updateEdge,
|
||||
getConnectedEdges,
|
||||
internalsSymbol,
|
||||
} from '@xyflow/system';
|
||||
|
||||
@@ -2,27 +2,20 @@ import { createWithEqualityFn } from 'zustand/traditional';
|
||||
import {
|
||||
clampPosition,
|
||||
fitView as fitViewSystem,
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
updateAbsolutePositions,
|
||||
panBy as panBySystem,
|
||||
Dimensions,
|
||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||
updateNodeInternals as updateNodeInternalsSystem,
|
||||
updateConnectionLookup,
|
||||
handleParentExpand,
|
||||
NodeChange,
|
||||
EdgeSelectionChange,
|
||||
NodeSelectionChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||
import getInitialState from './initialState';
|
||||
import type {
|
||||
ReactFlowState,
|
||||
Node,
|
||||
Edge,
|
||||
NodeDimensionChange,
|
||||
EdgeSelectionChange,
|
||||
NodeSelectionChange,
|
||||
NodePositionChange,
|
||||
UnselectNodesAndEdgesParams,
|
||||
FitViewOptions,
|
||||
} from '../types';
|
||||
import type { ReactFlowState, Node, Edge, UnselectNodesAndEdgesParams, FitViewOptions, InternalNode } from '../types';
|
||||
|
||||
const createRFStore = ({
|
||||
nodes,
|
||||
@@ -52,9 +45,9 @@ const createRFStore = ({
|
||||
//
|
||||
// When this happens, we take the note objects passed by the user and extend them with fields
|
||||
// relevant for internal React Flow operations.
|
||||
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
|
||||
set({ nodes: nodesWithInternalData });
|
||||
set({ nodes });
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { connectionLookup, edgeLookup } = get();
|
||||
@@ -78,11 +71,10 @@ const createRFStore = ({
|
||||
// Every node gets registerd at a ResizeObserver. Whenever a node
|
||||
// changes its dimensions, this function is called to measure the
|
||||
// new dimensions and update the nodes.
|
||||
updateNodeDimensions: (updates) => {
|
||||
updateNodeInternals: (updates) => {
|
||||
const {
|
||||
onNodesChange,
|
||||
fitView,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
fitViewOnInit,
|
||||
fitViewDone,
|
||||
@@ -91,35 +83,21 @@ const createRFStore = ({
|
||||
nodeOrigin,
|
||||
debug,
|
||||
} = get();
|
||||
const changes: NodeDimensionChange[] = [];
|
||||
|
||||
const updatedNodes = updateNodeDimensionsSystem(
|
||||
updates,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
domNode,
|
||||
nodeOrigin,
|
||||
(id: string, dimensions: Dimensions) => {
|
||||
changes.push({
|
||||
id: id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
});
|
||||
}
|
||||
);
|
||||
const { changes, updatedInternals } = updateNodeInternalsSystem(updates, nodeLookup, domNode, nodeOrigin);
|
||||
|
||||
if (!updatedNodes) {
|
||||
if (!updatedInternals) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin);
|
||||
updateAbsolutePositions(nodeLookup, { nodeOrigin });
|
||||
|
||||
// we call fitView once initially after all dimensions are set
|
||||
let nextFitViewDone = fitViewDone;
|
||||
if (!fitViewDone && fitViewOnInit) {
|
||||
nextFitViewDone = fitView(nextNodes, {
|
||||
nextFitViewDone = fitView({
|
||||
...fitViewOnInitOptions,
|
||||
nodes: fitViewOnInitOptions?.nodes || nextNodes,
|
||||
nodes: fitViewOnInitOptions?.nodes,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -128,7 +106,7 @@ const createRFStore = ({
|
||||
// has not provided an onNodesChange handler.
|
||||
// Nodes are only rendered if they have a width and height
|
||||
// attribute which they get from this handler.
|
||||
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
||||
set({ fitViewDone: nextFitViewDone });
|
||||
|
||||
if (changes?.length > 0) {
|
||||
if (debug) {
|
||||
@@ -138,18 +116,41 @@ const createRFStore = ({
|
||||
}
|
||||
},
|
||||
updateNodePositions: (nodeDragItems, dragging = false) => {
|
||||
const changes = nodeDragItems.map((node) => {
|
||||
const change: NodePositionChange = {
|
||||
const { nodeLookup } = get();
|
||||
const triggerChangeNodes: InternalNode[] = [];
|
||||
|
||||
const changes: NodeChange[] = nodeDragItems.map((node) => {
|
||||
// @todo add expandParent to drag item so that we can get rid of the look up here
|
||||
const internalNode = nodeLookup.get(node.id);
|
||||
const change: NodeChange = {
|
||||
id: node.id,
|
||||
type: 'position',
|
||||
position: node.position,
|
||||
positionAbsolute: node.computed?.positionAbsolute,
|
||||
dragging,
|
||||
};
|
||||
|
||||
if (internalNode?.expandParent && change.position) {
|
||||
triggerChangeNodes.push({
|
||||
...internalNode,
|
||||
position: change.position,
|
||||
internals: {
|
||||
...internalNode.internals,
|
||||
positionAbsolute: node.internals.positionAbsolute,
|
||||
},
|
||||
});
|
||||
|
||||
change.position.x = Math.max(0, change.position.x);
|
||||
change.position.y = Math.max(0, change.position.y);
|
||||
}
|
||||
|
||||
return change;
|
||||
});
|
||||
|
||||
if (triggerChangeNodes.length > 0) {
|
||||
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
|
||||
changes.push(...parentExpandChanges);
|
||||
}
|
||||
|
||||
get().triggerNodeChanges(changes);
|
||||
},
|
||||
triggerNodeChanges: (changes) => {
|
||||
@@ -185,7 +186,7 @@ const createRFStore = ({
|
||||
}
|
||||
},
|
||||
addSelectedNodes: (selectedNodeIds) => {
|
||||
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
|
||||
if (multiSelectionActive) {
|
||||
const nodeChanges = selectedNodeIds.map((nodeId) => createSelectionChange(nodeId, true));
|
||||
@@ -193,11 +194,11 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
triggerNodeChanges(getSelectionChanges(nodes, new Set([...selectedNodeIds]), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edges));
|
||||
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set([...selectedNodeIds]), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edgeLookup));
|
||||
},
|
||||
addSelectedEdges: (selectedEdgeIds) => {
|
||||
const { multiSelectionActive, edges, nodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
const { multiSelectionActive, edgeLookup, nodeLookup, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
|
||||
if (multiSelectionActive) {
|
||||
const changedEdges = selectedEdgeIds.map((edgeId) => createSelectionChange(edgeId, true));
|
||||
@@ -205,8 +206,8 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
triggerEdgeChanges(getSelectionChanges(edges, new Set([...selectedEdgeIds])));
|
||||
triggerNodeChanges(getSelectionChanges(nodes, new Set(), true));
|
||||
triggerEdgeChanges(getSelectionChanges(edgeLookup, new Set([...selectedEdgeIds])));
|
||||
triggerNodeChanges(getSelectionChanges(nodeLookup, new Set(), true));
|
||||
},
|
||||
unselectNodesAndEdges: ({ nodes, edges }: UnselectNodesAndEdgesParams = {}) => {
|
||||
const { edges: storeEdges, nodes: storeNodes, triggerNodeChanges, triggerEdgeChanges } = get();
|
||||
@@ -255,29 +256,30 @@ const createRFStore = ({
|
||||
triggerEdgeChanges(edgeChanges);
|
||||
},
|
||||
setNodeExtent: (nodeExtent) => {
|
||||
const { nodes } = get();
|
||||
const { nodeLookup } = get();
|
||||
|
||||
for (const [, node] of nodeLookup) {
|
||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
||||
|
||||
nodeLookup.set(node.id, {
|
||||
...node,
|
||||
internals: {
|
||||
...node.internals,
|
||||
positionAbsolute,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
set({
|
||||
nodeExtent,
|
||||
nodes: nodes.map((node) => {
|
||||
const positionAbsolute = clampPosition(node.position, nodeExtent);
|
||||
|
||||
return {
|
||||
...node,
|
||||
computed: {
|
||||
...node.computed,
|
||||
positionAbsolute,
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
},
|
||||
panBy: (delta): boolean => {
|
||||
const { transform, width, height, panZoom, translateExtent } = get();
|
||||
return panBySystem({ delta, panZoom, transform, translateExtent, width, height });
|
||||
},
|
||||
fitView: (nodes: Node[], options?: FitViewOptions): boolean => {
|
||||
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin } = get();
|
||||
fitView: (options?: FitViewOptions): boolean => {
|
||||
const { panZoom, width, height, minZoom, maxZoom, nodeOrigin, nodeLookup } = get();
|
||||
|
||||
if (!panZoom) {
|
||||
return false;
|
||||
@@ -285,7 +287,7 @@ const createRFStore = ({
|
||||
|
||||
return fitViewSystem(
|
||||
{
|
||||
nodes,
|
||||
nodeLookup,
|
||||
width,
|
||||
height,
|
||||
panZoom,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
infiniteExtent,
|
||||
ConnectionMode,
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
Transform,
|
||||
@@ -35,7 +35,7 @@ const getInitialState = ({
|
||||
const storeNodes = defaultNodes ?? nodes ?? [];
|
||||
|
||||
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
|
||||
const nextNodes = adoptUserProvidedNodes(storeNodes, nodeLookup, {
|
||||
adoptUserNodes(storeNodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false,
|
||||
});
|
||||
@@ -43,7 +43,7 @@ const getInitialState = ({
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
if (fitView && width && height) {
|
||||
const nodesWithDimensions = nextNodes.filter(
|
||||
const nodesWithDimensions = storeNodes.filter(
|
||||
(node) => (node.width || node.initialWidth) && (node.height || node.initialHeight)
|
||||
);
|
||||
// @todo users nodeOrigin should be used here
|
||||
@@ -57,7 +57,7 @@ const getInitialState = ({
|
||||
width: 0,
|
||||
height: 0,
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodes: storeNodes,
|
||||
nodeLookup,
|
||||
edges: storeEdges,
|
||||
edgeLookup,
|
||||
|
||||
@@ -12,9 +12,11 @@ import {
|
||||
XYPosition,
|
||||
OnBeforeDeleteBase,
|
||||
Connection,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { NodeChange, EdgeChange, Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
|
||||
import type { Node, Edge, ReactFlowInstance, EdgeProps, NodeProps } from '.';
|
||||
|
||||
export type OnNodesChange<NodeType extends Node = Node> = (changes: NodeChange<NodeType>[]) => void;
|
||||
export type OnEdgesChange<EdgeType extends Edge = Edge> = (changes: EdgeChange<EdgeType>[]) => void;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export * from './nodes';
|
||||
export * from './edges';
|
||||
export * from './changes';
|
||||
export * from './component-props';
|
||||
export * from './general';
|
||||
export * from './store';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-namespace */
|
||||
import type { Rect, Viewport } from '@xyflow/system';
|
||||
import type { Node, Edge, ViewportHelperFunctions } from '.';
|
||||
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
|
||||
|
||||
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
nodes: NodeType[];
|
||||
@@ -20,6 +20,7 @@ export namespace Instance {
|
||||
) => void;
|
||||
export type AddNodes<NodeType extends Node = Node> = (payload: NodeType[] | NodeType) => void;
|
||||
export type GetNode<NodeType extends Node = Node> = (id: string) => NodeType | undefined;
|
||||
export type GetInternalNode<NodeType extends Node = Node> = (id: string) => InternalNode<NodeType> | undefined;
|
||||
export type GetEdges<EdgeType extends Edge = Edge> = () => EdgeType[];
|
||||
export type SetEdges<EdgeType extends Edge = Edge> = (
|
||||
payload: EdgeType[] | ((edges: EdgeType[]) => EdgeType[])
|
||||
@@ -83,6 +84,13 @@ export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edg
|
||||
* @returns the node or undefined if no node was found
|
||||
*/
|
||||
getNode: Instance.GetNode<NodeType>;
|
||||
/**
|
||||
* Returns an internal node by id.
|
||||
*
|
||||
* @param id - the node id
|
||||
* @returns the internal node or undefined if no node was found
|
||||
*/
|
||||
getInternalNode: Instance.GetInternalNode<NodeType>;
|
||||
/**
|
||||
* Returns edges.
|
||||
*
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
import type {
|
||||
CoordinateExtent,
|
||||
NodeBase,
|
||||
NodeOrigin,
|
||||
OnError,
|
||||
NodeProps as NodePropsBase,
|
||||
InternalNodeBase,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { NodeTypes } from './general';
|
||||
|
||||
@@ -17,6 +24,14 @@ export type Node<
|
||||
focusable?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* The node data structure that gets used for internal nodes.
|
||||
* There are some data structures added under node.internal
|
||||
* that are needed for tracking some properties
|
||||
* @public
|
||||
*/
|
||||
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
|
||||
|
||||
export type NodeMouseHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, node: NodeType) => void;
|
||||
export type SelectionDragHandler<NodeType extends Node = Node> = (event: ReactMouseEvent, nodes: NodeType[]) => void;
|
||||
export type OnNodeDrag<NodeType extends Node = Node> = (
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
ConnectionMode,
|
||||
type ConnectionStatus,
|
||||
type CoordinateExtent,
|
||||
type NodeDimensionUpdate,
|
||||
type InternalNodeUpdate,
|
||||
type UpdateNodePositions,
|
||||
type NodeOrigin,
|
||||
type OnConnect,
|
||||
@@ -25,12 +25,13 @@ import {
|
||||
type EdgeLookup,
|
||||
type ConnectionLookup,
|
||||
type NodeLookup,
|
||||
NodeChange,
|
||||
EdgeChange,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type {
|
||||
Edge,
|
||||
Node,
|
||||
NodeChange,
|
||||
OnNodesChange,
|
||||
OnEdgesChange,
|
||||
DefaultEdgeOptions,
|
||||
@@ -43,7 +44,7 @@ import type {
|
||||
OnNodeDrag,
|
||||
OnBeforeDelete,
|
||||
IsValidConnection,
|
||||
EdgeChange,
|
||||
InternalNode,
|
||||
} from '.';
|
||||
|
||||
export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
|
||||
@@ -52,7 +53,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: NodeType[];
|
||||
nodeLookup: NodeLookup<NodeType>;
|
||||
nodeLookup: NodeLookup<InternalNode<NodeType>>;
|
||||
edges: Edge[];
|
||||
edgeLookup: EdgeLookup<EdgeType>;
|
||||
connectionLookup: ConnectionLookup;
|
||||
@@ -153,7 +154,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
setNodes: (nodes: NodeType[]) => void;
|
||||
setEdges: (edges: EdgeType[]) => void;
|
||||
setDefaultNodesAndEdges: (nodes?: NodeType[], edges?: EdgeType[]) => void;
|
||||
updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void;
|
||||
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
resetSelectedElements: () => void;
|
||||
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
|
||||
@@ -169,7 +170,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
|
||||
triggerNodeChanges: (changes: NodeChange<NodeType>[]) => void;
|
||||
triggerEdgeChanges: (changes: EdgeChange<EdgeType>[]) => void;
|
||||
panBy: PanBy;
|
||||
fitView: (nodes: NodeType[], options?: FitViewOptions) => boolean;
|
||||
fitView: (options?: FitViewOptions) => boolean;
|
||||
};
|
||||
|
||||
export type ReactFlowState<NodeType extends Node = Node, EdgeType extends Edge = Edge> = ReactFlowStore<
|
||||
|
||||
@@ -1,51 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { EdgeLookup, NodeLookup } from '@xyflow/system';
|
||||
import type { Node, Edge, EdgeChange, NodeChange, NodeSelectionChange, EdgeSelectionChange } from '../types';
|
||||
|
||||
export function handleParentExpand(updatedElements: any[], updateItem: any) {
|
||||
for (const [index, item] of updatedElements.entries()) {
|
||||
if (item.id === updateItem.parentNode) {
|
||||
const parent = { ...item };
|
||||
parent.computed ??= {};
|
||||
|
||||
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
|
||||
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
|
||||
|
||||
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
|
||||
parent.width = parent.width ?? parent.computed.width;
|
||||
parent.height = parent.height ?? parent.computed.height;
|
||||
|
||||
if (extendWidth > 0) {
|
||||
parent.width += extendWidth;
|
||||
}
|
||||
|
||||
if (extendHeight > 0) {
|
||||
parent.height += extendHeight;
|
||||
}
|
||||
|
||||
if (updateItem.position.x < 0) {
|
||||
const xDiff = Math.abs(updateItem.position.x);
|
||||
parent.position.x = parent.position.x - xDiff;
|
||||
parent.width += xDiff;
|
||||
updateItem.position.x = 0;
|
||||
}
|
||||
|
||||
if (updateItem.position.y < 0) {
|
||||
const yDiff = Math.abs(updateItem.position.y);
|
||||
parent.position.y = parent.position.y - yDiff;
|
||||
parent.height += yDiff;
|
||||
updateItem.position.y = 0;
|
||||
}
|
||||
|
||||
parent.computed.width = parent.width;
|
||||
parent.computed.height = parent.height;
|
||||
|
||||
updatedElements[index] = parent;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
import {
|
||||
EdgeLookup,
|
||||
NodeLookup,
|
||||
EdgeChange,
|
||||
NodeChange,
|
||||
NodeSelectionChange,
|
||||
EdgeSelectionChange,
|
||||
} from '@xyflow/system';
|
||||
import type { Node, Edge, InternalNode } from '../types';
|
||||
|
||||
// This function applies changes to nodes or edges that are triggered by React Flow internally.
|
||||
// When you drag a node for example, React Flow will send a position change update.
|
||||
@@ -103,7 +65,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
const updatedElement = { ...element };
|
||||
|
||||
for (const change of changes) {
|
||||
applyChange(change, updatedElement, updatedElements);
|
||||
applyChange(change, updatedElement);
|
||||
}
|
||||
|
||||
updatedElements.push(updatedElement);
|
||||
@@ -113,7 +75,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
}
|
||||
|
||||
// Applies a single change to an element. This is a *mutable* update.
|
||||
function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
function applyChange(change: any, element: any): any {
|
||||
switch (change.type) {
|
||||
case 'select': {
|
||||
element.selected = change.selected;
|
||||
@@ -125,26 +87,18 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
element.position = change.position;
|
||||
}
|
||||
|
||||
if (typeof change.positionAbsolute !== 'undefined') {
|
||||
element.computed ??= {};
|
||||
element.computed.positionAbsolute = change.positionAbsolute;
|
||||
}
|
||||
|
||||
if (typeof change.dragging !== 'undefined') {
|
||||
element.dragging = change.dragging;
|
||||
}
|
||||
|
||||
if (element.expandParent) {
|
||||
handleParentExpand(elements, element);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dimensions': {
|
||||
if (typeof change.dimensions !== 'undefined') {
|
||||
element.computed ??= {};
|
||||
element.computed.width = change.dimensions.width;
|
||||
element.computed.height = change.dimensions.height;
|
||||
element.measured ??= {};
|
||||
element.measured.width = change.dimensions.width;
|
||||
element.measured.height = change.dimensions.height;
|
||||
|
||||
if (change.resizing) {
|
||||
element.width = change.dimensions.width;
|
||||
@@ -156,10 +110,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
|
||||
element.resizing = change.resizing;
|
||||
}
|
||||
|
||||
if (element.expandParent) {
|
||||
handleParentExpand(elements, element);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -228,13 +178,13 @@ export function createSelectionChange(id: string, selected: boolean): NodeSelect
|
||||
}
|
||||
|
||||
export function getSelectionChanges(
|
||||
items: any[],
|
||||
items: Map<string, any>,
|
||||
selectedIds: Set<string> = new Set(),
|
||||
mutateItem = false
|
||||
): NodeSelectionChange[] | EdgeSelectionChange[] {
|
||||
const changes: NodeSelectionChange[] | EdgeSelectionChange[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
for (const [, item] of items) {
|
||||
const willBeSelected = selectedIds.has(item.id);
|
||||
|
||||
// we don't want to set all items to selected=false on the first selection
|
||||
@@ -266,7 +216,7 @@ export function getElementsDiffChanges({
|
||||
lookup,
|
||||
}: {
|
||||
items: Node[] | undefined;
|
||||
lookup: NodeLookup<Node>;
|
||||
lookup: NodeLookup<InternalNode<Node>>;
|
||||
}): NodeChange[];
|
||||
export function getElementsDiffChanges({
|
||||
items,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.DS_Store
|
||||
node_modules
|
||||
/build
|
||||
/dist
|
||||
/.svelte-kit
|
||||
/package
|
||||
.env
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
const {
|
||||
connectionMode,
|
||||
domNode,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
connectionRadius,
|
||||
viewport,
|
||||
isValidConnection,
|
||||
@@ -72,7 +72,7 @@
|
||||
isTarget,
|
||||
connectionRadius: $connectionRadius,
|
||||
domNode: $domNode,
|
||||
nodes: $nodes,
|
||||
nodeLookup: $nodeLookup,
|
||||
connectionMode: $connectionMode,
|
||||
lib: $lib,
|
||||
autoPanOnConnect: $autoPanOnConnect,
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
export let sourcePosition: $$Props['sourcePosition'] = undefined;
|
||||
export let targetPosition: $$Props['targetPosition'] = undefined;
|
||||
export let zIndex: $$Props['zIndex'];
|
||||
export let computedWidth: $$Props['computedWidth'] = undefined;
|
||||
export let computedHeight: $$Props['computedHeight'] = undefined;
|
||||
export let measuredWidth: $$Props['measuredWidth'] = undefined;
|
||||
export let measuredHeight: $$Props['measuredHeight'] = undefined;
|
||||
export let initialWidth: $$Props['initialWidth'] = undefined;
|
||||
export let initialHeight: $$Props['initialHeight'] = undefined;
|
||||
export let width: $$Props['width'] = undefined;
|
||||
@@ -52,7 +52,7 @@
|
||||
nodeDragThreshold,
|
||||
selectNodesOnDrag,
|
||||
handleNodeSelection,
|
||||
updateNodeDimensions
|
||||
updateNodeInternals
|
||||
} = store;
|
||||
|
||||
let nodeRef: HTMLDivElement;
|
||||
@@ -79,8 +79,8 @@
|
||||
height,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
computedWidth,
|
||||
computedHeight
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
});
|
||||
|
||||
$: {
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
if (doUpdate) {
|
||||
requestAnimationFrame(() =>
|
||||
updateNodeDimensions(
|
||||
updateNodeInternals(
|
||||
new Map([
|
||||
[
|
||||
id,
|
||||
|
||||
@@ -21,8 +21,8 @@ export type NodeWrapperProps = Pick<
|
||||
| 'initialWidth'
|
||||
| 'initialHeight'
|
||||
> & {
|
||||
computedWidth?: number;
|
||||
computedHeight?: number;
|
||||
measuredWidth?: number;
|
||||
measuredHeight?: number;
|
||||
type: string;
|
||||
positionX: number;
|
||||
positionY: number;
|
||||
|
||||
@@ -3,20 +3,20 @@ export function getNodeInlineStyleDimensions({
|
||||
height,
|
||||
initialWidth,
|
||||
initialHeight,
|
||||
computedWidth,
|
||||
computedHeight
|
||||
measuredWidth,
|
||||
measuredHeight
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
computedWidth?: number;
|
||||
computedHeight?: number;
|
||||
measuredWidth?: number;
|
||||
measuredHeight?: number;
|
||||
}): {
|
||||
width: string | undefined;
|
||||
height: string | undefined;
|
||||
} {
|
||||
if (computedWidth === undefined && computedHeight === undefined) {
|
||||
if (measuredWidth === undefined && measuredHeight === undefined) {
|
||||
const styleWidth = width ?? initialWidth;
|
||||
const styleHeight = height ?? initialHeight;
|
||||
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
import {
|
||||
internalsSymbol,
|
||||
getPositionWithOrigin,
|
||||
getNodeDimensions,
|
||||
nodeHasDimensions
|
||||
} from '@xyflow/system';
|
||||
import { getPositionWithOrigin, getNodeDimensions, nodeHasDimensions } from '@xyflow/system';
|
||||
|
||||
import { NodeWrapper } from '$lib/components/NodeWrapper';
|
||||
import { useStore } from '$lib/store';
|
||||
@@ -15,7 +10,7 @@
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
elementsSelectable,
|
||||
updateNodeDimensions
|
||||
updateNodeInternals
|
||||
} = useStore();
|
||||
|
||||
const resizeObserver: ResizeObserver | null =
|
||||
@@ -34,7 +29,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
updateNodeDimensions(updates);
|
||||
updateNodeInternals(updates);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
@@ -46,8 +41,8 @@
|
||||
{#each $visibleNodes as node (node.id)}
|
||||
{@const nodeDimesions = getNodeDimensions(node)}
|
||||
{@const posOrigin = getPositionWithOrigin({
|
||||
x: node.computed?.positionAbsolute?.x ?? 0,
|
||||
y: node.computed?.positionAbsolute?.y ?? 0,
|
||||
x: node.internals.positionAbsolute.x,
|
||||
y: node.internals.positionAbsolute.y,
|
||||
...nodeDimesions,
|
||||
origin: node.origin
|
||||
})}
|
||||
@@ -66,26 +61,26 @@
|
||||
node.connectable ||
|
||||
($nodesConnectable && typeof node.connectable === 'undefined')
|
||||
)}
|
||||
positionX={node.computed?.positionAbsolute?.x ?? 0}
|
||||
positionY={node.computed?.positionAbsolute?.y ?? 0}
|
||||
positionX={node.internals.positionAbsolute.x}
|
||||
positionY={node.internals.positionAbsolute.y}
|
||||
positionOriginX={posOrigin.x ?? 0}
|
||||
positionOriginY={posOrigin.y ?? 0}
|
||||
isParent={!!node[internalsSymbol]?.isParent}
|
||||
isParent={!!node.internals.isParent}
|
||||
style={node.style}
|
||||
class={node.class}
|
||||
type={node.type ?? 'default'}
|
||||
sourcePosition={node.sourcePosition}
|
||||
targetPosition={node.targetPosition}
|
||||
dragging={node.dragging}
|
||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
||||
zIndex={node.internals.z ?? 0}
|
||||
dragHandle={node.dragHandle}
|
||||
initialized={nodeHasDimensions(node)}
|
||||
width={node.width}
|
||||
height={node.height}
|
||||
initialWidth={node.initialWidth}
|
||||
initialHeight={node.initialHeight}
|
||||
computedWidth={node.computed?.width}
|
||||
computedHeight={node.computed?.height}
|
||||
measuredWidth={node.measured.width}
|
||||
measuredHeight={node.measured.height}
|
||||
{resizeObserver}
|
||||
on:nodeclick
|
||||
on:nodemouseenter
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
const isSelected = ids.includes(item.id);
|
||||
|
||||
if (item.selected !== isSelected) {
|
||||
return {
|
||||
...item,
|
||||
selected: isSelected
|
||||
};
|
||||
item.selected = isSelected;
|
||||
}
|
||||
|
||||
return item;
|
||||
@@ -56,6 +53,7 @@
|
||||
}>();
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
edges,
|
||||
viewport,
|
||||
dragging,
|
||||
@@ -130,8 +128,8 @@
|
||||
const prevSelectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
const prevSelectedEdgeIds = getConnectedEdges(selectedNodes, $edges).map((e) => e.id);
|
||||
|
||||
selectedNodes = getNodesInside<Node>(
|
||||
$nodes,
|
||||
selectedNodes = getNodesInside(
|
||||
$nodeLookup,
|
||||
nextUserSelectRect,
|
||||
[$viewport.x, $viewport.y, $viewport.zoom],
|
||||
$selectionMode === SelectionMode.Partial,
|
||||
@@ -172,7 +170,7 @@
|
||||
selectionRect.set(null);
|
||||
|
||||
if (selectedNodes.length > 0) {
|
||||
selectionRectMode.set('nodes');
|
||||
$selectionRectMode = 'nodes';
|
||||
}
|
||||
|
||||
// onSelectionEnd?.(event);
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useNodesData(nodeIds: any): any {
|
||||
const _nodeIds = isArrayOfIds ? nodeIds : [nodeIds];
|
||||
|
||||
for (const nodeId of _nodeIds) {
|
||||
const node = nodeLookup.get(nodeId);
|
||||
const node = nodeLookup.get(nodeId)?.internals.userNode;
|
||||
if (node) {
|
||||
nextNodesData.push({
|
||||
id: node.id,
|
||||
|
||||
@@ -256,17 +256,28 @@ export function useSvelteFlow(): {
|
||||
nodeUpdate: Partial<Node> | ((node: Node) => Partial<Node>),
|
||||
options: { replace: boolean } = { replace: false }
|
||||
) => {
|
||||
nodes.update((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === id) {
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
const node = get(nodeLookup).get(id)?.internals.userNode;
|
||||
|
||||
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
|
||||
|
||||
if (options.replace) {
|
||||
nodes.update((nds) =>
|
||||
nds.map((node) => {
|
||||
if (node.id === id) {
|
||||
return isNode(nextNode) ? nextNode : { ...node, ...nextNode };
|
||||
}
|
||||
|
||||
return node;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
Object.assign(node, nextNode);
|
||||
nodes.update((nds) => nds);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -331,11 +342,12 @@ export function useSvelteFlow(): {
|
||||
}
|
||||
|
||||
return (nodesToIntersect || get(nodes)).filter((n) => {
|
||||
if (!isRect && (n.id === nodeOrRect.id || !n.computed?.positionAbsolute)) {
|
||||
const internalNode = get(nodeLookup).get(n.id);
|
||||
if (!internalNode || (!isRect && n.id === nodeOrRect.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const currNodeRect = nodeToRect(n);
|
||||
const currNodeRect = nodeToRect(internalNode);
|
||||
const overlappingArea = getOverlappingArea(currNodeRect, nodeRect);
|
||||
const partiallyVisible = partially && overlappingArea > 0;
|
||||
|
||||
@@ -447,13 +459,17 @@ export function useSvelteFlow(): {
|
||||
},
|
||||
updateNode,
|
||||
updateNodeData: (id, dataUpdate, options) => {
|
||||
updateNode(id, (node) => {
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
const node = get(nodeLookup).get(id)?.internals.userNode;
|
||||
|
||||
return options?.replace
|
||||
? { ...node, data: nextData }
|
||||
: { ...node, data: { ...node.data, ...nextData } };
|
||||
});
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
|
||||
|
||||
node.data = options?.replace ? nextData : { ...node.data, ...nextData };
|
||||
|
||||
nodes.update((nds) => nds);
|
||||
},
|
||||
viewport
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useStore } from '$lib/store';
|
||||
* @returns function for updating node internals
|
||||
*/
|
||||
export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
const { domNode, updateNodeDimensions } = useStore();
|
||||
const { domNode, updateNodeInternals } = useStore();
|
||||
|
||||
// @todo: do we want to add this to system?
|
||||
const updateInternals = (id: string | string[]) => {
|
||||
@@ -27,7 +27,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
}
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
requestAnimationFrame(() => updateNodeInternals(updates));
|
||||
};
|
||||
|
||||
return updateInternals;
|
||||
|
||||
@@ -116,6 +116,5 @@ export {
|
||||
getOutgoers,
|
||||
getConnectedEdges,
|
||||
addEdge,
|
||||
updateEdge,
|
||||
internalsSymbol
|
||||
updateEdge
|
||||
} from '@xyflow/system';
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
};
|
||||
},
|
||||
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
|
||||
const node = $nodeLookup.get(id);
|
||||
const node = $nodeLookup.get(id)?.internals.userNode;
|
||||
if (node) {
|
||||
node.height = change.isHeightChange ? change.height : node.height;
|
||||
node.width = change.isWidthChange ? change.width : node.width;
|
||||
@@ -79,7 +79,7 @@
|
||||
: node.position;
|
||||
|
||||
for (const childChange of childChanges) {
|
||||
const childNode = $nodeLookup.get(childChange.id);
|
||||
const childNode = $nodeLookup.get(childChange.id)?.internals.userNode;
|
||||
if (childNode) {
|
||||
childNode.position = childChange.position;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { getContext } from 'svelte';
|
||||
import {
|
||||
getNodesBounds,
|
||||
Position,
|
||||
type Rect,
|
||||
internalsSymbol,
|
||||
getNodeToolbarTransform
|
||||
} from '@xyflow/system';
|
||||
import { getNodesBounds, Position, type Rect, getNodeToolbarTransform } from '@xyflow/system';
|
||||
import portal from '$lib/actions/portal';
|
||||
import type { Node } from '$lib/types';
|
||||
import type { InternalNode } from '$lib/types';
|
||||
import { useStore } from '$lib/store';
|
||||
|
||||
import type { NodeToolbarProps } from './types';
|
||||
@@ -25,26 +19,26 @@
|
||||
const contextNodeId = getContext<string>('svelteflow__node_id');
|
||||
|
||||
let transform: string;
|
||||
let toolbarNodes: Node[] = [];
|
||||
let toolbarNodes: InternalNode[] = [];
|
||||
let _offset = offset !== undefined ? offset : 10;
|
||||
let _position = position !== undefined ? position : Position.Top;
|
||||
let _align = align !== undefined ? align : 'center';
|
||||
|
||||
$: {
|
||||
// $nodes only needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates
|
||||
if ($nodes) {
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
|
||||
// nly needed to trigger updates, $nodeLookup is just a helper that does not trigger any updates
|
||||
$nodes;
|
||||
|
||||
toolbarNodes = nodeIds.reduce<Node[]>((res, nodeId) => {
|
||||
const node = $nodeLookup.get(nodeId);
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId];
|
||||
|
||||
if (node) {
|
||||
res.push(node);
|
||||
}
|
||||
toolbarNodes = nodeIds.reduce<InternalNode[]>((res, nodeId) => {
|
||||
const node = $nodeLookup.get(nodeId);
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
if (node) {
|
||||
res.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
$: {
|
||||
@@ -54,8 +48,8 @@
|
||||
const toolbarNode = toolbarNodes[0];
|
||||
nodeRect = {
|
||||
...toolbarNode.position,
|
||||
width: toolbarNode.computed?.width ?? toolbarNode.width ?? 0,
|
||||
height: toolbarNode.computed?.height ?? toolbarNode.height ?? 0
|
||||
width: toolbarNode.measured.width ?? toolbarNode.width ?? 0,
|
||||
height: toolbarNode.measured.height ?? toolbarNode.height ?? 0
|
||||
};
|
||||
} else if (toolbarNodes.length > 1) {
|
||||
nodeRect = getNodesBounds(toolbarNodes, { nodeOrigin: $nodeOrigin });
|
||||
@@ -69,7 +63,7 @@
|
||||
$: zIndex =
|
||||
toolbarNodes.length === 0
|
||||
? 1
|
||||
: Math.max(...toolbarNodes.map((node) => (node[internalsSymbol]?.z || 5) + 1));
|
||||
: Math.max(...toolbarNodes.map((node) => (node.internals.z || 5) + 1));
|
||||
|
||||
//FIXME: Possible performance bottleneck
|
||||
$: selectedNodesCount = $nodes.filter((node) => node.selected).length;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
Position,
|
||||
internalsSymbol,
|
||||
type HandleElement
|
||||
} from '@xyflow/system';
|
||||
|
||||
@@ -65,8 +64,9 @@ export function getDerivedConnectionProps(
|
||||
return initConnectionProps;
|
||||
}
|
||||
|
||||
// TODO: it should bail out if the node is not found
|
||||
const fromNode = nodeLookup.get(connection.connectionStartHandle?.nodeId);
|
||||
const fromHandleBounds = fromNode?.[internalsSymbol]?.handleBounds;
|
||||
const fromHandleBounds = fromNode?.internals.handleBounds;
|
||||
const handleBoundsStrict =
|
||||
fromHandleBounds?.[connection.connectionStartHandle.type || 'source'] || [];
|
||||
const handleBoundsLoose: HandleElement[] | undefined | null = handleBoundsStrict
|
||||
@@ -81,12 +81,12 @@ export function getDerivedConnectionProps(
|
||||
: handleBounds?.[0];
|
||||
const fromHandleX = fromHandle
|
||||
? fromHandle.x + fromHandle.width / 2
|
||||
: (fromNode?.computed?.width ?? 0) / 2;
|
||||
: (fromNode?.measured.width ?? 0) / 2;
|
||||
const fromHandleY = fromHandle
|
||||
? fromHandle.y + fromHandle.height / 2
|
||||
: fromNode?.computed?.height ?? 0;
|
||||
const fromX = (fromNode?.computed?.positionAbsolute?.x ?? 0) + fromHandleX;
|
||||
const fromY = (fromNode?.computed?.positionAbsolute?.y ?? 0) + fromHandleY;
|
||||
: 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 = fromPosition ? oppositePosition[fromPosition] : undefined;
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import {
|
||||
fitView as fitViewUtil,
|
||||
getElementsToRemove,
|
||||
panBy as panBySystem,
|
||||
updateNodeDimensions as updateNodeDimensionsSystem,
|
||||
updateNodeInternals as updateNodeInternalsSystem,
|
||||
addEdge as addEdgeUtil,
|
||||
type UpdateNodePositions,
|
||||
type NodeDimensionUpdate,
|
||||
type InternalNodeUpdate,
|
||||
type ViewportHelperFunctionOptions,
|
||||
type Connection,
|
||||
type XYPosition,
|
||||
@@ -64,52 +64,71 @@ export function createStore({
|
||||
const updateNodePositions: UpdateNodePositions = (nodeDragItems, dragging = false) => {
|
||||
const nodeLookup = get(store.nodeLookup);
|
||||
|
||||
nodeDragItems.forEach((nodeDragItem) => {
|
||||
const node = nodeLookup.get(nodeDragItem.id);
|
||||
for (const nodeDragItem of nodeDragItems) {
|
||||
const node = nodeLookup.get(nodeDragItem.id)?.internals.userNode;
|
||||
|
||||
if (node) {
|
||||
node.position = nodeDragItem.position;
|
||||
node.dragging = dragging;
|
||||
node.computed = {
|
||||
...node.computed,
|
||||
positionAbsolute: nodeDragItem.computed?.positionAbsolute
|
||||
};
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
store.nodes.set(get(store.nodes));
|
||||
node.position = nodeDragItem.position;
|
||||
node.dragging = dragging;
|
||||
}
|
||||
|
||||
store.nodes.update((nds) => nds);
|
||||
};
|
||||
|
||||
function updateNodeDimensions(updates: Map<string, NodeDimensionUpdate>) {
|
||||
const nextNodes = updateNodeDimensionsSystem(
|
||||
function updateNodeInternals(updates: Map<string, InternalNodeUpdate>) {
|
||||
const nodeLookup = get(store.nodeLookup);
|
||||
const { changes, updatedInternals } = updateNodeInternalsSystem(
|
||||
updates,
|
||||
get(store.nodes),
|
||||
get(store.nodeLookup),
|
||||
nodeLookup,
|
||||
get(store.domNode),
|
||||
get(store.nodeOrigin)
|
||||
);
|
||||
|
||||
if (!nextNodes) {
|
||||
if (!updatedInternals) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!get(store.fitViewOnInitDone) && get(store.fitViewOnInit)) {
|
||||
const fitViewOptions = get(store.fitViewOptions);
|
||||
const fitViewOnInitDone = fitView(nextNodes, {
|
||||
const fitViewOnInitDone = fitView({
|
||||
...fitViewOptions,
|
||||
nodes: fitViewOptions?.nodes || nextNodes
|
||||
nodes: fitViewOptions?.nodes
|
||||
});
|
||||
store.fitViewOnInitDone.set(fitViewOnInitDone);
|
||||
}
|
||||
|
||||
store.nodes.set(nextNodes);
|
||||
for (const change of changes) {
|
||||
const node = nodeLookup.get(change.id)?.internals.userNode;
|
||||
|
||||
if (!node) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (change.type) {
|
||||
case 'dimensions': {
|
||||
const measured = { ...node.measured, ...change.dimensions };
|
||||
node.width = change.dimensions?.width ?? node.width;
|
||||
node.height = change.dimensions?.height ?? node.height;
|
||||
node.measured = measured;
|
||||
break;
|
||||
}
|
||||
case 'position':
|
||||
node.position = change.position ?? node.position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
store.nodes.update((nds) => nds);
|
||||
|
||||
if (!get(store.nodesInitialized)) {
|
||||
store.nodesInitialized.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
function fitView(nodes: Node[], options?: FitViewOptions) {
|
||||
function fitView(options?: FitViewOptions) {
|
||||
const panZoom = get(store.panZoom);
|
||||
|
||||
if (!panZoom) {
|
||||
@@ -118,7 +137,7 @@ export function createStore({
|
||||
|
||||
return fitViewUtil(
|
||||
{
|
||||
nodes,
|
||||
nodeLookup: get(store.nodeLookup),
|
||||
width: get(store.width),
|
||||
height: get(store.height),
|
||||
minZoom: get(store.minZoom),
|
||||
@@ -186,10 +205,10 @@ export function createStore({
|
||||
|
||||
function unselectNodesAndEdges(params?: { nodes?: Node[]; edges?: Edge[] }) {
|
||||
const resetNodes = resetSelectedElements(params?.nodes || get(store.nodes));
|
||||
if (resetNodes) store.nodes.set(get(store.nodes));
|
||||
if (resetNodes) store.nodes.update((nds) => nds);
|
||||
|
||||
const resetEdges = resetSelectedElements(params?.edges || get(store.edges));
|
||||
if (resetEdges) store.edges.set(get(store.edges));
|
||||
if (resetEdges) store.edges.update((nds) => nds);
|
||||
}
|
||||
|
||||
store.deleteKeyPressed.subscribe(async (deleteKeyPressed) => {
|
||||
@@ -381,10 +400,10 @@ export function createStore({
|
||||
setEdgeTypes,
|
||||
addEdge,
|
||||
updateNodePositions,
|
||||
updateNodeDimensions,
|
||||
updateNodeInternals,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitView: (options?: FitViewOptions) => fitView(get(store.nodes), options),
|
||||
fitView: (options?: FitViewOptions) => fitView(options),
|
||||
setMinZoom,
|
||||
setMaxZoom,
|
||||
setTranslateExtent,
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ConnectionMode,
|
||||
ConnectionLineType,
|
||||
devWarn,
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
getNodesBounds,
|
||||
getViewportForBounds,
|
||||
updateConnectionLookup,
|
||||
@@ -47,7 +47,8 @@ import type {
|
||||
OnDelete,
|
||||
OnEdgeCreate,
|
||||
OnBeforeDelete,
|
||||
IsValidConnection
|
||||
IsValidConnection,
|
||||
InternalNode
|
||||
} from '$lib/types';
|
||||
import { createNodesStore, createEdgesStore } from './utils';
|
||||
import { initConnectionProps, type ConnectionProps } from './derived-connection-props';
|
||||
@@ -80,9 +81,10 @@ export const getInitialStore = ({
|
||||
fitView?: boolean;
|
||||
}) => {
|
||||
const nodeLookup: NodeLookup = new Map();
|
||||
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
|
||||
adoptUserNodes(nodes, nodeLookup, {
|
||||
nodeOrigin: [0, 0],
|
||||
elevateNodesOnSelect: false
|
||||
elevateNodesOnSelect: false,
|
||||
checkEquality: false
|
||||
});
|
||||
const connectionLookup = new Map();
|
||||
const edgeLookup = new Map();
|
||||
@@ -91,9 +93,10 @@ export const getInitialStore = ({
|
||||
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
if (fitView && width && height) {
|
||||
const nodesWithDimensions = nextNodes.filter(
|
||||
const nodesWithDimensions = nodes.filter(
|
||||
(node) => (node.width && node.height) || (node.initialWidth && 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);
|
||||
@@ -101,10 +104,10 @@ export const getInitialStore = ({
|
||||
|
||||
return {
|
||||
flowId: writable<string | null>(null),
|
||||
nodes: createNodesStore(nextNodes, nodeLookup),
|
||||
nodeLookup: readable<NodeLookup<Node>>(nodeLookup),
|
||||
nodes: createNodesStore(nodes, nodeLookup),
|
||||
nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
|
||||
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
|
||||
visibleNodes: readable<Node[]>([]),
|
||||
visibleNodes: readable<InternalNode[]>([]),
|
||||
edges: createEdgesStore(edges, connectionLookup, edgeLookup),
|
||||
visibleEdges: readable<EdgeLayouted[]>([]),
|
||||
connectionLookup: readable<ConnectionLookup>(connectionLookup),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Writable } from 'svelte/store';
|
||||
import type {
|
||||
NodeDimensionUpdate,
|
||||
InternalNodeUpdate,
|
||||
XYPosition,
|
||||
ViewportHelperFunctionOptions,
|
||||
Connection,
|
||||
@@ -27,7 +27,7 @@ export type SvelteFlowStoreActions = {
|
||||
setTranslateExtent: (extent: CoordinateExtent) => void;
|
||||
fitView: (options?: FitViewOptions) => boolean;
|
||||
updateNodePositions: UpdateNodePositions;
|
||||
updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void;
|
||||
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
|
||||
unselectNodesAndEdges: (params?: { nodes?: Node[]; edges?: Edge[] }) => void;
|
||||
addSelectedNodes: (ids: string[]) => void;
|
||||
addSelectedEdges: (ids: string[]) => void;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type Writable
|
||||
} from 'svelte/store';
|
||||
import {
|
||||
adoptUserProvidedNodes,
|
||||
adoptUserNodes,
|
||||
updateConnectionLookup,
|
||||
type Viewport,
|
||||
type PanZoomInstance,
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
type NodeLookup
|
||||
} from '@xyflow/system';
|
||||
|
||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
|
||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
|
||||
|
||||
// we need to sync the user nodes and the internal nodes so that the user can receive the updates
|
||||
// made by Svelte Flow (like dragging or selecting a node).
|
||||
@@ -127,7 +127,7 @@ export type NodeStoreOptions = {
|
||||
// The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes.
|
||||
export const createNodesStore = (
|
||||
nodes: Node[],
|
||||
nodeLookup: NodeLookup<Node>
|
||||
nodeLookup: NodeLookup<InternalNode>
|
||||
): {
|
||||
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
|
||||
update: (this: void, updater: Updater<Node[]>) => void;
|
||||
@@ -141,12 +141,13 @@ export const createNodesStore = (
|
||||
let elevateNodesOnSelect = true;
|
||||
|
||||
const _set = (nds: Node[]): Node[] => {
|
||||
const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
|
||||
adoptUserNodes(nds, nodeLookup, {
|
||||
elevateNodesOnSelect,
|
||||
defaults
|
||||
defaults,
|
||||
checkEquality: false
|
||||
});
|
||||
|
||||
value = nextNodes;
|
||||
value = nds;
|
||||
|
||||
set(value);
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { getNodesInside, type Transform } from '@xyflow/system';
|
||||
|
||||
import type { Node } from '$lib/types';
|
||||
import type { SvelteFlowStoreState } from './types';
|
||||
|
||||
export function getVisibleNodes(store: SvelteFlowStoreState) {
|
||||
return derived(
|
||||
[store.nodes, store.onlyRenderVisibleElements, store.width, store.height, store.viewport],
|
||||
([nodes, onlyRenderVisibleElements, width, height, viewport]) => {
|
||||
[
|
||||
store.nodeLookup,
|
||||
store.onlyRenderVisibleElements,
|
||||
store.width,
|
||||
store.height,
|
||||
store.viewport,
|
||||
store.nodes
|
||||
],
|
||||
([nodeLookup, onlyRenderVisibleElements, width, height, viewport]) => {
|
||||
const transform: Transform = [viewport.x, viewport.y, viewport.zoom];
|
||||
|
||||
return onlyRenderVisibleElements
|
||||
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true)
|
||||
: nodes;
|
||||
? getNodesInside(nodeLookup, { x: 0, y: 0, width, height }, transform, true)
|
||||
: Array.from(nodeLookup.values());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { ComponentType, SvelteComponent } from 'svelte';
|
||||
import type { NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
import type { InternalNodeBase, NodeBase, NodeProps as NodePropsBase } from '@xyflow/system';
|
||||
|
||||
/**
|
||||
* The node data structure that gets used for internal nodes.
|
||||
* There are some data structures added under node.internal
|
||||
* that are needed for tracking some properties
|
||||
* @public
|
||||
*/
|
||||
export type InternalNode<NodeType extends Node = Node> = InternalNodeBase<NodeType>;
|
||||
|
||||
/**
|
||||
* The node data structure that gets used for the nodes prop.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@xyflow/system",
|
||||
"version": "0.0.21",
|
||||
"version": "0.0.23",
|
||||
"description": "xyflow core system that powers React Flow and Svelte Flow.",
|
||||
"keywords": [
|
||||
"node-based UI",
|
||||
|
||||
@@ -25,8 +25,6 @@ export const errorMessages = {
|
||||
`Node with id "${id}" does not exist, it may have been removed. This can happen when a node is deleted before the "onNodeClick" handler is called.`,
|
||||
};
|
||||
|
||||
export const internalsSymbol = Symbol.for('internals');
|
||||
|
||||
export const infiniteExtent: CoordinateExtent = [
|
||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */
|
||||
|
||||
.xy-flow {
|
||||
direction: ltr;
|
||||
|
||||
--xy-edge-stroke-default: #b1b1b7;
|
||||
--xy-edge-stroke-width-default: 1;
|
||||
--xy-edge-stroke-selected-default: #555;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { XYPosition, Dimensions } from '@xyflow/system';
|
||||
|
||||
import type { Node, Edge } from '.';
|
||||
import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
|
||||
|
||||
export type NodeDimensionChange = {
|
||||
id: string;
|
||||
@@ -28,12 +26,12 @@ export type NodeRemoveChange = {
|
||||
type: 'remove';
|
||||
};
|
||||
|
||||
export type NodeAddChange<NodeType extends Node = Node> = {
|
||||
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
|
||||
item: NodeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type NodeReplaceChange<NodeType extends Node = Node> = {
|
||||
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
|
||||
id: string;
|
||||
item: NodeType;
|
||||
type: 'replace';
|
||||
@@ -43,7 +41,7 @@ export type NodeReplaceChange<NodeType extends Node = Node> = {
|
||||
* Union type of all possible node changes.
|
||||
* @public
|
||||
*/
|
||||
export type NodeChange<NodeType extends Node = Node> =
|
||||
export type NodeChange<NodeType extends NodeBase = NodeBase> =
|
||||
| NodeDimensionChange
|
||||
| NodePositionChange
|
||||
| NodeSelectionChange
|
||||
@@ -53,18 +51,18 @@ export type NodeChange<NodeType extends Node = Node> =
|
||||
|
||||
export type EdgeSelectionChange = NodeSelectionChange;
|
||||
export type EdgeRemoveChange = NodeRemoveChange;
|
||||
export type EdgeAddChange<EdgeType extends Edge = Edge> = {
|
||||
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||
item: EdgeType;
|
||||
type: 'add';
|
||||
};
|
||||
|
||||
export type EdgeReplaceChange<EdgeType extends Edge = Edge> = {
|
||||
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
|
||||
id: string;
|
||||
item: EdgeType;
|
||||
type: 'replace';
|
||||
};
|
||||
|
||||
export type EdgeChange<EdgeType extends Edge = Edge> =
|
||||
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
|
||||
| EdgeSelectionChange
|
||||
| EdgeRemoveChange
|
||||
| EdgeAddChange<EdgeType>
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
|
||||
|
||||
import type { XYPosition, Rect } from './utils';
|
||||
import type { NodeBase, NodeDragItem, NodeOrigin } from './nodes';
|
||||
import type { InternalNodeBase, NodeBase, NodeDragItem, NodeOrigin } from './nodes';
|
||||
import type { ConnectingHandle, HandleType } from './handles';
|
||||
import { PanZoomInstance } from './panzoom';
|
||||
import { EdgeBase } from '..';
|
||||
@@ -52,7 +52,7 @@ export type OnConnectEnd = (event: MouseEvent | TouchEvent) => void;
|
||||
export type IsValidConnection = (edge: EdgeBase | Connection) => boolean;
|
||||
|
||||
export type FitViewParamsBase<NodeType extends NodeBase> = {
|
||||
nodes: NodeType[];
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>;
|
||||
width: number;
|
||||
height: number;
|
||||
panZoom: PanZoomInstance;
|
||||
@@ -127,7 +127,7 @@ export type SelectionRect = Rect & {
|
||||
|
||||
export type OnError = (id: string, message: string) => void;
|
||||
|
||||
export type UpdateNodePositions = (dragItems: NodeDragItem[] | NodeBase[], dragging?: boolean) => void;
|
||||
export type UpdateNodePositions = (dragItems: NodeDragItem[] | InternalNodeBase[], dragging?: boolean) => void;
|
||||
export type PanBy = (delta: XYPosition) => boolean;
|
||||
|
||||
export type UpdateConnection = (params: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './changes';
|
||||
export * from './general';
|
||||
export * from './nodes';
|
||||
export * from './edges';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { internalsSymbol } from '../constants';
|
||||
import type { XYPosition, Position, CoordinateExtent, HandleElement } from '.';
|
||||
import { Optional } from '../utils/types';
|
||||
|
||||
@@ -44,7 +43,7 @@ export type NodeBase<
|
||||
initialWidth?: number;
|
||||
initialHeight?: number;
|
||||
/** Parent node id, used for creating sub-flows */
|
||||
parentNode?: string;
|
||||
parentId?: string;
|
||||
zIndex?: number;
|
||||
/** Boundary a node can be moved in
|
||||
* @example 'parent' or [[0, 0], [100, 100]]
|
||||
@@ -60,21 +59,26 @@ export type NodeBase<
|
||||
*/
|
||||
origin?: NodeOrigin;
|
||||
handles?: NodeHandle[];
|
||||
computed?: {
|
||||
measured?: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
positionAbsolute?: XYPosition;
|
||||
};
|
||||
};
|
||||
|
||||
// Only used internally
|
||||
[internalsSymbol]?: {
|
||||
z?: number;
|
||||
export type InternalNodeBase<NodeType extends NodeBase = NodeBase> = NodeType & {
|
||||
measured: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
};
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
z: number;
|
||||
// @todo should we rename this to "handles" and use same type as node.handles?
|
||||
isParent: boolean;
|
||||
/** Holds a reference to the original node object provided by the user.
|
||||
* Used as an optimization to avoid certain operations. */
|
||||
userNode: NodeType;
|
||||
handleBounds?: NodeHandleBounds;
|
||||
isParent?: boolean;
|
||||
/** Holds a reference to the original node object provided by the user
|
||||
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used
|
||||
* as an optimization to avoid certain operations. */
|
||||
userProvidedNode: NodeBase<NodeData, NodeType>;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -101,10 +105,10 @@ export type NodeHandleBounds = {
|
||||
target: HandleElement[] | null;
|
||||
};
|
||||
|
||||
export type NodeDimensionUpdate = {
|
||||
export type InternalNodeUpdate = {
|
||||
id: string;
|
||||
nodeElement: HTMLDivElement;
|
||||
forceUpdate?: boolean;
|
||||
force?: boolean;
|
||||
};
|
||||
|
||||
export type NodeBounds = XYPosition & {
|
||||
@@ -117,13 +121,15 @@ export type NodeDragItem = {
|
||||
position: XYPosition;
|
||||
// distance from the mouse cursor to the node when start dragging
|
||||
distance: XYPosition;
|
||||
computed: {
|
||||
measured: {
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
};
|
||||
internals: {
|
||||
positionAbsolute: XYPosition;
|
||||
};
|
||||
extent?: 'parent' | CoordinateExtent;
|
||||
parentNode?: string;
|
||||
parentId?: string;
|
||||
dragging?: boolean;
|
||||
origin?: NodeOrigin;
|
||||
expandParent?: boolean;
|
||||
@@ -137,4 +143,4 @@ export type NodeHandle = Optional<HandleElement, 'width' | 'height'>;
|
||||
|
||||
export type Align = 'center' | 'start' | 'end';
|
||||
|
||||
export type NodeLookup<NodeType extends NodeBase = NodeBase> = Map<string, NodeType>;
|
||||
export type NodeLookup<NodeType extends InternalNodeBase = InternalNodeBase> = Map<string, NodeType>;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Connection, Transform, errorMessages, internalsSymbol, isEdgeBase } from '../..';
|
||||
import { EdgeBase, NodeBase } from '../../types';
|
||||
import { Connection, InternalNodeBase, Transform, errorMessages, isEdgeBase, EdgeBase } from '../..';
|
||||
import { getOverlappingArea, boxToRect, nodeToBox, getBoundsOfBoxes, devWarn } from '../general';
|
||||
|
||||
// this is used for straight edges and simple smoothstep edges (LTR, RTL, BTT, TTB)
|
||||
@@ -24,8 +23,8 @@ export function getEdgeCenter({
|
||||
}
|
||||
|
||||
export type GetEdgeZIndexParams = {
|
||||
sourceNode: NodeBase;
|
||||
targetNode: NodeBase;
|
||||
sourceNode: InternalNodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
selected?: boolean;
|
||||
zIndex?: number;
|
||||
elevateOnSelect?: boolean;
|
||||
@@ -43,14 +42,14 @@ export function getElevatedEdgeZIndex({
|
||||
}
|
||||
|
||||
const edgeOrConnectedNodeSelected = selected || targetNode.selected || sourceNode.selected;
|
||||
const selectedZIndex = Math.max(sourceNode[internalsSymbol]?.z || 0, targetNode[internalsSymbol]?.z || 0, 1000);
|
||||
const selectedZIndex = Math.max(sourceNode.internals.z || 0, targetNode.internals.z || 0, 1000);
|
||||
|
||||
return zIndex + (edgeOrConnectedNodeSelected ? selectedZIndex : 0);
|
||||
}
|
||||
|
||||
type IsEdgeVisibleParams = {
|
||||
sourceNode: NodeBase;
|
||||
targetNode: NodeBase;
|
||||
sourceNode: InternalNodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
width: number;
|
||||
height: number;
|
||||
transform: Transform;
|
||||
|
||||
@@ -1,25 +1,26 @@
|
||||
import { EdgePosition } from '../../types/edges';
|
||||
import { ConnectionMode, OnError } from '../../types/general';
|
||||
import { NodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { InternalNodeBase, NodeHandle } from '../../types/nodes';
|
||||
import { Position } from '../../types/utils';
|
||||
import { errorMessages, internalsSymbol } from '../../constants';
|
||||
import { errorMessages } from '../../constants';
|
||||
import { HandleElement } from '../../types';
|
||||
import { getNodeDimensions } from '../general';
|
||||
|
||||
export type GetEdgePositionParams = {
|
||||
id: string;
|
||||
sourceNode: NodeBase;
|
||||
sourceNode: InternalNodeBase;
|
||||
sourceHandle: string | null;
|
||||
targetNode: NodeBase;
|
||||
targetNode: InternalNodeBase;
|
||||
targetHandle: string | null;
|
||||
connectionMode: ConnectionMode;
|
||||
onError?: OnError;
|
||||
};
|
||||
|
||||
function isNodeInitialized(node: NodeBase): boolean {
|
||||
function isNodeInitialized(node: InternalNodeBase): boolean {
|
||||
return (
|
||||
!!(node?.[internalsSymbol]?.handleBounds || node?.handles?.length) &&
|
||||
!!(node?.computed?.width || node?.width || node?.initialWidth)
|
||||
node &&
|
||||
!!(node.internals.handleBounds || node.handles?.length) &&
|
||||
!!(node.measured.width || node.width || node.initialWidth)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,8 +31,8 @@ export function getEdgePosition(params: GetEdgePositionParams): EdgePosition | n
|
||||
return null;
|
||||
}
|
||||
|
||||
const sourceHandleBounds = sourceNode[internalsSymbol]?.handleBounds || toHandleBounds(sourceNode.handles);
|
||||
const targetHandleBounds = targetNode[internalsSymbol]?.handleBounds || toHandleBounds(targetNode.handles);
|
||||
const sourceHandleBounds = sourceNode.internals.handleBounds || toHandleBounds(sourceNode.handles);
|
||||
const targetHandleBounds = targetNode.internals.handleBounds || toHandleBounds(targetNode.handles);
|
||||
|
||||
const sourceHandle = getHandle(sourceHandleBounds?.source ?? [], params.sourceHandle);
|
||||
const targetHandle = getHandle(
|
||||
@@ -95,9 +96,9 @@ function toHandleBounds(handles?: NodeHandle[]) {
|
||||
};
|
||||
}
|
||||
|
||||
function getHandlePosition(position: Position, node: NodeBase, handle: HandleElement | null = null): number[] {
|
||||
const x = (handle?.x ?? 0) + (node.computed?.positionAbsolute?.x ?? 0);
|
||||
const y = (handle?.y ?? 0) + (node.computed?.positionAbsolute?.y ?? 0);
|
||||
function getHandlePosition(position: Position, node: InternalNodeBase, handle: HandleElement | null = null): number[] {
|
||||
const x = (handle?.x ?? 0) + node.internals.positionAbsolute.x;
|
||||
const y = (handle?.y ?? 0) + node.internals.positionAbsolute.y;
|
||||
const { width, height } = handle ?? getNodeDimensions(node);
|
||||
|
||||
switch (position) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
NodeOrigin,
|
||||
SnapGrid,
|
||||
Transform,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
import { type Viewport } from '../types';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
@@ -65,23 +66,23 @@ export const boxToRect = ({ x, y, x2, y2 }: Box): Rect => ({
|
||||
height: y2 - y,
|
||||
});
|
||||
|
||||
export const nodeToRect = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
export const nodeToRect = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Rect => {
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
|
||||
return {
|
||||
...positionAbsolute,
|
||||
width: node.computed?.width ?? node.width ?? 0,
|
||||
height: node.computed?.height ?? node.height ?? 0,
|
||||
width: node.measured?.width ?? node.width ?? 0,
|
||||
height: node.measured?.height ?? node.height ?? 0,
|
||||
};
|
||||
};
|
||||
|
||||
export const nodeToBox = (node: NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
||||
export const nodeToBox = (node: InternalNodeBase | NodeBase, nodeOrigin: NodeOrigin = [0, 0]): Box => {
|
||||
const { positionAbsolute } = getNodePositionWithOrigin(node, node.origin || nodeOrigin);
|
||||
|
||||
return {
|
||||
...positionAbsolute,
|
||||
x2: positionAbsolute.x + (node.computed?.width ?? node.width ?? 0),
|
||||
y2: positionAbsolute.y + (node.computed?.height ?? node.height ?? 0),
|
||||
x2: positionAbsolute.x + (node.measured?.width ?? node.width ?? 0),
|
||||
y2: positionAbsolute.y + (node.measured?.height ?? node.height ?? 0),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -207,14 +208,14 @@ export function getNodeDimensions<NodeType extends NodeBase = NodeBase>(
|
||||
node: NodeType
|
||||
): { width: number; height: number } {
|
||||
return {
|
||||
width: node.computed?.width ?? node.width ?? node.initialWidth ?? 0,
|
||||
height: node.computed?.height ?? node.height ?? node.initialHeight ?? 0,
|
||||
width: node.measured?.width ?? node.width ?? node.initialWidth ?? 0,
|
||||
height: node.measured?.height ?? node.height ?? node.initialHeight ?? 0,
|
||||
};
|
||||
}
|
||||
|
||||
export function nodeHasDimensions<NodeType extends NodeBase = NodeBase>(node: NodeType): boolean {
|
||||
return (
|
||||
(node.computed?.width ?? node.width ?? node.initialWidth) !== undefined &&
|
||||
(node.computed?.height ?? node.height ?? node.initialHeight) !== undefined
|
||||
(node.measured?.width ?? node.width ?? node.initialWidth) !== undefined &&
|
||||
(node.measured?.height ?? node.height ?? node.initialHeight) !== undefined
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
OnError,
|
||||
OnBeforeDeleteBase,
|
||||
NodeLookup,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
import { errorMessages } from '../constants';
|
||||
|
||||
@@ -47,6 +48,10 @@ export const isEdgeBase = <EdgeType extends EdgeBase = EdgeBase>(element: any):
|
||||
export const isNodeBase = <NodeType extends NodeBase = NodeBase>(element: any): element is NodeType =>
|
||||
'id' in element && 'position' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
export const isInternalNodeBase = <NodeType extends InternalNodeBase = InternalNodeBase>(
|
||||
element: any
|
||||
): element is NodeType => 'id' in element && 'internals' in element && !('source' in element) && !('target' in element);
|
||||
|
||||
/**
|
||||
* Pass in a node, and get connected nodes where edge.source === node.id
|
||||
* @public
|
||||
@@ -101,7 +106,7 @@ export const getIncomers = <NodeType extends NodeBase = NodeBase, EdgeType exten
|
||||
};
|
||||
|
||||
export const getNodePositionWithOrigin = (
|
||||
node: NodeBase | undefined,
|
||||
node: InternalNodeBase | NodeBase | undefined,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): { position: XYPosition; positionAbsolute: XYPosition } => {
|
||||
if (!node) {
|
||||
@@ -128,12 +133,13 @@ export const getNodePositionWithOrigin = (
|
||||
|
||||
return {
|
||||
position,
|
||||
positionAbsolute: node.computed?.positionAbsolute
|
||||
? {
|
||||
x: node.computed.positionAbsolute.x - offsetX,
|
||||
y: node.computed.positionAbsolute.y - offsetY,
|
||||
}
|
||||
: position,
|
||||
positionAbsolute:
|
||||
'internals' in node
|
||||
? {
|
||||
x: node.internals.positionAbsolute.x - offsetX,
|
||||
y: node.internals.positionAbsolute.y - offsetY,
|
||||
}
|
||||
: position,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -151,6 +157,7 @@ export type GetNodesBoundsParams = {
|
||||
* @param params.useRelativePosition - Whether to use the relative or absolute node positions
|
||||
* @returns Bounding box enclosing all nodes
|
||||
*/
|
||||
// @todo how to handle this if users do not have absolute positions?
|
||||
export const getNodesBounds = (
|
||||
nodes: NodeBase[],
|
||||
params: GetNodesBoundsParams = { nodeOrigin: [0, 0], useRelativePosition: false }
|
||||
@@ -176,28 +183,69 @@ export const getNodesBounds = (
|
||||
return boxToRect(box);
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
export type GetInternalNodesBoundsParams = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
useRelativePosition?: boolean;
|
||||
filter?: (node: NodeBase) => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Determines a bounding box that contains all given nodes in an array
|
||||
* @internal
|
||||
*/
|
||||
export const getInternalNodesBounds = (
|
||||
nodeLookup: NodeLookup,
|
||||
params: GetInternalNodesBoundsParams = {
|
||||
nodeOrigin: [0, 0],
|
||||
useRelativePosition: false,
|
||||
}
|
||||
): Rect => {
|
||||
if (nodeLookup.size === 0) {
|
||||
return { x: 0, y: 0, width: 0, height: 0 };
|
||||
}
|
||||
|
||||
let box = { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity };
|
||||
|
||||
nodeLookup.forEach((node) => {
|
||||
if (params.filter == undefined || params.filter(node)) {
|
||||
const nodePos = getNodePositionWithOrigin(node, node.origin || params.nodeOrigin);
|
||||
box = getBoundsOfBoxes(
|
||||
box,
|
||||
rectToBox({
|
||||
...nodePos[params.useRelativePosition ? 'position' : 'positionAbsolute'],
|
||||
...getNodeDimensions(node),
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return boxToRect(box);
|
||||
};
|
||||
|
||||
export const getNodesInside = <NodeType extends NodeBase = NodeBase>(
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
rect: Rect,
|
||||
[tx, ty, tScale]: Transform = [0, 0, 1],
|
||||
partially = false,
|
||||
// set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute
|
||||
excludeNonSelectableNodes = false,
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): NodeType[] => {
|
||||
): InternalNodeBase<NodeType>[] => {
|
||||
const paneRect = {
|
||||
...pointToRendererPoint(rect, [tx, ty, tScale]),
|
||||
width: rect.width / tScale,
|
||||
height: rect.height / tScale,
|
||||
};
|
||||
|
||||
const visibleNodes = nodes.reduce<NodeType[]>((res, node) => {
|
||||
const { computed, selectable = true, hidden = false } = node;
|
||||
const width = computed?.width ?? node.width ?? node.initialWidth ?? null;
|
||||
const height = computed?.height ?? node.height ?? node.initialHeight ?? null;
|
||||
const visibleNodes: InternalNodeBase<NodeType>[] = [];
|
||||
|
||||
for (const [, node] of nodeLookup) {
|
||||
const { measured, selectable = true, hidden = false } = node;
|
||||
const width = measured.width ?? node.width ?? node.initialWidth ?? null;
|
||||
const height = measured.height ?? node.height ?? node.initialHeight ?? null;
|
||||
|
||||
if ((excludeNonSelectableNodes && !selectable) || hidden) {
|
||||
return res;
|
||||
continue;
|
||||
}
|
||||
|
||||
const overlappingArea = getOverlappingArea(paneRect, nodeToRect(node, nodeOrigin));
|
||||
@@ -208,11 +256,9 @@ export const getNodesInside = <NodeType extends NodeBase>(
|
||||
const isVisible = notInitialized || partiallyVisible || overlappingArea >= area;
|
||||
|
||||
if (isVisible || node.dragging) {
|
||||
res.push(node);
|
||||
visibleNodes.push(node);
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return visibleNodes;
|
||||
};
|
||||
@@ -236,17 +282,21 @@ export const getConnectedEdges = <NodeType extends NodeBase = NodeBase, EdgeType
|
||||
};
|
||||
|
||||
export function fitView<Params extends FitViewParamsBase<NodeBase>, Options extends FitViewOptionsBase<NodeBase>>(
|
||||
{ nodes, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
||||
{ nodeLookup, width, height, panZoom, minZoom, maxZoom, nodeOrigin = [0, 0] }: Params,
|
||||
options?: Options
|
||||
) {
|
||||
const filteredNodes = nodes.filter((n) => {
|
||||
const isVisible = n.computed?.width && n.computed?.height && (options?.includeHiddenNodes || !n.hidden);
|
||||
const filteredNodes: InternalNodeBase[] = [];
|
||||
|
||||
if (options?.nodes?.length) {
|
||||
return isVisible && options?.nodes.some((optionNode) => optionNode.id === n.id);
|
||||
nodeLookup.forEach((n) => {
|
||||
const isVisible = n.measured.width && n.measured.height && (options?.includeHiddenNodes || !n.hidden);
|
||||
|
||||
// TODO: this remove options.nodes.some with a Set
|
||||
if (
|
||||
isVisible &&
|
||||
(!options?.nodes || (options?.nodes.length && options?.nodes.some((optionNode) => optionNode.id === n.id)))
|
||||
) {
|
||||
filteredNodes.push(n);
|
||||
}
|
||||
|
||||
return isVisible;
|
||||
});
|
||||
|
||||
if (filteredNodes.length > 0) {
|
||||
@@ -284,7 +334,7 @@ function clampNodeExtent<NodeType extends NodeBase>(
|
||||
if (!extent || extent === 'parent') {
|
||||
return extent;
|
||||
}
|
||||
return [extent[0], [extent[1][0] - (node.computed?.width ?? 0), extent[1][1] - (node.computed?.height ?? 0)]];
|
||||
return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,26 +353,27 @@ export function calculateNodePosition<NodeType extends NodeBase>({
|
||||
}: {
|
||||
nodeId: string;
|
||||
nextPosition: XYPosition;
|
||||
nodeLookup: NodeLookup<NodeType>;
|
||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>;
|
||||
nodeOrigin?: NodeOrigin;
|
||||
nodeExtent?: CoordinateExtent;
|
||||
onError?: OnError;
|
||||
}): { position: XYPosition; positionAbsolute: XYPosition } {
|
||||
const node = nodeLookup.get(nodeId)!;
|
||||
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : undefined;
|
||||
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
|
||||
const { x: parentX, y: parentY } = parentNode
|
||||
? getNodePositionWithOrigin(parentNode, parentNode.origin || nodeOrigin).positionAbsolute
|
||||
: { x: 0, y: 0 };
|
||||
|
||||
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
|
||||
|
||||
if (node.extent === 'parent' && !node.expandParent) {
|
||||
if (!parentNode) {
|
||||
onError?.('005', errorMessages['error005']());
|
||||
} else {
|
||||
const nodeWidth = node.computed?.width;
|
||||
const nodeHeight = node.computed?.height;
|
||||
const parentWidth = parentNode?.computed?.width;
|
||||
const parentHeight = parentNode?.computed?.height;
|
||||
const nodeWidth = node.measured.width;
|
||||
const nodeHeight = node.measured.height;
|
||||
const parentWidth = parentNode.measured.width;
|
||||
const parentHeight = parentNode.measured.height;
|
||||
|
||||
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
|
||||
const currNodeOrigin = node.origin || nodeOrigin;
|
||||
@@ -390,7 +441,7 @@ export async function getElementsToRemove<NodeType extends NodeBase = NodeBase,
|
||||
}
|
||||
|
||||
const isIncluded = nodeIds.includes(node.id);
|
||||
const parentHit = !isIncluded && node.parentNode && matchingNodes.find((n) => n.id === node.parentNode);
|
||||
const parentHit = !isIncluded && node.parentId && matchingNodes.find((n) => n.id === node.parentId);
|
||||
|
||||
if (isIncluded || parentHit) {
|
||||
matchingNodes.push(node);
|
||||
|
||||
+186
-104
@@ -1,9 +1,7 @@
|
||||
import { internalsSymbol } from '../constants';
|
||||
import {
|
||||
NodeBase,
|
||||
CoordinateExtent,
|
||||
Dimensions,
|
||||
NodeDimensionUpdate,
|
||||
InternalNodeUpdate,
|
||||
NodeOrigin,
|
||||
PanZoomInstance,
|
||||
Transform,
|
||||
@@ -12,184 +10,254 @@ import {
|
||||
ConnectionLookup,
|
||||
EdgeBase,
|
||||
EdgeLookup,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
Rect,
|
||||
NodeDimensionChange,
|
||||
NodePositionChange,
|
||||
} from '../types';
|
||||
import { getDimensions, getHandleBounds } from './dom';
|
||||
import { isNumeric } from './general';
|
||||
import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
|
||||
import { getNodePositionWithOrigin } from './graph';
|
||||
|
||||
type ParentNodes = Record<string, boolean>;
|
||||
|
||||
export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeOrigin: NodeOrigin = [0, 0],
|
||||
parentNodes?: ParentNodes
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
options: UpdateNodesOptions<NodeType> = {
|
||||
nodeOrigin: [0, 0] as NodeOrigin,
|
||||
elevateNodesOnSelect: true,
|
||||
defaults: {},
|
||||
},
|
||||
parentNodeIds?: Set<string>
|
||||
) {
|
||||
return nodes.map((node) => {
|
||||
if (node.parentNode && !nodeLookup.has(node.parentNode)) {
|
||||
throw new Error(`Parent node ${node.parentNode} not found`);
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
|
||||
for (const [id, node] of nodeLookup) {
|
||||
const parentId = node.parentId;
|
||||
|
||||
if (parentId && !nodeLookup.has(parentId)) {
|
||||
throw new Error(`Parent node ${parentId} not found`);
|
||||
}
|
||||
|
||||
if (node.parentNode || parentNodes?.[node.id]) {
|
||||
const parentNode = node.parentNode ? nodeLookup.get(node.parentNode) : null;
|
||||
if (parentId || node.internals.isParent || parentNodeIds?.has(id)) {
|
||||
const parentNode = parentId ? nodeLookup.get(parentId) : null;
|
||||
const { x, y, z } = calculateXYZPosition(
|
||||
node,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
{
|
||||
...node.position,
|
||||
z: node[internalsSymbol]?.z ?? 0,
|
||||
z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0),
|
||||
},
|
||||
parentNode?.origin || nodeOrigin
|
||||
parentNode?.origin || options.nodeOrigin
|
||||
);
|
||||
|
||||
const positionChanged = x !== node.computed?.positionAbsolute?.x || y !== node.computed?.positionAbsolute?.y;
|
||||
node.computed!.positionAbsolute = positionChanged
|
||||
? {
|
||||
x,
|
||||
y,
|
||||
}
|
||||
: node.computed?.positionAbsolute;
|
||||
const currPosition = node.internals.positionAbsolute;
|
||||
const positionChanged = x !== currPosition.x || y !== currPosition.y;
|
||||
|
||||
node[internalsSymbol]!.z = z;
|
||||
node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition;
|
||||
node.internals.z = z;
|
||||
|
||||
if (parentNodes?.[node.id]) {
|
||||
node[internalsSymbol]!.isParent = true;
|
||||
if (parentNodeIds !== undefined) {
|
||||
node.internals.isParent = !!parentNodeIds?.has(id);
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
nodeLookup.set(id, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||
nodeOrigin?: NodeOrigin;
|
||||
elevateNodesOnSelect?: boolean;
|
||||
defaults?: Partial<NodeType>;
|
||||
checkEquality?: boolean;
|
||||
};
|
||||
|
||||
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
|
||||
export function adoptUserNodes<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
options: UpdateNodesOptions<NodeType> = {
|
||||
nodeOrigin: [0, 0] as NodeOrigin,
|
||||
elevateNodesOnSelect: true,
|
||||
defaults: {},
|
||||
checkEquality: true,
|
||||
}
|
||||
): NodeType[] {
|
||||
) {
|
||||
const tmpLookup = new Map(nodeLookup);
|
||||
nodeLookup.clear();
|
||||
const parentNodes: ParentNodes = {};
|
||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
||||
const parentNodeIds = new Set<string>();
|
||||
|
||||
const nextNodes = nodes.map((n) => {
|
||||
const currentStoreNode = tmpLookup.get(n.id);
|
||||
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
|
||||
nodeLookup.set(n.id, currentStoreNode);
|
||||
return currentStoreNode;
|
||||
nodes.forEach((userNode) => {
|
||||
const currentStoreNode = tmpLookup.get(userNode.id);
|
||||
|
||||
if (userNode.parentId) {
|
||||
parentNodeIds.add(userNode.parentId);
|
||||
}
|
||||
|
||||
const node: NodeType = {
|
||||
...options.defaults,
|
||||
...n,
|
||||
computed: {
|
||||
positionAbsolute: n.position,
|
||||
width: n.computed?.width,
|
||||
height: n.computed?.height,
|
||||
},
|
||||
};
|
||||
const z = (isNumeric(n.zIndex) ? n.zIndex : 0) + (n.selected ? selectedNodeZ : 0);
|
||||
const currInternals = n?.[internalsSymbol] || currentStoreNode?.[internalsSymbol];
|
||||
|
||||
if (node.parentNode) {
|
||||
parentNodes[node.parentNode] = true;
|
||||
if (options.checkEquality && userNode === currentStoreNode?.internals.userNode) {
|
||||
nodeLookup.set(userNode.id, currentStoreNode);
|
||||
} else {
|
||||
nodeLookup.set(userNode.id, {
|
||||
...options.defaults,
|
||||
...userNode,
|
||||
measured: {
|
||||
width: userNode.measured?.width,
|
||||
height: userNode.measured?.height,
|
||||
},
|
||||
internals: {
|
||||
positionAbsolute: userNode.position,
|
||||
handleBounds: currentStoreNode?.internals.handleBounds,
|
||||
z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0),
|
||||
userNode,
|
||||
isParent: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Object.defineProperty(node, internalsSymbol, {
|
||||
enumerable: false,
|
||||
value: {
|
||||
handleBounds: currInternals?.handleBounds,
|
||||
z,
|
||||
userProvidedNode: n,
|
||||
},
|
||||
});
|
||||
|
||||
nodeLookup.set(node.id, node);
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
const nodesWithPositions = updateAbsolutePositions(nextNodes, nodeLookup, options.nodeOrigin, parentNodes);
|
||||
|
||||
return nodesWithPositions;
|
||||
if (parentNodeIds.size > 0) {
|
||||
updateAbsolutePositions(nodeLookup, options, parentNodeIds);
|
||||
}
|
||||
}
|
||||
|
||||
function calculateXYZPosition<NodeType extends NodeBase>(
|
||||
node: NodeType,
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
result: XYZPosition,
|
||||
nodeOrigin: NodeOrigin
|
||||
nodeOrigin: NodeOrigin = [0, 0]
|
||||
): XYZPosition {
|
||||
if (!node.parentNode) {
|
||||
if (!node.parentId) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const parentNode = nodeLookup.get(node.parentNode)!;
|
||||
const parentNode = nodeLookup.get(node.parentId)!;
|
||||
const { position: parentNodePosition } = getNodePositionWithOrigin(parentNode, parentNode?.origin || nodeOrigin);
|
||||
|
||||
return calculateXYZPosition(
|
||||
parentNode,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
{
|
||||
x: (result.x ?? 0) + parentNodePosition.x,
|
||||
y: (result.y ?? 0) + parentNodePosition.y,
|
||||
z: (parentNode[internalsSymbol]?.z ?? 0) > (result.z ?? 0) ? parentNode[internalsSymbol]?.z ?? 0 : result.z ?? 0,
|
||||
z: (parentNode.internals.z ?? 0) > (result.z ?? 0) ? parentNode.internals.z ?? 0 : result.z ?? 0,
|
||||
},
|
||||
parentNode.origin || nodeOrigin
|
||||
);
|
||||
}
|
||||
|
||||
export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
updates: Map<string, NodeDimensionUpdate>,
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
domNode: HTMLElement | null,
|
||||
nodeOrigin?: NodeOrigin,
|
||||
onUpdate?: (id: string, dimensions: Dimensions) => void
|
||||
): NodeType[] | null {
|
||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||
export function handleParentExpand(
|
||||
nodes: InternalNodeBase[],
|
||||
nodeLookup: NodeLookup
|
||||
): (NodeDimensionChange | NodePositionChange)[] {
|
||||
const changes: (NodeDimensionChange | NodePositionChange)[] = [];
|
||||
const chilNodeRects = new Map<string, Rect>();
|
||||
|
||||
if (!viewportNode) {
|
||||
return null;
|
||||
nodes.forEach((node) => {
|
||||
const parentId = node.parentId;
|
||||
if (node.expandParent && parentId) {
|
||||
const parentNode = nodeLookup.get(parentId);
|
||||
|
||||
if (parentNode) {
|
||||
const parentRect = chilNodeRects.get(parentId) || nodeToRect(parentNode, node.origin);
|
||||
const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin));
|
||||
chilNodeRects.set(parentId, expandedRect);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (chilNodeRects.size > 0) {
|
||||
chilNodeRects.forEach((rect, id) => {
|
||||
const origParent = nodeLookup.get(id)!;
|
||||
const { position } = getNodePositionWithOrigin(origParent, origParent.origin);
|
||||
const dimensions = getNodeDimensions(origParent);
|
||||
|
||||
if (rect.x < position.x || rect.y < position.y) {
|
||||
const xChange = Math.round(Math.abs(position.x - rect.x));
|
||||
const yChange = Math.round(Math.abs(position.y - rect.y));
|
||||
|
||||
changes.push({
|
||||
id,
|
||||
type: 'position',
|
||||
position: {
|
||||
x: position.x - xChange,
|
||||
y: position.y - yChange,
|
||||
},
|
||||
});
|
||||
|
||||
changes.push({
|
||||
id,
|
||||
type: 'dimensions',
|
||||
resizing: true,
|
||||
dimensions: {
|
||||
width: dimensions.width + xChange,
|
||||
height: dimensions.height + yChange,
|
||||
},
|
||||
});
|
||||
|
||||
// @todo we need to reset child node positions if < 0
|
||||
} else if (dimensions.width < rect.width || dimensions.height < rect.height) {
|
||||
changes.push({
|
||||
id,
|
||||
type: 'dimensions',
|
||||
resizing: true,
|
||||
dimensions: {
|
||||
width: Math.max(dimensions.width, rect.width),
|
||||
height: Math.max(dimensions.height, rect.height),
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
||||
updates: Map<string, InternalNodeUpdate>,
|
||||
nodeLookup: Map<string, NodeType>,
|
||||
domNode: HTMLElement | null,
|
||||
nodeOrigin?: NodeOrigin
|
||||
): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } {
|
||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||
let updatedInternals = false;
|
||||
|
||||
if (!viewportNode) {
|
||||
return { changes: [], updatedInternals };
|
||||
}
|
||||
|
||||
const changes: (NodeDimensionChange | NodePositionChange)[] = [];
|
||||
const style = window.getComputedStyle(viewportNode);
|
||||
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
|
||||
// in this array we collect nodes, that might trigger changes (like expanding parent)
|
||||
const triggerChangeNodes: NodeType[] = [];
|
||||
|
||||
const nextNodes = nodes.map((node) => {
|
||||
const update = updates.get(node.id);
|
||||
updates.forEach((update) => {
|
||||
const node = nodeLookup.get(update.id);
|
||||
|
||||
if (update) {
|
||||
if (node?.hidden) {
|
||||
nodeLookup.set(node.id, {
|
||||
...node,
|
||||
internals: {
|
||||
...node.internals,
|
||||
handleBounds: undefined,
|
||||
},
|
||||
});
|
||||
updatedInternals = true;
|
||||
} else if (node) {
|
||||
const dimensions = getDimensions(update.nodeElement);
|
||||
const dimensionChanged = node.measured.width !== dimensions.width || node.measured.height !== dimensions.height;
|
||||
const doUpdate = !!(
|
||||
dimensions.width &&
|
||||
dimensions.height &&
|
||||
(node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate)
|
||||
(dimensionChanged || !node.internals.handleBounds || update.force)
|
||||
);
|
||||
|
||||
if (doUpdate) {
|
||||
onUpdate?.(node.id, dimensions);
|
||||
|
||||
const newNode = {
|
||||
...node,
|
||||
computed: {
|
||||
...node.computed,
|
||||
...dimensions,
|
||||
},
|
||||
[internalsSymbol]: {
|
||||
...node[internalsSymbol],
|
||||
measured: dimensions,
|
||||
internals: {
|
||||
...node.internals,
|
||||
handleBounds: {
|
||||
source: getHandleBounds('.source', update.nodeElement, zoom, node.origin || nodeOrigin),
|
||||
target: getHandleBounds('.target', update.nodeElement, zoom, node.origin || nodeOrigin),
|
||||
@@ -198,15 +266,29 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
|
||||
};
|
||||
|
||||
nodeLookup.set(node.id, newNode);
|
||||
updatedInternals = true;
|
||||
|
||||
return newNode;
|
||||
if (dimensionChanged) {
|
||||
changes.push({
|
||||
id: newNode.id,
|
||||
type: 'dimensions',
|
||||
dimensions,
|
||||
});
|
||||
|
||||
if (newNode.expandParent) {
|
||||
triggerChangeNodes.push(newNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
|
||||
return nextNodes;
|
||||
if (triggerChangeNodes.length > 0) {
|
||||
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
|
||||
changes.push(...parentExpandChanges);
|
||||
}
|
||||
|
||||
return { changes, updatedInternals };
|
||||
}
|
||||
|
||||
export function panBy({
|
||||
|
||||
@@ -26,13 +26,14 @@ import type {
|
||||
OnSelectionDrag,
|
||||
UpdateNodePositions,
|
||||
Box,
|
||||
InternalNodeBase,
|
||||
} from '../types';
|
||||
|
||||
export type OnDrag = (event: MouseEvent, dragItems: NodeDragItem[], node: NodeBase, nodes: NodeBase[]) => void;
|
||||
|
||||
type StoreItems<OnNodeDrag> = {
|
||||
nodes: NodeBase[];
|
||||
nodeLookup: Map<string, NodeBase>;
|
||||
nodeLookup: Map<string, InternalNodeBase>;
|
||||
edges: EdgeBase[];
|
||||
nodeExtent: CoordinateExtent;
|
||||
snapGrid: SnapGrid;
|
||||
@@ -130,19 +131,23 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
|
||||
// if there is selection with multiple nodes and a node extent is set, we need to adjust the node extent for each node
|
||||
// based on its position so that the node stays at it's position relative to the selection.
|
||||
const adjustedNodeExtent: CoordinateExtent = [
|
||||
let adjustedNodeExtent: CoordinateExtent = [
|
||||
[nodeExtent[0][0], nodeExtent[0][1]],
|
||||
[nodeExtent[1][0], nodeExtent[1][1]],
|
||||
];
|
||||
|
||||
if (dragItems.length > 1 && nodeExtent && !n.extent) {
|
||||
adjustedNodeExtent[0][0] = n.computed.positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
||||
adjustedNodeExtent[1][0] =
|
||||
n.computed.positionAbsolute.x + (n.computed?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
|
||||
const { positionAbsolute } = n.internals;
|
||||
const x1 = positionAbsolute.x - nodesBox.x + nodeExtent[0][0];
|
||||
const x2 = positionAbsolute.x + (n.measured?.width ?? 0) - nodesBox.x2 + nodeExtent[1][0];
|
||||
|
||||
adjustedNodeExtent[0][1] = n.computed.positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
||||
adjustedNodeExtent[1][1] =
|
||||
n.computed.positionAbsolute.y + (n.computed?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
|
||||
const y1 = positionAbsolute.y - nodesBox.y + nodeExtent[0][1];
|
||||
const y2 = positionAbsolute.y + (n.measured?.height ?? 0) - nodesBox.y2 + nodeExtent[1][1];
|
||||
|
||||
adjustedNodeExtent = [
|
||||
[x1, y1],
|
||||
[x2, y2],
|
||||
];
|
||||
}
|
||||
|
||||
const { position, positionAbsolute } = calculateNodePosition({
|
||||
@@ -158,7 +163,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
hasChange = hasChange || n.position.x !== position.x || n.position.y !== position.y;
|
||||
|
||||
n.position = position;
|
||||
n.computed.positionAbsolute = positionAbsolute;
|
||||
n.internals.positionAbsolute = positionAbsolute;
|
||||
|
||||
return n;
|
||||
});
|
||||
@@ -208,7 +213,6 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
|
||||
function startDrag(event: UseDragEvent) {
|
||||
const {
|
||||
nodes,
|
||||
nodeLookup,
|
||||
multiSelectionActive,
|
||||
nodesDraggable,
|
||||
@@ -236,7 +240,7 @@ export function XYDrag<OnNodeDrag extends (e: any, nodes: any, node: any) => voi
|
||||
|
||||
const pointerPos = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
lastPos = pointerPos;
|
||||
dragItems = getDragItems(nodes, nodesDraggable, pointerPos, nodeId);
|
||||
dragItems = getDragItems(nodeLookup, nodesDraggable, pointerPos, nodeId);
|
||||
|
||||
if (dragItems.length > 0 && (onDragStart || onNodeDragStart || (!nodeId && onSelectionDragStart))) {
|
||||
const [currentNode, currentNodes] = getEventHandlerParams({
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { type NodeDragItem, type XYPosition, NodeBase } from '../types';
|
||||
import { type NodeDragItem, type XYPosition, InternalNodeBase, NodeBase, NodeLookup } from '../types';
|
||||
|
||||
export function wrapSelectionDragFunc(selectionFunc?: (event: MouseEvent, nodes: NodeBase[]) => void) {
|
||||
return (event: MouseEvent, _: NodeBase, nodes: NodeBase[]) => selectionFunc?.(event, nodes);
|
||||
}
|
||||
|
||||
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodes: NodeType[]): boolean {
|
||||
if (!node.parentNode) {
|
||||
export function isParentSelected<NodeType extends NodeBase>(node: NodeType, nodeLookup: NodeLookup): boolean {
|
||||
if (!node.parentId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentNode = nodes.find((node) => node.id === node.parentNode);
|
||||
const parentNode = nodeLookup.get(node.parentId);
|
||||
|
||||
if (!parentNode) {
|
||||
return false;
|
||||
@@ -19,7 +19,7 @@ export function isParentSelected<NodeType extends NodeBase>(node: NodeType, node
|
||||
return true;
|
||||
}
|
||||
|
||||
return isParentSelected(parentNode, nodes);
|
||||
return isParentSelected(parentNode, nodeLookup);
|
||||
}
|
||||
|
||||
export function hasSelector(target: Element, selector: string, domNode: Element): boolean {
|
||||
@@ -36,39 +36,43 @@ export function hasSelector(target: Element, selector: string, domNode: Element)
|
||||
|
||||
// looks for all selected nodes and created a NodeDragItem for each of them
|
||||
export function getDragItems<NodeType extends NodeBase>(
|
||||
nodes: NodeType[],
|
||||
nodeLookup: Map<string, InternalNodeBase<NodeType>>,
|
||||
nodesDraggable: boolean,
|
||||
mousePos: XYPosition,
|
||||
nodeId?: string
|
||||
): NodeDragItem[] {
|
||||
return nodes
|
||||
.filter(
|
||||
(n) =>
|
||||
(n.selected || n.id === nodeId) &&
|
||||
(!n.parentNode || !isParentSelected(n, nodes)) &&
|
||||
(n.draggable || (nodesDraggable && typeof n.draggable === 'undefined'))
|
||||
)
|
||||
.map((n) => ({
|
||||
id: n.id,
|
||||
position: n.position || { x: 0, y: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - (n.computed?.positionAbsolute?.x ?? 0),
|
||||
y: mousePos.y - (n.computed?.positionAbsolute?.y ?? 0),
|
||||
},
|
||||
delta: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
extent: n.extent,
|
||||
parentNode: n.parentNode,
|
||||
origin: n.origin,
|
||||
expandParent: n.expandParent,
|
||||
computed: {
|
||||
positionAbsolute: n.computed?.positionAbsolute || { x: 0, y: 0 },
|
||||
width: n.computed?.width || 0,
|
||||
height: n.computed?.height || 0,
|
||||
},
|
||||
}));
|
||||
const dragItems: NodeDragItem[] = [];
|
||||
|
||||
for (const [id, node] of nodeLookup) {
|
||||
if (
|
||||
(node.selected || node.id === nodeId) &&
|
||||
(!node.parentId || !isParentSelected(node, nodeLookup)) &&
|
||||
(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'))
|
||||
) {
|
||||
const internalNode = nodeLookup.get(id)!;
|
||||
|
||||
dragItems.push({
|
||||
id: internalNode.id,
|
||||
position: internalNode.position || { x: 0, y: 0 },
|
||||
distance: {
|
||||
x: mousePos.x - internalNode.internals.positionAbsolute.x,
|
||||
y: mousePos.y - internalNode.internals.positionAbsolute.y,
|
||||
},
|
||||
extent: internalNode.extent,
|
||||
parentId: internalNode.parentId,
|
||||
origin: internalNode.origin,
|
||||
expandParent: internalNode.expandParent,
|
||||
internals: {
|
||||
positionAbsolute: internalNode.internals.positionAbsolute || { x: 0, y: 0 },
|
||||
},
|
||||
measured: {
|
||||
width: internalNode.measured.width || 0,
|
||||
height: internalNode.measured.height || 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return dragItems;
|
||||
}
|
||||
|
||||
// returns two params:
|
||||
@@ -89,9 +93,8 @@ export function getEventHandlerParams<NodeType extends NodeBase>({
|
||||
return {
|
||||
...node,
|
||||
position: n.position,
|
||||
computed: {
|
||||
...n.computed,
|
||||
positionAbsolute: n.computed.positionAbsolute,
|
||||
measured: {
|
||||
...n.measured,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -6,13 +6,13 @@ import {
|
||||
type HandleType,
|
||||
type Connection,
|
||||
type PanBy,
|
||||
type NodeBase,
|
||||
type Transform,
|
||||
type ConnectingHandle,
|
||||
type OnConnectEnd,
|
||||
type UpdateConnection,
|
||||
type IsValidConnection,
|
||||
type ConnectionHandle,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
|
||||
import { getClosestHandle, getConnectionStatus, getHandleLookup, getHandleType } from './utils';
|
||||
@@ -25,7 +25,7 @@ export type OnPointerDownParams = {
|
||||
handleId: string | null;
|
||||
nodeId: string;
|
||||
isTarget: boolean;
|
||||
nodes: NodeBase[];
|
||||
nodeLookup: NodeLookup;
|
||||
lib: string;
|
||||
flowId: string | null;
|
||||
edgeUpdaterType?: HandleType;
|
||||
@@ -79,7 +79,7 @@ function onPointerDown(
|
||||
edgeUpdaterType,
|
||||
isTarget,
|
||||
domNode,
|
||||
nodes,
|
||||
nodeLookup,
|
||||
lib,
|
||||
autoPanOnConnect,
|
||||
flowId,
|
||||
@@ -116,7 +116,7 @@ function onPointerDown(
|
||||
let handleDomNode: Element | null = null;
|
||||
|
||||
const handleLookup = getHandleLookup({
|
||||
nodes,
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
|
||||
@@ -3,15 +3,15 @@ import {
|
||||
type HandleType,
|
||||
type NodeHandleBounds,
|
||||
type XYPosition,
|
||||
type NodeBase,
|
||||
type ConnectionHandle,
|
||||
InternalNodeBase,
|
||||
NodeLookup,
|
||||
} from '../types';
|
||||
import { internalsSymbol } from '../constants';
|
||||
|
||||
// this functions collects all handles and adds an absolute position
|
||||
// so that we can later find the closest handle to the mouse position
|
||||
export function getHandles(
|
||||
node: NodeBase,
|
||||
node: InternalNodeBase,
|
||||
handleBounds: NodeHandleBounds,
|
||||
type: HandleType,
|
||||
currentHandle: string
|
||||
@@ -22,8 +22,8 @@ export function getHandles(
|
||||
id: h.id || null,
|
||||
type,
|
||||
nodeId: node.id,
|
||||
x: (node.computed?.positionAbsolute?.x ?? 0) + h.x + h.width / 2,
|
||||
y: (node.computed?.positionAbsolute?.y ?? 0) + h.y + h.height / 2,
|
||||
x: node.internals.positionAbsolute.x + h.x + h.width / 2,
|
||||
y: node.internals.positionAbsolute.y + h.y + h.height / 2,
|
||||
});
|
||||
}
|
||||
return res;
|
||||
@@ -62,28 +62,30 @@ export function getClosestHandle(
|
||||
}
|
||||
|
||||
type GetHandleLookupParams = {
|
||||
nodes: NodeBase[];
|
||||
nodeLookup: NodeLookup;
|
||||
nodeId: string;
|
||||
handleId: string | null;
|
||||
handleType: string;
|
||||
};
|
||||
|
||||
export function getHandleLookup({ nodes, nodeId, handleId, handleType }: GetHandleLookupParams) {
|
||||
return nodes.reduce<ConnectionHandle[]>((res, node) => {
|
||||
if (node[internalsSymbol]) {
|
||||
const { handleBounds } = node[internalsSymbol];
|
||||
let sourceHandles: ConnectionHandle[] = [];
|
||||
let targetHandles: ConnectionHandle[] = [];
|
||||
export function getHandleLookup({
|
||||
nodeLookup,
|
||||
nodeId,
|
||||
handleId,
|
||||
handleType,
|
||||
}: GetHandleLookupParams): ConnectionHandle[] {
|
||||
const connectionHandles: ConnectionHandle[] = [];
|
||||
|
||||
if (handleBounds) {
|
||||
sourceHandles = getHandles(node, handleBounds, 'source', `${nodeId}-${handleId}-${handleType}`);
|
||||
targetHandles = getHandles(node, handleBounds, 'target', `${nodeId}-${handleId}-${handleType}`);
|
||||
}
|
||||
|
||||
res.push(...sourceHandles, ...targetHandles);
|
||||
for (const [, node] of nodeLookup) {
|
||||
if (node.internals.handleBounds) {
|
||||
const id = `${nodeId}-${handleId}-${handleType}`;
|
||||
const sourceHandles = getHandles(node, node.internals.handleBounds, 'source', id);
|
||||
const targetHandles = getHandles(node, node.internals.handleBounds, 'target', id);
|
||||
connectionHandles.push(...sourceHandles, ...targetHandles);
|
||||
}
|
||||
return res;
|
||||
}, []);
|
||||
}
|
||||
|
||||
return connectionHandles;
|
||||
}
|
||||
|
||||
export function getHandleType(
|
||||
|
||||
@@ -69,6 +69,7 @@ export function XYPanZoom({
|
||||
);
|
||||
|
||||
const d3ZoomHandler = d3Selection.on('wheel.zoom')!;
|
||||
const d3DblClickZoomHandler = d3Selection.on('dblclick.zoom')!;
|
||||
d3ZoomInstance.wheelDelta(wheelDelta);
|
||||
|
||||
function setTransform(transform: ZoomTransform, options?: PanZoomTransformOptions) {
|
||||
@@ -165,6 +166,15 @@ export function XYPanZoom({
|
||||
lib,
|
||||
});
|
||||
d3ZoomInstance.filter(filter);
|
||||
|
||||
// We cannot add zoomOnDoubleClick to the filter above because
|
||||
// double tapping on touch screens circumvents the filter and
|
||||
// dblclick.zoom is fired on the selection directly
|
||||
if (zoomOnDoubleClick) {
|
||||
d3Selection.on('dblclick.zoom', d3DblClickZoomHandler);
|
||||
} else {
|
||||
d3Selection.on('dblclick.zoom', null);
|
||||
}
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
|
||||
@@ -138,7 +138,10 @@ export function createPanOnScrollHandler({
|
||||
|
||||
export function createZoomOnScrollHandler({ noWheelClassName, preventScrolling, d3ZoomHandler }: ZoomOnScrollParams) {
|
||||
return function (this: Element, event: any, d: unknown) {
|
||||
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
|
||||
// we still want to enable pinch zooming even if preventScrolling is set to false
|
||||
const preventZoom = !preventScrolling && event.type === 'wheel' && !event.ctrlKey;
|
||||
|
||||
if (preventZoom || isWrappedWithClass(event, noWheelClassName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,11 +48,6 @@ export function createFilter({
|
||||
return false;
|
||||
}
|
||||
|
||||
// if zoom on double click is disabled, we prevent the double click event
|
||||
if (!zoomOnDoubleClick && event.type === 'dblclick') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the target element is inside an element with the nowheel class, we prevent zooming
|
||||
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
|
||||
return false;
|
||||
|
||||
@@ -71,15 +71,15 @@ export type XYResizerInstance = {
|
||||
function nodeToParentExtent(node: NodeBase): CoordinateExtent {
|
||||
return [
|
||||
[0, 0],
|
||||
[node.computed!.width!, node.computed!.height!],
|
||||
[node.measured!.width!, node.measured!.height!],
|
||||
];
|
||||
}
|
||||
|
||||
function nodeToChildExtent(child: NodeBase, parent: NodeBase, nodeOrigin: NodeOrigin): CoordinateExtent {
|
||||
const x = parent.position.x + child.position.x;
|
||||
const y = parent.position.y + child.position.y;
|
||||
const width = child.computed!.width! ?? 0;
|
||||
const height = child.computed!.height! ?? 0;
|
||||
const width = child.measured!.width! ?? 0;
|
||||
const height = child.measured!.height! ?? 0;
|
||||
const originOffsetX = nodeOrigin[0] * width;
|
||||
const originOffsetY = nodeOrigin[1] * height;
|
||||
|
||||
@@ -121,8 +121,8 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
prevValues = {
|
||||
width: node.computed?.width ?? 0,
|
||||
height: node.computed?.height ?? 0,
|
||||
width: node.measured?.width ?? 0,
|
||||
height: node.measured?.height ?? 0,
|
||||
x: node.position.x ?? 0,
|
||||
y: node.position.y ?? 0,
|
||||
};
|
||||
@@ -136,7 +136,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
|
||||
|
||||
parentNode = undefined;
|
||||
if (node.extent === 'parent' || node.expandParent) {
|
||||
parentNode = nodeLookup.get(node.parentNode!);
|
||||
parentNode = nodeLookup.get(node.parentId!);
|
||||
if (parentNode && node.extent === 'parent') {
|
||||
parentExtent = nodeToParentExtent(parentNode);
|
||||
}
|
||||
@@ -148,7 +148,7 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize
|
||||
childExtent = undefined;
|
||||
|
||||
for (const [childId, child] of nodeLookup) {
|
||||
if (child.parentNode === nodeId) {
|
||||
if (child.parentId === nodeId) {
|
||||
childNodes.push({
|
||||
id: childId,
|
||||
position: { ...child.position },
|
||||
|
||||
Reference in New Issue
Block a user