feat(props): add nodeExtent prop closes #854
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import ReactFlow, { ReactFlowProvider, addEdge, removeElements, Controls, isNode } from 'react-flow-renderer';
|
||||
import dagre from 'dagre';
|
||||
|
||||
@@ -9,6 +9,11 @@ import './layouting.css';
|
||||
const dagreGraph = new dagre.graphlib.Graph();
|
||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||
|
||||
const nodeExtent = [
|
||||
[0, 0],
|
||||
[1000, 1000],
|
||||
];
|
||||
|
||||
const LayoutFlow = () => {
|
||||
const [elements, setElements] = useState(initialElements);
|
||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||
@@ -44,14 +49,16 @@ const LayoutFlow = () => {
|
||||
setElements(layoutedElements);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onLayout('TB');
|
||||
}, [onLayout]);
|
||||
|
||||
return (
|
||||
<div className="layoutflow">
|
||||
<ReactFlowProvider>
|
||||
<ReactFlow elements={elements} onConnect={onConnect} onElementsRemove={onElementsRemove}>
|
||||
<ReactFlow
|
||||
elements={elements}
|
||||
onConnect={onConnect}
|
||||
onElementsRemove={onElementsRemove}
|
||||
nodeExtent={nodeExtent}
|
||||
onLoad={() => onLayout('TB')}
|
||||
>
|
||||
<Controls />
|
||||
</ReactFlow>
|
||||
<div className="controls">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
KeyCode,
|
||||
PanOnScrollMode,
|
||||
OnEdgeUpdateFunc,
|
||||
NodeExtent,
|
||||
} from '../../types';
|
||||
|
||||
import '../../style.css';
|
||||
@@ -97,6 +98,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, '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}
|
||||
|
||||
@@ -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<StoreModel, number>;
|
||||
|
||||
setTranslateExtent: Action<StoreModel, TranslateExtent>;
|
||||
setNodeExtent: Action<StoreModel, NodeExtent>;
|
||||
|
||||
setSnapToGrid: Action<StoreModel, boolean>;
|
||||
setSnapGrid: Action<StoreModel, SnapGrid>;
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
@@ -339,6 +339,7 @@ export type FlowTransform = {
|
||||
};
|
||||
|
||||
export type TranslateExtent = [[number, number], [number, number]];
|
||||
export type NodeExtent = TranslateExtent;
|
||||
|
||||
export type KeyCode = number | string;
|
||||
|
||||
|
||||
@@ -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<StoreModel>) => {
|
||||
};
|
||||
};
|
||||
|
||||
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: {},
|
||||
|
||||
@@ -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]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user