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 ReactFlow, { ReactFlowProvider, addEdge, removeElements, Controls, isNode } from 'react-flow-renderer';
|
||||||
import dagre from 'dagre';
|
import dagre from 'dagre';
|
||||||
|
|
||||||
@@ -9,6 +9,11 @@ import './layouting.css';
|
|||||||
const dagreGraph = new dagre.graphlib.Graph();
|
const dagreGraph = new dagre.graphlib.Graph();
|
||||||
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
dagreGraph.setDefaultEdgeLabel(() => ({}));
|
||||||
|
|
||||||
|
const nodeExtent = [
|
||||||
|
[0, 0],
|
||||||
|
[1000, 1000],
|
||||||
|
];
|
||||||
|
|
||||||
const LayoutFlow = () => {
|
const LayoutFlow = () => {
|
||||||
const [elements, setElements] = useState(initialElements);
|
const [elements, setElements] = useState(initialElements);
|
||||||
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
const onConnect = (params) => setElements((els) => addEdge(params, els));
|
||||||
@@ -44,14 +49,16 @@ const LayoutFlow = () => {
|
|||||||
setElements(layoutedElements);
|
setElements(layoutedElements);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
onLayout('TB');
|
|
||||||
}, [onLayout]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="layoutflow">
|
<div className="layoutflow">
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<ReactFlow elements={elements} onConnect={onConnect} onElementsRemove={onElementsRemove}>
|
<ReactFlow
|
||||||
|
elements={elements}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onElementsRemove={onElementsRemove}
|
||||||
|
nodeExtent={nodeExtent}
|
||||||
|
onLoad={() => onLayout('TB')}
|
||||||
|
>
|
||||||
<Controls />
|
<Controls />
|
||||||
</ReactFlow>
|
</ReactFlow>
|
||||||
<div className="controls">
|
<div className="controls">
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const GraphView = ({
|
|||||||
defaultZoom,
|
defaultZoom,
|
||||||
defaultPosition,
|
defaultPosition,
|
||||||
translateExtent,
|
translateExtent,
|
||||||
|
nodeExtent,
|
||||||
arrowHeadColor,
|
arrowHeadColor,
|
||||||
markerEndId,
|
markerEndId,
|
||||||
zoomOnScroll,
|
zoomOnScroll,
|
||||||
@@ -97,6 +98,7 @@ const GraphView = ({
|
|||||||
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
|
const setMinZoom = useStoreActions((actions) => actions.setMinZoom);
|
||||||
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
|
const setMaxZoom = useStoreActions((actions) => actions.setMaxZoom);
|
||||||
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
|
const setTranslateExtent = useStoreActions((actions) => actions.setTranslateExtent);
|
||||||
|
const setNodeExtent = useStoreActions((actions) => actions.setNodeExtent);
|
||||||
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
|
const setConnectionMode = useStoreActions((actions) => actions.setConnectionMode);
|
||||||
const currentStore = useStore();
|
const currentStore = useStore();
|
||||||
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
|
const { zoomIn, zoomOut, zoomTo, transform, fitView, initialized } = useZoomPanHelper();
|
||||||
@@ -192,6 +194,12 @@ const GraphView = ({
|
|||||||
}
|
}
|
||||||
}, [translateExtent]);
|
}, [translateExtent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof nodeExtent !== 'undefined') {
|
||||||
|
setNodeExtent(nodeExtent);
|
||||||
|
}
|
||||||
|
}, [nodeExtent]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof connectionMode !== 'undefined') {
|
if (typeof connectionMode !== 'undefined') {
|
||||||
setConnectionMode(connectionMode);
|
setConnectionMode(connectionMode);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
KeyCode,
|
KeyCode,
|
||||||
PanOnScrollMode,
|
PanOnScrollMode,
|
||||||
OnEdgeUpdateFunc,
|
OnEdgeUpdateFunc,
|
||||||
|
NodeExtent,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
|
|
||||||
import '../../style.css';
|
import '../../style.css';
|
||||||
@@ -97,6 +98,7 @@ export interface ReactFlowProps extends Omit<HTMLAttributes<HTMLDivElement>, 'on
|
|||||||
defaultZoom?: number;
|
defaultZoom?: number;
|
||||||
defaultPosition?: [number, number];
|
defaultPosition?: [number, number];
|
||||||
translateExtent?: TranslateExtent;
|
translateExtent?: TranslateExtent;
|
||||||
|
nodeExtent?: NodeExtent;
|
||||||
arrowHeadColor?: string;
|
arrowHeadColor?: string;
|
||||||
markerEndId?: string;
|
markerEndId?: string;
|
||||||
zoomOnScroll?: boolean;
|
zoomOnScroll?: boolean;
|
||||||
@@ -154,6 +156,7 @@ const ReactFlow = ({
|
|||||||
defaultZoom = 1,
|
defaultZoom = 1,
|
||||||
defaultPosition = [0, 0],
|
defaultPosition = [0, 0],
|
||||||
translateExtent,
|
translateExtent,
|
||||||
|
nodeExtent,
|
||||||
arrowHeadColor = '#b1b1b7',
|
arrowHeadColor = '#b1b1b7',
|
||||||
markerEndId,
|
markerEndId,
|
||||||
zoomOnScroll = true,
|
zoomOnScroll = true,
|
||||||
@@ -216,6 +219,7 @@ const ReactFlow = ({
|
|||||||
defaultZoom={defaultZoom}
|
defaultZoom={defaultZoom}
|
||||||
defaultPosition={defaultPosition}
|
defaultPosition={defaultPosition}
|
||||||
translateExtent={translateExtent}
|
translateExtent={translateExtent}
|
||||||
|
nodeExtent={nodeExtent}
|
||||||
arrowHeadColor={arrowHeadColor}
|
arrowHeadColor={arrowHeadColor}
|
||||||
markerEndId={markerEndId}
|
markerEndId={markerEndId}
|
||||||
zoomOnScroll={zoomOnScroll}
|
zoomOnScroll={zoomOnScroll}
|
||||||
|
|||||||
+27
-5
@@ -2,7 +2,7 @@ import { createStore, Action, action, Thunk, thunk, computed, Computed } from 'e
|
|||||||
import isEqual from 'fast-deep-equal';
|
import isEqual from 'fast-deep-equal';
|
||||||
import { Selection as D3Selection, ZoomBehavior } from 'd3';
|
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 { getNodesInside, getConnectedEdges, getRectOfNodes, isNode, isEdge, parseElement } from '../utils/graph';
|
||||||
import { getHandleBounds } from '../components/Nodes/utils';
|
import { getHandleBounds } from '../components/Nodes/utils';
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
TranslateExtent,
|
TranslateExtent,
|
||||||
SnapGrid,
|
SnapGrid,
|
||||||
ConnectionMode,
|
ConnectionMode,
|
||||||
|
NodeExtent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
type NodeDimensionUpdate = {
|
type NodeDimensionUpdate = {
|
||||||
@@ -44,6 +45,7 @@ type InitD3Zoom = {
|
|||||||
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
d3ZoomHandler: ((this: Element, event: any, d: unknown) => void) | undefined;
|
||||||
transform: Transform;
|
transform: Transform;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface StoreModel {
|
export interface StoreModel {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
@@ -61,6 +63,7 @@ export interface StoreModel {
|
|||||||
minZoom: number;
|
minZoom: number;
|
||||||
maxZoom: number;
|
maxZoom: number;
|
||||||
translateExtent: TranslateExtent;
|
translateExtent: TranslateExtent;
|
||||||
|
nodeExtent: NodeExtent;
|
||||||
|
|
||||||
nodesSelectionActive: boolean;
|
nodesSelectionActive: boolean;
|
||||||
selectionActive: boolean;
|
selectionActive: boolean;
|
||||||
@@ -120,6 +123,7 @@ export interface StoreModel {
|
|||||||
setMaxZoom: Action<StoreModel, number>;
|
setMaxZoom: Action<StoreModel, number>;
|
||||||
|
|
||||||
setTranslateExtent: Action<StoreModel, TranslateExtent>;
|
setTranslateExtent: Action<StoreModel, TranslateExtent>;
|
||||||
|
setNodeExtent: Action<StoreModel, NodeExtent>;
|
||||||
|
|
||||||
setSnapToGrid: Action<StoreModel, boolean>;
|
setSnapToGrid: Action<StoreModel, boolean>;
|
||||||
setSnapGrid: Action<StoreModel, SnapGrid>;
|
setSnapGrid: Action<StoreModel, SnapGrid>;
|
||||||
@@ -162,6 +166,10 @@ export const storeModel: StoreModel = {
|
|||||||
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
],
|
],
|
||||||
|
nodeExtent: [
|
||||||
|
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
|
||||||
|
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
|
||||||
|
],
|
||||||
|
|
||||||
nodesSelectionActive: false,
|
nodesSelectionActive: false,
|
||||||
selectionActive: false,
|
selectionActive: false,
|
||||||
@@ -236,7 +244,10 @@ export const storeModel: StoreModel = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (positionChanged) {
|
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) {
|
if (typeChanged) {
|
||||||
@@ -252,7 +263,7 @@ export const storeModel: StoreModel = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// add new element
|
// 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) => {
|
state.elements.forEach((n) => {
|
||||||
if (n.id === id && isNode(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) => {
|
state.elements.forEach((n) => {
|
||||||
if (isNode(n) && (id === n.id || state.selectedElements?.find((sNode) => sNode.id === n.id))) {
|
if (isNode(n) && (id === n.id || state.selectedElements?.find((sNode) => sNode.id === n.id))) {
|
||||||
if (diff) {
|
if (diff) {
|
||||||
n.__rf.position = {
|
const position = {
|
||||||
x: n.__rf.position.x + diff.x,
|
x: n.__rf.position.x + diff.x,
|
||||||
y: n.__rf.position.y + diff.y,
|
y: n.__rf.position.y + diff.y,
|
||||||
};
|
};
|
||||||
|
n.__rf.position = clampPosition(position, state.nodeExtent);
|
||||||
}
|
}
|
||||||
n.__rf.isDragging = isDragging;
|
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) => {
|
setConnectionPosition: action((state, position) => {
|
||||||
state.connectionPosition = position;
|
state.connectionPosition = position;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -339,6 +339,7 @@ export type FlowTransform = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type TranslateExtent = [[number, number], [number, number]];
|
export type TranslateExtent = [[number, number], [number, number]];
|
||||||
|
export type NodeExtent = TranslateExtent;
|
||||||
|
|
||||||
export type KeyCode = number | string;
|
export type KeyCode = number | string;
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -1,5 +1,7 @@
|
|||||||
import { Store } from 'easy-peasy';
|
import { Store } from 'easy-peasy';
|
||||||
|
|
||||||
import { StoreModel } from '../store';
|
import { StoreModel } from '../store';
|
||||||
|
import { clampPosition } from '../utils';
|
||||||
import {
|
import {
|
||||||
ElementId,
|
ElementId,
|
||||||
Node,
|
Node,
|
||||||
@@ -11,6 +13,7 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Connection,
|
Connection,
|
||||||
FlowExportObject,
|
FlowExportObject,
|
||||||
|
NodeExtent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
|
||||||
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
|
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) {
|
if (!element.id) {
|
||||||
throw new Error('All nodes and edges need to have an 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(),
|
id: element.id.toString(),
|
||||||
type: element.type || 'default',
|
type: element.type || 'default',
|
||||||
__rf: {
|
__rf: {
|
||||||
position: element.position,
|
position: clampPosition(element.position, nodeExtent),
|
||||||
width: null,
|
width: null,
|
||||||
height: null,
|
height: null,
|
||||||
handleBounds: {},
|
handleBounds: {},
|
||||||
|
|||||||
+6
-1
@@ -1,7 +1,7 @@
|
|||||||
import { DraggableEvent } from 'react-draggable';
|
import { DraggableEvent } from 'react-draggable';
|
||||||
import { MouseEvent as ReactMouseEvent } from 'react';
|
import { MouseEvent as ReactMouseEvent } from 'react';
|
||||||
|
|
||||||
import { Dimensions } from '../types';
|
import { Dimensions, XYPosition, NodeExtent } from '../types';
|
||||||
|
|
||||||
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
|
export const isInputDOMNode = (e: ReactMouseEvent | DraggableEvent | KeyboardEvent) => {
|
||||||
const target = e?.target as HTMLElement;
|
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 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