import { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Editor, EditorContent, Extensions, useEditor } from "@tiptap/react"; import { Button } from "../button"; import { Bold, Italic, Strikethrough, Code, List, ListOrdered, Quote, Undo, Redo, Eraser, Link2, Underline, PilcrowIcon } from "~/components/ui/icons/vendor"; import Color from "@tiptap/extension-color"; import { createTiptapEditorProps } from "~/modules/novel/ui/editor/props"; import { createTiptapExtensions } from "~/modules/novel/ui/editor/extensions"; import { cn } from "~/lib/utils"; import { useRootData } from "~/utils/data/useRootData"; function MenuBar({ editor, disabled }: { editor: Editor | null; disabled?: boolean }) { const { t } = useTranslation(); const headingLevels: (1 | 2 | 3)[] = [1, 2, 3]; return (
{/* Underline */} {headingLevels.map((level) => ( ))} {/* Link Button */} {/* Highlight */} {/* Color Picker Button */}
{/* Hidden native color input */} editor?.chain().focus().setColor(e.target.value).run()} /> {/* Button that triggers the hidden color input */}
); } type ValueDto = { html?: string; json?: any; text?: string; }; export interface InputRichTextEditorClientProps { content: string | any | undefined; onChange: (value: ValueDto) => void; disabled?: boolean; className?: string; dirty?: boolean; placeholder?: string; required?: boolean; style?: React.CSSProperties; } export default function InputRichTextEditorClient({ content, onChange, disabled, className, dirty, placeholder, required, style }: InputRichTextEditorClientProps) { const rootData = useRootData(); const [hydrated, setHydrated] = useState(false); const imageUploadOptions = useMemo(() => ({ apiBaseUrl: rootData.apiBaseUrl, tenantId: rootData.currentTenant?.id ?? null }), [rootData.apiBaseUrl, rootData.currentTenant?.id]); const extensions: Extensions = useMemo( () => [ ...createTiptapExtensions(imageUploadOptions), Color.configure({ types: ["textStyle"] }), // enables color ], [imageUploadOptions] ); const editorProps = useMemo(() => createTiptapEditorProps(imageUploadOptions), [imageUploadOptions]); const debouncedUpdates = useCallback( ({ editor }: { editor: Editor }) => { const json = editor.getJSON(); const html = editor.getHTML(); const text = editor.getText(); // onChange(json); onChange({ json, html, text, }); }, [onChange] ); const editor = useEditor({ immediatelyRender: false, extensions, editorProps, content, onUpdate: (e) => { debouncedUpdates(e); // const json = editor?.getJSON(); // const html = editor?.getHTML(); // onChange({ json, html }); }, }); // Hydrate the editor with the content from localStorage. useEffect(() => { if (editor && content && !hydrated) { editor.commands.setContent(content); setHydrated(true); } }, [editor, content, hydrated]); return (
{/* Mouse-only hit-area enhancement: clicking the editor padding focuses the TipTap editor. The editing surface (EditorContent) is itself keyboard focusable, so this wrapper must NOT be a focusable/role-bearing element — that would add a redundant, confusing tab stop ahead of the real editor. */} {/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
{ editor?.chain().focus().run(); }} > {disabled ? (
) : ( )}
); }