/** biome-ignore-all lint/a11y/noStaticElementInteractions: need it */ /** biome-ignore-all lint/a11y/useAriaPropsSupportedByRole: nedd it */ import { Typography } from '@mui/material'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import type { LexicalEditor } from 'lexical'; import { isEmpty, isNil } from 'ramda'; import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; interface StyleProps { editable: boolean; error?: string; minInputHeight: number; } const useStyles = makeStyles()( (theme, { minInputHeight, editable, error }) => ({ container: { backgroundColor: theme.palette.background.paper, border: error ? `1px solid ${theme.palette.error.main}` : '1px solid transparent', borderRadius: theme.shape.borderRadius, padding: theme.spacing(0.5, 1) }, emptyInput: { marginTop: '-22px' }, input: { minHeight: editable ? minInputHeight : 'min-content', outline: '0px solid transparent' }, inputFocused: { border: error ? `1px solid ${theme.palette.error.main}` : `1px solid ${theme.palette.primary.main}` }, notEditable: { backgroundColor: 'transparent' }, placeholder: { color: theme.palette.grey[500], pointerEvents: 'none' }, root: { '& p,h1,h2,h3,h4,h5,h6,span': { margin: 0 } } }) ); interface Props { className?: string; disabled?: boolean; editable: boolean; editorState?: string; error?: string; hasInitialTextContent?: boolean; initialEditorState?: string; initialize?: (editor: LexicalEditor) => void; inputClassname?: string; minInputHeight: number; namespace: string; onBlur?: (e: string) => void; placeholder: string; resetEditorToInitialStateCondition?: () => boolean; } const defaultState = '{"root":{"children":[{"children":[],"direction":null,"format":"","indent":0,"type":"paragraph","version":1}],"direction":null,"format":"","indent":0,"type":"root","version":1}}'; const ContentEditable = ({ minInputHeight, inputClassname, placeholder, hasInitialTextContent, editable, editorState, namespace, resetEditorToInitialStateCondition, initialEditorState, error, onBlur, className, disabled, initialize }: Props): JSX.Element => { const { classes, cx } = useStyles({ editable, error, minInputHeight }); const { t } = useTranslation(); const [editor] = useLexicalComposerContext(); const [isFocused, setFocused] = useState(false); const [root, setRoot] = useState(''); const ref = useCallback( (rootElement: null | HTMLElement) => { editor.setRootElement(rootElement); }, [editor] ); useLayoutEffect(() => { if (!editable) { const newEditorState = editor.parseEditorState( editorState || defaultState ); editor.setEditorState(newEditorState); } }, [editor, editorState, editable]); useEffect(() => { editor.registerTextContentListener((currentRoot) => { setRoot(currentRoot); }); if (!hasInitialTextContent) { return; } setRoot(' '); }, [editor, hasInitialTextContent]); useEffect(() => { const shouldResetEditorToInitialState = resetEditorToInitialStateCondition?.(); if (!shouldResetEditorToInitialState) { return; } const newEditorState = editor.parseEditorState( initialEditorState || defaultState ); editor.setEditorState(newEditorState); }, [ editor.parseEditorState, editor.setEditorState, initialEditorState, resetEditorToInitialStateCondition ]); const isTextEmpty = isEmpty(root) && // @ts-expect-error - suppressing pre-existing type mismatch !editor.getEditorState().toJSON().root.children?.[0]?.children?.length; const handleBlur = (event: React.FocusEvent): void => { setFocused(false); // @ts-expect-error - suppressing pre-existing type mismatch onBlur?.(event); }; const isEditable = editor.isEditable(); useEffect(() => { if (isNil(disabled)) { return; } editor.setEditable(!disabled); }, [disabled, editor.setEditable]); useEffect(() => { initialize?.(editor); }, [editor, initialize]); return (
{editable && isTextEmpty && ( {t(placeholder)} )}
setFocused(true)} ref={ref} />
); }; export default ContentEditable;