refactor(resizer): export resize control, add line control, styling

This commit is contained in:
moklick
2022-12-04 15:04:07 +01:00
parent afce7a2fee
commit 8e43021050
11 changed files with 387 additions and 188 deletions

View File

@@ -0,0 +1,27 @@
import { memo, FC, CSSProperties } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
import ResizeIcon from './ResizeIcon';
const controlStyle = {
background: 'transparent',
border: 'none',
};
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl nodeId={id} style={controlStyle}>
<ResizeIcon />
</NodeResizeControl>
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);

View File

@@ -0,0 +1,19 @@
import { memo, FC } from 'react';
import { Handle, Position, NodeProps } from 'reactflow';
import { NodeResizer, NodeResizeControl } from '@reactflow/node-resizer';
import '@reactflow/node-resizer/dist/style.css';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizeControl nodeId={id} position="top" />
<NodeResizeControl nodeId={id} position="bottom" />
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};
export default memo(CustomNode);

View File

@@ -2,17 +2,17 @@ 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 ColorSelectorNode: FC<NodeProps> = ({ id, data }) => {
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeResizer nodeId={id} />
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
<NodeResizer nodeId={id} />
</>
);
};
export default memo(ColorSelectorNode);
export default memo(CustomNode);

View File

@@ -0,0 +1,24 @@
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="8"
height="8"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
style={{ position: 'absolute', right: 2, bottom: 2 }}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="16 20 20 20 20 16" />
<line x1="14" y1="14" x2="20" y2="20" />
<polyline points="8 4 4 4 4 8" />
<line x1="4" y1="4" x2="10" y2="10" />
</svg>
);
}
export default ResizeIcon;

View File

@@ -1,10 +1,14 @@
import { useCallback } from 'react';
import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState } from 'reactflow';
import CustomNode from './CustomNode';
import NodeResizerNode from './NodeResizerNode';
import CustomResizer from './CustomResizer';
import CustomResizer2 from './CustomResizer2';
const nodeTypes = {
custom: CustomNode,
resizer: NodeResizerNode,
customResizer: CustomResizer,
customResizer2: CustomResizer2,
};
const initialEdges = [
@@ -25,10 +29,24 @@ const initialNodes = [
},
{
id: '2',
type: 'custom',
data: { label: 'resize me!' },
type: 'resizer',
data: { label: 'default resizer' },
position: { x: 250, y: 0 },
style: { padding: 10, border: '1px solid #222' },
style: { padding: 10, border: '1px solid #222', fontSize: 10 },
},
{
id: '3',
type: 'customResizer',
data: { label: 'resize control with child component' },
position: { x: 250, y: 150 },
style: { padding: 10, border: '1px solid #222', fontSize: 10, width: 100 },
},
{
id: '4',
type: 'customResizer2',
data: { label: 'resize controls' },
position: { x: 100, y: 150 },
style: { padding: 10, border: '1px solid #222', fontSize: 10 },
},
];

View File

@@ -31,7 +31,9 @@
},
"scripts": {
"dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch",
"build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production",
"build": "rollup --config node:@reactflow/rollup-config --environment NODE_ENV:production && npm run css",
"css": "postcss src/*.css --config ../../tooling/postcss-config/postcss.config.js --dir dist",
"css-watch": "pnpm css --watch",
"lint": "eslint --ext .js,.jsx,.ts,.tsx src",
"typecheck": "tsc --noEmit"
},

View File

