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. * * It also imperatively tags each node's real DOM element with two * editor-only data attributes (never part of `toHtml` export, which walks * the Craft node tree, not the live DOM): * - `data-craft-node`: set on actual Craft.js droppable containers * (`node.data.isCanvas`, excluding ROOT). `editor.css`'s dashed "guide" * outlines target this attribute instead of blanket tag selectors * (div/section/header/...), so a component's own internal wrapper markup * no longer picks up a guide outline it isn't a real drop target for. * - `data-craft-hovered`: mirrors `node.events.hovered` (Craft's hover * event set). Craft's own `connectors.connect()` (called by every * component) wires a native mouseover/mouseleave listener to this event * internally, so a plain mouse hover over any connected node sets it -- * this attribute is a real-mouse-hover canvas highlight. (The Layers * panel's row-hover -> canvas-highlight sync, item 12, uses a sibling * `data-layer-hovered` attribute written directly by LayersPanel.tsx * instead of this event, since the action that would drive it here * -- `actions.setNodeEvent` -- is stripped from the public `useEditor()` * API at runtime.) editor.css matches both attributes for the same * outline and suppresses both under `.guides-off`. */ export const RenderNode: React.FC = ({ render }) => { const { actions } = useEditor(); const { id, isSelected, dom, name, parent, isCanvas, isHovered } = useNode((node) => ({ isSelected: node.events.selected, isHovered: node.events.hovered, dom: node.dom, name: (node.data.props?.aiName as string) || node.data.displayName, parent: node.data.parent, isCanvas: node.data.isCanvas, })); 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]); useEffect(() => { if (!dom) return; if (isCanvas && id !== 'ROOT') { dom.setAttribute('data-craft-node', ''); } else { dom.removeAttribute('data-craft-node'); } }, [dom, isCanvas, id]); useEffect(() => { if (!dom) return; if (isHovered) { dom.setAttribute('data-craft-hovered', ''); } else { dom.removeAttribute('data-craft-hovered'); } }, [dom, isHovered]); if (!active) return <>{render}; return ( <> {render} {createPortal(
{name} {parent && ( )}
, document.body )} ); };