in libs/designer-ui/src/lib/editor/base/plugins/FloatingLinkEditor.tsx [34:278]
function FloatingLinkEditor({
editor,
isLink,
setIsLink,
anchorElem,
isMainEditorFocused,
}: {
editor: LexicalEditor;
isLink: boolean;
setIsLink: Dispatch<boolean>;
anchorElem: HTMLElement;
isMainEditorFocused: boolean;
}): JSX.Element {
const { isInverted } = useTheme();
const editorRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement>(null);
const [linkUrl, setLinkUrl] = useState('');
const [editedLinkUrl, setEditedLinkUrl] = useState('');
const [isEditMode, setEditMode] = useState(false);
const [lastSelection, setLastSelection] = useState<BaseSelection | null>(null);
const [showFloatingLink, setShowFloatingLink] = useState(isMainEditorFocused);
useEffect(() => {
if (isMainEditorFocused) {
setShowFloatingLink(isMainEditorFocused);
}
}, [isMainEditorFocused]);
useOutsideClick([editorRef], () => {
if (!isMainEditorFocused && showFloatingLink) {
setShowFloatingLink(false);
setEditMode(false);
}
});
useEffect(() => {
editor.registerCommand<boolean>(
TOGGLE_LINK_COMMAND,
(_) => {
setShowFloatingLink(true);
return false;
},
COMMAND_PRIORITY_CRITICAL
);
}, [editor, showFloatingLink]);
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 !== null &&
rootElement !== null &&
rootElement.contains(nativeSelection.anchorNode) &&
editor.isEditable()
) {
const domRect: DOMRect | undefined = nativeSelection.focusNode?.parentElement?.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);
setEditMode(false);
setLinkUrl('');
}
return true;
}, [anchorElem, editor]);
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(() => {
return 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 (isEditMode && inputRef.current) {
inputRef.current.focus();
}
}, [isEditMode]);
const monitorInputInteraction = (event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => {
if (event.key === 'Enter') {
event.preventDefault();
handleLinkSubmission();
} else if (event.key === 'Escape') {
event.preventDefault();
setEditMode(false);
}
};
const handleLinkSubmission = () => {
if (lastSelection !== null) {
if (linkUrl !== '') {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, sanitizeUrl(editedLinkUrl));
}
setEditMode(false);
}
};
return (
<div ref={editorRef} className={css('msla-html-link-editor', isInverted && 'inverted')}>
{!showFloatingLink || !isLink ? null : isEditMode ? (
<div className="msla-html-link-view">
<TextField
styles={{ root: { padding: '4px', width: '80%' } }}
elementRef={inputRef}
className="link-input"
value={editedLinkUrl}
onChange={(_, value) => {
setEditedLinkUrl(value ?? '');
}}
onKeyDown={(event) => {
monitorInputInteraction(event);
}}
/>
<div>
<IconButton
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
iconProps={{ iconName: 'Cancel' }}
styles={buttonStyles}
onClick={() => {
setEditMode(false);
}}
/>
<IconButton
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
iconProps={{ iconName: 'CheckMark' }}
styles={buttonStyles}
onClick={handleLinkSubmission}
/>
</div>
</div>
) : (
<div className="msla-html-link-view">
<a href={sanitizeUrl(linkUrl)} target="_blank" rel="noopener noreferrer">
{linkUrl}
</a>
<div>
<IconButton
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
iconProps={{ iconName: 'Edit' }}
styles={buttonStyles}
onClick={() => {
setEditedLinkUrl(linkUrl);
setEditMode(true);
}}
/>
<IconButton
tabIndex={0}
onMouseDown={(event) => event.preventDefault()}
iconProps={{ iconName: 'Delete' }}
styles={buttonStyles}
onClick={() => {
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
}}
/>
</div>
</div>
)}
</div>
);
}