refactor: inner blocks note links (#1973)
This commit is contained in:
@@ -25,6 +25,9 @@ import YouTubePlugin from '../Lexical/Plugins/YouTubePlugin';
|
||||
import AutoEmbedPlugin from '../Lexical/Plugins/AutoEmbedPlugin';
|
||||
import CollapsiblePlugin from '../Lexical/Plugins/CollapsiblePlugin';
|
||||
import DraggableBlockPlugin from '../Lexical/Plugins/DraggableBlockPlugin';
|
||||
import CodeHighlightPlugin from '../Lexical/Plugins/CodeHighlightPlugin';
|
||||
import FloatingTextFormatToolbarPlugin from '../Lexical/Plugins/FloatingTextFormatToolbarPlugin';
|
||||
import FloatingLinkEditorPlugin from '../Lexical/Plugins/FloatingLinkEditorPlugin';
|
||||
|
||||
const BlockDragEnabled = false;
|
||||
|
||||
@@ -88,12 +91,20 @@ export const BlocksEditor: FunctionComponent<BlocksEditorProps> = ({
|
||||
<AutoFocusPlugin />
|
||||
<ClearEditorPlugin />
|
||||
<CheckListPlugin />
|
||||
<CodeHighlightPlugin />
|
||||
<LinkPlugin />
|
||||
<HashtagPlugin />
|
||||
|
||||
<AutoEmbedPlugin />
|
||||
<TwitterPlugin />
|
||||
<YouTubePlugin />
|
||||
<CollapsiblePlugin />
|
||||
{floatingAnchorElem && (
|
||||
<>
|
||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||
<FloatingLinkEditorPlugin />
|
||||
</>
|
||||
)}
|
||||
{floatingAnchorElem && BlockDragEnabled && (
|
||||
<>{<DraggableBlockPlugin anchorElem={floatingAnchorElem} />}</>
|
||||
)}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const classNames = (...values: (string | boolean | undefined)[]): string => {
|
||||
return values
|
||||
.map((value) => (typeof value === 'string' ? value : null))
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
export const PopoverClassNames = classNames(
|
||||
'typeahead-popover file-picker-menu absolute z-dropdown-menu flex w-full min-w-80',
|
||||
'cursor-auto flex-col overflow-y-auto rounded bg-default md:h-auto md:max-w-xs h-auto overflow-y-scroll',
|
||||
);
|
||||
|
||||
export const PopoverItemClassNames = classNames(
|
||||
'flex w-full items-center text-base gap-4 overflow-hidden py-2 px-3 hover:bg-contrast hover:text-foreground',
|
||||
'focus:bg-info-backdrop cursor-pointer m-0 focus:bg-contrast focus:text-foreground',
|
||||
);
|
||||
|
||||
export const PopoverItemSelectedClassNames = classNames(
|
||||
'bg-contrast text-foreground',
|
||||
);
|
||||
@@ -1,5 +0,0 @@
|
||||
import {createCommand, LexicalCommand} from 'lexical';
|
||||
|
||||
export const INSERT_FILE_COMMAND: LexicalCommand<string> = createCommand(
|
||||
'INSERT_FILE_COMMAND',
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import {registerCodeHighlighting} from '@lexical/code';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {useEffect} from 'react';
|
||||
|
||||
export default function CodeHighlightPlugin(): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
|
||||
useEffect(() => {
|
||||
return registerCodeHighlighting(editor);
|
||||
}, [editor]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -8,8 +8,8 @@
|
||||
*/
|
||||
|
||||
.Collapsible__container {
|
||||
background: #fcfcfc;
|
||||
border: 1px solid #eee;
|
||||
background: var(--sn-stylekit-contrast-background-color);
|
||||
border: 1px solid var(--sn-stylekit-contrast-border-color);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -44,7 +44,7 @@
|
||||
.Collapsible__container[open] .Collapsible__title:before {
|
||||
border-color: transparent;
|
||||
border-width: 6px 4px 0 4px;
|
||||
border-top-color: #000;
|
||||
border-top-color: var(--sn-stylekit-contrast-color);
|
||||
}
|
||||
|
||||
.Collapsible__content {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
.link-editor {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
opacity: 0;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
transition: opacity 0.5s;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.link-editor .button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
padding: 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.link-editor .button.hovered {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
display: inline-block;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.link-editor .button i,
|
||||
.actions i {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
vertical-align: -0.25em;
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import './index.css';
|
||||
|
||||
import {$isAutoLinkNode, $isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {$findMatchingParent, mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
GridSelection,
|
||||
LexicalEditor,
|
||||
NodeSelection,
|
||||
RangeSelection,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
import LinkPreview from '../../UI/LinkPreview';
|
||||
import {getSelectedNode} from '../../Utils/getSelectedNode';
|
||||
import {sanitizeUrl} from '../../Utils/sanitizeUrl';
|
||||
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
|
||||
|
||||
function FloatingLinkEditor({
|
||||
editor,
|
||||
anchorElem,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
}): JSX.Element {
|
||||
const editorRef = useRef<HTMLDivElement | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [isEditMode, setEditMode] = useState(false);
|
||||
const [lastSelection, setLastSelection] = useState<
|
||||
RangeSelection | GridSelection | NodeSelection | null
|
||||
>(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 !== null &&
|
||||
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 as HTMLElement;
|
||||
}
|
||||
rect = inner.getBoundingClientRect();
|
||||
} else {
|
||||
rect = domRange.getBoundingClientRect();
|
||||
}
|
||||
|
||||
setFloatingElemPosition(rect, editorElem, anchorElem);
|
||||
setLastSelection(selection);
|
||||
} else if (!activeElement || activeElement.className !== 'link-input') {
|
||||
if (rootElement !== null) {
|
||||
setFloatingElemPosition(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, 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,
|
||||
sanitizeUrl(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>
|
||||
<LinkPreview url={linkUrl} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useFloatingLinkEditorToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null {
|
||||
const [activeEditor, setActiveEditor] = useState(editor);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
(_payload, newEditor) => {
|
||||
updateToolbar();
|
||||
setActiveEditor(newEditor);
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_CRITICAL,
|
||||
);
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
return isLink
|
||||
? createPortal(
|
||||
<FloatingLinkEditor editor={activeEditor} anchorElem={anchorElem} />,
|
||||
anchorElem,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
export default function FloatingLinkEditorPlugin({
|
||||
anchorElem = document.body,
|
||||
}: {
|
||||
anchorElem?: HTMLElement;
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return useFloatingLinkEditorToolbar(editor, anchorElem);
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
.floating-text-format-popup {
|
||||
display: flex;
|
||||
vertical-align: middle;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
opacity: 0;
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
color: var(--sn-stylekit-contrast-color);
|
||||
box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||
border-radius: 8px;
|
||||
transition: opacity 0.5s;
|
||||
height: 35px;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item.spaced {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item i.format {
|
||||
background-size: contain;
|
||||
display: inline-block;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
margin-top: 2px;
|
||||
vertical-align: -0.25em;
|
||||
display: flex;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item:disabled i.format {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item.active {
|
||||
background-color: rgba(223, 232, 250, 0.3);
|
||||
}
|
||||
|
||||
.floating-text-format-popup button.popup-item.active i {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.floating-text-format-popup .popup-item:hover:not([disabled]) {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.floating-text-format-popup select.popup-item {
|
||||
border: 0;
|
||||
display: flex;
|
||||
background: none;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
width: 70px;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.floating-text-format-popup select.code-language {
|
||||
text-transform: capitalize;
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
.floating-text-format-popup .popup-item .text {
|
||||
display: flex;
|
||||
line-height: 20px;
|
||||
width: 200px;
|
||||
vertical-align: middle;
|
||||
font-size: 14px;
|
||||
color: #777;
|
||||
text-overflow: ellipsis;
|
||||
width: 70px;
|
||||
overflow: hidden;
|
||||
height: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.floating-text-format-popup .popup-item .icon {
|
||||
display: flex;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
user-select: none;
|
||||
margin-right: 8px;
|
||||
line-height: 16px;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.floating-text-format-popup i.chevron-down {
|
||||
margin-top: 3px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.floating-text-format-popup i.chevron-down.inside {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
margin-left: -25px;
|
||||
margin-top: 11px;
|
||||
margin-right: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.floating-text-format-popup .divider {
|
||||
width: 1px;
|
||||
background-color: #eee;
|
||||
margin: 0 4px;
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import './index.css';
|
||||
|
||||
import {$isCodeHighlightNode} from '@lexical/code';
|
||||
import {$isLinkNode, TOGGLE_LINK_COMMAND} from '@lexical/link';
|
||||
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';
|
||||
import {mergeRegister} from '@lexical/utils';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
$isTextNode,
|
||||
COMMAND_PRIORITY_LOW,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
LexicalEditor,
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
} from 'lexical';
|
||||
import {useCallback, useEffect, useRef, useState} from 'react';
|
||||
import {createPortal} from 'react-dom';
|
||||
|
||||
import {getDOMRangeRect} from '../../Utils/getDOMRangeRect';
|
||||
import {getSelectedNode} from '../../Utils/getSelectedNode';
|
||||
import {setFloatingElemPosition} from '../../Utils/setFloatingElemPosition';
|
||||
import {
|
||||
TypeItalic,
|
||||
TypeStrikethrough,
|
||||
TypeSubscript,
|
||||
TypeSuperscript,
|
||||
TypeUnderline,
|
||||
TypeBold,
|
||||
LexicalCode,
|
||||
LexicalLink,
|
||||
} from '@standardnotes/icons';
|
||||
|
||||
function TextFormatFloatingToolbar({
|
||||
editor,
|
||||
anchorElem,
|
||||
isLink,
|
||||
isBold,
|
||||
isItalic,
|
||||
isUnderline,
|
||||
isCode,
|
||||
isStrikethrough,
|
||||
isSubscript,
|
||||
isSuperscript,
|
||||
}: {
|
||||
editor: LexicalEditor;
|
||||
anchorElem: HTMLElement;
|
||||
isBold: boolean;
|
||||
isCode: boolean;
|
||||
isItalic: boolean;
|
||||
isLink: boolean;
|
||||
isStrikethrough: boolean;
|
||||
isSubscript: boolean;
|
||||
isSuperscript: boolean;
|
||||
isUnderline: boolean;
|
||||
}): JSX.Element {
|
||||
const popupCharStylesEditorRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const insertLink = useCallback(() => {
|
||||
if (!isLink) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, 'https://');
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}
|
||||
}, [editor, isLink]);
|
||||
|
||||
const updateTextFormatFloatingToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
|
||||
const popupCharStylesEditorElem = popupCharStylesEditorRef.current;
|
||||
const nativeSelection = window.getSelection();
|
||||
|
||||
if (popupCharStylesEditorElem === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rootElement = editor.getRootElement();
|
||||
if (
|
||||
selection !== null &&
|
||||
nativeSelection !== null &&
|
||||
!nativeSelection.isCollapsed &&
|
||||
rootElement !== null &&
|
||||
rootElement.contains(nativeSelection.anchorNode)
|
||||
) {
|
||||
const rangeRect = getDOMRangeRect(nativeSelection, rootElement);
|
||||
|
||||
setFloatingElemPosition(rangeRect, popupCharStylesEditorElem, anchorElem);
|
||||
}
|
||||
}, [editor, anchorElem]);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollerElem = anchorElem.parentElement;
|
||||
|
||||
const update = () => {
|
||||
editor.getEditorState().read(() => {
|
||||
updateTextFormatFloatingToolbar();
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.addEventListener('scroll', update);
|
||||
}
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', update);
|
||||
if (scrollerElem) {
|
||||
scrollerElem.removeEventListener('scroll', update);
|
||||
}
|
||||
};
|
||||
}, [editor, updateTextFormatFloatingToolbar, anchorElem]);
|
||||
|
||||
useEffect(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
updateTextFormatFloatingToolbar();
|
||||
});
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({editorState}) => {
|
||||
editorState.read(() => {
|
||||
updateTextFormatFloatingToolbar();
|
||||
});
|
||||
}),
|
||||
|
||||
editor.registerCommand(
|
||||
SELECTION_CHANGE_COMMAND,
|
||||
() => {
|
||||
updateTextFormatFloatingToolbar();
|
||||
return false;
|
||||
},
|
||||
COMMAND_PRIORITY_LOW,
|
||||
),
|
||||
);
|
||||
}, [editor, updateTextFormatFloatingToolbar]);
|
||||
|
||||
return (
|
||||
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup">
|
||||
{editor.isEditable() && (
|
||||
<>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isBold ? 'active' : '')}
|
||||
aria-label="Format text as bold">
|
||||
<TypeBold />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isItalic ? 'active' : '')}
|
||||
aria-label="Format text as italics">
|
||||
<TypeItalic />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'underline');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isUnderline ? 'active' : '')}
|
||||
aria-label="Format text to underlined">
|
||||
<TypeUnderline />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isStrikethrough ? 'active' : '')}
|
||||
aria-label="Format text with a strikethrough">
|
||||
<TypeStrikethrough />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isSubscript ? 'active' : '')}
|
||||
title="Subscript"
|
||||
aria-label="Format Subscript">
|
||||
<TypeSubscript />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'superscript');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isSuperscript ? 'active' : '')}
|
||||
title="Superscript"
|
||||
aria-label="Format Superscript">
|
||||
<TypeSuperscript />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
||||
}}
|
||||
className={'popup-item spaced ' + (isCode ? 'active' : '')}
|
||||
aria-label="Insert code block">
|
||||
<LexicalCode />
|
||||
</button>
|
||||
<button
|
||||
onClick={insertLink}
|
||||
className={'popup-item spaced ' + (isLink ? 'active' : '')}
|
||||
aria-label="Insert link">
|
||||
<LexicalLink />
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useFloatingTextFormatToolbar(
|
||||
editor: LexicalEditor,
|
||||
anchorElem: HTMLElement,
|
||||
): JSX.Element | null {
|
||||
const [isText, setIsText] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isSubscript, setIsSubscript] = useState(false);
|
||||
const [isSuperscript, setIsSuperscript] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
|
||||
const updatePopup = useCallback(() => {
|
||||
editor.getEditorState().read(() => {
|
||||
// Should not to pop up the floating toolbar when using IME input
|
||||
if (editor.isComposing()) {
|
||||
return;
|
||||
}
|
||||
const selection = $getSelection();
|
||||
const nativeSelection = window.getSelection();
|
||||
const rootElement = editor.getRootElement();
|
||||
|
||||
if (
|
||||
nativeSelection !== null &&
|
||||
(!$isRangeSelection(selection) ||
|
||||
rootElement === null ||
|
||||
!rootElement.contains(nativeSelection.anchorNode))
|
||||
) {
|
||||
setIsText(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$isRangeSelection(selection)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = getSelectedNode(selection);
|
||||
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsSubscript(selection.hasFormat('subscript'));
|
||||
setIsSuperscript(selection.hasFormat('superscript'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
|
||||
// Update links
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
|
||||
if (
|
||||
!$isCodeHighlightNode(selection.anchor.getNode()) &&
|
||||
selection.getTextContent() !== ''
|
||||
) {
|
||||
setIsText($isTextNode(node));
|
||||
} else {
|
||||
setIsText(false);
|
||||
}
|
||||
});
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('selectionchange', updatePopup);
|
||||
return () => {
|
||||
document.removeEventListener('selectionchange', updatePopup);
|
||||
};
|
||||
}, [updatePopup]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(() => {
|
||||
updatePopup();
|
||||
}),
|
||||
editor.registerRootListener(() => {
|
||||
if (editor.getRootElement() === null) {
|
||||
setIsText(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}, [editor, updatePopup]);
|
||||
|
||||
if (!isText || isLink) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return createPortal(
|
||||
<TextFormatFloatingToolbar
|
||||
editor={editor}
|
||||
anchorElem={anchorElem}
|
||||
isLink={isLink}
|
||||
isBold={isBold}
|
||||
isItalic={isItalic}
|
||||
isStrikethrough={isStrikethrough}
|
||||
isSubscript={isSubscript}
|
||||
isSuperscript={isSuperscript}
|
||||
isUnderline={isUnderline}
|
||||
isCode={isCode}
|
||||
/>,
|
||||
anchorElem,
|
||||
);
|
||||
}
|
||||
|
||||
export default function FloatingTextFormatToolbarPlugin({
|
||||
anchorElem = document.body,
|
||||
}: {
|
||||
anchorElem?: HTMLElement;
|
||||
}): JSX.Element | null {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
return useFloatingTextFormatToolbar(editor, anchorElem);
|
||||
}
|
||||
@@ -1219,7 +1219,7 @@ body {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 2px;
|
||||
background-color: #ccc;
|
||||
background-color: var(--sn-stylekit-contrast-border-color);
|
||||
line-height: 2px;
|
||||
}
|
||||
|
||||
|
||||
@@ -61,10 +61,12 @@
|
||||
vertical-align: super;
|
||||
}
|
||||
.Lexical__textCode {
|
||||
background-color: rgb(240, 242, 245);
|
||||
padding: 1px 0.25rem;
|
||||
background-color: var(--sn-stylekit-secondary-background-color);
|
||||
color: var(--sn-stylekit-info-color);
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
font-size: 94%;
|
||||
font-size: 85%;
|
||||
}
|
||||
.Lexical__hashtag {
|
||||
background-color: rgba(88, 144, 255, 0.15);
|
||||
@@ -78,7 +80,7 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
.Lexical__code {
|
||||
background-color: rgb(240, 242, 245);
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
font-family: Menlo, Consolas, Monaco, monospace;
|
||||
display: block;
|
||||
padding: 8px 8px 8px 52px;
|
||||
@@ -95,12 +97,12 @@
|
||||
.Lexical__code:before {
|
||||
content: attr(data-gutter);
|
||||
position: absolute;
|
||||
background-color: #eee;
|
||||
background-color: var(--sn-stylekit-secondary-background-color);
|
||||
left: 0;
|
||||
top: 0;
|
||||
border-right: 1px solid #ccc;
|
||||
border-right: 1px solid var(--sn-stylekit-contrast-border-color);
|
||||
padding: 8px;
|
||||
color: #777;
|
||||
color: var(--sn-stylekit-info-color);
|
||||
white-space: pre-wrap;
|
||||
text-align: right;
|
||||
min-width: 25px;
|
||||
@@ -113,12 +115,13 @@
|
||||
table-layout: fixed;
|
||||
width: calc(100% - 25px);
|
||||
margin: 30px 0;
|
||||
color: var(--sn-stylekit-contrast-foreground-color);
|
||||
}
|
||||
.Lexical__tableSelected {
|
||||
outline: 2px solid rgb(60, 132, 244);
|
||||
}
|
||||
.Lexical__tableCell {
|
||||
border: 1px solid #bbb;
|
||||
border: 1px solid var(--sn-stylekit-border-color);
|
||||
min-width: 75px;
|
||||
vertical-align: top;
|
||||
text-align: start;
|
||||
@@ -147,7 +150,8 @@
|
||||
top: 0;
|
||||
}
|
||||
.Lexical__tableCellHeader {
|
||||
background-color: #f2f3f5;
|
||||
background-color: var(--sn-stylekit-contrast-background-color);
|
||||
border-color: var(--sn-stylekit-contrast-border-color);
|
||||
text-align: start;
|
||||
}
|
||||
.Lexical__tableCellSelected {
|
||||
@@ -306,7 +310,7 @@
|
||||
list-style-position: inside;
|
||||
}
|
||||
.Lexical__listItem {
|
||||
margin: 0 32px;
|
||||
margin: 0 0px;
|
||||
}
|
||||
.Lexical__listItemChecked,
|
||||
.Lexical__listItemUnchecked {
|
||||
@@ -317,6 +321,7 @@
|
||||
padding-right: 24px;
|
||||
list-style-type: none;
|
||||
outline: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.Lexical__listItemChecked {
|
||||
text-decoration: line-through;
|
||||
@@ -326,10 +331,9 @@
|
||||
content: '';
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
top: 5px;
|
||||
left: 0;
|
||||
top: 5px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
background-size: cover;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
@@ -81,42 +81,10 @@ i.bucket {
|
||||
background-image: url(#{$blocks-editor-icons-path}/paint-bucket.svg);
|
||||
}
|
||||
|
||||
i.bold {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-bold.svg);
|
||||
}
|
||||
|
||||
i.italic {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-italic.svg);
|
||||
}
|
||||
|
||||
i.clear {
|
||||
background-image: url(#{$blocks-editor-icons-path}/trash.svg);
|
||||
}
|
||||
|
||||
i.code {
|
||||
background-image: url(#{$blocks-editor-icons-path}/code.svg);
|
||||
}
|
||||
|
||||
i.underline {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-underline.svg);
|
||||
}
|
||||
|
||||
i.strikethrough {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-strikethrough.svg);
|
||||
}
|
||||
|
||||
i.subscript {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-subscript.svg);
|
||||
}
|
||||
|
||||
i.superscript {
|
||||
background-image: url(#{$blocks-editor-icons-path}/type-superscript.svg);
|
||||
}
|
||||
|
||||
i.link {
|
||||
background-image: url(#{$blocks-editor-icons-path}/link.svg);
|
||||
}
|
||||
|
||||
i.horizontal-rule {
|
||||
background-image: url(#{$blocks-editor-icons-path}/horizontal-rule.svg);
|
||||
}
|
||||
|
||||
69
packages/blocks-editor/src/Lexical/UI/LinkPreview.css
Normal file
69
packages/blocks-editor/src/Lexical/UI/LinkPreview.css
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
@keyframes glimmer-animation {
|
||||
0% {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
.50% {
|
||||
background: #eeeeee;
|
||||
}
|
||||
.100% {
|
||||
background: #f9f9f9;
|
||||
}
|
||||
}
|
||||
|
||||
.LinkPreview__container {
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.LinkPreview__imageWrapper {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.LinkPreview__image {
|
||||
max-width: 100%;
|
||||
max-height: 250px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.LinkPreview__title {
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.LinkPreview__description {
|
||||
color: #999;
|
||||
font-size: 90%;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.LinkPreview__domain {
|
||||
color: #999;
|
||||
font-size: 90%;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.LinkPreview__glimmer {
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
height: 18px;
|
||||
margin-bottom: 8px;
|
||||
margin-left: 12px;
|
||||
margin-right: 12px;
|
||||
animation-duration: 3s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
animation-name: glimmer-animation;
|
||||
}
|
||||
117
packages/blocks-editor/src/Lexical/UI/LinkPreview.tsx
Normal file
117
packages/blocks-editor/src/Lexical/UI/LinkPreview.tsx
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
import './LinkPreview.css';
|
||||
|
||||
import {CSSProperties, Suspense} from 'react';
|
||||
|
||||
type Preview = {
|
||||
title: string;
|
||||
description: string;
|
||||
img: string;
|
||||
domain: string;
|
||||
} | null;
|
||||
|
||||
// Cached responses or running request promises
|
||||
const PREVIEW_CACHE: Record<string, Promise<Preview> | {preview: Preview}> = {};
|
||||
|
||||
const URL_MATCHER =
|
||||
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/;
|
||||
|
||||
function useSuspenseRequest(url: string) {
|
||||
let cached = PREVIEW_CACHE[url];
|
||||
|
||||
if (!url.match(URL_MATCHER)) {
|
||||
return {preview: null};
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
cached = PREVIEW_CACHE[url] = fetch(
|
||||
`/api/link-preview?url=${encodeURI(url)}`,
|
||||
)
|
||||
.then((response) => response.json())
|
||||
.then((preview) => {
|
||||
PREVIEW_CACHE[url] = preview;
|
||||
return preview;
|
||||
})
|
||||
.catch(() => {
|
||||
PREVIEW_CACHE[url] = {preview: null};
|
||||
});
|
||||
}
|
||||
|
||||
if (cached instanceof Promise) {
|
||||
throw cached;
|
||||
}
|
||||
|
||||
return cached;
|
||||
}
|
||||
|
||||
function LinkPreviewContent({
|
||||
url,
|
||||
}: Readonly<{
|
||||
url: string;
|
||||
}>): JSX.Element | null {
|
||||
const {preview} = useSuspenseRequest(url);
|
||||
if (preview === null) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="LinkPreview__container">
|
||||
{preview.img && (
|
||||
<div className="LinkPreview__imageWrapper">
|
||||
<img
|
||||
src={preview.img}
|
||||
alt={preview.title}
|
||||
className="LinkPreview__image"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{preview.domain && (
|
||||
<div className="LinkPreview__domain">{preview.domain}</div>
|
||||
)}
|
||||
{preview.title && (
|
||||
<div className="LinkPreview__title">{preview.title}</div>
|
||||
)}
|
||||
{preview.description && (
|
||||
<div className="LinkPreview__description">{preview.description}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Glimmer(props: {style: CSSProperties; index: number}): JSX.Element {
|
||||
return (
|
||||
<div
|
||||
className="LinkPreview__glimmer"
|
||||
{...props}
|
||||
style={{
|
||||
animationDelay: String((props.index || 0) * 300),
|
||||
...(props.style || {}),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default function LinkPreview({
|
||||
url,
|
||||
}: Readonly<{
|
||||
url: string;
|
||||
}>): JSX.Element {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<>
|
||||
<Glimmer style={{height: '80px'}} index={0} />
|
||||
<Glimmer style={{width: '60%'}} index={1} />
|
||||
<Glimmer style={{width: '80%'}} index={2} />
|
||||
</>
|
||||
}>
|
||||
<LinkPreviewContent url={url} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
27
packages/blocks-editor/src/Lexical/Utils/getDOMRangeRect.ts
Normal file
27
packages/blocks-editor/src/Lexical/Utils/getDOMRangeRect.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
export function getDOMRangeRect(
|
||||
nativeSelection: Selection,
|
||||
rootElement: HTMLElement,
|
||||
): DOMRect {
|
||||
const domRange = nativeSelection.getRangeAt(0);
|
||||
|
||||
let rect;
|
||||
|
||||
if (nativeSelection.anchorNode === rootElement) {
|
||||
let inner = rootElement;
|
||||
while (inner.firstElementChild != null) {
|
||||
inner = inner.firstElementChild as HTMLElement;
|
||||
}
|
||||
rect = inner.getBoundingClientRect();
|
||||
} else {
|
||||
rect = domRange.getBoundingClientRect();
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
27
packages/blocks-editor/src/Lexical/Utils/getSelectedNode.ts
Normal file
27
packages/blocks-editor/src/Lexical/Utils/getSelectedNode.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
import {$isAtNodeEnd} from '@lexical/selection';
|
||||
import {ElementNode, RangeSelection, TextNode} from 'lexical';
|
||||
|
||||
export function getSelectedNode(
|
||||
selection: RangeSelection,
|
||||
): TextNode | ElementNode {
|
||||
const anchor = selection.anchor;
|
||||
const focus = selection.focus;
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const focusNode = selection.focus.getNode();
|
||||
if (anchorNode === focusNode) {
|
||||
return anchorNode;
|
||||
}
|
||||
const isBackward = selection.isBackward();
|
||||
if (isBackward) {
|
||||
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
||||
} else {
|
||||
return $isAtNodeEnd(anchor) ? focusNode : anchorNode;
|
||||
}
|
||||
}
|
||||
23
packages/blocks-editor/src/Lexical/Utils/sanitizeUrl.ts
Normal file
23
packages/blocks-editor/src/Lexical/Utils/sanitizeUrl.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
|
||||
export const sanitizeUrl = (url: string): string => {
|
||||
/** A pattern that matches safe URLs. */
|
||||
const SAFE_URL_PATTERN =
|
||||
/^(?:(?:https?|mailto|ftp|tel|file|sms):|[^&:/?#]*(?:[/?#]|$))/gi;
|
||||
|
||||
/** A pattern that matches safe data URLs. */
|
||||
const DATA_URL_PATTERN =
|
||||
/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;
|
||||
|
||||
url = String(url).trim();
|
||||
|
||||
if (url.match(SAFE_URL_PATTERN) || url.match(DATA_URL_PATTERN)) return url;
|
||||
|
||||
return `https://`;
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*
|
||||
*/
|
||||
const VERTICAL_GAP = 10;
|
||||
const HORIZONTAL_OFFSET = 5;
|
||||
|
||||
export function setFloatingElemPosition(
|
||||
targetRect: ClientRect | null,
|
||||
floatingElem: HTMLElement,
|
||||
anchorElem: HTMLElement,
|
||||
verticalGap: number = VERTICAL_GAP,
|
||||
horizontalOffset: number = HORIZONTAL_OFFSET,
|
||||
): void {
|
||||
const scrollerElem = anchorElem.parentElement;
|
||||
|
||||
if (targetRect === null || !scrollerElem) {
|
||||
floatingElem.style.opacity = '0';
|
||||
floatingElem.style.transform = 'translate(-10000px, -10000px)';
|
||||
return;
|
||||
}
|
||||
|
||||
const floatingElemRect = floatingElem.getBoundingClientRect();
|
||||
const anchorElementRect = anchorElem.getBoundingClientRect();
|
||||
const editorScrollerRect = scrollerElem.getBoundingClientRect();
|
||||
|
||||
let top = targetRect.top - floatingElemRect.height - verticalGap;
|
||||
let left = targetRect.left - horizontalOffset;
|
||||
|
||||
if (top < editorScrollerRect.top) {
|
||||
top += floatingElemRect.height + targetRect.height + verticalGap * 2;
|
||||
}
|
||||
|
||||
if (left + floatingElemRect.width > editorScrollerRect.right) {
|
||||
left = editorScrollerRect.right - floatingElemRect.width - horizontalOffset;
|
||||
}
|
||||
|
||||
top -= anchorElementRect.top;
|
||||
left -= anchorElementRect.left;
|
||||
|
||||
floatingElem.style.opacity = '1';
|
||||
floatingElem.style.transform = `translate(${left}px, ${top}px)`;
|
||||
}
|
||||
@@ -1,4 +1,2 @@
|
||||
export * from './Editor/BlocksEditor';
|
||||
export * from './Editor/BlocksEditorComposer';
|
||||
export * from './Editor/Commands';
|
||||
export * from './Editor/ClassNames';
|
||||
|
||||
Reference in New Issue
Block a user