"use client"; import { useEffect, useState } from "react"; import { $createParagraphNode, $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, FORMAT_TEXT_COMMAND, mergeRegister, SELECTION_CHANGE_COMMAND, type LexicalEditor, type LexicalNode, type RangeSelection, } from "lexical"; import { $createHeadingNode, $createQuoteNode, $isHeadingNode, $isQuoteNode, } from "@lexical/rich-text"; import { $setBlocksType } from "@lexical/selection"; import { $isListNode, INSERT_CHECK_LIST_COMMAND, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, type ListType, } from "@lexical/list"; import { Bold, Code2, Italic, List, ListChecks, ListOrdered, Minus, Quote, Strikethrough, Underline, type LucideIcon, } from "lucide-react"; import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/extension"; import { Separator } from "@/components/ui/separator"; import { LexicalRichTextToolbarButton } from "./LexicalRichTextToolbarButton"; type BlockType = "paragraph" | "h2" | "h3" | "h4" | "quote"; interface ToolbarState { bold: boolean; italic: boolean; underline: boolean; strikethrough: boolean; code: boolean; blockType: BlockType; listType: ListType | null; } const DEFAULT_TOOLBAR_STATE: ToolbarState = { blockType: "paragraph", bold: false, code: false, italic: false, listType: null, strikethrough: false, underline: false, }; function getBlockType(selection: RangeSelection): BlockType { const topLevelElement = selection.anchor.getNode().getTopLevelElement(); if ($isHeadingNode(topLevelElement)) { const tag = topLevelElement.getTag(); if (tag === "h2" || tag === "h3" || tag === "h4") return tag; } if ($isQuoteNode(topLevelElement)) return "quote"; return "paragraph"; } function getListType(selection: RangeSelection): ListType | null { let node: LexicalNode | null = selection.anchor.getNode(); while (node) { if ($isListNode(node)) return node.getListType(); node = node.getParent(); } return null; } function readToolbarState(editor: LexicalEditor): ToolbarState { let state = DEFAULT_TOOLBAR_STATE; editor.getEditorState().read(() => { const selection = $getSelection(); if (!$isRangeSelection(selection)) return; state = { blockType: getBlockType(selection), bold: selection.hasFormat("bold"), code: selection.hasFormat("code"), italic: selection.hasFormat("italic"), listType: getListType(selection), strikethrough: selection.hasFormat("strikethrough"), underline: selection.hasFormat("underline"), }; }); return state; } export function LexicalRichTextToolbar({ disabled }: { disabled: boolean }) { const [editor] = useLexicalComposerContext(); const [state, setState] = useState(DEFAULT_TOOLBAR_STATE); useEffect(() => { const update = () => setState(readToolbarState(editor)); update(); return mergeRegister( editor.registerUpdateListener(update), editor.registerCommand( SELECTION_CHANGE_COMMAND, () => { update(); return false; }, COMMAND_PRIORITY_LOW, ), ); }, [editor]); function formatText(format: "bold" | "italic" | "underline" | "strikethrough" | "code") { editor.dispatchCommand(FORMAT_TEXT_COMMAND, format); } function setBlockType(type: BlockType) { editor.update(() => { const selection = $getSelection(); if (!$isRangeSelection(selection)) return; const nextType = state.blockType === type && type !== "paragraph" ? "paragraph" : type; if (nextType === "paragraph") { $setBlocksType(selection, () => $createParagraphNode()); } else if (nextType === "quote") { $setBlocksType(selection, () => $createQuoteNode()); } else { $setBlocksType(selection, () => $createHeadingNode(nextType)); } }); } const inlineButtons: { label: string; icon: LucideIcon; active: boolean; onClick: () => void; }[] = [ { label: "Bold", icon: Bold, active: state.bold, onClick: () => formatText("bold") }, { label: "Italic", icon: Italic, active: state.italic, onClick: () => formatText("italic") }, { label: "Underline", icon: Underline, active: state.underline, onClick: () => formatText("underline"), }, { label: "Strikethrough", icon: Strikethrough, active: state.strikethrough, onClick: () => formatText("strikethrough"), }, { label: "Inline code", icon: Code2, active: state.code, onClick: () => formatText("code") }, ]; const blockButtons: { label: string; icon: LucideIcon; active: boolean; pressed?: boolean; onClick: () => void; }[] = [ { label: "Bulleted list", icon: List, active: state.listType === "bullet", onClick: () => editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined), }, { label: "Numbered list", icon: ListOrdered, active: state.listType === "number", onClick: () => editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined), }, { label: "Checklist", icon: ListChecks, active: state.listType === "check", onClick: () => editor.dispatchCommand(INSERT_CHECK_LIST_COMMAND, undefined), }, { label: "Blockquote", icon: Quote, active: state.blockType === "quote", onClick: () => setBlockType("quote"), }, { label: "Horizontal rule", icon: Minus, active: false, pressed: false, onClick: () => editor.dispatchCommand(INSERT_HORIZONTAL_RULE_COMMAND, undefined), }, ]; return (