feat: add TextEditor component
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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 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
|
||||
rules?: RegisterOptions
|
||||
disabled?: boolean
|
||||
isRequired?: boolean
|
||||
category: string
|
||||
}
|
||||
|
||||
export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
||||
properties: TProperties<TFormValues>,
|
||||
) => {
|
||||
const {
|
||||
id,
|
||||
label,
|
||||
name,
|
||||
labelClassName,
|
||||
isRequired,
|
||||
className,
|
||||
inputClassName,
|
||||
category,
|
||||
disabled = false,
|
||||
} = properties
|
||||
|
||||
const [isPlainHTML, setIsPlainHTML] = useState(false)
|
||||
const [init, setInit] = useState(true)
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
const generatedId = useId()
|
||||
|
||||
const {
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useRemixFormContext()
|
||||
|
||||
const watchContent = watch(name)
|
||||
const error = get(errors, name)
|
||||
|
||||
const editor = useEditor({
|
||||
editable: !disabled,
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Highlight,
|
||||
Image.configure({
|
||||
inline: true,
|
||||
}),
|
||||
TextStyle,
|
||||
Color.configure({
|
||||
types: ['textStyle'],
|
||||
}),
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
TextAlign.configure({
|
||||
types: ['heading', 'paragraph'],
|
||||
}),
|
||||
],
|
||||
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 (
|
||||
<div className={twMerge('', className)}>
|
||||
{label && (
|
||||
<label
|
||||
htmlFor={id ?? generatedId}
|
||||
className={twMerge('mb-2 block text-sm font-bold', labelClassName)}
|
||||
>
|
||||
{label}
|
||||
{isRequired && <sup className="text-[#DF0000]">*</sup>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
{isPlainHTML ? (
|
||||
<EditorTextArea
|
||||
setIsPlainHTML={setIsPlainHTML}
|
||||
name={name}
|
||||
disabled={disabled}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<EditorMenuBar
|
||||
disabled={disabled}
|
||||
category={category}
|
||||
editor={editor}
|
||||
setIsPlainHTML={setIsPlainHTML}
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
/>
|
||||
<EditorContent
|
||||
readOnly={disabled}
|
||||
editor={editor}
|
||||
id={id ?? generatedId}
|
||||
className={twMerge(
|
||||
'prose mb-1 max-h-96 max-w-none cursor-text overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] px-4 py-1',
|
||||
darkMode ? 'bg-[#00000055]' : '',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => editor?.commands.focus()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p className="text-xs text-[#DF0000] italic">
|
||||
{error?.message?.toString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user