All checks were successful
Build & Deploy to K3s / build-and-deploy (push) Successful in 2m55s
The Page d'accueil global now uses the same edit-in-place pattern as products: hero (with image upload), collection header, contact, and footer are all clickable-to-edit directly on a scoped replica of the live page. Technical fields (WhatsApp number, Instagram URL, response time, SEO) move to a "Réglages avancés" drawer. - HomePreviewEditor + home-panel.css mirror the public layout - HeroImageUploadSlot: single-image click-to-replace variant - HomeSettingsDrawer for non-visual fields - InlineEditable: add `separator` prop so stored `|` line-breaks render as newlines in the editor and roundtrip on save (used for heroTitle, contactTitle) - Remove unused ProductPanelInfo + editor.css Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
'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<string>({ path })
|
|
const ref = useRef<HTMLElement | null>(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<HTMLElement>) => {
|
|
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<HTMLElement>) => {
|
|
if (!isMultiline && e.key === 'Enter') {
|
|
e.preventDefault()
|
|
;(e.currentTarget as HTMLElement).blur()
|
|
}
|
|
}
|
|
|
|
const onPaste = (e: React.ClipboardEvent<HTMLElement>) => {
|
|
e.preventDefault()
|
|
const text = e.clipboardData.getData('text/plain')
|
|
document.execCommand('insertText', false, text)
|
|
}
|
|
|
|
return (
|
|
<Tag
|
|
ref={ref as never}
|
|
className={`rebours-editable${className ? ' ' + className : ''}`}
|
|
contentEditable
|
|
suppressContentEditableWarning
|
|
spellCheck={false}
|
|
onBlur={commit}
|
|
onKeyDown={onKeyDown}
|
|
onPaste={onPaste}
|
|
data-placeholder={placeholder ?? ''}
|
|
data-as={Tag}
|
|
/>
|
|
)
|
|
}
|