Skip to content

Commit

Permalink
Merge pull request #2626 from wbkd/feat/resize-node
Browse files Browse the repository at this point in the history
Feat: Add NodeResizer package
  • Loading branch information
moklick committed Dec 6, 2022
2 parents 449e468 + d29c401 commit e0fc4ae
Show file tree
Hide file tree
Showing 36 changed files with 858 additions and 129 deletions.
5 changes: 5 additions & 0 deletions .changeset/nasty-suns-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactflow/node-toolbar': patch
---

Get nodeId from React Flow context if it is not passed explicitly as prop
5 changes: 5 additions & 0 deletions .changeset/smooth-swans-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactflow/core': patch
---

Export the useNodeId hook, refactor how changes are applied and create a helper function
5 changes: 5 additions & 0 deletions .changeset/twelve-icons-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@reactflow/node-resizer': major
---

Add a node resizer component that can be used to resize a custom node
1 change: 1 addition & 0 deletions examples/vite-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"test-e2e": "start-server-and-test 'pnpm serve' http-get://localhost:3000 'pnpm test-e2e-cypress'"
},
"dependencies": {
"@reactflow/node-resizer": "workspace:^0.0.0",
"classcat": "^5.0.3",
"dagre": "^0.8.5",
"localforage": "^1.10.0",
Expand Down
6 changes: 6 additions & 0 deletions examples/vite-app/src/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Intersection from '../examples/Intersection';
import Layouting from '../examples/Layouting';
import MultiFlows from '../examples/MultiFlows';
import NestedNodes from '../examples/NestedNodes';
import NodeResizer from '../examples/NodeResizer';
import NodeTypeChange from '../examples/NodeTypeChange';
import NodeTypesObjectChange from '../examples/NodeTypesObjectChange';
import Overview from '../examples/Overview';
Expand Down Expand Up @@ -174,6 +175,11 @@ const routes: IRoute[] = [
path: '/node-toolbar',
component: NodeToolbar,
},
{
name: 'NodeResizer',
path: '/node-resizer',
component: NodeResizer,
},
{
name: 'Overview',
path: '/overview',
Expand Down
2 changes: 1 addition & 1 deletion examples/vite-app/src/examples/Basic/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const BasicFlow = () => {
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
nodeOrigin={nodeOrigin}
// nodeOrigin={nodeOrigin}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />
Expand Down
27 changes: 27 additions & 0 deletions examples/vite-app/src/examples/NodeResizer/CustomResizer.tsx
Original file line number Diff line number Diff line change
@@ -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 style={controlStyle}>
<ResizeIcon />
</NodeResizeControl>

<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};

export default memo(CustomNode);
19 changes: 19 additions & 0 deletions examples/vite-app/src/examples/NodeResizer/CustomResizer2.tsx
Original file line number Diff line number Diff line change
@@ -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 color="red" position="top" />
<NodeResizeControl color="red" 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);
18 changes: 18 additions & 0 deletions examples/vite-app/src/examples/NodeResizer/NodeResizerNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
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 CustomNode: FC<NodeProps> = ({ id, data, selected }) => {
return (
<>
<NodeResizer isVisible={selected} />
<Handle type="target" position={Position.Left} />
<div>{data.label}</div>
<Handle type="source" position={Position.Right} />
</>
);
};

export default memo(CustomNode);
24 changes: 24 additions & 0 deletions examples/vite-app/src/examples/NodeResizer/ResizeIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
function ResizeIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="8"
height="8"
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="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;
97 changes: 97 additions & 0 deletions examples/vite-app/src/examples/NodeResizer/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { CSSProperties, useCallback, useState } from 'react';
import ReactFlow, { Controls, addEdge, Position, Connection, useNodesState, useEdgesState, Panel } from 'reactflow';

import NodeResizerNode from './NodeResizerNode';
import CustomResizer from './CustomResizer';
import CustomResizer2 from './CustomResizer2';

const nodeTypes = {
resizer: NodeResizerNode,
customResizer: CustomResizer,
customResizer2: CustomResizer2,
};

const initialEdges = [
{
id: 'e1-2',
source: '1',
target: '2',
},
];

const initialNodes = [
{
id: '1',
type: 'input',
data: { label: 'An input node' },
position: { x: 0, y: 0 },
sourcePosition: Position.Right,
},
{
id: '2',
type: 'resizer',
data: { label: 'default resizer' },
position: { x: 250, y: 0 },
style: {
width: 200,
height: 150,
border: '1px solid #222',
fontSize: 10,
},
},
{
id: '3',
type: 'customResizer',
data: { label: 'resize control with child component' },
position: { x: 250, y: 150 },
style: { border: '1px solid #222', fontSize: 10, width: 100 },
parentNode: '2',
},
{
id: '4',
type: 'customResizer2',
data: { label: 'resize controls' },
position: { x: 100, y: 150 },
style: { border: '1px solid #222', fontSize: 10 },
parentNode: '2',
},
{
id: '5',
type: 'customResizer2',
data: { label: 'min width and height' },
position: { x: 100, y: 150 },
style: { border: '1px solid #222', fontSize: 10 },
},
];

const CustomNodeFlow = () => {
const [snapToGrid, setSnapToGrid] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

const onConnect = useCallback(
(connection: Connection) => setEdges((eds) => addEdge({ ...connection }, eds)),
[setEdges]
);

return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
nodeTypes={nodeTypes}
minZoom={-5}
maxZoom={5}
snapToGrid={snapToGrid}
>
<Controls />
<Panel position="bottom-right">
<button onClick={() => setSnapToGrid(!snapToGrid)}>snapToGrid: {snapToGrid ? 'on' : 'off'}</button>
</Panel>
</ReactFlow>
);
};

