Merge pull request #3727 from xyflow/perf/noderenderer-minimap

Perf: NodeRenderer, Minimap and Nodes
This commit is contained in:
Moritz Klack
2023-12-16 11:23:48 +01:00
committed by GitHub
48 changed files with 1021 additions and 1284 deletions
-6
View File
@@ -21,7 +21,6 @@ import Interaction from '../examples/Interaction';
import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
@@ -185,11 +184,6 @@ const routes: IRoute[] = [
path: 'multiflows',
component: MultiFlows,
},
{
name: 'Nested Nodes',
path: 'nested-nodes',
component: NestedNodes,
},
{
name: 'Node Type Change',
path: 'nodetype-change',
@@ -4,8 +4,12 @@ import { Node, Position, MarkerType, XYPosition } from '@xyflow/react';
// of the line between the center of the intersectionNode and the target node
function getNodeIntersection(intersectionNode: Node, targetNode: Node) {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode;
const intersectionNodePosition = intersectionNode.computed?.positionAbsolute!;
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
positionAbsolute: intersectionNodePosition,
} = intersectionNode.computed || {};
const targetPosition = targetNode.computed?.positionAbsolute!;
const w = intersectionNodeWidth! / 2;
@@ -5,11 +5,11 @@ import { Position, XYPosition, Node, Edge } from '@xyflow/react';
function getNodeIntersection(intersectionNode: Node, targetNode: Node): XYPosition {
// https://math.stackexchange.com/questions/1724792/an-algorithm-for-finding-the-intersection-point-between-a-center-of-vision-and-a
const {
width: intersectionNodeWidth,
height: intersectionNodeHeight,
position: intersectionNodePosition,
} = intersectionNode;
const { position: intersectionNodePosition } = intersectionNode;
const { width: intersectionNodeWidth, height: intersectionNodeHeight } = intersectionNode.computed ?? {
width: 0,
height: 0,
};
const targetPosition = targetNode.position;
const w = (intersectionNodeWidth ?? 0) / 2;
+4 -2
View File
@@ -48,8 +48,10 @@ const initialEdges: Edge[] = [
const setHidden = (hidden: boolean) => (els: any[]) =>
els.map((e: any) => {
e.hidden = hidden;
return e;
return {
...e,
hidden,
};
});
const HiddenFlow = () => {
@@ -56,16 +56,16 @@ const LayoutFlow = () => {
const layoutedNodes = nodes.map((node) => {
const nodeWithPosition = dagreGraph.node(node.id);
node.targetPosition = isHorizontal ? Position.Left : Position.Top;
node.sourcePosition = isHorizontal ? Position.Right : Position.Bottom;
// we need to pass a slightly different position in order to notify react flow about the change
// @TODO how can we change the position handling so that we dont need this hack?
node.position = {
x: nodeWithPosition.x + Math.random() / 1000,
y: nodeWithPosition.y,
};
return node;
return {
...node,
targetPosition: isHorizontal ? Position.Left : Position.Top,
sourcePosition: isHorizontal ? Position.Right : Position.Bottom,
position: {
x: nodeWithPosition.x,
y: nodeWithPosition.y,
},
};
});
setNodes(layoutedNodes);
@@ -1,187 +0,0 @@
import { useState, MouseEvent, useCallback } from 'react';
import {
ReactFlow,
Controls,
MiniMap,
Background,
addEdge,
useNodesState,
useEdgesState,
Node,
Edge,
ReactFlowInstance,
Connection,
} from '@xyflow/react';
const onNodeDragStop = (_: MouseEvent, node: Node) => console.log('drag stop', node);
const onNodeClick = (_: MouseEvent, node: Node) => console.log('click', node);
const onEdgeClick = (_: MouseEvent, edge: Edge) => console.log('click', edge);
const initialNodes: Node[] = [
{
id: '1',
type: 'input',
data: { label: 'Node 1' },
position: { x: 250, y: 5 },
className: 'light',
},
{
id: '2',
data: { label: 'Node 2' },
position: { x: 100, y: 100 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.8)', width: 200, height: 200 },
},
{
id: '2a',
data: { label: 'Node 2a' },
position: { x: 10, y: 50 },
parentNode: '2',
},
{
id: '3',
data: { label: 'Node 3' },
position: { x: 320, y: 100 },
className: 'light',
},
{
id: '4',
data: { label: 'Node 4' },
position: { x: 320, y: 200 },
className: 'light',
style: { backgroundColor: 'rgba(255, 0, 0, 0.7)', width: 300, height: 300 },
},
{
id: '4a',
data: { label: 'Node 4a' },
position: { x: 15, y: 65 },
className: 'light',
parentNode: '4',
extent: 'parent',
},
{
id: '4b',
data: { label: 'Node 4b' },
position: { x: 15, y: 120 },
className: 'light',
style: {
backgroundColor: 'rgba(255, 0, 255, 0.7)',
height: 150,
width: 270,
},
parentNode: '4',
},
{
id: '4b1',
data: { label: 'Node 4b1' },
position: { x: 20, y: 40 },
className: 'light',
parentNode: '4b',
},
{
id: '4b2',
data: { label: 'Node 4b2' },
position: { x: 100, y: 100 },
className: 'light',
parentNode: '4b',
},
];
const initialEdges: Edge[] = [
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
{ id: 'e2a-4a', source: '2a', target: '4a' },
{ id: 'e3-4', source: '3', target: '4' },
{ id: 'e3-4b', source: '3', target: '4b' },
{ id: 'e4a-4b1', source: '4a', target: '4b1' },
{ id: 'e4a-4b2', source: '4a', target: '4b2' },
{ id: 'e4b1-4b2', source: '4b1', target: '4b2' },
];
const NestedFlow = () => {
const [rfInstance, setRfInstance] = useState<ReactFlowInstance | null>(null);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
const onConnect = useCallback(
(connection: Connection) => {
setEdges((eds) => addEdge(connection, eds));
},
[setEdges]
);
const onInit = useCallback((reactFlowInstance: ReactFlowInstance) => setRfInstance(reactFlowInstance), []);
const updatePos = () => {
setNodes((nds) => {
return nds.map((n) => {
n.position = {
x: Math.random() * 400,
y: Math.random() * 400,
};
return n;
});
});
};
const logToObject = () => console.log(rfInstance?.toObject());
const resetTransform = () => rfInstance?.setViewport({ x: 0, y: 0, zoom: 1 });
const toggleClassnames = () => {
setNodes((nds) => {
return nds.map((n) => {
n.className = n.className === 'light' ? 'dark' : 'light';
return n;
});
});
};
const toggleChildNodes = () => {
setNodes((nds) => {
return nds.map((n) => {
n.hidden = !!n.parentNode && !n.hidden;
return n;
});
});
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
onInit={onInit}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeClick={onNodeClick}
onEdgeClick={onEdgeClick}
onConnect={onConnect}
onNodeDragStop={onNodeDragStop}
className="react-flow-basic-example"
minZoom={0.2}
maxZoom={4}
onlyRenderVisibleElements={false}
>
<MiniMap pannable />
<Controls />
<Background />
<div style={{ position: 'absolute', right: 10, top: 10, zIndex: 4 }}>
<button onClick={resetTransform} style={{ marginRight: 5 }}>
reset transform
</button>
<button onClick={updatePos} style={{ marginRight: 5 }}>
change pos
</button>
<button onClick={toggleClassnames} style={{ marginRight: 5 }}>
toggle classnames
</button>
<button style={{ marginRight: 5 }} onClick={toggleChildNodes}>
toggleChildNodes
</button>
<button onClick={logToObject}>toObject</button>
</div>
</ReactFlow>
);
};
export default NestedFlow;
@@ -64,7 +64,6 @@ const StressFlow = () => {
minZoom={0.2}
fitView
>
<MiniMap />
<Controls />
<Background />
@@ -11,13 +11,13 @@ const idStyle: CSSProperties = {
left: 2,
};
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsolute, id }) => {
const DebugNode: FC<NodeProps> = ({ zIndex, positionAbsoluteX, positionAbsoluteY, id }) => {
return (
<>
<Handle type="target" position={Position.Top} />
<div style={idStyle}>{id}</div>
<div style={infoStyle}>
x:{Math.round(positionAbsolute.x)} y:{Math.round(positionAbsolute.y)} z:{zIndex}
x:{Math.round(positionAbsoluteX)} y:{Math.round(positionAbsoluteY)} z:{zIndex}
</div>
<Handle type="source" position={Position.Bottom} />
</>
@@ -24,9 +24,12 @@ const UpdateNode = () => {
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
n.data = {
...n.data,
label: nodeName,
return {
...n,
data: {
...n.data,
label: nodeName,
},
};
}
@@ -40,7 +43,10 @@ const UpdateNode = () => {
nds.map((n) => {
if (n.id === '1') {
// it's important that you create a new object here in order to notify react flow about the change
n.style = { ...n.style, backgroundColor: nodeBg };
return {
...n,
style: { ...n.style, backgroundColor: nodeBg },
};
}
return n;
@@ -52,8 +58,10 @@ const UpdateNode = () => {
setNodes((nds) =>
nds.map((n) => {
if (n.id === '1' || n.id === 'e1-2') {
// when you update a simple type you can just update the value
n.hidden = nodeHidden;
return {
...n,
hidden: nodeHidden,
};
}
return n;
@@ -12,8 +12,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
[nodeId]
);
const connections = useHandleConnections({
handleType: handleProps.type,
handleId: handleProps.id,
type: handleProps.type,
id: handleProps.id,
onConnect,
onDisconnect,
});
@@ -15,8 +15,8 @@ function CustomHandle({ nodeId, ...handleProps }: HandleComponentProps & { nodeI
[nodeId]
);
const connections = useHandleConnections({
handleType: handleProps.type,
handleId: handleProps.id,
type: handleProps.type,
id: handleProps.id,
onConnect,
onDisconnect,
});
@@ -1,15 +1,16 @@
<script lang="ts">
import { Handle, Position, type NodeProps, type XYPosition } from '@xyflow/svelte';
import { Handle, Position, type NodeProps } from '@xyflow/svelte';
type $$Props = NodeProps;
export let data: { label: string } = { label: 'Node' };
export let positionAbsolute: XYPosition = { x: 0, y: 0 };
export let positionAbsoluteX: number = 0;
export let positionAbsoluteY: number = 0;
</script>
<div class="custom">
<div>{data.label}</div>
<div>{~~positionAbsolute.x}, {~~positionAbsolute.y}</div>
<div>{~~positionAbsoluteX}, {~~positionAbsoluteY}</div>
<Handle type="target" position={Position.Top} />
@@ -1,21 +1,18 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { memo } from 'react';
import { ComponentType, memo } from 'react';
import { NodeOrigin, getNodePositionWithOrigin } from '@xyflow/system';
import { shallow } from 'zustand/shallow';
import { getNodePositionWithOrigin } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import type { ReactFlowState } from '../../types';
import MiniMapNode from './MiniMapNode';
import type { MiniMapNodes, GetMiniMapNodeAttribute } from './types';
import type { MiniMapNodes as MiniMapNodesProps, GetMiniMapNodeAttribute, MiniMapNodeProps } from './types';
declare const window: any;
const selector = (s: ReactFlowState) => s.nodeOrigin;
const selectorNodes = (s: ReactFlowState) =>
s.nodes.filter(
(node) => !node.hidden && (node.computed?.width || node.width) && (node.computed?.height || node.height)
);
const selectorNodeIds = (s: ReactFlowState) => s.nodes.map((node) => node.id);
const getAttrFunction = (func: any): GetMiniMapNodeAttribute => (func instanceof Function ? func : () => func);
function MiniMapNodes({
@@ -28,8 +25,8 @@ function MiniMapNodes({
// a component properly.
nodeComponent: NodeComponent = MiniMapNode,
onClick,
}: MiniMapNodes) {
const nodes = useStore(selectorNodes, shallow);
}: MiniMapNodesProps) {
const nodeIds = useStore(selectorNodeIds, shallow);
const nodeOrigin = useStore(selector);
const nodeColorFunc = getAttrFunction(nodeColor);
const nodeStrokeColorFunc = getAttrFunction(nodeStrokeColor);
@@ -39,33 +36,78 @@ function MiniMapNodes({
return (
<>
{nodes.map((node) => {
const { x, y } = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
const color = nodeColor === undefined ? undefined : nodeColorFunc(node);
const strokeColor = nodeStrokeColor === undefined ? undefined : nodeStrokeColorFunc(node);
return (
<NodeComponent
key={node.id}
x={x}
y={y}
width={node.computed?.width ?? node.width ?? 0}
height={node.computed?.height ?? node.height ?? 0}
style={node.style}
selected={!!node.selected}
className={nodeClassNameFunc(node)}
color={color}
borderRadius={nodeBorderRadius}
strokeColor={strokeColor}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
onClick={onClick}
id={node.id}
/>
);
})}
{nodeIds.map((nodeId) => (
// The split of responsibilities between MiniMapNodes and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For more details, see a similar commit in `NodeRenderer/index.tsx`.
<NodeComponentWrapper
key={nodeId}
id={nodeId}
nodeOrigin={nodeOrigin}
nodeColorFunc={nodeColorFunc}
nodeStrokeColorFunc={nodeStrokeColorFunc}
nodeClassNameFunc={nodeClassNameFunc}
nodeBorderRadius={nodeBorderRadius}
nodeStrokeWidth={nodeStrokeWidth}
NodeComponent={NodeComponent}
onClick={onClick}
shapeRendering={shapeRendering}
/>
))}
</>
);
}
const NodeComponentWrapper = memo(function NodeComponentWrapper({
id,
nodeOrigin,
nodeColorFunc,
nodeStrokeColorFunc,
nodeClassNameFunc,
nodeBorderRadius,
nodeStrokeWidth,
shapeRendering,
NodeComponent,
onClick,
}: {
id: string;
nodeOrigin: NodeOrigin;
nodeColorFunc: GetMiniMapNodeAttribute;
nodeStrokeColorFunc: GetMiniMapNodeAttribute;
nodeClassNameFunc: GetMiniMapNodeAttribute;
nodeBorderRadius: number;
nodeStrokeWidth?: number;
NodeComponent: ComponentType<MiniMapNodeProps>;
onClick: MiniMapNodesProps['onClick'];
shapeRendering: string;
}) {
const node = useStore((s) => s.nodeLookup.get(id));
if (!node || node.hidden || !(node.computed?.width || node.width) || !(node.computed?.height || node.height)) {
return null;
}
const positionOrigin = getNodePositionWithOrigin(node, node.origin || nodeOrigin).positionAbsolute;
return (
<NodeComponent
x={positionOrigin.x}
y={positionOrigin.y}
width={node.computed?.width ?? node.width ?? 0}
height={node.computed?.height ?? node.height ?? 0}
style={node.style}
selected={!!node.selected}
className={nodeClassNameFunc(node)}
color={nodeColorFunc(node)}
borderRadius={nodeBorderRadius}
strokeColor={nodeStrokeColorFunc(node)}
strokeWidth={nodeStrokeWidth}
shapeRendering={shapeRendering}
onClick={onClick}
id={node.id}
/>
);
});
export default memo(MiniMapNodes);
@@ -9,16 +9,20 @@ import { useNodeId } from '../../contexts/NodeIdContext';
import NodeToolbarPortal from './NodeToolbarPortal';
import { NodeToolbarProps } from './types';
const nodeEqualityFn = (a: Node | undefined, b: Node | undefined) =>
a?.computed?.positionAbsolute?.x === b?.computed?.positionAbsolute?.x &&
a?.computed?.positionAbsolute?.y === b?.computed?.positionAbsolute?.y &&
a?.width === b?.width &&
a?.height === b?.height &&
a?.selected === b?.selected &&
a?.[internalsSymbol]?.z === b?.[internalsSymbol]?.z;
const nodeEqualityFn = (a?: Node, b?: Node) =>
a?.computed?.positionAbsolute?.x !== b?.computed?.positionAbsolute?.x ||
a?.computed?.positionAbsolute?.y !== b?.computed?.positionAbsolute?.y ||
a?.computed?.width !== b?.computed?.width ||
a?.computed?.height !== b?.computed?.height ||
a?.selected !== b?.selected ||
a?.[internalsSymbol]?.z !== b?.[internalsSymbol]?.z;
const nodesEqualityFn = (a: Node[], b: Node[]) => {
return a.length === b.length && a.every((node, i) => nodeEqualityFn(node, b[i]));
if (a.length !== b.length) {
return false;
}
return !a.some((node, i) => nodeEqualityFn(node, b[i]));
};
const storeSelector = (state: ReactFlowState) => ({
@@ -0,0 +1,307 @@
import { memo, useState, useMemo, useRef, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import {
getMarkerId,
elementSelectionKeys,
XYHandle,
type Connection,
getEdgePosition,
errorMessages,
} from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import { getMouseHandler } from '../Edges/utils';
import type { EdgeWrapperProps } from '../../types';
import { builtinEdgeTypes } from './utils';
function EdgeWrapper({
id,
className,
type,
data,
onClick,
onEdgeDoubleClick,
selected,
animated,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
source,
target,
isSelectable,
hidden,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
edgeTypes,
onError,
}: EdgeWrapperProps): JSX.Element | null {
let edgeType = type || 'default';
let EdgeComponent = edgeTypes?.[edgeType] || builtinEdgeTypes[edgeType];
if (EdgeComponent === undefined) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
EdgeComponent = builtinEdgeTypes.default;
}
const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const edgePosition = useStore(
useCallback(
(state) => {
const sourceNode = state.nodeLookup.get(source);
const targetNode = state.nodeLookup.get(target);
if (!sourceNode || !targetNode) {
return null;
}
return getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: state.connectionMode,
onError: state.onError,
});
},
[source, target]
),
shallow
);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden || !edgePosition) {
return null;
}
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
const edge = edges.find((e) => e.id === id);
if (!edge) {
return;
}
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (edge.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
edgeRef.current?.blur();
} else {
addSelectedEdges([id]);
}
}
if (onClick) {
onClick(event, edge);
}
};
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
edges,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
const inactive = !isSelectable && !onClick;
const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
} else {
addSelectedEdges([id]);
}
}
};
return (
<g
className={cc([
'react-flow__edge',
`react-flow__edge-${type}`,
className,
{ selected, animated, inactive, updating: updateHover },
])}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : 'img'}
data-id={id}
data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (
<EdgeComponent
id={id}
source={source}
target={target}
selected={selected}
animated={animated}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
pathOptions={pathOptions}
interactionWidth={interactionWidth}
/>
)}
{isUpdatable && (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
)}
</g>
);
}
EdgeWrapper.displayName = 'EdgeWrapper';
export default memo(EdgeWrapper);
@@ -0,0 +1,17 @@
import type { ComponentType } from 'react';
import type { EdgeProps, EdgeTypes } from '../../types';
import {
BezierEdgeInternal,
StraightEdgeInternal,
StepEdgeInternal,
SmoothStepEdgeInternal,
SimpleBezierEdgeInternal,
} from '../Edges';
export const builtinEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal as ComponentType<EdgeProps>,
straight: StraightEdgeInternal as ComponentType<EdgeProps>,
step: StepEdgeInternal as ComponentType<EdgeProps>,
smoothstep: SmoothStepEdgeInternal as ComponentType<EdgeProps>,
simplebezier: SimpleBezierEdgeInternal as ComponentType<EdgeProps>,
};
@@ -1,290 +0,0 @@
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent, useCallback } from 'react';
import cc from 'classcat';
import { shallow } from 'zustand/shallow';
import { getMarkerId, elementSelectionKeys, XYHandle, type Connection, getEdgePosition } from '@xyflow/system';
import { useStoreApi, useStore } from '../../hooks/useStore';
import { ARIA_EDGE_DESC_KEY } from '../A11yDescriptions';
import { EdgeAnchor } from './EdgeAnchor';
import { getMouseHandler } from './utils';
import type { EdgeProps, WrapEdgeProps } from '../../types';
export default (EdgeComponent: ComponentType<EdgeProps>) => {
const EdgeWrapper = ({
id,
className,
type,
data,
onClick,
onEdgeDoubleClick,
selected,
animated,
label,
labelStyle,
labelShowBg,
labelBgStyle,
labelBgPadding,
labelBgBorderRadius,
style,
source,
target,
isSelectable,
hidden,
sourceHandleId,
targetHandleId,
onContextMenu,
onMouseEnter,
onMouseMove,
onMouseLeave,
edgeUpdaterRadius,
onEdgeUpdate,
onEdgeUpdateStart,
onEdgeUpdateEnd,
markerEnd,
markerStart,
rfId,
ariaLabel,
isFocusable,
isUpdatable,
pathOptions,
interactionWidth,
}: WrapEdgeProps): JSX.Element | null => {
const edgeRef = useRef<SVGGElement>(null);
const [updateHover, setUpdateHover] = useState<boolean>(false);
const [updating, setUpdating] = useState<boolean>(false);
const store = useStoreApi();
const edgePosition = useStore(
useCallback(
(state) => {
const sourceNode = state.nodeLookup.get(source);
const targetNode = state.nodeLookup.get(target);
if (!sourceNode || !targetNode) {
return null;
}
return getEdgePosition({
id,
sourceNode,
targetNode,
sourceHandle: sourceHandleId || null,
targetHandle: targetHandleId || null,
connectionMode: state.connectionMode,
onError: state.onError,
});
},
[source, target]
),
shallow
);
const markerStartUrl = useMemo(() => `url(#${getMarkerId(markerStart, rfId)})`, [markerStart, rfId]);
const markerEndUrl = useMemo(() => `url(#${getMarkerId(markerEnd, rfId)})`, [markerEnd, rfId]);
if (hidden || !edgePosition) {
return null;
}
const onEdgeClick = (event: React.MouseEvent<SVGGElement, MouseEvent>): void => {
const { edges, addSelectedEdges, unselectNodesAndEdges, multiSelectionActive } = store.getState();
const edge = edges.find((e) => e.id === id);
if (!edge) {
return;
}
if (isSelectable) {
store.setState({ nodesSelectionActive: false });
if (edge.selected && multiSelectionActive) {
unselectNodesAndEdges({ nodes: [], edges: [edge] });
edgeRef.current?.blur();
} else {
addSelectedEdges([id]);
}
}
if (onClick) {
onClick(event, edge);
}
};
const onEdgeDoubleClickHandler = getMouseHandler(id, store.getState, onEdgeDoubleClick);
const onEdgeContextMenu = getMouseHandler(id, store.getState, onContextMenu);
const onEdgeMouseEnter = getMouseHandler(id, store.getState, onMouseEnter);
const onEdgeMouseMove = getMouseHandler(id, store.getState, onMouseMove);
const onEdgeMouseLeave = getMouseHandler(id, store.getState, onMouseLeave);
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
}
const {
autoPanOnConnect,
domNode,
edges,
isValidConnection,
connectionMode,
connectionRadius,
lib,
onConnectStart,
onConnectEnd,
cancelConnection,
nodes,
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? target : source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const edge = edges.find((e) => e.id === id)!;
setUpdating(true);
onEdgeUpdateStart?.(event, edge, handleType);
const _onEdgeUpdateEnd = (evt: MouseEvent | TouchEvent) => {
setUpdating(false);
onEdgeUpdateEnd?.(evt, edge, handleType);
};
const onConnectEdge = (connection: Connection) => onEdgeUpdate?.(edge, connection);
XYHandle.onPointerDown(event.nativeEvent, {
autoPanOnConnect,
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
nodes,
isTarget,
edgeUpdaterType: handleType,
lib,
cancelConnection,
panBy,
isValidConnection,
onConnect: onConnectEdge,
onConnectStart,
onConnectEnd,
onEdgeUpdateEnd: _onEdgeUpdateEnd,
updateConnection,
getTransform: () => store.getState().transform,
});
};
const onEdgeUpdaterSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
const onEdgeUpdaterTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
const onEdgeUpdaterMouseEnter = () => setUpdateHover(true);
const onEdgeUpdaterMouseOut = () => setUpdateHover(false);
const inactive = !isSelectable && !onClick;
const onKeyDown = (event: KeyboardEvent) => {
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const { unselectNodesAndEdges, addSelectedEdges, edges } = store.getState();
const unselect = event.key === 'Escape';
if (unselect) {
edgeRef.current?.blur();
unselectNodesAndEdges({ edges: [edges.find((e) => e.id === id)!] });
} else {
addSelectedEdges([id]);
}
}
};
return (
<g
className={cc([
'react-flow__edge',
`react-flow__edge-${type}`,
className,
{ selected, animated, inactive, updating: updateHover },
])}
onClick={onEdgeClick}
onDoubleClick={onEdgeDoubleClickHandler}
onContextMenu={onEdgeContextMenu}
onMouseEnter={onEdgeMouseEnter}
onMouseMove={onEdgeMouseMove}
onMouseLeave={onEdgeMouseLeave}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : 'img'}
data-id={id}
data-testid={`rf__edge-${id}`}
aria-label={ariaLabel === null ? undefined : ariaLabel ? ariaLabel : `Edge from ${source} to ${target}`}
aria-describedby={isFocusable ? `${ARIA_EDGE_DESC_KEY}-${rfId}` : undefined}
ref={edgeRef}
>
{!updating && (
<EdgeComponent
id={id}
source={source}
target={target}
selected={selected}
animated={animated}
label={label}
labelStyle={labelStyle}
labelShowBg={labelShowBg}
labelBgStyle={labelBgStyle}
labelBgPadding={labelBgPadding}
labelBgBorderRadius={labelBgBorderRadius}
data={data}
style={style}
sourceX={edgePosition.sourceX}
sourceY={edgePosition.sourceY}
targetX={edgePosition.targetX}
targetY={edgePosition.targetY}
sourcePosition={edgePosition.sourcePosition}
targetPosition={edgePosition.targetPosition}
sourceHandleId={sourceHandleId}
targetHandleId={targetHandleId}
markerStart={markerStartUrl}
markerEnd={markerEndUrl}
pathOptions={pathOptions}
interactionWidth={interactionWidth}
/>
)}
{isUpdatable && (
<>
{(isUpdatable === 'source' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.sourcePosition}
centerX={edgePosition.sourceX}
centerY={edgePosition.sourceY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterSourceMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="source"
/>
)}
{(isUpdatable === 'target' || isUpdatable === true) && (
<EdgeAnchor
position={edgePosition.targetPosition}
centerX={edgePosition.targetX}
centerY={edgePosition.targetY}
radius={edgeUpdaterRadius}
onMouseDown={onEdgeUpdaterTargetMouseDown}
onMouseEnter={onEdgeUpdaterMouseEnter}
onMouseOut={onEdgeUpdaterMouseOut}
type="target"
/>
)}
</>
)}
</g>
);
};
EdgeWrapper.displayName = 'EdgeWrapper';
return memo(EdgeWrapper);
};
@@ -0,0 +1,256 @@
import { useEffect, useRef, memo, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import {
clampPosition,
elementSelectionKeys,
errorMessages,
getPositionWithOrigin,
internalsSymbol,
isInputDOMNode,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import useDrag from '../../hooks/useDrag';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { handleNodeClick } from '../Nodes/utils';
import type { NodeWrapperProps } from '../../types';
import { arrowKeyDiffs, builtinNodeTypes } from './utils';
const NodeWrapper = ({
id,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
nodesDraggable,
elementsSelectable,
nodesConnectable,
nodesFocusable,
resizeObserver,
noDragClassName,
noPanClassName,
disableKeyboardA11y,
rfId,
nodeTypes,
nodeExtent,
nodeOrigin,
onError,
}: NodeWrapperProps) => {
const node = useStore((s) => s.nodeLookup.get(id)!);
let nodeType = node.type || 'default';
let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType];
if (NodeComponent === undefined) {
onError?.('003', errorMessages['error003'](nodeType));
nodeType = 'default';
NodeComponent = builtinNodeTypes.default;
}
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 store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(node.sourcePosition);
const prevTargetPosition = useRef(node.targetPosition);
const prevType = useRef(nodeType);
const updatePositions = useUpdateNodePositions();
useEffect(() => {
if (nodeRef.current && !node.hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, [node.hidden]);
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== nodeType;
const sourcePosChanged = prevSourcePosition.current !== node.sourcePosition;
const targetPosChanged = prevTargetPosition.current !== node.targetPosition;
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = nodeType;
}
if (sourcePosChanged) {
prevSourcePosition.current = node.sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = node.targetPosition;
}
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
}
}, [id, nodeType, node.sourcePosition, node.targetPosition]);
const dragging = useDrag({
nodeRef,
disabled: node.hidden || !isDraggable,
noDragClassName,
handleSelector: node.dragHandle,
nodeId: id,
isSelectable,
});
if (node.hidden) {
return null;
}
const width = node.width ?? undefined;
const height = node.height ?? undefined;
const computedWidth = node.computed?.width;
const computedHeight = node.computed?.height;
const positionAbsolute = nodeExtent
? clampPosition(node.computed?.positionAbsolute, nodeExtent)
: node.computed?.positionAbsolute || { x: 0, y: 0 };
const positionAbsoluteOrigin = getPositionWithOrigin({
x: positionAbsolute.x,
y: positionAbsolute.y,
width: computedWidth ?? width ?? 0,
height: computedHeight ?? height ?? 0,
origin: node.origin || nodeOrigin,
});
const initialized = (!!computedWidth && !!computedHeight) || (!!width && !!height);
const zIndex = node[internalsSymbol]?.z ?? 0;
const isParent = !!node[internalsSymbol]?.isParent;
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const onMouseEnterHandler =
onMouseEnter === undefined ? undefined : (event: MouseEvent) => onMouseEnter(event, { ...node });
const onMouseMoveHandler =
onMouseMove === undefined ? undefined : (event: MouseEvent) => onMouseMove(event, { ...node });
const onMouseLeaveHandler =
onMouseLeave === undefined ? undefined : (event: MouseEvent) => onMouseLeave(event, { ...node });
const onContextMenuHandler =
onContextMenu === undefined ? undefined : (event: MouseEvent) => onContextMenu(event, { ...node });
const onDoubleClickHandler =
onDoubleClick === undefined ? undefined : (event: MouseEvent) => onDoubleClick(event, { ...node });
const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
onClick(event, { ...node });
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event.nativeEvent)) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
} else if (
!disableKeyboardA11y &&
isDraggable &&
node.selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`,
});
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${nodeType}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
node.className,
{
selected: node.selected,
selectable: isSelectable,
parent: isParent,
dragging,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
width,
height,
...node.style,
}}
data-id={id}
data-testid={`rf__node-${id}`}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={node.ariaLabel}
>
<Provider value={id}>
<NodeComponent
id={id}
data={node.data}
type={nodeType}
width={computedWidth}
height={computedHeight}
positionAbsoluteX={positionAbsolute.x}
positionAbsoluteY={positionAbsolute.y}
selected={node.selected}
isConnectable={isConnectable}
sourcePosition={node.sourcePosition}
targetPosition={node.targetPosition}
dragging={dragging}
dragHandle={node.dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
NodeWrapper.displayName = 'NodeWrapper';
export default memo(NodeWrapper);
@@ -0,0 +1,22 @@
import type { ComponentType } from 'react';
import type { NodeProps, XYPosition } from '@xyflow/system';
import InputNode from '../Nodes/InputNode';
import DefaultNode from '../Nodes/DefaultNode';
import GroupNode from '../Nodes/GroupNode';
import OutputNode from '../Nodes/OutputNode';
import type { NodeTypes } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export const builtinNodeTypes: NodeTypes = {
input: InputNode as ComponentType<NodeProps>,
default: DefaultNode as ComponentType<NodeProps>,
output: OutputNode as ComponentType<NodeProps>,
group: GroupNode as ComponentType<NodeProps>,
};
+2 -15
View File
@@ -1,22 +1,9 @@
import type { MouseEvent, RefObject } from 'react';
import type { RefObject } from 'react';
import type { StoreApi } from 'zustand';
import type { Node, ReactFlowState } from '../../types';
import type { ReactFlowState } from '../../types';
import { errorMessages } from '@xyflow/system';
export function getMouseHandler(
id: string,
getState: StoreApi<ReactFlowState>['getState'],
handler?: (event: MouseEvent, node: Node) => void
) {
return handler === undefined
? handler
: (event: MouseEvent) => {
const node = getState().nodeLookup.get(id)!;
handler(event, { ...node });
};
}
// this handler is called by
// 1. the click handler when node is not draggable or selectNodesOnDrag = false
// or
@@ -1,232 +0,0 @@
import { useEffect, useRef, memo, type ComponentType, type MouseEvent, type KeyboardEvent } from 'react';
import cc from 'classcat';
import { elementSelectionKeys, isInputDOMNode, type NodeProps, type XYPosition } from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { Provider } from '../../contexts/NodeIdContext';
import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions';
import useDrag from '../../hooks/useDrag';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import { getMouseHandler, handleNodeClick } from './utils';
import type { WrapNodeProps } from '../../types';
export const arrowKeyDiffs: Record<string, XYPosition> = {
ArrowUp: { x: 0, y: -1 },
ArrowDown: { x: 0, y: 1 },
ArrowLeft: { x: -1, y: 0 },
ArrowRight: { x: 1, y: 0 },
};
export default (NodeComponent: ComponentType<NodeProps>) => {
const NodeWrapper = ({
id,
type,
data,
xPos,
yPos,
xPosOrigin,
yPosOrigin,
selected,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onDoubleClick,
style,
className,
isDraggable,
isSelectable,
isConnectable,
isFocusable,
sourcePosition,
targetPosition,
hidden,
resizeObserver,
dragHandle,
zIndex,
isParent,
noDragClassName,
noPanClassName,
initialized,
disableKeyboardA11y,
ariaLabel,
rfId,
positionAbsolute,
width,
height,
}: WrapNodeProps) => {
const store = useStoreApi();
const nodeRef = useRef<HTMLDivElement>(null);
const prevSourcePosition = useRef(sourcePosition);
const prevTargetPosition = useRef(targetPosition);
const prevType = useRef(type);
const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave;
const updatePositions = useUpdateNodePositions();
const onMouseEnterHandler = getMouseHandler(id, store.getState, onMouseEnter);
const onMouseMoveHandler = getMouseHandler(id, store.getState, onMouseMove);
const onMouseLeaveHandler = getMouseHandler(id, store.getState, onMouseLeave);
const onContextMenuHandler = getMouseHandler(id, store.getState, onContextMenu);
const onDoubleClickHandler = getMouseHandler(id, store.getState, onDoubleClick);
const onSelectNodeHandler = (event: MouseEvent) => {
const { selectNodesOnDrag, nodeDragThreshold } = store.getState();
if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) {
// this handler gets called by XYDrag on drag start when selectNodesOnDrag=true
// here we only need to call it when selectNodesOnDrag=false
handleNodeClick({
id,
store,
nodeRef,
});
}
if (onClick) {
const node = store.getState().nodes.find((n) => n.id === id)!;
onClick(event, { ...node });
}
};
const onKeyDown = (event: KeyboardEvent) => {
if (isInputDOMNode(event.nativeEvent)) {
return;
}
if (elementSelectionKeys.includes(event.key) && isSelectable) {
const unselect = event.key === 'Escape';
handleNodeClick({
id,
store,
unselect,
nodeRef,
});
} else if (
!disableKeyboardA11y &&
isDraggable &&
selected &&
Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)
) {
store.setState({
ariaLiveMessage: `Moved selected node ${event.key
.replace('Arrow', '')
.toLowerCase()}. New position, x: ${~~xPos}, y: ${~~yPos}`,
});
updatePositions({
x: arrowKeyDiffs[event.key].x,
y: arrowKeyDiffs[event.key].y,
isShiftPressed: event.shiftKey,
});
}
};
useEffect(() => {
if (nodeRef.current && !hidden) {
const currNode = nodeRef.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, [hidden]);
useEffect(() => {
// when the user programmatically changes the source or handle position, we re-initialize the node
const typeChanged = prevType.current !== type;
const sourcePosChanged = prevSourcePosition.current !== sourcePosition;
const targetPosChanged = prevTargetPosition.current !== targetPosition;
if (nodeRef.current && (typeChanged || sourcePosChanged || targetPosChanged)) {
if (typeChanged) {
prevType.current = type;
}
if (sourcePosChanged) {
prevSourcePosition.current = sourcePosition;
}
if (targetPosChanged) {
prevTargetPosition.current = targetPosition;
}
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
}
}, [id, type, sourcePosition, targetPosition]);
const dragging = useDrag({
nodeRef,
disabled: hidden || !isDraggable,
noDragClassName,
handleSelector: dragHandle,
nodeId: id,
isSelectable,
});
if (hidden) {
return null;
}
return (
<div
className={cc([
'react-flow__node',
`react-flow__node-${type}`,
{
// this is overwritable by passing `nopan` as a class name
[noPanClassName]: isDraggable,
},
className,
{
selected,
selectable: isSelectable,
parent: isParent,
dragging,
},
])}
ref={nodeRef}
style={{
zIndex,
transform: `translate(${xPosOrigin}px,${yPosOrigin}px)`,
pointerEvents: hasPointerEvents ? 'all' : 'none',
visibility: initialized ? 'visible' : 'hidden',
width,
height,
...style,
}}
data-id={id}
data-testid={`rf__node-${id}`}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onDoubleClickHandler}
onKeyDown={isFocusable ? onKeyDown : undefined}
tabIndex={isFocusable ? 0 : undefined}
role={isFocusable ? 'button' : undefined}
aria-describedby={disableKeyboardA11y ? undefined : `${ARIA_NODE_DESC_KEY}-${rfId}`}
aria-label={ariaLabel}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
width={width}
height={height}
positionAbsolute={positionAbsolute}
selected={selected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
dragging={dragging}
dragHandle={dragHandle}
zIndex={zIndex}
/>
</Provider>
</div>
);
};
NodeWrapper.displayName = 'NodeWrapper';
return memo(NodeWrapper);
};
@@ -10,9 +10,9 @@ import { getNodesBounds } from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
import useDrag from '../../hooks/useDrag';
import { arrowKeyDiffs } from '../Nodes/wrapNode';
import useUpdateNodePositions from '../../hooks/useUpdateNodePositions';
import type { Node, ReactFlowState } from '../../types';
import { arrowKeyDiffs } from '../NodeWrapper/utils';
export type NodesSelectionProps = {
onSelectionContextMenu?: (event: MouseEvent, nodes: Node[]) => void;
@@ -1,13 +1,13 @@
import { memo, ReactNode } from 'react';
import { shallow } from 'zustand/shallow';
import cc from 'classcat';
import { errorMessages } from '@xyflow/system';
import { useStore } from '../../hooks/useStore';
import useVisibleEdges from '../../hooks/useVisibleEdges';
import MarkerDefinitions from './MarkerDefinitions';
import { GraphViewProps } from '../GraphView';
import type { EdgeTypesWrapped, ReactFlowState } from '../../types';
import type { ReactFlowState } from '../../types';
import EdgeWrapper from '../../components/EdgeWrapper';
type EdgeRendererProps = Pick<
GraphViewProps,
@@ -27,8 +27,8 @@ type EdgeRendererProps = Pick<
| 'elevateEdgesOnSelect'
| 'rfId'
| 'disableKeyboardA11y'
| 'edgeTypes'
> & {
edgeTypes: EdgeTypesWrapped;
elevateEdgesOnSelect: boolean;
children: ReactNode;
};
@@ -74,14 +74,6 @@ const EdgeRenderer = ({
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
<>
{edges.map((edge) => {
let edgeType = edge.type || 'default';
if (!edgeTypes[edgeType]) {
onError?.('011', errorMessages['error011'](edgeType));
edgeType = 'default';
}
const EdgeComponent = edgeTypes[edgeType];
const isFocusable = !!(edge.focusable || (edgesFocusable && typeof edge.focusable === 'undefined'));
const isUpdatable =
typeof onEdgeUpdate !== 'undefined' &&
@@ -92,7 +84,7 @@ const EdgeRenderer = ({
);
return (
<EdgeComponent
<EdgeWrapper
key={edge.id}
id={edge.id}
className={cc([edge.className, noPanClassName])}
@@ -131,6 +123,8 @@ const EdgeRenderer = ({
isUpdatable={isUpdatable}
pathOptions={'pathOptions' in edge ? edge.pathOptions : undefined}
interactionWidth={edge.interactionWidth}
onError={onError}
edgeTypes={edgeTypes}
/>
);
})}
@@ -1,37 +0,0 @@
import type { ComponentType } from 'react';
import {
BezierEdgeInternal,
SmoothStepEdgeInternal,
StepEdgeInternal,
StraightEdgeInternal,
SimpleBezierEdgeInternal,
} from '../../components/Edges';
import wrapEdge from '../../components/Edges/wrapEdge';
import type { EdgeProps, EdgeTypes, EdgeTypesWrapped } from '../../types';
export type CreateEdgeTypes = (edgeTypes: EdgeTypes) => EdgeTypesWrapped;
export function createEdgeTypes(edgeTypes: EdgeTypes): EdgeTypesWrapped {
const standardTypes: EdgeTypesWrapped = {
default: wrapEdge((edgeTypes.default || BezierEdgeInternal) as ComponentType<EdgeProps>),
straight: wrapEdge((edgeTypes.bezier || StraightEdgeInternal) as ComponentType<EdgeProps>),
step: wrapEdge((edgeTypes.step || StepEdgeInternal) as ComponentType<EdgeProps>),
smoothstep: wrapEdge((edgeTypes.step || SmoothStepEdgeInternal) as ComponentType<EdgeProps>),
simplebezier: wrapEdge((edgeTypes.simplebezier || SimpleBezierEdgeInternal) 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] || BezierEdgeInternal) as ComponentType<EdgeProps>);
return res;
}, wrappedTypes);
return {
...standardTypes,
...specialTypes,
};
}
@@ -8,19 +8,15 @@ import useOnInitHandler from '../../hooks/useOnInitHandler';
import useViewportSync from '../../hooks/useViewportSync';
import ConnectionLine from '../../components/ConnectionLine';
import type { ReactFlowProps } from '../../types';
import { createNodeTypes } from '../NodeRenderer/utils';
import { createEdgeTypes } from '../EdgeRenderer/utils';
import { useNodeOrEdgeTypes } from './utils';
import useNodeOrEdgeTypesWarning from './useNodeOrEdgeTypesWarning';
export type GraphViewProps = Omit<
ReactFlowProps,
'onSelectionChange' | 'nodes' | 'edges' | 'nodeTypes' | 'edgeTypes' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
'onSelectionChange' | 'nodes' | 'edges' | 'onMove' | 'onMoveStart' | 'onMoveEnd'
> &
Required<
Pick<
ReactFlowProps,
| 'nodeTypes'
| 'edgeTypes'
| 'selectionKeyCode'
| 'deleteKeyCode'
| 'multiSelectionKeyCode'
@@ -108,8 +104,8 @@ const GraphView = ({
viewport,
onViewportChange,
}: GraphViewProps) => {
const nodeTypesWrapped = useNodeOrEdgeTypes(nodeTypes, createNodeTypes);
const edgeTypesWrapped = useNodeOrEdgeTypes(edgeTypes, createEdgeTypes);
useNodeOrEdgeTypesWarning(nodeTypes);
useNodeOrEdgeTypesWarning(edgeTypes);
useOnInitHandler(onInit);
useViewportSync(viewport);
@@ -154,7 +150,7 @@ const GraphView = ({
>
<ViewportWrapper>
<EdgeRenderer
edgeTypes={edgeTypesWrapped}
edgeTypes={edgeTypes}
onEdgeClick={onEdgeClick}
onEdgeDoubleClick={onEdgeDoubleClick}
onEdgeUpdate={onEdgeUpdate}
@@ -182,7 +178,7 @@ const GraphView = ({
<div className="react-flow__edgelabel-renderer" />
<NodeRenderer
nodeTypes={nodeTypesWrapped}
nodeTypes={nodeTypes}
onNodeClick={onNodeClick}
onNodeDoubleClick={onNodeDoubleClick}
onNodeMouseEnter={onNodeMouseEnter}
@@ -0,0 +1,27 @@
import { useEffect, useRef } from 'react';
import { errorMessages } from '@xyflow/system';
import type { EdgeTypes, NodeTypes } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
const emptyTypes = {};
/*
* This hook warns the user if node or edgeTypes change.
*/
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: NodeTypes): void;
export function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes?: EdgeTypes): void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function useNodeOrEdgeTypesWarning(nodeOrEdgeTypes: any = emptyTypes): any {
const updateCount = useRef(0);
const store = useStoreApi();
useEffect(() => {
if (process.env.NODE_ENV === 'development') {
if (updateCount.current > 1) {
store.getState().onError?.('002', errorMessages['error002']());
}
updateCount.current += 1;
}
}, [nodeOrEdgeTypes]);
}
@@ -1,31 +0,0 @@
import { useMemo, useRef } from 'react';
import { shallow } from 'zustand/shallow';
import { errorMessages } from '@xyflow/system';
import { CreateEdgeTypes } from '../EdgeRenderer/utils';
import { CreateNodeTypes } from '../NodeRenderer/utils';
import type { EdgeTypes, EdgeTypesWrapped, NodeTypes, NodeTypesWrapped } from '../../types';
import { useStoreApi } from '../../hooks/useStore';
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 store = useStoreApi();
const typesParsed = useMemo(() => {
if (process.env.NODE_ENV === 'development') {
const typeKeys = Object.keys(nodeOrEdgeTypes);
if (shallow(typesKeysRef.current, typeKeys)) {
store.getState().onError?.('002', errorMessages['error002']());
}
typesKeysRef.current = typeKeys;
}
return createTypes(nodeOrEdgeTypes);
}, [nodeOrEdgeTypes]);
return typesParsed;
}
@@ -1,14 +1,15 @@
import { memo, useMemo, useEffect, useRef, type ComponentType } from 'react';
import { memo } from 'react';
import { shallow } from 'zustand/shallow';
import { internalsSymbol, errorMessages, Position, clampPosition, getPositionWithOrigin } from '@xyflow/system';
import useVisibleNodes from '../../hooks/useVisibleNodes';
import useVisibleNodesIds from '../../hooks/useVisibleNodes';
import { useStore } from '../../hooks/useStore';
import { containerStyle } from '../../styles/utils';
import { GraphViewProps } from '../GraphView';
import type { NodeTypesWrapped, ReactFlowState, WrapNodeProps } from '../../types';
import type { ReactFlowState } from '../../types';
import useResizeObserver from './useResizeObserver';
import NodeWrapper from '../../components/NodeWrapper';
type NodeRendererProps = Pick<
export type NodeRendererProps = Pick<
GraphViewProps,
| 'onNodeClick'
| 'onNodeDoubleClick'
@@ -23,127 +24,71 @@ type NodeRendererProps = Pick<
| 'disableKeyboardA11y'
| 'nodeOrigin'
| 'nodeExtent'
> & {
nodeTypes: NodeTypesWrapped;
};
| 'nodeTypes'
>;
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 = 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();
};
}, []);
const { nodesDraggable, nodesConnectable, nodesFocusable, elementsSelectable, onError } = useStore(selector, shallow);
const nodeIds = useVisibleNodesIds(props.onlyRenderVisibleElements);
const resizeObserver = useResizeObserver();
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.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);
{nodeIds.map((nodeId) => {
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 }}
// The split of responsibilities between NodeRenderer and
// NodeComponentWrapper may appear weird. However, its designed to
// minimize the cost of updates when individual nodes change.
//
// For example, when youre dragging a single node, that node gets
// updated multiple times per second. If `NodeRenderer` were to update
// every time, it would have to re-run the `nodes.map()` loop every
// time. This gets pricey with hundreds of nodes, especially if every
// loop cycle does more than just rendering a JSX element!
//
// As a result of this choice, we took the following implementation
// decisions:
// - NodeRenderer subscribes *only* to node IDs and therefore
// rerender *only* when visible nodes are added or removed.
// - NodeRenderer performs all operations the result of which can be
// shared between nodes (such as creating the `ResizeObserver`
// instance, or subscribing to `selector`). This means extra prop
// drilling into `NodeComponentWrapper`, but it means we need to run
// these operations only once instead of once per node.
// - Any operations that youd normally write inside `nodes.map` are
// moved into `NodeComponentWrapper`. This ensures they are
// memorized so if `NodeRenderer` *has* to rerender, it only
// needs to regenerate the list of nodes, nothing else.
<NodeWrapper
key={nodeId}
id={nodeId}
nodeTypes={props.nodeTypes}
nodeExtent={props.nodeExtent}
nodeOrigin={props.nodeOrigin}
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={initialized}
rfId={props.rfId}
disableKeyboardA11y={props.disableKeyboardA11y}
ariaLabel={node.ariaLabel}
resizeObserver={resizeObserver}
nodesDraggable={nodesDraggable}
nodesConnectable={nodesConnectable}
nodesFocusable={nodesFocusable}
elementsSelectable={elementsSelectable}
onError={onError}
/>
);
})}
@@ -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,34 +0,0 @@
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,
};
}
@@ -12,40 +12,15 @@ import {
} from '@xyflow/system';
import Attribution from '../../components/Attribution';
import {
BezierEdgeInternal,
SmoothStepEdgeInternal,
StepEdgeInternal,
StraightEdgeInternal,
SimpleBezierEdgeInternal,
} 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 GraphView from '../GraphView';
import Wrapper from './Wrapper';
import type { EdgeTypes, NodeTypes, ReactFlowProps, ReactFlowRefType } from '../../types';
import type { ReactFlowProps, ReactFlowRefType } from '../../types';
import useColorModeClass from '../../hooks/useColorModeClass';
const defaultNodeTypes: NodeTypes = {
input: InputNode,
default: DefaultNode,
output: OutputNode,
group: GroupNode,
};
const defaultEdgeTypes: EdgeTypes = {
default: BezierEdgeInternal,
straight: StraightEdgeInternal,
step: StepEdgeInternal,
smoothstep: SmoothStepEdgeInternal,
simplebezier: SimpleBezierEdgeInternal,
};
const initNodeOrigin: NodeOrigin = [0, 0];
const initSnapGrid: [number, number] = [15, 15];
const initDefaultViewport: Viewport = { x: 0, y: 0, zoom: 1 };
@@ -66,8 +41,8 @@ const ReactFlow = forwardRef<ReactFlowRefType, ReactFlowProps>(
defaultNodes,
defaultEdges,
className,
nodeTypes = defaultNodeTypes,
edgeTypes = defaultEdgeTypes,
nodeTypes,
edgeTypes,
onNodeClick,
onEdgeClick,
onInit,
+14 -13
View File
@@ -1,21 +1,22 @@
import { useCallback } from 'react';
import { getNodesInside } from '@xyflow/system';
import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore';
import type { Node, ReactFlowState } from '../types';
import { useCallback } from 'react';
function useVisibleNodes(onlyRenderVisible: boolean) {
const nodes = useStore(
useCallback(
(s: ReactFlowState) =>
onlyRenderVisible
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true)
: s.nodes,
[onlyRenderVisible]
)
);
const selector = (onlyRenderVisible: boolean) => (s: ReactFlowState) => {
return onlyRenderVisible
? getNodesInside<Node>(s.nodes, { x: 0, y: 0, width: s.width, height: s.height }, s.transform, true).map(
(node) => node.id
)
: Array.from(s.nodeLookup.keys());
};
return nodes;
function useVisibleNodeIds(onlyRenderVisible: boolean) {
const nodeIds = useStore(useCallback(selector(onlyRenderVisible), [onlyRenderVisible]), shallow);
return nodeIds;
}
export default useVisibleNodes;
export default useVisibleNodeIds;
+13 -7
View File
@@ -2,7 +2,7 @@ import { createWithEqualityFn } from 'zustand/traditional';
import {
clampPosition,
fitView as fitViewSystem,
updateNodes,
adoptUserProvidedNodes,
updateAbsolutePositions,
panBy as panBySystem,
Dimensions,
@@ -43,11 +43,16 @@ const createRFStore = ({
...getInitialState({ nodes, edges, width, height, fitView }),
setNodes: (nodes: Node[]) => {
const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get();
// Whenver new nodes are set, we need to calculate the absolute positions of the nodes
// and update the nodeLookup.
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
// setNodes() is called exclusively in response to user actions:
// - either when the `<ReactFlow nodes>` prop is updated in the controlled ReactFlow setup,
// - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup.
//
// When this happens, we take the note objects passed by the user and extend them with fields
// relevant for internal React Flow operations.
// TODO: consider updating the types to reflect the distinction between user-provided nodes and internal nodes.
const nodesWithInternalData = adoptUserProvidedNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect });
set({ nodes: nextNodes });
set({ nodes: nodesWithInternalData });
},
setEdges: (edges: Edge[]) => {
const { defaultEdgeOptions = {}, connectionLookup } = get();
@@ -74,7 +79,8 @@ const createRFStore = ({
};
if (hasDefaultNodes) {
nextState.nodes = updateNodes(nodes, new Map(), {
const { nodeLookup } = get();
nextState.nodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: get().nodeOrigin,
elevateNodesOnSelect: get().elevateNodesOnSelect,
});
@@ -168,7 +174,7 @@ const createRFStore = ({
if (changes?.length) {
if (hasDefaultNodes) {
const updatedNodes = applyNodeChanges(changes, nodes);
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
const nextNodes = adoptUserProvidedNodes(updatedNodes, nodeLookup, {
nodeOrigin,
elevateNodesOnSelect,
});
+5 -2
View File
@@ -1,7 +1,7 @@
import {
infiniteExtent,
ConnectionMode,
updateNodes,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
Transform,
@@ -25,7 +25,10 @@ const getInitialState = ({
} = {}): ReactFlowStore => {
const nodeLookup = new Map();
const connectionLookup = updateConnectionLookup(new Map(), edges);
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false,
});
let transform: Transform = [0, 0, 1];
+6 -3
View File
@@ -15,9 +15,10 @@ import type {
EdgePosition,
Optional,
StepPathOptions,
OnError,
} from '@xyflow/system';
import { Node } from '.';
import { EdgeTypes, Node } from '.';
export type EdgeLabelOptions = {
label?: string | ReactNode;
@@ -57,7 +58,7 @@ export type Edge<T = any> = DefaultEdge<T> | SmoothStepEdgeType<T> | BezierEdgeT
export type EdgeMouseHandler = (event: ReactMouseEvent, edge: Edge) => void;
export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & {
export type EdgeWrapperProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandle'> & {
onClick?: EdgeMouseHandler;
onEdgeDoubleClick?: EdgeMouseHandler;
sourceHandleId?: string | null;
@@ -75,6 +76,8 @@ export type WrapEdgeProps<T = any> = Omit<Edge<T>, 'sourceHandle' | 'targetHandl
isUpdatable: EdgeUpdatable;
isSelectable: boolean;
pathOptions?: BezierPathOptions | SmoothStepPathOptions;
edgeTypes?: EdgeTypes;
onError?: OnError;
};
export type DefaultEdgeOptions = DefaultEdgeOptionsBase<Edge>;
@@ -90,7 +93,7 @@ export type EdgeProps<T = any> = Pick<
Edge<T>,
'id' | 'animated' | 'data' | 'style' | 'selected' | 'source' | 'target'
> &
Pick<WrapEdgeProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
Pick<EdgeWrapperProps, 'sourceHandleId' | 'targetHandleId' | 'interactionWidth'> &
EdgePosition &
EdgeLabelOptions & {
markerStart?: string;
+2 -4
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { ComponentType, MemoExoticComponent } from 'react';
import type { ComponentType } from 'react';
import {
FitViewParamsBase,
FitViewOptionsBase,
@@ -14,7 +14,7 @@ import {
XYPosition,
} from '@xyflow/system';
import type { NodeChange, EdgeChange, Node, WrapNodeProps, Edge, EdgeProps, WrapEdgeProps, ReactFlowInstance } from '.';
import type { NodeChange, EdgeChange, Node, Edge, EdgeProps, ReactFlowInstance } from '.';
export type OnNodesChange = (changes: NodeChange[]) => void;
export type OnEdgesChange = (changes: EdgeChange[]) => void;
@@ -24,9 +24,7 @@ export type OnEdgesDelete = (edges: Edge[]) => void;
export type OnDelete = (params: { nodes: Node[]; edges: Edge[] }) => void;
export type NodeTypes = { [key: string]: ComponentType<NodeProps> };
export type NodeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapNodeProps>> };
export type EdgeTypes = { [key: string]: ComponentType<EdgeProps> };
export type EdgeTypesWrapped = { [key: string]: MemoExoticComponent<ComponentType<WrapEdgeProps>> };
export type UnselectNodesAndEdgesParams = {
nodes?: Node[];
+24 -31
View File
@@ -1,5 +1,6 @@
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
import type { NodeBase, XYPosition } from '@xyflow/system';
import type { CoordinateExtent, NodeBase, NodeOrigin, OnError } from '@xyflow/system';
import { NodeTypes } from './general';
export type Node<NodeData = any, NodeType extends string | undefined = string | undefined> = NodeBase<
NodeData,
@@ -14,33 +15,25 @@ export type NodeMouseHandler = (event: ReactMouseEvent, node: Node) => void;
export type NodeDragHandler = (event: ReactMouseEvent, node: Node, nodes: Node[]) => void;
export type SelectionDragHandler = (event: ReactMouseEvent, nodes: Node[]) => void;
export type WrapNodeProps<NodeData = any> = Pick<
Node<NodeData>,
'id' | 'data' | 'style' | 'className' | 'dragHandle' | 'sourcePosition' | 'targetPosition' | 'hidden' | 'ariaLabel'
> &
Required<Pick<Node<NodeData>, 'selected' | 'type' | 'zIndex'>> & {
isConnectable: boolean;
xPos: number;
yPos: number;
xPosOrigin: number;
yPosOrigin: number;
positionAbsolute: XYPosition;
initialized: boolean;
isSelectable: boolean;
isDraggable: boolean;
isFocusable: boolean;
onClick?: NodeMouseHandler;
onDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
resizeObserver: ResizeObserver | null;
isParent: boolean;
noDragClassName: string;
noPanClassName: string;
rfId: string;
disableKeyboardA11y: boolean;
width?: number;
height?: number;
};
export type NodeWrapperProps = {
id: string;
nodesConnectable: boolean;
elementsSelectable: boolean;
nodesDraggable: boolean;
nodesFocusable: boolean;
onClick?: NodeMouseHandler;
onDoubleClick?: NodeMouseHandler;
onMouseEnter?: NodeMouseHandler;
onMouseMove?: NodeMouseHandler;
onMouseLeave?: NodeMouseHandler;
onContextMenu?: NodeMouseHandler;
resizeObserver: ResizeObserver | null;
noDragClassName: string;
noPanClassName: string;
rfId: string;
disableKeyboardA11y: boolean;
nodeTypes?: NodeTypes;
nodeExtent?: CoordinateExtent;
nodeOrigin: NodeOrigin;
onError?: OnError;
};
+14 -11
View File
@@ -55,14 +55,17 @@ function applyChanges(changes: any[], elements: any[]): any[] {
}
let remainingChanges = changes;
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
const updatedElements: any[] = [];
return elements.reduce((res: any[], item: any) => {
for (let i = 0; i < elements.length; i++) {
const nextChanges: any[] = [];
const _remainingChanges: any[] = [];
const item = elements[i];
remainingChanges.forEach((c) => {
if (c.id === item.id) {
if (c.type === 'add') {
updatedElements.push(c.item);
} else if (c.id === item.id) {
nextChanges.push(c);
} else {
_remainingChanges.push(c);
@@ -71,8 +74,8 @@ function applyChanges(changes: any[], elements: any[]): any[] {
remainingChanges = _remainingChanges;
if (nextChanges.length === 0) {
res.push(item);
return res;
updatedElements.push(item);
continue;
}
const updateItem = { ...item };
@@ -101,7 +104,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
handleParentExpand(updatedElements, updateItem);
}
break;
}
@@ -123,20 +126,20 @@ function applyChanges(changes: any[], elements: any[]): any[] {
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
handleParentExpand(updatedElements, updateItem);
}
break;
}
case 'remove': {
return res;
continue;
}
}
}
updatedElements.push(updateItem);
}
}
res.push(updateItem);
return res;
}, initElements);
return updatedElements;
}
export function applyNodeChanges<NodeData = any>(changes: NodeChange[], nodes: Node<NodeData>[]): Node<NodeData>[] {
@@ -191,7 +191,8 @@
{dragging}
{dragHandle}
isConnectable={connectable}
positionAbsolute={{ x: positionX, y: positionY }}
positionAbsoluteX={positionX}
positionAbsoluteY={positionY}
{width}
{height}
/>
@@ -18,10 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = {
x: 0,
y: 0
};
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this
@@ -33,7 +31,8 @@
zIndex;
dragging;
dragHandle;
positionAbsolute;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script>
@@ -15,10 +15,8 @@
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = {
x: 0,
y: 0
};
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this
@@ -33,6 +31,7 @@
zIndex;
dragging;
dragHandle;
positionAbsolute;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script>
@@ -18,10 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = {
x: 0,
y: 0
};
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this
@@ -34,7 +32,8 @@
zIndex;
dragging;
dragHandle;
positionAbsolute;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script>
@@ -18,7 +18,8 @@
export let zIndex: $$Props['zIndex'] = undefined;
export let dragging: $$Props['dragging'] = false;
export let dragHandle: $$Props['dragHandle'] = undefined;
export let positionAbsolute: $$Props['positionAbsolute'] = { x: 0, y: 0 };
export let positionAbsoluteX: $$Props['positionAbsoluteX'] = 0;
export let positionAbsoluteY: $$Props['positionAbsoluteY'] = 0;
export let isConnectable: $$Props['isConnectable'] = undefined;
// @todo: there must be a better way to do this
@@ -31,7 +32,8 @@
zIndex;
dragging;
dragHandle;
positionAbsolute;
positionAbsoluteX;
positionAbsoluteY;
isConnectable;
</script>
@@ -15,7 +15,7 @@ import {
type OnError,
devWarn,
type Viewport,
updateNodes,
adoptUserProvidedNodes,
getNodesBounds,
getViewportForBounds,
updateConnectionLookup,
@@ -78,7 +78,7 @@ export const getInitialStore = ({
fitView?: boolean;
}) => {
const nodeLookup = new Map();
const nextNodes = updateNodes(nodes, nodeLookup, {
const nextNodes = adoptUserProvidedNodes(nodes, nodeLookup, {
nodeOrigin: [0, 0],
elevateNodesOnSelect: false
});
+4 -4
View File
@@ -7,11 +7,11 @@ import {
get
} from 'svelte/store';
import {
updateNodes,
adoptUserProvidedNodes,
updateConnectionLookup,
type Viewport,
type PanZoomInstance,
type ConnectionLookup,
updateConnectionLookup
type ConnectionLookup
} from '@xyflow/system';
import type { DefaultEdgeOptions, DefaultNodeOptions, Edge, Node } from '$lib/types';
@@ -139,7 +139,7 @@ export const createNodesStore = (
let elevateNodesOnSelect = true;
const _set = (nds: Node[]): Node[] => {
const nextNodes = updateNodes(nds, nodeLookup, {
const nextNodes = adoptUserProvidedNodes(nds, nodeLookup, {
elevateNodesOnSelect,
defaults
});
+6 -1
View File
@@ -40,6 +40,10 @@ export type NodeBase<T = any, U extends string | undefined = string | undefined>
z?: number;
handleBounds?: NodeHandleBounds;
isParent?: boolean;
/** Holds a reference to the original node object provided by the user
* (which may lack some fields, like `computed` or `[internalSymbol]`. Used
* as an optimization to avoid certain operations. */
userProvidedNode: NodeBase<T, U>;
};
};
@@ -52,7 +56,8 @@ export type NodeProps<T = any> = {
selected: NodeBase['selected'];
isConnectable: NodeBase['connectable'];
zIndex: NodeBase['zIndex'];
positionAbsolute: XYPosition;
positionAbsoluteX: number;
positionAbsoluteY: number;
width?: number;
height?: number;
dragging: boolean;
+11 -5
View File
@@ -64,7 +64,7 @@ type UpdateNodesOptions<NodeType extends NodeBase> = {
defaults?: Partial<NodeType>;
};
export function updateNodes<NodeType extends NodeBase>(
export function adoptUserProvidedNodes<NodeType extends NodeBase>(
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
options: UpdateNodesOptions<NodeType> = {
@@ -80,6 +80,11 @@ export function updateNodes<NodeType extends NodeBase>(
const nextNodes = nodes.map((n) => {
const currentStoreNode = tmpLookup.get(n.id);
if (n === currentStoreNode?.[internalsSymbol]?.userProvidedNode) {
nodeLookup.set(n.id, currentStoreNode);
return currentStoreNode;
}
const node: NodeType = {
...options.defaults,
...n,
@@ -101,6 +106,7 @@ export function updateNodes<NodeType extends NodeBase>(
value: {
handleBounds: currInternals?.handleBounds,
z,
userProvidedNode: n,
},
});
@@ -141,14 +147,14 @@ function calculateXYZPosition<NodeType extends NodeBase>(
);
}
export function updateNodeDimensions(
export function updateNodeDimensions<NodeType extends NodeBase>(
updates: Map<string, NodeDimensionUpdate>,
nodes: NodeBase[],
nodeLookup: Map<string, NodeBase>,
nodes: NodeType[],
nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin,
onUpdate?: (id: string, dimensions: Dimensions) => void
): NodeBase[] | null {
): NodeType[] | null {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) {
+27 -121
View File
@@ -61,10 +61,10 @@ importers:
dependencies:
'@astrojs/react':
specifier: ^3.0.2
version: registry.npmjs.org/@astrojs/react@3.0.2(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.1)
version: registry.npmjs.org/@astrojs/react@3.0.2(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0)
'@astrojs/svelte':
specifier: ^4.0.2
version: registry.npmjs.org/@astrojs/svelte@4.0.2(astro@3.2.2)(svelte@4.2.1)(typescript@5.3.3)(vite@4.5.1)
version: registry.npmjs.org/@astrojs/svelte@4.0.2(astro@3.2.2)(svelte@4.2.1)(typescript@5.2.2)(vite@4.5.0)
'@types/react':
specifier: ^18.2.24
version: registry.npmjs.org/@types/react@18.2.24
@@ -305,7 +305,7 @@ importers:
version: registry.npmjs.org/@sveltejs/adapter-auto@2.1.0(@sveltejs/kit@1.22.6)
'@sveltejs/kit':
specifier: ^1.22.6
version: registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.1)
version: registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.0)
'@sveltejs/package':
specifier: ^2.2.1
version: registry.npmjs.org/@sveltejs/package@2.2.1(svelte@4.2.1)(typescript@5.1.3)
@@ -417,7 +417,7 @@ importers:
dependencies:
'@playwright/experimental-ct-react':
specifier: ^1.39.0
version: registry.npmjs.org/@playwright/experimental-ct-react@1.39.0(@types/node@18.7.16)(vite@4.5.1)
version: registry.npmjs.org/@playwright/experimental-ct-react@1.39.0(@types/node@18.7.16)(vite@4.5.0)
'@types/react':
specifier: ^18.2.31
version: registry.npmjs.org/@types/react@18.2.31
@@ -551,7 +551,7 @@ packages:
prismjs: registry.npmjs.org/prismjs@1.29.0
dev: false
registry.npmjs.org/@astrojs/react@3.0.2(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.1):
registry.npmjs.org/@astrojs/react@3.0.2(@types/react-dom@18.2.8)(@types/react@18.2.24)(react-dom@18.2.0)(react@18.2.0)(vite@4.5.0):
resolution: {integrity: sha512-aooNIuQxTg+IWGMZPuIEwBeBi4/TCPCMsr3714zuLjAjukVd5ZrX/bCNxJqDWU4HNwUm4XFU1OhcEvYOHa5uMQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@astrojs/react/-/react-3.0.2.tgz}
id: registry.npmjs.org/@astrojs/react/3.0.2
name: '@astrojs/react'
@@ -565,7 +565,7 @@ packages:
dependencies:
'@types/react': registry.npmjs.org/@types/react@18.2.24
'@types/react-dom': registry.npmjs.org/@types/react-dom@18.2.8
'@vitejs/plugin-react': registry.npmjs.org/@vitejs/plugin-react@4.1.1(vite@4.5.1)
'@vitejs/plugin-react': registry.npmjs.org/@vitejs/plugin-react@4.1.1(vite@4.5.0)
react: registry.npmjs.org/react@18.2.0
react-dom: registry.npmjs.org/react-dom@18.2.0(react@18.2.0)
ultrahtml: registry.npmjs.org/ultrahtml@1.5.2
@@ -574,7 +574,7 @@ packages:
- vite
dev: false
registry.npmjs.org/@astrojs/svelte@4.0.2(astro@3.2.2)(svelte@4.2.1)(typescript@5.3.3)(vite@4.5.1):
registry.npmjs.org/@astrojs/svelte@4.0.2(astro@3.2.2)(svelte@4.2.1)(typescript@5.2.2)(vite@4.5.0):
resolution: {integrity: sha512-XB9Sexq+iW5aUpctDk6zuKHbWIAuPlAnZKbfgaS9VOaOUtx1t12crP9YoKEVcTifXYgoRuWWfeEAm2I8pYlLIQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@astrojs/svelte/-/svelte-4.0.2.tgz}
id: registry.npmjs.org/@astrojs/svelte/4.0.2
name: '@astrojs/svelte'
@@ -584,10 +584,10 @@ packages:
astro: ^3.0.11
svelte: ^3.55.0 || ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.1)
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.0)
astro: registry.npmjs.org/astro@3.2.2
svelte: registry.npmjs.org/svelte@4.2.1
svelte2tsx: registry.npmjs.org/svelte2tsx@0.6.23(svelte@4.2.1)(typescript@5.3.3)
svelte2tsx: registry.npmjs.org/svelte2tsx@0.6.23(svelte@4.2.1)(typescript@5.2.2)
transitivePeerDependencies:
- supports-color
- typescript
@@ -1756,7 +1756,7 @@ packages:
debug: registry.npmjs.org/debug@4.3.4(supports-color@8.1.1)
espree: registry.npmjs.org/espree@9.6.1
globals: registry.npmjs.org/globals@13.23.0
ignore: registry.npmjs.org/ignore@5.2.4
ignore: registry.npmjs.org/ignore@5.3.0
import-fresh: registry.npmjs.org/import-fresh@3.3.0
js-yaml: registry.npmjs.org/js-yaml@4.1.0
minimatch: registry.npmjs.org/minimatch@3.1.2
@@ -1938,7 +1938,7 @@ packages:
- terser
dev: false
registry.npmjs.org/@playwright/experimental-ct-react@1.39.0(@types/node@18.7.16)(vite@4.5.1):
registry.npmjs.org/@playwright/experimental-ct-react@1.39.0(@types/node@18.7.16)(vite@4.5.0):
resolution: {integrity: sha512-6GaDVVo6x8P9Jdw3yPbVaEP59nesRsETn3kznHAy+2493VsP3IaVATat2G28z0Sivf/K9Tkh5xRYjiawN0ebgw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@playwright/experimental-ct-react/-/experimental-ct-react-1.39.0.tgz}
id: registry.npmjs.org/@playwright/experimental-ct-react/1.39.0
name: '@playwright/experimental-ct-react'
@@ -1947,7 +1947,7 @@ packages:
hasBin: true
dependencies:
'@playwright/experimental-ct-core': registry.npmjs.org/@playwright/experimental-ct-core@1.39.0(@types/node@18.7.16)
'@vitejs/plugin-react': registry.npmjs.org/@vitejs/plugin-react@4.1.1(vite@4.5.1)
'@vitejs/plugin-react': registry.npmjs.org/@vitejs/plugin-react@4.1.1(vite@4.5.0)
transitivePeerDependencies:
- '@types/node'
- less
@@ -2134,7 +2134,7 @@ packages:
peerDependencies:
'@sveltejs/kit': ^1.0.0
dependencies:
'@sveltejs/kit': registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.1)
'@sveltejs/kit': registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.0)
import-meta-resolve: registry.npmjs.org/import-meta-resolve@3.0.0
dev: true
@@ -2150,7 +2150,7 @@ packages:
import-meta-resolve: registry.npmjs.org/import-meta-resolve@4.0.0
dev: true
registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.1):
registry.npmjs.org/@sveltejs/kit@1.22.6(svelte@4.2.1)(vite@4.5.0):
resolution: {integrity: sha512-SDKxI/QpsReCwIn5czjT53fKlPBybbmMk67d317gUqfeORroBAFN1Z6s/x0E1JYi+04i7kKllS+Sz9wVfmUkAQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@sveltejs/kit/-/kit-1.22.6.tgz}
id: registry.npmjs.org/@sveltejs/kit/1.22.6
name: '@sveltejs/kit'
@@ -2162,7 +2162,7 @@ packages:
svelte: ^3.54.0 || ^4.0.0-next.0
vite: ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.1)
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.0)
'@types/cookie': registry.npmjs.org/@types/cookie@0.5.3
cookie: registry.npmjs.org/cookie@0.5.0
devalue: registry.npmjs.org/devalue@4.3.2
@@ -2175,7 +2175,7 @@ packages:
sirv: registry.npmjs.org/sirv@2.0.3
svelte: registry.npmjs.org/svelte@4.2.1
undici: registry.npmjs.org/undici@5.23.0
vite: registry.npmjs.org/vite@4.5.1(@types/node@18.7.16)
vite: registry.npmjs.org/vite@4.5.0(@types/node@18.7.16)
transitivePeerDependencies:
- supports-color
dev: true
@@ -2231,7 +2231,7 @@ packages:
- typescript
dev: true
registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.1)(vite@4.5.1):
registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.1)(vite@4.5.0):
resolution: {integrity: sha512-zjiuZ3yydBtwpF3bj0kQNV0YXe+iKE545QGZVTaylW3eAzFr+pJ/cwK8lZEaRp4JtaJXhD5DyWAV4AxLh6DgaQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-1.0.4.tgz}
id: registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/1.0.4
name: '@sveltejs/vite-plugin-svelte-inspector'
@@ -2242,10 +2242,10 @@ packages:
svelte: ^3.54.0 || ^4.0.0
vite: ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.1)
'@sveltejs/vite-plugin-svelte': registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.0)
debug: registry.npmjs.org/debug@4.3.4(supports-color@8.1.1)
svelte: registry.npmjs.org/svelte@4.2.1
vite: registry.npmjs.org/vite@4.5.1(@types/node@18.7.16)
vite: registry.npmjs.org/vite@4.5.0(@types/node@18.7.16)
transitivePeerDependencies:
- supports-color
@@ -2268,7 +2268,7 @@ packages:
- supports-color
dev: true
registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.1):
registry.npmjs.org/@sveltejs/vite-plugin-svelte@2.4.6(svelte@4.2.1)(vite@4.5.0):
resolution: {integrity: sha512-zO79p0+DZnXPnF0ltIigWDx/ux7Ni+HRaFOw720Qeivc1azFUrJxTl0OryXVibYNx1hCboGia1NRV3x8RNv4cA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-2.4.6.tgz}
id: registry.npmjs.org/@sveltejs/vite-plugin-svelte/2.4.6
name: '@sveltejs/vite-plugin-svelte'
@@ -2278,15 +2278,15 @@ packages:
svelte: ^3.54.0 || ^4.0.0
vite: ^4.0.0
dependencies:
'@sveltejs/vite-plugin-svelte-inspector': registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.1)(vite@4.5.1)
'@sveltejs/vite-plugin-svelte-inspector': registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector@1.0.4(@sveltejs/vite-plugin-svelte@2.4.6)(svelte@4.2.1)(vite@4.5.0)
debug: registry.npmjs.org/debug@4.3.4(supports-color@8.1.1)
deepmerge: registry.npmjs.org/deepmerge@4.3.1
kleur: registry.npmjs.org/kleur@4.1.5
magic-string: registry.npmjs.org/magic-string@0.30.5
svelte: registry.npmjs.org/svelte@4.2.1
svelte-hmr: registry.npmjs.org/svelte-hmr@0.15.3(svelte@4.2.1)
vite: registry.npmjs.org/vite@4.5.1(@types/node@18.7.16)
vitefu: registry.npmjs.org/vitefu@0.2.5(vite@4.5.1)
vite: registry.npmjs.org/vite@4.5.0(@types/node@18.7.16)
vitefu: registry.npmjs.org/vitefu@0.2.5(vite@4.5.0)
transitivePeerDependencies:
- supports-color
@@ -2872,6 +2872,7 @@ packages:
resolution: {integrity: sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz}
name: '@types/node'
version: 18.7.16
requiresBuild: true
registry.npmjs.org/@types/normalize-package-data@2.4.3:
resolution: {integrity: sha512-ehPtgRgaULsFG8x0NeYJvmyH1hmlfsNLujHe9dQEia/7MAJYdzMSi19JtchUHjmBA6XC/75dK55mzZH+RyieSg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.3.tgz}
@@ -2966,12 +2967,6 @@ packages:
version: 6.2.5
dev: true
registry.npmjs.org/@types/semver@7.5.4:
resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/semver/-/semver-7.5.4.tgz}
name: '@types/semver'
version: 7.5.4
dev: true
registry.npmjs.org/@types/semver@7.5.6:
resolution: {integrity: sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz}
name: '@types/semver'
@@ -3381,7 +3376,7 @@ packages:
dependencies:
'@eslint-community/eslint-utils': registry.npmjs.org/@eslint-community/eslint-utils@4.4.0(eslint@8.43.0)
'@types/json-schema': registry.npmjs.org/@types/json-schema@7.0.15
'@types/semver': registry.npmjs.org/@types/semver@7.5.4
'@types/semver': registry.npmjs.org/@types/semver@7.5.6
'@typescript-eslint/scope-manager': registry.npmjs.org/@typescript-eslint/scope-manager@5.60.0
'@typescript-eslint/types': registry.npmjs.org/@typescript-eslint/types@5.60.0
'@typescript-eslint/typescript-estree': registry.npmjs.org/@typescript-eslint/typescript-estree@5.60.0(typescript@5.1.3)
@@ -3504,26 +3499,6 @@ packages:
vite: registry.npmjs.org/vite@4.5.0(@types/node@18.7.16)
transitivePeerDependencies:
- supports-color
dev: true
registry.npmjs.org/@vitejs/plugin-react@4.1.1(vite@4.5.1):
resolution: {integrity: sha512-Jie2HERK+uh27e+ORXXwEP5h0Y2lS9T2PRGbfebiHGlwzDO0dEnd2aNtOR/qjBlPb1YgxwAONeblL1xqLikLag==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.1.1.tgz}
id: registry.npmjs.org/@vitejs/plugin-react/4.1.1
name: '@vitejs/plugin-react'
version: 4.1.1
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
vite: ^4.2.0
dependencies:
'@babel/core': registry.npmjs.org/@babel/core@7.23.2
'@babel/plugin-transform-react-jsx-self': registry.npmjs.org/@babel/plugin-transform-react-jsx-self@7.22.5(@babel/core@7.23.2)
'@babel/plugin-transform-react-jsx-source': registry.npmjs.org/@babel/plugin-transform-react-jsx-source@7.22.5(@babel/core@7.23.2)
'@types/babel__core': registry.npmjs.org/@types/babel__core@7.20.4
react-refresh: registry.npmjs.org/react-refresh@0.14.0
vite: registry.npmjs.org/vite@4.5.1(@types/node@18.7.16)
transitivePeerDependencies:
- supports-color
dev: false
registry.npmjs.org/acorn-jsx@5.3.2(acorn@8.10.0):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz}
@@ -9520,16 +9495,6 @@ packages:
picocolors: registry.npmjs.org/picocolors@1.0.0
source-map-js: registry.npmjs.org/source-map-js@1.0.2
registry.npmjs.org/postcss@8.4.32:
resolution: {integrity: sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz}
name: postcss
version: 8.4.32
engines: {node: ^10 || ^12 || >=14}
dependencies:
nanoid: registry.npmjs.org/nanoid@3.3.7
picocolors: registry.npmjs.org/picocolors@1.0.0
source-map-js: registry.npmjs.org/source-map-js@1.0.2
registry.npmjs.org/prebuild-install@7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz}
name: prebuild-install
@@ -11249,7 +11214,7 @@ packages:
typescript: registry.npmjs.org/typescript@5.1.3
dev: true
registry.npmjs.org/svelte2tsx@0.6.23(svelte@4.2.1)(typescript@5.3.3):
registry.npmjs.org/svelte2tsx@0.6.23(svelte@4.2.1)(typescript@5.2.2):
resolution: {integrity: sha512-3bwd1PuWUA3oEXy8+85zrLDnmJOsVpShpKVAehGWeYsz/66zMihTpRpUN97VVAKTZbO5tP4wnchHUXYs0zOwdw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.6.23.tgz}
id: registry.npmjs.org/svelte2tsx/0.6.23
name: svelte2tsx
@@ -11261,7 +11226,7 @@ packages:
dedent-js: registry.npmjs.org/dedent-js@1.0.1
pascal-case: registry.npmjs.org/pascal-case@3.1.2
svelte: registry.npmjs.org/svelte@4.2.1
typescript: registry.npmjs.org/typescript@5.3.3
typescript: registry.npmjs.org/typescript@5.2.2
dev: false
registry.npmjs.org/svelte@4.2.1:
@@ -11798,14 +11763,6 @@ packages:
engines: {node: '>=14.17'}
hasBin: true
registry.npmjs.org/typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz}
name: typescript
version: 5.3.3
engines: {node: '>=14.17'}
hasBin: true
dev: false
registry.npmjs.org/ultrahtml@1.5.2:
resolution: {integrity: sha512-qh4mBffhlkiXwDAOxvSGxhL0QEQsTbnP9BozOK3OYPEGvPvdWzvAUaXNtUSMdNsKDtuyjEbyVUPFZ52SSLhLqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.2.tgz}
name: ultrahtml
@@ -12141,44 +12098,6 @@ packages:
optionalDependencies:
fsevents: registry.npmjs.org/fsevents@2.3.3
registry.npmjs.org/vite@4.5.1(@types/node@18.7.16):
resolution: {integrity: sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/vite/-/vite-4.5.1.tgz}
id: registry.npmjs.org/vite/4.5.1
name: vite
version: 4.5.1
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
lightningcss: ^1.21.0
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
lightningcss:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': registry.npmjs.org/@types/node@18.7.16
esbuild: registry.npmjs.org/esbuild@0.18.20
postcss: registry.npmjs.org/postcss@8.4.32
rollup: registry.npmjs.org/rollup@3.29.4
optionalDependencies:
fsevents: registry.npmjs.org/fsevents@2.3.3
registry.npmjs.org/vitefu@0.2.5(vite@4.5.0):
resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz}
id: registry.npmjs.org/vitefu/0.2.5
@@ -12192,19 +12111,6 @@ packages:
dependencies:
vite: registry.npmjs.org/vite@4.5.0(@types/node@18.7.16)
registry.npmjs.org/vitefu@0.2.5(vite@4.5.1):
resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz}
id: registry.npmjs.org/vitefu/0.2.5
name: vitefu
version: 0.2.5
peerDependencies:
vite: ^3.0.0 || ^4.0.0 || ^5.0.0
peerDependenciesMeta:
vite:
optional: true
dependencies:
vite: registry.npmjs.org/vite@4.5.1(@types/node@18.7.16)
registry.npmjs.org/vscode-oniguruma@1.7.0:
resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz}
name: vscode-oniguruma