Merge branch 'next' into refactor/edge-rendering

This commit is contained in:
moklick
2023-12-17 11:04:32 +01:00
187 changed files with 5192 additions and 2186 deletions
@@ -0,0 +1,35 @@
import { useEffect, useState } from 'react';
import type { ColorMode, ColorModeClass } from '@xyflow/system';
function getMediaQuery() {
if (typeof window === 'undefined' || !window.matchMedia) {
return null;
}
return window.matchMedia('(prefers-color-scheme: dark)');
}
export default function useColorModeClass(colorMode: ColorMode): ColorModeClass {
const [colorModeClass, setColorModeClass] = useState<ColorModeClass | null>(
colorMode === 'system' ? null : colorMode
);
useEffect(() => {
if (colorMode !== 'system') {
setColorModeClass(colorMode);
return;
}
const mediaQuery = getMediaQuery();
const updateColorModeClass = () => setColorModeClass(mediaQuery?.matches ? 'dark' : 'light');
updateColorModeClass();
mediaQuery?.addEventListener('change', updateColorModeClass);
return () => {
mediaQuery?.removeEventListener('change', updateColorModeClass);
};
}, [colorMode]);
return colorModeClass !== null ? colorModeClass : getMediaQuery()?.matches ? 'dark' : 'light';
}
@@ -0,0 +1,54 @@
import { useEffect, useMemo, useRef } from 'react';
import { Connection, HandleType, areConnectionMapsEqual, handleConnectionChange } from '@xyflow/system';
import { useStore } from './useStore';
import { useNodeId } from '../contexts/NodeIdContext';
type useHandleConnectionsParams = {
type: HandleType;
id?: string | null;
nodeId?: string;
onConnect?: (connections: Connection[]) => void;
onDisconnect?: (connections: Connection[]) => void;
};
/**
* Hook to check if a <Handle /> is connected to another <Handle /> and get the connections.
*
* @public
* @param param.type - handle type 'source' or 'target'
* @param param.id - the handle id (this is only needed if the node has multiple handles of the same type)
* @param param.nodeId - node id - if not provided, the node id from the NodeIdContext is used
* @param param.onConnect - gets called when a connection is established
* @param param.onDisconnect - gets called when a connection is removed
* @returns an array with connections
*/
export function useHandleConnections({
type,
id = null,
nodeId,
onConnect,
onDisconnect,
}: useHandleConnectionsParams): Connection[] {
const _nodeId = useNodeId();
const prevConnections = useRef<Map<string, Connection> | null>(null);
const currentNodeId = nodeId || _nodeId;
const connections = useStore(
(state) => state.connectionLookup.get(`${currentNodeId}-${type}-${id}`),
areConnectionMapsEqual
);
useEffect(() => {
// @todo dicuss if onConnect/onDisconnect should be called when the component mounts/unmounts
if (prevConnections.current && prevConnections.current !== connections) {
const _connections = connections ?? new Map();
handleConnectionChange(prevConnections.current, _connections, onDisconnect);
handleConnectionChange(_connections, prevConnections.current, onConnect);
}
prevConnections.current = connections ?? new Map();
}, [connections, onConnect, onDisconnect]);
return useMemo(() => Array.from(connections?.values() ?? []), [connections]);
}
+40
View File
@@ -0,0 +1,40 @@
import { useCallback } from 'react';
import { shallow } from 'zustand/shallow';
import { useStore } from '../hooks/useStore';
import type { Node } from '../types';
export function useNodesData<NodeType extends Node = Node>(nodeId: string): NodeType['data'] | null;
export function useNodesData<NodeType extends Node = Node>(nodeIds: string[]): NodeType['data'][];
export function useNodesData<NodeType extends Node = Node>(
nodeIds: string[],
guard: (node: Node) => node is NodeType
): NodeType['data'][];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useNodesData(nodeIds: any): any {
const nodesData = useStore(
useCallback(
(s) => {
if (!Array.isArray(nodeIds)) {
return s.nodeLookup.get(nodeIds)?.data || null;
}
const data = [];
for (const nodeId of nodeIds) {
const nodeData = s.nodeLookup.get(nodeId)?.data;
if (nodeData) {
data.push(nodeData);
}
}
return data;
},
[nodeIds]
),
shallow
);
return nodesData;
}
+35 -2
View File
@@ -10,7 +10,7 @@ import {
} from '@xyflow/system';
import useViewportHelper from './useViewportHelper';
import { useStoreApi } from '../hooks/useStore';
import { useStoreApi } from './useStore';
import type {
ReactFlowInstance,
Instance,
@@ -24,6 +24,7 @@ import type {
Node,
Edge,
} from '../types';
import { isNode } from '../utils';
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlowInstance<NodeData, EdgeData> {
@@ -206,7 +207,7 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
}
return (nodes || store.getState().nodes).filter((n) => {
if (!isRect && (n.id === node!.id || !n.positionAbsolute)) {
if (!isRect && (n.id === node!.id || !n.computed?.positionAbsolute)) {
return false;
}
@@ -271,6 +272,36 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
return getOutgoersBase(node, nodes, edges);
}, []);
const updateNode = useCallback<Instance.UpdateNode>(
(id, nodeUpdate, options = { replace: true }) => {
setNodes((prevNodes) =>
prevNodes.map((node) => {
if (node.id === id) {
const nextNode = typeof nodeUpdate === 'function' ? nodeUpdate(node as Node) : nodeUpdate;
return options.replace && isNode(nextNode) ? nextNode : { ...node, ...nextNode };
}
return node;
})
);
},
[setNodes]
);
const updateNodeData = useCallback<Instance.UpdateNodeData>(
(id, dataUpdate, options = { replace: false }) => {
updateNode(
id,
(node) => {
const nextData = typeof dataUpdate === 'function' ? dataUpdate(node) : dataUpdate;
return options.replace ? { ...node, data: nextData } : { ...node, data: { ...node.data, ...nextData } };
},
options
);
},
[updateNode]
);
return useMemo(() => {
return {
...viewportHelper,
@@ -289,6 +320,8 @@ export default function useReactFlow<NodeData = any, EdgeData = any>(): ReactFlo
getConnectedEdges,
getIncomers,
getOutgoers,
updateNode,
updateNodeData,
};
}, [
viewportHelper,
@@ -23,8 +23,11 @@ function useUpdateNodePositions() {
const yDiff = params.y * yVelo * factor;
const nodeUpdates = selectedNodes.map((node) => {
if (node.positionAbsolute) {
let nextPosition = { x: node.positionAbsolute.x + xDiff, y: node.positionAbsolute.y + yDiff };
if (node.computed?.positionAbsolute) {
let nextPosition = {
x: node.computed?.positionAbsolute.x + xDiff,
y: node.computed?.positionAbsolute.y + yDiff,
};
if (snapToGrid) {
nextPosition = snapPosition(nextPosition, snapGrid);
@@ -40,7 +43,10 @@ function useUpdateNodePositions() {
);
node.position = position;
node.positionAbsolute = positionAbsolute;
if (!node.computed) {
node.computed = {};
}
node.computed.positionAbsolute = positionAbsolute;
}
return node;
+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;