/* eslint-disable import/no-extraneous-dependencies */ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { SELECTION_CHANGE_COMMAND, FORMAT_TEXT_COMMAND, $getSelection, $isRangeSelection, $getNodeByKey, RangeSelection, LexicalEditor, NodeSelection, GridSelection, } from 'lexical'; import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; import { $isAtNodeEnd } from '@lexical/selection'; import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils'; import { $isListNode, ListNode } from '@lexical/list'; import { createPortal } from 'react-dom'; import { $isHeadingNode } from '@lexical/rich-text'; import { $isCodeNode, getDefaultCodeLanguage, getCodeLanguages } from '@lexical/code'; import { Fab } from 'components/fab'; import { Bold, Coding, Italic, Link, StrikeThrough, UnderLine } from 'icons'; import { useTheme } from 'hooks'; import { Divider } from 'components/divider'; const LowPriority = 1; const positionEditorElement = (editor: HTMLElement, rect: DOMRect | null) => { 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`; } }; const FloatingLinkEditor: FC = ({ editor }) => { const editorRef = useRef(null); const inputRef = useRef(null); const mouseDownRef = useRef(false); const [linkUrl, setLinkUrl] = useState(''); const [isEditMode, setEditMode] = useState(false); const [lastSelection, setLastSelection] = useState< RangeSelection | NodeSelection | GridSelection | null >(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 as Node) ) { const domRange = nativeSelection?.getRangeAt(0); let rect; if (nativeSelection?.anchorNode === rootElement) { let inner = rootElement; while (inner.firstElementChild != null) { inner = inner.firstElementChild as HTMLElement; } rect = inner.getBoundingClientRect(); } else { rect = domRange?.getBoundingClientRect(); } if (!mouseDownRef.current) { positionEditorElement(editorElem, rect as DOMRect); } 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 as unknown as HTMLInputElement).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}
event.preventDefault()} onClick={() => { setEditMode(true); }} />
)}
); }; const Select: FC = ({ onChange, className, options, value }) => { return ( ); }; const getSelectedNode = (selection: RangeSelection) => { 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; } }; export const Toolbar = () => { const [editor] = useLexicalComposerContext(); const toolbarRef = useRef(null); const [blockType, setBlockType] = useState('paragraph'); const [selectedElementKey, setSelectedElementKey] = useState(null); const [codeLanguage, setCodeLanguage] = useState(''); 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 { theme } = useTheme(); const { colors } = theme; 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()); } } } // Update text format setIsBold(selection.hasFormat('bold')); setIsItalic(selection.hasFormat('italic')); setIsUnderline(selection.hasFormat('underline')); setIsStrikethrough(selection.hasFormat('strikethrough')); setIsCode(selection.hasFormat('code')); // Update links 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, (_payload, newEditor) => { updateToolbar(); return false; }, LowPriority ) ); }, [editor, updateToolbar]); const codeLanguages = 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]); const getColor = useCallback((isActive) => { return isActive ? 'base-02' : 'transparent'; }, []); return (
{blockType === 'code' ? ( <>