cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

ToolbarPlugin.tsx (26491B)


      1 import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
      2 import {
      3     useCallback,
      4     useEffect,
      5     useMemo,
      6     useRef,
      7     useState,
      8     ChangeEventHandler,
      9     ChangeEvent,
     10     MutableRefObject,
     11     Dispatch,
     12     SetStateAction,
     13     createElement,
     14     memo
     15 } from "react";
     16 import {
     17     CAN_REDO_COMMAND,
     18     CAN_UNDO_COMMAND,
     19     REDO_COMMAND,
     20     UNDO_COMMAND,
     21     SELECTION_CHANGE_COMMAND,
     22     FORMAT_TEXT_COMMAND,
     23     $getSelection,
     24     $isRangeSelection,
     25     $getNodeByKey,
     26     LexicalEditor,
     27     RangeSelection,
     28     NodeSelection,
     29     GridSelection
     30 } from "lexical";
     31 import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
     32 import {
     33     $isParentElementRTL,
     34     $wrapNodes,
     35     $isAtNodeEnd
     36 } from "@lexical/selection";
     37 import { $getNearestNodeOfType, mergeRegister } from "@lexical/utils";
     38 import {
     39     INSERT_ORDERED_LIST_COMMAND,
     40     INSERT_UNORDERED_LIST_COMMAND,
     41     REMOVE_LIST_COMMAND,
     42     $isListNode,
     43     ListNode
     44 } from "@lexical/list";
     45 import { createPortal } from "react-dom";
     46 import {
     47     $createHeadingNode,
     48     $createQuoteNode,
     49     $isHeadingNode
     50 } from "@lexical/rich-text";
     51 import {
     52     $createCodeNode,
     53     $isCodeNode,
     54     getDefaultCodeLanguage,
     55     getCodeLanguages
     56 } from "@lexical/code";
     57 import { Bold, ChevronDown, Code, Heading1, Heading2, Heading4, Heading5, Italic, Link, List, ListOrdered, Quote, Redo, Strikethrough, Text, Underline, Undo } from "lucide-react";
     58 import { Heading3 } from "lucide-react";
     59 import { $createCustomParagraphNode } from "../customNodes/CustomParagraphNode";
     60 
     61 const LowPriority = 1;
     62 
     63 const supportedBlockTypes = new Set([
     64     "custom-paragraph",
     65     "quote",
     66     "code",
     67     "h1",
     68     "h2",
     69     "ul",
     70     "ol"
     71 ]);
     72 
     73 interface BlockTypes {
     74     "code": string,
     75     h1: string,
     76     h2: string,
     77     h3: string,
     78     h4: string,
     79     h5: string,
     80     ol: string,
     81     "custom-paragraph": string,
     82     quote: string,
     83     ul: string
     84 }
     85 
     86 const blockTypeToBlockName: BlockTypes = {
     87     "code": "Code Block",
     88     h1: "Large Heading",
     89     h2: "Small Heading",
     90     h3: "Heading",
     91     h4: "Heading",
     92     h5: "Heading",
     93     ol: "Numbered List",
     94     "custom-paragraph": "Normal",
     95     quote: "Quote",
     96     ul: "Bulleted List"
     97 } as const;
     98 
     99 type BlockType = keyof typeof blockTypeToBlockName;
    100 
    101 const Divider = memo(() => {
    102     return <div className="divider" />;
    103 })
    104 
    105 function positionEditorElement(editor: HTMLDivElement, rect: DOMRect | undefined) {
    106     if (!rect) {
    107         editor.style.opacity = "0";
    108         editor.style.top = "-1000px";
    109         editor.style.left = "-1000px";
    110     } else {
    111         editor.style.opacity = "1";
    112         editor.style.top = `${rect.top + rect.height + window.pageYOffset + 10}px`;
    113         editor.style.left = `${rect.left + window.pageXOffset - editor.offsetWidth / 2 + rect.width / 2
    114             }px`;
    115     }
    116 }
    117 
    118 const FloatingLinkEditor = memo(({ editor }: { editor: LexicalEditor }) => {
    119     const editorRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
    120     const inputRef: MutableRefObject<HTMLInputElement | null> = useRef(null);
    121     const mouseDownRef = useRef(false);
    122     const [linkUrl, setLinkUrl] = useState("");
    123     const [isEditMode, setEditMode] = useState(false);
    124     const [lastSelection, setLastSelection] = useState<RangeSelection | GridSelection | NodeSelection | null>(null);
    125 
    126     const updateLinkEditor = useCallback(() => {
    127         const selection = $getSelection();
    128         if ($isRangeSelection(selection)) {
    129             const node = getSelectedNode(selection);
    130             const parent = node.getParent();
    131             if ($isLinkNode(parent)) {
    132                 setLinkUrl(parent.getURL());
    133             } else if ($isLinkNode(node)) {
    134                 setLinkUrl(node.getURL());
    135             } else {
    136                 setLinkUrl("");
    137             }
    138         }
    139         const editorElem = editorRef.current;
    140         const nativeSelection = window.getSelection();
    141         const activeElement = document.activeElement;
    142 
    143         if (editorElem === null) {
    144             return;
    145         }
    146 
    147         const rootElement = editor.getRootElement();
    148         if (
    149             selection !== null &&
    150             !nativeSelection?.isCollapsed &&
    151             rootElement !== null &&
    152             rootElement.contains(nativeSelection?.anchorNode as (Node | null))
    153         ) {
    154             const domRange = nativeSelection?.getRangeAt(0);
    155             let rect;
    156             if (nativeSelection?.anchorNode === rootElement) {
    157                 let inner = rootElement;
    158                 while (inner.firstElementChild != null) {
    159                     inner = inner.firstElementChild as HTMLElement;
    160                 }
    161                 rect = inner.getBoundingClientRect();
    162             } else {
    163                 rect = domRange?.getBoundingClientRect();
    164             }
    165 
    166             if (!mouseDownRef.current) {
    167                 positionEditorElement(editorElem, rect);
    168             }
    169             setLastSelection(selection);
    170         } else if (!activeElement || activeElement.className !== "link-input") {
    171             positionEditorElement(editorElem, undefined);
    172             setLastSelection(null);
    173             setEditMode(false);
    174             setLinkUrl("");
    175         }
    176 
    177         return true;
    178     }, [editor]);
    179 
    180     useEffect(() => {
    181         return mergeRegister(
    182             editor.registerUpdateListener(({ editorState }) => {
    183                 editorState.read(() => {
    184                     updateLinkEditor();
    185                 });
    186             }),
    187 
    188             editor.registerCommand(
    189                 SELECTION_CHANGE_COMMAND,
    190                 () => {
    191                     updateLinkEditor();
    192                     return true;
    193                 },
    194                 LowPriority
    195             )
    196         );
    197     }, [editor, updateLinkEditor]);
    198 
    199     useEffect(() => {
    200         editor.getEditorState().read(() => {
    201             updateLinkEditor();
    202         });
    203     }, [editor, updateLinkEditor]);
    204 
    205     useEffect(() => {
    206         if (isEditMode && inputRef.current) {
    207             inputRef.current.focus();
    208         }
    209     }, [isEditMode]);
    210 
    211     const safeLink = createElement("a", { href: linkUrl, target: "_blank", rel: "noopener noreferrer" }, linkUrl);
    212 
    213     return (
    214         <div ref={editorRef} className="link-editor">
    215             {isEditMode ? (
    216                 <input
    217                     ref={inputRef}
    218                     className="link-input"
    219                     value={linkUrl}
    220                     onChange={(event) => {
    221                         setLinkUrl(event.target.value);
    222                     }}
    223                     onKeyDown={(event) => {
    224                         if (event.key === "Enter") {
    225                             event.preventDefault();
    226                             if (lastSelection !== null) {
    227                                 if (linkUrl !== "") {
    228                                     editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
    229                                 }
    230                                 setEditMode(false);
    231                             }
    232                         } else if (event.key === "Escape") {
    233                             event.preventDefault();
    234                             setEditMode(false);
    235                         }
    236                     }}
    237                 />
    238             ) : (
    239                 <>
    240                     <div className="link-input">
    241                         {safeLink}
    242                         <div
    243                             className="link-edit"
    244                             role="button"
    245                             tabIndex={0}
    246                             onMouseDown={(event) => event.preventDefault()}
    247                             onClick={() => {
    248                                 setEditMode(true);
    249                             }}
    250                         />
    251                     </div>
    252                 </>
    253             )}
    254         </div>
    255     );
    256 })
    257 
    258 const Select = memo(({ onChange, className, options, value }: { onChange: ChangeEventHandler<HTMLSelectElement>, className: string, options: string[], value: string | ReadonlyArray<string> | number }) => {
    259     return (
    260         <select className={className} onChange={onChange} value={value}>
    261             <option hidden={true} value="" />
    262             {options.map((option) => (
    263                 <option key={option} value={option}>
    264                     {option}
    265                 </option>
    266             ))}
    267         </select>
    268     );
    269 })
    270 
    271 function getSelectedNode(selection: RangeSelection | GridSelection) {
    272     const anchor = selection.anchor;
    273     const focus = selection.focus;
    274     const anchorNode = selection.anchor.getNode();
    275     const focusNode = selection.focus.getNode();
    276     if (anchorNode === focusNode) {
    277         return anchorNode;
    278     }
    279     const isBackward = selection.isBackward();
    280     if (isBackward) {
    281         return $isAtNodeEnd(focus) ? anchorNode : focusNode;
    282     } else {
    283         return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
    284     }
    285 }
    286 
    287 const BlockOptionsDropdownList = memo(({
    288     editor,
    289     blockType,
    290     toolbarRef,
    291     setShowBlockOptionsDropDown
    292 }: { editor: LexicalEditor, blockType: BlockType, toolbarRef: MutableRefObject<HTMLDivElement | null>, setShowBlockOptionsDropDown: Dispatch<SetStateAction<boolean>> }) => {
    293     const dropDownRef: MutableRefObject<HTMLDivElement | null> = useRef(null);
    294 
    295     useEffect(() => {
    296         const dropDown = dropDownRef.current;
    297         const toolbar = toolbarRef.current;
    298 
    299         if (dropDown !== null && toolbar !== null) {
    300             const editorContainer = document.getElementsByClassName("editor-container")[0];
    301             if (editorContainer) {
    302                 dropDown.style.bottom = `${editorContainer.clientHeight}px`;
    303             }
    304 
    305             const handle = (event: MouseEvent) => {
    306                 const target = event.target;
    307 
    308                 if (!dropDown.contains(target as Node) && !toolbar.contains(target as Node)) {
    309                     setShowBlockOptionsDropDown(false);
    310                 }
    311             };
    312             document.addEventListener("click", handle);
    313 
    314             return () => {
    315                 document.removeEventListener("click", handle);
    316             };
    317         }
    318     }, [dropDownRef, setShowBlockOptionsDropDown, toolbarRef]);
    319 
    320     const formatParagraph = () => {
    321         if (blockType !== "custom-paragraph") {
    322             editor.update(() => {
    323                 const selection = $getSelection();
    324 
    325                 if ($isRangeSelection(selection)) {
    326                     $wrapNodes(selection, () => $createCustomParagraphNode());
    327                 }
    328             });
    329         }
    330         setShowBlockOptionsDropDown(false);
    331     };
    332 
    333     const formatLargeHeading = () => {
    334         if (blockType !== "h1") {
    335             editor.update(() => {
    336                 const selection = $getSelection();
    337 
    338                 if ($isRangeSelection(selection)) {
    339                     $wrapNodes(selection, () => $createHeadingNode("h1"));
    340                 }
    341             });
    342         }
    343         setShowBlockOptionsDropDown(false);
    344     };
    345 
    346     const formatSmallHeading = () => {
    347         if (blockType !== "h2") {
    348             editor.update(() => {
    349                 const selection = $getSelection();
    350 
    351                 if ($isRangeSelection(selection)) {
    352                     $wrapNodes(selection, () => $createHeadingNode("h2"));
    353                 }
    354             });
    355         }
    356         setShowBlockOptionsDropDown(false);
    357     };
    358 
    359     const formatBulletList = () => {
    360         if (blockType !== "ul") {
    361             editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
    362         } else {
    363             editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
    364         }
    365         setShowBlockOptionsDropDown(false);
    366     };
    367 
    368     const formatNumberedList = () => {
    369         if (blockType !== "ol") {
    370             editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
    371         } else {
    372             editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
    373         }
    374         setShowBlockOptionsDropDown(false);
    375     };
    376 
    377     const formatQuote = () => {
    378         if (blockType !== "quote") {
    379             editor.update(() => {
    380                 const selection = $getSelection();
    381 
    382                 if ($isRangeSelection(selection)) {
    383                     $wrapNodes(selection, () => $createQuoteNode());
    384                 }
    385             });
    386         }
    387         setShowBlockOptionsDropDown(false);
    388     };
    389 
    390     const formatCode = () => {
    391         if (blockType !== "code") {
    392             editor.update(() => {
    393                 const selection = $getSelection();
    394 
    395                 if ($isRangeSelection(selection)) {
    396                     $wrapNodes(selection, () => $createCodeNode());
    397                 }
    398             });
    399         }
    400         setShowBlockOptionsDropDown(false);
    401     };
    402 
    403     return (
    404         <div className="dropdown" ref={dropDownRef}>
    405             <button className="item" onClick={formatParagraph}>
    406                 <Text className="icon" size={20} />
    407                 <span className="text">Normal</span>
    408                 {blockType === "custom-paragraph" && <span className="active" />}
    409             </button>
    410             <button className="item" onClick={formatLargeHeading}>
    411                 <Heading1 className="icon" size={20} />
    412                 <span className="text">Large Heading</span>
    413                 {blockType === "h1" && <span className="active" />}
    414             </button>
    415             <button className="item" onClick={formatSmallHeading}>
    416                 <Heading2 className="icon" size={20} />
    417                 <span className="text">Small Heading</span>
    418                 {blockType === "h2" && <span className="active" />}
    419             </button>
    420             <button className="item" onClick={formatBulletList}>
    421                 <List className="icon" size={20} />
    422                 <span className="text">Bullet List</span>
    423                 {blockType === "ul" && <span className="active" />}
    424             </button>
    425             <button className="item" onClick={formatNumberedList}>
    426                 <ListOrdered className="icon" size={20} />
    427                 <span className="text">Numbered List</span>
    428                 {blockType === "ol" && <span className="active" />}
    429             </button>
    430             <button className="item" onClick={formatQuote}>
    431                 <Quote className="icon" size={20} />
    432                 <span className="text">Quote</span>
    433                 {blockType === "quote" && <span className="active" />}
    434             </button>
    435             <button className="item" onClick={formatCode}>
    436                 <Code className="icon" size={20} />
    437                 <span className="text">Code Block</span>
    438                 {blockType === "code" && <span className="active" />}
    439             </button>
    440         </div>
    441     );
    442 })
    443 
    444 const ToolbarPlugin = memo(() => {
    445     const [editor] = useLexicalComposerContext();
    446     const toolbarRef = useRef(null);
    447     const [canUndo, setCanUndo] = useState(false);
    448     const [canRedo, setCanRedo] = useState(false);
    449     const [blockType, setBlockType] = useState<BlockType>("custom-paragraph");
    450     const [selectedElementKey, setSelectedElementKey] = useState(null);
    451     const [showBlockOptionsDropDown, setShowBlockOptionsDropDown] = useState(
    452         false
    453     );
    454     const [codeLanguage, setCodeLanguage] = useState("");
    455     const [_isRTL, setIsRTL] = useState(false);
    456     const [isLink, setIsLink] = useState(false);
    457     const [isBold, setIsBold] = useState(false);
    458     const [isItalic, setIsItalic] = useState(false);
    459     const [isUnderline, setIsUnderline] = useState(false);
    460     const [isStrikethrough, setIsStrikethrough] = useState(false);
    461     const [isCode, setIsCode] = useState(false);
    462 
    463     const updateToolbar = useCallback(() => {
    464         const selection = $getSelection();
    465         if ($isRangeSelection(selection)) {
    466             const anchorNode = selection.anchor.getNode();
    467             const element =
    468                 anchorNode.getKey() === "root"
    469                     ? anchorNode
    470                     : anchorNode.getTopLevelElementOrThrow();
    471             const elementKey = element.getKey();
    472             const elementDOM = editor.getElementByKey(elementKey);
    473             if (elementDOM !== null) {
    474                 setSelectedElementKey(elementKey);
    475                 if ($isListNode(element)) {
    476                     const parentList = $getNearestNodeOfType(anchorNode, ListNode);
    477                     const type = parentList ? parentList.getTag() : element.getTag();
    478                     setBlockType(type);
    479                 } else {
    480                     const type = $isHeadingNode(element)
    481                         ? element.getTag()
    482                         : element.getType();
    483                     setBlockType(type);
    484                     if ($isCodeNode(element)) {
    485                         setCodeLanguage(element.getLanguage() || getDefaultCodeLanguage());
    486                     }
    487                 }
    488             }
    489             // Update text format
    490             setIsBold(selection.hasFormat("bold"));
    491             setIsItalic(selection.hasFormat("italic"));
    492             setIsUnderline(selection.hasFormat("underline"));
    493             setIsStrikethrough(selection.hasFormat("strikethrough"));
    494             setIsCode(selection.hasFormat("code"));
    495             setIsRTL($isParentElementRTL(selection));
    496 
    497             // Update links
    498             const node = getSelectedNode(selection);
    499             const parent = node.getParent();
    500             if ($isLinkNode(parent) || $isLinkNode(node)) {
    501                 setIsLink(true);
    502             } else {
    503                 setIsLink(false);
    504             }
    505         }
    506     }, [editor]);
    507 
    508     useEffect(() => {
    509         return mergeRegister(
    510             editor.registerUpdateListener(({ editorState }) => {
    511                 editorState.read(() => {
    512                     updateToolbar();
    513                 });
    514             }),
    515             editor.registerCommand(
    516                 SELECTION_CHANGE_COMMAND,
    517                 (_payload, _newEditor) => {
    518                     updateToolbar();
    519                     return false;
    520                 },
    521                 LowPriority
    522             ),
    523             editor.registerCommand(
    524                 CAN_UNDO_COMMAND,
    525                 (payload) => {
    526                     setCanUndo(payload);
    527                     return false;
    528                 },
    529                 LowPriority
    530             ),
    531             editor.registerCommand(
    532                 CAN_REDO_COMMAND,
    533                 (payload) => {
    534                     setCanRedo(payload);
    535                     return false;
    536                 },
    537                 LowPriority
    538             )
    539         );
    540     }, [editor, updateToolbar]);
    541 
    542     const codeLanguges = useMemo(() => getCodeLanguages(), []);
    543     const onCodeLanguageSelect = useCallback(
    544         (e: ChangeEvent<HTMLSelectElement>) => {
    545             editor.update(() => {
    546                 if (selectedElementKey !== null) {
    547                     const node = $getNodeByKey(selectedElementKey);
    548                     if ($isCodeNode(node)) {
    549                         node.setLanguage(e.target.value);
    550                     }
    551                 }
    552             });
    553         },
    554         [editor, selectedElementKey]
    555     );
    556 
    557     const insertLink = useCallback(() => {
    558         if (!isLink) {
    559             editor.dispatchCommand(TOGGLE_LINK_COMMAND, "https://");
    560         } else {
    561             editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
    562         }
    563     }, [editor, isLink]);
    564 
    565     const [portalContainer, setPortalContainer] = useState<HTMLDivElement | null>(null);
    566     useEffect(() => {
    567         const container = document.getElementsByClassName("room-wrapper")[0];
    568         const portalContainer = document.createElement('div');
    569         container?.prepend(portalContainer)
    570         setPortalContainer(portalContainer)
    571         return () => {
    572             container?.removeChild(portalContainer)
    573         }
    574     }, [])
    575 
    576     return (
    577         <div className="toolbar" ref={toolbarRef}>
    578             <button
    579                 disabled={!canUndo}
    580                 onClick={() => {
    581                     editor.dispatchCommand(UNDO_COMMAND, undefined);
    582                 }}
    583                 className="toolbar-item spaced"
    584                 aria-label="Undo"
    585             >
    586                 <Undo className="format" size={20} />
    587             </button>
    588             <button
    589                 disabled={!canRedo}
    590                 onClick={() => {
    591                     editor.dispatchCommand(REDO_COMMAND, undefined);
    592                 }}
    593                 className="toolbar-item"
    594                 aria-label="Redo"
    595             >
    596                 <Redo className="format" size={20} />
    597             </button>
    598             <Divider />
    599             {supportedBlockTypes.has(blockType) && (
    600                 <>
    601                     <button
    602                         className="toolbar-item block-controls"
    603                         onClick={() =>
    604                             setShowBlockOptionsDropDown(!showBlockOptionsDropDown)
    605                         }
    606                         aria-label="Formatting Options"
    607                     >
    608                         {
    609                             blockType === "h1" ?
    610                                 <Heading1 className="icon" size={20} /> :
    611                                 blockType === "h2" ?
    612                                     <Heading2 className="icon" size={20} /> :
    613                                     blockType === "h3" ?
    614                                         <Heading3 className="icon" size={20} /> :
    615                                         blockType === "h4" ?
    616                                             <Heading4 className="icon" size={20} /> :
    617                                             blockType === "h5" ?
    618                                                 <Heading5 className="icon" size={20} /> :
    619                                                 blockType === "code" ?
    620                                                     <Code className="icon" size={20} /> :
    621                                                     blockType === "custom-paragraph" ?
    622                                                         <Text className="icon" size={20} /> :
    623                                                         blockType === "ol" ?
    624                                                             <ListOrdered className="icon" size={20} /> :
    625                                                             blockType === "ul" ?
    626                                                                 <List className="icon" size={20} /> :
    627                                                                 blockType === "quote" ?
    628                                                                     <Quote className="icon" size={20} /> :
    629                                                                     <></>
    630                         }
    631                         <span className="text">{blockTypeToBlockName[blockType]}</span>
    632                         <ChevronDown size={20} />
    633                     </button>
    634                     {showBlockOptionsDropDown &&
    635                         createPortal(
    636                             <BlockOptionsDropdownList
    637                                 editor={editor}
    638                                 blockType={blockType}
    639                                 toolbarRef={toolbarRef}
    640                                 setShowBlockOptionsDropDown={setShowBlockOptionsDropDown}
    641                             />,
    642                             portalContainer!!
    643                         )}
    644                     <Divider />
    645                 </>
    646             )}
    647             {blockType === "code" ? (
    648                 <>
    649                     <Select
    650                         className="toolbar-item code-language"
    651                         onChange={onCodeLanguageSelect}
    652                         options={codeLanguges}
    653                         value={codeLanguage}
    654                     />
    655                     <i className="chevron-down inside" />
    656                 </>
    657             ) : (
    658                 <>
    659                     <button
    660                         onClick={() => {
    661                             editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
    662                         }}
    663                         className={"toolbar-item spaced " + (isBold ? "active" : "")}
    664                         aria-label="Format Bold"
    665                     >
    666                         <Bold className="format" size={20} />
    667                     </button>
    668                     <button
    669                         onClick={() => {
    670                             editor.dispatchCommand(FORMAT_TEXT_COMMAND, "italic");
    671                         }}
    672                         className={"toolbar-item spaced " + (isItalic ? "active" : "")}
    673                         aria-label="Format Italics"
    674                     >
    675                         <Italic className="format" size={20} />
    676                     </button>
    677                     <button
    678                         onClick={() => {
    679                             editor.dispatchCommand(FORMAT_TEXT_COMMAND, "underline");
    680                         }}
    681                         className={"toolbar-item spaced " + (isUnderline ? "active" : "")}
    682                         aria-label="Format Underline"
    683                     >
    684                         <Underline className="format" size={20} />
    685                     </button>
    686                     <button
    687                         onClick={() => {
    688                             editor.dispatchCommand(FORMAT_TEXT_COMMAND, "strikethrough");
    689                         }}
    690                         className={
    691                             "toolbar-item spaced " + (isStrikethrough ? "active" : "")
    692                         }
    693                         aria-label="Format Strikethrough"
    694                     >
    695                         <Strikethrough className="format" size={20} />
    696                     </button>
    697                     <button
    698                         onClick={() => {
    699                             editor.dispatchCommand(FORMAT_TEXT_COMMAND, "code");
    700                         }}
    701                         className={"toolbar-item spaced " + (isCode ? "active" : "")}
    702                         aria-label="Insert Code"
    703                     >
    704                         <Code className="format" size={20} />
    705                     </button>
    706                     <button
    707                         onClick={insertLink}
    708                         className={"toolbar-item spaced " + (isLink ? "active" : "")}
    709                         aria-label="Insert Link"
    710                     >
    711                         <Link className="format" size={20} />
    712                     </button>
    713                     {isLink &&
    714                         createPortal(<FloatingLinkEditor editor={editor} />, document.body)}
    715                     {" "}
    716                 </>
    717             )}
    718         </div>
    719     );
    720 })
    721 
    722 export default ToolbarPlugin;