Merge pull request #2626 from wbkd/feat/resize-node

Feat: Add NodeResizer package
This commit is contained in:
Moritz Klack
2022-12-06 18:17:27 +01:00
committed by GitHub
36 changed files with 859 additions and 130 deletions

View File

@@ -0,0 +1,5 @@
---
'@reactflow/node-toolbar': patch
---
Get nodeId from React Flow context if it is not passed explicitly as prop

View File

@@ -0,0 +1,5 @@
---
'@reactflow/core': patch
---
Export the useNodeId hook, refactor how changes are applied and create a helper function

View File

@@ -0,0 +1,5 @@
---
'@reactflow/node-resizer': major
---
Add a node resizer component that can be used to resize a custom node

View File

@@ -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",

View File

@@ -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';
@@ -174,6 +175,11 @@ const routes: IRoute[] = [
path: '/node-toolbar',
component: NodeToolbar,
},
{
name: 'NodeResizer',
path: '/node-resizer',
component: NodeResizer,
},
{
name: 'Overview',
path: '/overview',

View File

@@ -94,7 +94,7 @@ const BasicFlow = () => {
fitView
defaultEdgeOptions={defaultEdgeOptions}
selectNodesOnDrag={false}
nodeOrigin={nodeOrigin}
// nodeOrigin={nodeOrigin}
>
<Background variant={BackgroundVariant.Dots} />
<MiniMap />

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 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 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);

View File

@@ -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);

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"
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;

View File

@@ -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;

View File

@@ -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>

View File

@@ -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';
@@ -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;

View File

@@ -57,6 +57,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
nodeElement: entry.target as HTMLDivElement,
forceUpdate: true,
}));
updateNodeDimensions(updates);
});

View File

@@ -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;

View File

