UNPKG

phx-react

Version:

PHX REACT

190 lines • 9.08 kB
import { $isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link'; import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { $findMatchingParent, mergeRegister } from '@lexical/utils'; import { $getSelection, $isRangeSelection, CLICK_COMMAND, COMMAND_PRIORITY_CRITICAL, COMMAND_PRIORITY_HIGH, COMMAND_PRIORITY_LOW, KEY_ESCAPE_COMMAND, SELECTION_CHANGE_COMMAND, } from 'lexical'; import { useCallback, useEffect, useRef, useState } from 'react'; import * as React from 'react'; import { createPortal } from 'react-dom'; import { getSelectedNode } from '../../utils/getSelectedNode'; import { setFloatingElemPositionForLinkEditor } from '../../utils/setFloatingElemPositionForLinkEditor'; import { sanitizeUrl } from '../../utils/url'; function FloatingLinkEditor({ anchorElem, editor, isLink, isLinkEditMode, setIsLink, setIsLinkEditMode, }) { const editorRef = useRef(null); const inputRef = useRef(null); const [linkUrl, setLinkUrl] = useState(''); const [editedLinkUrl, setEditedLinkUrl] = useState('https://'); const [lastSelection, setLastSelection] = useState(null); const updateLinkEditor = useCallback(() => { var _a, _b; const selection = $getSelection(); if ($isRangeSelection(selection)) { const node = getSelectedNode(selection); const linkParent = $findMatchingParent(node, $isLinkNode); if (linkParent) { setLinkUrl(linkParent.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 !== null && rootElement !== null && rootElement.contains(nativeSelection.anchorNode) && editor.isEditable()) { const domRect = (_b = (_a = nativeSelection.focusNode) === null || _a === void 0 ? void 0 : _a.parentElement) === null || _b === void 0 ? void 0 : _b.getBoundingClientRect(); if (domRect) { domRect.y += 40; setFloatingElemPositionForLinkEditor(domRect, editorElem, anchorElem); } setLastSelection(selection); } else if (!activeElement || activeElement.className !== 'link-input') { if (rootElement !== null) { setFloatingElemPositionForLinkEditor(null, editorElem, anchorElem); } setLastSelection(null); setIsLinkEditMode(false); setLinkUrl(''); } return true; }, [anchorElem, editor, setIsLinkEditMode]); useEffect(() => { const scrollerElem = anchorElem.parentElement; const update = () => { editor.getEditorState().read(() => { updateLinkEditor(); }); }; window.addEventListener('resize', update); if (scrollerElem) { scrollerElem.addEventListener('scroll', update); } return () => { window.removeEventListener('resize', update); if (scrollerElem) { scrollerElem.removeEventListener('scroll', update); } }; }, [anchorElem.parentElement, editor, updateLinkEditor]); useEffect(() => mergeRegister(editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateLinkEditor(); }); }), editor.registerCommand(SELECTION_CHANGE_COMMAND, () => { updateLinkEditor(); return true; }, COMMAND_PRIORITY_LOW), editor.registerCommand(KEY_ESCAPE_COMMAND, () => { if (isLink) { setIsLink(false); return true; } return false; }, COMMAND_PRIORITY_HIGH)), [editor, updateLinkEditor, setIsLink, isLink]); useEffect(() => { editor.getEditorState().read(() => { updateLinkEditor(); }); }, [editor, updateLinkEditor]); useEffect(() => { if (isLinkEditMode && inputRef.current) { inputRef.current.focus(); } }, [isLinkEditMode, isLink]); const monitorInputInteraction = (event) => { if (event.key === 'Enter') { event.preventDefault(); // eslint-disable-next-line no-use-before-define handleLinkSubmission(); } else if (event.key === 'Escape') { event.preventDefault(); setIsLinkEditMode(false); } }; const handleLinkSubmission = () => { if (lastSelection !== null) { if (linkUrl !== '') { editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl)); } setEditedLinkUrl('https://'); setIsLinkEditMode(false); } }; return (React.createElement("div", { ref: editorRef, className: 'link-editor' }, !isLink ? null : isLinkEditMode ? (React.createElement(React.Fragment, null, React.createElement("input", { ref: inputRef, className: 'link-input', onChange: (event) => { setEditedLinkUrl(event.target.value); }, onKeyDown: (event) => { monitorInputInteraction(event); }, value: editedLinkUrl }), React.createElement("div", null, React.createElement("div", { className: 'link-cancel', onClick: () => { setIsLinkEditMode(false); }, onMouseDown: (event) => event.preventDefault(), role: 'button', tabIndex: 0 }), React.createElement("div", { className: 'link-confirm', onClick: handleLinkSubmission, onMouseDown: (event) => event.preventDefault(), role: 'button', tabIndex: 0 })))) : (React.createElement("div", { className: 'link-view' }, React.createElement("a", { href: sanitizeUrl(linkUrl), rel: 'noopener noreferrer', target: '_blank' }, linkUrl), React.createElement("div", { className: 'link-edit', onClick: () => { setEditedLinkUrl(linkUrl); setIsLinkEditMode(true); }, onMouseDown: (event) => event.preventDefault(), role: 'button', tabIndex: 0 }), React.createElement("div", { className: 'link-trash', onClick: () => { editor.dispatchCommand(TOGGLE_LINK_COMMAND, null); }, onMouseDown: (event) => event.preventDefault(), role: 'button', tabIndex: 0 }))))); } function useFloatingLinkEditorToolbar(editor, anchorElem, isLinkEditMode, setIsLinkEditMode) { const [activeEditor, setActiveEditor] = useState(editor); const [isLink, setIsLink] = useState(false); useEffect(() => { function updateToolbar() { const selection = $getSelection(); if ($isRangeSelection(selection)) { const node = getSelectedNode(selection); const linkParent = $findMatchingParent(node, $isLinkNode); const autoLinkParent = $findMatchingParent(node, $isAutoLinkNode); // We don't want this menu to open for auto links. if (linkParent !== null && autoLinkParent === null) { setIsLink(true); } else { setIsLink(false); } } } return mergeRegister(editor.registerUpdateListener(({ editorState }) => { editorState.read(() => { updateToolbar(); }); }), editor.registerCommand(SELECTION_CHANGE_COMMAND, (_payload, newEditor) => { updateToolbar(); setActiveEditor(newEditor); return false; }, COMMAND_PRIORITY_CRITICAL), editor.registerCommand(CLICK_COMMAND, (payload) => { const selection = $getSelection(); if ($isRangeSelection(selection)) { const node = getSelectedNode(selection); const linkNode = $findMatchingParent(node, $isLinkNode); if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) { window.open(linkNode.getURL(), '_blank'); return true; } } return false; }, COMMAND_PRIORITY_LOW)); }, [editor]); return createPortal(React.createElement(FloatingLinkEditor, { anchorElem: anchorElem, editor: activeEditor, isLink: isLink, isLinkEditMode: isLinkEditMode, setIsLink: setIsLink, setIsLinkEditMode: setIsLinkEditMode }), anchorElem); } export default function FloatingLinkEditorPlugin({ anchorElem = document.body, isLinkEditMode, setIsLinkEditMode, }) { const [editor] = useLexicalComposerContext(); return useFloatingLinkEditorToolbar(editor, anchorElem, isLinkEditMode, setIsLinkEditMode); } //# sourceMappingURL=index.js.map