refactor(react/svelte): use nodeLookup
This commit is contained in:
@@ -118,7 +118,7 @@ function MiniMap({
|
||||
|
||||
const onSvgNodeClick = onNodeClick
|
||||
? useCallback((event: MouseEvent, nodeId: string) => {
|
||||
const node = store.getState().nodes.find((n) => n.id === nodeId)!;
|
||||
const node = store.getState().nodeLookup.get(nodeId)!;
|
||||
onNodeClick(event, node);
|
||||
}, [])
|
||||
: undefined;
|
||||
|
||||
@@ -65,8 +65,8 @@ function ResizeControl({
|
||||
|
||||
const dragHandler = drag<HTMLDivElement, unknown>()
|
||||
.on('start', (event: ResizeDragEvent) => {
|
||||
const { nodesLookup, transform, snapGrid, snapToGrid } = store.getState();
|
||||
const node = nodesLookup.get(id);
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid } = store.getState();
|
||||
const node = nodeLookup.get(id);
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
|
||||
prevValues.current = {
|
||||
@@ -86,9 +86,9 @@ function ResizeControl({
|
||||
onResizeStart?.(event, { ...prevValues.current });
|
||||
})
|
||||
.on('drag', (event: ResizeDragEvent) => {
|
||||
const { nodesLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState();
|
||||
const { nodeLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState();
|
||||
const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid });
|
||||
const node = nodesLookup.get(id);
|
||||
const node = nodeLookup.get(id);
|
||||
|
||||
if (node) {
|
||||
const changes: NodeChange[] = [];
|
||||
|
||||
@@ -87,12 +87,12 @@ function NodeToolbar({
|
||||
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
|
||||
|
||||
return nodeIds.reduce<Node[]>((acc, id) => {
|
||||
const node = state.nodes.find((n) => n.id === id);
|
||||
const node = state.nodeLookup.get(id);
|
||||
if (node) {
|
||||
acc.push(node);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Node[]);
|
||||
}, []);
|
||||
},
|
||||
[nodeId, contextNodeId]
|
||||
);
|
||||
|
||||
@@ -43,7 +43,7 @@ const ConnectionLine = ({
|
||||
const { fromNode, handleId, toX, toY, connectionMode } = useStore(
|
||||
useCallback(
|
||||
(s: ReactFlowStore) => ({
|
||||
fromNode: s.nodesLookup.get(nodeId),
|
||||
fromNode: s.nodeLookup.get(nodeId),
|
||||
handleId: s.connectionStartHandle?.handleId,
|
||||
toX: (s.connectionPosition.x - s.transform[0]) / s.transform[2],
|
||||
toY: (s.connectionPosition.y - s.transform[1]) / s.transform[2],
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { isNumeric } from '@xyflow/system';
|
||||
|
||||
import type { BaseEdgeProps } from '../../types';
|
||||
|
||||
import EdgeText from './EdgeText';
|
||||
|
||||
const BaseEdge = ({
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useState, useMemo, useRef, type ComponentType, type KeyboardEvent } from 'react';
|
||||
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';
|
||||
@@ -53,26 +53,30 @@ export default (EdgeComponent: ComponentType<EdgeProps>) => {
|
||||
const [updateHover, setUpdateHover] = useState<boolean>(false);
|
||||
const [updating, setUpdating] = useState<boolean>(false);
|
||||
const store = useStoreApi();
|
||||
const edgePosition = useStore(function edgeSelector(state) {
|
||||
const sourceNode = state.nodesLookup.get(source);
|
||||
const targetNode = state.nodesLookup.get(target);
|
||||
const edgePosition = useStore(
|
||||
useCallback(
|
||||
(state) => {
|
||||
const sourceNode = state.nodeLookup.get(source);
|
||||
const targetNode = state.nodeLookup.get(target);
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const pos = getEdgePosition({
|
||||
id,
|
||||
sourceNode,
|
||||
targetNode,
|
||||
sourceHandle: sourceHandleId || null,
|
||||
targetHandle: targetHandleId || null,
|
||||
connectionMode: state.connectionMode,
|
||||
onError: state.onError,
|
||||
});
|
||||
|
||||
return pos;
|
||||
}, shallow);
|
||||
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]);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* The Handle component is used to connect nodes. When the user mousedowns a handle, we start the connection process.
|
||||
* The user can then drag the connection to another handle or node. When the user releases the mouse, we check if the
|
||||
* connection is valid and if so, we call the onConnect callback.
|
||||
*/
|
||||
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent, TouchEvent as ReactTouchEvent } from 'react';
|
||||
import cc from 'classcat';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
@@ -12,7 +12,7 @@ export function getMouseHandler(
|
||||
return handler === undefined
|
||||
? handler
|
||||
: (event: MouseEvent) => {
|
||||
const node = getState().nodes.find((n) => n.id === id)!;
|
||||
const node = getState().nodeLookup.get(id)!;
|
||||
handler(event, { ...node });
|
||||
};
|
||||
}
|
||||
@@ -35,8 +35,8 @@ export function handleNodeClick({
|
||||
unselect?: boolean;
|
||||
nodeRef?: RefObject<HTMLDivElement>;
|
||||
}) {
|
||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodes, onError } = store.getState();
|
||||
const node = nodes.find((n) => n.id === id)!;
|
||||
const { addSelectedNodes, unselectNodesAndEdges, multiSelectionActive, nodeLookup, onError } = store.getState();
|
||||
const node = nodeLookup.get(id);
|
||||
|
||||
if (!node) {
|
||||
onError?.('012', errorMessages['error012'](id));
|
||||
|
||||
@@ -146,7 +146,7 @@ export default (NodeComponent: ComponentType<NodeProps>) => {
|
||||
if (targetPosChanged) {
|
||||
prevTargetPosition.current = targetPosition;
|
||||
}
|
||||
store.getState().updateNodeDimensions([{ id, nodeElement: nodeRef.current, forceUpdate: true }]);
|
||||
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]]));
|
||||
}
|
||||
}, [id, type, sourcePosition, targetPosition]);
|
||||
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
/*
|
||||
* This is a helper component for calling the onSelectionChange listener.
|
||||
* It will only be mounted if the user has passed an onSelectionChange listener
|
||||
* or is using the useOnSelectionChange hook.
|
||||
* @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
*/
|
||||
import { memo, useEffect } from 'react';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
|
||||
@@ -24,8 +30,6 @@ function areEqual(a: SelectorSlice, b: SelectorSlice) {
|
||||
);
|
||||
}
|
||||
|
||||
// This is just a helper component for calling the onSelectionChange listener.
|
||||
// @TODO: Now that we have the onNodesChange and on EdgesChange listeners, do we still need this component?
|
||||
const SelectionListener = memo(({ onSelectionChange }: SelectionListenerProps) => {
|
||||
const store = useStoreApi();
|
||||
const { selectedNodes, selectedEdges } = useStore(selector, areEqual);
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
/*
|
||||
* This component helps us to update the store with the vlues coming from the user.
|
||||
* We distinguish between values we can update directly with `useDirectStoreUpdater` (like `snapGrid`)
|
||||
* and values that have a dedicated setter function in the store (like `setNodes`).
|
||||
*/
|
||||
import { useEffect } from 'react';
|
||||
import { StoreApi } from 'zustand';
|
||||
import { shallow } from 'zustand/shallow';
|
||||
@@ -70,10 +75,10 @@ const selector = (s: ReactFlowState) => ({
|
||||
reset: s.reset,
|
||||
});
|
||||
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreState: (param: T) => void) {
|
||||
function useStoreUpdater<T>(value: T | undefined, setStoreAction: (param: T) => void) {
|
||||
useEffect(() => {
|
||||
if (typeof value !== 'undefined') {
|
||||
setStoreState(value);
|
||||
setStoreAction(value);
|
||||
}
|
||||
}, [value]);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,8 @@ const EdgeRenderer = ({
|
||||
children,
|
||||
}: EdgeRendererProps) => {
|
||||
const { edgesFocusable, edgesUpdatable, elementsSelectable, onError } = useStore(selector, shallow);
|
||||
// we are grouping edges by zIndex here in order to be able to render them in the correct order
|
||||
// each zIndex gets its own svg element
|
||||
const edgeTree = useVisibleEdges(onlyRenderVisibleElements, elevateEdgesOnSelect);
|
||||
|
||||
return (
|
||||
@@ -70,7 +72,7 @@ const EdgeRenderer = ({
|
||||
{edgeTree.map(({ level, edges, isMaxLevel }) => (
|
||||
<svg key={level} style={{ zIndex: level }} className="react-flow__edges react-flow__container">
|
||||
{isMaxLevel && <MarkerDefinitions defaultColor={defaultMarkerColor} rfId={rfId} />}
|
||||
<g>
|
||||
<>
|
||||
{edges.map((edge) => {
|
||||
let edgeType = edge.type || 'default';
|
||||
|
||||
@@ -132,7 +134,7 @@ const EdgeRenderer = ({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</g>
|
||||
</>
|
||||
</svg>
|
||||
))}
|
||||
{children}
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
}, []);
|
||||
|
||||
const getNode = useCallback<Instance.GetNode<NodeData>>((id) => {
|
||||
return store.getState().nodes.find((n) => n.id === id);
|
||||
return store.getState().nodeLookup.get(id);
|
||||
}, []);
|
||||
|
||||
const getEdges = useCallback<Instance.GetEdges<EdgeData>>(() => {
|
||||
@@ -181,7 +181,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
|
||||
nodeOrRect: Node<NodeData> | { id: Node['id'] } | Rect
|
||||
): [Rect | null, Node<NodeData> | null | undefined, boolean] => {
|
||||
const isRect = isRectObject(nodeOrRect);
|
||||
const node = isRect ? null : store.getState().nodes.find((n) => n.id === nodeOrRect.id);
|
||||
const node = isRect ? null : store.getState().nodeLookup.get(nodeOrRect.id);
|
||||
|
||||
if (!isRect && !node) {
|
||||
[null, null, isRect];
|
||||
|
||||
@@ -8,17 +8,16 @@ function useUpdateNodeInternals(): UpdateNodeInternals {
|
||||
|
||||
return useCallback<UpdateNodeInternals>((id: string | string[]) => {
|
||||
const { domNode, updateNodeDimensions } = store.getState();
|
||||
|
||||
const updateIds = Array.isArray(id) ? id : [id];
|
||||
const updates = updateIds.reduce<NodeDimensionUpdate[]>((res, updateId) => {
|
||||
const updates = new Map<string, NodeDimensionUpdate>();
|
||||
|
||||
updateIds.forEach((updateId) => {
|
||||
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
|
||||
|
||||
if (nodeElement) {
|
||||
res.push({ id: updateId, nodeElement, forceUpdate: true });
|
||||
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true });
|
||||
}
|
||||
|
||||
return res;
|
||||
}, []);
|
||||
});
|
||||
|
||||
requestAnimationFrame(() => updateNodeDimensions(updates));
|
||||
}, []);
|
||||
|
||||
@@ -12,8 +12,8 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole
|
||||
const visibleEdges =
|
||||
onlyRenderVisible && s.width && s.height
|
||||
? s.edges.filter((e) => {
|
||||
const sourceNode = s.nodesLookup.get(e.source);
|
||||
const targetNode = s.nodesLookup.get(e.target);
|
||||
const sourceNode = s.nodeLookup.get(e.source);
|
||||
const targetNode = s.nodeLookup.get(e.target);
|
||||
|
||||
return (
|
||||
sourceNode &&
|
||||
@@ -29,7 +29,7 @@ function useVisibleEdges(onlyRenderVisible: boolean, elevateEdgesOnSelect: boole
|
||||
})
|
||||
: s.edges;
|
||||
|
||||
return groupEdgesByZLevel(visibleEdges, s.nodesLookup, elevateEdgesOnSelect);
|
||||
return groupEdgesByZLevel(visibleEdges, s.nodeLookup, elevateEdgesOnSelect);
|
||||
},
|
||||
[onlyRenderVisible, elevateEdgesOnSelect]
|
||||
),
|
||||
|
||||
@@ -41,18 +41,19 @@ const createRFStore = ({
|
||||
(set, get) => ({
|
||||
...getInitialState({ nodes, edges, width, height, fitView }),
|
||||
setNodes: (nodes: Node[]) => {
|
||||
const { nodesLookup, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin, elevateNodesOnSelect });
|
||||
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 });
|
||||
|
||||
set({ nodes: nextNodes });
|
||||
},
|
||||
getNodes: () => {
|
||||
return get().nodes;
|
||||
},
|
||||
setEdges: (edges: Edge[]) => {
|
||||
const { defaultEdgeOptions = {} } = get();
|
||||
set({ edges: edges.map((e) => ({ ...defaultEdgeOptions, ...e })) });
|
||||
},
|
||||
// when the user works with an uncontrolled flow,
|
||||
// we set a flag `hasDefaultNodes` / `hasDefaultEdges`
|
||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => {
|
||||
const hasDefaultNodes = typeof nodes !== 'undefined';
|
||||
const hasDefaultEdges = typeof edges !== 'undefined';
|
||||
@@ -79,12 +80,15 @@ const createRFStore = ({
|
||||
|
||||
set(nextState);
|
||||
},
|
||||
// Every node gets registerd at a ResizeObserver. Whenever a node
|
||||
// changes its dimensions, this function is called to measure the
|
||||
// new dimensions and update the nodes.
|
||||
updateNodeDimensions: (updates) => {
|
||||
const {
|
||||
onNodesChange,
|
||||
fitView,
|
||||
nodes,
|
||||
nodesLookup,
|
||||
nodeLookup,
|
||||
fitViewOnInit,
|
||||
fitViewDone,
|
||||
fitViewOnInitOptions,
|
||||
@@ -96,7 +100,7 @@ const createRFStore = ({
|
||||
const updatedNodes = updateNodeDimensionsSystem(
|
||||
updates,
|
||||
nodes,
|
||||
nodesLookup,
|
||||
nodeLookup,
|
||||
domNode,
|
||||
nodeOrigin,
|
||||
(id: string, dimensions: Dimensions) => {
|
||||
@@ -112,8 +116,9 @@ const createRFStore = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodesLookup, nodeOrigin);
|
||||
const nextNodes = updateAbsolutePositions(updatedNodes, nodeLookup, nodeOrigin);
|
||||
|
||||
// we call fitView once initially after all dimensions are set
|
||||
let nextFitViewDone = fitViewDone;
|
||||
if (!fitViewDone && fitViewOnInit) {
|
||||
nextFitViewDone = fitView(nextNodes, {
|
||||
@@ -122,6 +127,11 @@ const createRFStore = ({
|
||||
});
|
||||
}
|
||||
|
||||
// here we are cirmumventing the onNodesChange handler
|
||||
// in order to be able to display nodes even if the user
|
||||
// has not provided an onNodesChange handler.
|
||||
// Nodes are only rendered if they have a width and height
|
||||
// attribute which they get from this handler.
|
||||
set({ nodes: nextNodes, fitViewDone: nextFitViewDone });
|
||||
|
||||
if (changes?.length > 0) {
|
||||
@@ -148,12 +158,12 @@ const createRFStore = ({
|
||||
},
|
||||
|
||||
triggerNodeChanges: (changes) => {
|
||||
const { onNodesChange, nodesLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
const { onNodesChange, nodeLookup, nodes, hasDefaultNodes, nodeOrigin, elevateNodesOnSelect } = get();
|
||||
|
||||
if (changes?.length) {
|
||||
if (hasDefaultNodes) {
|
||||
const updatedNodes = applyNodeChanges(changes, nodes);
|
||||
const nextNodes = updateNodes(updatedNodes, nodesLookup, {
|
||||
const nextNodes = updateNodes(updatedNodes, nodeLookup, {
|
||||
nodeOrigin,
|
||||
elevateNodesOnSelect,
|
||||
});
|
||||
|
||||
@@ -22,8 +22,8 @@ const getInitialState = ({
|
||||
height?: number;
|
||||
fitView?: boolean;
|
||||
} = {}): ReactFlowStore => {
|
||||
const nodesLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodesLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
const nodeLookup = new Map<string, Node>();
|
||||
const nextNodes = updateNodes(nodes, nodeLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false });
|
||||
|
||||
let transform: Transform = [0, 0, 1];
|
||||
|
||||
@@ -44,7 +44,7 @@ const getInitialState = ({
|
||||
height: 0,
|
||||
transform,
|
||||
nodes: nextNodes,
|
||||
nodesLookup,
|
||||
nodeLookup,
|
||||
edges: edges,
|
||||
onNodesChange: null,
|
||||
onEdgesChange: null,
|
||||
|
||||
@@ -46,7 +46,7 @@ export type ReactFlowStore = {
|
||||
height: number;
|
||||
transform: Transform;
|
||||
nodes: Node[];
|
||||
nodesLookup: Map<string, Node>;
|
||||
nodeLookup: Map<string, Node>;
|
||||
edges: Edge[];
|
||||
onNodesChange: OnNodesChange | null;
|
||||
onEdgesChange: OnEdgesChange | null;
|
||||
@@ -139,7 +139,6 @@ export type ReactFlowStore = {
|
||||
|
||||
export type ReactFlowActions = {
|
||||
setNodes: (nodes: Node[]) => void;
|
||||
getNodes: () => Node[];
|
||||
setEdges: (edges: Edge[]) => void;
|
||||
setDefaultNodesAndEdges: (nodes?: Node[], edges?: Edge[]) => void;
|
||||
updateNodeDimensions: (updates: Map<string, NodeDimensionUpdate>) => void;
|
||||
|
||||
@@ -42,24 +42,39 @@ export function handleParentExpand(res: any[], updateItem: any) {
|
||||
}
|
||||
}
|
||||
|
||||
// This function applies changes to nodes or edges that are triggered by React Flow internally.
|
||||
// When you drag a node for example, React Flow will send a position change update.
|
||||
// This function then applies the changes and returns the updated elements.
|
||||
function applyChanges(changes: any[], elements: any[]): any[] {
|
||||
// we need this hack to handle the setNodes and setEdges function of the useReactFlow hook for controlled flows
|
||||
if (changes.some((c) => c.type === 'reset')) {
|
||||
return changes.filter((c) => c.type === 'reset').map((c) => c.item);
|
||||
}
|
||||
|
||||
let remainingChanges = changes;
|
||||
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
|
||||
|
||||
return elements.reduce((res: any[], item: any) => {
|
||||
const currentChanges = changes.filter((c) => c.id === item.id);
|
||||
const nextChanges: any[] = [];
|
||||
const _remainingChanges: any[] = [];
|
||||
|
||||
if (currentChanges.length === 0) {
|
||||
remainingChanges.forEach((c) => {
|
||||
if (c.id === item.id) {
|
||||
nextChanges.push(c);
|
||||
} else {
|
||||
_remainingChanges.push(c);
|
||||
}
|
||||
});
|
||||
remainingChanges = _remainingChanges;
|
||||
|
||||
if (nextChanges.length === 0) {
|
||||
res.push(item);
|
||||
return res;
|
||||
}
|
||||
|
||||
const updateItem = { ...item };
|
||||
|
||||
for (const currentChange of currentChanges) {
|
||||
for (const currentChange of nextChanges) {
|
||||
if (currentChange) {
|
||||
switch (currentChange.type) {
|
||||
case 'select': {
|
||||
|
||||
Reference in New Issue
Block a user