feat(node-resizer): add flag to keep aspect ratio, cleanup example

This commit is contained in:
Christopher Möller
2023-03-07 16:41:15 +01:00
parent 5becbb1ebe
commit 8b0808ec03
12 changed files with 241 additions and 151 deletions
@@ -10,10 +10,21 @@ const controlStyle = {
border: 'none', border: 'none',
}; };
const CustomNode: FC<NodeProps> = ({ id, data }) => { const CustomResizerNode: FC<NodeProps> = ({ data }) => {
return ( return (
<> <>
<NodeResizeControl style={controlStyle}> <NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
style={controlStyle}
>
<ResizeIcon /> <ResizeIcon />
</NodeResizeControl> </NodeResizeControl>
@@ -24,4 +35,4 @@ const CustomNode: FC<NodeProps> = ({ id, data }) => {
); );
}; };
export default memo(CustomNode); export default memo(CustomResizerNode);
@@ -1,19 +0,0 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl color="red" position={Position.Top} />
<NodeResizeControl color="red" position={Position.Bottom} />
<Handle type="target" position={Position.Left} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);
@@ -1,33 +0,0 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizeControl, OnResize, ShouldResize } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const shouldResize: ShouldResize = (event, params) => {
console.log('before resize', params);
if (params.width > 100) {
return false;
}
return true;
};
const onResize: OnResize = (event, params) => {
console.log('resize', params.direction);
};
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl color="red" position={Position.Left} />
<NodeResizeControl color="red" position={Position.Right} shouldResize={shouldResize} onResize={onResize} />
<Handle type="target" position={Position.Top} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(CustomNode);
@@ -0,0 +1,29 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const DefaultResizerNode: FC<NodeProps> = ({ data, selected }) => {
return (
<>
<NodeResizer
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
isVisible={data.isVisible ?? selected}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(DefaultResizerNode);
@@ -0,0 +1,43 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const HorizontalResizerNode: FC<NodeProps> = ({ data }) => {
return (
<>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Left}
/>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Right}
/>
<Handle type="target" position={Position.Top} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Bottom} />
</>
);
};
export default memo(HorizontalResizerNode);
@@ -1,44 +0,0 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, ShouldResize, OnResize, OnResizeEnd, OnResizeStart } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const onResizeStart: OnResizeStart = (_, params) => {
console.log('resize start', params);
};
const onResize: OnResize = (_, params) => {
console.log('resize', params);
};
const onResizeEnd: OnResizeEnd = (_, params) => {
console.log('resize end', params);
};
const shouldResize: ShouldResize = (_, params) => {
console.log('should resize', params);
return true;
};
const CustomNode: FC<NodeProps> = ({ data, selected }) => {
return (
<>
<NodeResizer
minWidth={100}
minHeight={100}
isVisible={selected}
shouldResize={shouldResize}
onResizeStart={onResizeStart}
onResize={onResize}
onResizeEnd={onResizeEnd}
/>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);
@@ -0,0 +1,43 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Top}
/>
<NodeResizeControl
minWidth={data.minWidth ?? undefined}
maxWidth={data.maxWidth ?? undefined}
minHeight={data.minHeight ?? undefined}
maxHeight={data.maxHeight ?? undefined}
shouldResize={data.shouldResize ?? undefined}
onResizeStart={data.onResizeStart ?? undefined}
onResize={data.onResize ?? undefined}
onResizeEnd={data.onResizeEnd ?? undefined}
keepAspectRatio={data.keepAspectRatio ?? undefined}
color="red"
position={Position.Bottom}
/>
<Handle type="target" position={Position.Left} />
<div style={{ padding: 10 }}>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);
@@ -1,73 +1,97 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState, Panel } from 'reactflow'; import ReactFlow, { Controls, addEdge, Connection, useNodesState, useEdgesState, Panel, Node, Edge } from 'reactflow';
import NodeResizerNode from './NodeResizerNode'; import DefaultResizer from './DefaultResizer';
import CustomResizer from './CustomResizer'; import CustomResizer from './CustomResizer';
import CustomResizer2 from './CustomResizer2'; import VerticalResizer from './VerticalResizer';
import CustomResizer3 from './CustomResizer3'; import HorizontalResizer from './HorizontalResizer';
const nodeTypes = { const nodeTypes = {
resizer: NodeResizerNode, defaultResizer: DefaultResizer,
customResizer: CustomResizer, customResizer: CustomResizer,
customResizer2: CustomResizer2, verticalResizer: VerticalResizer,
customResizer3: CustomResizer3, horizontalResizer: HorizontalResizer,
}; };
const initialEdges = [ const nodeStyle = {
{ border: '1px solid #222',
id: 'e1-2', fontSize: 10,
source: '1', backgroundColor: '#ddd',
target: '2', };
},
];
const initialNodes = [ const initialEdges: Edge[] = [];
const initialNodes: Node[] = [
{ {
id: '1', id: '1',
type: 'input', type: 'defaultResizer',
data: { label: 'An input node' }, data: { label: 'default resizer' },
position: { x: 0, y: 0 }, position: { x: 0, y: 0 },
sourcePosition: Position.Right, style: { ...nodeStyle },
}, },
{ {
id: '2', id: '1a',
type: 'resizer', type: 'defaultResizer',
data: { label: 'default resizer' }, data: {
label: 'default resizer with min and max dimensions',
minWidth: 100,
minHeight: 80,
maxWidth: 200,
maxHeight: 200,
},
position: { x: 0, y: 60 },
style: { ...nodeStyle, width: 100, height: 80 },
},
{
id: '1b',
type: 'defaultResizer',
data: { label: 'default resizer with initial size and aspect ratio', keepAspectRatio: true },
position: { x: 250, y: 0 }, position: { x: 250, y: 0 },
style: { style: {
width: 200, width: 174,
height: 150, height: 123,
border: '1px solid #222', ...nodeStyle,
fontSize: 10,
}, },
}, },
{ {
id: '3', id: '2',
type: 'customResizer', type: 'customResizer',
data: { label: 'resize control with child component' }, data: { label: 'custom resize icon' },
position: { x: 0, y: 200 },
style: { width: 100, height: 60, ...nodeStyle },
},
{
id: '3',
type: 'verticalResizer',
data: { label: 'vertical resizer' },
position: { x: 250, y: 200 }, position: { x: 250, y: 200 },
style: { border: '1px solid #222', fontSize: 10, width: 100 }, style: { ...nodeStyle },
},
{
id: '3a',
type: 'verticalResizer',
data: {
label: 'vertical resizer with min/maxHeight and aspect ratio',
minHeight: 50,
maxHeight: 200,
keepAspectRatio: true,
},
position: { x: 400, y: 200 },
style: { ...nodeStyle, height: 50 },
}, },
{ {
id: '4', id: '4',
type: 'customResizer2', type: 'horizontalResizer',
data: { label: 'resize controls' }, data: { label: 'horizontal resizer with aspect ratio', keepAspectRatio: true },
position: { x: 100, y: 150 }, position: { x: 250, y: 300 },
style: { border: '1px solid #222', fontSize: 10 }, style: { ...nodeStyle },
}, },
{ {
id: '5', id: '4a',
type: 'customResizer2', type: 'horizontalResizer',
data: { label: 'min width and height' }, data: { label: 'horizontal resizer with maxWidth', maxWidth: 300 },
position: { x: 250, y: 250 }, position: { x: 250, y: 400 },
style: { border: '1px solid #222', fontSize: 10 }, style: { ...nodeStyle },
},
{
id: '6',
type: 'customResizer3',
data: { label: 'resize controls' },
position: { x: 400, y: 200 },
style: { border: '1px solid #222', fontSize: 10 },
}, },
]; ];
+1 -1
View File
@@ -8,7 +8,7 @@ export { default as SimpleBezierEdge, getSimpleBezierPath } from './components/E
export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge'; export { default as SmoothStepEdge, getSmoothStepPath } from './components/Edges/SmoothStepEdge';
export { default as BaseEdge } from './components/Edges/BaseEdge'; export { default as BaseEdge } from './components/Edges/BaseEdge';
export { internalsSymbol, rectToBox, boxToRect, getBoundsOfRects } from './utils'; export { internalsSymbol, rectToBox, boxToRect, getBoundsOfRects, clamp } from './utils';
export { export {
isNode, isNode,
isEdge, isEdge,
@@ -16,6 +16,7 @@ export default function NodeResizer({
minHeight = 10, minHeight = 10,
maxWidth = Number.MAX_VALUE, maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
shouldResize, shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
@@ -41,6 +42,7 @@ export default function NodeResizer({
maxWidth={maxWidth} maxWidth={maxWidth}
maxHeight={maxHeight} maxHeight={maxHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio}
shouldResize={shouldResize} shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
@@ -59,6 +61,7 @@ export default function NodeResizer({
maxWidth={maxWidth} maxWidth={maxWidth}
maxHeight={maxHeight} maxHeight={maxHeight}
onResizeStart={onResizeStart} onResizeStart={onResizeStart}
keepAspectRatio={keepAspectRatio}
shouldResize={shouldResize} shouldResize={shouldResize}
onResize={onResize} onResize={onResize}
onResizeEnd={onResizeEnd} onResizeEnd={onResizeEnd}
+28 -6
View File
@@ -9,6 +9,7 @@ import {
NodeDimensionChange, NodeDimensionChange,
useNodeId, useNodeId,
NodePositionChange, NodePositionChange,
clamp,
} from '@reactflow/core'; } from '@reactflow/core';
import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types'; import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types';
@@ -20,6 +21,7 @@ const initStartValues = {
...initPrevValues, ...initPrevValues,
pointerX: 0, pointerX: 0,
pointerY: 0, pointerY: 0,
aspectRatio: 1,
}; };
function ResizeControl({ function ResizeControl({
@@ -34,6 +36,7 @@ function ResizeControl({
minHeight = 10, minHeight = 10,
maxWidth = Number.MAX_VALUE, maxWidth = Number.MAX_VALUE,
maxHeight = Number.MAX_VALUE, maxHeight = Number.MAX_VALUE,
keepAspectRatio = false,
shouldResize, shouldResize,
onResizeStart, onResizeStart,
onResize, onResize,
@@ -55,6 +58,12 @@ function ResizeControl({
} }
const selection = select(resizeControlRef.current); const selection = select(resizeControlRef.current);
const enableX = controlPosition.includes('right') || controlPosition.includes('left');
const enableY = controlPosition.includes('bottom') || controlPosition.includes('top');
const invertX = controlPosition.includes('left');
const invertY = controlPosition.includes('top');
const dragHandler = drag<HTMLDivElement, unknown>() const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => { .on('start', (event: ResizeDragEvent) => {
const node = store.getState().nodeInternals.get(id); const node = store.getState().nodeInternals.get(id);
@@ -71,6 +80,7 @@ function ResizeControl({
...prevValues.current, ...prevValues.current,
pointerX: xSnapped, pointerX: xSnapped,
pointerY: ySnapped, pointerY: ySnapped,
aspectRatio: prevValues.current.width / prevValues.current.height,
}; };
onResizeStart?.(event, { ...prevValues.current }); onResizeStart?.(event, { ...prevValues.current });
@@ -79,10 +89,6 @@ function ResizeControl({
const { nodeInternals, triggerNodeChanges } = store.getState(); const { nodeInternals, triggerNodeChanges } = store.getState();
const { xSnapped, ySnapped } = getPointerPosition(event); const { xSnapped, ySnapped } = getPointerPosition(event);
const node = nodeInternals.get(id); const node = nodeInternals.get(id);
const enableX = controlPosition.includes('right') || controlPosition.includes('left');
const enableY = controlPosition.includes('bottom') || controlPosition.includes('top');
const invertX = controlPosition.includes('left');
const invertY = controlPosition.includes('top');
if (node) { if (node) {
const changes: NodeChange[] = []; const changes: NodeChange[] = [];
@@ -93,13 +99,29 @@ function ResizeControl({
height: startHeight, height: startHeight,
x: startNodeX, x: startNodeX,
y: startNodeY, y: startNodeY,
aspectRatio,
} = startValues.current; } = startValues.current;
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current; const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current;
const distX = Math.floor(enableX ? xSnapped - startX : 0); const distX = Math.floor(enableX ? xSnapped - startX : 0);
const distY = Math.floor(enableY ? ySnapped - startY : 0); const distY = Math.floor(enableY ? ySnapped - startY : 0);
const width = Math.min(Math.max(startWidth + (invertX ? -distX : distX), minWidth), maxWidth);
const height = Math.min(Math.max(startHeight + (invertY ? -distY : distY), minHeight), maxHeight); let width = startWidth + (invertX ? -distX : distX);
let height = startHeight + (invertY ? -distY : distY);
width = clamp(width, minWidth, maxWidth);
height = clamp(height, minHeight, maxHeight);
if (keepAspectRatio) {
const nextAspectRatio = width / height;
const isDiagonal = enableX && enableY;
const isHorizontal = enableX && !enableY;
const isVertical = enableY && !enableX;
width = (nextAspectRatio <= aspectRatio && isDiagonal) || isVertical ? height * aspectRatio : width;
height = (nextAspectRatio > aspectRatio && isDiagonal) || isHorizontal ? width / aspectRatio : height;
}
const isWidthChange = width !== prevWidth; const isWidthChange = width !== prevWidth;
const isHeightChange = height !== prevHeight; const isHeightChange = height !== prevHeight;
+12 -1
View File
@@ -31,6 +31,7 @@ export type NodeResizerProps = {
minHeight?: number; minHeight?: number;
maxWidth?: number; maxWidth?: number;
maxHeight?: number; maxHeight?: number;
keepAspectRatio?: boolean;
shouldResize?: ShouldResize; shouldResize?: ShouldResize;
onResizeStart?: OnResizeStart; onResizeStart?: OnResizeStart;
onResize?: OnResize; onResize?: OnResize;
@@ -48,7 +49,17 @@ export enum ResizeControlVariant {
export type ResizeControlProps = Pick< export type ResizeControlProps = Pick<
NodeResizerProps, NodeResizerProps,
'nodeId' | 'color' | 'minWidth' | 'minHeight' | 'maxWidth' | 'maxHeight' | 'shouldResize' | 'onResizeStart' | 'onResize' | 'onResizeEnd' | 'nodeId'
| 'color'
| 'minWidth'
| 'minHeight'
| 'maxWidth'
| 'maxHeight'
| 'keepAspectRatio'
| 'shouldResize'
| 'onResizeStart'
| 'onResize'
| 'onResizeEnd'
> & { > & {
position?: ControlPosition; position?: ControlPosition;
variant?: ResizeControlVariant; variant?: ResizeControlVariant;