mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
157 lines
4.3 KiB
TypeScript
157 lines
4.3 KiB
TypeScript
|
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<any>(null);
|
||
|
const inputRef = useRef<any>(null);
|
||
|
const mouseDownRef = useRef(false);
|
||
|
// states
|
||
|
const [linkUrl, setLinkUrl] = useState('');
|
||
|
const [isEditMode, setEditMode] = useState(false);
|
||
|
const [lastSelection, setLastSelection] = useState<any>(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 (
|
||
|
<div ref={editorRef} className="link-editor">
|
||
|
{isEditMode ? (
|
||
|
<input
|
||
|
ref={inputRef}
|
||
|
className="link-input"
|
||
|
value={linkUrl}
|
||
|
onChange={(event) => {
|
||
|
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);
|
||
|
}
|
||
|
}}
|
||
|
/>
|
||
|
) : (
|
||
|
<>
|
||
|
<div className="link-input">
|
||
|
<a href={linkUrl} target="_blank" rel="noopener noreferrer">
|
||
|
{linkUrl}
|
||
|
</a>
|
||
|
<div
|
||
|
className="link-edit"
|
||
|
role="button"
|
||
|
tabIndex={0}
|
||
|
onMouseDown={(event) => event.preventDefault()}
|
||
|
onClick={() => {
|
||
|
setEditMode(true);
|
||
|
}}
|
||
|
/>
|
||
|
</div>
|
||
|
</>
|
||
|
)}
|
||
|
</div>
|
||
|
);
|
||
|
};
|