import React, { useEffect, useRef, memo, ComponentType, CSSProperties, useMemo, MouseEvent, useCallback } from 'react'; import { DraggableCore, DraggableData, DraggableEvent } from 'react-draggable'; import cc from 'classcat'; import { useStoreActions } from '../../store/hooks'; import { Provider } from '../../contexts/NodeIdContext'; import { NodeComponentProps, WrapNodeProps } from '../../types'; export default (NodeComponent: ComponentType) => { const NodeWrapper = ({ id, type, data, scale, xPos, yPos, selected, onClick, onMouseEnter, onMouseMove, onMouseLeave, onContextMenu, onNodeDoubleClick, onNodeDragStart, onNodeDrag, onNodeDragStop, style, className, isDraggable, isSelectable, isConnectable, selectNodesOnDrag, sourcePosition, targetPosition, isHidden, isInitialized, snapToGrid, snapGrid, isDragging, resizeObserver, }: WrapNodeProps) => { const updateNodeDimensions = useStoreActions((actions) => actions.updateNodeDimensions); const addSelectedElements = useStoreActions((actions) => actions.addSelectedElements); const updateNodePosDiff = useStoreActions((actions) => actions.updateNodePosDiff); const unsetNodesSelection = useStoreActions((actions) => actions.unsetNodesSelection); const nodeElement = useRef(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: selected ? 10 : 3, 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, ...style, }), [ selected, xPos, yPos, isSelectable, isDraggable, onClick, isInitialized, style, onMouseEnter, onMouseMove, onMouseLeave, ] ); 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 (!selected) { addSelectedElements(node); } } onClick?.(event, node); } }, [isSelectable, selected, isDraggable, onClick, node] ); const onDragStart = useCallback( (event: DraggableEvent) => { onNodeDragStart?.(event as MouseEvent, node); if (selectNodesOnDrag && isSelectable) { unsetNodesSelection(); if (!selected) { addSelectedElements(node); } } else if (!selectNodesOnDrag && !selected && isSelectable) { unsetNodesSelection(); addSelectedElements([]); } }, [node, selected, selectNodesOnDrag, isSelectable, onNodeDragStart] ); const onDrag = useCallback( (event: DraggableEvent, draggableData: DraggableData) => { if (onNodeDrag) { node.position.x += draggableData.deltaX; node.position.y += draggableData.deltaY; onNodeDrag(event as MouseEvent, node); } updateNodePosDiff({ id, diff: { x: draggableData.deltaX, y: draggableData.deltaY, }, isDragging: true, }); }, [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 && !selected) { addSelectedElements(node); } onClick?.(event as MouseEvent, node); return; } updateNodePosDiff({ id: node.id, isDragging: false, }); onNodeDragStop?.(event as MouseEvent, node); }, [node, isSelectable, selectNodesOnDrag, onClick, onNodeDragStop, isDragging, selected] ); const onNodeDoubleClickHandler = useCallback( (event: MouseEvent) => { onNodeDoubleClick?.(event, node); }, [node, onNodeDoubleClick] ); useEffect(() => { if (nodeElement.current && !isHidden) { updateNodeDimensions([{ id, nodeElement: nodeElement.current, forceUpdate: true }]); } }, [id, isHidden, sourcePosition, targetPosition]); useEffect(() => { if (nodeElement.current) { const currNode = nodeElement.current; resizeObserver?.observe(currNode); return () => resizeObserver?.unobserve(currNode); } return; }, []); if (isHidden) { return null; } const nodeClasses = cc([ 'react-flow__node', `react-flow__node-${type}`, className, { selected, selectable: isSelectable, }, ]); return (
); }; NodeWrapper.displayName = 'NodeWrapper'; return memo(NodeWrapper); };