import React, { CSSProperties, useCallback, useRef, useEffect } from 'react'; import { useNode, UserComponent } from '@craftjs/core'; import { cssPropsToString } from '../../utils/style-helpers'; import { SettingsTabs } from '../../ui/SettingsTabs'; import { TypographyControl } from '../../ui/TypographyControl'; import { AdvancedTab } from '../../ui/AdvancedTab'; type HeadingLevel = 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; interface HeadingProps { text?: string; level?: HeadingLevel; style?: CSSProperties; cssId?: string; cssClass?: string; hideOnDesktop?: boolean; hideOnTablet?: boolean; hideOnMobile?: boolean; animation?: string; animationDelay?: string; } export const Heading: UserComponent = ({ text = 'Heading', level = 'h2', style = {}, }) => { const { connectors: { connect, drag }, selected, actions: { setProp }, } = useNode((node) => ({ selected: node.events.selected, })); const elRef = useRef(null); const editedTextRef = useRef(null); const commitText = useCallback(() => { if (elRef.current) { const newText = elRef.current.innerText; editedTextRef.current = newText; setProp((p: HeadingProps) => { p.text = newText; }); } }, [setProp]); // Commit on blur const handleBlur = useCallback(() => { commitText(); }, [commitText]); // Also commit on deselect via effect useEffect(() => { if (!selected && editedTextRef.current !== null) { setProp((p: HeadingProps) => { p.text = editedTextRef.current!; }); editedTextRef.current = null; } }, [selected, setProp]); // Set DOM text on mount and when text prop changes externally (not during editing) useEffect(() => { if (elRef.current && !selected && editedTextRef.current === null) { elRef.current.innerText = text || ''; } }, [text, selected]); return React.createElement(level, { ref: (ref: HTMLElement | null): void => { elRef.current = ref; if (ref) connect(drag(ref)); }, contentEditable: selected, suppressContentEditableWarning: true, onBlur: handleBlur, onInput: () => { // Track that we have unsaved edits if (elRef.current) { editedTextRef.current = elRef.current.innerText; } }, style: { outline: 'none', cursor: selected ? 'text' : 'pointer', minHeight: '1em', ...style }, }); }; /* ---------- Settings panel ---------- */ const HeadingSettings: React.FC = () => { const { actions: { setProp }, props } = useNode((node) => ({ props: node.data.props as HeadingProps, })); const levels: HeadingLevel[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']; return (
{levels.map((l) => ( ))}
setProp((p: HeadingProps) => { p.text = e.target.value; })} style={{ width: '100%', padding: '6px 8px', background: '#27272a', color: '#e4e4e7', border: '1px solid #3f3f46', borderRadius: 4, fontSize: 13 }} />
} style={ setProp((p: HeadingProps) => { p.style = { ...p.style, ...updates }; })} /> } advanced={ setProp((p: HeadingProps) => { p.style = { ...p.style, ...updates }; })} cssId={props.cssId || ''} onCssIdChange={(id) => setProp((p: HeadingProps) => { p.cssId = id; })} cssClass={props.cssClass || ''} onCssClassChange={(cls) => setProp((p: HeadingProps) => { p.cssClass = cls; })} hideOnDesktop={props.hideOnDesktop} onHideOnDesktopChange={(v) => setProp((p: HeadingProps) => { p.hideOnDesktop = v; })} hideOnTablet={props.hideOnTablet} onHideOnTabletChange={(v) => setProp((p: HeadingProps) => { p.hideOnTablet = v; })} hideOnMobile={props.hideOnMobile} onHideOnMobileChange={(v) => setProp((p: HeadingProps) => { p.hideOnMobile = v; })} animation={props.animation} onAnimationChange={(v) => setProp((p: HeadingProps) => { p.animation = v; })} animationDelay={props.animationDelay} onAnimationDelayChange={(v) => setProp((p: HeadingProps) => { p.animationDelay = v; })} /> } /> ); }; Heading.craft = { displayName: 'Heading', props: { text: 'Your Heading', level: 'h2' as HeadingLevel, style: { fontSize: '36px', fontWeight: '700', fontFamily: 'Inter, sans-serif', color: '#1f2937', marginBottom: '16px', }, }, rules: { canDrag: () => true, canMoveIn: () => false, canMoveOut: () => true, }, related: { settings: HeadingSettings, }, }; (Heading as any).toHtml = (props: HeadingProps, _childrenHtml: string) => { const tag = props.level || 'h2'; const safeText = (props.text || '').replace(//g, '>'); const styleStr = cssPropsToString(props.style); return { html: `<${tag}${styleStr ? ` style="${styleStr}"` : ''}>${safeText}` }; };