import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { $findMatchingParent, $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; import clsx from "clsx"; import { $getSelection, $isElementNode, $isRangeSelection, $isRootOrShadowRoot, CAN_REDO_COMMAND, CAN_UNDO_COMMAND, COMMAND_PRIORITY_CRITICAL, FORMAT_TEXT_COMMAND, REDO_COMMAND, SELECTION_CHANGE_COMMAND, UNDO_COMMAND, } from "lexical"; import { INSERT_HORIZONTAL_RULE_COMMAND } from "@lexical/react/LexicalHorizontalRuleNode"; import type { ButtonProps } from "../../../button"; import { Button } from "../../../button"; import type { Dispatch } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslate } from "../../../i18n"; import { focusNextElement } from "../utils/dom"; import type { ToolbarVariantProps } from "../style"; import { toolbarVariants } from "../style"; import type { BlockType } from "../ToolbarContext"; import { blockTypes, useToolbarState } from "../ToolbarContext"; import { getSelectedNode } from "../utils/getSelectedNode"; import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; import { $isListNode, ListNode } from "@lexical/list"; import { $isHeadingNode } from "@lexical/rich-text"; import { BlockFormatDropdown } from "../components/BlockFormatDropdown"; import { ElementFormatDropdown } from "../components/ElementFormatDropdown"; function Divider() { return
; } const buttonBaseProps: ( label: string | undefined, selected: boolean, isFocusable: boolean, ) => ButtonProps = (label, selected, isFocusable) => ({ "aria-label": label, selected, tabIndex: isFocusable ? 0 : -1, color: "gray", variant: "text", icon: true, type: "button", }); interface Props extends ToolbarVariantProps { className?: string; setIsLinkEditMode: Dispatch; extendedToolbar?: boolean; } export function ToolbarPlugin({ extendedToolbar, className, sticky, visible, setIsLinkEditMode, }: Props) { const [editor] = useLexicalComposerContext(); const toolbarRef = useRef(null); const [isFocusable, setIsFocusable] = useState(false); const translate = useTranslate(); const { toolbarState, updateToolbarState } = useToolbarState(); const $updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); let element = anchorNode.getKey() === "root" ? anchorNode : $findMatchingParent(anchorNode, (e) => { const parent = e.getParent(); return parent !== null && $isRootOrShadowRoot(parent); }); if (element === null) { element = anchorNode.getTopLevelElementOrThrow(); } const elementKey = element.getKey(); const elementDOM = editor.getElementByKey(elementKey); const node = getSelectedNode(selection); const parent = node.getParent(); const isLink = $isLinkNode(parent) || $isLinkNode(node); updateToolbarState("isLink", isLink); if (elementDOM !== null) { if ($isListNode(element)) { const parentList = $getNearestNodeOfType(anchorNode, ListNode); const type = parentList ? parentList.getListType() : element.getListType(); updateToolbarState("blockType", type); } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); if (blockTypes.includes(type as BlockType)) { updateToolbarState("blockType", type as BlockType); } } } let matchingParent; if ($isLinkNode(parent)) { // If node is a link, we need to fetch the parent paragraph node to set format matchingParent = $findMatchingParent( node, (parentNode) => $isElementNode(parentNode) && !parentNode.isInline(), ); } updateToolbarState( "elementFormat", $isElementNode(matchingParent) ? matchingParent.getFormatType() : $isElementNode(node) ? node.getFormatType() : parent?.getFormatType() || "left", ); updateToolbarState("isBold", selection.hasFormat("bold")); updateToolbarState("isItalic", selection.hasFormat("italic")); updateToolbarState("isUnderline", selection.hasFormat("underline")); } }, [editor, updateToolbarState]); useEffect(() => { return mergeRegister( editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { $updateToolbar(); }); }), editor.registerCommand( SELECTION_CHANGE_COMMAND, (_payload, _newEditor) => { if (_newEditor !== editor) { console.error("_newEditor is different !!"); } $updateToolbar(); return false; }, COMMAND_PRIORITY_CRITICAL, ), editor.registerCommand( CAN_UNDO_COMMAND, (payload) => { updateToolbarState("canUndo", payload); return false; }, COMMAND_PRIORITY_CRITICAL, ), editor.registerCommand( CAN_REDO_COMMAND, (payload) => { updateToolbarState("canRedo", payload); return false; }, COMMAND_PRIORITY_CRITICAL, ), ); }, [editor, $updateToolbar, updateToolbarState]); const insertLink = useCallback(() => { if (!toolbarState.isLink) { setIsLinkEditMode(true); editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); } else { setIsLinkEditMode(false); editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } }, [toolbarState.isLink, setIsLinkEditMode, editor]); return (
{extendedToolbar && ( <> {blockTypes.includes(toolbarState.blockType) && ( )} )} {extendedToolbar && ( )}
); }