diff --git a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx index 2052c98b..8e6ca2f1 100644 --- a/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx @@ -9,6 +9,10 @@ import { type NodeChange, type NodeDimensionChange, type NodePositionChange, + handleExpandParent, + evaluateAbsolutePosition, + ParentExpandChild, + XYPosition, } from '@xyflow/system'; import { useStoreApi } from '../../hooks/useStore'; @@ -62,24 +66,49 @@ function ResizeControl({ }; }, onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { - const { triggerNodeChanges } = store.getState(); + const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState(); const changes: NodeChange[] = []; + const nextPosition = { x: change.x, y: change.y }; - if (change.isXPosChange || change.isYPosChange) { - const positionChange: NodePositionChange = { - id, - type: 'position', - position: { - x: change.x, - y: change.y, + const node = nodeLookup.get(id); + if (node && node.expandParent && node.parentId) { + const child: ParentExpandChild = { + id: node.id, + parentId: node.parentId, + rect: { + width: change.width ?? node.measured.width!, + height: change.height ?? node.measured.height!, + ...evaluateAbsolutePosition( + { + x: change.x ?? node.position.x, + y: change.y ?? node.position.y, + }, + node.parentId, + nodeLookup, + node.origin ?? nodeOrigin + ), }, }; + const parentExpandChanges = handleExpandParent([child], nodeLookup, parentLookup, nodeOrigin); + changes.push(...parentExpandChanges); + + // when the parent was expanded by the child node, its position will be clamped at 0,0 + nextPosition.x = change.x ? Math.max(0, change.x) : undefined; + nextPosition.y = change.y ? Math.max(0, change.y) : undefined; + } + + if (nextPosition.x !== undefined && nextPosition.y !== undefined) { + const positionChange: NodePositionChange = { + id, + type: 'position', + position: { ...(nextPosition as XYPosition) }, + }; changes.push(positionChange); } - if (change.isWidthChange || change.isHeightChange) { + if (change.width !== undefined && change.height !== undefined) { const dimensionChange: NodeDimensionChange = { id, type: 'dimensions', diff --git a/packages/react/src/components/NodeWrapper/index.tsx b/packages/react/src/components/NodeWrapper/index.tsx index 18c2d314..acc0d7fe 100644 --- a/packages/react/src/components/NodeWrapper/index.tsx +++ b/packages/react/src/components/NodeWrapper/index.tsx @@ -42,21 +42,14 @@ export function NodeWrapper({ nodeOrigin, onError, }: NodeWrapperProps) { - const { node, positionAbsoluteX, positionAbsoluteY, zIndex, isParent } = useStore((s) => { + const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; - - const positionAbsolute = nodeExtent - ? clampPosition(node.internals.positionAbsolute, nodeExtent) - : node.internals.positionAbsolute || { x: 0, y: 0 }; + const isParent = s.parentLookup.has(id); return { node, - // we are mutating positionAbsolute, z and isParent attributes for sub flows - // so we we need to force a re-render when some change - positionAbsoluteX: positionAbsolute.x, - positionAbsoluteY: positionAbsolute.y, - zIndex: node.internals.z, - isParent: node.internals.isParent, + internals: node.internals, + isParent, }; }, shallow); @@ -140,10 +133,15 @@ export function NodeWrapper({ return null; } - const positionAbsoluteOrigin = getPositionWithOrigin({ - x: positionAbsoluteX, - y: positionAbsoluteY, - ...nodeDimensions, + const positionAbsolute = nodeExtent + ? clampPosition(node.internals.positionAbsolute, nodeExtent) + : node.internals.positionAbsolute || { x: 0, y: 0 }; + + const positionWithOrigin = getPositionWithOrigin({ + x: positionAbsolute.x, + y: positionAbsolute.y, + width: nodeDimensions.width, + height: nodeDimensions.height, origin: node.origin || nodeOrigin, }); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; @@ -190,7 +188,7 @@ export function NodeWrapper({ store.setState({ ariaLiveMessage: `Moved selected node ${event.key .replace('Arrow', '') - .toLowerCase()}. New position, x: ${~~positionAbsoluteX}, y: ${~~positionAbsoluteY}`, + .toLowerCase()}. New position, x: ${~~positionAbsolute.x}, y: ${~~positionAbsolute.y}`, }); moveSelectedNodes({ @@ -220,8 +218,8 @@ export function NodeWrapper({ ])} ref={nodeRef} style={{ - zIndex, - transform: `translate(${positionAbsoluteOrigin.x}px,${positionAbsoluteOrigin.y}px)`, + zIndex: internals.z, + transform: `translate(${positionWithOrigin.x}px,${positionWithOrigin.y}px)`, pointerEvents: hasPointerEvents ? 'all' : 'none', visibility: initialized ? 'visible' : 'hidden', ...node.style, @@ -246,15 +244,15 @@ export function NodeWrapper({ id={id} data={node.data} type={nodeType} - positionAbsoluteX={positionAbsoluteX} - positionAbsoluteY={positionAbsoluteY} + positionAbsoluteX={positionAbsolute.x} + positionAbsoluteY={positionAbsolute.y} selected={node.selected} isConnectable={isConnectable} sourcePosition={node.sourcePosition} targetPosition={node.targetPosition} dragging={dragging} dragHandle={node.dragHandle} - zIndex={zIndex} + zIndex={internals.z} {...nodeDimensions} /> diff --git a/packages/react/src/hooks/useReactFlow.ts b/packages/react/src/hooks/useReactFlow.ts index 31035bd1..5f3d2acb 100644 --- a/packages/react/src/hooks/useReactFlow.ts +++ b/packages/react/src/hooks/useReactFlow.ts @@ -1,6 +1,6 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { - evaluateNodePosition, + evaluateAbsolutePosition, getElementsToRemove, getOverlappingArea, isRectObject, @@ -232,10 +232,21 @@ export function useReactFlow { const { nodeLookup, nodeOrigin } = store.getState(); - const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id)!; - const nodeWithPos = evaluateNodePosition(nodeToUse, nodeLookup, nodeOrigin); - return nodeWithPos ? nodeToRect(nodeWithPos) : null; + const nodeToUse = isNode(node) ? node : nodeLookup.get(node.id)!; + const position = nodeToUse.parentId + ? evaluateAbsolutePosition(nodeToUse.position, nodeToUse.parentId, nodeLookup, nodeOrigin) + : nodeToUse.position; + + const nodeWithPosition = { + id: nodeToUse.id, + position, + width: nodeToUse.measured?.width ?? nodeToUse.width, + height: nodeToUse.measured?.height ?? nodeToUse.height, + data: nodeToUse.data, + }; + + return nodeToRect(nodeWithPosition); }, []); const getIntersectingNodes = useCallback>( diff --git a/packages/react/src/store/index.ts b/packages/react/src/store/index.ts index 7ce263c9..50d981c2 100644 --- a/packages/react/src/store/index.ts +++ b/packages/react/src/store/index.ts @@ -7,10 +7,11 @@ import { panBy as panBySystem, updateNodeInternals as updateNodeInternalsSystem, updateConnectionLookup, - handleParentExpand, + handleExpandParent, NodeChange, EdgeSelectionChange, NodeSelectionChange, + ParentExpandChild, } from '@xyflow/system'; import { applyEdgeChanges, applyNodeChanges, createSelectionChange, getSelectionChanges } from '../utils/changes'; @@ -38,14 +39,14 @@ const createRFStore = ({ (set, get) => ({ ...getInitialState({ nodes, edges, width, height, fitView, defaultNodes, defaultEdges }), setNodes: (nodes: Node[]) => { - const { nodeLookup, nodeOrigin, elevateNodesOnSelect } = get(); + const { nodeLookup, parentLookup, nodeOrigin, elevateNodesOnSelect } = get(); // setNodes() is called exclusively in response to user actions: // - either when the `` prop is updated in the controlled ReactFlow setup, // - or when the user calls something like `reactFlowInstance.setNodes()` in an uncontrolled ReactFlow setup. // // When this happens, we take the note objects passed by the user and extend them with fields // relevant for internal React Flow operations. - adoptUserNodes(nodes, nodeLookup, { nodeOrigin, elevateNodesOnSelect }); + adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin, elevateNodesOnSelect, checkEquality: true }); set({ nodes }); }, @@ -76,6 +77,7 @@ const createRFStore = ({ onNodesChange, fitView, nodeLookup, + parentLookup, fitViewOnInit, fitViewDone, fitViewOnInitOptions, @@ -84,7 +86,13 @@ const createRFStore = ({ debug, } = get(); - const { changes, updatedInternals } = updateNodeInternalsSystem(updates, nodeLookup, domNode, nodeOrigin); + const { changes, updatedInternals } = updateNodeInternalsSystem( + updates, + nodeLookup, + parentLookup, + domNode, + nodeOrigin + ); if (!updatedInternals) { return; @@ -116,8 +124,8 @@ const createRFStore = ({ } }, updateNodePositions: (nodeDragItems, dragging = false) => { - const { nodeLookup } = get(); - const triggerChangeNodes: InternalNode[] = []; + const { nodeLookup, parentLookup } = get(); + const parentExpandChildren: ParentExpandChild[] = []; const changes: NodeChange[] = nodeDragItems.map((node) => { // @todo add expandParent to drag item so that we can get rid of the look up here @@ -129,13 +137,14 @@ const createRFStore = ({ dragging, }; - if (internalNode?.expandParent && change.position) { - triggerChangeNodes.push({ - ...internalNode, - position: change.position, - internals: { - ...internalNode.internals, - positionAbsolute: node.internals.positionAbsolute, + if (internalNode?.expandParent && internalNode?.parentId && change.position) { + parentExpandChildren.push({ + id: internalNode.id, + parentId: internalNode.parentId, + rect: { + ...node.internals.positionAbsolute, + width: internalNode.measured.width!, + height: internalNode.measured.height!, }, }); @@ -146,8 +155,8 @@ const createRFStore = ({ return change; }); - if (triggerChangeNodes.length > 0) { - const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + if (parentExpandChildren.length > 0) { + const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup); changes.push(...parentExpandChanges); } diff --git a/packages/react/src/store/initialState.ts b/packages/react/src/store/initialState.ts index cc754976..d3812725 100644 --- a/packages/react/src/store/initialState.ts +++ b/packages/react/src/store/initialState.ts @@ -29,13 +29,14 @@ const getInitialState = ({ fitView?: boolean; } = {}): ReactFlowStore => { const nodeLookup = new Map(); + const parentLookup = new Map(); const connectionLookup = new Map(); const edgeLookup = new Map(); const storeEdges = defaultEdges ?? edges ?? []; const storeNodes = defaultNodes ?? nodes ?? []; updateConnectionLookup(connectionLookup, edgeLookup, storeEdges); - adoptUserNodes(storeNodes, nodeLookup, { + adoptUserNodes(storeNodes, nodeLookup, parentLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, }); @@ -59,6 +60,7 @@ const getInitialState = ({ transform, nodes: storeNodes, nodeLookup, + parentLookup, edges: storeEdges, edgeLookup, connectionLookup, diff --git a/packages/react/src/types/store.ts b/packages/react/src/types/store.ts index 397ca6f1..bd5f5720 100644 --- a/packages/react/src/types/store.ts +++ b/packages/react/src/types/store.ts @@ -54,6 +54,7 @@ export type ReactFlowStore>; + parentLookup: Map[]>; edges: Edge[]; edgeLookup: EdgeLookup; connectionLookup: ConnectionLookup; diff --git a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte index c0b8165e..dc08aee0 100644 --- a/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte +++ b/packages/svelte/src/lib/container/NodeRenderer/NodeRenderer.svelte @@ -10,7 +10,8 @@ nodesDraggable, nodesConnectable, elementsSelectable, - updateNodeInternals + updateNodeInternals, + parentLookup } = useStore(); const resizeObserver: ResizeObserver | null = @@ -65,7 +66,7 @@ positionY={node.internals.positionAbsolute.y} positionOriginX={posOrigin.x ?? 0} positionOriginY={posOrigin.y ?? 0} - isParent={!!node.internals.isParent} + isParent={$parentLookup.has(node.id)} style={node.style} class={node.class} type={node.type ?? 'default'} diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index c346bd53..1e5f39e2 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -70,23 +70,27 @@ }, onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => { const node = $nodeLookup.get(id)?.internals.userNode; - if (node) { - node.height = change.isHeightChange ? change.height : node.height; - node.width = change.isWidthChange ? change.width : node.width; - node.position = - change.isXPosChange || change.isYPosChange - ? { x: change.x, y: change.y } - : node.position; - - for (const childChange of childChanges) { - const childNode = $nodeLookup.get(childChange.id)?.internals.userNode; - if (childNode) { - childNode.position = childChange.position; - } - } - - $nodes = $nodes; + if (!node) { + return; } + + if (change.x !== undefined && change.y !== undefined) { + node.position = { x: change.x, y: change.y }; + } + + if (change.width !== undefined && change.height !== undefined) { + node.width = change.width; + node.height = change.height; + } + + for (const childChange of childChanges) { + const childNode = $nodeLookup.get(childChange.id)?.internals.userNode; + if (childNode) { + childNode.position = childChange.position; + } + } + + $nodes = $nodes; } }); } diff --git a/packages/svelte/src/lib/store/index.ts b/packages/svelte/src/lib/store/index.ts index c808d2af..07d4ab76 100644 --- a/packages/svelte/src/lib/store/index.ts +++ b/packages/svelte/src/lib/store/index.ts @@ -83,6 +83,7 @@ export function createStore({ const { changes, updatedInternals } = updateNodeInternalsSystem( updates, nodeLookup, + get(store.parentLookup), get(store.domNode), get(store.nodeOrigin) ); diff --git a/packages/svelte/src/lib/store/initial-store.ts b/packages/svelte/src/lib/store/initial-store.ts index 7d2b2444..073f206c 100644 --- a/packages/svelte/src/lib/store/initial-store.ts +++ b/packages/svelte/src/lib/store/initial-store.ts @@ -81,7 +81,8 @@ export const getInitialStore = ({ fitView?: boolean; }) => { const nodeLookup: NodeLookup = new Map(); - adoptUserNodes(nodes, nodeLookup, { + const parentLookup = new Map(); + adoptUserNodes(nodes, nodeLookup, parentLookup, { nodeOrigin: [0, 0], elevateNodesOnSelect: false, checkEquality: false @@ -104,8 +105,9 @@ export const getInitialStore = ({ return { flowId: writable(null), - nodes: createNodesStore(nodes, nodeLookup), + nodes: createNodesStore(nodes, nodeLookup, parentLookup), nodeLookup: readable>(nodeLookup), + parentLookup: readable>(parentLookup), edgeLookup: readable>(edgeLookup), visibleNodes: readable([]), edges: createEdgesStore(edges, connectionLookup, edgeLookup), diff --git a/packages/svelte/src/lib/store/utils.ts b/packages/svelte/src/lib/store/utils.ts index 4dabbd33..5fd66cfe 100644 --- a/packages/svelte/src/lib/store/utils.ts +++ b/packages/svelte/src/lib/store/utils.ts @@ -127,7 +127,8 @@ export type NodeStoreOptions = { // The user only passes in relative positions, so we need to calculate the absolute positions based on the parent nodes. export const createNodesStore = ( nodes: Node[], - nodeLookup: NodeLookup + nodeLookup: NodeLookup, + parentLookup: Map ): { subscribe: (this: void, run: Subscriber) => Unsubscriber; update: (this: void, updater: Updater) => void; @@ -141,7 +142,7 @@ export const createNodesStore = ( let elevateNodesOnSelect = true; const _set = (nds: Node[]): Node[] => { - adoptUserNodes(nds, nodeLookup, { + adoptUserNodes(nds, nodeLookup, parentLookup, { elevateNodesOnSelect, defaults, checkEquality: false diff --git a/packages/system/src/types/nodes.ts b/packages/system/src/types/nodes.ts index 6f853e9f..a79ca24d 100644 --- a/packages/system/src/types/nodes.ts +++ b/packages/system/src/types/nodes.ts @@ -73,8 +73,6 @@ export type InternalNodeBase = NodeType & internals: { positionAbsolute: XYPosition; z: number; - // @todo should we rename this to "handles" and use same type as node.handles? - isParent: boolean; /** Holds a reference to the original node object provided by the user. * Used as an optimization to avoid certain operations. */ userNode: NodeType; @@ -144,3 +142,4 @@ export type NodeHandle = Optional; export type Align = 'center' | 'start' | 'end'; export type NodeLookup = Map; +export type ParentLookup = Map; diff --git a/packages/system/src/utils/general.ts b/packages/system/src/utils/general.ts index 55c11646..0c259bbc 100644 --- a/packages/system/src/utils/general.ts +++ b/packages/system/src/utils/general.ts @@ -226,31 +226,27 @@ export function nodeHasDimensions(node: No } /** - * Helper to calculate the absolute position of a node + * Convert child position to aboslute position * * @internal - * @param node + * @param position + * @param parentId * @param nodeLookup * @param nodeOrigin * @returns an internal node with an absolute position */ -export function evaluateNodePosition( - node: NodeBase | InternalNodeBase, +export function evaluateAbsolutePosition( + position: XYPosition, + parentId: string, nodeLookup: NodeLookup, nodeOrigin: NodeOrigin = [0, 0] -): InternalNodeBase | null { - const internalNode = nodeLookup.get(node.id); +): XYPosition { + let nextParentId: string | undefined = parentId; + const positionAbsolute = { ...position }; - if (!internalNode) { - return null; - } - - let parentId = internalNode.parentId; - const positionAbsolute = { ...node.position }; - - while (parentId) { + while (nextParentId) { const parent = nodeLookup.get(parentId); - parentId = parent?.parentId; + nextParentId = parent?.parentId; if (parent) { const origin = parent.origin || nodeOrigin; @@ -261,11 +257,5 @@ export function evaluateNodePosition( } } - return { - ...internalNode, - internals: { - ...internalNode.internals, - positionAbsolute, - }, - }; + return positionAbsolute; } diff --git a/packages/system/src/utils/store.ts b/packages/system/src/utils/store.ts index 0d7f2645..67a7c365 100644 --- a/packages/system/src/utils/store.ts +++ b/packages/system/src/utils/store.ts @@ -15,10 +15,12 @@ import { Rect, NodeDimensionChange, NodePositionChange, + ParentLookup, } from '../types'; import { getDimensions, getHandleBounds } from './dom'; import { getBoundsOfRects, getNodeDimensions, isNumeric, nodeToRect } from './general'; import { getNodePositionWithOrigin } from './graph'; +import { ParentExpandChild } from './types'; export function updateAbsolutePositions( nodeLookup: Map>, @@ -26,41 +28,41 @@ export function updateAbsolutePositions( nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, defaults: {}, - }, - parentNodeIds?: Set + } ) { const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; - for (const [id, node] of nodeLookup) { + for (const [, node] of nodeLookup) { const parentId = node.parentId; - if (parentId && !nodeLookup.has(parentId)) { + if (!parentId) { + continue; + } + + if (!nodeLookup.has(parentId)) { throw new Error(`Parent node ${parentId} not found`); } - if (parentId || node.internals.isParent || parentNodeIds?.has(id)) { - const parentNode = parentId ? nodeLookup.get(parentId) : null; - const { x, y, z } = calculateXYZPosition( - node, - nodeLookup, - { - ...node.position, - z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0), - }, - parentNode?.origin || options.nodeOrigin - ); + const parentNode = nodeLookup.get(parentId); + const { x, y, z } = calculateXYZPosition( + node, + nodeLookup, + { + ...node.position, + z: (isNumeric(node.zIndex) ? node.zIndex : 0) + (node.selected ? selectedNodeZ : 0), + }, + parentNode?.origin ?? options.nodeOrigin + ); - const currPosition = node.internals.positionAbsolute; - const positionChanged = x !== currPosition.x || y !== currPosition.y; + const currPosition = node.internals.positionAbsolute; + const positionChanged = x !== currPosition.x || y !== currPosition.y; - node.internals.positionAbsolute = positionChanged ? { x, y } : currPosition; - node.internals.z = z; - - if (parentNodeIds !== undefined) { - node.internals.isParent = !!parentNodeIds?.has(id); - } - - nodeLookup.set(id, node); + if (positionChanged || z !== node.internals.z) { + node.internals = { + ...node.internals, + positionAbsolute: positionChanged ? { x, y } : currPosition, + z, + }; } } } @@ -75,6 +77,7 @@ type UpdateNodesOptions = { export function adoptUserNodes( nodes: NodeType[], nodeLookup: Map>, + parentLookup: Map[]>, options: UpdateNodesOptions = { nodeOrigin: [0, 0] as NodeOrigin, elevateNodesOnSelect: true, @@ -84,20 +87,18 @@ export function adoptUserNodes( ) { const tmpLookup = new Map(nodeLookup); nodeLookup.clear(); + parentLookup.clear(); + const selectedNodeZ: number = options?.elevateNodesOnSelect ? 1000 : 0; - const parentNodeIds = new Set(); nodes.forEach((userNode) => { const currentStoreNode = tmpLookup.get(userNode.id); - if (userNode.parentId) { - parentNodeIds.add(userNode.parentId); - } - + let internalNode = currentStoreNode!; if (options.checkEquality && userNode === currentStoreNode?.internals.userNode) { nodeLookup.set(userNode.id, currentStoreNode); } else { - nodeLookup.set(userNode.id, { + internalNode = { ...options.defaults, ...userNode, measured: { @@ -109,14 +110,23 @@ export function adoptUserNodes( handleBounds: currentStoreNode?.internals.handleBounds, z: (isNumeric(userNode.zIndex) ? userNode.zIndex : 0) + (userNode.selected ? selectedNodeZ : 0), userNode, - isParent: false, }, - }); + }; + nodeLookup.set(userNode.id, internalNode); + } + + if (userNode.parentId) { + const childNodes = parentLookup.get(userNode.parentId); + if (childNodes) { + childNodes.push(internalNode); + } else { + parentLookup.set(userNode.parentId, [internalNode]); + } } }); - if (parentNodeIds.size > 0) { - updateAbsolutePositions(nodeLookup, options, parentNodeIds); + if (parentLookup.size > 0) { + updateAbsolutePositions(nodeLookup, options); } } @@ -145,38 +155,40 @@ function calculateXYZPosition( ); } -export function handleParentExpand( - nodes: InternalNodeBase[], - nodeLookup: NodeLookup +export function handleExpandParent( + children: ParentExpandChild[], + nodeLookup: NodeLookup, + parentLookup: ParentLookup, + nodeOrigin?: NodeOrigin ): (NodeDimensionChange | NodePositionChange)[] { const changes: (NodeDimensionChange | NodePositionChange)[] = []; - const chilNodeRects = new Map(); + const parentExpansions = new Map(); - nodes.forEach((node) => { - const parentId = node.parentId; - if (node.expandParent && parentId) { - const parentNode = nodeLookup.get(parentId); - - if (parentNode) { - const parentRect = chilNodeRects.get(parentId) || nodeToRect(parentNode, node.origin); - const expandedRect = getBoundsOfRects(parentRect, nodeToRect(node, node.origin)); - chilNodeRects.set(parentId, expandedRect); - } + // determine the expanded rectangle the child nodes would take for each parent + for (const child of children) { + const parent = nodeLookup.get(child.parentId); + if (!parent) { + continue; } - }); - if (chilNodeRects.size > 0) { - chilNodeRects.forEach((rect, id) => { - const origParent = nodeLookup.get(id)!; - const { position } = getNodePositionWithOrigin(origParent, origParent.origin); - const dimensions = getNodeDimensions(origParent); + const parentRect = + parentExpansions.get(child.parentId)?.expandedRect ?? nodeToRect(parent, parent.origin ?? nodeOrigin); + const expandedRect = getBoundsOfRects(parentRect, child.rect); + parentExpansions.set(child.parentId, { expandedRect, parent }); + } - if (rect.x < position.x || rect.y < position.y) { - const xChange = Math.round(Math.abs(position.x - rect.x)); - const yChange = Math.round(Math.abs(position.y - rect.y)); + if (parentExpansions.size > 0) { + parentExpansions.forEach(({ expandedRect, parent }, parentId) => { + // determine the position & dimensions of the parent + const { position } = getNodePositionWithOrigin(parent, parent.origin); + const dimensions = getNodeDimensions(parent); + // determine how much the parent expands by moving the position + let xChange = expandedRect.x < position.x ? Math.round(Math.abs(position.x - expandedRect.x)) : 0; + let yChange = expandedRect.y < position.y ? Math.round(Math.abs(position.y - expandedRect.y)) : 0; + if (xChange > 0 || yChange > 0) { changes.push({ - id, + id: parentId, type: 'position', position: { x: position.x - xChange, @@ -184,25 +196,31 @@ export function handleParentExpand( }, }); - changes.push({ - id, - type: 'dimensions', - resizing: true, - dimensions: { - width: dimensions.width + xChange, - height: dimensions.height + yChange, - }, + // We move all child nodes in the oppsite direction + // so the x,y changes of the parent do not move the children + const childNodes = parentLookup.get(parentId); + childNodes?.forEach((childNode) => { + if (!children.some((child) => child.id === childNode.id)) { + changes.push({ + id: childNode.id, + type: 'position', + position: { + x: childNode.position.x + xChange, + y: childNode.position.y + yChange, + }, + }); + } }); + } - // @todo we need to reset child node positions if < 0 - } else if (dimensions.width < rect.width || dimensions.height < rect.height) { + if (dimensions.width < expandedRect.width || dimensions.height < expandedRect.height) { changes.push({ - id, + id: parentId, type: 'dimensions', resizing: true, dimensions: { - width: Math.max(dimensions.width, rect.width), - height: Math.max(dimensions.height, rect.height), + width: Math.max(dimensions.width, Math.round(expandedRect.width)), + height: Math.max(dimensions.height, Math.round(expandedRect.height)), }, }); } @@ -214,7 +232,8 @@ export function handleParentExpand( export function updateNodeInternals( updates: Map, - nodeLookup: Map, + nodeLookup: NodeLookup, + parentLookup: ParentLookup, domNode: HTMLElement | null, nodeOrigin?: NodeOrigin ): { changes: (NodeDimensionChange | NodePositionChange)[]; updatedInternals: boolean } { @@ -229,7 +248,7 @@ export function updateNodeInternals( 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[] = []; + const parentExpandChildren: ParentExpandChild[] = []; updates.forEach((update) => { const node = nodeLookup.get(update.id); @@ -275,16 +294,20 @@ export function updateNodeInternals( dimensions, }); - if (newNode.expandParent) { - triggerChangeNodes.push(newNode); + if (newNode.expandParent && newNode.parentId) { + parentExpandChildren.push({ + id: newNode.id, + parentId: newNode.parentId, + rect: nodeToRect(newNode, newNode.origin || nodeOrigin), + }); } } } } }); - if (triggerChangeNodes.length > 0) { - const parentExpandChanges = handleParentExpand(triggerChangeNodes, nodeLookup); + if (parentExpandChildren.length > 0) { + const parentExpandChanges = handleExpandParent(parentExpandChildren, nodeLookup, parentLookup, nodeOrigin); changes.push(...parentExpandChanges); } diff --git a/packages/system/src/utils/types.ts b/packages/system/src/utils/types.ts index 4a4634c2..92a1f903 100644 --- a/packages/system/src/utils/types.ts +++ b/packages/system/src/utils/types.ts @@ -1 +1,5 @@ +import { Rect } from '../types'; + export type Optional = Pick, K> & Omit; + +export type ParentExpandChild = { id: string; parentId: string; rect: Rect }; diff --git a/packages/system/src/xyresizer/XYResizer.ts b/packages/system/src/xyresizer/XYResizer.ts index 50a3e4aa..093bdf94 100644 --- a/packages/system/src/xyresizer/XYResizer.ts +++ b/packages/system/src/xyresizer/XYResizer.ts @@ -15,19 +15,13 @@ const initStartValues = { aspectRatio: 1, }; -const initChange = { - x: 0, - y: 0, - width: 0, - height: 0, - isXPosChange: false, - isYPosChange: false, - isWidthChange: false, - isHeightChange: false, +export type XYResizerChange = { + x?: number; + y?: number; + width?: number; + height?: number; }; -export type XYResizerChange = typeof initChange; - export type XYResizerChildChange = { id: string; position: XYPosition; @@ -117,158 +111,157 @@ export function XYResizer({ domNode, nodeId, getStoreItems, onChange }: XYResize const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin } = getStoreItems(); node = nodeLookup.get(nodeId); - if (node) { - const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); + if (!node) { + return; + } - prevValues = { - width: node.measured?.width ?? 0, - height: node.measured?.height ?? 0, - x: node.position.x ?? 0, - y: node.position.y ?? 0, - }; + const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); - startValues = { - ...prevValues, - pointerX: xSnapped, - pointerY: ySnapped, - aspectRatio: prevValues.width / prevValues.height, - }; + prevValues = { + width: node.measured?.width ?? 0, + height: node.measured?.height ?? 0, + x: node.position.x ?? 0, + y: node.position.y ?? 0, + }; - parentNode = undefined; - if (node.extent === 'parent' || node.expandParent) { - parentNode = nodeLookup.get(node.parentId!); - if (parentNode && node.extent === 'parent') { - parentExtent = nodeToParentExtent(parentNode); - } + startValues = { + ...prevValues, + pointerX: xSnapped, + pointerY: ySnapped, + aspectRatio: prevValues.width / prevValues.height, + }; + + parentNode = undefined; + if (node.extent === 'parent' || node.expandParent) { + parentNode = nodeLookup.get(node.parentId!); + if (parentNode && node.extent === 'parent') { + parentExtent = nodeToParentExtent(parentNode); } + } - // Collect all child nodes to correct their relative positions when top/left changes - // Determine largest minimal extent the parent node is allowed to resize to - childNodes = []; - childExtent = undefined; + // Collect all child nodes to correct their relative positions when top/left changes + // Determine largest minimal extent the parent node is allowed to resize to + childNodes = []; + childExtent = undefined; - for (const [childId, child] of nodeLookup) { - if (child.parentId === nodeId) { - childNodes.push({ - id: childId, - position: { ...child.position }, - extent: child.extent, - }); + for (const [childId, child] of nodeLookup) { + if (child.parentId === nodeId) { + childNodes.push({ + id: childId, + position: { ...child.position }, + extent: child.extent, + }); - if (child.extent === 'parent' || child.expandParent) { - const extent = nodeToChildExtent(child, node!, child.origin ?? nodeOrigin); + if (child.extent === 'parent' || child.expandParent) { + const extent = nodeToChildExtent(child, node!, child.origin ?? nodeOrigin); - if (childExtent) { - childExtent = [ - [Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])], - [Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])], - ]; - } else { - childExtent = extent; - } + if (childExtent) { + childExtent = [ + [Math.min(extent[0][0], childExtent[0][0]), Math.min(extent[0][1], childExtent[0][1])], + [Math.max(extent[1][0], childExtent[1][0]), Math.max(extent[1][1], childExtent[1][1])], + ]; + } else { + childExtent = extent; } } } - - onResizeStart?.(event, { ...prevValues }); } + + onResizeStart?.(event, { ...prevValues }); }) .on('drag', (event: ResizeDragEvent) => { const { transform, snapGrid, snapToGrid, nodeOrigin: storeNodeOrigin } = getStoreItems(); const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const childChanges: XYResizerChildChange[] = []; - if (node) { - const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; - const change = { ...initChange }; - const nodeOrigin = node.origin ?? storeNodeOrigin; - - const { width, height, x, y } = getDimensionsAfterResize( - startValues, - controlDirection, - pointerPosition, - boundaries, - keepAspectRatio, - nodeOrigin, - parentExtent, - childExtent - ); - - const isWidthChange = width !== prevWidth; - const isHeightChange = height !== prevHeight; - - const isXPosChange = x !== prevX && isWidthChange; - const isYPosChange = y !== prevY && isHeightChange; - - if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] == 1) { - change.isXPosChange = isXPosChange; - change.isYPosChange = isYPosChange; - change.x = isXPosChange ? x : prevX; - change.y = isYPosChange ? y : prevY; - - prevValues.x = change.x; - prevValues.y = change.y; - - // Fix expandParent when resizing from top/left - if (parentNode && node.expandParent) { - if (change.x < 0) { - prevValues.x = 0; - startValues.x = startValues.x - change.x; - } - - if (change.y < 0) { - prevValues.y = 0; - startValues.y = startValues.y - change.y; - } - } - - if (childNodes.length > 0) { - const xChange = x - prevX; - const yChange = y - prevY; - - for (const childNode of childNodes) { - childNode.position = { - x: childNode.position.x - xChange + nodeOrigin[0] * (width - prevWidth), - y: childNode.position.y - yChange + nodeOrigin[1] * (height - prevHeight), - }; - childChanges.push(childNode); - } - } - } - - if (isWidthChange || isHeightChange) { - change.isWidthChange = isWidthChange; - change.isHeightChange = isHeightChange; - change.width = width; - change.height = height; - prevValues.width = change.width; - prevValues.height = change.height; - } - - if (!change.isXPosChange && !change.isYPosChange && !isWidthChange && !isHeightChange) { - return; - } - - const direction = getResizeDirection({ - width: prevValues.width, - prevWidth, - height: prevValues.height, - prevHeight, - affectsX: controlDirection.affectsX, - affectsY: controlDirection.affectsY, - }); - - const nextValues = { ...prevValues, direction }; - - const callResize = shouldResize?.(event, nextValues); - - if (callResize === false) { - return; - } - - onResize?.(event, nextValues); - onChange(change, childChanges); + if (!node) { + return; } + const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; + const change: XYResizerChange = {}; + const nodeOrigin = node.origin ?? storeNodeOrigin; + + const { width, height, x, y } = getDimensionsAfterResize( + startValues, + controlDirection, + pointerPosition, + boundaries, + keepAspectRatio, + nodeOrigin, + parentExtent, + childExtent + ); + + const isWidthChange = width !== prevWidth; + const isHeightChange = height !== prevHeight; + + const isXPosChange = x !== prevX && isWidthChange; + const isYPosChange = y !== prevY && isHeightChange; + + if (!isXPosChange && !isYPosChange && !isWidthChange && !isHeightChange) { + return; + } + + if (isXPosChange || isYPosChange || nodeOrigin[0] === 1 || nodeOrigin[1] == 1) { + change.x = isXPosChange ? x : prevValues.x; + change.y = isYPosChange ? y : prevValues.y; + + prevValues.x = change.x; + prevValues.y = change.y; + + // Fix expandParent when resizing from top/left + if (parentNode && node.expandParent) { + if (change.x && change.x < 0) { + prevValues.x = 0; + startValues.x = startValues.x - change.x; + } + + if (change.y && change.y < 0) { + prevValues.y = 0; + startValues.y = startValues.y - change.y; + } + } + + if (childNodes.length > 0) { + const xChange = x - prevX; + const yChange = y - prevY; + + for (const childNode of childNodes) { + childNode.position = { + x: childNode.position.x - xChange + nodeOrigin[0] * (width - prevWidth), + y: childNode.position.y - yChange + nodeOrigin[1] * (height - prevHeight), + }; + childChanges.push(childNode); + } + } + } + + if (isWidthChange || isHeightChange) { + change.width = isWidthChange ? width : prevValues.width; + change.height = isHeightChange ? height : prevValues.height; + prevValues.width = change.width; + prevValues.height = change.height; + } + + const direction = getResizeDirection({ + width: prevValues.width, + prevWidth, + height: prevValues.height, + prevHeight, + affectsX: controlDirection.affectsX, + affectsY: controlDirection.affectsY, + }); + + const nextValues = { ...prevValues, direction }; + + const callResize = shouldResize?.(event, nextValues); + + if (callResize === false) { + return; + } + + onResize?.(event, nextValues); + onChange(change, childChanges); }) .on('end', (event: ResizeDragEvent) => { onResizeEnd?.(event, { ...prevValues });