Merge pull request #2626 from wbkd/feat/resize-node
Feat: Add NodeResizer package
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -57,6 +57,7 @@ const NodeRenderer = (props: NodeRendererProps) => {
|
||||
nodeElement: entry.target as HTMLDivElement,
|
||||
forceUpdate: true,
|
||||
}));
|
||||
|
||||
updateNodeDimensions(updates);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -31,6 +31,7 @@ export type Node<T = any> = {
|
||||
positionAbsolute?: XYPosition;
|
||||
ariaLabel?: string;
|
||||
focusable?: boolean;
|
||||
resizing?: boolean;
|
||||
|
||||
// only used internally
|
||||
[internalsSymbol]?: {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user