+ onLayout('TB')}
+ >
diff --git a/src/container/GraphView/index.tsx b/src/container/GraphView/index.tsx
index 5defbb05..363b50b9 100644
--- a/src/container/GraphView/index.tsx
+++ b/src/container/GraphView/index.tsx
@@ -70,6 +70,7 @@ const GraphView = ({
defaultZoom,
defaultPosition,
translateExtent,
+ nodeExtent,
arrowHeadColor,
markerEndId,
zoomOnScroll,
@@ -97,6 +98,7 @@ const GraphView = ({
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
+ const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
const currentStore = useStore();
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
@@ -192,6 +194,12 @@ const GraphView = ({
}
}, [translateExtent]);
+ useEffect(() => {
+ if (typeof nodeExtent !== 'undefined') {
+ setNodeExtent(nodeExtent);
+ }
+ }, [nodeExtent]);
+
useEffect(() => {
if (typeof connectionMode !== 'undefined') {
setConnectionMode(connectionMode);
diff --git a/src/container/ReactFlow/index.tsx b/src/container/ReactFlow/index.tsx
index fbc0cc0d..6c974de4 100644
--- a/src/container/ReactFlow/index.tsx
+++ b/src/container/ReactFlow/index.tsx
@@ -30,6 +30,7 @@ import {
KeyCode,
PanOnScrollMode,
OnEdgeUpdateFunc,
+ NodeExtent,
} from '../../types';
import '../../style.css';
@@ -97,6 +98,7 @@ export interface ReactFlowProps extends Omit, 'on
defaultZoom?: number;
defaultPosition?: [number, number];
translateExtent?: TranslateExtent;
+ nodeExtent?: NodeExtent;
arrowHeadColor?: string;
markerEndId?: string;
zoomOnScroll?: boolean;
@@ -154,6 +156,7 @@ const ReactFlow = ({
defaultZoom = 1,
defaultPosition = [0, 0],
translateExtent,
+ nodeExtent,
arrowHeadColor = '#b1b1b7',
markerEndId,
zoomOnScroll = true,
@@ -216,6 +219,7 @@ const ReactFlow = ({
defaultZoom={defaultZoom}
defaultPosition={defaultPosition}
translateExtent={translateExtent}
+ nodeExtent={nodeExtent}
arrowHeadColor={arrowHeadColor}
markerEndId={markerEndId}
zoomOnScroll={zoomOnScroll}
diff --git a/src/store/index.ts b/src/store/index.ts
index e9f3973a..7a094dc9 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -2,7 +2,7 @@ import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'e
import isEqual from 'fast-deep-equal';
import { Selection as D3Selection, ZoomBehavior } from 'd3';
-import { getDimensions } from '../utils';
+import { clampPosition, getDimensions } from '../utils';
import { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge, parseElement } from '../utils/graph';
import { getHandleBounds } from '../components/Nodes/utils';
@@ -27,6 +27,7 @@ import {
TranslateExtent,
SnapGrid,
ConnectionMode,
+ NodeExtent,
} from '../types';
type NodeDimensionUpdate = {
@@ -44,6 +45,7 @@ type InitD3Zoom = {
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
transform: Transform;
};
+
export interface StoreModel {
width: number;
height: number;
@@ -61,6 +63,7 @@ export interface StoreModel {
minZoom: number;
maxZoom: number;
translateExtent: TranslateExtent;
+ nodeExtent: NodeExtent;
nodesSelectionActive: boolean;
selectionActive: boolean;
@@ -120,6 +123,7 @@ export interface StoreModel {
setMaxZoom: Action;
setTranslateExtent: Action;
+ setNodeExtent: Action;
setSnapToGrid: Action;
setSnapGrid: Action;
@@ -162,6 +166,10 @@ export const storeModel: StoreModel = {
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
+ nodeExtent: [
+ [Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
+ [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
+ ],
nodesSelectionActive: false,
selectionActive: false,
@@ -236,7 +244,10 @@ export const storeModel: StoreModel = {
};
if (positionChanged) {
- (state.elements[storeElementIndex] as Node).__rf.position = propNode.position;
+ (state.elements[storeElementIndex] as Node).__rf.position = clampPosition(
+ propNode.position,
+ state.nodeExtent
+ );
}
if (typeChanged) {
@@ -252,7 +263,7 @@ export const storeModel: StoreModel = {
}
} else {
// add new element
- state.elements.push(parseElement(el));
+ state.elements.push(parseElement(el, state.nodeExtent));
}
});
}),
@@ -304,7 +315,7 @@ export const storeModel: StoreModel = {
state.elements.forEach((n) => {
if (n.id === id && isNode(n)) {
- n.__rf.position = position;
+ n.__rf.position = clampPosition(position, state.nodeExtent);
}
});
}),
@@ -313,10 +324,11 @@ export const storeModel: StoreModel = {
state.elements.forEach((n) => {
if (isNode(n) && (id === n.id || state.selectedElements?.find((sNode) => sNode.id === n.id))) {
if (diff) {
- n.__rf.position = {
+ const position = {
x: n.__rf.position.x + diff.x,
y: n.__rf.position.y + diff.y,
};
+ n.__rf.position = clampPosition(position, state.nodeExtent);
}
n.__rf.isDragging = isDragging;
}
@@ -465,6 +477,16 @@ export const storeModel: StoreModel = {
}
}),
+ setNodeExtent: action((state, nodeExtent) => {
+ state.nodeExtent = nodeExtent;
+
+ state.elements.forEach((el) => {
+ if (isNode(el)) {
+ el.__rf.position = clampPosition(el.__rf.position, nodeExtent);
+ }
+ });
+ }),
+
setConnectionPosition: action((state, position) => {
state.connectionPosition = position;
}),
diff --git a/src/types/index.ts b/src/types/index.ts
index 11a2ffb8..888a5b00 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -339,6 +339,7 @@ export type FlowTransform = {
};
export type TranslateExtent = [[number, number], [number, number]];
+export type NodeExtent = TranslateExtent;
export type KeyCode = number | string;
diff --git a/src/utils/graph.ts b/src/utils/graph.ts
index 772fd87c..5e8c2638 100644
--- a/src/utils/graph.ts
+++ b/src/utils/graph.ts
@@ -1,5 +1,7 @@
import { Store } from 'easy-peasy';
+
import { StoreModel } from '../store';
+import { clampPosition } from '../utils';
import {
ElementId,
Node,
@@ -11,6 +13,7 @@ import {
Box,
Connection,
FlowExportObject,
+ NodeExtent,
} from '../types';
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
@@ -142,7 +145,7 @@ export const onLoadProject = (currentStore: Store) => {
};
};
-export const parseElement = (element: Node | Edge): Node | Edge => {
+export const parseElement = (element: Node | Edge, nodeExtent: NodeExtent): Node | Edge => {
if (!element.id) {
throw new Error('All nodes and edges need to have an id.');
}
@@ -164,7 +167,7 @@ export const parseElement = (element: Node | Edge): Node | Edge => {
id: element.id.toString(),
type: element.type || 'default',
__rf: {
- position: element.position,
+ position: clampPosition(element.position, nodeExtent),
width: null,
height: null,
handleBounds: {},
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 13832a31..9ddb29aa 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,7 +1,7 @@
import { DraggableEvent } from 'react-draggable';
import { MouseEvent as ReactMouseEvent } from 'react';
-import { Dimensions } from '../types';
+import { Dimensions, XYPosition, NodeExtent } from '../types';
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
const target = e?.target as HTMLElement;
@@ -17,3 +17,8 @@ export const getDimensions = (node: HTMLDivElement): Dimensions => ({
});
export const clamp = (val: number, min: number = 0, max: number = 1): number => Math.min(Math.max(val, min), max);
+
+export const clampPosition = (position: XYPosition, extent: NodeExtent) => ({
+ x: clamp(position.x, extent[0][0], extent[1][0]),
+ y: clamp(position.y, extent[0][1], extent[1][1]),
+});