From 8e43021050c68460397dc7fc6762c4af0e3c5265 Mon Sep 17 00:00:00 2001 From: moklick Date: Sun, 4 Dec 2022 15:04:07 +0100 Subject: [PATCH] refactor(resizer): export resize control, add line control, styling --- .../examples/NodeResizer/CustomResizer.tsx | 27 +++ .../examples/NodeResizer/CustomResizer2.tsx | 19 ++ .../{CustomNode.tsx => NodeResizerNode.tsx} | 8 +- .../src/examples/NodeResizer/ResizeIcon.tsx | 24 +++ .../src/examples/NodeResizer/index.tsx | 28 ++- packages/node-resizer/package.json | 4 +- packages/node-resizer/src/NodeResizer.tsx | 190 ++---------------- packages/node-resizer/src/ResizeControl.tsx | 141 +++++++++++++ packages/node-resizer/src/index.tsx | 2 + packages/node-resizer/src/style.css | 100 +++++++++ packages/node-resizer/src/types.ts | 32 ++- 11 files changed, 387 insertions(+), 188 deletions(-) create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx create mode 100644 examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx rename examples/vite-app/src/examples/NodeResizer/{CustomNode.tsx => NodeResizerNode.tsx} (72%) create mode 100644 examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx create mode 100644 packages/node-resizer/src/ResizeControl.tsx create mode 100644 packages/node-resizer/src/style.css diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx new file mode 100644 index 00000000..7ad2e3a3 --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx @@ -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 = ({ id, data }) => { + return ( + <> + + + + + +
{data.label}
+ + + ); +}; + +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx new file mode 100644 index 00000000..c25a01b6 --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx @@ -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 = ({ id, data }) => { + return ( + <> + + + +
{data.label}
+ + + ); +}; + +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx similarity index 72% rename from examples/vite-app/src/examples/NodeResizer/CustomNode.tsx rename to examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx index 7a5dda5e..3fab7d5d 100644 --- a/examples/vite-app/src/examples/NodeResizer/CustomNode.tsx +++ b/examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx @@ -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 = ({ id, data }) => { +const CustomNode: FC = ({ id, data }) => { return ( <> +
{data.label}
- - ); }; -export default memo(ColorSelectorNode); +export default memo(CustomNode); diff --git a/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx new file mode 100644 index 00000000..f37b9a5d --- /dev/null +++ b/examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx @@ -0,0 +1,24 @@ +function ResizeIcon() { + return ( + + + + + + + + ); +} + +export default ResizeIcon; diff --git a/examples/vite-app/src/examples/NodeResizer/index.tsx b/examples/vite-app/src/examples/NodeResizer/index.tsx index af0a660c..3f3b982a 100644 --- a/examples/vite-app/src/examples/NodeResizer/index.tsx +++ b/examples/vite-app/src/examples/NodeResizer/index.tsx @@ -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 }, }, ]; diff --git a/packages/node-resizer/package.json b/packages/node-resizer/package.json index 4ef4f1ba..3025ccb7 100644 --- a/packages/node-resizer/package.json +++ b/packages/node-resizer/package.json @@ -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" }, diff --git a/packages/node-resizer/src/NodeResizer.tsx b/packages/node-resizer/src/NodeResizer.tsx index 0660adf6..9074ea2f 100644 --- a/packages/node-resizer/src/NodeResizer.tsx +++ b/packages/node-resizer/src/NodeResizer.tsx @@ -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; - -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(null); - const initialDimensionsRef = useRef({ - width: 0, - height: 0, - x: 0, - y: 0, - nodeX: 0, - nodeY: 0, - }); - const nodeElementRef = useRef(null); - const getPointerPosition = useGetPointerPosition(); - - useEffect(() => { - if (!resizeHandleRef.current) { - return; - } - - const selection = select(resizeHandleRef.current); - const dragHandler = drag() - .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 ( -
- ); -} - -export default function NodeResizer({ nodeId }: NodeResizerProps) { + handleClassName, + handleStyle, + lineClassName, + lineStyle, +}: NodeResizerProps) { return ( <> - - - - - - - - + + + + + + + + + + ); } diff --git a/packages/node-resizer/src/ResizeControl.tsx b/packages/node-resizer/src/ResizeControl.tsx new file mode 100644 index 00000000..b57f021a --- /dev/null +++ b/packages/node-resizer/src/ResizeControl.tsx @@ -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(null); + const initialDimensionsRef = useRef({ + width: 0, + height: 0, + x: 0, + y: 0, + nodeX: 0, + nodeY: 0, + }); + const nodeElementRef = useRef(null); + const getPointerPosition = useGetPointerPosition(); + + useEffect(() => { + if (!resizeHandleRef.current) { + return; + } + + const selection = select(resizeHandleRef.current); + const dragHandler = drag() + .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 ( +
+ {children} +
+ ); +} + +export function ResizeControlLine(props: ResizeControlLineProps) { + return ; +} + +export default ResizeControl; diff --git a/packages/node-resizer/src/index.tsx b/packages/node-resizer/src/index.tsx index ea5bf55b..c3cd1d5d 100644 --- a/packages/node-resizer/src/index.tsx +++ b/packages/node-resizer/src/index.tsx @@ -1,2 +1,4 @@ export { default as NodeResizer } from './NodeResizer'; +export { default as NodeResizeControl } from './ResizeControl'; + export * from './types'; diff --git a/packages/node-resizer/src/style.css b/packages/node-resizer/src/style.css new file mode 100644 index 00000000..71077758 --- /dev/null +++ b/packages/node-resizer/src/style.css @@ -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%; +} diff --git a/packages/node-resizer/src/types.ts b/packages/node-resizer/src/types.ts index 1e6576b6..7eb6b32a 100644 --- a/packages/node-resizer/src/types.ts +++ b/packages/node-resizer/src/types.ts @@ -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 & { +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;