153 lines
3.8 KiB
TypeScript
153 lines
3.8 KiB
TypeScript
import { Field, Label } from '@headlessui/react'
|
|
import { Color } from '@tiptap/extension-color'
|
|
import Highlight from '@tiptap/extension-highlight'
|
|
import Image from '@tiptap/extension-image'
|
|
import Link from '@tiptap/extension-link'
|
|
import TextAlign from '@tiptap/extension-text-align'
|
|
import TextStyle from '@tiptap/extension-text-style'
|
|
import { EditorContent, useEditor } from '@tiptap/react'
|
|
import StarterKit from '@tiptap/starter-kit'
|
|
import { useEffect, useId, useState } from 'react'
|
|
import {
|
|
get,
|
|
type FieldError,
|
|
type FieldValues,
|
|
type Path,
|
|
type RegisterOptions,
|
|
} from 'react-hook-form'
|
|
import { useRemixFormContext } from 'remix-hook-form'
|
|
import { twMerge } from 'tailwind-merge'
|
|
|
|
import { EditorMenuBar } from './editor-menubar'
|
|
import { EditorTextArea } from './editor-textarea'
|
|
|
|
type TProperties<TFormValues extends FieldValues> = {
|
|
id?: string
|
|
name: Path<TFormValues>
|
|
label?: string
|
|
placeholder?: string
|
|
labelClassName?: string
|
|
className?: string
|
|
inputClassName?: string
|
|
containerClassName?: string
|
|
rules?: RegisterOptions
|
|
disabled?: boolean
|
|
isRequired?: boolean
|
|
category: string
|
|
}
|
|
|
|
export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
|
properties: TProperties<TFormValues>,
|
|
) => {
|
|
const {
|
|
id,
|
|
label,
|
|
name,
|
|
labelClassName,
|
|
className,
|
|
inputClassName,
|
|
category,
|
|
disabled = false,
|
|
containerClassName,
|
|
} = properties
|
|
|
|
const [isPlainHTML, setIsPlainHTML] = useState(false)
|
|
const [init, setInit] = useState(true)
|
|
const generatedId = useId()
|
|
|
|
const {
|
|
setValue,
|
|
watch,
|
|
formState: { errors },
|
|
} = useRemixFormContext()
|
|
|
|
const watchContent = watch(name)
|
|
const error: FieldError = get(errors, name)
|
|
|
|
const editor = useEditor({
|
|
editable: !disabled,
|
|
extensions: [
|
|
StarterKit.configure({
|
|
paragraph: {
|
|
HTMLAttributes: {
|
|
class: 'min-h-1',
|
|
},
|
|
},
|
|
}),
|
|
Highlight,
|
|
Image.configure({
|
|
inline: true,
|
|
}),
|
|
TextStyle,
|
|
Color.configure({
|
|
types: ['textStyle'],
|
|
}),
|
|
Link.configure({
|
|
openOnClick: false,
|
|
}),
|
|
TextAlign.configure({
|
|
types: ['heading', 'paragraph'],
|
|
}),
|
|
],
|
|
immediatelyRender: false,
|
|
content: watchContent,
|
|
onUpdate: ({ editor }) => {
|
|
setValue(name, editor.getHTML() as any) // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
},
|
|
})
|
|
useEffect(() => {
|
|
if (
|
|
watchContent &&
|
|
watchContent.length > 0 &&
|
|
editor &&
|
|
(isPlainHTML || init)
|
|
) {
|
|
editor.commands.setContent(watchContent)
|
|
setInit(false)
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [watchContent])
|
|
|
|
return (
|
|
<Field
|
|
className={twMerge('relative', containerClassName)}
|
|
disabled={disabled}
|
|
id={id}
|
|
>
|
|
{label && (
|
|
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
|
|
{label}{' '}
|
|
{error && <span className="text-red-500">{error.message}</span>}
|
|
</Label>
|
|
)}
|
|
|
|
{isPlainHTML ? (
|
|
<EditorTextArea
|
|
setIsPlainHTML={setIsPlainHTML}
|
|
name={name}
|
|
disabled={disabled}
|
|
/>
|
|
) : (
|
|
<div className={twMerge('rounded-md', className)}>
|
|
<EditorMenuBar
|
|
disabled={disabled}
|
|
category={category}
|
|
editor={editor}
|
|
setIsPlainHTML={setIsPlainHTML}
|
|
/>
|
|
<EditorContent
|
|
readOnly={disabled}
|
|
editor={editor}
|
|
id={id ?? generatedId}
|
|
className={twMerge(
|
|
'prose prose-headings:my-0.5 prose-p:my-0.5 max-w-none cursor-text overflow-y-auto rounded-b-md p-2',
|
|
inputClassName,
|
|
)}
|
|
onClick={() => editor?.commands.focus()}
|
|
/>
|
|
</div>
|
|
)}
|
|
</Field>
|
|
)
|
|
}
|