diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 8ca227b0..7dcff4d9 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -90,7 +90,7 @@ export function NodeWrapper({ const inlineDimensions = getNodeInlineStyleDimensions(node); // TODO: clamping should happen earlier const clampedPosition = nodeExtent - ? clampPosition(internals.positionAbsolute, nodeExtent, node.measured) + ? clampPosition(internals.positionAbsolute, nodeExtent, nodeDimensions) : internals.positionAbsolute; const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; diff --git a/packages/react/src/components/ReactFlowProvider/index.tsx b/packages/react/src/components/ReactFlowProvider/index.tsx index 4843afeb..b29f5f17 100644 --- a/packages/react/src/components/ReactFlowProvider/index.tsx +++ b/packages/react/src/components/ReactFlowProvider/index.tsx @@ -4,7 +4,7 @@ import { Provider } from '../../contexts/StoreContext'; import { createStore } from '../../store'; import { BatchProvider } from '../BatchProvider'; import type { Node, Edge } from '../../types'; -import { NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; export type ReactFlowProviderProps = { initialNodes?: Node[]; @@ -15,6 +15,7 @@ export type ReactFlowProviderProps = { initialHeight?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; children: ReactNode; }; @@ -27,6 +28,7 @@ export function ReactFlowProvider({ initialHeight: height, fitView, nodeOrigin, + nodeExtent, children, }: ReactFlowProviderProps) { const [store] = useState(() => @@ -39,6 +41,7 @@ export function ReactFlowProvider({ height, fitView, nodeOrigin, + nodeExtent, }) ); diff --git a/packages/react/src/container/ReactFlow/Wrapper.tsx b/packages/react/src/container/ReactFlow/Wrapper.tsx index ce1b5a74..5856809a 100644 --- a/packages/react/src/container/ReactFlow/Wrapper.tsx +++ b/packages/react/src/container/ReactFlow/Wrapper.tsx @@ -3,7 +3,7 @@ import { useContext, type ReactNode } from 'react'; import StoreContext from '../../contexts/StoreContext'; import { ReactFlowProvider } from '../../components/ReactFlowProvider'; import type { Node, Edge } from '../../types'; -import { NodeOrigin } from '@xyflow/system'; +import { CoordinateExtent, NodeOrigin } from '@xyflow/system'; export function Wrapper({ children, @@ -15,6 +15,7 @@ export function Wrapper({ height, fitView, nodeOrigin, + nodeExtent, }: { children: ReactNode; nodes?: Node[]; @@ -25,6 +26,7 @@ export function Wrapper({ height?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; }) { const isWrapped = useContext(StoreContext); @@ -44,6 +46,7 @@ export function Wrapper({ initialHeight={height} fitView={fitView} nodeOrigin={nodeOrigin} + nodeExtent={nodeExtent} > {children} diff --git a/packages/react/src/container/ReactFlow/index.tsx b/packages/react/src/container/ReactFlow/index.tsx index d6e3e5a4..962b6846 100644 --- a/packages/react/src/container/ReactFlow/index.tsx +++ b/packages/react/src/container/ReactFlow/index.tsx @@ -160,7 +160,15 @@ function ReactFlow( data-testid="rf__wrapper" id={id} > - + onInit={onInit} onNodeClick={onNodeClick} diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index c3fc0683..46ed38a6 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -15,6 +15,7 @@ import { ParentExpandChild, initialConnection, NodeOrigin, + CoordinateExtent, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -30,6 +31,7 @@ const createStore = ({ height, fitView, nodeOrigin, + nodeExtent, }: { nodes?: Node[]; edges?: Edge[]; @@ -39,10 +41,11 @@ const createStore = ({ height?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; }) => createWithEqualityFn( (set, get) => ({ - ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, defaultNodes, defaultEdges }), + ...getInitialState({ nodes, edges, width, height, fitView, nodeOrigin, nodeExtent, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); // setNodes() is called exclusively in response to user actions: diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index 279506ba..af52f2a2 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -9,6 +9,7 @@ import { getInternalNodesBounds, NodeOrigin, initialConnection, + CoordinateExtent, } from '@xyflow/system'; import type { Edge, InternalNode, Node, ReactFlowStore } from '../types'; @@ -22,6 +23,7 @@ const getInitialState = ({ height, fitView, nodeOrigin, + nodeExtent, }: { nodes?: Node[]; edges?: Edge[]; @@ -31,18 +33,22 @@ const getInitialState = ({ height?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; } = {}): ReactFlowStore => { const nodeLookup = new Map(); const parentLookup = new Map(); const connectionLookup = new Map(); const edgeLookup = new Map(); + const storeEdges = defaultEdges ?? edges ?? []; const storeNodes = defaultNodes ?? nodes ?? []; const storeNodeOrigin = nodeOrigin ?? [0, 0]; + const storeNodeExtent = nodeExtent ?? infiniteExtent; updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); adoptUserNodes(storeNodes, nodeLookup, parentLookup, { nodeOrigin: storeNodeOrigin, + nodeExtent: storeNodeExtent, elevateNodesOnSelect: false, }); @@ -76,7 +82,7 @@ const getInitialState = ({ minZoom: 0.5, maxZoom: 2, translateExtent: infiniteExtent, - nodeExtent: infiniteExtent, + nodeExtent: storeNodeExtent, nodesSelectionActive: false, userSelectionActive: false, userSelectionRect: null, diff --git a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte index 87f061d0..4fc060a6 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte +++ b/packages/svelte/src/lib/container/SvelteFlow/SvelteFlow.svelte @@ -53,6 +53,7 @@ export let onMoveEnd: $$Props['onMoveEnd'] = undefined; export let isValidConnection: $$Props['isValidConnection'] = undefined; export let translateExtent: $$Props['translateExtent'] = undefined; + export let nodeExtent: $$Props['nodeExtent'] = undefined; export let onlyRenderVisibleElements: $$Props['onlyRenderVisibleElements'] = undefined; export let panOnScrollMode: $$Props['panOnScrollMode'] = PanOnScrollMode.Free; export let preventScrolling: $$Props['preventScrolling'] = true; @@ -102,7 +103,8 @@ width, height, fitView, - nodeOrigin + nodeOrigin, + nodeExtent }); onMount(() => { diff --git a/packages/svelte/src/lib/container/SvelteFlow/types.ts b/packages/svelte/src/lib/container/SvelteFlow/types.ts index b5e81331..0963424e 100644 --- a/packages/svelte/src/lib/container/SvelteFlow/types.ts +++ b/packages/svelte/src/lib/container/SvelteFlow/types.ts @@ -206,6 +206,12 @@ export type SvelteFlowProps = DOMAttributes & { * @example [[-1000, -10000], [1000, 1000]] */ 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. * @default true */ diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index 9ed41bc0..24279a45 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -37,7 +37,8 @@ export function createStore({ width, height, fitView: fitViewOnCreate, - nodeOrigin + nodeOrigin, + nodeExtent }: { nodes?: Node[]; edges?: Edge[]; @@ -45,6 +46,7 @@ export function createStore({ height?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; }): SvelteFlowStore { const store = getInitialStore({ nodes, @@ -52,7 +54,8 @@ export function createStore({ width, height, fitView: fitViewOnCreate, - nodeOrigin + nodeOrigin, + nodeExtent }); function setNodeTypes(nodeTypes: NodeTypes) { @@ -482,7 +485,8 @@ export function createStoreContext({ width, height, fitView, - nodeOrigin + nodeOrigin, + nodeExtent }: { nodes?: Node[]; edges?: Edge[]; @@ -490,8 +494,9 @@ export function createStoreContext({ height?: number; fitView?: boolean; 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, { getStore: () => store diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 5c9b9f64..ffd7703a 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -75,7 +75,8 @@ export const getInitialStore = ({ width, height, fitView, - nodeOrigin + nodeOrigin, + nodeExtent }: { nodes?: Node[]; edges?: Edge[]; @@ -83,17 +84,23 @@ export const getInitialStore = ({ height?: number; fitView?: boolean; nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; }) => { const nodeLookup: NodeLookup = new Map(); const parentLookup = new Map(); + const connectionLookup = new Map(); + const edgeLookup = new Map(); + const storeNodeOrigin = nodeOrigin ?? [0, 0]; + const storeNodeExtent = nodeExtent ?? infiniteExtent; + adoptUserNodes(nodes, nodeLookup, parentLookup, { + nodeExtent: storeNodeExtent, nodeOrigin: storeNodeOrigin, elevateNodesOnSelect: false, checkEquality: false }); - const connectionLookup = new Map(); - const edgeLookup = new Map(); + updateConnectionLookup(connectionLookup, edgeLookup, edges); let viewport: Viewport = { x: 0, y: 0, zoom: 1 }; @@ -121,7 +128,7 @@ export const getInitialStore = ({ maxZoom: writable(2), nodeOrigin: writable(storeNodeOrigin), nodeDragThreshold: writable(1), - nodeExtent: writable(infiniteExtent), + nodeExtent: writable(storeNodeExtent), translateExtent: writable(infiniteExtent), autoPanOnNodeDrag: writable(true), autoPanOnConnect: writable(true), diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index f2cf1bfd..d1bd40fc 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,3 +1,4 @@ +import { infiniteExtent } from '..'; import { NodeBase, CoordinateExtent, @@ -23,6 +24,7 @@ import { ParentExpandChild } from './types'; const defaultOptions = { nodeOrigin: [0, 0] as NodeOrigin, + nodeExtent: infiniteExtent, elevateNodesOnSelect: true, defaults: {}, }; @@ -48,6 +50,7 @@ export function updateAbsolutePositions( type UpdateNodesOptions = { nodeOrigin?: NodeOrigin; + nodeExtent?: CoordinateExtent; elevateNodesOnSelect?: boolean; defaults?: Partial; checkEquality?: boolean;