refactor(packages): change package structure
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { memo, useCallback } from 'react';
|
||||
import { type MarkerProps, createMarkerIds } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { useMarkerSymbol } from './MarkerSymbols';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
type MarkerDefinitionsProps = {
|
||||
defaultColor: string;
|
||||
rfId?: string;
|
||||
};
|
||||
|
||||
const Marker = ({
|
||||
id,
|
||||
type,
|
||||
color,
|
||||
width = 12.5,
|
||||
height = 12.5,
|
||||
markerUnits = 'strokeWidth',
|
||||
strokeWidth,
|
||||
orient = 'auto-start-reverse',
|
||||
}: MarkerProps) => {
|
||||
const Symbol = useMarkerSymbol(type);
|
||||
|
||||
if (!Symbol) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 markers = createMarkerIds(s.edges, { id: rfId, defaultColor });
|
||||
|
||||
return markers;
|
||||
};
|
||||
|
||||
// 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,57 @@
|
||||
import { useMemo } from 'react';
|
||||
import { errorMessages, MarkerType, type EdgeMarker } from '@xyflow/system';
|
||||
|
||||
import { useStoreApi } from '../../hooks/useStore';
|
||||
|
||||
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 store = useStoreApi();
|
||||
|
||||
const symbol = useMemo(() => {
|
||||
const symbolExists = Object.prototype.hasOwnProperty.call(MarkerSymbols, type);
|
||||
|
||||
if (!symbolExists) {
|
||||
store.getState().onError?.('009', errorMessages['error009'](type));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return MarkerSymbols[type];
|
||||
}, [type]);
|
||||
|
||||
return symbol;
|
||||
}
|
||||
|
||||
export default MarkerSymbols;
|
||||
@@ -0,0 +1,196 @@
|
||||
import { memo, ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { errorMessages, ConnectionMode, Position, getHandle } from '@xyflow/system';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import useVisibleEdges from '../../hooks/useVisibleEdges';
|
||||
import MarkerDefinitions from './MarkerDefinitions';
|
||||
import { getEdgePositions, getNodeData } from './utils';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { Edge, ReactFlowState } from '../../types';
|
||||
|
||||
type EdgeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
| 'edgeTypes'
|
||||
| 'onEdgeClick'
|
||||
| 'onEdgeDoubleClick'
|
||||
| 'defaultMarkerColor'
|
||||
| 'onlyRenderVisibleElements'
|
||||
| 'onEdgeUpdate'
|
||||
| 'onEdgeContextMenu'
|
||||
| 'onEdgeMouseEnter'
|
||||
| 'onEdgeMouseMove'
|
||||
| 'onEdgeMouseLeave'
|
||||
| 'onEdgeUpdateStart'
|
||||
| 'onEdgeUpdateEnd'
|
||||
| 'edgeUpdaterRadius'
|
||||
| 'noPanClassName'
|
||||
| 'elevateEdgesOnSelect'
|
||||
| 'rfId'
|
||||
| 'disableKeyboardA11y'
|
||||
> & {
|
||||
elevateEdgesOnSelect: boolean;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
nodesConnectable: s.nodesConnectable,
|
||||
edgesFocusable: s.edgesFocusable,
|
||||
edgesUpdatable: s.edgesUpdatable,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
width: s.width,
|
||||
height: s.height,
|
||||
connectionMode: s.connectionMode,
|
||||
nodeInternals: s.nodeInternals,
|
||||
onError: s.onError,
|
||||
});
|
||||
|
||||
const EdgeRenderer = ({
|
||||
defaultMarkerColor,
|
||||
onlyRenderVisibleElements,
|
||||
elevateEdgesOnSelect,
|
||||
rfId,
|
||||
edgeTypes,
|
||||
noPanClassName,
|
||||
onEdgeUpdate,
|
||||
onEdgeContextMenu,
|
||||
onEdgeMouseEnter,
|
||||
onEdgeMouseMove,
|
||||
onEdgeMouseLeave,
|
||||
onEdgeClick,
|
||||
edgeUpdaterRadius,
|
||||
onEdgeDoubleClick,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
children,
|
||||
}: EdgeRendererProps) => {
|
||||
const { edgesFocusable, edgesUpdatable, elementsSelectable, width, height, connectionMode, nodeInternals, onError } =
|
||||
useStore(selector, shallow);
|
||||
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, nodeInternals, elevateEdgesOnSelect);
|
||||
|
||||
if (!width) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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={rfId} />}
|
||||
<g>
|
||||
{edges.map((edge: Edge) => {
|
||||
const [sourceNodeRect, sourceHandleBounds, sourceIsValid] = getNodeData(nodeInternals.get(edge.source));
|
||||
const [targetNodeRect, targetHandleBounds, targetIsValid] = getNodeData(nodeInternals.get(edge.target));
|
||||
|
||||
if (!sourceIsValid || !targetIsValid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let edgeType = edge.type || 'default';
|
||||
|
||||
if (!edgeTypes[edgeType]) {
|
||||
onError?.('011', errorMessages['error011'](edgeType));
|
||||
edgeType = 'default';
|
||||
}
|
||||
|
||||
const EdgeComponent = edgeTypes[edgeType] || edgeTypes.default;
|
||||
// when connection type is loose we can define all handles as sources and connect source -> source
|
||||
const targetNodeHandles =
|
||||
connectionMode === ConnectionMode.Strict
|
||||
? targetHandleBounds!.target
|
||||
: (targetHandleBounds!.target ?? []).concat(targetHandleBounds!.source ?? []);
|
||||
const sourceHandle = getHandle(sourceHandleBounds!.source!, edge.sourceHandle);
|
||||
const targetHandle = getHandle(targetNodeHandles!, edge.targetHandle);
|
||||
const sourcePosition = sourceHandle?.position || Position.Bottom;
|
||||
const targetPosition = targetHandle?.position || Position.Top;
|
||||
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
|
||||
const isUpdatable =
|
||||
typeof onEdgeUpdate !== 'undefined' &&
|
||||
(edge.updatable || (edgesUpdatable && typeof edge.updatable === 'undefined'));
|
||||
const isSelectable = !!(
|
||||
edge.selectable ||
|
||||
(elementsSelectable && typeof edge.selectable === 'undefined')
|
||||
);
|
||||
|
||||
if (!sourceHandle || !targetHandle) {
|
||||
onError?.('008', errorMessages['error008'](sourceHandle, edge));
|
||||
|
||||
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, 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}
|
||||
isSelectable={isSelectable}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onContextMenu={onEdgeContextMenu}
|
||||
onMouseEnter={onEdgeMouseEnter}
|
||||
onMouseMove={onEdgeMouseMove}
|
||||
onMouseLeave={onEdgeMouseLeave}
|
||||
onClick={onEdgeClick}
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
rfId={rfId}
|
||||
ariaLabel={edge.ariaLabel}
|
||||
isFocusable={isFocusable}
|
||||
isUpdatable={isUpdatable}
|
||||
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
|
||||
interactionWidth={edge.interactionWidth}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</svg>
|
||||
))}
|
||||
{children}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
EdgeRenderer.displayName = 'EdgeRenderer';
|
||||
|
||||
export default memo(EdgeRenderer);
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import {
|
||||
internalsSymbol,
|
||||
Position,
|
||||
rectToBox,
|
||||
getHandlePosition,
|
||||
type HandleElement,
|
||||
type NodeHandleBounds,
|
||||
type Rect,
|
||||
type Transform,
|
||||
type XYPosition,
|
||||
} from '@xyflow/system';
|
||||
|
||||
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge, SimpleBezierEdge } from '../../components/Edges';
|
||||
import wrapEdge from '../../components/Edges/wrapEdge';
|
||||
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped, Node } from '../../types';
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
interface EdgePositions {
|
||||
sourceX: number;
|
||||
sourceY: number;
|
||||
targetX: number;
|
||||
targetY: number;
|
||||
}
|
||||
|
||||
export const getEdgePositions = (
|
||||
sourceNodeRect: Rect,
|
||||
sourceHandle: HandleElement,
|
||||
sourcePosition: Position,
|
||||
targetNodeRect: Rect,
|
||||
targetHandle: HandleElement,
|
||||
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(node?: Node): [Rect, NodeHandleBounds | null, boolean] {
|
||||
const handleBounds = node?.[internalsSymbol]?.handleBounds || null;
|
||||
|
||||
const isValid =
|
||||
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,
|
||||
!!isValid,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { memo, type ReactNode } from 'react';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
|
||||
import useKeyPress from '../../hooks/useKeyPress';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import ZoomPane from '../ZoomPane';
|
||||
import Pane from '../Pane';
|
||||
import NodesSelection from '../../components/NodesSelection';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
export type FlowRendererProps = Omit<
|
||||
GraphViewProps,
|
||||
| 'snapToGrid'
|
||||
| 'nodeTypes'
|
||||
| 'edgeTypes'
|
||||
| 'snapGrid'
|
||||
| 'connectionLineType'
|
||||
| 'connectionLineContainerStyle'
|
||||
| 'arrowHeadColor'
|
||||
| 'onlyRenderVisibleElements'
|
||||
| 'selectNodesOnDrag'
|
||||
| 'defaultMarkerColor'
|
||||
| 'rfId'
|
||||
| 'nodeOrigin'
|
||||
> & {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => s.nodesSelectionActive;
|
||||
|
||||
const FlowRenderer = ({
|
||||
children,
|
||||
onPaneClick,
|
||||
onPaneMouseEnter,
|
||||
onPaneMouseMove,
|
||||
onPaneMouseLeave,
|
||||
onPaneContextMenu,
|
||||
onPaneScroll,
|
||||
deleteKeyCode,
|
||||
selectionKeyCode,
|
||||
selectionOnDrag,
|
||||
selectionMode,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
multiSelectionKeyCode,
|
||||
panActivationKeyCode,
|
||||
zoomActivationKeyCode,
|
||||
elementsSelectable,
|
||||
zoomOnScroll,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollSpeed,
|
||||
panOnScrollMode,
|
||||
zoomOnDoubleClick,
|
||||
panOnDrag: _panOnDrag,
|
||||
defaultViewport,
|
||||
translateExtent,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
preventScrolling,
|
||||
onSelectionContextMenu,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
disableKeyboardA11y,
|
||||
}: FlowRendererProps) => {
|
||||
const nodesSelectionActive = useStore(selector);
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode);
|
||||
const panActivationKeyPressed = useKeyPress(panActivationKeyCode);
|
||||
|
||||
const panOnDrag = panActivationKeyPressed || _panOnDrag;
|
||||
const isSelecting = selectionKeyPressed || (selectionOnDrag && panOnDrag !== true);
|
||||
|
||||
useGlobalKeyHandler({ deleteKeyCode, multiSelectionKeyCode });
|
||||
|
||||
return (
|
||||
<ZoomPane
|
||||
onPaneContextMenu={onPaneContextMenu}
|
||||
elementsSelectable={elementsSelectable}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
zoomOnPinch={zoomOnPinch}
|
||||
panOnScroll={panOnScroll}
|
||||
panOnScrollSpeed={panOnScrollSpeed}
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
panOnDrag={!selectionKeyPressed && panOnDrag}
|
||||
defaultViewport={defaultViewport}
|
||||
translateExtent={translateExtent}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
zoomActivationKeyCode={zoomActivationKeyCode}
|
||||
preventScrolling={preventScrolling}
|
||||
noWheelClassName={noWheelClassName}
|
||||
noPanClassName={noPanClassName}
|
||||
>
|
||||
<Pane
|
||||
onSelectionStart={onSelectionStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
onPaneClick={onPaneClick}
|
||||
onPaneMouseEnter={onPaneMouseEnter}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
onPaneMouseLeave={onPaneMouseLeave}
|
||||
onPaneContextMenu={onPaneContextMenu}
|
||||
onPaneScroll={onPaneScroll}
|
||||
panOnDrag={panOnDrag}
|
||||
isSelecting={!!isSelecting}
|
||||
selectionMode={selectionMode}
|
||||
>
|
||||
{children}
|
||||
{nodesSelectionActive && (
|
||||
<NodesSelection
|
||||
onSelectionContextMenu={onSelectionContextMenu}
|
||||
noPanClassName={noPanClassName}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
/>
|
||||
)}
|
||||
</Pane>
|
||||
</ZoomPane>
|
||||
);
|
||||
};
|
||||
|
||||
FlowRenderer.displayName = 'FlowRenderer';
|
||||
|
||||
export default memo(FlowRenderer);
|
||||
@@ -0,0 +1,195 @@
|
||||
import { memo } from 'react';
|
||||
|
||||
import FlowRenderer from '../FlowRenderer';
|
||||
import NodeRenderer from '../NodeRenderer';
|
||||
import EdgeRenderer from '../EdgeRenderer';
|
||||
import ViewportWrapper from '../Viewport';
|
||||
import useOnInitHandler from '../../hooks/useOnInitHandler';
|
||||
import ConnectionLine from '../../components/ConnectionLine';
|
||||
import type { EdgeTypesWrapped, NodeTypesWrapped, ReactFlowProps } from '../../types';
|
||||
|
||||
export type GraphViewProps = Omit<
|
||||
ReactFlowProps,
|
||||
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
|
||||
> &
|
||||
Required<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
| 'selectionKeyCode'
|
||||
| 'deleteKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
| 'connectionLineType'
|
||||
| 'onlyRenderVisibleElements'
|
||||
| 'translateExtent'
|
||||
| 'minZoom'
|
||||
| 'maxZoom'
|
||||
| 'defaultMarkerColor'
|
||||
| 'noDragClassName'
|
||||
| 'noDragClassName'
|
||||
| 'noWheelClassName'
|
||||
| 'noPanClassName'
|
||||
| 'defaultViewport'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'nodeOrigin'
|
||||
>
|
||||
> & {
|
||||
nodeTypes: NodeTypesWrapped;
|
||||
edgeTypes: EdgeTypesWrapped;
|
||||
rfId: string;
|
||||
};
|
||||
|
||||
const GraphView = ({
|
||||
nodeTypes,
|
||||
edgeTypes,
|
||||
onInit,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
onNodeDoubleClick,
|
||||
onEdgeDoubleClick,
|
||||
onNodeMouseEnter,
|
||||
onNodeMouseMove,
|
||||
onNodeMouseLeave,
|
||||
onNodeContextMenu,
|
||||
onSelectionContextMenu,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
connectionLineType,
|
||||
connectionLineStyle,
|
||||
connectionLineComponent,
|
||||
connectionLineContainerStyle,
|
||||
selectionKeyCode,
|
||||
selectionOnDrag,
|
||||
selectionMode,
|
||||
multiSelectionKeyCode,
|
||||
panActivationKeyCode,
|
||||
zoomActivationKeyCode,
|
||||
deleteKeyCode,
|
||||
onlyRenderVisibleElements,
|
||||
elementsSelectable,
|
||||
defaultViewport,
|
||||
translateExtent,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
preventScrolling,
|
||||
defaultMarkerColor,
|
||||
zoomOnScroll,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollSpeed,
|
||||
panOnScrollMode,
|
||||
zoomOnDoubleClick,
|
||||
panOnDrag,
|
||||
onPaneClick,
|
||||
onPaneMouseEnter,
|
||||
onPaneMouseMove,
|
||||
onPaneMouseLeave,
|
||||
onPaneScroll,
|
||||
onPaneContextMenu,
|
||||
onEdgeUpdate,
|
||||
onEdgeContextMenu,
|
||||
onEdgeMouseEnter,
|
||||
onEdgeMouseMove,
|
||||
onEdgeMouseLeave,
|
||||
edgeUpdaterRadius,
|
||||
onEdgeUpdateStart,
|
||||
onEdgeUpdateEnd,
|
||||
noDragClassName,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
elevateEdgesOnSelect,
|
||||
disableKeyboardA11y,
|
||||
nodeOrigin,
|
||||
nodeExtent,
|
||||
rfId,
|
||||
}: GraphViewProps) => {
|
||||
useOnInitHandler(onInit);
|
||||
|
||||
return (
|
||||
<FlowRenderer
|
||||
onPaneClick={onPaneClick}
|
||||
onPaneMouseEnter={onPaneMouseEnter}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
onPaneMouseLeave={onPaneMouseLeave}
|
||||
onPaneContextMenu={onPaneContextMenu}
|
||||
onPaneScroll={onPaneScroll}
|
||||
deleteKeyCode={deleteKeyCode}
|
||||
selectionKeyCode={selectionKeyCode}
|
||||
selectionOnDrag={selectionOnDrag}
|
||||
selectionMode={selectionMode}
|
||||
onSelectionStart={onSelectionStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||
panActivationKeyCode={panActivationKeyCode}
|
||||
zoomActivationKeyCode={zoomActivationKeyCode}
|
||||
elementsSelectable={elementsSelectable}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
zoomOnPinch={zoomOnPinch}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
panOnScroll={panOnScroll}
|
||||
panOnScrollSpeed={panOnScrollSpeed}
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
panOnDrag={panOnDrag}
|
||||
defaultViewport={defaultViewport}
|
||||
translateExtent={translateExtent}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
onSelectionContextMenu={onSelectionContextMenu}
|
||||
preventScrolling={preventScrolling}
|
||||
noDragClassName={noDragClassName}
|
||||
noWheelClassName={noWheelClassName}
|
||||
noPanClassName={noPanClassName}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
>
|
||||
<ViewportWrapper>
|
||||
<EdgeRenderer
|
||||
edgeTypes={edgeTypes}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onEdgeDoubleClick={onEdgeDoubleClick}
|
||||
onEdgeUpdate={onEdgeUpdate}
|
||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||
onEdgeContextMenu={onEdgeContextMenu}
|
||||
onEdgeMouseEnter={onEdgeMouseEnter}
|
||||
onEdgeMouseMove={onEdgeMouseMove}
|
||||
onEdgeMouseLeave={onEdgeMouseLeave}
|
||||
onEdgeUpdateStart={onEdgeUpdateStart}
|
||||
onEdgeUpdateEnd={onEdgeUpdateEnd}
|
||||
edgeUpdaterRadius={edgeUpdaterRadius}
|
||||
defaultMarkerColor={defaultMarkerColor}
|
||||
noPanClassName={noPanClassName}
|
||||
elevateEdgesOnSelect={!!elevateEdgesOnSelect}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
rfId={rfId}
|
||||
>
|
||||
<ConnectionLine
|
||||
style={connectionLineStyle}
|
||||
type={connectionLineType}
|
||||
component={connectionLineComponent}
|
||||
containerStyle={connectionLineContainerStyle}
|
||||
/>
|
||||
</EdgeRenderer>
|
||||
<div className="react-flow__edgelabel-renderer" />
|
||||
|
||||
<NodeRenderer
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={onNodeClick}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||
noPanClassName={noPanClassName}
|
||||
noDragClassName={noDragClassName}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
nodeOrigin={nodeOrigin}
|
||||
nodeExtent={nodeExtent}
|
||||
rfId={rfId}
|
||||
/>
|
||||
</ViewportWrapper>
|
||||
</FlowRenderer>
|
||||
);
|
||||
};
|
||||
|
||||
GraphView.displayName = 'GraphView';
|
||||
|
||||
export default memo(GraphView);
|
||||
@@ -0,0 +1,146 @@
|
||||
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
||||
|
||||
import useVisibleNodes from '../../hooks/useVisibleNodes';
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles';
|
||||
import { GraphViewProps } from '../GraphView';
|
||||
import type { ReactFlowState, WrapNodeProps } from '../../types';
|
||||
|
||||
type NodeRendererProps = Pick<
|
||||
GraphViewProps,
|
||||
| 'nodeTypes'
|
||||
| 'onNodeClick'
|
||||
| 'onNodeDoubleClick'
|
||||
| 'onNodeMouseEnter'
|
||||
| 'onNodeMouseMove'
|
||||
| 'onNodeMouseLeave'
|
||||
| 'onNodeContextMenu'
|
||||
| 'onlyRenderVisibleElements'
|
||||
| 'noPanClassName'
|
||||
| 'noDragClassName'
|
||||
| 'rfId'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'nodeOrigin'
|
||||
| 'nodeExtent'
|
||||
>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
nodesDraggable: s.nodesDraggable,
|
||||
nodesConnectable: s.nodesConnectable,
|
||||
nodesFocusable: s.nodesFocusable,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
updateNodeDimensions: s.updateNodeDimensions,
|
||||
onError: s.onError,
|
||||
});
|
||||
|
||||
const NodeRenderer = (props: NodeRendererProps) => {
|
||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
|
||||
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" style={containerStyle}>
|
||||
{nodes.map((node) => {
|
||||
let nodeType = node.type || 'default';
|
||||
|
||||
if (!props.nodeTypes[nodeType]) {
|
||||
onError?.('003', errorMessages['error003'](nodeType));
|
||||
|
||||
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'));
|
||||
const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined'));
|
||||
|
||||
const clampedPosition = props.nodeExtent
|
||||
? clampPosition(node.positionAbsolute, props.nodeExtent)
|
||||
: node.positionAbsolute;
|
||||
|
||||
const posX = clampedPosition?.x ?? 0;
|
||||
const posY = clampedPosition?.y ?? 0;
|
||||
const posOrigin = getPositionWithOrigin({
|
||||
x: posX,
|
||||
y: posY,
|
||||
width: node.width ?? 0,
|
||||
height: node.height ?? 0,
|
||||
origin: node.origin || props.nodeOrigin,
|
||||
});
|
||||
|
||||
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={posX}
|
||||
yPos={posY}
|
||||
xPosOrigin={posOrigin.x}
|
||||
yPosOrigin={posOrigin.y}
|
||||
onClick={props.onNodeClick}
|
||||
onMouseEnter={props.onNodeMouseEnter}
|
||||
onMouseMove={props.onNodeMouseMove}
|
||||
onMouseLeave={props.onNodeMouseLeave}
|
||||
onContextMenu={props.onNodeContextMenu}
|
||||
onDoubleClick={props.onNodeDoubleClick}
|
||||
selected={!!node.selected}
|
||||
isDraggable={isDraggable}
|
||||
isSelectable={isSelectable}
|
||||
isConnectable={isConnectable}
|
||||
isFocusable={isFocusable}
|
||||
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}
|
||||
rfId={props.rfId}
|
||||
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||
ariaLabel={node.ariaLabel}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
NodeRenderer.displayName = 'NodeRenderer';
|
||||
|
||||
export default memo(NodeRenderer);
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { ComponentType } from 'react';
|
||||
import type { NodeProps } from '@xyflow/system';
|
||||
|
||||
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 type { NodeTypes, 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,236 @@
|
||||
/**
|
||||
* The user selection rectangle gets displayed when a user drags the mouse while pressing shift
|
||||
*/
|
||||
|
||||
import { memo, useRef, type MouseEvent as ReactMouseEvent, type ReactNode } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import cc from 'classcat';
|
||||
import { getNodesInside, getEventPosition, SelectionMode } from '@xyflow/system';
|
||||
|
||||
import UserSelection from '../../components/UserSelection';
|
||||
import { containerStyle } from '../../styles';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { getSelectionChanges, getConnectedEdges } from '../../utils';
|
||||
import type { ReactFlowProps, ReactFlowState, NodeChange, EdgeChange, Node } from '../../types';
|
||||
|
||||
type PaneProps = {
|
||||
isSelecting: boolean;
|
||||
children: ReactNode;
|
||||
} & Partial<
|
||||
Pick<
|
||||
ReactFlowProps,
|
||||
| 'selectionMode'
|
||||
| 'panOnDrag'
|
||||
| 'onSelectionStart'
|
||||
| 'onSelectionEnd'
|
||||
| 'onPaneClick'
|
||||
| 'onPaneContextMenu'
|
||||
| 'onPaneScroll'
|
||||
| 'onPaneMouseEnter'
|
||||
| 'onPaneMouseMove'
|
||||
| 'onPaneMouseLeave'
|
||||
>
|
||||
>;
|
||||
|
||||
const wrapHandler = (
|
||||
handler: React.MouseEventHandler | undefined,
|
||||
containerRef: React.MutableRefObject<HTMLDivElement | null>
|
||||
): React.MouseEventHandler => {
|
||||
return (event: ReactMouseEvent) => {
|
||||
if (event.target !== containerRef.current) {
|
||||
return;
|
||||
}
|
||||
handler?.(event);
|
||||
};
|
||||
};
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
elementsSelectable: s.elementsSelectable,
|
||||
dragging: s.paneDragging,
|
||||
});
|
||||
|
||||
const Pane = memo(
|
||||
({
|
||||
isSelecting,
|
||||
selectionMode = SelectionMode.Full,
|
||||
panOnDrag,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
onPaneClick,
|
||||
onPaneContextMenu,
|
||||
onPaneScroll,
|
||||
onPaneMouseEnter,
|
||||
onPaneMouseMove,
|
||||
onPaneMouseLeave,
|
||||
children,
|
||||
}: PaneProps) => {
|
||||
const container = useRef<HTMLDivElement | null>(null);
|
||||
const store = useStoreApi();
|
||||
const prevSelectedNodesCount = useRef<number>(0);
|
||||
const prevSelectedEdgesCount = useRef<number>(0);
|
||||
const containerBounds = useRef<DOMRect>();
|
||||
const { userSelectionActive, elementsSelectable, dragging } = useStore(selector, shallow);
|
||||
|
||||
const resetUserSelection = () => {
|
||||
store.setState({ userSelectionActive: false, userSelectionRect: null });
|
||||
|
||||
prevSelectedNodesCount.current = 0;
|
||||
prevSelectedEdgesCount.current = 0;
|
||||
};
|
||||
|
||||
const onClick = (event: ReactMouseEvent) => {
|
||||
onPaneClick?.(event);
|
||||
store.getState().resetSelectedElements();
|
||||
store.setState({ nodesSelectionActive: false });
|
||||
};
|
||||
|
||||
const onContextMenu = (event: ReactMouseEvent) => {
|
||||
if (Array.isArray(panOnDrag) && panOnDrag?.includes(2)) {
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
onPaneContextMenu?.(event);
|
||||
};
|
||||
|
||||
const onWheel = onPaneScroll ? (event: React.WheelEvent) => onPaneScroll(event) : undefined;
|
||||
|
||||
const onMouseDown = (event: ReactMouseEvent): void => {
|
||||
const { resetSelectedElements, domNode } = store.getState();
|
||||
containerBounds.current = domNode?.getBoundingClientRect();
|
||||
|
||||
if (
|
||||
!elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container.current ||
|
||||
!containerBounds.current
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { x, y } = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
|
||||
resetSelectedElements();
|
||||
|
||||
store.setState({
|
||||
userSelectionRect: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: x,
|
||||
startY: y,
|
||||
x,
|
||||
y,
|
||||
},
|
||||
});
|
||||
|
||||
onSelectionStart?.(event);
|
||||
};
|
||||
|
||||
const onMouseMove = (event: ReactMouseEvent): void => {
|
||||
const { userSelectionRect, edges, transform, onNodesChange, onEdgesChange, nodeOrigin, getNodes } =
|
||||
store.getState();
|
||||
if (!isSelecting || !containerBounds.current || !userSelectionRect) {
|
||||
return;
|
||||
}
|
||||
|
||||
store.setState({ userSelectionActive: true, nodesSelectionActive: false });
|
||||
|
||||
const mousePos = getEventPosition(event.nativeEvent, containerBounds.current);
|
||||
const startX = userSelectionRect.startX ?? 0;
|
||||
const startY = userSelectionRect.startY ?? 0;
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...userSelectionRect,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
};
|
||||
|
||||
const nodes = getNodes();
|
||||
const selectedNodes = getNodesInside<Node>(
|
||||
nodes,
|
||||
nextUserSelectRect,
|
||||
transform,
|
||||
selectionMode === SelectionMode.Partial,
|
||||
true,
|
||||
nodeOrigin
|
||||
);
|
||||
const selectedEdgeIds = getConnectedEdges(selectedNodes, edges).map((e) => e.id);
|
||||
const selectedNodeIds = selectedNodes.map((n) => n.id);
|
||||
|
||||
if (prevSelectedNodesCount.current !== selectedNodeIds.length) {
|
||||
prevSelectedNodesCount.current = selectedNodeIds.length;
|
||||
const changes = getSelectionChanges(nodes, selectedNodeIds) as NodeChange[];
|
||||
if (changes.length) {
|
||||
onNodesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
if (prevSelectedEdgesCount.current !== selectedEdgeIds.length) {
|
||||
prevSelectedEdgesCount.current = selectedEdgeIds.length;
|
||||
const changes = getSelectionChanges(edges, selectedEdgeIds) as EdgeChange[];
|
||||
if (changes.length) {
|
||||
onEdgesChange?.(changes);
|
||||
}
|
||||
}
|
||||
|
||||
store.setState({
|
||||
userSelectionRect: nextUserSelectRect,
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = (event: ReactMouseEvent) => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
const { userSelectionRect } = store.getState();
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
if (!userSelectionActive && userSelectionRect && event.target === container.current) {
|
||||
onClick?.(event);
|
||||
}
|
||||
|
||||
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
|
||||
|
||||
resetUserSelection();
|
||||
onSelectionEnd?.(event);
|
||||
};
|
||||
|
||||
const onMouseLeave = (event: ReactMouseEvent) => {
|
||||
if (userSelectionActive) {
|
||||
store.setState({ nodesSelectionActive: prevSelectedNodesCount.current > 0 });
|
||||
onSelectionEnd?.(event);
|
||||
}
|
||||
|
||||
resetUserSelection();
|
||||
};
|
||||
|
||||
const hasActiveSelection = elementsSelectable && (isSelecting || userSelectionActive);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cc(['react-flow__pane', { dragging, selection: isSelecting }])}
|
||||
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
|
||||
onContextMenu={wrapHandler(onContextMenu, container)}
|
||||
onWheel={wrapHandler(onWheel, container)}
|
||||
onMouseEnter={hasActiveSelection ? undefined : onPaneMouseEnter}
|
||||
onMouseDown={hasActiveSelection ? onMouseDown : undefined}
|
||||
onMouseMove={hasActiveSelection ? onMouseMove : onPaneMouseMove}
|
||||
onMouseUp={hasActiveSelection ? onMouseUp : undefined}
|
||||
onMouseLeave={hasActiveSelection ? onMouseLeave : onPaneMouseLeave}
|
||||
ref={container}
|
||||
style={containerStyle}
|
||||
>
|
||||
{children}
|
||||
<UserSelection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Pane.displayName = 'Pane';
|
||||
|
||||
export default Pane;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { useContext, type FC, type PropsWithChildren } from 'react';
|
||||
|
||||
import StoreContext from '../../contexts/RFStoreContext';
|
||||
import ReactFlowProvider from '../../components/ReactFlowProvider';
|
||||
|
||||
const Wrapper: FC<PropsWithChildren<unknown>> = ({ children }) => {
|
||||
const isWrapped = useContext(StoreContext);
|
||||
|
||||
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 <ReactFlowProvider>{children}</ReactFlowProvider>;
|
||||
};
|
||||
|
||||
Wrapper.displayName = 'ReactFlowWrapper';
|
||||
|
||||
export default Wrapper;
|
||||
@@ -0,0 +1,319 @@
|
||||
import { forwardRef, type CSSProperties } from 'react';
|
||||
import cc from 'classcat';
|
||||
import {
|
||||
ConnectionLineType,
|
||||
ConnectionMode,
|
||||
PanOnScrollMode,
|
||||
SelectionMode,
|
||||
infiniteExtent,
|
||||
type NodeOrigin,
|
||||
type Viewport,
|
||||
} from '@xyflow/system';
|
||||
|
||||
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 GroupNode from '../../components/Nodes/GroupNode';
|
||||
import SelectionListener from '../../components/SelectionListener';
|
||||
import StoreUpdater from '../../components/StoreUpdater';
|
||||
import A11yDescriptions from '../../components/A11yDescriptions';
|
||||
import { createEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { createNodeTypes } from '../NodeRenderer/utils';
|
||||
import GraphView from '../GraphView';
|
||||
import Wrapper from './Wrapper';
|
||||
import { useNodeOrEdgeTypes } from './utils';
|
||||
import type {
|
||||
EdgeTypes,
|
||||
EdgeTypesWrapped,
|
||||
NodeTypes,
|
||||
NodeTypesWrapped,
|
||||
ReactFlowProps,
|
||||
ReactFlowRefType,
|
||||
} from '../../types';
|
||||
|
||||
const defaultNodeTypes: NodeTypes = {
|
||||
input: InputNode,
|
||||
default: DefaultNode,
|
||||
output: OutputNode,
|
||||
group: GroupNode,
|
||||
};
|
||||
|
||||
const defaultEdgeTypes: EdgeTypes = {
|
||||
default: BezierEdge,
|
||||
straight: StraightEdge,
|
||||
step: StepEdge,
|
||||
smoothstep: SmoothStepEdge,
|
||||
simplebezier: SimpleBezierEdge,
|
||||
};
|
||||
|
||||
const initNodeOrigin: NodeOrigin = [0, 0];
|
||||
const initSnapGrid: [number, number] = [15, 15];
|
||||
const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
|
||||
|
||||
const wrapperStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
zIndex: 0,
|
||||
};
|
||||
|
||||
const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
|
||||
(
|
||||
{
|
||||
nodes,
|
||||
edges,
|
||||
defaultNodes,
|
||||
defaultEdges,
|
||||
className,
|
||||
nodeTypes = defaultNodeTypes,
|
||||
edgeTypes = defaultEdgeTypes,
|
||||
onNodeClick,
|
||||
onEdgeClick,
|
||||
onInit,
|
||||
onMove,
|
||||
onMoveStart,
|
||||
onMoveEnd,
|
||||
onConnect,
|
||||
onConnectStart,
|
||||
onConnectEnd,
|
||||
onClickConnectStart,
|
||||
onClickConnectEnd,
|
||||
onNodeMouseEnter,
|
||||
onNodeMouseMove,
|
||||
onNodeMouseLeave,
|
||||
onNodeContextMenu,
|
||||
onNodeDoubleClick,
|
||||
onNodeDragStart,
|
||||
onNodeDrag,
|
||||
onNodeDragStop,
|
||||
onNodesDelete,
|
||||
onEdgesDelete,
|
||||
onSelectionChange,
|
||||
onSelectionDragStart,
|
||||
onSelectionDrag,
|
||||
onSelectionDragStop,
|
||||
onSelectionContextMenu,
|
||||
onSelectionStart,
|
||||
onSelectionEnd,
|
||||
connectionMode = ConnectionMode.Strict,
|
||||
connectionLineType = ConnectionLineType.Bezier,
|
||||
connectionLineStyle,
|
||||
connectionLineComponent,
|
||||
connectionLineContainerStyle,
|
||||
deleteKeyCode = 'Backspace',
|
||||
selectionKeyCode = 'Shift',
|
||||
selectionOnDrag = false,
|
||||
selectionMode = SelectionMode.Full,
|
||||
panActivationKeyCode = 'Space',
|
||||
multiSelectionKeyCode = 'Meta',
|
||||
zoomActivationKeyCode = 'Meta',
|
||||
snapToGrid = false,
|
||||
snapGrid = initSnapGrid,
|
||||
onlyRenderVisibleElements = false,
|
||||
selectNodesOnDrag = true,
|
||||
nodesDraggable,
|
||||
nodesConnectable,
|
||||
nodesFocusable,
|
||||
nodeOrigin = initNodeOrigin,
|
||||
edgesFocusable,
|
||||
edgesUpdatable,
|
||||
elementsSelectable = true,
|
||||
defaultViewport = initDefaultViewport,
|
||||
minZoom = 0.5,
|
||||
maxZoom = 2,
|
||||
translateExtent = infiniteExtent,
|
||||
preventScrolling = true,
|
||||
nodeExtent,
|
||||
defaultMarkerColor = '#b1b1b7',
|
||||
zoomOnScroll = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnScrollSpeed = 0.5,
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
zoomOnDoubleClick = true,
|
||||
panOnDrag = true,
|
||||
onPaneClick,
|
||||
onPaneMouseEnter,
|
||||
onPaneMouseMove,
|
||||
onPaneMouseLeave,
|
||||
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,
|
||||
elevateNodesOnSelect = true,
|
||||
elevateEdgesOnSelect = false,
|
||||
disableKeyboardA11y = false,
|
||||
autoPanOnConnect = true,
|
||||
autoPanOnNodeDrag = true,
|
||||
connectionRadius = 20,
|
||||
isValidConnection,
|
||||
onError,
|
||||
style,
|
||||
id,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes) as NodeTypesWrapped;
|
||||
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes) as EdgeTypesWrapped;
|
||||
const rfId = id || '1';
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
style={{ ...style, ...wrapperStyle }}
|
||||
ref={ref}
|
||||
className={cc(['react-flow', className])}
|
||||
data-testid="rf__wrapper"
|
||||
id={id}
|
||||
>
|
||||
<Wrapper>
|
||||
<GraphView
|
||||
onInit={onInit}
|
||||
onNodeClick={onNodeClick}
|
||||
onEdgeClick={onEdgeClick}
|
||||
onNodeMouseEnter={onNodeMouseEnter}
|
||||
onNodeMouseMove={onNodeMouseMove}
|
||||
onNodeMouseLeave={onNodeMouseLeave}
|
||||
onNodeContextMenu={onNodeContextMenu}
|
||||
onNodeDoubleClick={onNodeDoubleClick}
|
||||
nodeTypes={nodeTypesWrapped}
|
||||
edgeTypes={edgeTypesWrapped}
|
||||
connectionLineType={connectionLineType}
|
||||
connectionLineStyle={connectionLineStyle}
|
||||
connectionLineComponent={connectionLineComponent}
|
||||
connectionLineContainerStyle={connectionLineContainerStyle}
|
||||
selectionKeyCode={selectionKeyCode}
|
||||
selectionOnDrag={selectionOnDrag}
|
||||
selectionMode={selectionMode}
|
||||
deleteKeyCode={deleteKeyCode}
|
||||
multiSelectionKeyCode={multiSelectionKeyCode}
|
||||
panActivationKeyCode={panActivationKeyCode}
|
||||
zoomActivationKeyCode={zoomActivationKeyCode}
|
||||
onlyRenderVisibleElements={onlyRenderVisibleElements}
|
||||
defaultViewport={defaultViewport}
|
||||
translateExtent={translateExtent}
|
||||
minZoom={minZoom}
|
||||
maxZoom={maxZoom}
|
||||
preventScrolling={preventScrolling}
|
||||
zoomOnScroll={zoomOnScroll}
|
||||
zoomOnPinch={zoomOnPinch}
|
||||
zoomOnDoubleClick={zoomOnDoubleClick}
|
||||
panOnScroll={panOnScroll}
|
||||
panOnScrollSpeed={panOnScrollSpeed}
|
||||
panOnScrollMode={panOnScrollMode}
|
||||
panOnDrag={panOnDrag}
|
||||
onPaneClick={onPaneClick}
|
||||
onPaneMouseEnter={onPaneMouseEnter}
|
||||
onPaneMouseMove={onPaneMouseMove}
|
||||
onPaneMouseLeave={onPaneMouseLeave}
|
||||
onPaneScroll={onPaneScroll}
|
||||
onPaneContextMenu={onPaneContextMenu}
|
||||
onSelectionContextMenu={onSelectionContextMenu}
|
||||
onSelectionStart={onSelectionStart}
|
||||
onSelectionEnd={onSelectionEnd}
|
||||
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}
|
||||
rfId={rfId}
|
||||
disableKeyboardA11y={disableKeyboardA11y}
|
||||
nodeOrigin={nodeOrigin}
|
||||
nodeExtent={nodeExtent}
|
||||
/>
|
||||
<StoreUpdater
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
defaultNodes={defaultNodes}
|
||||
defaultEdges={defaultEdges}
|
||||
onConnect={onConnect}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
onClickConnectStart={onClickConnectStart}
|
||||
onClickConnectEnd={onClickConnectEnd}
|
||||
nodesDraggable={nodesDraggable}
|
||||
nodesConnectable={nodesConnectable}
|
||||
nodesFocusable={nodesFocusable}
|
||||
edgesFocusable={edgesFocusable}
|
||||
edgesUpdatable={edgesUpdatable}
|
||||
elementsSelectable={elementsSelectable}
|
||||
elevateNodesOnSelect={elevateNodesOnSelect}
|
||||
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}
|
||||
onNodeDragStart={onNodeDragStart}
|
||||
onNodeDrag={onNodeDrag}
|
||||
onNodeDragStop={onNodeDragStop}
|
||||
onSelectionDrag={onSelectionDrag}
|
||||
onSelectionDragStart={onSelectionDragStart}
|
||||
onSelectionDragStop={onSelectionDragStop}
|
||||
onMove={onMove}
|
||||
onMoveStart={onMoveStart}
|
||||
onMoveEnd={onMoveEnd}
|
||||
noPanClassName={noPanClassName}
|
||||
nodeOrigin={nodeOrigin}
|
||||
rfId={rfId}
|
||||
autoPanOnConnect={autoPanOnConnect}
|
||||
autoPanOnNodeDrag={autoPanOnNodeDrag}
|
||||
onError={onError}
|
||||
connectionRadius={connectionRadius}
|
||||
isValidConnection={isValidConnection}
|
||||
selectNodesOnDrag={selectNodesOnDrag}
|
||||
/>
|
||||
<SelectionListener onSelectionChange={onSelectionChange} />
|
||||
{children}
|
||||
<Attribution proOptions={proOptions} position={attributionPosition} />
|
||||
<A11yDescriptions rfId={rfId} disableKeyboardA11y={disableKeyboardA11y} />
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ReactFlow.displayName = 'ReactFlow';
|
||||
|
||||
export default ReactFlow;
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { errorMessages, devWarn } from '@xyflow/system';
|
||||
|
||||
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
|
||||
import { CreateNodeTypes } from '../NodeRenderer/utils';
|
||||
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
|
||||
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: NodeTypes, createTypes: CreateNodeTypes): NodeTypesWrapped;
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: EdgeTypes, createTypes: CreateEdgeTypes): EdgeTypesWrapped;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function useNodeOrEdgeTypes(nodeOrEdgeTypes: any, createTypes: any): any {
|
||||
const typesKeysRef = useRef<string[] | null>(null);
|
||||
|
||||
const typesParsed = useMemo(() => {
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
const typeKeys = Object.keys(nodeOrEdgeTypes);
|
||||
if (shallow(typesKeysRef.current, typeKeys)) {
|
||||
devWarn('002', errorMessages['error002']());
|
||||
}
|
||||
|
||||
typesKeysRef.current = typeKeys;
|
||||
}
|
||||
return createTypes(nodeOrEdgeTypes);
|
||||
}, [nodeOrEdgeTypes]);
|
||||
|
||||
return typesParsed;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
import { useStore } from '../../hooks/useStore';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
const selector = (s: ReactFlowState) => `translate(${s.transform[0]}px,${s.transform[1]}px) scale(${s.transform[2]})`;
|
||||
|
||||
type ViewportProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
function Viewport({ children }: ViewportProps) {
|
||||
const transform = useStore(selector);
|
||||
|
||||
return (
|
||||
<div className="react-flow__viewport react-flow__container" style={{ transform }}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Viewport;
|
||||
@@ -0,0 +1,137 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
import { XYPanZoom, PanOnScrollMode, type Transform, type PanZoomInstance } from '@xyflow/system';
|
||||
|
||||
import useKeyPress from '../../hooks/useKeyPress';
|
||||
import useResizeHandler from '../../hooks/useResizeHandler';
|
||||
import { useStore, useStoreApi } from '../../hooks/useStore';
|
||||
import { containerStyle } from '../../styles';
|
||||
import type { FlowRendererProps } from '../FlowRenderer';
|
||||
import type { ReactFlowState } from '../../types';
|
||||
|
||||
type ZoomPaneProps = Omit<
|
||||
FlowRendererProps,
|
||||
| 'deleteKeyCode'
|
||||
| 'selectionKeyCode'
|
||||
| 'multiSelectionKeyCode'
|
||||
| 'noDragClassName'
|
||||
| 'disableKeyboardA11y'
|
||||
| 'selectionOnDrag'
|
||||
>;
|
||||
|
||||
const selector = (s: ReactFlowState) => ({
|
||||
userSelectionActive: s.userSelectionActive,
|
||||
lib: s.lib,
|
||||
});
|
||||
|
||||
const ZoomPane = ({
|
||||
onPaneContextMenu,
|
||||
zoomOnScroll = true,
|
||||
zoomOnPinch = true,
|
||||
panOnScroll = false,
|
||||
panOnScrollSpeed = 0.5,
|
||||
panOnScrollMode = PanOnScrollMode.Free,
|
||||
zoomOnDoubleClick = true,
|
||||
panOnDrag = true,
|
||||
defaultViewport,
|
||||
translateExtent,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
zoomActivationKeyCode,
|
||||
preventScrolling = true,
|
||||
children,
|
||||
noWheelClassName,
|
||||
noPanClassName,
|
||||
}: ZoomPaneProps) => {
|
||||
const store = useStoreApi();
|
||||
const zoomPane = useRef<HTMLDivElement>(null);
|
||||
const { userSelectionActive, lib } = useStore(selector, shallow);
|
||||
const zoomActivationKeyPressed = useKeyPress(zoomActivationKeyCode);
|
||||
const panZoom = useRef<PanZoomInstance>();
|
||||
|
||||
useResizeHandler(zoomPane);
|
||||
|
||||
useEffect(() => {
|
||||
if (zoomPane.current) {
|
||||
panZoom.current = XYPanZoom({
|
||||
domNode: zoomPane.current,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
translateExtent,
|
||||
viewport: defaultViewport,
|
||||
onTransformChange: (transform: Transform) => store.setState({ transform }),
|
||||
onDraggingChange: (paneDragging: boolean) => store.setState({ paneDragging }),
|
||||
onPanZoomStart: (event, vp) => {
|
||||
const { onViewportChangeStart, onMoveStart } = store.getState();
|
||||
onMoveStart?.(event, vp);
|
||||
onViewportChangeStart?.(vp);
|
||||
},
|
||||
onPanZoom: (event, vp) => {
|
||||
const { onViewportChange, onMove } = store.getState();
|
||||
onMove?.(event, vp);
|
||||
onViewportChange?.(vp);
|
||||
},
|
||||
onPanZoomEnd: (event, vp) => {
|
||||
const { onViewportChangeEnd, onMoveEnd } = store.getState();
|
||||
onMoveEnd?.(event, vp);
|
||||
onViewportChangeEnd?.(vp);
|
||||
},
|
||||
});
|
||||
|
||||
const { x, y, zoom } = panZoom.current!.getViewport();
|
||||
|
||||
store.setState({
|
||||
panZoom: panZoom.current,
|
||||
transform: [x, y, zoom],
|
||||
domNode: zoomPane.current.closest('.react-flow') as HTMLDivElement,
|
||||
});
|
||||
|
||||
return () => {
|
||||
panZoom.current?.destroy();
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
panZoom.current?.update({
|
||||
onPaneContextMenu,
|
||||
zoomOnScroll,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollSpeed,
|
||||
panOnScrollMode,
|
||||
zoomOnDoubleClick,
|
||||
panOnDrag,
|
||||
zoomActivationKeyPressed,
|
||||
preventScrolling,
|
||||
noPanClassName,
|
||||
userSelectionActive,
|
||||
noWheelClassName,
|
||||
lib,
|
||||
});
|
||||
}, [
|
||||
onPaneContextMenu,
|
||||
zoomOnScroll,
|
||||
zoomOnPinch,
|
||||
panOnScroll,
|
||||
panOnScrollSpeed,
|
||||
panOnScrollMode,
|
||||
zoomOnDoubleClick,
|
||||
panOnDrag,
|
||||
zoomActivationKeyPressed,
|
||||
preventScrolling,
|
||||
noPanClassName,
|
||||
userSelectionActive,
|
||||
noWheelClassName,
|
||||
lib,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="react-flow__renderer" ref={zoomPane} style={containerStyle}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ZoomPane;
|
||||
Reference in New Issue
Block a user