import { useRef, useState, useCallback, useEffect } from 'react'; import { SELECTION_CHANGE_COMMAND, $getSelection, $isRangeSelection } from 'lexical'; import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; import { mergeRegister } from '@lexical/utils'; // helper functions import { positionEditorElement } from '../helpers/editor'; import { getSelectedNode } from '../helpers/node'; const LowPriority = 1; export interface FloatingLinkEditorProps { editor: any; } export const FloatingLinkEditor = ({ editor }: FloatingLinkEditorProps) => { // refs const editorRef = useRef(null); const inputRef = useRef(null); const mouseDownRef = useRef(false); // states 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(); } if (!mouseDownRef.current) { 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 }: any) => { 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}
event.preventDefault()} onClick={() => { setEditMode(true); }} />
)}
); };