feat(props): add nodeExtent prop closes #854

This commit is contained in:
moklick
2021-01-26 10:09:04 +01:00
parent 3d3646796b
commit 1a9493e3fa
7 changed files with 64 additions and 14 deletions

View File

@@ -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">

View File

@@ -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);

View File

@@ -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}

View File

@@ -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;
}),

View File

@@ -339,6 +339,7 @@ export type FlowTransform = {
};
export type TranslateExtent = [[number, number], [number, number]];
export type NodeExtent = TranslateExtent;
export type KeyCode = number | string;

View File

@@ -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: {},

View File

@@ -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]),
});