Changes
Co-authored-by: renee-png <262607627+renee-png@users.noreply.github.com>
This commit is contained in:
co-authored by
renee-png
parent
f3c2d1fabf
commit
c7374382e1
@@ -0,0 +1,149 @@
|
||||
import { useEditor, EditorContent, Editor } from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Bold, Italic, Underline as UnderlineIcon, List, ListOrdered,
|
||||
AlignLeft, AlignCenter, AlignRight, AlignJustify, Quote, Undo, Redo,
|
||||
Heading2, Superscript as SupIcon,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface RichTextEditorProps {
|
||||
value: string; // HTML
|
||||
onChange: (html: string) => void;
|
||||
onInsertFootnote?: () => void;
|
||||
className?: string;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
function ToolbarButton({
|
||||
onClick, active, disabled, children, title,
|
||||
}: {
|
||||
onClick: () => void; active?: boolean; disabled?: boolean; children: React.ReactNode; title: string;
|
||||
}) {
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant={active ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
title={title}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export function RichTextEditor({
|
||||
value, onChange, onInsertFootnote, className, minHeight = 400,
|
||||
}: RichTextEditorProps) {
|
||||
const editor = useEditor({
|
||||
extensions: [
|
||||
StarterKit.configure({ heading: { levels: [2, 3] } }),
|
||||
Underline,
|
||||
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
||||
],
|
||||
content: value || "<p></p>",
|
||||
onUpdate: ({ editor }) => onChange(editor.getHTML()),
|
||||
editorProps: {
|
||||
attributes: {
|
||||
class: "prose prose-sm max-w-none focus:outline-none px-10 py-8 bg-white text-black",
|
||||
style: 'font-family: "Bookman Old Style", "URW Bookman", Georgia, serif; font-size: 12pt; line-height: 1.5;',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Keep editor in sync when value is reset externally
|
||||
useEffect(() => {
|
||||
if (editor && value !== editor.getHTML()) {
|
||||
editor.commands.setContent(value || "<p></p>", { emitUpdate: false });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [value === ""]);
|
||||
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className={cn("border rounded-md overflow-hidden bg-card", className)}>
|
||||
<Toolbar editor={editor} onInsertFootnote={onInsertFootnote} />
|
||||
<div className="bg-muted/20 overflow-auto" style={{ minHeight }}>
|
||||
<div className="max-w-[8.5in] mx-auto my-4 shadow-sm">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Toolbar({ editor, onInsertFootnote }: { editor: Editor; onInsertFootnote?: () => void }) {
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-0.5 px-2 py-1.5 border-b bg-muted/30">
|
||||
<ToolbarButton title="Undo" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()}>
|
||||
<Undo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Redo" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()}>
|
||||
<Redo className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Bold" active={editor.isActive("bold")} onClick={() => editor.chain().focus().toggleBold().run()}>
|
||||
<Bold className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Italic" active={editor.isActive("italic")} onClick={() => editor.chain().focus().toggleItalic().run()}>
|
||||
<Italic className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Underline" active={editor.isActive("underline")} onClick={() => editor.chain().focus().toggleUnderline().run()}>
|
||||
<UnderlineIcon className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Heading" active={editor.isActive("heading", { level: 2 })} onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Bullet list" active={editor.isActive("bulletList")} onClick={() => editor.chain().focus().toggleBulletList().run()}>
|
||||
<List className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Numbered list" active={editor.isActive("orderedList")} onClick={() => editor.chain().focus().toggleOrderedList().run()}>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Block quote" active={editor.isActive("blockquote")} onClick={() => editor.chain().focus().toggleBlockquote().run()}>
|
||||
<Quote className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
|
||||
<ToolbarButton title="Align left" active={editor.isActive({ textAlign: "left" })} onClick={() => editor.chain().focus().setTextAlign("left").run()}>
|
||||
<AlignLeft className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Center" active={editor.isActive({ textAlign: "center" })} onClick={() => editor.chain().focus().setTextAlign("center").run()}>
|
||||
<AlignCenter className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Align right" active={editor.isActive({ textAlign: "right" })} onClick={() => editor.chain().focus().setTextAlign("right").run()}>
|
||||
<AlignRight className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
<ToolbarButton title="Justify" active={editor.isActive({ textAlign: "justify" })} onClick={() => editor.chain().focus().setTextAlign("justify").run()}>
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
</ToolbarButton>
|
||||
|
||||
{onInsertFootnote && (
|
||||
<>
|
||||
<Separator orientation="vertical" className="h-6 mx-1" />
|
||||
<Button type="button" variant="ghost" size="sm" className="h-8 px-2 text-xs" onClick={onInsertFootnote} title="Insert footnote at cursor">
|
||||
<SupIcon className="h-3.5 w-3.5 mr-1" /> Footnote
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper exposed for the parent to insert a numbered footnote marker
|
||||
export function insertFootnoteMarker(editor: Editor | null, num: number) {
|
||||
if (!editor) return;
|
||||
editor.chain().focus().insertContent(`<sup>[${num}]</sup>`).run();
|
||||
}
|
||||
Reference in New Issue
Block a user