From 67d979985de52b9466c80df2967160e69aec47f2 Mon Sep 17 00:00:00 2001 From: moklick Date: Thu, 28 Mar 2024 17:08:05 +0100 Subject: [PATCH] refactor(react): handle parent expand in store instead of applyChanges --- .../src/components/NodeWrapper/index.tsx | 8 +- .../NodeRenderer/useResizeObserver.ts | 4 +- .../react/src/hooks/useUpdateNodeInternals.ts | 2 +- packages/react/src/index.ts | 2 +- packages/react/src/store/index.ts | 39 +++--- packages/react/src/utils/changes.ts | 56 +------- packages/system/src/types/changes.ts | 69 ++++++++++ packages/system/src/types/index.ts | 1 + packages/system/src/types/nodes.ts | 2 +- packages/system/src/utils/store.ts | 120 +++++++++++++++--- 10 files changed, 203 insertions(+), 100 deletions(-) create mode 100644 packages/system/src/types/changes.ts diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 5e2b91e2..e3fbf16c 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -88,9 +88,11 @@ export function NodeWrapper({ const moveSelectedNodes = useMoveSelectedNodes(); useEffect(() => { + const currNode = nodeRef.current; + return () => { - if (nodeRef.current) { - resizeObserver?.unobserve(nodeRef.current); + if (currNode) { + resizeObserver?.unobserve(currNode); } }; }, []); @@ -122,7 +124,7 @@ export function NodeWrapper({ if (targetPosChanged) { 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]); diff --git a/packages/react/src/container/NodeRenderer/useResizeObserver.ts b/packages/react/src/container/NodeRenderer/useResizeObserver.ts index f0d0a22e..82c079bd 100644 --- a/packages/react/src/container/NodeRenderer/useResizeObserver.ts +++ b/packages/react/src/container/NodeRenderer/useResizeObserver.ts @@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef } from 'react'; import { ReactFlowState } from '../../types'; import { useStore } from '../../hooks/useStore'; +import { NodeDimensionUpdate } from '@xyflow/system'; const selector = (s: ReactFlowState) => s.updateNodeDimensions; @@ -15,14 +16,13 @@ export function useResizeObserver() { } const observer = new ResizeObserver((entries: ResizeObserverEntry[]) => { - const updates = new Map(); + const updates = new Map(); entries.forEach((entry: ResizeObserverEntry) => { const id = entry.target.getAttribute('data-id') as string; updates.set(id, { id, nodeElement: entry.target as HTMLDivElement, - forceUpdate: true, }); }); diff --git a/packages/react/src/hooks/useUpdateNodeInternals.ts b/packages/react/src/hooks/useUpdateNodeInternals.ts index 1d7cc681..b156d506 100644 --- a/packages/react/src/hooks/useUpdateNodeInternals.ts +++ b/packages/react/src/hooks/useUpdateNodeInternals.ts @@ -21,7 +21,7 @@ export function useUpdateNodeInternals(): UpdateNodeInternals { const nodeElement = domNode?.querySelector(`.react-flow__node[data-id="${updateId}"]`) as HTMLDivElement; if (nodeElement) { - updates.set(updateId, { id: updateId, nodeElement, forceUpdate: true }); + updates.set(updateId, { id: updateId, nodeElement, force: true }); } }); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 800ebd94..91be19eb 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -28,7 +28,7 @@ export { useNodesData } from './hooks/useNodesData'; export { useConnection } from './hooks/useConnection'; 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 * from './additional-components'; diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 3eddf5bc..b5a5effa 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -5,9 +5,10 @@ import { adoptUserProvidedNodes, updateAbsolutePositions, panBy as panBySystem, - Dimensions, updateNodeDimensions as updateNodeDimensionsSystem, updateConnectionLookup, + handleParentExpand, + NodeChange, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -16,12 +17,12 @@ import type { ReactFlowState, Node, Edge, - NodeDimensionChange, EdgeSelectionChange, NodeSelectionChange, NodePositionChange, UnselectNodesAndEdgesParams, FitViewOptions, + InternalNode, } from '../types'; const createRFStore = ({ @@ -91,23 +92,10 @@ const createRFStore = ({ nodeOrigin, debug, } = get(); - const changes: NodeDimensionChange[] = []; - const updatedNodes = updateNodeDimensionsSystem( - updates, - nodeLookup, - domNode, - nodeOrigin, - (id: string, dimensions: Dimensions) => { - changes.push({ - id: id, - type: 'dimensions', - dimensions, - }); - } - ); + const changes = updateNodeDimensionsSystem(updates, nodeLookup, domNode, nodeOrigin); - if (!updatedNodes) { + if (changes.length === 0) { return; } @@ -137,17 +125,30 @@ const createRFStore = ({ } }, updateNodePositions: (nodeDragItems, dragging = false) => { - const changes = nodeDragItems.map((node) => { - const change: NodePositionChange = { + const { nodeLookup } = get(); + const triggerChangeNodes: InternalNode[] = []; + + const changes: NodeChange[] = nodeDragItems.map((node) => { + const internalNode = nodeLookup.get(node.id); + const change: NodeChange = { id: node.id, type: 'position', position: node.position, dragging, }; + if (internalNode?.expandParent) { + triggerChangeNodes.push(internalNode); + } + return change; }); + if (triggerChangeNodes.length > 0) { + const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + changes.push(...parentExpandChanges); + } + get().triggerNodeChanges(changes); }, triggerNodeChanges: (changes) => { diff --git a/packages/react/src/utils/changes.ts b/packages/react/src/utils/changes.ts index 437c80dc..e4d0464d 100644 --- a/packages/react/src/utils/changes.ts +++ b/packages/react/src/utils/changes.ts @@ -10,51 +10,6 @@ import type { InternalNode, } 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. // 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. @@ -111,7 +66,7 @@ function applyChanges(changes: any[], elements: any[]): any[] { const updatedElement = { ...element }; for (const change of changes) { - applyChange(change, updatedElement, updatedElements); + applyChange(change, 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. -function applyChange(change: any, element: any, elements: any[] = []): any { +function applyChange(change: any, element: any): any { switch (change.type) { case 'select': { element.selected = change.selected; @@ -137,9 +92,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.dragging = change.dragging; } - if (element.expandParent) { - handleParentExpand(elements, element); - } break; } @@ -159,10 +111,6 @@ function applyChange(change: any, element: any, elements: any[] = []): any { element.resizing = change.resizing; } - if (element.expandParent) { - handleParentExpand(elements, element); - } - break; } } diff --git a/packages/system/src/types/changes.ts b/packages/system/src/types/changes.ts new file mode 100644 index 00000000..d2da81cf --- /dev/null +++ b/packages/system/src/types/changes.ts @@ -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 = { + item: NodeType; + type: 'add'; +}; + +export type NodeReplaceChange = { + id: string; + item: NodeType; + type: 'replace'; +}; + +/** + * Union type of all possible node changes. + * @public + */ +export type NodeChange = + | NodeDimensionChange + | NodePositionChange + | NodeSelectionChange + | NodeRemoveChange + | NodeAddChange + | NodeReplaceChange; + +export type EdgeSelectionChange = NodeSelectionChange; +export type EdgeRemoveChange = NodeRemoveChange; +export type EdgeAddChange = { + item: EdgeType; + type: 'add'; +}; + +export type EdgeReplaceChange = { + id: string; + item: EdgeType; + type: 'replace'; +}; + +export type EdgeChange = + | EdgeSelectionChange + | EdgeRemoveChange + | EdgeAddChange + | EdgeReplaceChange; diff --git a/packages/system/src/types/index.ts b/packages/system/src/types/index.ts index 421a8531..99653baf 100644 --- a/packages/system/src/types/index.ts +++ b/packages/system/src/types/index.ts @@ -1,3 +1,4 @@ +export * from './changes'; export * from './general'; export * from './nodes'; export * from './edges'; diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 6929321b..2d9dfd5d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -110,7 +110,7 @@ export type NodeHandleBounds = { export type NodeDimensionUpdate = { id: string; nodeElement: HTMLDivElement; - forceUpdate?: boolean; + force?: boolean; }; export type NodeBounds = XYPosition & { diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 21d5f9cf..1c14d5f5 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -1,7 +1,6 @@ import { NodeBase, CoordinateExtent, - Dimensions, NodeDimensionUpdate, NodeOrigin, PanZoomInstance, @@ -12,9 +11,12 @@ import { EdgeBase, EdgeLookup, InternalNodeBase, + NodeChange, + NodeLookup, + Rect, } from '../types'; import { getDimensions, getHandleBounds } from './dom'; -import { isNumeric } from './general'; +import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getNodePositionWithOrigin } from './graph'; type ParentNodes = Set; @@ -139,38 +141,102 @@ function calculateXYZPosition( ); } -export function updateNodeDimensions( - updates: Map, - nodeLookup: Map>, - domNode: HTMLElement | null, - nodeOrigin?: NodeOrigin, - onUpdate?: (id: string, dimensions: Dimensions) => void -): { hasUpdate: boolean } { - const viewportNode = domNode?.querySelector('.xyflow__viewport'); - let hasUpdate = false; +export function handleParentExpand(nodes: InternalNodeBase[], nodeLookup: NodeLookup): NodeChange[] { + const changes: NodeChange[] = []; + const chilNodeRects = new Map(); - if (!viewportNode) { - return { hasUpdate }; + nodes.forEach((node) => { + 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( + updates: Map, + nodeLookup: Map, + domNode: HTMLElement | null, + nodeOrigin?: NodeOrigin +): NodeChange[] { + const viewportNode = domNode?.querySelector('.xyflow__viewport'); + + if (!viewportNode) { + return []; + } + + const changes: NodeChange[] = []; const style = window.getComputedStyle(viewportNode); 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) => { 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 doUpdate = !!( dimensions.width && 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) { - hasUpdate = true; - onUpdate?.(node.id, dimensions); - const newNode = { ...node, computed: { @@ -185,12 +251,28 @@ export function updateNodeDimensions( }, }, }; + 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({