Merge branch 'main' into nodesbounds

This commit is contained in:
Moritz Klack
2024-08-29 00:09:57 +02:00
committed by GitHub
44 changed files with 1388 additions and 450 deletions
@@ -24,7 +24,7 @@ function BackgroundComponent({
// only used for lines and cross
size,
lineWidth = 1,
offset = 2,
offset = 0,
color,
bgColor,
style,
@@ -39,13 +39,11 @@ function BackgroundComponent({
const gapXY: [number, number] = Array.isArray(gap) ? gap : [gap, gap];
const scaledGap: [number, number] = [gapXY[0] * transform[2] || 1, gapXY[1] * transform[2] || 1];
const scaledSize = patternSize * transform[2];
const offsetXY: [number, number] = Array.isArray(offset) ? offset : [offset, offset];
const scaledOffset: [number, number] = [offsetXY[0] * transform[2] || 1, offsetXY[1] * transform[2] || 1]
const patternDimensions: [number, number] = isCross ? [scaledSize, scaledSize] : scaledGap;
const patternOffset = isDots
? [scaledSize / offset, scaledSize / offset]
: [patternDimensions[0] / offset, patternDimensions[1] / offset];
const _patternId = `${patternId}${id ? id : ''}`;
return (
@@ -69,10 +67,10 @@ function BackgroundComponent({
width={scaledGap[0]}
height={scaledGap[1]}
patternUnits="userSpaceOnUse"
patternTransform={`translate(-${patternOffset[0]},-${patternOffset[1]})`}
patternTransform={`translate(-${scaledOffset[0]},-${scaledOffset[1]})`}
>
{isDots ? (
<DotPattern radius={scaledSize / offset} className={patternClassName} />
<DotPattern radius={scaledSize / 2} className={patternClassName} />
) : (
<LinePattern
dimensions={patternDimensions}
@@ -21,7 +21,7 @@ export type BackgroundProps = {
/** Size of a single pattern element */
size?: number;
/** Offset of the pattern */
offset?: number;
offset?: number | [number, number];
/** Line width of the Line pattern */
lineWidth?: number;
/** Variant of the pattern
@@ -43,7 +43,7 @@ export function useQueue<T>(runQueue: (items: QueueItem<T>[]) => void) {
queue.reset();
}
// Beacuse we're using reactive state to trigger this effect, we need to flip
// Because we're using reactive state to trigger this effect, we need to flip
// it back to false.
setShouldFlush(false);
}, [shouldFlush]);
@@ -1,5 +1,5 @@
// Reconnectable edges have a anchors around their handles to reconnect the edge.
import { XYHandle, type Connection, EdgePosition } from '@xyflow/system';
import { XYHandle, type Connection, EdgePosition, FinalConnectionState, HandleType } from '@xyflow/system';
import { EdgeAnchor } from '../Edges/EdgeAnchor';
import type { EdgeWrapperProps, Edge } from '../../types/edges';
@@ -9,8 +9,6 @@ type EdgeUpdateAnchorsProps<EdgeType extends Edge = Edge> = {
edge: EdgeType;
isReconnectable: boolean | 'source' | 'target';
reconnectRadius: EdgeWrapperProps['reconnectRadius'];
sourceHandleId: Edge['sourceHandle'];
targetHandleId: Edge['targetHandle'];
onReconnect: EdgeWrapperProps<EdgeType>['onReconnect'];
onReconnectStart: EdgeWrapperProps<EdgeType>['onReconnectStart'];
onReconnectEnd: EdgeWrapperProps<EdgeType>['onReconnectEnd'];
@@ -22,8 +20,6 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
isReconnectable,
reconnectRadius,
edge,
targetHandleId,
sourceHandleId,
sourceX,
sourceY,
targetX,
@@ -38,7 +34,10 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
}: EdgeUpdateAnchorsProps<EdgeType>) {
const store = useStoreApi();
const handleEdgeUpdater = (event: React.MouseEvent<SVGGElement, MouseEvent>, isSourceHandle: boolean) => {
const handleEdgeUpdater = (
event: React.MouseEvent<SVGGElement, MouseEvent>,
oppositeHandle: { nodeId: string; id: string | null; type: HandleType }
) => {
// avoid triggering edge updater if mouse btn is not left
if (event.button !== 0) {
return;
@@ -59,18 +58,14 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
panBy,
updateConnection,
} = store.getState();
const nodeId = isSourceHandle ? edge.target : edge.source;
const handleId = (isSourceHandle ? targetHandleId : sourceHandleId) || null;
const handleType = isSourceHandle ? 'target' : 'source';
const isTarget = isSourceHandle;
const isTarget = oppositeHandle.type === 'target';
setReconnecting(true);
onReconnectStart?.(event, edge, handleType);
onReconnectStart?.(event, edge, oppositeHandle.type);
const _onReconnectEnd = (evt: MouseEvent | TouchEvent) => {
const _onReconnectEnd = (evt: MouseEvent | TouchEvent, connectionState: FinalConnectionState) => {
setReconnecting(false);
onReconnectEnd?.(evt, edge, handleType);
onReconnectEnd?.(evt, edge, oppositeHandle.type, connectionState);
};
const onConnectEdge = (connection: Connection) => onReconnect?.(edge, connection);
@@ -80,11 +75,11 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
connectionMode,
connectionRadius,
domNode,
handleId,
nodeId,
handleId: oppositeHandle.id,
nodeId: oppositeHandle.nodeId,
nodeLookup,
isTarget,
edgeUpdaterType: handleType,
edgeUpdaterType: oppositeHandle.type,
lib,
flowId,
cancelConnection,
@@ -101,15 +96,15 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
};
const onReconnectSourceMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, true);
handleEdgeUpdater(event, { nodeId: edge.target, id: edge.targetHandle ?? null, type: 'target' });
const onReconnectTargetMouseDown = (event: React.MouseEvent<SVGGElement, MouseEvent>): void =>
handleEdgeUpdater(event, false);
handleEdgeUpdater(event, { nodeId: edge.source, id: edge.sourceHandle ?? null, type: 'source' });
const onReconnectMouseEnter = () => setUpdateHover(true);
const onReconnectMouseOut = () => setUpdateHover(false);
return (
<>
{(isReconnectable === 'source' || isReconnectable === true) && (
{(isReconnectable === true || isReconnectable === 'source') && (
<EdgeAnchor
position={sourcePosition}
centerX={sourceX}
@@ -121,7 +116,7 @@ export function EdgeUpdateAnchors<EdgeType extends Edge = Edge>({
type="source"
/>
)}
{(isReconnectable === 'target' || isReconnectable === true) && (
{(isReconnectable === true || isReconnectable === 'target') && (
<EdgeAnchor
position={targetPosition}
centerX={targetX}
@@ -255,8 +255,6 @@ export function EdgeWrapper<EdgeType extends Edge = Edge>({
targetPosition={targetPosition}
setUpdateHover={setUpdateHover}
setReconnecting={setReconnecting}
sourceHandleId={edge.sourceHandle}
targetHandleId={edge.targetHandle}
/>
)}
</g>
@@ -19,6 +19,8 @@ import {
type HandleType,
ConnectionMode,
OnConnect,
ConnectionState,
Optional,
} from '@xyflow/system';
import { useStore, useStoreApi } from '../../hooks/useStore';
@@ -159,6 +161,8 @@ function HandleComponent(
isValidConnection: isValidConnectionStore,
lib,
rfId: flowId,
nodeLookup,
connection: connectionState,
} = store.getState();
if (!nodeId || (!connectionClickStartHandle && !isConnectableStart)) {
@@ -187,13 +191,17 @@ function HandleComponent(
flowId,
doc,
lib,
nodeLookup,
});
if (isValid && connection) {
onConnectExtended(connection);
}
onClickConnectEnd?.(event as unknown as MouseEvent);
const connectionClone = structuredClone(connectionState) as Optional<ConnectionState, 'inProgress'>;
delete connectionClone.inProgress;
connectionClone.toPosition = connectionClone.toHandle ? connectionClone.toHandle.position : null;
onClickConnectEnd?.(event as unknown as MouseEvent, connectionClone);
store.setState({ connectionClickStartHandle: null });
};
@@ -1,5 +1,5 @@
/*
* This component helps us to update the store with the vlues coming from the user.
* This component helps us to update the store with the values 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`).
*/
+5 -2
View File
@@ -120,7 +120,6 @@ export function Pane({
const onPointerDown = (event: ReactPointerEvent): void => {
const { resetSelectedElements, domNode, edgeLookup } = store.getState();
containerBounds.current = domNode?.getBoundingClientRect();
(event.target as Element)?.setPointerCapture?.(event.pointerId);
if (
!elementsSelectable ||
@@ -132,6 +131,8 @@ export function Pane({
return;
}
(event.target as Element)?.setPointerCapture?.(event.pointerId);
selectionStarted.current = true;
selectionInProgress.current = false;
edgeIdLookup.current = new Map();
@@ -252,9 +253,11 @@ export function Pane({
selectionStarted.current = false;
};
const draggable = panOnDrag === true || (Array.isArray(panOnDrag) && panOnDrag.includes(0));
return (
<div
className={cc(['react-flow__pane', { draggable: panOnDrag, dragging, selection: isSelecting }])}
className={cc(['react-flow__pane', { draggable, dragging, selection: isSelecting }])}
onClick={hasActiveSelection ? undefined : wrapHandler(onClick, container)}
onContextMenu={wrapHandler(onContextMenu, container)}
onWheel={wrapHandler(onWheel, container)}
+21 -4
View File
@@ -4,17 +4,34 @@ import { ConnectionState, pointToRendererPoint } from '@xyflow/system';
import { useStore } from './useStore';
import type { InternalNode, Node, ReactFlowStore } from '../types';
const selector = (s: ReactFlowStore) => {
function storeSelector(s: ReactFlowStore) {
return s.connection.inProgress
? { ...s.connection, to: pointToRendererPoint(s.connection.to, s.transform) }
: { ...s.connection };
};
}
function getSelector<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
connectionSelector?: (connection: ConnectionState<InternalNode<NodeType>>) => SelectorReturn
): (s: ReactFlowStore) => SelectorReturn | ConnectionState<InternalNode> {
if (connectionSelector) {
const combinedSelector = (s: ReactFlowStore) => {
const connection = storeSelector(s) as ConnectionState<InternalNode<NodeType>>;
return connectionSelector(connection);
};
return combinedSelector;
}
return storeSelector;
}
/**
* Hook for accessing the connection state.
*
* @public
* @returns ConnectionState
*/
export function useConnection<NodeType extends Node = Node>(): ConnectionState<InternalNode<NodeType>> {
return useStore(selector, shallow) as ConnectionState<InternalNode<NodeType>>;
export function useConnection<NodeType extends Node = Node, SelectorReturn = ConnectionState<InternalNode<NodeType>>>(
connectionSelector?: (connection: ConnectionState<InternalNode<NodeType>>) => SelectorReturn
): SelectorReturn {
const combinedSelector = getSelector<NodeType, SelectorReturn>(connectionSelector);
return useStore(combinedSelector, shallow) as SelectorReturn;
}
+7
View File
@@ -258,6 +258,13 @@ export function useReactFlow<NodeType extends Node = Node, EdgeType extends Edge
return boxToRect(box);
},
getHandleConnections: ({ type, id, nodeId }) =>
Array.from(
store
.getState()
.connectionLookup.get(`${nodeId}-${type}-${id ?? null}`)
?.values() ?? []
),
};
}, []);
@@ -25,6 +25,6 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
}
});
requestAnimationFrame(() => updateNodeInternals(updates));
requestAnimationFrame(() => updateNodeInternals(updates, { triggerFitView: false }));
}, []);
}
+8
View File
@@ -101,8 +101,16 @@ export {
type EdgeAddChange,
type EdgeReplaceChange,
type KeyCode,
type ConnectionState,
type FinalConnectionState,
type ConnectionInProgress,
type NoConnection,
} from '@xyflow/system';
// we need this workaround to prevent a duplicate identifier error
import { type Handle as HandleBound } from '@xyflow/system';
export type Handle = HandleBound;
// system utils
export {
type GetBezierPathParams,
+21 -16
View File
@@ -77,7 +77,7 @@ const createStore = ({
// 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.
updateNodeInternals: (updates) => {
updateNodeInternals: (updates, params = { triggerFitView: true }) => {
const {
triggerNodeChanges,
nodeLookup,
@@ -105,23 +105,28 @@ const createStore = ({
updateAbsolutePositions(nodeLookup, parentLookup, { nodeOrigin });
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (params.triggerFitView) {
// we call fitView once initially after all dimensions are set
let nextFitViewDone = fitViewDone;
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
if (!fitViewDone && fitViewOnInit) {
nextFitViewDone = fitViewSync({
...fitViewOnInitOptions,
nodes: fitViewOnInitOptions?.nodes,
});
}
// 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({ fitViewDone: nextFitViewDone });
} else {
// we always want to trigger useStore calls whenever updateNodeInternals is called
set({});
}
// 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({ fitViewDone: nextFitViewDone });
if (changes?.length > 0) {
if (debug) {
console.log('React Flow: trigger node changes', changes);
@@ -276,7 +281,7 @@ const createStore = ({
const { nodeLookup } = get();
for (const [, node] of nodeLookup) {
const positionAbsolute = clampPosition(node.position, nodeExtent);
const positionAbsolute = clampPosition(node.internals.positionAbsolute, nodeExtent);
nodeLookup.set(node.id, {
...node,
+8 -1
View File
@@ -14,6 +14,8 @@ import type {
EdgePosition,
StepPathOptions,
OnError,
ConnectionState,
FinalConnectionState,
} from '@xyflow/system';
import { EdgeTypes, InternalNode, Node } from '.';
@@ -78,7 +80,12 @@ export type EdgeWrapperProps<EdgeType extends Edge = Edge> = {
onMouseLeave?: EdgeMouseHandler<EdgeType>;
reconnectRadius?: number;
onReconnectStart?: (event: ReactMouseEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (event: MouseEvent | TouchEvent, edge: EdgeType, handleType: HandleType) => void;
onReconnectEnd?: (
event: MouseEvent | TouchEvent,
edge: EdgeType,
handleType: HandleType,
connectionState: FinalConnectionState
) => void;
rfId?: string;
edgeTypes?: EdgeTypes;
onError?: OnError;
+17 -1
View File
@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-namespace */
import type { Rect, Viewport } from '@xyflow/system';
import type { HandleConnection, HandleType, Rect, Viewport } from '@xyflow/system';
import type { Node, Edge, ViewportHelperFunctions, InternalNode } from '.';
export type ReactFlowJsonObject<NodeType extends Node = Node, EdgeType extends Edge = Edge> = {
@@ -181,6 +181,22 @@ export type GeneralHelpers<NodeType extends Node = Node, EdgeType extends Edge =
* @returns the bounds of the given nodes
*/
getNodesBounds: (nodes: (NodeType | string)[]) => Rect;
* Gets all connections for a given handle belonging to a specific node.
*
* @param type - handle type 'source' or 'target'
* @param id - the handle id (this is only needed if you have multiple handles of the same type, meaning you have to provide a unique id for each handle)
* @param nodeId - the node id the handle belongs to
* @returns an array with handle connections
*/
getHandleConnections: ({
type,
id,
nodeId,
}: {
type: HandleType;
nodeId: string;
id?: string | null;
}) => HandleConnection[];
};
export type ReactFlowInstance<NodeType extends Node = Node, EdgeType extends Edge = Edge> = GeneralHelpers<
+2 -2
View File
@@ -55,7 +55,7 @@ export type ReactFlowStore<NodeType extends Node = Node, EdgeType extends Edge =
nodes: NodeType[];
nodeLookup: NodeLookup<InternalNode<NodeType>>;
parentLookup: ParentLookup<InternalNode<NodeType>>;
edges: Edge[];
edges: EdgeType[];
edgeLookup: EdgeLookup<EdgeType>;
connectionLookup: ConnectionLookup;
onNodesChange: OnNodesChange<NodeType> | null;
@@ -152,7 +152,7 @@ export type ReactFlowActions<NodeType extends Node, EdgeType extends Edge> = {
setNodes: (nodes: NodeType[]) => void;
setEdges: (edges: EdgeType[]) => void;
setDefaultNodesAndEdges: (nodes?: NodeType[], edges?: EdgeType[]) => void;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>) => void;
updateNodeInternals: (updates: Map<string, InternalNodeUpdate>, params?: { triggerFitView: boolean }) => void;
updateNodePositions: UpdateNodePositions;
resetSelectedElements: () => void;
unselectNodesAndEdges: (params?: UnselectNodesAndEdgesParams) => void;
+16 -3
View File
@@ -19,10 +19,11 @@ function applyChanges(changes: any[], elements: any[]): any[] {
// By storing a map of changes for each element, we can a quick lookup as we
// iterate over the elements array!
const changesMap = new Map<any, any[]>();
const addItemChanges: any[] = [];
for (const change of changes) {
if (change.type === 'add') {
updatedElements.push(change.item);
addItemChanges.push(change);
continue;
} else if (change.type === 'remove' || change.type === 'replace') {
// For a 'remove' change we can safely ignore any other changes queued for
@@ -73,6 +74,18 @@ function applyChanges(changes: any[], elements: any[]): any[] {
updatedElements.push(updatedElement);
}
// we need to wait for all changes to be applied before adding new items
// to be able to add them at the correct index
if (addItemChanges.length) {
addItemChanges.forEach((change) => {
if (change.index !== undefined) {
updatedElements.splice(change.index, 0, { ...change.item });
} else {
updatedElements.push({ ...change.item });
}
});
}
return updatedElements;
}
@@ -237,7 +250,7 @@ export function getElementsDiffChanges({
const changes: any[] = [];
const itemsLookup = new Map<string, any>(items.map((item) => [item.id, item]));
for (const item of items) {
for (const [index, item] of items.entries()) {
const lookupItem = lookup.get(item.id);
const storeItem = lookupItem?.internals?.userNode ?? lookupItem;
@@ -246,7 +259,7 @@ export function getElementsDiffChanges({
}
if (storeItem === undefined) {
changes.push({ item: item, type: 'add' });
changes.push({ item: item, type: 'add', index });
}
}