@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Added ability to set global nodeExtent
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
'@xyflow/react': patch
|
||||||
|
'@xyflow/svelte': patch
|
||||||
|
'@xyflow/system': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix extent on nodes not working properly
|
||||||
@@ -13,9 +13,9 @@ import {
|
|||||||
MiniMap,
|
MiniMap,
|
||||||
Background,
|
Background,
|
||||||
Panel,
|
Panel,
|
||||||
NodeOrigin,
|
|
||||||
useUpdateNodeInternals,
|
useUpdateNodeInternals,
|
||||||
ReactFlowProvider,
|
ReactFlowProvider,
|
||||||
|
CoordinateExtent,
|
||||||
} from '@xyflow/react';
|
} from '@xyflow/react';
|
||||||
|
|
||||||
import DebugNode from './DebugNode';
|
import DebugNode from './DebugNode';
|
||||||
@@ -27,11 +27,20 @@ const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
|
|||||||
|
|
||||||
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
const defaultViewport = { x: 0, y: 0, zoom: 1.5 };
|
||||||
const initialNodes: Node[] = [
|
const initialNodes: Node[] = [
|
||||||
|
{
|
||||||
|
id: 'extent',
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
width: 1000,
|
||||||
|
height: 1000,
|
||||||
|
data: { label: 'Extent' },
|
||||||
|
origin: [0, 0],
|
||||||
|
zIndex: -1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
type: 'input',
|
type: 'input',
|
||||||
data: { label: 'Node 1' },
|
data: { label: 'Node 1' },
|
||||||
position: { x: 250, y: 5 },
|
position: { x: -200, y: -500 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
origin: [0.5, 0.5],
|
origin: [0.5, 0.5],
|
||||||
},
|
},
|
||||||
@@ -40,7 +49,7 @@ const initialNodes: Node[] = [
|
|||||||
data: { label: 'Node 4' },
|
data: { label: 'Node 4' },
|
||||||
position: { x: 100, y: 200 },
|
position: { x: 100, y: 200 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
origin: [0.5, 0.5],
|
origin: [0, 0],
|
||||||
style: {
|
style: {
|
||||||
backgroundColor: 'rgba(255,50, 50, 0.5)',
|
backgroundColor: 'rgba(255,50, 50, 0.5)',
|
||||||
width: 500,
|
width: 500,
|
||||||
@@ -50,14 +59,13 @@ const initialNodes: Node[] = [
|
|||||||
{
|
{
|
||||||
id: '4a',
|
id: '4a',
|
||||||
data: { label: 'Node 4a' },
|
data: { label: 'Node 4a' },
|
||||||
position: { x: 15, y: 15 },
|
position: { x: -15, y: -15 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentId: '4',
|
parentId: '4',
|
||||||
origin: [0.5, 0.5],
|
origin: [0, 0],
|
||||||
|
|
||||||
extent: [
|
extent: [
|
||||||
[0, 0],
|
[0, 0],
|
||||||
[100, 100],
|
[300, 100],
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -98,9 +106,13 @@ const initialNodes: Node[] = [
|
|||||||
{
|
{
|
||||||
id: '5a',
|
id: '5a',
|
||||||
data: { label: 'Node 5a' },
|
data: { label: 'Node 5a' },
|
||||||
position: { x: 0, y: 0 },
|
position: { x: -100, y: -100 },
|
||||||
className: 'light',
|
className: 'light',
|
||||||
parentId: '5',
|
parentId: '5',
|
||||||
|
// extent: [
|
||||||
|
// [0, 0],
|
||||||
|
// [300, 300],
|
||||||
|
// ],
|
||||||
extent: 'parent',
|
extent: 'parent',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -151,6 +163,11 @@ const nodeTypes = {
|
|||||||
default: DebugNode,
|
default: DebugNode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const nodeExtent: CoordinateExtent = [
|
||||||
|
[0, 0],
|
||||||
|
[1000, 1000],
|
||||||
|
];
|
||||||
|
|
||||||
const Subflow = () => {
|
const Subflow = () => {
|
||||||
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
@@ -220,6 +237,7 @@ const Subflow = () => {
|
|||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
fitView
|
fitView
|
||||||
nodeOrigin={[0, 0]}
|
nodeOrigin={[0, 0]}
|
||||||
|
nodeExtent={nodeExtent}
|
||||||
>
|
>
|
||||||
<MiniMap />
|
<MiniMap />
|
||||||
<Controls />
|
<Controls />
|
||||||
@@ -239,7 +257,7 @@ const Subflow = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider nodeExtent={nodeExtent}>
|
||||||
<Subflow />
|
<Subflow />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { type MouseEvent, type KeyboardEvent } from 'react';
|
|||||||
import cc from 'classcat';
|
import cc from 'classcat';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import {
|
import {
|
||||||
clampPosition,
|
|
||||||
elementSelectionKeys,
|
elementSelectionKeys,
|
||||||
errorMessages,
|
errorMessages,
|
||||||
getNodeDimensions,
|
getNodeDimensions,
|
||||||
@@ -87,10 +86,6 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
|
|
||||||
const nodeDimensions = getNodeDimensions(node);
|
const nodeDimensions = getNodeDimensions(node);
|
||||||
const inlineDimensions = getNodeInlineStyleDimensions(node);
|
const inlineDimensions = getNodeInlineStyleDimensions(node);
|
||||||
// TODO: clamping should happen earlier
|
|
||||||
const clampedPosition = nodeExtent
|
|
||||||
? clampPosition(internals.positionAbsolute, nodeExtent)
|
|
||||||
: internals.positionAbsolute;
|
|
||||||
|
|
||||||
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
|
||||||
|
|
||||||
@@ -146,7 +141,7 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
store.setState({
|
store.setState({
|
||||||
ariaLiveMessage: `Moved selected node ${event.key
|
ariaLiveMessage: `Moved selected node ${event.key
|
||||||
.replace('Arrow', '')
|
.replace('Arrow', '')
|
||||||
.toLowerCase()}. New position, x: ${~~clampedPosition.x}, y: ${~~clampedPosition.y}`,
|
.toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
moveSelectedNodes({
|
moveSelectedNodes({
|
||||||
@@ -177,7 +172,7 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
ref={nodeRef}
|
ref={nodeRef}
|
||||||
style={{
|
style={{
|
||||||
zIndex: internals.z,
|
zIndex: internals.z,
|
||||||
transform: `translate(${clampedPosition.x}px,${clampedPosition.y}px)`,
|
transform: `translate(${internals.positionAbsolute.x}px,${internals.positionAbsolute.y}px)`,
|
||||||
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
pointerEvents: hasPointerEvents ? 'all' : 'none',
|
||||||
visibility: hasDimensions ? 'visible' : 'hidden',
|
visibility: hasDimensions ? 'visible' : 'hidden',
|
||||||
...node.style,
|
...node.style,
|
||||||
@@ -202,8 +197,8 @@ export function NodeWrapper<NodeType extends Node>({
|
|||||||
id={id}
|
id={id}
|
||||||
data={node.data}
|
data={node.data}
|
||||||
type={nodeType}
|
type={nodeType}
|
||||||
positionAbsoluteX={clampedPosition.x}
|
positionAbsoluteX={internals.positionAbsolute.x}
|
||||||
positionAbsoluteY={clampedPosition.y}
|
positionAbsoluteY={internals.positionAbsolute.y}
|
||||||
selected={node.selected}
|
selected={node.selected}
|
||||||
selectable={isSelectable}
|
selectable={isSelectable}
|
||||||
draggable={isDraggable}
|
draggable={isDraggable}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Provider } from '../../contexts/StoreContext';
|
|||||||
import { createStore } from '../../store';
|
import { createStore } from '../../store';
|
||||||
import { BatchProvider } from '../BatchProvider';
|
import { BatchProvider } from '../BatchProvider';
|
||||||
import type { Node, Edge } from '../../types';
|
import type { Node, Edge } from '../../types';
|
||||||
import { NodeOrigin } from '@xyflow/system';
|
import { CoordinateExtent, NodeOrigin } from '@xyflow/system';
|
||||||
|
|
||||||
export type ReactFlowProviderProps = {
|
export type ReactFlowProviderProps = {
|
||||||
initialNodes?: Node[];
|
initialNodes?: Node[];
|
||||||
@@ -15,6 +15,7 @@ export type ReactFlowProviderProps = {
|
|||||||
initialHeight?: number;
|
initialHeight?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,6 +28,7 @@ export function ReactFlowProvider({
|
|||||||
initialHeight: height,
|
initialHeight: height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
children,
|
children,
|
||||||
}: ReactFlowProviderProps) {
|
}: ReactFlowProviderProps) {
|
||||||
const [store] = useState(() =>
|
const [store] = useState(() =>
|
||||||
@@ -39,6 +41,7 @@ export function ReactFlowProvider({
|
|||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useContext, type ReactNode } from 'react';
|
|||||||
import StoreContext from '../../contexts/StoreContext';
|
import StoreContext from '../../contexts/StoreContext';
|
||||||
import { ReactFlowProvider } from '../../components/ReactFlowProvider';
|
import { ReactFlowProvider } from '../../components/ReactFlowProvider';
|
||||||
import type { Node, Edge } from '../../types';
|
import type { Node, Edge } from '../../types';
|
||||||
import { NodeOrigin } from '@xyflow/system';
|
import { CoordinateExtent, NodeOrigin } from '@xyflow/system';
|
||||||
|
|
||||||
export function Wrapper({
|
export function Wrapper({
|
||||||
children,
|
children,
|
||||||
@@ -15,6 +15,7 @@ export function Wrapper({
|
|||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
@@ -25,6 +26,7 @@ export function Wrapper({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
}) {
|
}) {
|
||||||
const isWrapped = useContext(StoreContext);
|
const isWrapped = useContext(StoreContext);
|
||||||
|
|
||||||
@@ -44,6 +46,7 @@ export function Wrapper({
|
|||||||
initialHeight={height}
|
initialHeight={height}
|
||||||
fitView={fitView}
|
fitView={fitView}
|
||||||
nodeOrigin={nodeOrigin}
|
nodeOrigin={nodeOrigin}
|
||||||
|
nodeExtent={nodeExtent}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
|||||||
@@ -160,7 +160,15 @@ function ReactFlow<NodeType extends Node = Node, EdgeType extends Edge = Edge>(
|
|||||||
data-testid="rf__wrapper"
|
data-testid="rf__wrapper"
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<Wrapper nodes={nodes} edges={edges} width={width} height={height} fitView={fitView} nodeOrigin={nodeOrigin}>
|
<Wrapper
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
fitView={fitView}
|
||||||
|
nodeOrigin={nodeOrigin}
|
||||||
|
nodeExtent={nodeExtent}
|
||||||
|
>
|
||||||
<GraphView<NodeType, EdgeType>
|
<GraphView<NodeType, EdgeType>
|
||||||
onInit={onInit}
|
onInit={onInit}
|
||||||
onNodeClick={onNodeClick}
|
onNodeClick={onNodeClick}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { createWithEqualityFn } from 'zustand/traditional';
|
import { createWithEqualityFn } from 'zustand/traditional';
|
||||||
import {
|
import {
|
||||||
clampPosition,
|
|
||||||
getFitViewNodes,
|
getFitViewNodes,
|
||||||
fitView as fitViewSystem,
|
fitView as fitViewSystem,
|
||||||
adoptUserNodes,
|
adoptUserNodes,
|
||||||
@@ -15,6 +14,7 @@ import {
|
|||||||
ParentExpandChild,
|
ParentExpandChild,
|
||||||
initialConnection,
|
initialConnection,
|
||||||
NodeOrigin,
|
NodeOrigin,
|
||||||
|
CoordinateExtent,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
|
||||||
@@ -30,6 +30,7 @@ const createStore = ({
|
|||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
}: {
|
}: {
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
edges?: Edge[];
|
edges?: Edge[];
|
||||||
@@ -39,10 +40,11 @@ const createStore = ({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
}) =>
|
}) =>
|
||||||
createWithEqualityFn<ReactFlowState>(
|
createWithEqualityFn<ReactFlowState>(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, defaultNodes, defaultEdges }),
|
...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }),
|
||||||
setNodes: (nodes: Node[]) => {
|
setNodes: (nodes: Node[]) => {
|
||||||
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||||
// setNodes() is called exclusively in response to user actions:
|
// setNodes() is called exclusively in response to user actions:
|
||||||
@@ -51,7 +53,12 @@ const createStore = ({
|
|||||||
//
|
//
|
||||||
// When this happens, we take the note objects passed by the user and extend them with fields
|
// When this happens, we take the note objects passed by the user and extend them with fields
|
||||||
// relevant for internal React Flow operations.
|
// relevant for internal React Flow operations.
|
||||||
adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, elevateNodesOnSelect, checkEquality: true });
|
adoptUserNodes(nodes, nodeLookup, parentLookup, {
|
||||||
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
|
elevateNodesOnSelect,
|
||||||
|
checkEquality: true,
|
||||||
|
});
|
||||||
|
|
||||||
set({ nodes });
|
set({ nodes });
|
||||||
},
|
},
|
||||||
@@ -87,6 +94,7 @@ const createStore = ({
|
|||||||
fitViewOnInitOptions,
|
fitViewOnInitOptions,
|
||||||
domNode,
|
domNode,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
debug,
|
debug,
|
||||||
fitViewSync,
|
fitViewSync,
|
||||||
} = get();
|
} = get();
|
||||||
@@ -96,14 +104,15 @@ const createStore = ({
|
|||||||
nodeLookup,
|
nodeLookup,
|
||||||
parentLookup,
|
parentLookup,
|
||||||
domNode,
|
domNode,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!updatedInternals) {
|
if (!updatedInternals) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
|
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin, nodeExtent });
|
||||||
|
|
||||||
if (params.triggerFitView) {
|
if (params.triggerFitView) {
|
||||||
// we call fitView once initially after all dimensions are set
|
// we call fitView once initially after all dimensions are set
|
||||||
@@ -277,24 +286,26 @@ const createStore = ({
|
|||||||
triggerNodeChanges(nodeChanges);
|
triggerNodeChanges(nodeChanges);
|
||||||
triggerEdgeChanges(edgeChanges);
|
triggerEdgeChanges(edgeChanges);
|
||||||
},
|
},
|
||||||
setNodeExtent: (nodeExtent) => {
|
setNodeExtent: (nextNodeExtent) => {
|
||||||
const { nodeLookup } = get();
|
const { nodes, nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect, nodeExtent } = get();
|
||||||
|
|
||||||
for (const [, node] of nodeLookup) {
|
if (
|
||||||
const positionAbsolute = clampPosition(node.internals.positionAbsolute, nodeExtent);
|
nextNodeExtent[0][0] === nodeExtent[0][0] &&
|
||||||
|
nextNodeExtent[0][1] === nodeExtent[0][1] &&
|
||||||
nodeLookup.set(node.id, {
|
nextNodeExtent[1][0] === nodeExtent[1][0] &&
|
||||||
...node,
|
nextNodeExtent[1][1] === nodeExtent[1][1]
|
||||||
internals: {
|
) {
|
||||||
...node.internals,
|
return;
|
||||||
positionAbsolute,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set({
|
adoptUserNodes(nodes, nodeLookup, parentLookup, {
|
||||||
nodeExtent,
|
nodeOrigin,
|
||||||
|
nodeExtent: nextNodeExtent,
|
||||||
|
elevateNodesOnSelect,
|
||||||
|
checkEquality: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
set({ nodeExtent: nextNodeExtent });
|
||||||
},
|
},
|
||||||
panBy: (delta): Promise<boolean> => {
|
panBy: (delta): Promise<boolean> => {
|
||||||
const { transform, width, height, panZoom, translateExtent } = get();
|
const { transform, width, height, panZoom, translateExtent } = get();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getInternalNodesBounds,
|
getInternalNodesBounds,
|
||||||
NodeOrigin,
|
NodeOrigin,
|
||||||
initialConnection,
|
initialConnection,
|
||||||
|
CoordinateExtent,
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
|
import type { Edge, InternalNode, Node, ReactFlowStore } from '../types';
|
||||||
@@ -22,6 +23,7 @@ const getInitialState = ({
|
|||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
}: {
|
}: {
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
edges?: Edge[];
|
edges?: Edge[];
|
||||||
@@ -31,18 +33,22 @@ const getInitialState = ({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
} = {}): ReactFlowStore => {
|
} = {}): ReactFlowStore => {
|
||||||
const nodeLookup = new Map<string, InternalNode>();
|
const nodeLookup = new Map<string, InternalNode>();
|
||||||
const parentLookup = new Map();
|
const parentLookup = new Map();
|
||||||
const connectionLookup = new Map();
|
const connectionLookup = new Map();
|
||||||
const edgeLookup = new Map();
|
const edgeLookup = new Map();
|
||||||
|
|
||||||
const storeEdges = defaultEdges ?? edges ?? [];
|
const storeEdges = defaultEdges ?? edges ?? [];
|
||||||
const storeNodes = defaultNodes ?? nodes ?? [];
|
const storeNodes = defaultNodes ?? nodes ?? [];
|
||||||
const storeNodeOrigin = nodeOrigin ?? [0, 0];
|
const storeNodeOrigin = nodeOrigin ?? [0, 0];
|
||||||
|
const storeNodeExtent = nodeExtent ?? infiniteExtent;
|
||||||
|
|
||||||
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
|
updateConnectionLookup(connectionLookup, edgeLookup, storeEdges);
|
||||||
adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
|
adoptUserNodes(storeNodes, nodeLookup, parentLookup, {
|
||||||
nodeOrigin: storeNodeOrigin,
|
nodeOrigin: storeNodeOrigin,
|
||||||
|
nodeExtent: storeNodeExtent,
|
||||||
elevateNodesOnSelect: false,
|
elevateNodesOnSelect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -76,7 +82,7 @@ const getInitialState = ({
|
|||||||
minZoom: 0.5,
|
minZoom: 0.5,
|
||||||
maxZoom: 2,
|
maxZoom: 2,
|
||||||
translateExtent: infiniteExtent,
|
translateExtent: infiniteExtent,
|
||||||
nodeExtent: infiniteExtent,
|
nodeExtent: storeNodeExtent,
|
||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
userSelectionActive: false,
|
userSelectionActive: false,
|
||||||
userSelectionRect: null,
|
userSelectionRect: null,
|
||||||
|
|||||||
@@ -53,6 +53,7 @@
|
|||||||
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
export let onMoveEnd: $$Props['onMoveEnd'] = undefined;
|
||||||
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
export let isValidConnection: $$Props['isValidConnection'] = undefined;
|
||||||
export let translateExtent: $$Props['translateExtent'] = undefined;
|
export let translateExtent: $$Props['translateExtent'] = undefined;
|
||||||
|
export let nodeExtent: $$Props['nodeExtent'] = undefined;
|
||||||
export let onlyRenderVisibleElements: $$Props['onlyRenderVisibleElements'] = undefined;
|
export let onlyRenderVisibleElements: $$Props['onlyRenderVisibleElements'] = undefined;
|
||||||
export let panOnScrollMode: $$Props['panOnScrollMode'] = PanOnScrollMode.Free;
|
export let panOnScrollMode: $$Props['panOnScrollMode'] = PanOnScrollMode.Free;
|
||||||
export let preventScrolling: $$Props['preventScrolling'] = true;
|
export let preventScrolling: $$Props['preventScrolling'] = true;
|
||||||
@@ -102,7 +103,8 @@
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ export type SvelteFlowProps = DOMAttributes<HTMLDivElement> & {
|
|||||||
* @example [[-1000, -10000], [1000, 1000]]
|
* @example [[-1000, -10000], [1000, 1000]]
|
||||||
*/
|
*/
|
||||||
translateExtent?: CoordinateExtent;
|
translateExtent?: CoordinateExtent;
|
||||||
|
/** By default the nodes can be placed anywhere. You can use this prop to set a boundary.
|
||||||
|
*
|
||||||
|
* The first pair of coordinates is the top left boundary and the second pair is the bottom right.
|
||||||
|
* @example [[-1000, -10000], [1000, 1000]]
|
||||||
|
*/
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
|
/** Disabling this prop will allow the user to scroll the page even when their pointer is over the flow.
|
||||||
* @default true
|
* @default true
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export function createStore({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fitView: fitViewOnCreate,
|
fitView: fitViewOnCreate,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
}: {
|
}: {
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
edges?: Edge[];
|
edges?: Edge[];
|
||||||
@@ -45,6 +46,7 @@ export function createStore({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
}): SvelteFlowStore {
|
}): SvelteFlowStore {
|
||||||
const store = getInitialStore({
|
const store = getInitialStore({
|
||||||
nodes,
|
nodes,
|
||||||
@@ -52,7 +54,8 @@ export function createStore({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fitView: fitViewOnCreate,
|
fitView: fitViewOnCreate,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
});
|
});
|
||||||
|
|
||||||
function setNodeTypes(nodeTypes: NodeTypes) {
|
function setNodeTypes(nodeTypes: NodeTypes) {
|
||||||
@@ -482,7 +485,8 @@ export function createStoreContext({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
}: {
|
}: {
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
edges?: Edge[];
|
edges?: Edge[];
|
||||||
@@ -490,8 +494,9 @@ export function createStoreContext({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
}) {
|
}) {
|
||||||
const store = createStore({ nodes, edges, width, height, fitView, nodeOrigin });
|
const store = createStore({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent });
|
||||||
|
|
||||||
setContext(key, {
|
setContext(key, {
|
||||||
getStore: () => store
|
getStore: () => store
|
||||||
|
|||||||
@@ -75,7 +75,8 @@ export const getInitialStore = ({
|
|||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
fitView,
|
fitView,
|
||||||
nodeOrigin
|
nodeOrigin,
|
||||||
|
nodeExtent
|
||||||
}: {
|
}: {
|
||||||
nodes?: Node[];
|
nodes?: Node[];
|
||||||
edges?: Edge[];
|
edges?: Edge[];
|
||||||
@@ -83,17 +84,23 @@ export const getInitialStore = ({
|
|||||||
height?: number;
|
height?: number;
|
||||||
fitView?: boolean;
|
fitView?: boolean;
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
}) => {
|
}) => {
|
||||||
const nodeLookup: NodeLookup = new Map();
|
const nodeLookup: NodeLookup = new Map();
|
||||||
const parentLookup = new Map();
|
const parentLookup = new Map();
|
||||||
|
const connectionLookup = new Map();
|
||||||
|
const edgeLookup = new Map();
|
||||||
|
|
||||||
const storeNodeOrigin = nodeOrigin ?? [0, 0];
|
const storeNodeOrigin = nodeOrigin ?? [0, 0];
|
||||||
|
const storeNodeExtent = nodeExtent ?? infiniteExtent;
|
||||||
|
|
||||||
adoptUserNodes(nodes, nodeLookup, parentLookup, {
|
adoptUserNodes(nodes, nodeLookup, parentLookup, {
|
||||||
|
nodeExtent: storeNodeExtent,
|
||||||
nodeOrigin: storeNodeOrigin,
|
nodeOrigin: storeNodeOrigin,
|
||||||
elevateNodesOnSelect: false,
|
elevateNodesOnSelect: false,
|
||||||
checkEquality: false
|
checkEquality: false
|
||||||
});
|
});
|
||||||
const connectionLookup = new Map();
|
|
||||||
const edgeLookup = new Map();
|
|
||||||
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
updateConnectionLookup(connectionLookup, edgeLookup, edges);
|
||||||
|
|
||||||
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
let viewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||||
@@ -107,7 +114,7 @@ export const getInitialStore = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
flowId: writable<string | null>(null),
|
flowId: writable<string | null>(null),
|
||||||
nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin),
|
nodes: createNodesStore(nodes, nodeLookup, parentLookup, storeNodeOrigin, storeNodeExtent),
|
||||||
nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
|
nodeLookup: readable<NodeLookup<InternalNode>>(nodeLookup),
|
||||||
parentLookup: readable<ParentLookup<InternalNode>>(parentLookup),
|
parentLookup: readable<ParentLookup<InternalNode>>(parentLookup),
|
||||||
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
|
edgeLookup: readable<EdgeLookup<Edge>>(edgeLookup),
|
||||||
@@ -121,7 +128,7 @@ export const getInitialStore = ({
|
|||||||
maxZoom: writable<number>(2),
|
maxZoom: writable<number>(2),
|
||||||
nodeOrigin: writable<NodeOrigin>(storeNodeOrigin),
|
nodeOrigin: writable<NodeOrigin>(storeNodeOrigin),
|
||||||
nodeDragThreshold: writable<number>(1),
|
nodeDragThreshold: writable<number>(1),
|
||||||
nodeExtent: writable<CoordinateExtent>(infiniteExtent),
|
nodeExtent: writable<CoordinateExtent>(storeNodeExtent),
|
||||||
translateExtent: writable<CoordinateExtent>(infiniteExtent),
|
translateExtent: writable<CoordinateExtent>(infiniteExtent),
|
||||||
autoPanOnNodeDrag: writable<boolean>(true),
|
autoPanOnNodeDrag: writable<boolean>(true),
|
||||||
autoPanOnConnect: writable<boolean>(true),
|
autoPanOnConnect: writable<boolean>(true),
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
type EdgeLookup,
|
type EdgeLookup,
|
||||||
type NodeLookup,
|
type NodeLookup,
|
||||||
type ParentLookup,
|
type ParentLookup,
|
||||||
type NodeOrigin
|
type NodeOrigin,
|
||||||
|
infiniteExtent,
|
||||||
|
type CoordinateExtent
|
||||||
} from '@xyflow/system';
|
} from '@xyflow/system';
|
||||||
|
|
||||||
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
|
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, InternalNode, Node } from '$lib/types';
|
||||||
@@ -131,7 +133,8 @@ export const createNodesStore = (
|
|||||||
nodes: Node[],
|
nodes: Node[],
|
||||||
nodeLookup: NodeLookup<InternalNode>,
|
nodeLookup: NodeLookup<InternalNode>,
|
||||||
parentLookup: ParentLookup<InternalNode>,
|
parentLookup: ParentLookup<InternalNode>,
|
||||||
nodeOrigin: NodeOrigin = [0, 0]
|
nodeOrigin: NodeOrigin = [0, 0],
|
||||||
|
nodeExtent: CoordinateExtent = infiniteExtent
|
||||||
): {
|
): {
|
||||||
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
|
subscribe: (this: void, run: Subscriber<Node[]>) => Unsubscriber;
|
||||||
update: (this: void, updater: Updater<Node[]>) => void;
|
update: (this: void, updater: Updater<Node[]>) => void;
|
||||||
@@ -148,6 +151,7 @@ export const createNodesStore = (
|
|||||||
adoptUserNodes(nds, nodeLookup, parentLookup, {
|
adoptUserNodes(nds, nodeLookup, parentLookup, {
|
||||||
elevateNodesOnSelect,
|
elevateNodesOnSelect,
|
||||||
nodeOrigin,
|
nodeOrigin,
|
||||||
|
nodeExtent,
|
||||||
defaults,
|
defaults,
|
||||||
checkEquality: false
|
checkEquality: false
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,11 +16,33 @@ import { getNodePositionWithOrigin, isInternalNodeBase } from './graph';
|
|||||||
|
|
||||||
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max);
|
||||||
|
|
||||||
export const clampPosition = (position: XYPosition = { x: 0, y: 0 }, extent: CoordinateExtent) => ({
|
export const clampPosition = (
|
||||||
x: clamp(position.x, extent[0][0], extent[1][0]),
|
position: XYPosition = { x: 0, y: 0 },
|
||||||
y: clamp(position.y, extent[0][1], extent[1][1]),
|
extent: CoordinateExtent,
|
||||||
|
dimensions: Partial<Dimensions>
|
||||||
|
) => ({
|
||||||
|
x: clamp(position.x, extent[0][0], extent[1][0] - (dimensions?.width ?? 0)),
|
||||||
|
y: clamp(position.y, extent[0][1], extent[1][1] - (dimensions?.height ?? 0)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function clampPositionToParent<NodeType extends NodeBase>(
|
||||||
|
childPosition: XYPosition,
|
||||||
|
childDimensions: Dimensions,
|
||||||
|
parent: InternalNodeBase<NodeType>
|
||||||
|
) {
|
||||||
|
const { width: parentWidth, height: parentHeight } = getNodeDimensions(parent);
|
||||||
|
const { x: parentX, y: parentY } = parent.internals.positionAbsolute;
|
||||||
|
|
||||||
|
return clampPosition(
|
||||||
|
childPosition,
|
||||||
|
[
|
||||||
|
[parentX, parentY],
|
||||||
|
[parentX + parentWidth, parentY + parentHeight],
|
||||||
|
],
|
||||||
|
childDimensions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculates the velocity of panning when the mouse is close to the edge of the canvas
|
* Calculates the velocity of panning when the mouse is close to the edge of the canvas
|
||||||
* @internal
|
* @internal
|
||||||
|
|||||||
@@ -297,24 +297,6 @@ export async function fitView<Params extends FitViewParamsBase<NodeBase>, Option
|
|||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This function clamps the passed extend by the node's width and height.
|
|
||||||
* This is needed to prevent the node from being dragged outside of its extent.
|
|
||||||
*
|
|
||||||
* @param node
|
|
||||||
* @param extent
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function clampNodeExtent<NodeType extends NodeBase>(
|
|
||||||
node: NodeType,
|
|
||||||
extent?: CoordinateExtent | 'parent'
|
|
||||||
): CoordinateExtent | 'parent' | undefined {
|
|
||||||
if (!extent || extent === 'parent') {
|
|
||||||
return extent;
|
|
||||||
}
|
|
||||||
return [extent[0], [extent[1][0] - (node.measured?.width ?? 0), extent[1][1] - (node.measured?.height ?? 0)]];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
|
* This function calculates the next position of a node, taking into account the node's extent, parent node, and origin.
|
||||||
*
|
*
|
||||||
@@ -339,40 +321,37 @@ export function calculateNodePosition<NodeType extends NodeBase>({
|
|||||||
const node = nodeLookup.get(nodeId)!;
|
const node = nodeLookup.get(nodeId)!;
|
||||||
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
|
const parentNode = node.parentId ? nodeLookup.get(node.parentId) : undefined;
|
||||||
const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
|
const { x: parentX, y: parentY } = parentNode ? parentNode.internals.positionAbsolute : { x: 0, y: 0 };
|
||||||
const origin = node.origin ?? nodeOrigin;
|
|
||||||
|
|
||||||
let currentExtent = clampNodeExtent(node, node.extent || nodeExtent);
|
const origin = node.origin ?? nodeOrigin;
|
||||||
|
let extent = nodeExtent;
|
||||||
|
|
||||||
if (node.extent === 'parent' && !node.expandParent) {
|
if (node.extent === 'parent' && !node.expandParent) {
|
||||||
if (!parentNode) {
|
if (!parentNode) {
|
||||||
onError?.('005', errorMessages['error005']());
|
onError?.('005', errorMessages['error005']());
|
||||||
} else {
|
} else {
|
||||||
const nodeWidth = node.measured.width;
|
|
||||||
const nodeHeight = node.measured.height;
|
|
||||||
const parentWidth = parentNode.measured.width;
|
const parentWidth = parentNode.measured.width;
|
||||||
const parentHeight = parentNode.measured.height;
|
const parentHeight = parentNode.measured.height;
|
||||||
|
|
||||||
if (nodeWidth && nodeHeight && parentWidth && parentHeight) {
|
if (parentWidth && parentHeight) {
|
||||||
currentExtent = [
|
extent = [
|
||||||
[parentX, parentY],
|
[parentX, parentY],
|
||||||
[parentX + parentWidth - nodeWidth, parentY + parentHeight - nodeHeight],
|
[parentX + parentWidth, parentY + parentHeight],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (parentNode && isCoordinateExtent(node.extent)) {
|
} else if (parentNode && isCoordinateExtent(node.extent)) {
|
||||||
currentExtent = [
|
extent = [
|
||||||
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
[node.extent[0][0] + parentX, node.extent[0][1] + parentY],
|
||||||
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
|
[node.extent[1][0] + parentX, node.extent[1][1] + parentY],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
const positionAbsolute = isCoordinateExtent(currentExtent)
|
const positionAbsolute = isCoordinateExtent(extent)
|
||||||
? clampPosition(nextPosition, currentExtent)
|
? clampPosition(nextPosition, extent, node.measured)
|
||||||
: nextPosition;
|
: nextPosition;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
position: {
|
position: {
|
||||||
// TODO: is there a better way to do this?
|
|
||||||
x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
|
x: positionAbsolute.x - parentX + node.measured.width! * origin[0],
|
||||||
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
|
y: positionAbsolute.y - parentY + node.measured.height! * origin[1],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { infiniteExtent } from '..';
|
||||||
import {
|
import {
|
||||||
NodeBase,
|
NodeBase,
|
||||||
CoordinateExtent,
|
CoordinateExtent,
|
||||||
@@ -17,37 +18,60 @@ import {
|
|||||||
ParentLookup,
|
ParentLookup,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getDimensions, getHandleBounds } from './dom';
|
import { getDimensions, getHandleBounds } from './dom';
|
||||||
import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
|
import {
|
||||||
|
clampPosition,
|
||||||
|
clampPositionToParent,
|
||||||
|
getBoundsOfRects,
|
||||||
|
getNodeDimensions,
|
||||||
|
isCoordinateExtent,
|
||||||
|
isNumeric,
|
||||||
|
nodeToRect,
|
||||||
|
} from './general';
|
||||||
import { getNodePositionWithOrigin } from './graph';
|
import { getNodePositionWithOrigin } from './graph';
|
||||||
import { ParentExpandChild } from './types';
|
import { ParentExpandChild } from './types';
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
nodeOrigin: [0, 0] as NodeOrigin,
|
nodeOrigin: [0, 0] as NodeOrigin,
|
||||||
|
nodeExtent: infiniteExtent,
|
||||||
elevateNodesOnSelect: true,
|
elevateNodesOnSelect: true,
|
||||||
defaults: {},
|
defaults: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
const adoptUserNodesDefaultOptions = {
|
const adoptUserNodesDefaultOptions = {
|
||||||
...defaultOptions,
|
...defaultOptions,
|
||||||
checkEquality: true,
|
checkEquality: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function mergeObjects<T extends Record<string, any>>(base: T, incoming?: Partial<T>): T {
|
||||||
|
const result = { ...base };
|
||||||
|
for (const key in incoming) {
|
||||||
|
if (incoming[key] !== undefined) {
|
||||||
|
// typecast is safe here, because we check for undefined
|
||||||
|
result[key] = (incoming as T)[key]!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function updateAbsolutePositions<NodeType extends NodeBase>(
|
export function updateAbsolutePositions<NodeType extends NodeBase>(
|
||||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||||
options?: UpdateNodesOptions<NodeType>
|
options?: UpdateNodesOptions<NodeType>
|
||||||
) {
|
) {
|
||||||
const _options = { ...defaultOptions, ...options };
|
const _options = mergeObjects(defaultOptions, options);
|
||||||
for (const node of nodeLookup.values()) {
|
for (const node of nodeLookup.values()) {
|
||||||
if (!node.parentId) {
|
if (!node.parentId) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateChildPosition(node, nodeLookup, parentLookup, _options);
|
updateChildNode(node, nodeLookup, parentLookup, _options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
type UpdateNodesOptions<NodeType extends NodeBase> = {
|
||||||
nodeOrigin?: NodeOrigin;
|
nodeOrigin?: NodeOrigin;
|
||||||
|
nodeExtent?: CoordinateExtent;
|
||||||
elevateNodesOnSelect?: boolean;
|
elevateNodesOnSelect?: boolean;
|
||||||
defaults?: Partial<NodeType>;
|
defaults?: Partial<NodeType>;
|
||||||
checkEquality?: boolean;
|
checkEquality?: boolean;
|
||||||
@@ -59,18 +83,23 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
|||||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||||
options?: UpdateNodesOptions<NodeType>
|
options?: UpdateNodesOptions<NodeType>
|
||||||
) {
|
) {
|
||||||
const _options = { ...adoptUserNodesDefaultOptions, ...options };
|
const _options = mergeObjects(adoptUserNodesDefaultOptions, options);
|
||||||
const tmpLookup = new Map(nodeLookup);
|
const tmpLookup = new Map(nodeLookup);
|
||||||
|
const selectedNodeZ: number = _options?.elevateNodesOnSelect ? 1000 : 0;
|
||||||
|
|
||||||
nodeLookup.clear();
|
nodeLookup.clear();
|
||||||
parentLookup.clear();
|
parentLookup.clear();
|
||||||
|
|
||||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
|
||||||
|
|
||||||
for (const userNode of nodes) {
|
for (const userNode of nodes) {
|
||||||
let internalNode = tmpLookup.get(userNode.id);
|
let internalNode = tmpLookup.get(userNode.id);
|
||||||
|
|
||||||
if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
|
if (_options.checkEquality && userNode === internalNode?.internals.userNode) {
|
||||||
nodeLookup.set(userNode.id, internalNode);
|
nodeLookup.set(userNode.id, internalNode);
|
||||||
} else {
|
} else {
|
||||||
|
const positionWithOrigin = getNodePositionWithOrigin(userNode, _options.nodeOrigin);
|
||||||
|
const extent = isCoordinateExtent(userNode.extent) ? userNode.extent : _options.nodeExtent;
|
||||||
|
const clampedPosition = clampPosition(positionWithOrigin, extent, getNodeDimensions(userNode));
|
||||||
|
|
||||||
internalNode = {
|
internalNode = {
|
||||||
..._options.defaults,
|
..._options.defaults,
|
||||||
...userNode,
|
...userNode,
|
||||||
@@ -79,30 +108,50 @@ export function adoptUserNodes<NodeType extends NodeBase>(
|
|||||||
height: userNode.measured?.height,
|
height: userNode.measured?.height,
|
||||||
},
|
},
|
||||||
internals: {
|
internals: {
|
||||||
positionAbsolute: getNodePositionWithOrigin(userNode, _options.nodeOrigin),
|
positionAbsolute: clampedPosition,
|
||||||
// if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured
|
// if user re-initializes the node or removes `measured` for whatever reason, we reset the handleBounds so that the node gets re-measured
|
||||||
handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds,
|
handleBounds: !userNode.measured ? undefined : internalNode?.internals.handleBounds,
|
||||||
z: calculateZ(userNode, selectedNodeZ),
|
z: calculateZ(userNode, selectedNodeZ),
|
||||||
userNode,
|
userNode,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
nodeLookup.set(userNode.id, internalNode);
|
nodeLookup.set(userNode.id, internalNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userNode.parentId) {
|
if (userNode.parentId) {
|
||||||
updateChildPosition(internalNode, nodeLookup, parentLookup, options);
|
updateChildNode(internalNode, nodeLookup, parentLookup, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateChildPosition<NodeType extends NodeBase>(
|
function updateParentLookup<NodeType extends NodeBase>(
|
||||||
|
node: InternalNodeBase<NodeType>,
|
||||||
|
parentLookup: ParentLookup<InternalNodeBase<NodeType>>
|
||||||
|
) {
|
||||||
|
if (!node.parentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childNodes = parentLookup.get(node.parentId);
|
||||||
|
|
||||||
|
if (childNodes) {
|
||||||
|
childNodes.set(node.id, node);
|
||||||
|
} else {
|
||||||
|
parentLookup.set(node.parentId, new Map([[node.id, node]]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates positionAbsolute and zIndex of a child node and the parentLookup.
|
||||||
|
*/
|
||||||
|
function updateChildNode<NodeType extends NodeBase>(
|
||||||
node: InternalNodeBase<NodeType>,
|
node: InternalNodeBase<NodeType>,
|
||||||
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
nodeLookup: NodeLookup<InternalNodeBase<NodeType>>,
|
||||||
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
parentLookup: ParentLookup<InternalNodeBase<NodeType>>,
|
||||||
options?: UpdateNodesOptions<NodeType>
|
options?: UpdateNodesOptions<NodeType>
|
||||||
) {
|
) {
|
||||||
const _options = { ...defaultOptions, ...options };
|
const { elevateNodesOnSelect, nodeOrigin, nodeExtent } = mergeObjects(defaultOptions, options);
|
||||||
|
|
||||||
const parentId = node.parentId!;
|
const parentId = node.parentId!;
|
||||||
const parentNode = nodeLookup.get(parentId);
|
const parentNode = nodeLookup.get(parentId);
|
||||||
|
|
||||||
@@ -113,25 +162,17 @@ function updateChildPosition<NodeType extends NodeBase>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// update the parentLookup
|
updateParentLookup(node, parentLookup);
|
||||||
const childNodes = parentLookup.get(parentId);
|
|
||||||
if (childNodes) {
|
|
||||||
childNodes.set(node.id, node);
|
|
||||||
} else {
|
|
||||||
parentLookup.set(parentId, new Map([[node.id, node]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0;
|
const selectedNodeZ = elevateNodesOnSelect ? 1000 : 0;
|
||||||
|
const { x, y, z } = calculateChildXYZ(node, parentNode, nodeOrigin, nodeExtent, selectedNodeZ);
|
||||||
const { x, y, z } = calculateChildXYZ(node, parentNode, _options.nodeOrigin!, selectedNodeZ);
|
const { positionAbsolute } = node.internals;
|
||||||
|
const positionChanged = x !== positionAbsolute.x || y !== positionAbsolute.y;
|
||||||
const currPosition = node.internals.positionAbsolute;
|
|
||||||
const positionChanged = x !== currPosition.x || y !== currPosition.y;
|
|
||||||
|
|
||||||
if (positionChanged || z !== node.internals.z) {
|
if (positionChanged || z !== node.internals.z) {
|
||||||
node.internals = {
|
node.internals = {
|
||||||
...node.internals,
|
...node.internals,
|
||||||
positionAbsolute: positionChanged ? { x, y } : currPosition,
|
positionAbsolute: positionChanged ? { x, y } : positionAbsolute,
|
||||||
z,
|
z,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -145,15 +186,32 @@ function calculateChildXYZ<NodeType extends NodeBase>(
|
|||||||
childNode: InternalNodeBase<NodeType>,
|
childNode: InternalNodeBase<NodeType>,
|
||||||
parentNode: InternalNodeBase<NodeType>,
|
parentNode: InternalNodeBase<NodeType>,
|
||||||
nodeOrigin: NodeOrigin,
|
nodeOrigin: NodeOrigin,
|
||||||
|
nodeExtent: CoordinateExtent,
|
||||||
selectedNodeZ: number
|
selectedNodeZ: number
|
||||||
) {
|
) {
|
||||||
const position = getNodePositionWithOrigin(childNode, nodeOrigin);
|
const { x: parentX, y: parentY } = parentNode.internals.positionAbsolute;
|
||||||
|
const childDimensions = getNodeDimensions(childNode);
|
||||||
|
const positionWithOrigin = getNodePositionWithOrigin(childNode, nodeOrigin);
|
||||||
|
const clampedPosition = isCoordinateExtent(childNode.extent)
|
||||||
|
? clampPosition(positionWithOrigin, childNode.extent, childDimensions)
|
||||||
|
: positionWithOrigin;
|
||||||
|
|
||||||
|
let absolutePosition = clampPosition(
|
||||||
|
{ x: parentX + clampedPosition.x, y: parentY + clampedPosition.y },
|
||||||
|
nodeExtent,
|
||||||
|
childDimensions
|
||||||
|
);
|
||||||
|
|
||||||
|
if (childNode.extent === 'parent') {
|
||||||
|
absolutePosition = clampPositionToParent(absolutePosition, childDimensions, parentNode);
|
||||||
|
}
|
||||||
|
|
||||||
const childZ = calculateZ(childNode, selectedNodeZ);
|
const childZ = calculateZ(childNode, selectedNodeZ);
|
||||||
const parentZ = parentNode.internals.z ?? 0;
|
const parentZ = parentNode.internals.z ?? 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
x: parentNode.internals.positionAbsolute.x + position.x,
|
x: absolutePosition.x,
|
||||||
y: parentNode.internals.positionAbsolute.y + position.y,
|
y: absolutePosition.y,
|
||||||
z: parentZ > childZ ? parentZ : childZ,
|
z: parentZ > childZ ? parentZ : childZ,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -249,7 +307,8 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
|||||||
nodeLookup: NodeLookup<NodeType>,
|
nodeLookup: NodeLookup<NodeType>,
|
||||||
parentLookup: ParentLookup<NodeType>,
|
parentLookup: ParentLookup<NodeType>,
|
||||||
domNode: HTMLElement | null,
|
domNode: HTMLElement | null,
|
||||||
nodeOrigin?: NodeOrigin
|
nodeOrigin?: NodeOrigin,
|
||||||
|
nodeExtent?: CoordinateExtent
|
||||||
): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } {
|
): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } {
|
||||||
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
const viewportNode = domNode?.querySelector('.xyflow__viewport');
|
||||||
let updatedInternals = false;
|
let updatedInternals = false;
|
||||||
@@ -287,18 +346,26 @@ export function updateNodeInternals<NodeType extends InternalNodeBase>(
|
|||||||
|
|
||||||
if (doUpdate) {
|
if (doUpdate) {
|
||||||
const nodeBounds = update.nodeElement.getBoundingClientRect();
|
const nodeBounds = update.nodeElement.getBoundingClientRect();
|
||||||
|
const extent = isCoordinateExtent(node.extent) ? node.extent : nodeExtent;
|
||||||
|
let { positionAbsolute } = node.internals;
|
||||||
|
|
||||||
|
if (node.parentId && node.extent === 'parent') {
|
||||||
|
positionAbsolute = clampPositionToParent(positionAbsolute, dimensions, nodeLookup.get(node.parentId)!);
|
||||||
|
} else if (extent) {
|
||||||
|
positionAbsolute = clampPosition(positionAbsolute, extent, dimensions);
|
||||||
|
}
|
||||||
|
|
||||||
node.measured = dimensions;
|
node.measured = dimensions;
|
||||||
node.internals = {
|
node.internals = {
|
||||||
...node.internals,
|
...node.internals,
|
||||||
positionAbsolute: getNodePositionWithOrigin(node, nodeOrigin),
|
positionAbsolute,
|
||||||
handleBounds: {
|
handleBounds: {
|
||||||
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
|
source: getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id),
|
||||||
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
|
target: getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if (node.parentId) {
|
if (node.parentId) {
|
||||||
updateChildPosition(node, nodeLookup, parentLookup, { nodeOrigin });
|
updateChildNode(node, nodeLookup, parentLookup, { nodeOrigin });
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedInternals = true;
|
updatedInternals = true;
|
||||||
|
|||||||
Reference in New Issue
Block a user