'use client' import { useField } from '@payloadcms/ui' import { useLayoutEffect, useRef } from 'react' type Props = { path: string as?: 'span' | 'h2' | 'p' | 'div' className?: string multiline?: boolean placeholder?: string /** * Stored separator that should display as a line-break in the editor. * When set (e.g. '|'), the stored value "FOO|BAR" is shown as two lines, * and newlines typed by the user are serialized back to the separator. * Implies `multiline`. */ separator?: string } export function InlineEditable({ path, as: Tag = 'span', className, multiline, placeholder, separator, }: Props) { const { value, setValue } = useField({ path }) const ref = useRef(null) const isMultiline = multiline || !!separator // Sync textContent from value. React doesn't reconcile children of // contentEditable elements, so we drive the DOM imperatively — and // never clobber the DOM while the user is actively typing in it. useLayoutEffect(() => { const el = ref.current if (!el) return if (typeof document !== 'undefined' && document.activeElement === el) return const raw = value ?? '' // Convert stored separator to newlines for display. const next = separator ? raw.split(separator).join('\n') : raw if (el.textContent !== next) el.textContent = next el.classList.toggle('rebours-editable--empty', !raw) }, [value, separator]) const commit = (e: React.FocusEvent) => { let next = (e.currentTarget.textContent ?? '').replace(/\u00A0/g, ' ') // Serialize newlines back to the stored separator. if (separator) next = next.replace(/\n/g, separator) if (next !== (value ?? '')) setValue(next) e.currentTarget.classList.toggle('rebours-editable--empty', !next) } const onKeyDown = (e: React.KeyboardEvent) => { if (!isMultiline && e.key === 'Enter') { e.preventDefault() ;(e.currentTarget as HTMLElement).blur() } } const onPaste = (e: React.ClipboardEvent) => { e.preventDefault() const text = e.clipboardData.getData('text/plain') document.execCommand('insertText', false, text) } return ( ) }