@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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]);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -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);
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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 })),
|
||||||
|
|||||||
Reference in New Issue
Block a user