/** * See https://codesandbox.io/p/sandbox/vigilant-kate-5tncvy?file=%2Fsrc%2Fplugins%2FToolbarPlugin.js */ import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CAN_REDO_COMMAND, CAN_UNDO_COMMAND, REDO_COMMAND, UNDO_COMMAND, SELECTION_CHANGE_COMMAND, FORMAT_TEXT_COMMAND, FORMAT_ELEMENT_COMMAND, $getSelection, $isRangeSelection, $createParagraphNode, $getNodeByKey, } from "lexical"; import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link"; import { $isParentElementRTL, $wrapNodes, $isAtNodeEnd, } from "@lexical/selection"; import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils"; import { INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND, $isListNode, ListNode, } from "@lexical/list"; import { createPortal } from "react-dom"; import { $createHeadingNode, $createQuoteNode, $isHeadingNode, } from "@lexical/rich-text"; import { $createCodeNode, $isCodeNode, getDefaultCodeLanguage, getCodeLanguages, } from "@lexical/code"; import { Dropdown } from "@douyinfe/semi-ui"; import "./styles/index.css"; const LowPriority = 1; const blockTypeToIcon = { code: "bi-code-slash", h1: "bi-type-h1", h2: "bi-type-h2", ol: "bi-list-ol", paragraph: "bi-text-paragraph", quote: "bi-chat-square-quote", ul: "bi-list-ul", }; const blockTypeToBlockName = { paragraph: "Paragraph", h1: "Large Heading", h2: "Small Heading", ul: "Bulleted List", ol: "Numbered List", code: "Code Block", quote: "Quote", }; function Divider() { return
; } function positionEditorElement(editor, rect) { if (rect === null) { editor.style.opacity = "0"; editor.style.top = "-1000px"; editor.style.left = "-1000px"; } else { editor.style.opacity = "1"; editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`; editor.style.left = `${ rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2 }px`; } } function FloatingLinkEditor({ editor }) { const editorRef = useRef(null); const inputRef = useRef(null); const [linkUrl, setLinkUrl] = useState(""); const [isEditMode, setEditMode] = useState(false); const [lastSelection, setLastSelection] = useState(null); const updateLinkEditor = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const node = getSelectedNode(selection); const parent = node.getParent(); if ($isLinkNode(parent)) { setLinkUrl(parent.getURL()); } else if ($isLinkNode(node)) { setLinkUrl(node.getURL()); } else { setLinkUrl(""); } } const editorElem = editorRef.current; const nativeSelection = window.getSelection(); const activeElement = document.activeElement; if (editorElem === null) { return; } const rootElement = editor.getRootElement(); if ( selection !== null && !nativeSelection.isCollapsed && rootElement !== null && rootElement.contains(nativeSelection.anchorNode) ) { const domRange = nativeSelection.getRangeAt(0); let rect; if (nativeSelection.anchorNode === rootElement) { let inner = rootElement; while (inner.firstElementChild != null) { inner = inner.firstElementChild; } rect = inner.getBoundingClientRect(); } else { rect = domRange.getBoundingClientRect(); } positionEditorElement(editorElem, rect); setLastSelection(selection); } else if (!activeElement || activeElement.className !== "link-input") { positionEditorElement(editorElem, null); setLastSelection(null); setEditMode(false); setLinkUrl(""); } return true; }, [editor]); useEffect(() => { return mergeRegister( editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateLinkEditor(); }); }), editor.registerCommand( SELECTION_CHANGE_COMMAND, () => { updateLinkEditor(); return true; }, LowPriority ) ); }, [editor, updateLinkEditor]); useEffect(() => { editor.getEditorState().read(() => { updateLinkEditor(); }); }, [editor, updateLinkEditor]); useEffect(() => { if (isEditMode && inputRef.current) { inputRef.current.focus(); } }, [isEditMode]); return (
{isEditMode ? ( { setLinkUrl(event.target.value); }} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); if (lastSelection !== null) { if (linkUrl !== "") { editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl); } setEditMode(false); } } else if (event.key === "Escape") { event.preventDefault(); setEditMode(false); } }} /> ) : ( <>
{linkUrl}
)}
); } function Select({ onChange, className, options, value }) { return ( ); } function getSelectedNode(selection) { const anchor = selection.anchor; const focus = selection.focus; const anchorNode = selection.anchor.getNode(); const focusNode = selection.focus.getNode(); if (anchorNode === focusNode) { return anchorNode; } const isBackward = selection.isBackward(); if (isBackward) { return $isAtNodeEnd(focus) ? anchorNode : focusNode; } else { return $isAtNodeEnd(anchor) ? focusNode : anchorNode; } } function BlockOptionsDropdownList({ editor, blockType }) { const formatParagraph = () => { if (blockType !== "paragraph") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createParagraphNode()); } }); } }; const formatLargeHeading = () => { if (blockType !== "h1") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h1")); } }); } }; const formatSmallHeading = () => { if (blockType !== "h2") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createHeadingNode("h2")); } }); } }; const formatBulletList = () => { if (blockType !== "ul") { editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND); } }; const formatNumberedList = () => { if (blockType !== "ol") { editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND); } else { editor.dispatchCommand(REMOVE_LIST_COMMAND); } }; const formatQuote = () => { if (blockType !== "quote") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createQuoteNode()); } }); } }; const formatCode = () => { if (blockType !== "code") { editor.update(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { $wrapNodes(selection, () => $createCodeNode()); } }); } }; return ( } > Paragraph } > Large Heading } > Small Heading } > Bullet List } > Numbered List } > Quote } > Code Block } > ); } export default function ToolbarPlugin() { const [editor] = useLexicalComposerContext(); const toolbarRef = useRef(null); const [canUndo, setCanUndo] = useState(false); const [canRedo, setCanRedo] = useState(false); const [blockType, setBlockType] = useState("paragraph"); const [selectedElementKey, setSelectedElementKey] = useState(null); const [codeLanguage, setCodeLanguage] = useState(""); const [, setIsRTL] = useState(false); const [isLink, setIsLink] = useState(false); const [isBold, setIsBold] = useState(false); const [isItalic, setIsItalic] = useState(false); const [isUnderline, setIsUnderline] = useState(false); const [isStrikethrough, setIsStrikethrough] = useState(false); const [isCode, setIsCode] = useState(false); const updateToolbar = useCallback(() => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const anchorNode = selection.anchor.getNode(); const element = anchorNode.getKey() === "root" ? anchorNode : anchorNode.getTopLevelElementOrThrow(); const elementKey = element.getKey(); const elementDOM = editor.getElementByKey(elementKey); if (elementDOM !== null) { setSelectedElementKey(elementKey); if ($isListNode(element)) { const parentList = $getNearestNodeOfType(anchorNode, ListNode); const type = parentList ? parentList.getTag() : element.getTag(); setBlockType(type); } else { const type = $isHeadingNode(element) ? element.getTag() : element.getType(); setBlockType(type); if ($isCodeNode(element)) { setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage()); } } } setIsBold(selection.hasFormat("bold")); setIsItalic(selection.hasFormat("italic")); setIsUnderline(selection.hasFormat("underline")); setIsStrikethrough(selection.hasFormat("strikethrough")); setIsCode(selection.hasFormat("code")); setIsRTL($isParentElementRTL(selection)); const node = getSelectedNode(selection); const parent = node.getParent(); if ($isLinkNode(parent) || $isLinkNode(node)) { setIsLink(true); } else { setIsLink(false); } } }, [editor]); useEffect(() => { return mergeRegister( editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateToolbar(); }); }), editor.registerCommand( SELECTION_CHANGE_COMMAND, () => { updateToolbar(); return false; }, LowPriority ), editor.registerCommand( CAN_UNDO_COMMAND, (payload) => { setCanUndo(payload); return false; }, LowPriority ), editor.registerCommand( CAN_REDO_COMMAND, (payload) => { setCanRedo(payload); return false; }, LowPriority ) ); }, [editor, updateToolbar]); const codeLanguges = useMemo(() => getCodeLanguages(), []); const onCodeLanguageSelect = useCallback( (e) => { editor.update(() => { if (selectedElementKey !== null) { const node = $getNodeByKey(selectedElementKey); if ($isCodeNode(node)) { node.setLanguage(e.target.value); } } }); }, [editor, selectedElementKey] ); const insertLink = useCallback(() => { if (!isLink) { editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://"); } else { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); } }, [editor, isLink]); return (
{blockType === "code" ? (