@@ -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 = {
@@ -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) {

View File

@@ -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;

View File

@@ -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';

View File

@@ -17,6 +17,7 @@ import type {
NodePositionChange,
NodeDragItem,
UnselectNodesAndEdgesParams,
NodeChange,
} from '../types';
const createRFStore = () =>
@@ -103,35 +104,40 @@ const createRFStore = () =>
}
},
updateNodePositions: (nodeDragItems: NodeDragItem[] | Node[], positionChanged = true, dragging = false) => {
const { triggerNodeChanges } = get();
const changes = nodeDragItems.map((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging,
};
if (positionChanged) {
change.positionAbsolute = node.positionAbsolute;
change.position = node.position;
}
return change;
});
triggerNodeChanges(changes);
},
triggerNodeChanges: (changes: NodeChange[]) => {
const { onNodesChange, nodeInternals, hasDefaultNodes, nodeOrigin } = get();
if (hasDefaultNodes || onNodesChange) {
const changes = nodeDragItems.map((node) => {
const change: NodePositionChange = {
id: node.id,
type: 'position',
dragging,
};
if (positionChanged) {
change.positionAbsolute = node.positionAbsolute;
change.position = node.position;
}
return change;
});
if (changes?.length) {
if (hasDefaultNodes) {
const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values()));
const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin);
set({ nodeInternals: nextNodeInternals });
}
onNodesChange?.(changes);
if (changes?.length) {
if (hasDefaultNodes) {
const nodes = applyNodeChanges(changes, Array.from(nodeInternals.values()));
const nextNodeInternals = createNodeInternals(nodes, nodeInternals, nodeOrigin);
set({ nodeInternals: nextNodeInternals });
}
onNodesChange?.(changes);
}
},
addSelectedNodes: (selectedNodeIds: string[]) => {
const { multiSelectionActive, nodeInternals, edges } = get();
let changedNodes: NodeSelectionChange[];

View File

@@ -7,7 +7,9 @@ import type { Edge } from './edges';
export type NodeDimensionChange = {
id: string;
type: 'dimensions';
dimensions: Dimensions;
dimensions?: Dimensions;
updateStyle?: boolean;
resizing?: boolean;
};
export type NodePositionChange = {

View File

@@ -1,9 +1,9 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { MouseEvent as ReactMouseEvent, ComponentType, MemoExoticComponent } from 'react';
import type { Selection as D3Selection, ZoomBehavior } from 'd3';
import type { D3DragEvent, Selection as D3Selection, SubjectPosition, ZoomBehavior } from 'd3';
import type { XYPosition, Rect, Transform, CoordinateExtent } from './utils';
import type { NodeChange, EdgeChange } from './changes';
import type { NodeChange, EdgeChange, NodePositionChange } from './changes';
import type {
Node,
NodeInternals,
@@ -226,6 +226,7 @@ export type ReactFlowActions = {
setNodeExtent: (nodeExtent: CoordinateExtent) => void;
cancelConnection: () => void;
reset: () => void;
triggerNodeChanges: (changes: NodeChange[]) => void;
};
export type ReactFlowState = ReactFlowStore & ReactFlowActions;
@@ -245,3 +246,5 @@ export type ProOptions = {
account?: string;
hideAttribution: boolean;
};
export type UseDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;

View File

@@ -31,6 +31,7 @@ export type Node<T = any> = {
positionAbsolute?: XYPosition;
ariaLabel?: string;
focusable?: boolean;
resizing?: boolean;
// only used internally
[internalsSymbol]?: {

View File

@@ -50,58 +50,67 @@ function applyChanges(changes: any[], elements: any[]): any[] {
const initElements: any[] = changes.filter((c) => c.type === 'add').map((c) => c.item);
return elements.reduce((res: any[], item: any) => {
const currentChange = changes.find((c) => c.id === item.id);
const currentChanges = changes.filter((c) => c.id === item.id);
if (currentChange) {
switch (currentChange.type) {
case 'select': {
res.push({ ...item, selected: currentChange.selected });
return res;
}
case 'position': {
const updateItem = { ...item };
if (currentChanges.length === 0) {
res.push(item);
return res;
}
if (typeof currentChange.position !== 'undefined') {
updateItem.position = currentChange.position;
const updateItem = { ...item };
for (const currentChange of currentChanges) {
if (currentChange) {
switch (currentChange.type) {
case 'select': {
updateItem.selected = currentChange.selected;
break;
}
case 'position': {
if (typeof currentChange.position !== 'undefined') {
updateItem.position = currentChange.position;
}
if (typeof currentChange.positionAbsolute !== 'undefined') {
updateItem.positionAbsolute = currentChange.positionAbsolute;
if (typeof currentChange.positionAbsolute !== 'undefined') {
updateItem.positionAbsolute = currentChange.positionAbsolute;
}
if (typeof currentChange.dragging !== 'undefined') {
updateItem.dragging = currentChange.dragging;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
break;
}
case 'dimensions': {
if (typeof currentChange.dimensions !== 'undefined') {
updateItem.width = currentChange.dimensions.width;
updateItem.height = currentChange.dimensions.height;
}
if (typeof currentChange.dragging !== 'undefined') {
updateItem.dragging = currentChange.dragging;
if (typeof currentChange.updateStyle !== 'undefined') {
updateItem.style = { ...(updateItem.style || {}), ...currentChange.dimensions };
}
if (typeof currentChange.resizing === 'boolean') {
updateItem.resizing = currentChange.resizing;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
break;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
case 'remove': {
return res;
}
res.push(updateItem);
return res;
}
case 'dimensions': {
const updateItem = { ...item };
if (typeof currentChange.dimensions !== 'undefined') {
updateItem.width = currentChange.dimensions.width;
updateItem.height = currentChange.dimensions.height;
}
if (updateItem.expandParent) {
handleParentExpand(res, updateItem);
}
res.push(updateItem);
return res;
}
case 'remove': {
return res;
}
}
}
res.push(item);
res.push(updateItem);
return res;
}, initElements);
}

View File

@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ['@reactflow/eslint-config'],
};

View File

@@ -0,0 +1 @@
# @reactflow/node-resizer

View File

@@ -0,0 +1,10 @@
# @reactflow/node-resizer
A resizer component for React Flow that can be attached to a node.
## Installation
```sh
npm install @reactflow/node-resizer
```

View File

@@ -0,0 +1,71 @@
{
"name": "@reactflow/node-resizer",
"version": "0.0.0",
"description": "A helper component for resizing nodes.",
"keywords": [
"react",
"node-based UI",
"graph",
"diagram",
"workflow",
"react-flow"
],
"files": [
"dist"
],
"source": "src/index.tsx",
"main": "dist/umd/index.js",
"module": "dist/esm/index.js",
"types": "dist/esm/index.d.ts",
"sideEffects": [
"*.css"
],
"publishConfig": {
"access": "public"
},
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/wbkd/react-flow.git",
"directory": "packages/node-resizer"
},
"scripts": {
"dev": "concurrently \"rollup --config node:@reactflow/rollup-config --watch\" pnpm:css-watch",
"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"
},
"dependencies": {
"@reactflow/core": "workspace:*",
"classcat": "^5.0.4",
"d3-drag": "^3.0.0",
"d3-selection": "^3.0.0",
"zustand": "^4.1.1"
},
"peerDependencies": {
"react": ">=17",
"react-dom": ">=17"
},
"devDependencies": {
"@reactflow/eslint-config": "workspace:*",
"@reactflow/rollup-config": "workspace:*",
"@reactflow/tsconfig": "workspace:*",
"@types/d3-drag": "^3.0.1",
"@types/d3-selection": "^3.0.3",
"@types/node": "^18.7.16",
"@types/react": "^18.0.19",
"@types/react-dom": "^18.0.6",
"react": "^18.2.0",
"typescript": "^4.8.3"
},
"rollup": {
"globals": {
"zustand": "Zustand",
"zustand/shallow": "zustandShallow",
"classcat": "cc"
},
"name": "ReactFlowNodeResizer"
}
}

View File

@@ -0,0 +1,45 @@
import ResizeControl from './ResizeControl';
import { ControlPosition, NodeResizerProps, ResizeControlVariant, ControlLinePosition } from './types';
const handleControls: ControlPosition[] = ['top-left', 'top-right', 'bottom-left', 'bottom-right'];
const lineControls: ControlLinePosition[] = ['top', 'right', 'bottom', 'left'];
export default function NodeResizer({
nodeId,
isVisible = true,
handleClassName,
handleStyle,
lineClassName,
lineStyle,
color,
}: NodeResizerProps) {
if (!isVisible) {
return null;
}
return (
<>
{lineControls.map((c) => (
<ResizeControl
key={c}
className={lineClassName}
style={lineStyle}
nodeId={nodeId}
position={c}
variant={ResizeControlVariant.Line}
color={color}
/>
))}
{handleControls.map((c) => (
<ResizeControl
key={c}
className={handleClassName}
style={handleStyle}
nodeId={nodeId}
position={c}
color={color}
/>
))}
</>
);
}

View File

@@ -0,0 +1,183 @@
import { useRef, useEffect, memo } from 'react';
import cc from 'classcat';
import { drag } from 'd3-drag';
import { select } from 'd3-selection';
import {
useStoreApi,
useGetPointerPosition,
NodeChange,
NodeDimensionChange,
useNodeId,
NodePositionChange,
} from '@reactflow/core';
import type { Dimensions, XYPosition } from '@reactflow/core';
import { ResizeDragEvent, ResizeControlProps, ResizeControlLineProps, ResizeControlVariant } from './types';
function ResizeControl({
nodeId,
position,
variant = ResizeControlVariant.Handle,
className,
style = {},
children,
color,
minWidth = 1,
minHeight = 1,
}: ResizeControlProps) {
const contextNodeId = useNodeId();
const id = typeof nodeId === 'string' ? nodeId : contextNodeId;
const store = useStoreApi();
const resizeControlRef = useRef<HTMLDivElement>(null);
const startValues = useRef<Dimensions & XYPosition & { nodeX: number; nodeY: number }>({
width: 0,
height: 0,
x: 0,
y: 0,
nodeX: 0,
nodeY: 0,
});
const prevValues = useRef<Dimensions & XYPosition>({ width: 0, height: 0, x: 0, y: 0 });
const getPointerPosition = useGetPointerPosition();
const defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right';
const controlPosition = position ?? defaultPosition;
useEffect(() => {
if (!resizeControlRef.current || !id) {
return;
}
const selection = select(resizeControlRef.current);
const dragHandler = drag<HTMLDivElement, unknown>()
.on('start', (event: ResizeDragEvent) => {
const node = store.getState().nodeInternals.get(id);
const { xSnapped, ySnapped } = getPointerPosition(event);
startValues.current = {
width: node?.width ?? 0,
height: node?.height ?? 0,
nodeX: node?.position.x ?? 0,
nodeY: node?.position.y ?? 0,
x: xSnapped,
y: ySnapped,
};
prevValues.current = {
width: node?.width ?? 0,
height: node?.height ?? 0,
x: node?.position.x ?? 0,
y: node?.position.y ?? 0,
};
})
.on('drag', (event: ResizeDragEvent) => {
const { nodeInternals, triggerNodeChanges } = store.getState();
const { xSnapped, ySnapped } = getPointerPosition(event);
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) {
const changes: NodeChange[] = [];
const {
x: startX,
y: startY,
width: startWidth,
height: startHeight,
nodeX: startNodeX,
nodeY: startNodeY,
} = startValues.current;
const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current;
const distX = Math.floor(enableX ? xSnapped - startX : 0);
const distY = Math.floor(enableY ? ySnapped - startY : 0);
const width = Math.max(startWidth + (invertX ? -distX : distX), minWidth);
const height = Math.max(startHeight + (invertY ? -distY : distY), minHeight);
const isWidthChange = width !== prevWidth;
const isHeightChange = height !== prevHeight;
if (invertX || invertY) {
const x = invertX ? startNodeX - (width - startWidth) : startNodeX;
const y = invertY ? startNodeY - (height - startHeight) : startNodeY;
// only transform the node if the width or height changes
const isXPosChange = x !== prevX && isWidthChange;
const isYPosChange = y !== prevY && isHeightChange;
if (isXPosChange || isYPosChange) {
const positionChange: NodePositionChange = {
id: node.id,
type: 'position',
position: {
x: isXPosChange ? x : prevX,
y: isYPosChange ? y : prevY,
},
};
changes.push(positionChange);
prevValues.current.x = positionChange.position!.x;
prevValues.current.y = positionChange.position!.y;
}
}
if (isWidthChange || isHeightChange) {
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
updateStyle: true,
resizing: true,
dimensions: {
width: width,
height: height,
},
};
changes.push(dimensionChange);
prevValues.current.width = width;
prevValues.current.height = height;
}
triggerNodeChanges(changes);
}
})
.on('end', () => {
const { triggerNodeChanges } = store.getState();
const dimensionChange: NodeDimensionChange = {
id: id,
type: 'dimensions',
resizing: true,
};
triggerNodeChanges([dimensionChange]);
});
selection.call(dragHandler);
return () => {
selection.on('.drag', null);
};
}, [id, controlPosition, getPointerPosition]);
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} />;
}
export default memo(ResizeControl);

View File

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

View File

@@ -0,0 +1,103 @@
.react-flow__resize-control {
position: absolute;
}
.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 {
width: 4px;
height: 4px;
border: 1px solid #fff;
border-radius: 1px;
background-color: #3367d9;
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-color: #3367d9;
border-width: 0;
border-style: solid;
}
.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-width: 1px;
}
.react-flow__resize-control.line.right {
left: 100%;
border-right-width: 1px;
}
.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-width: 1px;
}
.react-flow__resize-control.line.bottom {
border-bottom-width: 1px;
top: 100%;
}

View File

@@ -0,0 +1,39 @@
import type { CSSProperties, ReactNode } from 'react';
import type { D3DragEvent, SubjectPosition } from 'd3-drag';
export type NodeResizerProps = {
nodeId?: string;
color?: string;
handleClassName?: string;
handleStyle?: CSSProperties;
lineClassName?: string;
lineStyle?: CSSProperties;
isVisible?: boolean;
};
export type ControlLinePosition = 'top' | 'bottom' | 'left' | 'right';
export type ControlPosition = ControlLinePosition | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export enum ResizeControlVariant {
Line = 'line',
Handle = 'handle',
}
export type ResizeControlProps = {
nodeId?: string;
position?: ControlPosition;
variant?: ResizeControlVariant;
color?: string;
className?: string;
style?: CSSProperties;
children?: ReactNode;
minWidth?: number;
minHeight?: number;
};
export type ResizeControlLineProps = ResizeControlProps & {
position: ControlLinePosition;
};
export type ResizeDragEvent = D3DragEvent<HTMLDivElement, null, SubjectPosition>;

View File

@@ -0,0 +1,6 @@
{
"extends": "@reactflow/tsconfig/react.json",
"display": "@reactflow/node-resizer",
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules", "dist"]
}

View File

@@ -8,6 +8,7 @@ import {
Rect,
Position,
internalsSymbol,
useNodeId,
} from '@reactflow/core';
import cc from 'classcat';
import shallow from 'zustand/shallow';
@@ -72,9 +73,11 @@ function NodeToolbar({
offset = 10,
...rest
}: NodeToolbarProps) {
const contextNodeId = useNodeId();
const nodesSelector = useCallback(
(state: ReactFlowState): Node[] => {
const nodeIds: string[] = typeof nodeId === 'string' ? [nodeId] : nodeId;
const nodeIds = Array.isArray(nodeId) ? nodeId : [nodeId || contextNodeId || ''];
return nodeIds.reduce<Node[]>((acc, id) => {
const node = state.nodeInternals.get(id);
@@ -84,7 +87,7 @@ function NodeToolbar({
return acc;
}, [] as Node[]);
},
[nodeId]
[nodeId, contextNodeId]
);
const nodes = useStore(nodesSelector, nodesEqualityFn);
const { transform, nodeOrigin, selectedNodesCount } = useStore(storeSelector, shallow);

View File

@@ -2,7 +2,7 @@ import { Position } from '@reactflow/core';
import type { HTMLAttributes } from 'react';
export type NodeToolbarProps = HTMLAttributes<HTMLDivElement> & {
nodeId: string | string[];
nodeId?: string | string[];
isVisible?: boolean;
position?: Position;
offset?: number;

73
pnpm-lock.yaml generated
View File

@@ -57,6 +57,7 @@ importers:
examples/vite-app:
specifiers:
'@cypress/skip-test': ^2.6.1
'@reactflow/node-resizer': workspace:^0.0.0
'@types/react': ^18.0.17
'@types/react-dom': ^18.0.6
'@vitejs/plugin-react': ^2.1.0
@@ -73,6 +74,7 @@ importers:
typescript: ^4.8.3
vite: ^3.1.0
dependencies:
'@reactflow/node-resizer': link:../../packages/node-resizer
classcat: registry.npmjs.org/classcat/5.0.4
dagre: registry.npmjs.org/dagre/0.8.5
localforage: registry.npmjs.org/localforage/1.10.0
@@ -215,6 +217,45 @@ importers:
react: registry.npmjs.org/react/18.2.0
typescript: registry.npmjs.org/typescript/4.8.3
packages/node-resizer:
specifiers:
'@babel/runtime': ^7.18.9
'@reactflow/core': workspace:*
'@reactflow/eslint-config': workspace:*
'@reactflow/rollup-config': workspace:*
'@reactflow/tsconfig': workspace:*
'@types/d3': ^7.4.0
'@types/d3-drag': ^3.0.1
'@types/d3-selection': ^3.0.3
'@types/node': ^18.7.16
'@types/react': ^18.0.19
'@types/react-dom': ^18.0.6
classcat: ^5.0.4
d3-drag: ^3.0.0
d3-selection: ^3.0.0
react: ^18.2.0
typescript: ^4.8.3
zustand: ^4.1.1
dependencies:
'@babel/runtime': registry.npmjs.org/@babel/runtime/7.19.0
'@reactflow/core': link:../core
classcat: registry.npmjs.org/classcat/5.0.4
d3-drag: registry.npmjs.org/d3-drag/3.0.0
d3-selection: registry.npmjs.org/d3-selection/3.0.0
zustand: registry.npmjs.org/zustand/4.1.1_react@18.2.0
devDependencies:
'@reactflow/eslint-config': link:../../tooling/eslint-config
'@reactflow/rollup-config': link:../../tooling/rollup-config
'@reactflow/tsconfig': link:../../tooling/tsconfig
'@types/d3': registry.npmjs.org/@types/d3/7.4.0
'@types/d3-drag': registry.npmjs.org/@types/d3-drag/3.0.1
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
'@types/node': registry.npmjs.org/@types/node/18.7.16
'@types/react': registry.npmjs.org/@types/react/18.0.19
'@types/react-dom': registry.npmjs.org/@types/react-dom/18.0.6
react: registry.npmjs.org/react/18.2.0
typescript: registry.npmjs.org/typescript/4.8.3
packages/node-toolbar:
specifiers:
'@babel/runtime': ^7.18.9
@@ -1380,7 +1421,6 @@ packages:
resolution: {integrity: sha512-Reoy+pKnvsksN0lQUlcH6dOGjRZ/3WRwXR//m+/8lt1BXeI4xyaUZoqULNjyXXRuh0Mj4LNpkCvhUpQlY3X5xQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-array/-/d3-array-3.0.3.tgz}
name: '@types/d3-array'
version: 3.0.3
dev: false
registry.npmjs.org/@types/d3-axis/3.0.1:
resolution: {integrity: sha512-zji/iIbdd49g9WN0aIsGcwcTBUkgLsCSwB+uH+LPVDAiKWENMtI3cJEWt+7/YYwelMoZmbBfzA3qCdrZ2XFNnw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.1.tgz}
@@ -1388,7 +1428,6 @@ packages:
version: 3.0.1
dependencies:
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
dev: false
registry.npmjs.org/@types/d3-brush/3.0.1:
resolution: {integrity: sha512-B532DozsiTuQMHu2YChdZU0qsFJSio3Q6jmBYGYNp3gMDzBmuFFgPt9qKA4VYuLZMp4qc6eX7IUFUEsvHiXZAw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.1.tgz}
@@ -1396,19 +1435,16 @@ packages:
version: 3.0.1
dependencies:
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
dev: false
registry.npmjs.org/@types/d3-chord/3.0.1:
resolution: {integrity: sha512-eQfcxIHrg7V++W8Qxn6QkqBNBokyhdWSAS73AbkbMzvLQmVVBviknoz2SRS/ZJdIOmhcmmdCRE/NFOm28Z1AMw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.1.tgz}
name: '@types/d3-chord'
version: 3.0.1
dev: false
registry.npmjs.org/@types/d3-color/3.1.0:
resolution: {integrity: sha512-HKuicPHJuvPgCD+np6Se9MQvS6OCbJmOjGvylzMJRlDwUXjKTTXs6Pwgk79O09Vj/ho3u1ofXnhFOaEWWPrlwA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.0.tgz}
name: '@types/d3-color'
version: 3.1.0
dev: false
registry.npmjs.org/@types/d3-contour/3.0.1:
resolution: {integrity: sha512-C3zfBrhHZvrpAAK3YXqLWVAGo87A4SvJ83Q/zVJ8rFWJdKejUnDYaWZPkA8K84kb2vDA/g90LTQAz7etXcgoQQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.1.tgz}
@@ -1417,19 +1453,16 @@ packages:
dependencies:
'@types/d3-array': registry.npmjs.org/@types/d3-array/3.0.3
'@types/geojson': registry.npmjs.org/@types/geojson/7946.0.10
dev: false
registry.npmjs.org/@types/d3-delaunay/6.0.1:
resolution: {integrity: sha512-tLxQ2sfT0p6sxdG75c6f/ekqxjyYR0+LwPrsO1mbC9YDBzPJhs2HbJJRrn8Ez1DBoHRo2yx7YEATI+8V1nGMnQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.1.tgz}
name: '@types/d3-delaunay'
version: 6.0.1
dev: false
registry.npmjs.org/@types/d3-dispatch/3.0.1:
resolution: {integrity: sha512-NhxMn3bAkqhjoxabVJWKryhnZXXYYVQxaBnbANu0O94+O/nX9qSjrA1P1jbAQJxJf+VC72TxDX/YJcKue5bRqw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.1.tgz}
name: '@types/d3-dispatch'
version: 3.0.1
dev: false
registry.npmjs.org/@types/d3-drag/3.0.1:
resolution: {integrity: sha512-o1Va7bLwwk6h03+nSM8dpaGEYnoIG19P0lKqlic8Un36ymh9NSkNFX1yiXMKNMx8rJ0Kfnn2eovuFaL6Jvj0zA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.1.tgz}
@@ -1437,19 +1470,16 @@ packages:
version: 3.0.1
dependencies:
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
dev: false
registry.npmjs.org/@types/d3-dsv/3.0.0:
resolution: {integrity: sha512-o0/7RlMl9p5n6FQDptuJVMxDf/7EDEv2SYEO/CwdG2tr1hTfUVi0Iavkk2ax+VpaQ/1jVhpnj5rq1nj8vwhn2A==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.0.tgz}
name: '@types/d3-dsv'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-ease/3.0.0:
resolution: {integrity: sha512-aMo4eaAOijJjA6uU+GIeW018dvy9+oH5Y2VPPzjjfxevvGQ/oRDs+tfYC9b50Q4BygRR8yE2QCLsrT0WtAVseA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.0.tgz}
name: '@types/d3-ease'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-fetch/3.0.1:
resolution: {integrity: sha512-toZJNOwrOIqz7Oh6Q7l2zkaNfXkfR7mFSJvGvlD/Ciq/+SQ39d5gynHJZ/0fjt83ec3WL7+u3ssqIijQtBISsw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.1.tgz}
@@ -1457,19 +1487,16 @@ packages:
version: 3.0.1
dependencies:
'@types/d3-dsv': registry.npmjs.org/@types/d3-dsv/3.0.0
dev: false
registry.npmjs.org/@types/d3-force/3.0.3:
resolution: {integrity: sha512-z8GteGVfkWJMKsx6hwC3SiTSLspL98VNpmvLpEFJQpZPq6xpA1I8HNBDNSpukfK0Vb0l64zGFhzunLgEAcBWSA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.3.tgz}
name: '@types/d3-force'
version: 3.0.3
dev: false
registry.npmjs.org/@types/d3-format/3.0.1:
resolution: {integrity: sha512-5KY70ifCCzorkLuIkDe0Z9YTf9RR2CjBX1iaJG+rgM/cPP+sO+q9YdQ9WdhQcgPj1EQiJ2/0+yUkkziTG6Lubg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.1.tgz}
name: '@types/d3-format'
version: 3.0.1
dev: false
registry.npmjs.org/@types/d3-geo/3.0.2:
resolution: {integrity: sha512-DbqK7MLYA8LpyHQfv6Klz0426bQEf7bRTvhMy44sNGVyZoWn//B0c+Qbeg8Osi2Obdc9BLLXYAKpyWege2/7LQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.0.2.tgz}
@@ -1477,13 +1504,11 @@ packages:
version: 3.0.2
dependencies:
'@types/geojson': registry.npmjs.org/@types/geojson/7946.0.10
dev: false
registry.npmjs.org/@types/d3-hierarchy/3.1.0:
resolution: {integrity: sha512-g+sey7qrCa3UbsQlMZZBOHROkFqx7KZKvUpRzI/tAp/8erZWpYq7FgNKvYwebi2LaEiVs1klhUfd3WCThxmmWQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.0.tgz}
name: '@types/d3-hierarchy'
version: 3.1.0
dev: false
registry.npmjs.org/@types/d3-interpolate/3.0.1:
resolution: {integrity: sha512-jx5leotSeac3jr0RePOH1KdR9rISG91QIE4Q2PYTu4OymLTZfA3SrnURSLzKH48HmXVUru50b8nje4E79oQSQw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.1.tgz}
@@ -1491,37 +1516,31 @@ packages:
version: 3.0.1
dependencies:
'@types/d3-color': registry.npmjs.org/@types/d3-color/3.1.0
dev: false
registry.npmjs.org/@types/d3-path/3.0.0:
resolution: {integrity: sha512-0g/A+mZXgFkQxN3HniRDbXMN79K3CdTpLsevj+PXiTcb2hVyvkZUBg37StmgCQkaD84cUJ4uaDAWq7UJOQy2Tg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-path/-/d3-path-3.0.0.tgz}
name: '@types/d3-path'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-polygon/3.0.0:
resolution: {integrity: sha512-D49z4DyzTKXM0sGKVqiTDTYr+DHg/uxsiWDAkNrwXYuiZVd9o9wXZIo+YsHkifOiyBkmSWlEngHCQme54/hnHw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.0.tgz}
name: '@types/d3-polygon'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-quadtree/3.0.2:
resolution: {integrity: sha512-QNcK8Jguvc8lU+4OfeNx+qnVy7c0VrDJ+CCVFS9srBo2GL9Y18CnIxBdTF3v38flrGy5s1YggcoAiu6s4fLQIw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.2.tgz}
name: '@types/d3-quadtree'
version: 3.0.2
dev: false
registry.npmjs.org/@types/d3-random/3.0.1:
resolution: {integrity: sha512-IIE6YTekGczpLYo/HehAy3JGF1ty7+usI97LqraNa8IiDur+L44d0VOjAvFQWJVdZOJHukUJw+ZdZBlgeUsHOQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.1.tgz}
name: '@types/d3-random'
version: 3.0.1
dev: false
registry.npmjs.org/@types/d3-scale-chromatic/3.0.0:
resolution: {integrity: sha512-dsoJGEIShosKVRBZB0Vo3C8nqSDqVGujJU6tPznsBJxNJNwMF8utmS83nvCBKQYPpjCzaaHcrf66iTRpZosLPw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz}
name: '@types/d3-scale-chromatic'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-scale/4.0.2:
resolution: {integrity: sha512-Yk4htunhPAwN0XGlIwArRomOjdoBFXC3+kCxK2Ubg7I9shQlVSJy/pG/Ht5ASN+gdMIalpk8TJ5xV74jFsetLA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.2.tgz}
@@ -1529,13 +1548,11 @@ packages:
version: 4.0.2
dependencies:
'@types/d3-time': registry.npmjs.org/@types/d3-time/3.0.0
dev: false
registry.npmjs.org/@types/d3-selection/3.0.3:
resolution: {integrity: sha512-Mw5cf6nlW1MlefpD9zrshZ+DAWL4IQ5LnWfRheW6xwsdaWOb6IRRu2H7XPAQcyXEx1D7XQWgdoKR83ui1/HlEA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.3.tgz}
name: '@types/d3-selection'
version: 3.0.3
dev: false
registry.npmjs.org/@types/d3-shape/3.1.0:
resolution: {integrity: sha512-jYIYxFFA9vrJ8Hd4Se83YI6XF+gzDL1aC5DCsldai4XYYiVNdhtpGbA/GM6iyQ8ayhSp3a148LY34hy7A4TxZA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.0.tgz}
@@ -1543,25 +1560,21 @@ packages:
version: 3.1.0
dependencies:
'@types/d3-path': registry.npmjs.org/@types/d3-path/3.0.0
dev: false
registry.npmjs.org/@types/d3-time-format/4.0.0:
resolution: {integrity: sha512-yjfBUe6DJBsDin2BMIulhSHmr5qNR5Pxs17+oW4DoVPyVIXZ+m6bs7j1UVKP08Emv6jRmYrYqxYzO63mQxy1rw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.0.tgz}
name: '@types/d3-time-format'
version: 4.0.0
dev: false
registry.npmjs.org/@types/d3-time/3.0.0:
resolution: {integrity: sha512-sZLCdHvBUcNby1cB6Fd3ZBrABbjz3v1Vm90nysCQ6Vt7vd6e/h9Lt7SiJUoEX0l4Dzc7P5llKyhqSi1ycSf1Hg==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.0.tgz}
name: '@types/d3-time'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-timer/3.0.0:
resolution: {integrity: sha512-HNB/9GHqu7Fo8AQiugyJbv6ZxYz58wef0esl4Mv828w1ZKpAshw/uFWVDUcIB9KKFeFKoxS3cHY07FFgtTRZ1g==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.0.tgz}
name: '@types/d3-timer'
version: 3.0.0
dev: false
registry.npmjs.org/@types/d3-transition/3.0.2:
resolution: {integrity: sha512-jo5o/Rf+/u6uerJ/963Dc39NI16FQzqwOc54bwvksGAdVfvDrqDpVeq95bEvPtBwLCVZutAEyAtmSyEMxN7vxQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.2.tgz}
@@ -1569,7 +1582,6 @@ packages:
version: 3.0.2
dependencies:
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
dev: false
registry.npmjs.org/@types/d3-zoom/3.0.1:
resolution: {integrity: sha512-7s5L9TjfqIYQmQQEUcpMAcBOahem7TRoSO/+Gkz02GbMVuULiZzjF2BOdw291dbO2aNon4m2OdFsRGaCq2caLQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.1.tgz}
@@ -1578,7 +1590,6 @@ packages:
dependencies:
'@types/d3-interpolate': registry.npmjs.org/@types/d3-interpolate/3.0.1
'@types/d3-selection': registry.npmjs.org/@types/d3-selection/3.0.3
dev: false
registry.npmjs.org/@types/d3/7.4.0:
resolution: {integrity: sha512-jIfNVK0ZlxcuRDKtRS/SypEyOQ6UHaFQBKv032X45VvxSJ6Yi5G9behy9h6tNTHTDGh5Vq+KbmBjUWLgY4meCA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/d3/-/d3-7.4.0.tgz}
@@ -1615,7 +1626,6 @@ packages:
'@types/d3-timer': registry.npmjs.org/@types/d3-timer/3.0.0
'@types/d3-transition': registry.npmjs.org/@types/d3-transition/3.0.2
'@types/d3-zoom': registry.npmjs.org/@types/d3-zoom/3.0.1
dev: false
registry.npmjs.org/@types/estree/0.0.39:
resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz}
@@ -1633,7 +1643,6 @@ packages:
resolution: {integrity: sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz}
name: '@types/geojson'
version: 7946.0.10
dev: false
registry.npmjs.org/@types/is-ci/3.0.0:
resolution: {integrity: sha512-Q0Op0hdWbYd1iahB+IFNQcWXFq4O0Q5MwQP7uN0souuQ4rPg1vEYcnIOfr1gY+M+6rc8FGoRaBO1mOOvL29sEQ==, registry: https://registry.npmjs.com/, tarball: https://registry.npmjs.org/@types/is-ci/-/is-ci-3.0.0.tgz}