initial version of middlewares

This commit is contained in:
peterkogo
2025-08-28 10:44:54 +02:00
parent 04055c9625
commit cec7da54df
7 changed files with 172 additions and 5 deletions
+35 -1
View File
@@ -17,6 +17,8 @@ import {
ConnectionMode,
OnBeforeDelete,
OnDelete,
useNodeChangeMiddleware,
NodeChange,
} from '@xyflow/react';
const onNodeDragStart = (_: ReactMouseEvent, node: Node, nodes: Node[]) => console.log('drag start', node, nodes);
@@ -191,6 +193,37 @@ const nodeColor = (n: Node): string => {
return '#fff';
};
function RestrictExtent({
minX = -Infinity,
minY = -Infinity,
maxX = Infinity,
maxY = Infinity,
}: {
minX?: number;
minY?: number;
maxX?: number;
maxY?: number;
}) {
useNodeChangeMiddleware(
useCallback(
(changes: NodeChange[]) => {
return changes.map((change) => {
if (change.type === 'position' && change.position) {
const { position } = change;
position.x = Math.min(Math.max(position.x, minX), maxX);
position.y = Math.min(Math.max(position.y, minY), maxY);
change.position = position;
}
return change;
});
},
[minX, minY, maxX, maxY]
)
);
return null;
}
const OverviewFlow = () => {
const [nodes, , onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
@@ -234,8 +267,9 @@ const OverviewFlow = () => {
maxZoom={Infinity}
onBeforeDelete={onBeforeDelete}
onDelete={onDelete}
onPaneMouseMove={onPaneMouseMove}
// onPaneMouseMove={onPaneMouseMove}
>
<RestrictExtent minX={-100} maxX={500} />
<MiniMap nodeBorderRadius={2} />
<Controls orientation="horizontal" />
<Background gap={25} />
@@ -28,7 +28,15 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
const store = useStoreApi<NodeType, EdgeType>();
const nodeQueueHandler = useCallback((queueItems: QueueItem<NodeType>[]) => {
const { nodes = [], setNodes, hasDefaultNodes, onNodesChange, nodeLookup, fitViewQueued } = store.getState();
const {
nodes = [],
setNodes,
hasDefaultNodes,
onNodesChange,
nodeLookup,
fitViewQueued,
nodeChangeMiddleware,
} = store.getState();
/*
* This is essentially an `Array.reduce` in imperative clothing. Processing
@@ -40,11 +48,23 @@ export function BatchProvider<NodeType extends Node = Node, EdgeType extends Edg
next = typeof payload === 'function' ? payload(next) : payload;
}
const changes = getElementsDiffChanges({
let changes = getElementsDiffChanges({
items: next,
lookup: nodeLookup,
}) as NodeChange<NodeType>[];
console.log('this workds');
for (const middleware of nodeChangeMiddleware.values()) {
changes = middleware(changes);
}
// for (const middleware of nodeSyncMiddleware.values()) {
// changes = middleware(changes);
// }
// Do we always want to setNodes(next) or only when there are changes?
if (hasDefaultNodes) {
setNodes(next);
}
+97
View File
@@ -0,0 +1,97 @@
import { NodeChange } from '@xyflow/system';
import { useEffect, useRef } from 'react';
import { useStoreApi } from './useStore';
import { Edge, Node } from '../types';
/**
* We identify each middleware with a symbol that is unique for each hook instance.
*/
function useSymbol() {
const symbol = useRef<symbol>();
if (!symbol.current) {
symbol.current = Symbol();
}
return symbol;
}
export function useNodeChangeMiddleware<NodeType extends Node = Node>(
fn: (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]
) {
const store = useStoreApi<NodeType, Edge>();
const symbol = useSymbol();
useEffect(() => {
const { nodeChangeMiddleware } = store.getState();
nodeChangeMiddleware.set(symbol.current!, fn);
}, [fn]);
useEffect(() => {
const { nodeChangeMiddleware } = store.getState();
return () => {
nodeChangeMiddleware.delete(symbol.current!);
};
}, []);
}
export function useNodeChangeMiddlewareDynamic<NodeType extends Node = Node>(
fn: (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]
) {
const store = useStoreApi<NodeType, Edge>();
const symbol = useSymbol();
const { nodeChangeMiddleware } = store.getState();
nodeChangeMiddleware.delete(symbol.current!);
nodeChangeMiddleware.set(symbol.current!, fn);
}
// export function useEdgeChangeMiddleware(fn: (changes: EdgeChange[]) => EdgeChange[]) {
// const store = useStoreApi();
// const symbol = useSymbol();
// useEffect(() => {
// const { edgeChangeMiddleware } = store.getState();
// edgeChangeMiddleware.set(symbol.current!, fn);
// }, [fn]);
// useEffect(() => {
// const { edgeChangeMiddleware } = store.getState();
// return () => {
// edgeChangeMiddleware.delete(symbol.current!);
// };
// }, []);
// }
// export function useNodeSyncMiddleware(fn: (changes: NodeChange[]) => NodeChange[]) {
// const store = useStoreApi();
// const symbol = useSymbol();
// useEffect(() => {
// const { nodeSyncMiddleware } = store.getState();
// nodeSyncMiddleware.set(symbol.current!, fn);
// }, [fn]);
// useEffect(() => {
// const { nodeSyncMiddleware } = store.getState();
// return () => {
// nodeSyncMiddleware.delete(symbol.current!);
// };
// }, []);
// }
// export function useEdgeSyncMiddleware(fn: (changes: EdgeChange[]) => EdgeChange[]) {
// const store = useStoreApi();
// const symbol = useSymbol();
// useEffect(() => {
// const { edgeSyncMiddleware } = store.getState();
// edgeSyncMiddleware.set(symbol.current!, fn);
// }, [fn]);
// useEffect(() => {
// const { edgeSyncMiddleware } = store.getState();
// return () => {
// edgeSyncMiddleware.delete(symbol.current!);
// };
// }, []);
// }
+2
View File
@@ -30,6 +30,8 @@ export { useConnection } from './hooks/useConnection';
export { useInternalNode } from './hooks/useInternalNode';
export { useNodeId } from './contexts/NodeIdContext';
export { useNodeChangeMiddleware, useNodeChangeMiddlewareDynamic } from './hooks/useMiddleware';
export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { isNode, isEdge } from './utils/general';
+6 -2
View File
@@ -175,8 +175,8 @@ const createStore = ({
},
updateNodePositions: (nodeDragItems, dragging = false) => {
const parentExpandChildren: ParentExpandChild[] = [];
const changes = [];
const { nodeLookup, triggerNodeChanges } = get();
let changes = [];
const { nodeLookup, triggerNodeChanges, nodeChangeMiddleware } = get();
for (const [id, dragItem] of nodeDragItems) {
// we are using the nodelookup to be sure to use the current expandParent and parentId value
@@ -216,6 +216,10 @@ const createStore = ({
changes.push(...parentExpandChanges);
}
for (const middleware of nodeChangeMiddleware.values()) {
changes = middleware(changes);
}
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes) => {
+5
View File
@@ -146,6 +146,11 @@ const getInitialState = ({
lib: 'react',
debug: false,
ariaLabelConfig: defaultAriaLabelConfig,
nodeChangeMiddleware: new Map(),
edgeChangeMiddleware: new Map(),
nodeSyncMiddleware: new Map(),
edgeSyncMiddleware: new Map(),
};
};
+5
View File
@@ -152,6 +152,11 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
lib: string;
debug: boolean;
ariaLabelConfig: AriaLabelConfig;
nodeChangeMiddleware: Map<symbol, (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]>;
edgeChangeMiddleware: Map<symbol, (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]>;
nodeSyncMiddleware: Map<symbol, (changes: NodeChange<NodeType>[]) => NodeChange<NodeType>[]>;
edgeSyncMiddleware: Map<symbol, (changes: EdgeChange<EdgeType>[]) => EdgeChange<EdgeType>[]>;
};
export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {