phx-react
Version:
PHX REACT
190 lines • 9.08 kB
JavaScript
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