2023-03-13 12:16:37 -07:00
|
|
|
/**
|
|
|
|
* 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 { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
2023-03-30 09:17:12 -07:00
|
|
|
import clsx from 'clsx';
|
2023-03-13 12:16:37 -07:00
|
|
|
import {
|
|
|
|
$getSelection,
|
|
|
|
$isRangeSelection,
|
|
|
|
$isTextNode,
|
|
|
|
LexicalEditor,
|
|
|
|
RangeSelection,
|
|
|
|
} from 'lexical';
|
|
|
|
import React, {
|
|
|
|
MutableRefObject,
|
|
|
|
ReactPortal,
|
|
|
|
useCallback,
|
|
|
|
useEffect,
|
|
|
|
useRef,
|
|
|
|
useState,
|
|
|
|
} from 'react';
|
2023-03-30 09:17:12 -07:00
|
|
|
import ReactDOM from 'react-dom';
|
|
|
|
|
|
|
|
import { fetchComposeSuggestions } from 'soapbox/actions/compose';
|
2023-04-02 11:50:15 -07:00
|
|
|
import { useEmoji } from 'soapbox/actions/emojis';
|
2023-04-01 04:40:15 -07:00
|
|
|
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
|
2023-04-02 11:50:15 -07:00
|
|
|
import { isNativeEmoji } from 'soapbox/features/emoji';
|
2023-03-30 09:17:12 -07:00
|
|
|
import { useAppDispatch, useCompose } from 'soapbox/hooks';
|
2023-04-02 11:50:15 -07:00
|
|
|
import { textAtCursorMatchesToken } from 'soapbox/utils/suggestions';
|
2023-03-30 09:17:12 -07:00
|
|
|
|
|
|
|
import AutosuggestAccount from '../../components/autosuggest-account';
|
2023-04-05 14:04:24 -07:00
|
|
|
import { $createEmojiNode } from '../nodes/emoji-node';
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-01 04:40:15 -07:00
|
|
|
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
|
|
|
|
|
2023-04-18 15:22:22 -07:00
|
|
|
type QueryMatch = {
|
2023-03-13 12:16:37 -07:00
|
|
|
leadOffset: number
|
|
|
|
matchingString: string
|
|
|
|
};
|
|
|
|
|
2023-04-18 15:22:22 -07:00
|
|
|
type Resolution = {
|
2023-03-13 12:16:37 -07:00
|
|
|
match: QueryMatch
|
|
|
|
getRect: () => DOMRect
|
|
|
|
};
|
|
|
|
|
2023-04-18 15:22:22 -07:00
|
|
|
type MenuRenderFn = (
|
2023-03-13 12:16:37 -07:00
|
|
|
anchorElementRef: MutableRefObject<HTMLElement | null>,
|
|
|
|
) => ReactPortal | JSX.Element | null;
|
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const tryToPositionRange = (leadOffset: number, range: Range): boolean => {
|
2023-03-13 12:16:37 -07:00
|
|
|
const domSelection = window.getSelection();
|
|
|
|
if (domSelection === null || !domSelection.isCollapsed) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
const anchorNode = domSelection.anchorNode;
|
|
|
|
const startOffset = leadOffset;
|
|
|
|
const endOffset = domSelection.anchorOffset;
|
|
|
|
|
2023-03-14 09:22:23 -07:00
|
|
|
if (!anchorNode || !endOffset) {
|
2023-03-13 12:16:37 -07:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
range.setStart(anchorNode, startOffset);
|
|
|
|
range.setEnd(anchorNode, endOffset);
|
|
|
|
} catch (error) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return true;
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const isSelectionOnEntityBoundary = (
|
2023-03-13 12:16:37 -07:00
|
|
|
editor: LexicalEditor,
|
|
|
|
offset: number,
|
2023-04-11 14:22:34 -07:00
|
|
|
): boolean => {
|
2023-03-13 12:16:37 -07:00
|
|
|
if (offset !== 0) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
return editor.getEditorState().read(() => {
|
|
|
|
const selection = $getSelection();
|
|
|
|
if ($isRangeSelection(selection)) {
|
|
|
|
const anchor = selection.anchor;
|
|
|
|
const anchorNode = anchor.getNode();
|
|
|
|
const prevSibling = anchorNode.getPreviousSibling();
|
|
|
|
return $isTextNode(prevSibling) && prevSibling.isTextEntity();
|
|
|
|
}
|
|
|
|
return false;
|
|
|
|
});
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const startTransition = (callback: () => void) => {
|
2023-03-13 12:16:37 -07:00
|
|
|
if (React.startTransition) {
|
|
|
|
React.startTransition(callback);
|
|
|
|
} else {
|
|
|
|
callback();
|
|
|
|
}
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
|
|
|
// Got from https://stackoverflow.com/a/42543908/2013580
|
2023-04-18 15:22:22 -07:00
|
|
|
const getScrollParent = (
|
2023-03-13 12:16:37 -07:00
|
|
|
element: HTMLElement,
|
|
|
|
includeHidden: boolean,
|
2023-04-11 14:22:34 -07:00
|
|
|
): HTMLElement | HTMLBodyElement => {
|
2023-03-13 12:16:37 -07:00
|
|
|
let style = getComputedStyle(element);
|
|
|
|
const excludeStaticParent = style.position === 'absolute';
|
|
|
|
const overflowRegex = includeHidden
|
|
|
|
? /(auto|scroll|hidden)/
|
|
|
|
: /(auto|scroll)/;
|
|
|
|
if (style.position === 'fixed') {
|
|
|
|
return document.body;
|
|
|
|
}
|
|
|
|
for (
|
|
|
|
let parent: HTMLElement | null = element;
|
|
|
|
(parent = parent.parentElement);
|
|
|
|
|
|
|
|
) {
|
|
|
|
style = getComputedStyle(parent);
|
|
|
|
if (excludeStaticParent && style.position === 'static') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
overflowRegex.test(style.overflow + style.overflowY + style.overflowX)
|
|
|
|
) {
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return document.body;
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const isTriggerVisibleInNearestScrollContainer = (
|
2023-03-13 12:16:37 -07:00
|
|
|
targetElement: HTMLElement,
|
|
|
|
containerElement: HTMLElement,
|
2023-04-11 14:22:34 -07:00
|
|
|
): boolean => {
|
2023-03-13 12:16:37 -07:00
|
|
|
const tRect = targetElement.getBoundingClientRect();
|
|
|
|
const cRect = containerElement.getBoundingClientRect();
|
|
|
|
return tRect.top > cRect.top && tRect.top < cRect.bottom;
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
|
|
|
// Reposition the menu on scroll, window resize, and element resize.
|
2023-04-18 15:22:22 -07:00
|
|
|
const useDynamicPositioning = (
|
2023-03-13 12:16:37 -07:00
|
|
|
resolution: Resolution | null,
|
|
|
|
targetElement: HTMLElement | null,
|
|
|
|
onReposition: () => void,
|
|
|
|
onVisibilityChange?: (isInView: boolean) => void,
|
2023-04-11 14:22:34 -07:00
|
|
|
) => {
|
2023-03-13 12:16:37 -07:00
|
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
useEffect(() => {
|
2023-03-14 09:22:23 -07:00
|
|
|
if (targetElement && resolution) {
|
2023-03-13 12:16:37 -07:00
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
const rootScrollParent =
|
2023-03-14 09:22:23 -07:00
|
|
|
rootElement
|
2023-03-13 12:16:37 -07:00
|
|
|
? getScrollParent(rootElement, false)
|
|
|
|
: document.body;
|
|
|
|
let ticking = false;
|
|
|
|
let previousIsInView = isTriggerVisibleInNearestScrollContainer(
|
|
|
|
targetElement,
|
|
|
|
rootScrollParent,
|
|
|
|
);
|
2023-04-11 14:22:34 -07:00
|
|
|
const handleScroll = () => {
|
2023-03-13 12:16:37 -07:00
|
|
|
if (!ticking) {
|
2023-04-11 14:22:34 -07:00
|
|
|
window.requestAnimationFrame(() => {
|
2023-03-13 12:16:37 -07:00
|
|
|
onReposition();
|
|
|
|
ticking = false;
|
|
|
|
});
|
|
|
|
ticking = true;
|
|
|
|
}
|
|
|
|
const isInView = isTriggerVisibleInNearestScrollContainer(
|
|
|
|
targetElement,
|
|
|
|
rootScrollParent,
|
|
|
|
);
|
|
|
|
if (isInView !== previousIsInView) {
|
|
|
|
previousIsInView = isInView;
|
2023-03-14 09:22:23 -07:00
|
|
|
if (onVisibilityChange) {
|
2023-03-13 12:16:37 -07:00
|
|
|
onVisibilityChange(isInView);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
const resizeObserver = new ResizeObserver(onReposition);
|
|
|
|
window.addEventListener('resize', onReposition);
|
|
|
|
document.addEventListener('scroll', handleScroll, {
|
|
|
|
capture: true,
|
|
|
|
passive: true,
|
|
|
|
});
|
|
|
|
resizeObserver.observe(targetElement);
|
|
|
|
return () => {
|
|
|
|
resizeObserver.unobserve(targetElement);
|
|
|
|
window.removeEventListener('resize', onReposition);
|
|
|
|
document.removeEventListener('scroll', handleScroll);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, [targetElement, editor, onVisibilityChange, onReposition, resolution]);
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const LexicalPopoverMenu = ({ anchorElementRef, menuRenderFn }: {
|
2023-03-13 12:16:37 -07:00
|
|
|
anchorElementRef: MutableRefObject<HTMLElement>
|
2023-03-27 14:49:37 -07:00
|
|
|
menuRenderFn: MenuRenderFn
|
2023-04-11 14:22:34 -07:00
|
|
|
}): JSX.Element | null => menuRenderFn(anchorElementRef);
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-11 14:22:34 -07:00
|
|
|
const useMenuAnchorRef = (
|
2023-03-13 12:16:37 -07:00
|
|
|
resolution: Resolution | null,
|
|
|
|
setResolution: (r: Resolution | null) => void,
|
2023-04-11 14:22:34 -07:00
|
|
|
): MutableRefObject<HTMLElement> => {
|
2023-03-13 12:16:37 -07:00
|
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
const anchorElementRef = useRef<HTMLElement>(document.createElement('div'));
|
|
|
|
const positionMenu = useCallback(() => {
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
const containerDiv = anchorElementRef.current;
|
|
|
|
|
|
|
|
if (rootElement !== null && resolution !== null) {
|
|
|
|
const { left, top, width, height } = resolution.getRect();
|
|
|
|
containerDiv.style.top = `${top + window.pageYOffset}px`;
|
|
|
|
containerDiv.style.left = `${left + window.pageXOffset}px`;
|
|
|
|
containerDiv.style.height = `${height}px`;
|
|
|
|
containerDiv.style.width = `${width}px`;
|
|
|
|
|
|
|
|
if (!containerDiv.isConnected) {
|
|
|
|
containerDiv.setAttribute('aria-label', 'Typeahead menu');
|
|
|
|
containerDiv.setAttribute('id', 'typeahead-menu');
|
|
|
|
containerDiv.setAttribute('role', 'listbox');
|
|
|
|
containerDiv.style.display = 'block';
|
|
|
|
containerDiv.style.position = 'absolute';
|
|
|
|
document.body.append(containerDiv);
|
|
|
|
}
|
|
|
|
anchorElementRef.current = containerDiv;
|
|
|
|
rootElement.setAttribute('aria-controls', 'typeahead-menu');
|
|
|
|
}
|
2023-03-27 14:49:37 -07:00
|
|
|
}, [editor, resolution]);
|
2023-03-13 12:16:37 -07:00
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const rootElement = editor.getRootElement();
|
|
|
|
if (resolution !== null) {
|
|
|
|
positionMenu();
|
|
|
|
return () => {
|
|
|
|
if (rootElement !== null) {
|
|
|
|
rootElement.removeAttribute('aria-controls');
|
|
|
|
}
|
|
|
|
|
|
|
|
const containerDiv = anchorElementRef.current;
|
|
|
|
if (containerDiv !== null && containerDiv.isConnected) {
|
|
|
|
containerDiv.remove();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}, [editor, positionMenu, resolution]);
|
|
|
|
|
|
|
|
const onVisibilityChange = useCallback(
|
|
|
|
(isInView: boolean) => {
|
|
|
|
if (resolution !== null) {
|
|
|
|
if (!isInView) {
|
|
|
|
setResolution(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[resolution, setResolution],
|
|
|
|
);
|
|
|
|
|
|
|
|
useDynamicPositioning(
|
|
|
|
resolution,
|
|
|
|
anchorElementRef.current,
|
|
|
|
positionMenu,
|
|
|
|
onVisibilityChange,
|
|
|
|
);
|
|
|
|
|
|
|
|
return anchorElementRef;
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-18 15:22:22 -07:00
|
|
|
type AutosuggestPluginProps = {
|
2023-03-30 09:17:12 -07:00
|
|
|
composeId: string
|
|
|
|
suggestionsHidden: boolean
|
|
|
|
setSuggestionsHidden: (value: boolean) => void
|
|
|
|
};
|
|
|
|
|
2023-04-18 15:22:22 -07:00
|
|
|
const AutosuggestPlugin = ({
|
2023-03-30 09:17:12 -07:00
|
|
|
composeId,
|
|
|
|
suggestionsHidden,
|
|
|
|
setSuggestionsHidden,
|
2023-04-11 14:22:34 -07:00
|
|
|
}: AutosuggestPluginProps): JSX.Element | null => {
|
2023-03-30 09:17:12 -07:00
|
|
|
const { suggestions } = useCompose(composeId);
|
|
|
|
const dispatch = useAppDispatch();
|
|
|
|
|
2023-03-13 12:16:37 -07:00
|
|
|
const [editor] = useLexicalComposerContext();
|
|
|
|
const [resolution, setResolution] = useState<Resolution | null>(null);
|
2023-03-30 09:17:12 -07:00
|
|
|
const [selectedSuggestion] = useState(0);
|
2023-03-13 12:16:37 -07:00
|
|
|
const anchorElementRef = useMenuAnchorRef(
|
|
|
|
resolution,
|
|
|
|
setResolution,
|
|
|
|
);
|
|
|
|
|
2023-03-30 09:17:12 -07:00
|
|
|
const onSelectSuggestion: React.MouseEventHandler<HTMLDivElement> = (e) => {
|
|
|
|
e.preventDefault();
|
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any) as AutoSuggestion;
|
2023-03-30 09:17:12 -07:00
|
|
|
|
|
|
|
editor.update(() => {
|
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
dispatch((dispatch, getState) => {
|
2023-03-30 09:17:12 -07:00
|
|
|
const state = editor.getEditorState();
|
|
|
|
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
if (typeof suggestion === 'object' && suggestion.id) {
|
|
|
|
dispatch(useEmoji(suggestion)); // eslint-disable-line react-hooks/rules-of-hooks
|
|
|
|
|
|
|
|
const { leadOffset, matchingString } = resolution!.match;
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
if (isNativeEmoji(suggestion)) {
|
|
|
|
node.spliceText(leadOffset - 1, matchingString.length, `${suggestion.native} `, true);
|
|
|
|
} else {
|
2023-04-05 14:04:24 -07:00
|
|
|
const completion = suggestion.colons;
|
|
|
|
|
|
|
|
let emojiText;
|
|
|
|
|
|
|
|
if (leadOffset === 1) emojiText = node;
|
|
|
|
else [, emojiText] = node.splitText(leadOffset - 1);
|
|
|
|
|
|
|
|
[emojiText] = emojiText.splitText(matchingString.length);
|
|
|
|
|
|
|
|
emojiText?.replace($createEmojiNode(completion, suggestion.imageUrl));
|
2023-04-02 11:50:15 -07:00
|
|
|
}
|
|
|
|
} else if ((suggestion as string)[0] === '#') {
|
|
|
|
node.setTextContent(`${suggestion} `);
|
|
|
|
node.select();
|
|
|
|
} else {
|
|
|
|
const content = getState().accounts.get(suggestion)!.acct;
|
|
|
|
|
|
|
|
node.setTextContent(`@${content} `);
|
|
|
|
node.select();
|
|
|
|
}
|
2023-03-30 09:17:12 -07:00
|
|
|
});
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
const getQueryTextForSearch = (editor: LexicalEditor) => {
|
|
|
|
const state = editor.getEditorState();
|
|
|
|
const node = (state._selection as RangeSelection)?.anchor?.getNode();
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
if (!node) return null;
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
if (['mention', 'hashtag'].includes(node.getType())) {
|
|
|
|
const matchingString = node.getTextContent();
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
return { leadOffset: 0, matchingString };
|
|
|
|
}
|
|
|
|
|
|
|
|
if (node.getType() === 'text') {
|
|
|
|
const [leadOffset, matchingString] = textAtCursorMatchesToken(node.getTextContent(), (state._selection as RangeSelection)?.anchor?.offset, [':']);
|
|
|
|
|
|
|
|
if (!leadOffset || !matchingString) return null;
|
|
|
|
|
|
|
|
return { leadOffset, matchingString };
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
};
|
2023-03-30 09:17:12 -07:00
|
|
|
|
2023-04-01 04:40:15 -07:00
|
|
|
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
|
|
|
|
let inner;
|
|
|
|
let key;
|
|
|
|
|
|
|
|
if (typeof suggestion === 'object') {
|
|
|
|
inner = <AutosuggestEmoji emoji={suggestion} />;
|
|
|
|
key = suggestion.id;
|
2023-04-02 11:50:15 -07:00
|
|
|
} else if (suggestion[0] === '#') {
|
|
|
|
inner = suggestion;
|
|
|
|
key = suggestion;
|
2023-04-01 04:40:15 -07:00
|
|
|
} else {
|
|
|
|
inner = <AutosuggestAccount id={suggestion} />;
|
|
|
|
key = suggestion;
|
|
|
|
}
|
2023-03-30 09:17:12 -07:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div
|
|
|
|
role='button'
|
|
|
|
tabIndex={0}
|
|
|
|
key={key}
|
|
|
|
data-index={i}
|
|
|
|
className={clsx({
|
|
|
|
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
|
|
|
|
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
|
|
|
|
})}
|
|
|
|
onMouseDown={onSelectSuggestion}
|
|
|
|
>
|
|
|
|
{inner}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2023-03-13 12:16:37 -07:00
|
|
|
const closeTypeahead = useCallback(() => {
|
|
|
|
setResolution(null);
|
2023-03-30 09:17:12 -07:00
|
|
|
}, [resolution]);
|
2023-03-13 12:16:37 -07:00
|
|
|
|
|
|
|
const openTypeahead = useCallback(
|
|
|
|
(res: Resolution) => {
|
|
|
|
setResolution(res);
|
|
|
|
},
|
2023-03-30 09:17:12 -07:00
|
|
|
[resolution],
|
2023-03-13 12:16:37 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const updateListener = () => {
|
|
|
|
editor.getEditorState().read(() => {
|
|
|
|
const range = document.createRange();
|
2023-04-02 11:50:15 -07:00
|
|
|
const match = getQueryTextForSearch(editor);
|
2023-03-13 12:16:37 -07:00
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
if (!match) {
|
2023-03-13 12:16:37 -07:00
|
|
|
closeTypeahead();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-04-02 11:50:15 -07:00
|
|
|
dispatch(fetchComposeSuggestions(composeId, match.matchingString.trim()));
|
2023-03-13 12:16:37 -07:00
|
|
|
|
|
|
|
if (
|
|
|
|
!isSelectionOnEntityBoundary(editor, match.leadOffset)
|
|
|
|
) {
|
|
|
|
const isRangePositioned = tryToPositionRange(match.leadOffset, range);
|
|
|
|
if (isRangePositioned !== null) {
|
|
|
|
startTransition(() =>
|
|
|
|
openTypeahead({
|
|
|
|
getRect: () => range.getBoundingClientRect(),
|
|
|
|
match,
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
closeTypeahead();
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const removeUpdateListener = editor.registerUpdateListener(updateListener);
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
removeUpdateListener();
|
|
|
|
};
|
|
|
|
}, [
|
|
|
|
editor,
|
|
|
|
resolution,
|
|
|
|
closeTypeahead,
|
|
|
|
openTypeahead,
|
|
|
|
]);
|
|
|
|
|
2023-03-30 09:17:12 -07:00
|
|
|
useEffect(() => {
|
|
|
|
if (suggestions && suggestions.size > 0) setSuggestionsHidden(false);
|
|
|
|
}, [suggestions]);
|
|
|
|
|
2023-03-13 12:16:37 -07:00
|
|
|
return resolution === null || editor === null ? null : (
|
|
|
|
<LexicalPopoverMenu
|
|
|
|
anchorElementRef={anchorElementRef}
|
2023-03-30 09:17:12 -07:00
|
|
|
menuRenderFn={(anchorElementRef) =>
|
|
|
|
anchorElementRef.current
|
|
|
|
? ReactDOM.createPortal(
|
|
|
|
<div
|
|
|
|
className={clsx({
|
|
|
|
'mt-6 fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
|
|
|
hidden: suggestionsHidden || suggestions.isEmpty(),
|
|
|
|
block: !suggestionsHidden && !suggestions.isEmpty(),
|
|
|
|
})}
|
|
|
|
>
|
|
|
|
{suggestions.map(renderSuggestion)}
|
|
|
|
</div>,
|
|
|
|
anchorElementRef.current,
|
|
|
|
)
|
|
|
|
: null
|
|
|
|
}
|
2023-03-13 12:16:37 -07:00
|
|
|
/>
|
|
|
|
);
|
2023-04-11 14:22:34 -07:00
|
|
|
};
|
2023-04-18 15:22:22 -07:00
|
|
|
|
|
|
|
export default AutosuggestPlugin;
|