Merge pull request #283 from wbkd/develop

Develop
This commit is contained in:
Moritz
2020-06-05 12:39:55 +02:00
committed by GitHub
18 changed files with 162 additions and 130 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1, user-scaleable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
<meta name="theme-color" content="#222222" /> <meta name="theme-color" content="#222222" />
<meta name="description" content="react flow examples" /> <meta name="description" content="react flow examples" />
<title>React Flow Examples</title> <title>React Flow Examples</title>
+1 -1
View File
@@ -17,7 +17,7 @@ export default () => {
<div className="title">Nodes</div> <div className="title">Nodes</div>
{nodes.map((node) => ( {nodes.map((node) => (
<div key={node.id}> <div key={node.id}>
Node {node.id} - x: {node.__rg.position.x.toFixed(2)}, y: {node.__rg.position.y.toFixed(2)} Node {node.id} - x: {node.__rf.position.x.toFixed(2)}, y: {node.__rf.position.y.toFixed(2)}
</div> </div>
))} ))}
</aside> </aside>
+4 -2
View File
@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import ReactFlow, { ReactFlowProvider, addEdge, removeElements } from 'react-flow-renderer'; import ReactFlow, { ReactFlowProvider, addEdge, removeElements, Controls } from 'react-flow-renderer';
import Sidebar from './Sidebar'; import Sidebar from './Sidebar';
@@ -31,7 +31,9 @@ const ProviderFlow = () => {
onElementClick={onElementClick} onElementClick={onElementClick}
onConnect={onConnect} onConnect={onConnect}
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
/> >
<Controls />
</ReactFlow>
</div> </div>
</ReactFlowProvider> </ReactFlowProvider>
</div> </div>
+10 -4
View File
@@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import classnames from 'classnames'; import classnames from 'classnames';
import { fitView, zoomIn, zoomOut } from '../../utils/graph';
import { useStoreState, useStoreActions } from '../../store/hooks'; import { useStoreState, useStoreActions } from '../../store/hooks';
import PlusIcon from '../../../assets/icons/plus.svg'; import PlusIcon from '../../../assets/icons/plus.svg';
@@ -20,6 +19,10 @@ interface ControlProps extends React.HTMLAttributes<HTMLDivElement> {
const Controls = ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => { const Controls = ({ style, showZoom = true, showFitView = true, showInteractive = true, className }: ControlProps) => {
const setInteractive = useStoreActions((actions) => actions.setInteractive); const setInteractive = useStoreActions((actions) => actions.setInteractive);
const fitView = useStoreActions((actions) => actions.fitView);
const zoomIn = useStoreActions((actions) => actions.zoomIn);
const zoomOut = useStoreActions((actions) => actions.zoomOut);
const isInteractive = useStoreState((s) => s.isInteractive); const isInteractive = useStoreState((s) => s.isInteractive);
const mapClasses = classnames('react-flow__controls', className); const mapClasses = classnames('react-flow__controls', className);
@@ -27,16 +30,19 @@ const Controls = ({ style, showZoom = true, showFitView = true, showInteractive
<div className={mapClasses} style={style}> <div className={mapClasses} style={style}>
{showZoom && ( {showZoom && (
<> <>
<div className="react-flow__controls-button react-flow__controls-zoomin" onClick={zoomIn}> <div className="react-flow__controls-button react-flow__controls-zoomin" onClick={() => zoomIn()}>
<PlusIcon /> <PlusIcon />
</div> </div>
<div className="react-flow__controls-button react-flow__controls-zoomout" onClick={zoomOut}> <div className="react-flow__controls-button react-flow__controls-zoomout" onClick={() => zoomOut()}>
<MinusIcon /> <MinusIcon />
</div> </div>
</> </>
)} )}
{showFitView && ( {showFitView && (
<div className="react-flow__controls-button react-flow__controls-fitview" onClick={() => fitView()}> <div
className="react-flow__controls-button react-flow__controls-fitview"
onClick={() => fitView({ padding: 0.1 })}
>
<FitviewIcon /> <FitviewIcon />
</div> </div>
)} )}
@@ -13,7 +13,7 @@ const MiniMapNode = ({ node, color, borderRadius }: MiniMapNodeProps) => {
position: { x, y }, position: { x, y },
width, width,
height, height,
} = node.__rg; } = node.__rf;
const { background, backgroundColor } = node.style || {}; const { background, backgroundColor } = node.style || {};
const fill = (background || backgroundColor || color) as string; const fill = (background || backgroundColor || color) as string;
+6 -6
View File
@@ -46,12 +46,12 @@ export default ({
const connectionLineClasses: string = cx('react-flow__connection', className); const connectionLineClasses: string = cx('react-flow__connection', className);
const sourceHandle = handleId const sourceHandle = handleId
? sourceNode.__rg.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId) ? sourceNode.__rf.handleBounds[connectionHandleType].find((d: HandleElement) => d.id === handleId)
: sourceNode.__rg.handleBounds[connectionHandleType][0]; : sourceNode.__rf.handleBounds[connectionHandleType][0];
const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rg.width / 2; const sourceHandleX = sourceHandle ? sourceHandle.x + sourceHandle.width / 2 : sourceNode.__rf.width / 2;
const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rg.height; const sourceHandleY = sourceHandle ? sourceHandle.y + sourceHandle.height / 2 : sourceNode.__rf.height;
const sourceX = sourceNode.__rg.position.x + sourceHandleX; const sourceX = sourceNode.__rf.position.x + sourceHandleX;
const sourceY = sourceNode.__rg.position.y + sourceHandleY; const sourceY = sourceNode.__rf.position.y + sourceHandleY;
const targetX = (connectionPositionX - transform[0]) * (1 / transform[2]); const targetX = (connectionPositionX - transform[0]) * (1 / transform[2]);
const targetY = (connectionPositionY - transform[1]) * (1 / transform[2]); const targetY = (connectionPositionY - transform[1]) * (1 / transform[2]);
+3 -2
View File
@@ -1,7 +1,7 @@
import React, { memo, ComponentType, CSSProperties } from 'react'; import React, { memo, ComponentType, CSSProperties } from 'react';
import cx from 'classnames'; import cx from 'classnames';
import store from '../../store'; import { useStoreActions } from '../../store/hooks';
import { ElementId, Edge, EdgeCompProps } from '../../types'; import { ElementId, Edge, EdgeCompProps } from '../../types';
interface EdgeWrapperProps { interface EdgeWrapperProps {
@@ -38,6 +38,7 @@ export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
className, className,
...rest ...rest
}: EdgeWrapperProps) => { }: EdgeWrapperProps) => {
const setSelectedElements = useStoreActions((a) => a.setSelectedElements);
const edgeClasses = cx('react-flow__edge', `react-flow__edge-${type}`, className, { selected, animated }); const edgeClasses = cx('react-flow__edge', `react-flow__edge-${type}`, className, { selected, animated });
const edgeGroupStyle: CSSProperties = { const edgeGroupStyle: CSSProperties = {
pointerEvents: isInteractive ? 'all' : 'none', pointerEvents: isInteractive ? 'all' : 'none',
@@ -47,7 +48,7 @@ export default (EdgeComponent: ComponentType<EdgeCompProps>) => {
return; return;
} }
store.dispatch.setSelectedElements({ id, source, target }); setSelectedElements({ id, source, target });
if (onClick) { if (onClick) {
onClick({ id, source, target, type }); onClick({ id, source, target, type });
+7 -2
View File
@@ -17,8 +17,8 @@ function getStartPositions(nodes: Node[]): StartPositions {
return nodes.reduce((res, node) => { return nodes.reduce((res, node) => {
const startPosition = { const startPosition = {
x: node.__rg.position.x || node.position.x, x: node.__rf.position.x || node.position.x,
y: node.__rg.position.y || node.position.y, y: node.__rf.position.y || node.position.y,
}; };
res[node.id] = startPosition; res[node.id] = startPosition;
@@ -38,9 +38,14 @@ export default memo(() => {
const nodes = useStoreState((s) => s.nodes); const nodes = useStoreState((s) => s.nodes);
const updateNodePos = useStoreActions((a) => a.updateNodePos); const updateNodePos = useStoreActions((a) => a.updateNodePos);
const position = selectedNodesBbox; const position = selectedNodesBbox;
const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number]; const grid = (snapToGrid ? snapGrid : [1, 1])! as [number, number];
if (!selectedElements) {
return null;
}
const onStart = (evt: MouseEvent) => { const onStart = (evt: MouseEvent) => {
const scaledClient: XYPosition = { const scaledClient: XYPosition = {
x: evt.clientX / tScale, x: evt.clientX / tScale,
+21 -10
View File
@@ -2,13 +2,14 @@
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift * The user selection rectangle gets displayed when a user drags the mouse while pressing shift
*/ */
import React, { memo } from 'react'; import React, { memo, useEffect } from 'react';
import { useStoreActions, useStoreState } from '../../store/hooks'; import { useStoreActions, useStoreState } from '../../store/hooks';
import { XYPosition } from '../../types'; import { XYPosition } from '../../types';
type UserSelectionProps = { type UserSelectionProps = {
isInteractive: boolean; isInteractive: boolean;
selectionKeyPressed: boolean;
}; };
function getMousePosition(evt: React.MouseEvent): XYPosition | void { function getMousePosition(evt: React.MouseEvent): XYPosition | void {
@@ -44,25 +45,37 @@ const SelectionRect = () => {
); );
}; };
export default memo(({ isInteractive }: UserSelectionProps) => { export default memo(({ isInteractive, selectionKeyPressed }: UserSelectionProps) => {
const selectionActive = useStoreState((s) => s.selectionActive);
const setUserSelection = useStoreActions((a) => a.setUserSelection); const setUserSelection = useStoreActions((a) => a.setUserSelection);
const updateUserSelection = useStoreActions((a) => a.updateUserSelection); const updateUserSelection = useStoreActions((a) => a.updateUserSelection);
const unsetUserSelection = useStoreActions((a) => a.unsetUserSelection); const unsetUserSelection = useStoreActions((a) => a.unsetUserSelection);
const renderUserSelectionPane = selectionActive || selectionKeyPressed;
if (!isInteractive) { useEffect(() => {
if (!selectionKeyPressed) {
unsetUserSelection();
}
}, [selectionKeyPressed]);
if (!isInteractive || !renderUserSelectionPane) {
return null; return null;
} }
function onMouseDown(evt: React.MouseEvent): void { const onMouseDown = (evt: React.MouseEvent): void => {
const mousePos = getMousePosition(evt); const mousePos = getMousePosition(evt);
if (!mousePos) { if (!mousePos) {
return; return;
} }
setUserSelection(mousePos); setUserSelection(mousePos);
} };
function onMouseMove(evt: React.MouseEvent): void { const onMouseMove = (evt: React.MouseEvent): void => {
if (!selectionKeyPressed || !selectionActive) {
return;
}
const mousePos = getMousePosition(evt); const mousePos = getMousePosition(evt);
if (!mousePos) { if (!mousePos) {
@@ -70,11 +83,9 @@ export default memo(({ isInteractive }: UserSelectionProps) => {
} }
updateUserSelection(mousePos); updateUserSelection(mousePos);
} };
function onMouseUp() { const onMouseUp = () => unsetUserSelection();
unsetUserSelection();
}
return ( return (
<div <div
+13 -13
View File
@@ -26,23 +26,23 @@ function getHandlePosition(position: Position, node: Node, handle: any | null =
switch (position) { switch (position) {
case Position.Top: case Position.Top:
return { return {
x: node.__rg.width / 2, x: node.__rf.width / 2,
y: 0, y: 0,
}; };
case Position.Right: case Position.Right:
return { return {
x: node.__rg.width, x: node.__rf.width,
y: node.__rg.height / 2, y: node.__rf.height / 2,
}; };
case Position.Bottom: case Position.Bottom:
return { return {
x: node.__rg.width / 2, x: node.__rf.width / 2,
y: node.__rg.height, y: node.__rf.height,
}; };
case Position.Left: case Position.Left:
return { return {
x: 0, x: 0,
y: node.__rg.height / 2, y: node.__rf.height / 2,
}; };
} }
} }
@@ -98,12 +98,12 @@ function getEdgePositions(
targetPosition: Position targetPosition: Position
): EdgePositions { ): EdgePositions {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle); const sourceHandlePos = getHandlePosition(sourcePosition, sourceNode, sourceHandle);
const sourceX = sourceNode.__rg.position.x + sourceHandlePos.x; const sourceX = sourceNode.__rf.position.x + sourceHandlePos.x;
const sourceY = sourceNode.__rg.position.y + sourceHandlePos.y; const sourceY = sourceNode.__rf.position.y + sourceHandlePos.y;
const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle); const targetHandlePos = getHandlePosition(targetPosition, targetNode, targetHandle);
const targetX = targetNode.__rg.position.x + targetHandlePos.x; const targetX = targetNode.__rf.position.x + targetHandlePos.x;
const targetY = targetNode.__rg.position.y + targetHandlePos.y; const targetY = targetNode.__rf.position.y + targetHandlePos.y;
return { return {
sourceX, sourceX,
@@ -134,14 +134,14 @@ function renderEdge(
throw new Error(`couldn't create edge for target id: ${targetId}`); throw new Error(`couldn't create edge for target id: ${targetId}`);
} }
if (!sourceNode.__rg.width || !sourceNode.__rg.height) { if (!sourceNode.__rf.width || !sourceNode.__rf.height) {
return null; return null;
} }
const edgeType = edge.type || 'default'; const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default; const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
const sourceHandle = getHandle(sourceNode.__rg.handleBounds.source, sourceHandleId); const sourceHandle = getHandle(sourceNode.__rf.handleBounds.source, sourceHandleId);
const targetHandle = getHandle(targetNode.__rg.handleBounds.target, targetHandleId); const targetHandle = getHandle(targetNode.__rf.handleBounds.target, targetHandleId);
const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom; const sourcePosition = sourceHandle ? sourceHandle.position : Position.Bottom;
const targetPosition = targetHandle ? targetHandle.position : Position.Top; const targetPosition = targetHandle ? targetHandle.position : Position.Top;
+7 -6
View File
@@ -12,7 +12,7 @@ import useD3Zoom from '../../hooks/useD3Zoom';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler'; import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useElementUpdater from '../../hooks/useElementUpdater'; import useElementUpdater from '../../hooks/useElementUpdater';
import { getDimensions } from '../../utils'; import { getDimensions } from '../../utils';
import { fitView, zoomIn, zoomOut, project, getElements } from '../../utils/graph'; import { project, getElements } from '../../utils/graph';
import { import {
Elements, Elements,
NodeTypesType, NodeTypesType,
@@ -80,7 +80,6 @@ const GraphView = memo(
const height = useStoreState((s) => s.height); const height = useStoreState((s) => s.height);
const d3Initialised = useStoreState((s) => s.d3Initialised); const d3Initialised = useStoreState((s) => s.d3Initialised);
const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive); const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive);
const updateSize = useStoreActions((actions) => actions.updateSize); const updateSize = useStoreActions((actions) => actions.updateSize);
const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection); const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection);
const setOnConnect = useStoreActions((a) => a.setOnConnect); const setOnConnect = useStoreActions((a) => a.setOnConnect);
@@ -88,6 +87,8 @@ const GraphView = memo(
const setInteractive = useStoreActions((actions) => actions.setInteractive); const setInteractive = useStoreActions((actions) => actions.setInteractive);
const updateTransform = useStoreActions((actions) => actions.updateTransform); const updateTransform = useStoreActions((actions) => actions.updateTransform);
const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom); const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);
const fitView = useStoreActions((actions) => actions.fitView);
const zoom = useStoreActions((actions) => actions.zoom);
const selectionKeyPressed = useKeyPress(selectionKeyCode); const selectionKeyPressed = useKeyPress(selectionKeyCode);
const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive }); const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });
@@ -146,9 +147,9 @@ const GraphView = memo(
useEffect(() => { useEffect(() => {
if (d3Initialised && onLoad) { if (d3Initialised && onLoad) {
onLoad({ onLoad({
fitView, fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn, zoomIn: () => zoom(0.2),
zoomOut, zoomOut: () => zoom(-0.2),
project, project,
getElements, getElements,
}); });
@@ -188,7 +189,7 @@ const GraphView = memo(
connectionLineType={connectionLineType} connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle} connectionLineStyle={connectionLineStyle}
/> />
{selectionKeyPressed && <UserSelection isInteractive={isInteractive} />} <UserSelection selectionKeyPressed={selectionKeyPressed} isInteractive={isInteractive} />
{nodesSelectionActive && <NodesSelection />} {nodesSelectionActive && <NodesSelection />}
<div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} /> <div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} />
</div> </div>
+2 -2
View File
@@ -34,8 +34,8 @@ function renderNode(
id={node.id} id={node.id}
type={nodeType} type={nodeType}
data={node.data} data={node.data}
xPos={node.__rg.position.x} xPos={node.__rf.position.x}
yPos={node.__rg.position.y} yPos={node.__rf.position.y}
onClick={props.onElementClick} onClick={props.onElementClick}
onNodeDragStart={props.onNodeDragStart} onNodeDragStart={props.onNodeDragStart}
onNodeDragStop={props.onNodeDragStop} onNodeDragStop={props.onNodeDragStop}
+15 -32
View File
@@ -1,6 +1,5 @@
import { useEffect, MutableRefObject } from 'react'; import { useEffect, MutableRefObject } from 'react';
import { zoom, zoomIdentity } from 'd3-zoom'; import { event } from 'd3-selection';
import { select, event } from 'd3-selection';
import { useStoreState, useStoreActions } from '../store/hooks'; import { useStoreState, useStoreActions } from '../store/hooks';
@@ -11,8 +10,6 @@ interface UseD3ZoomParams {
} }
export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => { export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): void => {
const transform = useStoreState((s) => s.transform);
const d3Selection = useStoreState((s) => s.d3Selection);
const d3Zoom = useStoreState((s) => s.d3Zoom); const d3Zoom = useStoreState((s) => s.d3Zoom);
const initD3 = useStoreActions((actions) => actions.initD3); const initD3 = useStoreActions((actions) => actions.initD3);
@@ -20,41 +17,27 @@ export default ({ zoomPane, onMove, selectionKeyPressed }: UseD3ZoomParams): voi
useEffect(() => { useEffect(() => {
if (zoomPane.current) { if (zoomPane.current) {
const nextD3ZoomInstance = zoom(); initD3(zoomPane.current);
const selection = select(zoomPane.current).call(nextD3ZoomInstance);
initD3({ zoom: nextD3ZoomInstance, selection });
} }
}, []); }, []);
useEffect(() => { useEffect(() => {
if (!d3Zoom) { if (d3Zoom) {
return; if (selectionKeyPressed) {
} d3Zoom.on('zoom', null);
} else {
d3Zoom.on('zoom', function () {
if (!event.sourceEvent || (event.sourceEvent && event.sourceEvent.target !== zoomPane.current)) {
return;
}
if (selectionKeyPressed) { updateTransform(event.transform);
d3Zoom.on('zoom', null);
} else {
d3Zoom.on('zoom', () => {
if (event.sourceEvent && event.sourceEvent.target !== zoomPane.current) {
return;
}
updateTransform(event.transform); if (onMove) {
onMove();
if (onMove) { }
onMove(); });
}
});
if (d3Selection && d3Zoom) {
// we need to restore the graph transform otherwise d3 zoom transform and graph transform are not synced
const graphTransform = zoomIdentity.translate(transform[0], transform[1]).scale(transform[2]);
d3Selection.call(d3Zoom.transform, graphTransform);
} }
} }
return () => {
d3Zoom.on('zoom', null);
};
}, [selectionKeyPressed, d3Zoom]); }, [selectionKeyPressed, d3Zoom]);
}; };
+2 -2
View File
@@ -39,8 +39,8 @@ const useElementUpdater = (elements: Elements): void => {
}; };
if (positionChanged) { if (positionChanged) {
nodeProps.__rg = { nodeProps.__rf = {
...existingNode.__rg, ...existingNode.__rf,
position: propNode.position, position: propNode.position,
}; };
nodeProps.position = propNode.position; nodeProps.position = propNode.position;
+58 -16
View File
@@ -1,6 +1,8 @@
import { createStore, Action, action } from 'easy-peasy'; import { createStore, Action, action, Thunk, thunk } from 'easy-peasy';
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 { zoom, zoomIdentity, zoomTransform } from 'd3-zoom';
import { select } from 'd3-selection';
import { getDimensions } from '../utils'; import { getDimensions } from '../utils';
import { getHandleBounds } from '../components/Nodes/utils'; import { getHandleBounds } from '../components/Nodes/utils';
@@ -20,6 +22,7 @@ import {
HandleType, HandleType,
SetConnectionId, SetConnectionId,
NodePosUpdate, NodePosUpdate,
FitViewParams,
} from '../types'; } from '../types';
type TransformXYK = { type TransformXYK = {
@@ -38,11 +41,6 @@ type SelectionUpdate = {
selection?: SelectionRect; selection?: SelectionRect;
}; };
type D3Init = {
zoom: ZoomBehavior<Element, unknown>;
selection: D3Selection<Element, unknown, null, undefined>;
};
type SetMinMaxZoom = { type SetMinMaxZoom = {
minZoom: number; minZoom: number;
maxZoom: number; maxZoom: number;
@@ -107,7 +105,7 @@ export interface StoreModel {
updateSize: Action<StoreModel, Dimensions>; updateSize: Action<StoreModel, Dimensions>;
initD3: Action<StoreModel, D3Init>; initD3: Action<StoreModel, Element>;
setMinMaxZoom: Action<StoreModel, SetMinMaxZoom>; setMinMaxZoom: Action<StoreModel, SetMinMaxZoom>;
@@ -122,6 +120,11 @@ export interface StoreModel {
setUserSelection: Action<StoreModel, XYPosition>; setUserSelection: Action<StoreModel, XYPosition>;
updateUserSelection: Action<StoreModel, XYPosition>; updateUserSelection: Action<StoreModel, XYPosition>;
unsetUserSelection: Action<StoreModel>; unsetUserSelection: Action<StoreModel>;
fitView: Action<StoreModel, FitViewParams>;
zoom: Action<StoreModel, number>;
zoomIn: Thunk<StoreModel>;
zoomOut: Thunk<StoreModel>;
} }
export const storeModel: StoreModel = { export const storeModel: StoreModel = {
@@ -185,7 +188,7 @@ export const storeModel: StoreModel = {
// only update when size change // only update when size change
if ( if (
!matchingNode || !matchingNode ||
(matchingNode.__rg.width === dimensions.width && matchingNode.__rg.height === dimensions.height) (matchingNode.__rf.width === dimensions.width && matchingNode.__rf.height === dimensions.height)
) { ) {
return; return;
} }
@@ -197,8 +200,8 @@ export const storeModel: StoreModel = {
state.nodes.forEach((n) => { state.nodes.forEach((n) => {
if (n.id === id) { if (n.id === id) {
n.__rg = { n.__rf = {
...n.__rg, ...n.__rf,
...dimensions, ...dimensions,
handleBounds, handleBounds,
}; };
@@ -220,8 +223,8 @@ export const storeModel: StoreModel = {
state.nodes.forEach((n) => { state.nodes.forEach((n) => {
if (n.id === id) { if (n.id === id) {
n.__rg = { n.__rf = {
...n.__rg, ...n.__rf,
position, position,
}; };
} }
@@ -335,11 +338,11 @@ export const storeModel: StoreModel = {
state.height = size.height; state.height = size.height;
}), }),
initD3: action((state, { zoom, selection }) => { initD3: action((state, zoomPaneNode) => {
state.d3Zoom = zoom; const d3ZoomInstance = zoom().scaleExtent([state.minZoom, state.maxZoom]);
const selection = select(zoomPaneNode).call(d3ZoomInstance);
state.d3Zoom.scaleExtent([state.minZoom, state.maxZoom]);
state.d3Zoom = d3ZoomInstance;
state.d3Selection = selection; state.d3Selection = selection;
state.d3Initialised = true; state.d3Initialised = true;
}), }),
@@ -370,6 +373,45 @@ export const storeModel: StoreModel = {
setInteractive: action((state, isInteractive) => { setInteractive: action((state, isInteractive) => {
state.isInteractive = isInteractive; state.isInteractive = isInteractive;
}), }),
fitView: action((state, { padding = 0.1 }) => {
const { nodes, width, height, d3Selection, d3Zoom } = state;
if (!d3Selection || !d3Zoom || !nodes.length) {
return;
}
const bounds = getRectOfNodes(nodes);
const maxBoundsSize = Math.max(bounds.width, bounds.height);
const k = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
const boundsCenterX = bounds.x + bounds.width / 2;
const boundsCenterY = bounds.y + bounds.height / 2;
const transform = [width / 2 - boundsCenterX * k, height / 2 - boundsCenterY * k];
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
d3Selection.call(d3Zoom.transform, fittedTransform);
state.transform = [fittedTransform.x, fittedTransform.y, fittedTransform.k];
}),
zoom: action((state, amount) => {
const { d3Zoom, d3Selection, transform } = state;
const nextZoom = transform[2] + amount;
if (d3Zoom && d3Selection) {
d3Zoom.scaleTo(d3Selection, nextZoom);
const transforms = zoomTransform(d3Selection.node() as Element);
state.transform = [transforms.x, transforms.y, transforms.k];
}
}),
zoomIn: thunk((actions) => {
actions.zoom(0.2);
}),
zoomOut: thunk((actions) => {
actions.zoom(-0.2);
}),
}; };
const store = createStore(storeModel); const store = createStore(storeModel);
+2 -2
View File
@@ -69,6 +69,7 @@
.react-flow__edge-text { .react-flow__edge-text {
font-size: 12px; font-size: 12px;
pointer-events: none; pointer-events: none;
user-select: none;
} }
.react-flow__edge-textbg { .react-flow__edge-textbg {
@@ -132,7 +133,6 @@
.react-flow__node-output { .react-flow__node-output {
background: #55dd99; background: #55dd99;
} }
.react-flow__nodesselection { .react-flow__nodesselection {
@@ -184,4 +184,4 @@
right: 0; right: 0;
top: 50%; top: 50%;
transform: translate(0, -50%); transform: translate(0, -50%);
} }
+2 -2
View File
@@ -34,7 +34,7 @@ export interface Node {
id: ElementId; id: ElementId;
position: XYPosition; position: XYPosition;
type?: string; type?: string;
__rg?: any; __rf?: any;
data?: any; data?: any;
style?: CSSProperties; style?: CSSProperties;
className?: string; className?: string;
@@ -134,7 +134,7 @@ export interface WrapNodeProps {
} }
export type FitViewParams = { export type FitViewParams = {
padding: number; padding?: number;
}; };
export type FitViewFunc = (fitViewOptions: FitViewParams) => void; export type FitViewFunc = (fitViewOptions: FitViewParams) => void;
export type ProjectFunc = (position: XYPosition) => XYPosition; export type ProjectFunc = (position: XYPosition) => XYPosition;
+7 -26
View File
@@ -1,5 +1,3 @@
import { zoomIdentity } from 'd3-zoom';
import store from '../store'; import store from '../store';
import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types'; import { ElementId, Node, Edge, Elements, Transform, XYPosition, Rect, FitViewParams, Box, Connection } from '../types';
@@ -96,7 +94,7 @@ export const parseElement = (element: Node | Edge): Node | Edge => {
...element, ...element,
id: element.id.toString(), id: element.id.toString(),
type: element.type || 'default', type: element.type || 'default',
__rg: { __rf: {
position: element.position, position: element.position,
width: null, width: null,
height: null, height: null,
@@ -131,7 +129,7 @@ export const getBoundsofRects = (rect1: Rect, rect2: Rect): Rect =>
export const getRectOfNodes = (nodes: Node[]): Rect => { export const getRectOfNodes = (nodes: Node[]): Rect => {
const box = nodes.reduce( const box = nodes.reduce(
(currBox, { __rg: { position, width, height } }) => (currBox, { __rf: { position, width, height } }) =>
getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })), getBoundsOfBoxes(currBox, rectToBox({ ...position, width, height })),
{ x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity } { x: Infinity, y: Infinity, x2: -Infinity, y2: -Infinity }
); );
@@ -157,7 +155,7 @@ export const getNodesInside = (
height: rect.height / tScale, height: rect.height / tScale,
}); });
return nodes.filter(({ __rg: { position, width, height } }) => { return nodes.filter(({ __rf: { position, width, height } }) => {
const nBox = rectToBox({ ...position, width, height }); const nBox = rectToBox({ ...position, width, height });
const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x)); const xOverlap = Math.max(0, Math.min(rBox.x2, nBox.x2) - Math.max(rBox.x, nBox.x));
const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y)); const yOverlap = Math.max(0, Math.min(rBox.y2, nBox.y2) - Math.max(rBox.y, nBox.y));
@@ -182,29 +180,12 @@ export const getConnectedEdges = (nodes: Node[], edges: Edge[]): Edge[] => {
}); });
}; };
export const fitView = ({ padding }: FitViewParams = { padding: 0.1 }): void => { export const fitView = (params: FitViewParams = { padding: 0.1 }): void => {
const { nodes, width, height, d3Selection, d3Zoom } = store.getState(); store.getActions().fitView(params);
if (!d3Selection || !d3Zoom || !nodes.length) {
return;
}
const bounds = getRectOfNodes(nodes);
const maxBoundsSize = Math.max(bounds.width, bounds.height);
const k = Math.min(width, height) / (maxBoundsSize + maxBoundsSize * padding);
const boundsCenterX = bounds.x + bounds.width / 2;
const boundsCenterY = bounds.y + bounds.height / 2;
const transform = [width / 2 - boundsCenterX * k, height / 2 - boundsCenterY * k];
const fittedTransform = zoomIdentity.translate(transform[0], transform[1]).scale(k);
d3Selection.call(d3Zoom.transform, fittedTransform);
}; };
const zoom = (amount: number): void => { const zoom = (amount: number): void => {
const { d3Zoom, d3Selection, transform } = store.getState(); store.getActions().zoom(amount);
if (d3Zoom && d3Selection) {
d3Zoom.scaleTo(d3Selection, transform[2] + amount);
}
}; };
export const zoomIn = (): void => zoom(0.2); export const zoomIn = (): void => zoom(0.2);
@@ -218,7 +199,7 @@ export const getElements = (): Elements => {
...nodes.map((node) => { ...nodes.map((node) => {
const n = { ...node }; const n = { ...node };
delete n.__rg; delete n.__rf;
return n; return n;
}), }),
...edges.map((e) => ({ ...e })), ...edges.map((e) => ({ ...e })),