import React, { useCallback, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; import { useEditor, useNode } from '@craftjs/core'; interface RenderNodeProps { render: React.ReactElement; } /** * Craft.js `` override -- wraps every node's render output. * For the currently-selected node it portals a floating badge (component * displayName + a "select parent" chevron) positioned over the node's real * DOM element. Non-selected nodes (the overwhelming majority) and ROOT pass * straight through as a Fragment, so this never touches layout, never * appears in `toHtml` export (that walks the Craft node tree, not this * portal), and doesn't wrap every node in extra DOM. */ export const RenderNode: React.FC = ({ render }) => { const { actions } = useEditor(); const { id, isSelected, dom, name, parent } = useNode((node) => ({ isSelected: node.events.selected, dom: node.dom, name: (node.data.props?.aiName as string) || node.data.displayName, parent: node.data.parent, })); const badgeRef = useRef(null); const active = isSelected && id !== 'ROOT' && !!dom; const updatePosition = useCallback(() => { if (!dom || !badgeRef.current) return; const rect = dom.getBoundingClientRect(); const badgeHeight = 22; badgeRef.current.style.left = `${Math.max(rect.left, 0)}px`; badgeRef.current.style.top = `${Math.max(rect.top - badgeHeight, 0)}px`; }, [dom]); useEffect(() => { if (!active) return; updatePosition(); window.addEventListener('resize', updatePosition); document.addEventListener('scroll', updatePosition, true); return () => { window.removeEventListener('resize', updatePosition); document.removeEventListener('scroll', updatePosition, true); }; }, [active, updatePosition]); if (!active) return <>{render}; return ( <> {render} {createPortal(
{name} {parent && ( )}
, document.body )} ); };