chore: setup monorepo using preconstruct

This commit is contained in:
Christopher Möller
2022-07-11 18:04:27 +02:00
parent a47f1c1382
commit 1bb32c8eb7
188 changed files with 2590 additions and 288 deletions
@@ -0,0 +1,96 @@
import React, { memo, useCallback } from 'react';
import { useStore } from '../../store';
import { EdgeMarker, ReactFlowState } from '../../types';
import { getMarkerId } from '../../utils/graph';
import { useMarkerSymbol } from './MarkerSymbols';
interface MarkerProps extends EdgeMarker {
id: string;
}
interface MarkerDefinitionsProps {
defaultColor: string;
rfId?: string;
}
const Marker = ({
id,
type,
color,
width = 12.5,
height = 12.5,
markerUnits = 'strokeWidth',
strokeWidth,
orient = 'auto',
}: MarkerProps) => {
const Symbol = useMarkerSymbol(type);
return (
<marker
className="react-flow__arrowhead"
id={id}
markerWidth={`${width}`}
markerHeight={`${height}`}
viewBox="-10 -10 20 20"
markerUnits={markerUnits}
orient={orient}
refX="0"
refY="0"
>
<Symbol color={color} strokeWidth={strokeWidth} />
</marker>
);
};
const markerSelector =
({ defaultColor, rfId }: { defaultColor: string; rfId?: string }) =>
(s: ReactFlowState) => {
const ids: string[] = [];
return s.edges
.reduce<MarkerProps[]>((markers, edge) => {
[edge.markerStart, edge.markerEnd].forEach((marker) => {
if (marker && typeof marker === 'object') {
const markerId = getMarkerId(marker, rfId);
if (!ids.includes(markerId)) {
markers.push({ id: markerId, color: marker.color || defaultColor, ...marker });
ids.push(markerId);
}
}
});
return markers;
}, [])
.sort((a, b) => a.id.localeCompare(b.id));
};
// when you have multiple flows on a page and you hide the first one, the other ones have no markers anymore
// when they do have markers with the same ids. To prevent this the user can pass a unique id to the react flow wrapper
// that we can then use for creating our unique marker ids
const MarkerDefinitions = ({ defaultColor, rfId }: MarkerDefinitionsProps) => {
const markers = useStore(
useCallback(markerSelector({ defaultColor, rfId }), [defaultColor, rfId]),
// the id includes all marker options, so we just need to look at that part of the marker
(a, b) => !(a.length !== b.length || a.some((m, i) => m.id !== b[i].id))
);
return (
<defs>
{markers.map((marker: MarkerProps) => (
<Marker
id={marker.id}
key={marker.id}
type={marker.type}
color={marker.color}
width={marker.width}
height={marker.height}
markerUnits={marker.markerUnits}
strokeWidth={marker.strokeWidth}
orient={marker.orient}
/>
))}
</defs>
);
};
MarkerDefinitions.displayName = 'MarkerDefinitions';
export default memo(MarkerDefinitions);
@@ -0,0 +1,54 @@
import React, { useMemo } from 'react';
import { MarkerType, EdgeMarker } from '../../types';
type SymbolProps = Omit<EdgeMarker, 'type'>;
const ArrowSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill="none"
points="-5,-4 0,0 -5,4"
/>
);
};
const ArrowClosedSymbol = ({ color = 'none', strokeWidth = 1 }: SymbolProps) => {
return (
<polyline
stroke={color}
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={strokeWidth}
fill={color}
points="-5,-4 0,0 -5,4 -5,-4"
/>
);
};
export const MarkerSymbols = {
[MarkerType.Arrow]: ArrowSymbol,
[MarkerType.ArrowClosed]: ArrowClosedSymbol,
};
export function useMarkerSymbol(type: MarkerType) {
const symbol = useMemo(() => {
const symbolExists = MarkerSymbols.hasOwnProperty(type);
if (!symbolExists) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(`[React Flow]: Marker type "${type}" doesn't exist. Help: https://reactflow.dev/error#900`);
}
return () => null;
}
return MarkerSymbols[type];
}, [type]);
return symbol;
}
export default MarkerSymbols;
@@ -0,0 +1,221 @@
import React, { memo, CSSProperties } from 'react';
import shallow from 'zustand/shallow';
import cc from 'classcat';
import { useStore } from '../../store';
import ConnectionLine from '../../components/ConnectionLine/index';
import MarkerDefinitions from './MarkerDefinitions';
import { getEdgePositions, getHandle, getNodeData } from './utils';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import {
Position,
Edge,
ConnectionLineType,
ConnectionLineComponent,
ConnectionMode,
OnEdgeUpdateFunc,
HandleType,
ReactFlowState,
EdgeTypesWrapped,
} from '../../types';
interface EdgeRendererProps {
edgeTypes: EdgeTypesWrapped;
connectionLineType: ConnectionLineType;
connectionLineStyle?: CSSProperties;
connectionLineComponent?: ConnectionLineComponent;
connectionLineContainerStyle?: CSSProperties;
onEdgeClick?: (event: React.MouseEvent, node: Edge) => void;
onEdgeDoubleClick?: (event: React.MouseEvent, edge: Edge) => void;
defaultMarkerColor: string;
onlyRenderVisibleElements: boolean;
onEdgeUpdate?: OnEdgeUpdateFunc;
onEdgeContextMenu?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseEnter?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseMove?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeMouseLeave?: (event: React.MouseEvent, edge: Edge) => void;
onEdgeUpdateStart?: (event: React.MouseEvent, edge: Edge, handleType: HandleType) => void;
onEdgeUpdateEnd?: (event: MouseEvent, edge: Edge, handleType: HandleType) => void;
edgeUpdaterRadius?: number;
noPanClassName?: string;
elevateEdgesOnSelect: boolean;
rfId?: string;
}
const selector = (s: ReactFlowState) => ({
connectionNodeId: s.connectionNodeId,
connectionHandleId: s.connectionHandleId,
connectionHandleType: s.connectionHandleType,
connectionPosition: s.connectionPosition,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
width: s.width,
height: s.height,
connectionMode: s.connectionMode,
nodeInternals: s.nodeInternals,
});
const EdgeRenderer = (props: EdgeRendererProps) => {
const {
connectionNodeId,
connectionHandleId,
connectionHandleType,
connectionPosition,
nodesConnectable,
elementsSelectable,
width,
height,
connectionMode,
nodeInternals,
} = useStore(selector, shallow);
const edgeTree = useVisibleEdges(props.onlyRenderVisibleElements, nodeInternals, props.elevateEdgesOnSelect);
if (!width) {
return null;
}
const {
connectionLineType,
defaultMarkerColor,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
} = props;
const renderConnectionLine = connectionNodeId && connectionHandleType;
return (
<>
{edgeTree.map(({ level, edges, isMaxLevel }) => (
<svg
key={level}
style={{ zIndex: level }}
width={width}
height={height}
className="react-flow__edges react-flow__container"
>
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={props.rfId} />}
<g>
{edges.map((edge: Edge) => {
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals, edge.source);
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals, edge.target);
if (!sourceIsValid || !targetIsValid) {
return null;
}
const edgeType = edge.type || 'default';
const EdgeComponent = props.edgeTypes[edgeType] || props.edgeTypes.default;
// when connection type is loose we can define all handles as sources
const targetNodeHandles =
connectionMode === ConnectionMode.Strict
? targetHandleBounds!.target
: targetHandleBounds!.target || targetHandleBounds!.source;
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle || null);
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle || null);
const sourcePosition = sourceHandle?.position || Position.Bottom;
const targetPosition = targetHandle?.position || Position.Top;
if (!sourceHandle) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for source handle id: ${edge.sourceHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
return null;
}
if (!targetHandle) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Couldn't create edge for target handle id: ${edge.targetHandle}; edge id: ${edge.id}. Help: https://reactflow.dev/error#800`
);
}
return null;
}
const { sourceX, sourceY, targetX, targetY } = getEdgePositions(
sourceNodeRect,
sourceHandle,
sourcePosition,
targetNodeRect,
targetHandle,
targetPosition
);
return (
<EdgeComponent
key={edge.id}
id={edge.id}
className={cc([edge.className, props.noPanClassName])}
type={edgeType}
data={edge.data}
selected={!!edge.selected}
animated={!!edge.animated}
hidden={!!edge.hidden}
label={edge.label}
labelStyle={edge.labelStyle}
labelShowBg={edge.labelShowBg}
labelBgStyle={edge.labelBgStyle}
labelBgPadding={edge.labelBgPadding}
labelBgBorderRadius={edge.labelBgBorderRadius}
style={edge.style}
source={edge.source}
target={edge.target}
sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle}
markerEnd={edge.markerEnd}
markerStart={edge.markerStart}
sourceX={sourceX}
sourceY={sourceY}
targetX={targetX}
targetY={targetY}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
elementsSelectable={elementsSelectable}
onEdgeUpdate={props.onEdgeUpdate}
onContextMenu={props.onEdgeContextMenu}
onMouseEnter={props.onEdgeMouseEnter}
onMouseMove={props.onEdgeMouseMove}
onMouseLeave={props.onEdgeMouseLeave}
onClick={props.onEdgeClick}
edgeUpdaterRadius={props.edgeUpdaterRadius}
onEdgeDoubleClick={props.onEdgeDoubleClick}
onEdgeUpdateStart={props.onEdgeUpdateStart}
onEdgeUpdateEnd={props.onEdgeUpdateEnd}
rfId={props.rfId}
/>
);
})}
</g>
</svg>
))}
{renderConnectionLine && (
<svg
style={connectionLineContainerStyle}
width={width}
height={height}
className="react-flow__edges react-flow__connectionline react-flow__container"
>
<ConnectionLine
connectionNodeId={connectionNodeId!}
connectionHandleId={connectionHandleId}
connectionHandleType={connectionHandleType!}
connectionPositionX={connectionPosition.x}
connectionPositionY={connectionPosition.y}
connectionLineStyle={connectionLineStyle}
connectionLineType={connectionLineType}
isConnectable={nodesConnectable}
CustomConnectionLineComponent={connectionLineComponent}
/>
</svg>
)}
</>
);
};
EdgeRenderer.displayName = 'EdgeRenderer';
export default memo(EdgeRenderer);
@@ -0,0 +1,191 @@
import { ComponentType } from 'react';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import {
EdgeProps,
EdgeTypes,
EdgeTypesWrapped,
HandleElement,
NodeHandleBounds,
NodeInternals,
Position,
Rect,
Transform,
XYPosition,
} from '../../types';
import { internalsSymbol, rectToBox } from '../../utils';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypesWrapped = {
default: wrapEdge((edgeTypes.default || BezierEdge) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdge) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdge) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdge) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdge) as ComponentType<EdgeProps>),
};
const wrappedTypes = {} as EdgeTypesWrapped;
const specialTypes: EdgeTypesWrapped = Object.keys(edgeTypes)
.filter((k) => !['default', 'bezier'].includes(k))
.reduce((res, key) => {
res[key] = wrapEdge((edgeTypes[key] || BezierEdge) as ComponentType<EdgeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
export function getHandlePosition(position: Position, nodeRect: Rect, handle: any | null = null): XYPosition {
const x = (handle?.x || 0) + nodeRect.x;
const y = (handle?.y || 0) + nodeRect.y;
const width = handle?.width || nodeRect.width;
const height = handle?.height || nodeRect.height;
switch (position) {
case Position.Top:
return {
x: x + width / 2,
y,
};
case Position.Right:
return {
x: x + width,
y: y + height / 2,
};
case Position.Bottom:
return {
x: x + width / 2,
y: y + height,
};
case Position.Left:
return {
x,
y: y + height / 2,
};
}
}
export function getHandle(bounds: HandleElement[], handleId: string | null): HandleElement | null {
if (!bounds) {
return null;
}
// there is no handleId when there are no multiple handles/ handles with ids
// so we just pick the first one
let handle = null;
if (bounds.length === 1 || !handleId) {
handle = bounds[0];
} else if (handleId) {
handle = bounds.find((d) => d.id === handleId);
}
return typeof handle === 'undefined' ? null : handle;
}
interface EdgePositions {
sourceX: number;
sourceY: number;
targetX: number;
targetY: number;
}
export const getEdgePositions = (
sourceNodeRect: Rect,
sourceHandle: HandleElement | unknown,
sourcePosition: Position,
targetNodeRect: Rect,
targetHandle: HandleElement | unknown,
targetPosition: Position
): EdgePositions => {
const sourceHandlePos = getHandlePosition(sourcePosition, sourceNodeRect, sourceHandle);
const targetHandlePos = getHandlePosition(targetPosition, targetNodeRect, targetHandle);
return {
sourceX: sourceHandlePos.x,
sourceY: sourceHandlePos.y,
targetX: targetHandlePos.x,
targetY: targetHandlePos.y,
};
};
interface IsEdgeVisibleParams {
sourcePos: XYPosition;
targetPos: XYPosition;
sourceWidth: number;
sourceHeight: number;
targetWidth: number;
targetHeight: number;
width: number;
height: number;
transform: Transform;
}
export function isEdgeVisible({
sourcePos,
targetPos,
sourceWidth,
sourceHeight,
targetWidth,
targetHeight,
width,
height,
transform,
}: IsEdgeVisibleParams): boolean {
const edgeBox = {
x: Math.min(sourcePos.x, targetPos.x),
y: Math.min(sourcePos.y, targetPos.y),
x2: Math.max(sourcePos.x + sourceWidth, targetPos.x + targetWidth),
y2: Math.max(sourcePos.y + sourceHeight, targetPos.y + targetHeight),
};
if (edgeBox.x === edgeBox.x2) {
edgeBox.x2 += 1;
}
if (edgeBox.y === edgeBox.y2) {
edgeBox.y2 += 1;
}
const viewBox = rectToBox({
x: (0 - transform[0]) / transform[2],
y: (0 - transform[1]) / transform[2],
width: width / transform[2],
height: height / transform[2],
});
const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x));
const yOverlap = Math.max(0, Math.min(viewBox.y2, edgeBox.y2) - Math.max(viewBox.y, edgeBox.y));
const overlappingArea = Math.ceil(xOverlap * yOverlap);
return overlappingArea > 0;
}
export function getNodeData(nodeInternals: NodeInternals, nodeId: string): [Rect, NodeHandleBounds | null, boolean] {
const node = nodeInternals.get(nodeId);
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
const isInvalid =
!node ||
!handleBounds ||
!node.width ||
!node.height ||
typeof node.positionAbsolute?.x === 'undefined' ||
typeof node.positionAbsolute?.y === 'undefined';
return [
{
x: node?.positionAbsolute?.x || 0,
y: node?.positionAbsolute?.y || 0,
width: node?.width || 0,
height: node?.height || 0,
},
handleBounds,
!isInvalid,
];
}
@@ -0,0 +1,124 @@
import React, { memo, ReactNode, WheelEvent, MouseEvent } from 'react';
import shallow from 'zustand/shallow';
import { useStore, useStoreApi } from '../../store';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useKeyPress from '../../hooks/useKeyPress';
import { GraphViewProps } from '../GraphView';
import ZoomPane from '../ZoomPane';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import { ReactFlowState } from '../../types';
export type FlowRendererProps = Omit<
GraphViewProps,
| 'snapToGrid'
| 'nodeTypes'
| 'edgeTypes'
| 'snapGrid'
| 'connectionLineType'
| 'connectionLineContainerStyle'
| 'arrowHeadColor'
| 'onlyRenderVisibleElements'
| 'selectNodesOnDrag'
| 'defaultMarkerColor'
> & {
children: ReactNode;
};
const selector = (s: ReactFlowState) => ({
resetSelectedElements: s.resetSelectedElements,
nodesSelectionActive: s.nodesSelectionActive,
});
const FlowRenderer = ({
children,
onPaneClick,
onPaneContextMenu,
onPaneScroll,
deleteKeyCode,
onMove,
onMoveStart,
onMoveEnd,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
elementsSelectable,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
defaultPosition,
defaultZoom,
preventScrolling,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
noWheelClassName,
noPanClassName,
}: FlowRendererProps) => {
const store = useStoreApi();
const { resetSelectedElements, nodesSelectionActive } = useStore(selector, shallow);
const selectionKeyPressed = useKeyPress(selectionKeyCode);
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
const onClick = (event: MouseEvent) => {
onPaneClick?.(event);
resetSelectedElements();
store.setState({ nodesSelectionActive: false });
};
const onContextMenu = (event: MouseEvent) => onPaneContextMenu?.(event);
const onWheel = (event: WheelEvent) => onPaneScroll?.(event);
return (
<ZoomPane
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
selectionKeyPressed={selectionKeyPressed}
elementsSelectable={elementsSelectable}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
zoomActivationKeyCode={zoomActivationKeyCode}
preventScrolling={preventScrolling}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
{children}
<UserSelection selectionKeyPressed={selectionKeyPressed} />
{nodesSelectionActive && (
<NodesSelection
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
noPanClassName={noPanClassName}
/>
)}
<div
className="react-flow__pane react-flow__container"
onClick={onClick}
onContextMenu={onContextMenu}
onWheel={onWheel}
/>
</ZoomPane>
);
};
FlowRenderer.displayName = 'FlowRenderer';
export default memo(FlowRenderer);
@@ -0,0 +1,168 @@
import React, { memo } from 'react';
import FlowRenderer from '../FlowRenderer';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import Viewport from '../Viewport';
import useOnInitHandler from '../../hooks/useOnInitHandler';
import { NodeTypesWrapped, EdgeTypesWrapped, ConnectionLineType, KeyCode, ReactFlowProps } from '../../types';
export interface GraphViewProps
extends Omit<ReactFlowProps, 'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes'> {
nodeTypes: NodeTypesWrapped;
edgeTypes: EdgeTypesWrapped;
selectionKeyCode: KeyCode | null;
deleteKeyCode: KeyCode | null;
multiSelectionKeyCode: KeyCode | null;
connectionLineType: ConnectionLineType;
onlyRenderVisibleElements: boolean;
defaultZoom: number;
defaultPosition: [number, number];
defaultMarkerColor: string;
selectNodesOnDrag: boolean;
noDragClassName: string;
noWheelClassName: string;
noPanClassName: string;
}
const GraphView = ({
nodeTypes,
edgeTypes,
onMove,
onMoveStart,
onMoveEnd,
onInit,
onNodeClick,
onEdgeClick,
onNodeDoubleClick,
onEdgeDoubleClick,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
onNodeContextMenu,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionLineType,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
selectionKeyCode,
multiSelectionKeyCode,
zoomActivationKeyCode,
deleteKeyCode,
onlyRenderVisibleElements,
elementsSelectable,
selectNodesOnDrag,
defaultZoom,
defaultPosition,
preventScrolling,
defaultMarkerColor,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
panOnScrollSpeed,
panOnScrollMode,
zoomOnDoubleClick,
panOnDrag,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
onEdgeUpdate,
onEdgeContextMenu,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
edgeUpdaterRadius,
onEdgeUpdateStart,
onEdgeUpdateEnd,
noDragClassName,
noWheelClassName,
noPanClassName,
elevateEdgesOnSelect,
id,
}: GraphViewProps) => {
useOnInitHandler(onInit);
return (
<FlowRenderer
onPaneClick={onPaneClick}
onPaneContextMenu={onPaneContextMenu}
onPaneScroll={onPaneScroll}
deleteKeyCode={deleteKeyCode}
selectionKeyCode={selectionKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
elementsSelectable={elementsSelectable}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
panOnDrag={panOnDrag}
defaultPosition={defaultPosition}
defaultZoom={defaultZoom}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
preventScrolling={preventScrolling}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
>
<Viewport>
<EdgeRenderer
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
onEdgeUpdate={onEdgeUpdate}
onlyRenderVisibleElements={onlyRenderVisibleElements}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noPanClassName={noPanClassName}
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
rfId={id}
/>
<NodeRenderer
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDragStop={onNodeDragStop}
onNodeDrag={onNodeDrag}
onNodeDragStart={onNodeDragStart}
selectNodesOnDrag={selectNodesOnDrag}
onlyRenderVisibleElements={onlyRenderVisibleElements}
noPanClassName={noPanClassName}
noDragClassName={noDragClassName}
/>
</Viewport>
</FlowRenderer>
);
};
GraphView.displayName = 'GraphView';
export default memo(GraphView);
@@ -0,0 +1,135 @@
import React, { memo, useMemo, ComponentType, useEffect, useRef } from 'react';
import shallow from 'zustand/shallow';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import { useStore } from '../../store';
import {
NodeDragHandler,
NodeMouseHandler,
NodeTypesWrapped,
Position,
ReactFlowState,
WrapNodeProps,
} from '../../types';
import { internalsSymbol } from '../../utils';
interface NodeRendererProps {
nodeTypes: NodeTypesWrapped;
selectNodesOnDrag: boolean;
onNodeClick?: NodeMouseHandler;
onNodeDoubleClick?: NodeMouseHandler;
onNodeMouseEnter?: NodeMouseHandler;
onNodeMouseMove?: NodeMouseHandler;
onNodeMouseLeave?: NodeMouseHandler;
onNodeContextMenu?: NodeMouseHandler;
onNodeDragStart?: NodeDragHandler;
onNodeDrag?: NodeDragHandler;
onNodeDragStop?: NodeDragHandler;
onlyRenderVisibleElements: boolean;
noPanClassName: string;
noDragClassName: string;
}
const selector = (s: ReactFlowState) => ({
nodesDraggable: s.nodesDraggable,
nodesConnectable: s.nodesConnectable,
elementsSelectable: s.elementsSelectable,
updateNodeDimensions: s.updateNodeDimensions,
});
const NodeRenderer = (props: NodeRendererProps) => {
const { nodesDraggable, nodesConnectable, elementsSelectable, updateNodeDimensions } = useStore(selector, shallow);
const nodes = useVisibleNodes(props.onlyRenderVisibleElements);
const resizeObserverRef = useRef<ResizeObserver>();
const resizeObserver = useMemo(() => {
if (typeof ResizeObserver === 'undefined') {
return null;
}
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = entries.map((entry: ResizeObserverEntry) => ({
id: entry.target.getAttribute('data-id') as string,
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}));
updateNodeDimensions(updates);
});
resizeObserverRef.current = observer;
return observer;
}, []);
useEffect(() => {
return () => {
resizeObserverRef?.current?.disconnect();
};
}, []);
return (
<div className="react-flow__nodes react-flow__container">
{nodes.map((node) => {
let nodeType = node.type || 'default';
if (!props.nodeTypes[nodeType]) {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
console.warn(
`[React Flow]: Node type "${nodeType}" not found. Using fallback type "default". Help: https://reactflow.dev/error#300`
);
}
nodeType = 'default';
}
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined'));
const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined'));
const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined'));
return (
<NodeComponent
key={node.id}
id={node.id}
className={node.className}
style={node.style}
type={nodeType}
data={node.data}
sourcePosition={node.sourcePosition || Position.Bottom}
targetPosition={node.targetPosition || Position.Top}
hidden={node.hidden}
xPos={node.positionAbsolute?.x ?? 0}
yPos={node.positionAbsolute?.y ?? 0}
selectNodesOnDrag={props.selectNodesOnDrag}
onClick={props.onNodeClick}
onMouseEnter={props.onNodeMouseEnter}
onMouseMove={props.onNodeMouseMove}
onMouseLeave={props.onNodeMouseLeave}
onContextMenu={props.onNodeContextMenu}
onDoubleClick={props.onNodeDoubleClick}
onDragStart={props.onNodeDragStart}
onDrag={props.onNodeDrag}
onDragStop={props.onNodeDragStop}
selected={!!node.selected}
isDraggable={isDraggable}
isSelectable={isSelectable}
isConnectable={isConnectable}
resizeObserver={resizeObserver}
dragHandle={node.dragHandle}
zIndex={node[internalsSymbol]?.z ?? 0}
isParent={!!node[internalsSymbol]?.isParent}
noDragClassName={props.noDragClassName}
noPanClassName={props.noPanClassName}
initialized={!!node.width && !!node.height}
/>
);
})}
</div>
);
};
NodeRenderer.displayName = 'NodeRenderer';
export default memo(NodeRenderer);
@@ -0,0 +1,33 @@
import { ComponentType } from 'react';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import GroupNode from '../../components/Nodes/GroupNode';
import wrapNode from '../../components/Nodes/wrapNode';
import { NodeTypes, NodeProps, NodeTypesWrapped } from '../../types';
export type CreateNodeTypes = (nodeTypes: NodeTypes) => NodeTypesWrapped;
export function createNodeTypes(nodeTypes: NodeTypes): NodeTypesWrapped {
const standardTypes: NodeTypesWrapped = {
input: wrapNode((nodeTypes.input || InputNode) as ComponentType<NodeProps>),
default: wrapNode((nodeTypes.default || DefaultNode) as ComponentType<NodeProps>),
output: wrapNode((nodeTypes.output || OutputNode) as ComponentType<NodeProps>),
group: wrapNode((nodeTypes.group || GroupNode) as ComponentType<NodeProps>),
};
const wrappedTypes = {} as NodeTypesWrapped;
const specialTypes: NodeTypesWrapped = Object.keys(nodeTypes)
.filter((k) => !['input', 'default', 'output', 'group'].includes(k))
.reduce((res, key) => {
res[key] = wrapNode((nodeTypes[key] || DefaultNode) as ComponentType<NodeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -0,0 +1,25 @@
import React, { FC, PropsWithChildren } from 'react';
import { Provider, createStore, useStoreApi } from '../../store';
const Wrapper: FC<PropsWithChildren<{}>> = ({ children }) => {
let isWrapped = true;
try {
useStoreApi();
} catch (e) {
isWrapped = false;
}
if (isWrapped) {
// we need to wrap it with a fragment because it's not allowed for children to be a ReactNode
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/18051
return <>{children}</>;
}
return <Provider createStore={createStore}>{children}</Provider>;
};
Wrapper.displayName = 'ReactFlowWrapper';
export default Wrapper;
@@ -0,0 +1,258 @@
import cc from 'classcat';
import React, { forwardRef } from 'react';
import Attribution from '../../components/Attribution';
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
import DefaultNode from '../../components/Nodes/DefaultNode';
import InputNode from '../../components/Nodes/InputNode';
import OutputNode from '../../components/Nodes/OutputNode';
import SelectionListener from '../../components/SelectionListener';
import StoreUpdater from '../../components/StoreUpdater';
// import css from '../../style.css';
// import theme from '../../theme-default.css';
import {
ConnectionLineType,
ConnectionMode,
EdgeTypes,
EdgeTypesWrapped,
NodeTypes,
NodeTypesWrapped,
PanOnScrollMode,
ReactFlowProps,
ReactFlowRefType,
} from '../../types';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import GraphView from '../GraphView';
import { createNodeTypes } from '../NodeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
import Wrapper from './Wrapper';
// if (__INJECT_STYLES__) {
// injectStyle(css as unknown as string);
// injectStyle(theme as unknown as string);
// }
const defaultNodeTypes: NodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
};
const defaultEdgeTypes: EdgeTypes = {
default: BezierEdge,
straight: StraightEdge,
step: StepEdge,
smoothstep: SmoothStepEdge,
simplebezier: SimpleBezierEdge,
};
const initSnapGrid: [number, number] = [15, 15];
const initDefaultPosition: [number, number] = [0, 0];
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
(
{
nodes,
edges,
defaultNodes,
defaultEdges,
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
onNodeClick,
onEdgeClick,
onInit,
onMove,
onMoveStart,
onMoveEnd,
onConnect,
onConnectStart,
onConnectStop,
onConnectEnd,
onClickConnectStart,
onClickConnectStop,
onClickConnectEnd,
onNodeMouseEnter,
onNodeMouseMove,
onNodeMouseLeave,
onNodeContextMenu,
onNodeDoubleClick,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
onNodesDelete,
onEdgesDelete,
onSelectionChange,
onSelectionDragStart,
onSelectionDrag,
onSelectionDragStop,
onSelectionContextMenu,
connectionMode = ConnectionMode.Strict,
connectionLineType = ConnectionLineType.Bezier,
connectionLineStyle,
connectionLineComponent,
connectionLineContainerStyle,
deleteKeyCode = 'Backspace',
selectionKeyCode = 'Shift',
multiSelectionKeyCode = 'Meta',
zoomActivationKeyCode = 'Meta',
snapToGrid = false,
snapGrid = initSnapGrid,
onlyRenderVisibleElements = false,
selectNodesOnDrag = true,
nodesDraggable,
nodesConnectable,
elementsSelectable,
minZoom,
maxZoom,
defaultZoom = 1,
defaultPosition = initDefaultPosition,
translateExtent,
preventScrolling = true,
nodeExtent,
defaultMarkerColor = '#b1b1b7',
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
panOnDrag = true,
onPaneClick,
onPaneScroll,
onPaneContextMenu,
children,
onEdgeUpdate,
onEdgeContextMenu,
onEdgeDoubleClick,
onEdgeMouseEnter,
onEdgeMouseMove,
onEdgeMouseLeave,
onEdgeUpdateStart,
onEdgeUpdateEnd,
edgeUpdaterRadius = 10,
onNodesChange,
onEdgesChange,
noDragClassName = 'nodrag',
noWheelClassName = 'nowheel',
noPanClassName = 'nopan',
fitView = false,
fitViewOptions,
connectOnClick = true,
attributionPosition,
proOptions,
defaultEdgeOptions,
elevateEdgesOnSelect = false,
...rest
},
ref
) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
const reactFlowClasses = cc(['react-flow', className]);
return (
<div {...rest} ref={ref} className={reactFlowClasses}>
<Wrapper>
<GraphView
onInit={onInit}
onMove={onMove}
onMoveStart={onMoveStart}
onMoveEnd={onMoveEnd}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onNodeMouseEnter={onNodeMouseEnter}
onNodeMouseMove={onNodeMouseMove}
onNodeMouseLeave={onNodeMouseLeave}
onNodeContextMenu={onNodeContextMenu}
onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStart={onNodeDragStart}
onNodeDrag={onNodeDrag}
onNodeDragStop={onNodeDragStop}
nodeTypes={nodeTypesWrapped}
edgeTypes={edgeTypesWrapped}
connectionLineType={connectionLineType}
connectionLineStyle={connectionLineStyle}
connectionLineComponent={connectionLineComponent}
connectionLineContainerStyle={connectionLineContainerStyle}
selectionKeyCode={selectionKeyCode}
deleteKeyCode={deleteKeyCode}
multiSelectionKeyCode={multiSelectionKeyCode}
zoomActivationKeyCode={zoomActivationKeyCode}
onlyRenderVisibleElements={onlyRenderVisibleElements}
selectNodesOnDrag={selectNodesOnDrag}
defaultZoom={defaultZoom}
defaultPosition={defaultPosition}
preventScrolling={preventScrolling}
zoomOnScroll={zoomOnScroll}
zoomOnPinch={zoomOnPinch}
zoomOnDoubleClick={zoomOnDoubleClick}
panOnScroll={panOnScroll}
panOnScrollSpeed={panOnScrollSpeed}
panOnScrollMode={panOnScrollMode}
panOnDrag={panOnDrag}
onPaneClick={onPaneClick}
onPaneScroll={onPaneScroll}
onPaneContextMenu={onPaneContextMenu}
onSelectionDragStart={onSelectionDragStart}
onSelectionDrag={onSelectionDrag}
onSelectionDragStop={onSelectionDragStop}
onSelectionContextMenu={onSelectionContextMenu}
onEdgeUpdate={onEdgeUpdate}
onEdgeContextMenu={onEdgeContextMenu}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeMouseEnter={onEdgeMouseEnter}
onEdgeMouseMove={onEdgeMouseMove}
onEdgeMouseLeave={onEdgeMouseLeave}
onEdgeUpdateStart={onEdgeUpdateStart}
onEdgeUpdateEnd={onEdgeUpdateEnd}
edgeUpdaterRadius={edgeUpdaterRadius}
defaultMarkerColor={defaultMarkerColor}
noDragClassName={noDragClassName}
noWheelClassName={noWheelClassName}
noPanClassName={noPanClassName}
elevateEdgesOnSelect={elevateEdgesOnSelect}
id={rest?.id}
/>
<StoreUpdater
nodes={nodes}
edges={edges}
defaultNodes={defaultNodes}
defaultEdges={defaultEdges}
onConnect={onConnect}
onConnectStart={onConnectStart}
onConnectStop={onConnectStop}
onConnectEnd={onConnectEnd}
onClickConnectStart={onClickConnectStart}
onClickConnectStop={onClickConnectStop}
onClickConnectEnd={onClickConnectEnd}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
elementsSelectable={elementsSelectable}
minZoom={minZoom}
maxZoom={maxZoom}
nodeExtent={nodeExtent}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
snapToGrid={snapToGrid}
snapGrid={snapGrid}
connectionMode={connectionMode}
translateExtent={translateExtent}
connectOnClick={connectOnClick}
defaultEdgeOptions={defaultEdgeOptions}
fitView={fitView}
fitViewOptions={fitViewOptions}
onNodesDelete={onNodesDelete}
onEdgesDelete={onEdgesDelete}
/>
{onSelectionChange && <SelectionListener onSelectionChange={onSelectionChange} />}
{children}
<Attribution proOptions={proOptions} position={attributionPosition} />
</Wrapper>
</div>
);
}
);
ReactFlow.displayName = 'ReactFlow';
export default ReactFlow;
@@ -0,0 +1,40 @@
import { useMemo, useRef } from 'react';
import shallow from 'zustand/shallow';
import { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
const typesKeysRef = useRef<string[] | null>(null);
const typesParsed = useMemo(() => {
// @ts-ignore
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
console.warn(
"[React Flow]: It looks like you have created a new nodeTypes or edgeTypes object. If this wasn't on purpose please define the nodeTypes/edgeTypes outside of the component or memoize them. Help: https://reactflow.dev/error#200"
);
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
export function injectStyle(css: string): void {
if (!css || typeof document === 'undefined') return;
const head = document.head || document.getElementsByTagName('head')[0];
const style = document.createElement('style');
head.appendChild(style);
style.appendChild(document.createTextNode(css));
}
@@ -0,0 +1,25 @@
import React, { ReactNode } from 'react';
import { useStore } from '../../store';
import { ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => s.transform;
type ViewportProps = {
children: ReactNode;
};
function Viewport({ children }: ViewportProps) {
const transform = useStore(selector);
return (
<div
className="react-flow__viewport react-flow__container"
style={{ transform: `translate(${transform[0]}px,${transform[1]}px) scale(${transform[2]})` }}
>
{children}
</div>
);
}
export default Viewport;
@@ -0,0 +1,264 @@
import React, { useEffect, useRef } from 'react';
import { D3ZoomEvent, zoom, zoomIdentity } from 'd3-zoom';
import { select, pointer } from 'd3-selection';
import shallow from 'zustand/shallow';
import { clamp } from '../../utils';
import useKeyPress from '../../hooks/useKeyPress';
import useResizeHandler from '../../hooks/useResizeHandler';
import { useStore, useStoreApi } from '../../store';
import { Viewport, PanOnScrollMode, ReactFlowState } from '../../types';
import { FlowRendererProps } from '../FlowRenderer';
type ZoomPaneProps = Omit<
FlowRendererProps,
'deleteKeyCode' | 'selectionKeyCode' | 'multiSelectionKeyCode' | 'noDragClassName'
> & { selectionKeyPressed: boolean };
const viewChanged = (prevViewport: Viewport, eventViewport: any): boolean =>
prevViewport.x !== eventViewport.x || prevViewport.y !== eventViewport.y || prevViewport.zoom !== eventViewport.k;
const eventToFlowTransform = (eventViewport: any): Viewport => ({
x: eventViewport.x,
y: eventViewport.y,
zoom: eventViewport.k,
});
const isWrappedWithClass = (event: any, className: string | undefined) => event.target.closest(`.${className}`);
const selector = (s: ReactFlowState) => ({
d3Zoom: s.d3Zoom,
d3Selection: s.d3Selection,
d3ZoomHandler: s.d3ZoomHandler,
});
const ZoomPane = ({
onMove,
onMoveStart,
onMoveEnd,
zoomOnScroll = true,
zoomOnPinch = true,
panOnScroll = false,
panOnScrollSpeed = 0.5,
panOnScrollMode = PanOnScrollMode.Free,
zoomOnDoubleClick = true,
selectionKeyPressed,
elementsSelectable,
panOnDrag = true,
defaultPosition = [0, 0],
defaultZoom = 1,
zoomActivationKeyCode,
preventScrolling = true,
children,
noWheelClassName,
noPanClassName,
}: ZoomPaneProps) => {
const store = useStoreApi();
const zoomPane = useRef<HTMLDivElement>(null);
const prevTransform = useRef<Viewport>({ x: 0, y: 0, zoom: 0 });
const { d3Zoom, d3Selection, d3ZoomHandler } = useStore(selector, shallow);
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
useResizeHandler(zoomPane);
useEffect(() => {
if (zoomPane.current) {
const { minZoom, maxZoom, translateExtent } = store.getState();
const d3ZoomInstance = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent);
const selection = select(zoomPane.current as Element).call(d3ZoomInstance);
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0]);
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1]);
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom);
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);
d3ZoomInstance.transform(selection, updatedTransform);
store.setState({
d3Zoom: d3ZoomInstance,
d3Selection: selection,
d3ZoomHandler: selection.on('wheel.zoom'),
// we need to pass transform because zoom handler is not registered when we set the initial transform
transform: [clampedX, clampedY, clampedZoom],
});
}
}, []);
useEffect(() => {
if (d3Selection && d3Zoom) {
if (panOnScroll && !zoomActivationKeyPressed) {
d3Selection
.on('wheel', (event: any) => {
if (isWrappedWithClass(event, noWheelClassName)) {
return false;
}
event.preventDefault();
event.stopImmediatePropagation();
const currentZoom = d3Selection.property('__zoom').k || 1;
if (event.ctrlKey && zoomOnPinch) {
const point = pointer(event);
// taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
const zoom = currentZoom * Math.pow(2, pinchDelta);
d3Zoom.scaleTo(d3Selection, zoom, point);
return;
}
// increase scroll speed in firefox
// firefox: deltaMode === 1; chrome: deltaMode === 0
const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
const deltaX = panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
const deltaY = panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;
d3Zoom.translateBy(
d3Selection,
-(deltaX / currentZoom) * panOnScrollSpeed,
-(deltaY / currentZoom) * panOnScrollSpeed
);
})
.on('wheel.zoom', null);
} else if (typeof d3ZoomHandler !== 'undefined') {
d3Selection
.on('wheel', (event: any) => {
if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) {
return null;
}
event.preventDefault();
})
.on('wheel.zoom', d3ZoomHandler);
}
}
}, [
panOnScroll,
panOnScrollMode,
d3Selection,
d3Zoom,
d3ZoomHandler,
zoomActivationKeyPressed,
zoomOnPinch,
preventScrolling,
noWheelClassName,
]);
useEffect(() => {
if (d3Zoom) {
if (selectionKeyPressed) {
d3Zoom.on('zoom', null);
} else {
d3Zoom.on('zoom', (event: D3ZoomEvent<HTMLDivElement, any>) => {
store.setState({ transform: [event.transform.x, event.transform.y, event.transform.k] });
if (onMove) {
const flowTransform = eventToFlowTransform(event.transform);
onMove(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
}
}
}, [selectionKeyPressed, d3Zoom, onMove]);
useEffect(() => {
if (d3Zoom) {
if (onMoveStart) {
d3Zoom.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveStart(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
});
} else {
d3Zoom.on('start', null);
}
}
}, [d3Zoom, onMoveStart]);
useEffect(() => {
if (d3Zoom) {
if (onMoveEnd) {
d3Zoom.on('end', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.current, event.transform)) {
const flowTransform = eventToFlowTransform(event.transform);
prevTransform.current = flowTransform;
onMoveEnd(event.sourceEvent as MouseEvent | TouchEvent, flowTransform);
}
});
} else {
d3Zoom.on('end', null);
}
}
}, [d3Zoom, onMoveEnd]);
useEffect(() => {
if (d3Zoom) {
d3Zoom.filter((event: any) => {
const zoomScroll = zoomActivationKeyPressed || zoomOnScroll;
const pinchZoom = zoomOnPinch && event.ctrlKey;
// if all interactions are disabled, we prevent all zoom events
if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) {
return false;
}
// during a selection we prevent all other interactions
if (selectionKeyPressed) {
return false;
}
// if zoom on double click is disabled, we prevent the double click event
if (!zoomOnDoubleClick && event.type === 'dblclick') {
return false;
}
// if the target element is inside an element with the nowheel class, we prevent zooming
if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') {
return false;
}
// if the target element is inside an element with the nopan class, we prevent panning
if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') {
return false;
}
if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
return false;
}
// when there is no scroll handling enabled, we prevent all wheel events
if (!zoomScroll && !panOnScroll && !pinchZoom && event.type === 'wheel') {
return false;
}
// if the pane is not movable, we prevent dragging it with mousestart or touchstart
if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) {
return false;
}
// default filter for d3-zoom
return (!event.ctrlKey || event.type === 'wheel') && !event.button;
});
}
}, [
d3Zoom,
zoomOnScroll,
zoomOnPinch,
panOnScroll,
zoomOnDoubleClick,
panOnDrag,
selectionKeyPressed,
elementsSelectable,
zoomActivationKeyPressed,
]);
return (
<div className="react-flow__renderer react-flow__container" ref={zoomPane}>
{children}
</div>
);
};
export default ZoomPane;