feat(props): add nodeExtent prop closes #854

This commit is contained in:
moklick
2021-01-26 10:29:38 +01:00
parent 3d3646796b
commit 1a9493e3fa
7 changed files with 64 additions and 14 deletions
+13 -6
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 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">
+8
View File
@@ -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);
+4
View File
@@ -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
View File
@@ -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;
}), }),
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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]),
});