@@ -1,179 +1,25 @@
import { useRef, useEffect } from 'react';
import cc from 'classcat';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import {
useStoreApi,
useGetPointerPosition,
NodeChange,
NodePositionChange,
NodeDimensionChange,
} from '@reactflow/core';
import type { Dimensions, Node, XYPosition } from '@reactflow/core';
import type { D3DragEvent, SubjectPosition } from 'd3';
import ResizeControl from './ResizeControl';
import type { NodeResizerProps } from './types';
type NodeResizerProps = {
nodeId: string;
};
type ResizeHandleProps = {
nodeId: string;
invertX?: boolean;
invertY?: boolean;
enableX?: boolean;
enableY?: boolean;
top?: number | string;
left?: number | string;
className?: string;
};
type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
function ResizeHandle({
top = 0,
left = 0,
invertX = false,
invertY = false,
enableX = false,
enableY = false,
export default function NodeResizer({
nodeId,
className,
}: ResizeHandleProps) {
const store = useStoreApi();
const resizeHandleRef = useRef<HTMLDivElement>(null);
const initialDimensionsRef = useRef<Dimensions & XYPosition & { nodeX: number; nodeY: number }>({
width: 0,
height: 0,
x: 0,
y: 0,
nodeX: 0,
nodeY: 0,
});
const nodeElementRef = useRef<HTMLDivElement | null>(null);
const getPointerPosition = useGetPointerPosition();
useEffect(() => {
if (!resizeHandleRef.current) {
return;
}
const selection = select(resizeHandleRef.current);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const node = store.getState().nodeInternals.get(nodeId);
const pointerPos = getPointerPosition(event);
initialDimensionsRef.current = {
width: node?.width ?? 0,
height: node?.height ?? 0,
nodeX: node?.position.x ?? 0,
nodeY: node?.position.y ?? 0,
x: pointerPos.xSnapped,
y: pointerPos.ySnapped,
};
nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement;
})
.on('drag', (event: ResizeDragEvent) => {
const { updateNodePositions, nodeInternals, onNodesChange } = store.getState();
const pointerPos = getPointerPosition(event);
const nodeEl = nodeElementRef.current;
const node = nodeInternals.get(nodeId);
if (nodeEl && node) {
const changes: NodeChange[] = [];
const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0;
const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0;
const width = initialDimensionsRef.current.width + (invertX ? -distX : distX);
const height = initialDimensionsRef.current.height + (invertY ? -distY : distY);
if (invertX || invertY) {
const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX;
const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY;
if (x !== node.position.x || y !== node.position.y) {
const positionChanges: NodePositionChange[] | null = updateNodePositions(
[
{
id: nodeId,
position: { x, y },
} as Node,
],
true,
false,
false
);
if (positionChanges?.length) {
changes.push(positionChanges[0]);
}
}
}
if (width !== node.width || height !== node.height) {
const dimensionChange: NodeDimensionChange = {
id: nodeId,
type: 'dimensions',
updateStyle: true,
dimensions: {
width: width !== node.width ? width : node.width,
height: height !== node.height ? height : node.height,
},
};
changes.push(dimensionChange);
}
if (changes.length) {
onNodesChange?.(changes);
}
}
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}, [nodeId]);
return (
<div
className={cc([className, 'nodrag'])}
ref={resizeHandleRef}
style={{
position: 'absolute',
left,
top,
width: 10,
height: 10,
background: 'red',
opacity: 0.7,
borderRadius: '50%',
transform: 'translate(-50%, -50%)',
}}
/>
);
}
export default function NodeResizer({ nodeId }: NodeResizerProps) {
handleClassName,
handleStyle,
lineClassName,
lineStyle,
}: NodeResizerProps) {
return (
<>
<ResizeHandle
className="react-flow__node-resizer"
nodeId={nodeId}
top={0}
left={0}
enableX
enableY
invertX
invertY
/>
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top="50%" left={0} enableX invertX />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top="100%" left={0} enableX enableY />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top={0} left="50%" enableY invertY />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top={0} left="100%" enableX enableY invertY />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top="50%" left="100%" enableX />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top="100%" left="100%" enableX enableY />
<ResizeHandle className="react-flow__node-resizer" nodeId={nodeId} top="100%" left="50%" enableY />
<ResizeControl className={handleClassName} style={handleStyle} nodeId={nodeId} position="top-left" />
<ResizeControl className={lineClassName} variant="line" style={lineStyle} nodeId={nodeId} position="top" />
<ResizeControl className={handleClassName} style={handleStyle} nodeId={nodeId} position="top-right" />
<ResizeControl className={lineClassName} variant="line" style={lineStyle} nodeId={nodeId} position="left" />
<ResizeControl className={handleClassName} style={handleStyle} nodeId={nodeId} position="bottom-left" />
<ResizeControl className={lineClassName} variant="line" style={lineStyle} nodeId={nodeId} position="right" />
<ResizeControl className={handleClassName} style={handleStyle} nodeId={nodeId} position="bottom-right" />
<ResizeControl className={lineClassName} variant="line" position="bottom" style={lineStyle} nodeId={nodeId} />
</>
);
}

View File

@@ -0,0 +1,141 @@
import { useRef, useEffect } from 'react';
import cc from 'classcat';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import {
useStoreApi,
useGetPointerPosition,
NodeChange,
NodePositionChange,
NodeDimensionChange,
} from '@reactflow/core';
import type { Dimensions, Node, XYPosition } from '@reactflow/core';
import type { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps } from './types';
function ResizeControl({
nodeId,
position = 'bottom-right',
variant = 'handle',
className,
style = {},
children,
}: ResizeControlProps) {
const store = useStoreApi();
const resizeHandleRef = useRef<HTMLDivElement>(null);
const initialDimensionsRef = useRef<Dimensions & XYPosition & { nodeX: number; nodeY: number }>({
width: 0,
height: 0,
x: 0,
y: 0,
nodeX: 0,
nodeY: 0,
});
const nodeElementRef = useRef<HTMLDivElement | null>(null);
const getPointerPosition = useGetPointerPosition();
useEffect(() => {
if (!resizeHandleRef.current) {
return;
}
const selection = select(resizeHandleRef.current);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const node = store.getState().nodeInternals.get(nodeId);
const pointerPos = getPointerPosition(event);
initialDimensionsRef.current = {
width: node?.width ?? 0,
height: node?.height ?? 0,
nodeX: node?.position.x ?? 0,
nodeY: node?.position.y ?? 0,
x: pointerPos.xSnapped,
y: pointerPos.ySnapped,
};
nodeElementRef.current = document.querySelector(`.react-flow__node[data-id="${nodeId}"]`) as HTMLDivElement;
})
.on('drag', (event: ResizeDragEvent) => {
const { updateNodePositions, nodeInternals, onNodesChange } = store.getState();
const pointerPos = getPointerPosition(event);
const nodeEl = nodeElementRef.current;
const node = nodeInternals.get(nodeId);
const enableX = position.includes('right') || position.includes('left');
const enableY = position.includes('bottom') || position.includes('top');
const invertX = position.includes('left');
const invertY = position.includes('top');
if (nodeEl && node) {
const changes: NodeChange[] = [];
const distX = enableX ? pointerPos.xSnapped - initialDimensionsRef.current.x : 0;
const distY = enableY ? pointerPos.ySnapped - initialDimensionsRef.current.y : 0;
const width = initialDimensionsRef.current.width + (invertX ? -distX : distX);
const height = initialDimensionsRef.current.height + (invertY ? -distY : distY);
if (invertX || invertY) {
const x = invertX ? initialDimensionsRef.current.nodeX + distX : initialDimensionsRef.current.nodeX;
const y = invertY ? initialDimensionsRef.current.nodeY + distY : initialDimensionsRef.current.nodeY;
if (x !== node.position.x || y !== node.position.y) {
const positionChanges: NodePositionChange[] | null = updateNodePositions(
[
{
id: nodeId,
position: { x, y },
} as Node,
],
true,
false,
false
);
if (positionChanges?.length) {
changes.push(positionChanges[0]);
}
}
}
if (width !== node.width || height !== node.height) {
const dimensionChange: NodeDimensionChange = {
id: nodeId,
type: 'dimensions',
updateStyle: true,
dimensions: {
width: width !== node.width ? width : node.width,
height: height !== node.height ? height : node.height,
},
};
changes.push(dimensionChange);
}
if (changes.length) {
onNodesChange?.(changes);
}
}
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}, [nodeId, position, getPointerPosition]);
const positionClassNames = position.split('-');
return (
<div
className={cc([className, ...positionClassNames, variant, 'react-flow__resize-control', 'nodrag'])}
ref={resizeHandleRef}
style={style}
>
{children}
</div>
);
}
export function ResizeControlLine(props: ResizeControlLineProps) {
return <ResizeControl {...props} variant="line" />;
}
export default ResizeControl;

View File

@@ -1,2 +1,4 @@
export { default as NodeResizer } from './NodeResizer';
export { default as NodeResizeControl } from './ResizeControl';
export * from './types';

View File

@@ -0,0 +1,100 @@
.react-flow__resize-control {
position: absolute;
background-color: rgba(195, 195, 195, 1);
border: 1px solid white;
width: 4px;
height: 4px;
}
.react-flow__resize-control.left,
.react-flow__resize-control.right {
cursor: ew-resize;
}
.react-flow__resize-control.top,
.react-flow__resize-control.bottom {
cursor: ns-resize;
}
.react-flow__resize-control.top.left,
.react-flow__resize-control.bottom.right {
cursor: nwse-resize;
}
.react-flow__resize-control.bottom.left,
.react-flow__resize-control.top.right {
cursor: nesw-resize;
}
/* handle styles */
.react-flow__resize-control.handle {
transform: translate(-50%, -50%);
}
.react-flow__resize-control.handle.left {
left: 0;
top: 50%;
}
.react-flow__resize-control.handle.right {
left: 100%;
top: 50%;
}
.react-flow__resize-control.handle.top {
left: 50%;
top: 0;
}
.react-flow__resize-control.handle.bottom {
left: 50%;
top: 100%;
}
.react-flow__resize-control.handle.top.left {
left: 0;
}
.react-flow__resize-control.handle.bottom.left {
left: 0;
}
.react-flow__resize-control.handle.top.right {
left: 100%;
}
.react-flow__resize-control.handle.bottom.right {
left: 100%;
}
/* line styles */
.react-flow__resize-control.line {
border: none;
background: none;
}
.react-flow__resize-control.line.left,
.react-flow__resize-control.line.right {
width: 1px;
transform: translate(-50%, 0);
top: 0;
height: 100%;
}
.react-flow__resize-control.line.left {
left: 0;
border-left: 1px solid rgba(195, 195, 195, 1);
}
.react-flow__resize-control.line.right {
left: 100%;
border-right: 1px solid rgba(195, 195, 195, 1);
}
.react-flow__resize-control.line.top,
.react-flow__resize-control.line.bottom {
height: 1px;
transform: translate(0, -50%);
left: 0;
width: 100%;
}
.react-flow__resize-control.line.top {
top: 0;
border-top: 1px solid rgba(195, 195, 195, 1);
}
.react-flow__resize-control.line.bottom {
border-bottom: 1px solid rgba(195, 195, 195, 1);
top: 100%;
}

View File

@@ -1,9 +1,29 @@
import { Position } from '@reactflow/core';
import type { HTMLAttributes } from 'react';
import type { CSSProperties, ReactNode } from 'react';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
export type NodeResizerProps = {
nodeId: string;
isVisible?: boolean;
position?: Position;
offset?: number;
handleClassName?: string;
handleStyle?: CSSProperties;
lineClassName?: string;
lineStyle?: CSSProperties;
};
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export type ResizeControlProps = {
nodeId: string;
position: ControlPosition;
variant?: 'line' | 'handle';
className?: string;
style?: CSSProperties;
children?: ReactNode;
};
export type ResizeControlLineProps = ResizeControlProps & {
position: ControlLinePosition;
};
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;