Files
xyflow/src/components/Nodes/wrapNode.tsx

289 lines
7.7 KiB
TypeScript

import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react';
import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable';
import cc from 'classcat';
import shallow from 'zustand/shallow';
import { useStore } from '../../store';
import { Provider } from '../../contexts/NodeIdContext';
import { NodeComponentProps, WrapNodeProps, ReactFlowState } from '../../types';
const selector = (s: ReactFlowState) => ({
addSelectedElements: s.addSelectedElements,
unsetNodesSelection: s.unsetNodesSelection,
updateNodePosition: s.updateNodePosition,
updateNodeDimensions: s.updateNodeDimensions,
unselectNodesAndEdges: s.unselectNodesAndEdges,
});
export default (NodeComponent: ComponentType<NodeComponentProps>) => {
const NodeWrapper = ({
id,
type,
data,
scale,
xPos,
yPos,
width,
height,
isSelected,
onClick,
onMouseEnter,
onMouseMove,
onMouseLeave,
onContextMenu,
onNodeDoubleClick,
onNodeDragStart,
onNodeDrag,
onNodeDragStop,
style,
className,
isDraggable,
isSelectable,
isConnectable,
selectNodesOnDrag,
sourcePosition,
targetPosition,
isHidden,
isInitialized,
snapToGrid,
snapGrid,
isDragging,
resizeObserver,
dragHandle,
zIndex,
isParentNode,
}: WrapNodeProps) => {
const {
addSelectedElements,
unselectNodesAndEdges,
unsetNodesSelection,
updateNodePosition,
updateNodeDimensions,
} = useStore(selector, shallow);
const nodeElement = useRef<HTMLDivElement>(null);
const node = useMemo(() => ({ id, type, position: { x: xPos, y: yPos }, data }), [id, type, xPos, yPos, data]);
const grid = useMemo(() => (snapToGrid ? snapGrid : [1, 1])! as [number, number], [snapToGrid, snapGrid]);
const nodeStyle: CSSProperties = useMemo(
() => ({
zIndex: isSelected ? zIndex + 1 : zIndex,
transform: `translate(${xPos}px,${yPos}px)`,
pointerEvents:
isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave ? 'all' : 'none',
// prevents jumping of nodes on start
// opacity: isInitialized ? 1 : 0,
width: isParentNode && width !== null ? width : 'auto',
height: isParentNode && height !== null ? height : 'auto',
...style,
}),
[
isSelected,
xPos,
yPos,
isSelectable,
isDraggable,
onClick,
isInitialized,
style,
onMouseEnter,
onMouseMove,
onMouseLeave,
isParentNode,
width,
height,
]
);
const onMouseEnterHandler = useMemo(() => {
if (!onMouseEnter || isDragging) {
return;
}
return (event: MouseEvent) => onMouseEnter(event, node);
}, [onMouseEnter, isDragging, node]);
const onMouseMoveHandler = useMemo(() => {
if (!onMouseMove || isDragging) {
return;
}
return (event: MouseEvent) => onMouseMove(event, node);
}, [onMouseMove, isDragging, node]);
const onMouseLeaveHandler = useMemo(() => {
if (!onMouseLeave || isDragging) {
return;
}
return (event: MouseEvent) => onMouseLeave(event, node);
}, [onMouseLeave, isDragging, node]);
const onContextMenuHandler = useMemo(() => {
if (!onContextMenu) {
return;
}
return (event: MouseEvent) => onContextMenu(event, node);
}, [onContextMenu, node]);
const onSelectNodeHandler = useCallback(
(event: MouseEvent) => {
if (!isDraggable) {
if (isSelectable) {
unsetNodesSelection();
if (!isSelected) {
addSelectedElements([node]);
}
}
onClick?.(event, node);
}
},
[isSelectable, isSelected, isDraggable, onClick, node]
);
const onDragStart = useCallback(
(event: DraggableEvent) => {
onNodeDragStart?.(event as MouseEvent, node);
if (selectNodesOnDrag && isSelectable) {
unsetNodesSelection();
if (!isSelected) {
addSelectedElements([node]);
}
} else if (!selectNodesOnDrag && !isSelected && isSelectable) {
unselectNodesAndEdges();
unsetNodesSelection();
}
},
[node, isSelected, selectNodesOnDrag, isSelectable, onNodeDragStart]
);
const onDrag = useCallback(
(event: DraggableEvent, draggableData: DraggableData) => {
node.position.x += draggableData.deltaX;
node.position.y += draggableData.deltaY;
if (onNodeDrag) {
onNodeDrag(event as MouseEvent, node);
}
updateNodePosition({ id, isDragging: true, diff: { x: draggableData.deltaX, y: draggableData.deltaY } });
},
[id, node, onNodeDrag]
);
const onDragStop = useCallback(
(event: DraggableEvent) => {
// onDragStop also gets called when user just clicks on a node.
// Because of that we set dragging to true inside the onDrag handler and handle the click here
if (!isDragging) {
if (isSelectable && !selectNodesOnDrag && !isSelected) {
addSelectedElements([node]);
}
onClick?.(event as MouseEvent, node);
return;
}
updateNodePosition({
id: node.id,
isDragging: false,
});
onNodeDragStop?.(event as MouseEvent, node);
},
[node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, isSelected]
);
const onNodeDoubleClickHandler = useCallback(
(event: MouseEvent) => {
onNodeDoubleClick?.(event, node);
},
[node, onNodeDoubleClick]
);
useEffect(() => {
if (nodeElement.current && (!isHidden || !isInitialized)) {
updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]);
}
}, [id, isHidden, sourcePosition, targetPosition, isInitialized]);
useEffect(() => {
if (nodeElement.current) {
const currNode = nodeElement.current;
resizeObserver?.observe(currNode);
return () => resizeObserver?.unobserve(currNode);
}
}, []);
if (isHidden) {
return null;
}
const nodeClasses = cc([
'react-flow__node',
`react-flow__node-${type}`,
className,
{
selected: isSelected,
selectable: isSelectable,
parent: isParentNode,
},
]);
return (
<DraggableCore
onStart={onDragStart}
onDrag={onDrag}
onStop={onDragStop}
scale={scale}
disabled={!isDraggable}
cancel=".nodrag"
nodeRef={nodeElement}
grid={grid}
enableUserSelectHack={false}
handle={dragHandle}
>
<div
className={nodeClasses}
ref={nodeElement}
style={nodeStyle}
onMouseEnter={onMouseEnterHandler}
onMouseMove={onMouseMoveHandler}
onMouseLeave={onMouseLeaveHandler}
onContextMenu={onContextMenuHandler}
onClick={onSelectNodeHandler}
onDoubleClick={onNodeDoubleClickHandler}
data-id={id}
>
<Provider value={id}>
<NodeComponent
id={id}
data={data}
type={type}
xPos={xPos}
yPos={yPos}
isSelected={isSelected}
isConnectable={isConnectable}
sourcePosition={sourcePosition}
targetPosition={targetPosition}
isDragging={isDragging}
dragHandle={dragHandle}
/>
</Provider>
</div>
</DraggableCore>
);
};
NodeWrapper.displayName = 'NodeWrapper';
return memo(NodeWrapper);
};