export default CustomNodeFlow;
2 changes: 1 addition & 1 deletion examples/vite-app/src/examples/NodeToolbar/CustomNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Handle, Position, NodeProps, NodeToolbar } from 'reactflow';
const CustomNode: FC<NodeProps> = ({ id, data }) => {
return (
<>
<NodeToolbar nodeId={id} isVisible={data.toolbarVisible} position={data.toolbarPosition}>
<NodeToolbar isVisible={data.toolbarVisible} position={data.toolbarPosition}>
<button>delete</button>
<button>copy</button>
<button>expand</button>
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/components/Handle/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { memo, useContext, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react';
import { memo, HTMLAttributes, forwardRef, MouseEvent as ReactMouseEvent } from 'react';
import cc from 'classcat';
import shallow from 'zustand/shallow';

import { useStore, useStoreApi } from '../../hooks/useStore';
import NodeIdContext from '../../contexts/NodeIdContext';
import { useNodeId } from '../../contexts/NodeIdContext';
import { checkElementBelowIsValid, handleMouseDown } from './handler';
import { getHostForElement } from '../../utils';
import { addEdge } from '../../utils/graph';
Expand Down Expand Up @@ -37,7 +37,9 @@ const Handle = forwardRef<HTMLDivElement, HandleComponentProps>(
ref
) => {
const store = useStoreApi();
const nodeId = useContext(NodeIdContext) as string;

// @fixme: remove type assertion and handle nodeId === null
const nodeId = useNodeId() as string;
const { connectionStartHandle, connectOnClick, noPanClassName } = useStore(selector, shallow);

const handleId = id || null;
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/container/NodeRenderer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}));

updateNodeDimensions(updates);
});

Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/contexts/NodeIdContext.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { createContext } from 'react';
import { createContext, useContext } from 'react';

export const NodeIdContext = createContext<string | null>(null);
export const Provider = NodeIdContext.Provider;
export const Consumer = NodeIdContext.Consumer;

export const useNodeId = (): string | null => {
const nodeId = useContext(NodeIdContext);
return nodeId;
};

export default NodeIdContext;
26 changes: 4 additions & 22 deletions packages/core/src/hooks/useDrag/index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { useEffect, useRef, useState, useCallback } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { RefObject, MouseEvent } from 'react';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import type { D3DragEvent, SubjectPosition } from 'd3';

import { useStoreApi } from '../../hooks/useStore';
import { getDragItems, getEventHandlerParams, hasSelector, calcNextPosition } from './utils';
import { handleNodeClick } from '../../components/Nodes/utils';
import type { NodeDragItem, Node, SelectionDragHandler } from '../../types';
import useGetPointerPosition from '../useGetPointerPosition';
import type { NodeDragItem, Node, SelectionDragHandler, UseDragEvent } from '../../types';

export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;
export type UseDragData = { dx: number; dy: number };

type UseDragParams = {
Expand Down Expand Up @@ -40,24 +39,7 @@ function useDrag({
const dragItems = useRef<NodeDragItem[]>();
const lastPos = useRef<{ x: number | null; y: number | null }>({ x: null, y: null });

// returns the pointer position projected to the RF coordinate system
const getPointerPosition = useCallback(({ sourceEvent }: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;

const pointerPos = {
x: (x - transform[0]) / transform[2],
y: (y - transform[1]) / transform[2],
};

// we need the snapped position in order to be able to skip unnecessary drag events
return {
xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x,
ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y,
...pointerPos,
};
}, []);
const getPointerPosition = useGetPointerPosition();

useEffect(() => {
if (nodeRef?.current) {
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/hooks/useGetPointerPosition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { useCallback } from 'react';

import { useStoreApi } from './useStore';
import type { UseDragEvent } from '../types';

function useGetPointerPosition() {
const store = useStoreApi();

// returns the pointer position projected to the RF coordinate system
const getPointerPosition = useCallback(({ sourceEvent }: UseDragEvent) => {
const { transform, snapGrid, snapToGrid } = store.getState();
const x = sourceEvent.touches ? sourceEvent.touches[0].clientX : sourceEvent.clientX;
const y = sourceEvent.touches ? sourceEvent.touches[0].clientY : sourceEvent.clientY;

const pointerPos = {
x: (x - transform[0]) / transform[2],
y: (y - transform[1]) / transform[2],
};

// we need the snapped position in order to be able to skip unnecessary drag events
return {
xSnapped: snapToGrid ? snapGrid[0] * Math.round(pointerPos.x / snapGrid[0]) : pointerPos.x,
ySnapped: snapToGrid ? snapGrid[1] * Math.round(pointerPos.y / snapGrid[1]) : pointerPos.y,
...pointerPos,
};
}, []);

return getPointerPosition;
}

export default useGetPointerPosition;
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,7 @@ export { useStore, useStoreApi } from './hooks/useStore';
export { default as useOnViewportChange } from './hooks/useOnViewportChange';
export { default as useOnSelectionChange } from './hooks/useOnSelectionChange';
export { default as useNodesInitialized } from './hooks/useNodesInitialized';
export { default as useGetPointerPosition } from './hooks/useGetPointerPosition';
export { useNodeId } from './contexts/NodeIdContext';

export * from './types';
Loading

0 comments on commit e0fc4ae

Please sign in to comment.