refactor(node-renderer): simplify selectors, remove reselect, cleanup
This commit is contained in:
@@ -64,7 +64,6 @@ const StressFlow = () => {
|
|||||||
minZoom={0.2}
|
minZoom={0.2}
|
||||||
fitView
|
fitView
|
||||||
>
|
>
|
||||||
<MiniMap />
|
|
||||||
<Controls />
|
<Controls />
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,6 @@
|
|||||||
"d3-drag": "^3.0.0",
|
"d3-drag": "^3.0.0",
|
||||||
"d3-selection": "^3.0.0",
|
"d3-selection": "^3.0.0",
|
||||||
"d3-zoom": "^3.0.0",
|
"d3-zoom": "^3.0.0",
|
||||||
"reselect": "^4.1.8",
|
|
||||||
"zustand": "^4.4.0"
|
"zustand": "^4.4.0"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
@@ -1,30 +1,18 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
import { ComponentType, memo, MouseEvent } from 'react';
|
import { ComponentType, memo } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
|
||||||
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
|
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
|
||||||
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import type { ReactFlowState } from '../../types';
|
import type { ReactFlowState } from '../../types';
|
||||||
import MiniMapNode from './MiniMapNode';
|
import MiniMapNode from './MiniMapNode';
|
||||||
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
|
|
||||||
declare const window: any;
|
declare const window: any;
|
||||||
|
|
||||||
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
const selector = (s: ReactFlowState) => s.nodeOrigin;
|
||||||
const selectorNodeIds = createSelector(
|
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
|
||||||
[(s: ReactFlowState) => s.nodes],
|
|
||||||
(nodes) =>
|
|
||||||
nodes
|
|
||||||
.filter((node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height))
|
|
||||||
.map((node) => node.id),
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: shallow,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
|
||||||
|
|
||||||
function MiniMapNodes({
|
function MiniMapNodes({
|
||||||
@@ -38,7 +26,7 @@ function MiniMapNodes({
|
|||||||
nodeComponent: NodeComponent = MiniMapNode,
|
nodeComponent: NodeComponent = MiniMapNode,
|
||||||
onClick,
|
onClick,
|
||||||
}: MiniMapNodesProps) {
|
}: MiniMapNodesProps) {
|
||||||
const nodes = useStore(selectorNodeIds);
|
const nodeIds = useStore(selectorNodeIds, shallow);
|
||||||
const nodeOrigin = useStore(selector);
|
const nodeOrigin = useStore(selector);
|
||||||
const nodeColorFunc = getAttrFunction(nodeColor);
|
const nodeColorFunc = getAttrFunction(nodeColor);
|
||||||
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
|
||||||
@@ -48,7 +36,7 @@ function MiniMapNodes({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{nodes.map((nodeId) => (
|
{nodeIds.map((nodeId) => (
|
||||||
// The split of responsibilities between MiniMapNodes and
|
// The split of responsibilities between MiniMapNodes and
|
||||||
// NodeComponentWrapper may appear weird. However, it’s designed to
|
// NodeComponentWrapper may appear weird. However, it’s designed to
|
||||||
// minimize the cost of updates when individual nodes change.
|
// minimize the cost of updates when individual nodes change.
|
||||||
@@ -96,16 +84,16 @@ const NodeComponentWrapper = memo(function NodeComponentWrapper({
|
|||||||
shapeRendering: string;
|
shapeRendering: string;
|
||||||
}) {
|
}) {
|
||||||
const node = useStore((s) => s.nodeLookup.get(id));
|
const node = useStore((s) => s.nodeLookup.get(id));
|
||||||
if (!node) {
|
if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
const positionOrigin = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeComponent
|
<NodeComponent
|
||||||
x={x}
|
x={positionOrigin.x}
|
||||||
y={y}
|
y={positionOrigin.y}
|
||||||
width={node.computed?.width ?? node.width ?? 0}
|
width={node.computed?.width ?? node.width ?? 0}
|
||||||
height={node.computed?.height ?? node.height ?? 0}
|
height={node.computed?.height ?? node.height ?? 0}
|
||||||
style={node.style}
|
style={node.style}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
id,
|
id,
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
xPos,
|
|
||||||
yPos,
|
|
||||||
xPosOrigin,
|
xPosOrigin,
|
||||||
yPosOrigin,
|
yPosOrigin,
|
||||||
|
positionAbsoluteX,
|
||||||
|
positionAbsoluteY,
|
||||||
selected,
|
selected,
|
||||||
onClick,
|
onClick,
|
||||||
onMouseEnter,
|
onMouseEnter,
|
||||||
@@ -52,7 +52,6 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
disableKeyboardA11y,
|
disableKeyboardA11y,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
rfId,
|
rfId,
|
||||||
positionAbsolute,
|
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
}: WrapNodeProps) => {
|
}: WrapNodeProps) => {
|
||||||
@@ -111,7 +110,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
store.setState({
|
store.setState({
|
||||||
ariaLiveMessage: `Moved selected node ${event.key
|
ariaLiveMessage: `Moved selected node ${event.key
|
||||||
.replace('Arrow', '')
|
.replace('Arrow', '')
|
||||||
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
|
.toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
updatePositions({
|
updatePositions({
|
||||||
@@ -212,7 +211,8 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
|||||||
type={type}
|
type={type}
|
||||||
width={width}
|
width={width}
|
||||||
height={height}
|
height={height}
|
||||||
positionAbsolute={positionAbsolute}
|
positionAbsoluteX={positionAbsoluteX}
|
||||||
|
positionAbsoluteY={positionAbsoluteY}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
isConnectable={isConnectable}
|
isConnectable={isConnectable}
|
||||||
sourcePosition={sourcePosition}
|
sourcePosition={sourcePosition}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { memo, type ComponentType } from 'react';
|
||||||
|
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
||||||
|
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
import type { ReactFlowState, WrapNodeProps } from '../../types';
|
||||||
|
import { NodeRendererProps } from '.';
|
||||||
|
|
||||||
|
function NodeComponentWrapper(props: {
|
||||||
|
id: string;
|
||||||
|
nodeExtent: NodeRendererProps['nodeExtent'];
|
||||||
|
nodeTypes: NodeRendererProps['nodeTypes'];
|
||||||
|
nodeOrigin: NodeRendererProps['nodeOrigin'];
|
||||||
|
onNodeClick: NodeRendererProps['onNodeClick'];
|
||||||
|
onNodeMouseEnter: NodeRendererProps['onNodeMouseEnter'];
|
||||||
|
onNodeMouseMove: NodeRendererProps['onNodeMouseMove'];
|
||||||
|
onNodeMouseLeave: NodeRendererProps['onNodeMouseLeave'];
|
||||||
|
onNodeContextMenu: NodeRendererProps['onNodeContextMenu'];
|
||||||
|
onNodeDoubleClick: NodeRendererProps['onNodeDoubleClick'];
|
||||||
|
noDragClassName: NodeRendererProps['noDragClassName'];
|
||||||
|
noPanClassName: NodeRendererProps['noPanClassName'];
|
||||||
|
rfId: NodeRendererProps['rfId'];
|
||||||
|
disableKeyboardA11y: NodeRendererProps['disableKeyboardA11y'];
|
||||||
|
resizeObserver: ResizeObserver | null;
|
||||||
|
nodesDraggable: boolean;
|
||||||
|
nodesConnectable: boolean;
|
||||||
|
nodesFocusable: boolean;
|
||||||
|
elementsSelectable: boolean;
|
||||||
|
onError: ReactFlowState['onError'];
|
||||||
|
}) {
|
||||||
|
const node = useStore((s) => s.nodeLookup.get(props.id));
|
||||||
|
if (!node) return null;
|
||||||
|
|
||||||
|
let nodeType = node.type || 'default';
|
||||||
|
|
||||||
|
if (!props.nodeTypes[nodeType]) {
|
||||||
|
props.onError?.('003', errorMessages['error003'](nodeType));
|
||||||
|
|
||||||
|
nodeType = 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
||||||
|
const isDraggable = !!(node.draggable || (props.nodesDraggable && typeof node.draggable === 'undefined'));
|
||||||
|
const isSelectable = !!(node.selectable || (props.elementsSelectable && typeof node.selectable === 'undefined'));
|
||||||
|
const isConnectable = !!(node.connectable || (props.nodesConnectable && typeof node.connectable === 'undefined'));
|
||||||
|
const isFocusable = !!(node.focusable || (props.nodesFocusable && typeof node.focusable === 'undefined'));
|
||||||
|
|
||||||
|
const absolutePositionClamped = props.nodeExtent
|
||||||
|
? clampPosition(node.computed?.positionAbsolute, props.nodeExtent)
|
||||||
|
: node.computed?.positionAbsolute || { x: 0, y: 0 };
|
||||||
|
|
||||||
|
const posOrigin = getPositionWithOrigin({
|
||||||
|
x: absolutePositionClamped.x,
|
||||||
|
y: absolutePositionClamped.y,
|
||||||
|
width: node.computed?.width ?? node.width ?? 0,
|
||||||
|
height: node.computed?.height ?? node.height ?? 0,
|
||||||
|
origin: node.origin || props.nodeOrigin,
|
||||||
|
});
|
||||||
|
const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NodeComponent
|
||||||
|
key={node.id}
|
||||||
|
id={node.id}
|
||||||
|
className={node.className}
|
||||||
|
style={node.style}
|
||||||
|
width={node.width ?? undefined}
|
||||||
|
height={node.height ?? undefined}
|
||||||
|
type={nodeType}
|
||||||
|
data={node.data}
|
||||||
|
sourcePosition={node.sourcePosition || Position.Bottom}
|
||||||
|
targetPosition={node.targetPosition || Position.Top}
|
||||||
|
hidden={node.hidden}
|
||||||
|
xPosOrigin={posOrigin.x}
|
||||||
|
yPosOrigin={posOrigin.y}
|
||||||
|
positionAbsoluteX={absolutePositionClamped.x}
|
||||||
|
positionAbsoluteY={absolutePositionClamped.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={props.resizeObserver}
|
||||||
|
dragHandle={node.dragHandle}
|
||||||
|
zIndex={node[internalsSymbol]?.z ?? 0}
|
||||||
|
isParent={!!node[internalsSymbol]?.isParent}
|
||||||
|
noDragClassName={props.noDragClassName}
|
||||||
|
noPanClassName={props.noPanClassName}
|
||||||
|
initialized={initialized}
|
||||||
|
rfId={props.rfId}
|
||||||
|
disableKeyboardA11y={props.disableKeyboardA11y}
|
||||||
|
ariaLabel={node.ariaLabel}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeComponentWrapper.displayName = 'NodeComponentWrapper';
|
||||||
|
|
||||||
|
export default memo(NodeComponentWrapper);
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
|
import { memo } from 'react';
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
|
|
||||||
|
|
||||||
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
|
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
|
||||||
import { useStore } from '../../hooks/useStore';
|
import { useStore } from '../../hooks/useStore';
|
||||||
import { containerStyle } from '../../styles/utils';
|
import { containerStyle } from '../../styles/utils';
|
||||||
import { GraphViewProps } from '../GraphView';
|
import { GraphViewProps } from '../GraphView';
|
||||||
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
|
import type { NodeTypesWrapped, ReactFlowState } from '../../types';
|
||||||
|
import NodeComponentWrapper from './NodeComponentWrapper';
|
||||||
|
import useResizeObserver from './useResizeObserver';
|
||||||
|
|
||||||
type NodeRendererProps = Pick<
|
export type NodeRendererProps = Pick<
|
||||||
GraphViewProps,
|
GraphViewProps,
|
||||||
| 'onNodeClick'
|
| 'onNodeClick'
|
||||||
| 'onNodeDoubleClick'
|
| 'onNodeDoubleClick'
|
||||||
@@ -32,46 +33,13 @@ const selector = (s: ReactFlowState) => ({
|
|||||||
nodesConnectable: s.nodesConnectable,
|
nodesConnectable: s.nodesConnectable,
|
||||||
nodesFocusable: s.nodesFocusable,
|
nodesFocusable: s.nodesFocusable,
|
||||||
elementsSelectable: s.elementsSelectable,
|
elementsSelectable: s.elementsSelectable,
|
||||||
updateNodeDimensions: s.updateNodeDimensions,
|
|
||||||
onError: s.onError,
|
onError: s.onError,
|
||||||
});
|
});
|
||||||
|
|
||||||
const NodeRenderer = (props: NodeRendererProps) => {
|
const NodeRenderer = (props: NodeRendererProps) => {
|
||||||
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, updateNodeDimensions, onError } =
|
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||||
useStore(selector, shallow);
|
|
||||||
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
|
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
|
||||||
const resizeObserverRef = useRef<ResizeObserver>();
|
const resizeObserver = useResizeObserver();
|
||||||
|
|
||||||
const resizeObserver = useMemo(() => {
|
|
||||||
if (typeof ResizeObserver === 'undefined') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
|
||||||
const updates = new Map();
|
|
||||||
|
|
||||||
entries.forEach((entry: ResizeObserverEntry) => {
|
|
||||||
const id = entry.target.getAttribute('data-id') as string;
|
|
||||||
updates.set(id, {
|
|
||||||
id,
|
|
||||||
nodeElement: entry.target as HTMLDivElement,
|
|
||||||
forceUpdate: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
updateNodeDimensions(updates);
|
|
||||||
});
|
|
||||||
|
|
||||||
resizeObserverRef.current = observer;
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
return () => {
|
|
||||||
resizeObserverRef?.current?.disconnect();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="react-flow__nodes" style={containerStyle}>
|
<div className="react-flow__nodes" style={containerStyle}>
|
||||||
@@ -129,103 +97,6 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const NodeComponentWrapper = memo(function NodeComponentWrapper(props: {
|
|
||||||
id: string;
|
|
||||||
nodeExtent: NodeRendererProps['nodeExtent'];
|
|
||||||
nodeTypes: NodeRendererProps['nodeTypes'];
|
|
||||||
nodeOrigin: NodeRendererProps['nodeOrigin'];
|
|
||||||
onNodeClick: NodeRendererProps['onNodeClick'];
|
|
||||||
onNodeMouseEnter: NodeRendererProps['onNodeMouseEnter'];
|
|
||||||
onNodeMouseMove: NodeRendererProps['onNodeMouseMove'];
|
|
||||||
onNodeMouseLeave: NodeRendererProps['onNodeMouseLeave'];
|
|
||||||
onNodeContextMenu: NodeRendererProps['onNodeContextMenu'];
|
|
||||||
onNodeDoubleClick: NodeRendererProps['onNodeDoubleClick'];
|
|
||||||
noDragClassName: NodeRendererProps['noDragClassName'];
|
|
||||||
noPanClassName: NodeRendererProps['noPanClassName'];
|
|
||||||
rfId: NodeRendererProps['rfId'];
|
|
||||||
disableKeyboardA11y: NodeRendererProps['disableKeyboardA11y'];
|
|
||||||
resizeObserver: ResizeObserver | null;
|
|
||||||
nodesDraggable: boolean;
|
|
||||||
nodesConnectable: boolean;
|
|
||||||
nodesFocusable: boolean;
|
|
||||||
elementsSelectable: boolean;
|
|
||||||
onError: ReactFlowState['onError'];
|
|
||||||
}) {
|
|
||||||
const node = useStore((s) => s.nodeLookup.get(props.id));
|
|
||||||
if (!node) return null;
|
|
||||||
|
|
||||||
let nodeType = node.type || 'default';
|
|
||||||
|
|
||||||
if (!props.nodeTypes[nodeType]) {
|
|
||||||
props.onError?.('003', errorMessages['error003'](nodeType));
|
|
||||||
|
|
||||||
nodeType = 'default';
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeComponent = (props.nodeTypes[nodeType] || props.nodeTypes.default) as ComponentType<WrapNodeProps>;
|
|
||||||
const isDraggable = !!(node.draggable || (props.nodesDraggable && typeof node.draggable === 'undefined'));
|
|
||||||
const isSelectable = !!(node.selectable || (props.elementsSelectable && typeof node.selectable === 'undefined'));
|
|
||||||
const isConnectable = !!(node.connectable || (props.nodesConnectable && typeof node.connectable === 'undefined'));
|
|
||||||
const isFocusable = !!(node.focusable || (props.nodesFocusable && typeof node.focusable === 'undefined'));
|
|
||||||
|
|
||||||
const clampedPosition = props.nodeExtent
|
|
||||||
? clampPosition(node.computed?.positionAbsolute, props.nodeExtent)
|
|
||||||
: node.computed?.positionAbsolute;
|
|
||||||
|
|
||||||
const posX = clampedPosition?.x ?? 0;
|
|
||||||
const posY = clampedPosition?.y ?? 0;
|
|
||||||
const posOrigin = getPositionWithOrigin({
|
|
||||||
x: posX,
|
|
||||||
y: posY,
|
|
||||||
width: node.computed?.width ?? node.width ?? 0,
|
|
||||||
height: node.computed?.height ?? node.height ?? 0,
|
|
||||||
origin: node.origin || props.nodeOrigin,
|
|
||||||
});
|
|
||||||
const initialized = (!!node.computed?.width && !!node.computed?.height) || (!!node.width && !!node.height);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NodeComponent
|
|
||||||
key={node.id}
|
|
||||||
id={node.id}
|
|
||||||
className={node.className}
|
|
||||||
style={node.style}
|
|
||||||
width={node.width ?? undefined}
|
|
||||||
height={node.height ?? undefined}
|
|
||||||
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}
|
|
||||||
positionAbsolute={clampedPosition || { x: 0, y: 0 }}
|
|
||||||
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={props.resizeObserver}
|
|
||||||
dragHandle={node.dragHandle}
|
|
||||||
zIndex={node[internalsSymbol]?.z ?? 0}
|
|
||||||
isParent={!!node[internalsSymbol]?.isParent}
|
|
||||||
noDragClassName={props.noDragClassName}
|
|
||||||
noPanClassName={props.noPanClassName}
|
|
||||||
initialized={initialized}
|
|
||||||
rfId={props.rfId}
|
|
||||||
disableKeyboardA11y={props.disableKeyboardA11y}
|
|
||||||
ariaLabel={node.ariaLabel}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
NodeRenderer.displayName = 'NodeRenderer';
|
NodeRenderer.displayName = 'NodeRenderer';
|
||||||
|
|
||||||
export default memo(NodeRenderer);
|
export default memo(NodeRenderer);
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { ReactFlowState } from '../../types';
|
||||||
|
import { useStore } from '../../hooks/useStore';
|
||||||
|
|
||||||
|
const selector = (s: ReactFlowState) => s.updateNodeDimensions;
|
||||||
|
|
||||||
|
export default function useResizeObserver() {
|
||||||
|
const updateNodeDimensions = useStore(selector);
|
||||||
|
const resizeObserverRef = useRef<ResizeObserver>();
|
||||||
|
|
||||||
|
const resizeObserver = useMemo(() => {
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
|
||||||
|
const updates = new Map();
|
||||||
|
|
||||||
|
entries.forEach((entry: ResizeObserverEntry) => {
|
||||||
|
const id = entry.target.getAttribute('data-id') as string;
|
||||||
|
updates.set(id, {
|
||||||
|
id,
|
||||||
|
nodeElement: entry.target as HTMLDivElement,
|
||||||
|
forceUpdate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateNodeDimensions(updates);
|
||||||
|
});
|
||||||
|
|
||||||
|
resizeObserverRef.current = observer;
|
||||||
|
|
||||||
|
return observer;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
resizeObserverRef?.current?.disconnect();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return resizeObserver;
|
||||||
|
}
|
||||||
@@ -1,31 +1,20 @@
|
|||||||
import { getNodesInside } from '@xyflow/system';
|
import { getNodesInside } from '@xyflow/system';
|
||||||
import { createSelector } from 'reselect';
|
|
||||||
import { shallow } from 'zustand/shallow';
|
import { shallow } from 'zustand/shallow';
|
||||||
|
|
||||||
import { useStore } from '../hooks/useStore';
|
import { useStore } from '../hooks/useStore';
|
||||||
import type { Node, ReactFlowState } from '../types';
|
import type { Node, ReactFlowState } from '../types';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
const visibleNodeIdsSelector = createSelector(
|
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
|
||||||
[
|
return onlyRenderVisible
|
||||||
(s: ReactFlowState) => s.nodes,
|
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
|
||||||
(s: ReactFlowState) => s.width,
|
(node) => node.id
|
||||||
(s: ReactFlowState) => s.height,
|
)
|
||||||
(s: ReactFlowState) => s.transform,
|
: Array.from(s.nodeLookup.keys());
|
||||||
(_s: ReactFlowState, onlyRenderVisibleNodes: boolean) => onlyRenderVisibleNodes,
|
};
|
||||||
],
|
|
||||||
(nodes, width, height, transform, onlyRenderVisible) =>
|
|
||||||
onlyRenderVisible
|
|
||||||
? getNodesInside<Node>(nodes, { x: 0, y: 0, width, height }, transform, true).map((node) => node.id)
|
|
||||||
: nodes.map((node) => node.id),
|
|
||||||
{
|
|
||||||
memoizeOptions: {
|
|
||||||
resultEqualityCheck: shallow,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function useVisibleNodeIds(onlyRenderVisible: boolean) {
|
function useVisibleNodeIds(onlyRenderVisible: boolean) {
|
||||||
const nodeIds = useStore((s: ReactFlowState) => visibleNodeIdsSelector(s, onlyRenderVisible));
|
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);
|
||||||
|
|
||||||
return nodeIds;
|
return nodeIds;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||||
import type { NodeBase, XYPosition } from '@xyflow/system';
|
import type { NodeBase } from '@xyflow/system';
|
||||||
|
|
||||||
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
|
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
|
||||||
NodeData,
|
NodeData,
|
||||||
@@ -20,11 +20,10 @@ export type WrapNodeProps<NodeData = any> = Pick<
|
|||||||
> &
|
> &
|
||||||
Required<Pick<Node<NodeData>, 'selected' | 'type' | 'zIndex'>> & {
|
Required<Pick<Node<NodeData>, 'selected' | 'type' | 'zIndex'>> & {
|
||||||
isConnectable: boolean;
|
isConnectable: boolean;
|
||||||
xPos: number;
|
|
||||||
yPos: number;
|
|
||||||
xPosOrigin: number;
|
xPosOrigin: number;
|
||||||
yPosOrigin: number;
|
yPosOrigin: number;
|
||||||
positionAbsolute: XYPosition;
|
positionAbsoluteX: number;
|
||||||
|
positionAbsoluteY: number;
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
isSelectable: boolean;
|
isSelectable: boolean;
|
||||||
isDraggable: boolean;
|
isDraggable: boolean;
|
||||||
|
|||||||
@@ -191,7 +191,8 @@
|
|||||||
{dragging}
|
{dragging}
|
||||||
{dragHandle}
|
{dragHandle}
|
||||||
isConnectable={connectable}
|
isConnectable={connectable}
|
||||||
positionAbsolute={{ x: positionX, y: positionY }}
|
positionAbsoluteX={positionX}
|
||||||
|
positionAbsoluteY={positionY}
|
||||||
{width}
|
{width}
|
||||||
{height}
|
{height}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ export type NodeProps<T = any> = {
|
|||||||
selected: NodeBase['selected'];
|
selected: NodeBase['selected'];
|
||||||
isConnectable: NodeBase['connectable'];
|
isConnectable: NodeBase['connectable'];
|
||||||
zIndex: NodeBase['zIndex'];
|
zIndex: NodeBase['zIndex'];
|
||||||
positionAbsolute: XYPosition;
|
positionAbsoluteX: number;
|
||||||
|
positionAbsoluteY: number;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
|
|||||||
Generated
-9
@@ -235,9 +235,6 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: '>=17'
|
specifier: '>=17'
|
||||||
version: registry.npmjs.org/react-dom@18.2.0(react@18.2.0)
|
version: registry.npmjs.org/react-dom@18.2.0(react@18.2.0)
|
||||||
reselect:
|
|
||||||
specifier: ^4.1.8
|
|
||||||
version: registry.npmjs.org/reselect@4.1.8
|
|
||||||
zustand:
|
zustand:
|
||||||
specifier: ^4.4.0
|
specifier: ^4.4.0
|
||||||
version: registry.npmjs.org/zustand@4.4.1(@types/react@18.2.24)(react@18.2.0)
|
version: registry.npmjs.org/zustand@4.4.1(@types/react@18.2.24)(react@18.2.0)
|
||||||
@@ -10036,12 +10033,6 @@ packages:
|
|||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
registry.npmjs.org/reselect@4.1.8:
|
|
||||||
resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz}
|
|
||||||
name: reselect
|
|
||||||
version: 4.1.8
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
registry.npmjs.org/resolve-from@4.0.0:
|
registry.npmjs.org/resolve-from@4.0.0:
|
||||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz}
|
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz}
|
||||||
name: resolve-from
|
name: resolve-from
|
||||||
|
|||||||
Reference in New Issue
Block a user