refactor(react): handle parent expand in store instead of applyChanges

This commit is contained in:
moklick
2024-03-28 17:08:05 +01:00
parent 844d574c4f
commit 67d979985d
10 changed files with 203 additions and 100 deletions
@@ -88,9 +88,11 @@ export function NodeWrapper<NodeType extends Node>({
const moveSelectedNodes = useMoveSelectedNodes(); const moveSelectedNodes = useMoveSelectedNodes();
useEffect(() => { useEffect(() => {
const currNode = nodeRef.current;
return () => { return () => {
if (nodeRef.current) { if (currNode) {
resizeObserver?.unobserve(nodeRef.current); resizeObserver?.unobserve(currNode);
} }
}; };
}, []); }, []);
@@ -122,7 +124,7 @@ export function NodeWrapper<NodeType extends Node>({
if (targetPosChanged) { if (targetPosChanged) {
prevTargetPosition.current = node.targetPosition; prevTargetPosition.current = node.targetPosition;
} }
store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, forceUpdate: true }]])); store.getState().updateNodeDimensions(new Map([[id, { id, nodeElement: nodeRef.current, force: true }]]));
} }
}, [id, nodeType, node.sourcePosition, node.targetPosition]); }, [id, nodeType, node.sourcePosition, node.targetPosition]);
@@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from 'react';
import { ReactFlowState } from '../../types'; import { ReactFlowState } from '../../types';
import { useStore } from '../../hooks/useStore'; import { useStore } from '../../hooks/useStore';
import { NodeDimensionUpdate } from '@xyflow/system';
const selector = (s: ReactFlowState) => s.updateNodeDimensions; const selector = (s: ReactFlowState) => s.updateNodeDimensions;
@@ -15,14 +16,13 @@ export function useResizeObserver() {
} }
const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => {
const updates = new Map(); const updates = new Map<string, NodeDimensionUpdate>();
entries.forEach((entry: ResizeObserverEntry) => { entries.forEach((entry: ResizeObserverEntry) => {
const id = entry.target.getAttribute('data-id') as string; const id = entry.target.getAttribute('data-id') as string;
updates.set(id, { updates.set(id, {
id, id,
nodeElement: entry.target as HTMLDivElement, nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}); });
}); });
@@ -21,7 +21,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals {
const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement; const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement;
if (nodeElement) { if (nodeElement) {
updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); updates.set(updateId, { id: updateId, nodeElement, force: true });
} }
}); });
+1 -1
View File
@@ -28,7 +28,7 @@ export { useNodesData } from './hooks/useNodesData';
export { useConnection } from './hooks/useConnection'; export { useConnection } from './hooks/useConnection';
export { useNodeId } from './contexts/NodeIdContext'; export { useNodeId } from './contexts/NodeIdContext';
export { applyNodeChanges, applyEdgeChanges, handleParentExpand } from './utils/changes'; export { applyNodeChanges, applyEdgeChanges } from './utils/changes';
export { isNode, isEdge } from './utils/general'; export { isNode, isEdge } from './utils/general';
export * from './additional-components'; export * from './additional-components';
+20 -19
View File
@@ -5,9 +5,10 @@ import {
adoptUserProvidedNodes, adoptUserProvidedNodes,
updateAbsolutePositions, updateAbsolutePositions,
panBy as panBySystem, panBy as panBySystem,
Dimensions,
updateNodeDimensions as updateNodeDimensionsSystem, updateNodeDimensions as updateNodeDimensionsSystem,
updateConnectionLookup, updateConnectionLookup,
handleParentExpand,
NodeChange,
} from '@xyflow/system'; } from '@xyflow/system';
import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes';
@@ -16,12 +17,12 @@ import type {
ReactFlowState, ReactFlowState,
Node, Node,
Edge, Edge,
NodeDimensionChange,
EdgeSelectionChange, EdgeSelectionChange,
NodeSelectionChange, NodeSelectionChange,
NodePositionChange, NodePositionChange,
UnselectNodesAndEdgesParams, UnselectNodesAndEdgesParams,
FitViewOptions, FitViewOptions,
InternalNode,
} from '../types'; } from '../types';
const createRFStore = ({ const createRFStore = ({
@@ -91,23 +92,10 @@ const createRFStore = ({
nodeOrigin, nodeOrigin,
debug, debug,
} = get(); } = get();
const changes: NodeDimensionChange[] = [];
const updatedNodes = updateNodeDimensionsSystem( const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin);
updates,
nodeLookup,
domNode,
nodeOrigin,
(id: string, dimensions: Dimensions) => {
changes.push({
id: id,
type: 'dimensions',
dimensions,
});
}
);
if (!updatedNodes) { if (changes.length === 0) {
return; return;
} }
@@ -137,17 +125,30 @@ const createRFStore = ({
} }
}, },
updateNodePositions: (nodeDragItems, dragging = false) => { updateNodePositions: (nodeDragItems, dragging = false) => {
const changes = nodeDragItems.map((node) => { const { nodeLookup } = get();
const change: NodePositionChange = { const triggerChangeNodes: InternalNode[] = [];
const changes: NodeChange[] = nodeDragItems.map((node) => {
const internalNode = nodeLookup.get(node.id);
const change: NodeChange = {
id: node.id, id: node.id,
type: 'position', type: 'position',
position: node.position, position: node.position,
dragging, dragging,
}; };
if (internalNode?.expandParent) {
triggerChangeNodes.push(internalNode);
}
return change; return change;
}); });
if (triggerChangeNodes.length > 0) {
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
changes.push(...parentExpandChanges);
}
get().triggerNodeChanges(changes); get().triggerNodeChanges(changes);
}, },
triggerNodeChanges: (changes) => { triggerNodeChanges: (changes) => {
+2 -54
View File
@@ -10,51 +10,6 @@ import type {
InternalNode, InternalNode,
} from '../types'; } from '../types';
export function handleParentExpand(updatedElements: any[], updateItem: any) {
for (const [index, item] of updatedElements.entries()) {
if (item.id === updateItem.parentNode) {
const parent = { ...item };
parent.computed ??= {};
const extendWidth = updateItem.position.x + updateItem.computed.width - parent.computed.width;
const extendHeight = updateItem.position.y + updateItem.computed.height - parent.computed.height;
if (extendWidth > 0 || extendHeight > 0 || updateItem.position.x < 0 || updateItem.position.y < 0) {
parent.width = parent.width ?? parent.computed.width;
parent.height = parent.height ?? parent.computed.height;
if (extendWidth > 0) {
parent.width += extendWidth;
}
if (extendHeight > 0) {
parent.height += extendHeight;
}
if (updateItem.position.x < 0) {
const xDiff = Math.abs(updateItem.position.x);
parent.position.x = parent.position.x - xDiff;
parent.width += xDiff;
updateItem.position.x = 0;
}
if (updateItem.position.y < 0) {
const yDiff = Math.abs(updateItem.position.y);
parent.position.y = parent.position.y - yDiff;
parent.height += yDiff;
updateItem.position.y = 0;
}
parent.computed.width = parent.width;
parent.computed.height = parent.height;
updatedElements[index] = parent;
}
break;
}
}
}
// This function applies changes to nodes or edges that are triggered by React Flow internally. // 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. // 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. // This function then applies the changes and returns the updated elements.
@@ -111,7 +66,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
const updatedElement = { ...element }; const updatedElement = { ...element };
for (const change of changes) { for (const change of changes) {
applyChange(change, updatedElement, updatedElements); applyChange(change, updatedElement);
} }
updatedElements.push(updatedElement); updatedElements.push(updatedElement);
@@ -121,7 +76,7 @@ function applyChanges(changes: any[], elements: any[]): any[] {
} }
// Applies a single change to an element. This is a *mutable* update. // Applies a single change to an element. This is a *mutable* update.
function applyChange(change: any, element: any, elements: any[] = []): any { function applyChange(change: any, element: any): any {
switch (change.type) { switch (change.type) {
case 'select': { case 'select': {
element.selected = change.selected; element.selected = change.selected;
@@ -137,9 +92,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.dragging = change.dragging; element.dragging = change.dragging;
} }
if (element.expandParent) {
handleParentExpand(elements, element);
}
break; break;
} }
@@ -159,10 +111,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any {
element.resizing = change.resizing; element.resizing = change.resizing;
} }
if (element.expandParent) {
handleParentExpand(elements, element);
}
break; break;
} }
} }
+69
View File
@@ -0,0 +1,69 @@
import type { XYPosition, Dimensions, NodeBase, EdgeBase } from '.';
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions?: Dimensions;
resizing?: boolean;
};
export type NodePositionChange = {
id: string;
type: 'position';
position?: XYPosition;
positionAbsolute?: XYPosition;
dragging?: boolean;
};
export type NodeSelectionChange = {
id: string;
type: 'select';
selected: boolean;
};
export type NodeRemoveChange = {
id: string;
type: 'remove';
};
export type NodeAddChange<NodeType extends NodeBase = NodeBase> = {
item: NodeType;
type: 'add';
};
export type NodeReplaceChange<NodeType extends NodeBase = NodeBase> = {
id: string;
item: NodeType;
type: 'replace';
};
/**
* Union type of all possible node changes.
* @public
*/
export type NodeChange<NodeType extends NodeBase = NodeBase> =
| NodeDimensionChange
| NodePositionChange
| NodeSelectionChange
| NodeRemoveChange
| NodeAddChange<NodeType>
| NodeReplaceChange<NodeType>;
export type EdgeSelectionChange = NodeSelectionChange;
export type EdgeRemoveChange = NodeRemoveChange;
export type EdgeAddChange<EdgeType extends EdgeBase = EdgeBase> = {
item: EdgeType;
type: 'add';
};
export type EdgeReplaceChange<EdgeType extends EdgeBase = EdgeBase> = {
id: string;
item: EdgeType;
type: 'replace';
};
export type EdgeChange<EdgeType extends EdgeBase = EdgeBase> =
| EdgeSelectionChange
| EdgeRemoveChange
| EdgeAddChange<EdgeType>
| EdgeReplaceChange<EdgeType>;
+1
View File
@@ -1,3 +1,4 @@
export * from './changes';
export * from './general'; export * from './general';
export * from './nodes'; export * from './nodes';
export * from './edges'; export * from './edges';
+1 -1
View File
@@ -110,7 +110,7 @@ export type NodeHandleBounds = {
export type NodeDimensionUpdate = { export type NodeDimensionUpdate = {
id: string; id: string;
nodeElement: HTMLDivElement; nodeElement: HTMLDivElement;
forceUpdate?: boolean; force?: boolean;
}; };
export type NodeBounds = XYPosition & { export type NodeBounds = XYPosition & {
+101 -19
View File
@@ -1,7 +1,6 @@
import { import {
NodeBase, NodeBase,
CoordinateExtent, CoordinateExtent,
Dimensions,
NodeDimensionUpdate, NodeDimensionUpdate,
NodeOrigin, NodeOrigin,
PanZoomInstance, PanZoomInstance,
@@ -12,9 +11,12 @@ import {
EdgeBase, EdgeBase,
EdgeLookup, EdgeLookup,
InternalNodeBase, InternalNodeBase,
NodeChange,
NodeLookup,
Rect,
} from '../types'; } from '../types';
import { getDimensions, getHandleBounds } from './dom'; import { getDimensions, getHandleBounds } from './dom';
import { isNumeric } from './general'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general';
import { getNodePositionWithOrigin } from './graph'; import { getNodePositionWithOrigin } from './graph';
type ParentNodes = Set<string>; type ParentNodes = Set<string>;
@@ -139,38 +141,102 @@ function calculateXYZPosition<NodeType extends NodeBase>(
); );
} }
export function updateNodeDimensions<NodeType extends NodeBase>( export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] {
updates: Map<string, NodeDimensionUpdate>, const changes: NodeChange[] = [];
nodeLookup: Map<string, InternalNodeBase<NodeType>>, const chilNodeRects = new Map<string, Rect>();
domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin,
onUpdate?: (id: string, dimensions: Dimensions) => void
): { hasUpdate: boolean } {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
let hasUpdate = false;
if (!viewportNode) { nodes.forEach((node) => {
return { hasUpdate }; if (node.expandParent && node.parentNode) {
const parentNode = nodeLookup.get(node.parentNode);
if (parentNode) {
const parentRect = chilNodeRects.get(node.parentNode) || nodeToRect(parentNode, node.origin);
const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin));
chilNodeRects.set(node.parentNode, expandedRect);
}
}
});
if (chilNodeRects.size > 0) {
chilNodeRects.forEach((rect, id) => {
const origParent = nodeLookup.get(id)!;
const { position } = getNodePositionWithOrigin(origParent, origParent.origin);
const dimensions = getNodeDimensions(origParent);
let xChange = null;
let yChange = null;
if (rect.x < position.x || rect.y < position.y) {
xChange = Math.abs(position.x - rect.x);
yChange = Math.abs(position.y - rect.y);
changes.push({
id,
type: 'position',
position: {
x: position.x - xChange,
y: position.y - yChange,
},
});
// @todo we need to reset child node positions if < 0
}
if (dimensions.width < rect.width || dimensions.height < rect.height) {
changes.push({
id,
type: 'dimensions',
resizing: true,
dimensions: {
width: Math.max(dimensions.width, rect.width),
height: Math.max(dimensions.height, rect.height),
},
});
}
});
} }
return changes;
}
export function updateNodeDimensions<NodeType extends InternalNodeBase>(
updates: Map<string, NodeDimensionUpdate>,
nodeLookup: Map<string, NodeType>,
domNode: HTMLElement | null,
nodeOrigin?: NodeOrigin
): NodeChange[] {
const viewportNode = domNode?.querySelector('.xyflow__viewport');
if (!viewportNode) {
return [];
}
const changes: NodeChange[] = [];
const style = window.getComputedStyle(viewportNode); const style = window.getComputedStyle(viewportNode);
const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform); const { m22: zoom } = new window.DOMMatrixReadOnly(style.transform);
// in this array we collect nodes, that might trigger changes (like expanding parent)
const triggerChangeNodes: NodeType[] = [];
updates.forEach((update) => { updates.forEach((update) => {
const node = nodeLookup.get(update.id); const node = nodeLookup.get(update.id);
if (node) { if (node?.hidden) {
nodeLookup.set(node.id, {
...node,
internals: {
...node.internals,
handleBounds: undefined,
},
});
} else if (node) {
const dimensions = getDimensions(update.nodeElement); const dimensions = getDimensions(update.nodeElement);
const doUpdate = !!( const doUpdate = !!(
dimensions.width && dimensions.width &&
dimensions.height && dimensions.height &&
(node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.forceUpdate) (node.computed?.width !== dimensions.width || node.computed?.height !== dimensions.height || update.force)
); );
if (doUpdate) { if (doUpdate) {
hasUpdate = true;
onUpdate?.(node.id, dimensions);
const newNode = { const newNode = {
...node, ...node,
computed: { computed: {
@@ -185,12 +251,28 @@ export function updateNodeDimensions<NodeType extends NodeBase>(
}, },
}, },
}; };
nodeLookup.set(node.id, newNode); nodeLookup.set(node.id, newNode);
changes.push({
id: newNode.id,
type: 'dimensions',
dimensions,
});
if (newNode.expandParent) {
triggerChangeNodes.push(newNode);
}
} }
} }
}); });
return { hasUpdate }; if (triggerChangeNodes.length > 0) {
const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup);
changes.push(...parentExpandChanges);
}
return changes;
} }
export function panBy({ export function panBy({