import { type MouseEvent, type KeyboardEvent } from 'react'; import cc from 'classcat'; import { shallow } from 'zustand/shallow'; import { elementSelectionKeys, errorMessages, getNodeDimensions, isInputDOMNode, nodeHasDimensions, } from '@xyflow/system'; import { useStore, useStoreApi } from '../../hooks/useStore'; import { Provider } from '../../contexts/NodeIdContext'; import { ARIA_NODE_DESC_KEY } from '../A11yDescriptions'; import { useDrag } from '../../hooks/useDrag'; import { useMoveSelectedNodes } from '../../hooks/useMoveSelectedNodes'; import { handleNodeClick } from '../Nodes/utils'; import { arrowKeyDiffs, builtinNodeTypes, getNodeInlineStyleDimensions } from './utils'; import { useNodeObserver } from './useNodeObserver'; import type { InternalNode, Node, NodeWrapperProps } from '../../types'; export function NodeWrapper({ id, onClick, onMouseEnter, onMouseMove, onMouseLeave, onContextMenu, onDoubleClick, nodesDraggable, elementsSelectable, nodesConnectable, nodesFocusable, resizeObserver, noDragClassName, noPanClassName, disableKeyboardA11y, rfId, nodeTypes, nodeClickDistance, onError, }: NodeWrapperProps) { const { node, internals, isParent } = useStore((s) => { const node = s.nodeLookup.get(id)! as InternalNode; const isParent = s.parentLookup.has(id); return { node, internals: node.internals, isParent, }; }, shallow); let nodeType = node.type || 'default'; let NodeComponent = nodeTypes?.[nodeType] || builtinNodeTypes[nodeType]; if (NodeComponent === undefined) { onError?.('003', errorMessages['error003'](nodeType)); nodeType = 'default'; NodeComponent = builtinNodeTypes.default; } const isDraggable = !!(node.draggable || (nodesDraggable && typeof node.draggable === 'undefined')); const isSelectable = !!(node.selectable || (elementsSelectable && typeof node.selectable === 'undefined')); const isConnectable = !!(node.connectable || (nodesConnectable && typeof node.connectable === 'undefined')); const isFocusable = !!(node.focusable || (nodesFocusable && typeof node.focusable === 'undefined')); const store = useStoreApi(); const hasDimensions = nodeHasDimensions(node); const nodeRef = useNodeObserver({ node, nodeType, hasDimensions, resizeObserver }); const dragging = useDrag({ nodeRef, disabled: node.hidden || !isDraggable, noDragClassName, handleSelector: node.dragHandle, nodeId: id, isSelectable, nodeClickDistance, }); const moveSelectedNodes = useMoveSelectedNodes(); if (node.hidden) { return null; } const nodeDimensions = getNodeDimensions(node); const inlineDimensions = getNodeInlineStyleDimensions(node); const hasPointerEvents = isSelectable || isDraggable || onClick || onMouseEnter || onMouseMove || onMouseLeave; const onMouseEnterHandler = onMouseEnter ? (event: MouseEvent) => onMouseEnter(event, { ...internals.userNode }) : undefined; const onMouseMoveHandler = onMouseMove ? (event: MouseEvent) => onMouseMove(event, { ...internals.userNode }) : undefined; const onMouseLeaveHandler = onMouseLeave ? (event: MouseEvent) => onMouseLeave(event, { ...internals.userNode }) : undefined; const onContextMenuHandler = onContextMenu ? (event: MouseEvent) => onContextMenu(event, { ...internals.userNode }) : undefined; const onDoubleClickHandler = onDoubleClick ? (event: MouseEvent) => onDoubleClick(event, { ...internals.userNode }) : undefined; const onSelectNodeHandler = (event: MouseEvent) => { const { selectNodesOnDrag, nodeDragThreshold } = store.getState(); if (isSelectable && (!selectNodesOnDrag || !isDraggable || nodeDragThreshold > 0)) { /* * this handler gets called by XYDrag on drag start when selectNodesOnDrag=true * here we only need to call it when selectNodesOnDrag=false */ handleNodeClick({ id, store, nodeRef, }); } if (onClick) { onClick(event, { ...internals.userNode }); } }; const onKeyDown = (event: KeyboardEvent) => { if (isInputDOMNode(event.nativeEvent) || disableKeyboardA11y) { return; } if (elementSelectionKeys.includes(event.key) && isSelectable) { const unselect = event.key === 'Escape'; handleNodeClick({ id, store, unselect, nodeRef, }); } else if (isDraggable && node.selected && Object.prototype.hasOwnProperty.call(arrowKeyDiffs, event.key)) { // prevent default scrolling behavior on arrow key press when node is moved event.preventDefault(); store.setState({ ariaLiveMessage: `Moved selected node ${event.key .replace('Arrow', '') .toLowerCase()}. New position, x: ${~~internals.positionAbsolute.x}, y: ${~~internals.positionAbsolute.y}`, }); moveSelectedNodes({ direction: arrowKeyDiffs[event.key], factor: event.shiftKey ? 4 : 1, }); } }; return (
); }