Files
xyflow/packages/react/src/additional-components/NodeResizer/NodeResizeControl.tsx
2025-04-17 12:54:47 +02:00

218 lines
6.2 KiB
TypeScript

import { useRef, useEffect, memo } from 'react';
import cc from 'classcat';
import {
XYResizer,
ResizeControlVariant,
type XYResizerInstance,
type XYResizerChange,
type XYResizerChildChange,
type NodeChange,
type NodeDimensionChange,
type NodePositionChange,
handleExpandParent,
evaluateAbsolutePosition,
ParentExpandChild,
XYPosition,
} from '@xyflow/system';
import { useStoreApi } from '../../hooks/useStore';
import { useNodeId } from '../../contexts/NodeIdContext';
import type { ResizeControlProps, ResizeControlLineProps } from './types';
function ResizeControl({
nodeId,
position,
variant = ResizeControlVariant.Handle,
className,
style = {},
children,
color,
minWidth = 10,
minHeight = 10,
maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
resizeDirection,
shouldResize,
onResizeStart,
onResize,
onResizeEnd,
}: ResizeControlProps) {
const contextNodeId = useNodeId();
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi();
const resizeControlRef = useRef<HTMLDivElement>(null);
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
const controlPosition = position ?? defaultPosition;
const resizer = useRef<XYResizerInstance | null>(null);
useEffect(() => {
if (!resizeControlRef.current || !id) {
return;
}
if (!resizer.current) {
resizer.current = XYResizer({
domNode: resizeControlRef.current,
nodeId: id,
getStoreItems: () => {
const { nodeLookup, transform, snapGrid, snapToGrid, nodeOrigin, domNode } = store.getState();
return {
nodeLookup,
transform,
snapGrid,
snapToGrid,
nodeOrigin,
paneDomNode: domNode,
};
},
onChange: (change: XYResizerChange, childChanges: XYResizerChildChange[]) => {
const { triggerNodeChanges, nodeLookup, parentLookup, nodeOrigin } = store.getState();
const changes: NodeChange[] = [];
const nextPosition = { x: change.x, y: change.y };
const node = nodeLookup.get(id);
if (node && node.expandParent && node.parentId) {
const origin = node.origin ?? nodeOrigin;
const width = change.width ?? node.measured.width ?? 0;
const height = change.height ?? node.measured.height ?? 0;
const child: ParentExpandChild = {
id: node.id,
parentId: node.parentId,
rect: {
width,
height,
...evaluateAbsolutePosition(
{
x: change.x ?? node.position.x,
y: change.y ?? node.position.y,
},
{ width, height },
node.parentId,
nodeLookup,
origin
),
},
};
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 when node origin is 0,0 and to width, height if it's 1,1
*/
nextPosition.x = change.x ? Math.max(origin[0] * width, change.x) : undefined;
nextPosition.y = change.y ? Math.max(origin[1] * height, 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.width !== undefined && change.height !== undefined) {
const dimensionChange: NodeDimensionChange = {
id,
type: 'dimensions',
resizing: true,
setAttributes: true,
dimensions: {
width: change.width,
height: change.height,
},
};
changes.push(dimensionChange);
}
for (const childChange of childChanges) {
const positionChange: NodePositionChange = {
...childChange,
type: 'position',
};
changes.push(positionChange);
}
triggerNodeChanges(changes);
},
onEnd: ({ width, height }) => {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: false,
dimensions: {
width,
height,
},
};
store.getState().triggerNodeChanges([dimensionChange]);
},
});
}
resizer.current.update({
controlPosition,
boundaries: {
minWidth,
minHeight,
maxWidth,
maxHeight,
},
keepAspectRatio,
resizeDirection,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
});
return () => {
resizer.current?.destroy();
};
}, [
controlPosition,
minWidth,
minHeight,
maxWidth,
maxHeight,
keepAspectRatio,
onResizeStart,
onResize,
onResizeEnd,
shouldResize,
]);
const positionClassNames = controlPosition.split('-');
const colorStyleProp = variant === ResizeControlVariant.Line ? 'borderColor' : 'backgroundColor';
const controlStyle = color ? { ...style, [colorStyleProp]: color } : style;
return (
<div
className={cc(['react-flow__resize-control', 'nodrag', ...positionClassNames, variant, className])}
ref={resizeControlRef}
style={controlStyle}
>
{children}
</div>
);
}
export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant={ResizeControlVariant.Line} />;
}
/**
* To create your own resizing UI, you can use the `NodeResizeControl` component where you can pass children (such as icons).
* @public
*
*/
export const NodeResizeControl = memo(ResizeControl);