From e30ea97aab938f6b41902aafefcc82a5e8bbb583 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?marcin=20miko=C5=82ajczak?= Date: Thu, 30 Mar 2023 18:17:12 +0200 Subject: [PATCH] Lexical: Cleanup, move TypeaheadMenuPlugin and parts of MentionPlugin to new AutosuggestPlugin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: marcin mikołajczak --- app/soapbox/features/compose/editor/index.tsx | 4 +- ...menu-plugin.tsx => autosuggest-plugin.tsx} | 124 ++++++++++++++---- .../plugins/floating-link-editor-plugin.tsx | 4 +- .../compose/editor/plugins/mention-plugin.tsx | 115 ++-------------- 4 files changed, 115 insertions(+), 132 deletions(-) rename app/soapbox/features/compose/editor/plugins/{typeahead-menu-plugin.tsx => autosuggest-plugin.tsx} (74%) diff --git a/app/soapbox/features/compose/editor/index.tsx b/app/soapbox/features/compose/editor/index.tsx index 761442016..b2479a587 100644 --- a/app/soapbox/features/compose/editor/index.tsx +++ b/app/soapbox/features/compose/editor/index.tsx @@ -26,6 +26,7 @@ import { setEditorState } from 'soapbox/actions/compose'; import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import nodes from './nodes'; +import { AutosuggestPlugin } from './plugins/autosuggest-plugin'; import DraggableBlockPlugin from './plugins/draggable-block-plugin'; import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin'; import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin'; @@ -140,7 +141,8 @@ const ComposeEditor = React.forwardRef(({ composeId, condensed, onF /> - + + {features.richText && } {features.richText && floatingAnchorElem && ( <> diff --git a/app/soapbox/features/compose/editor/plugins/typeahead-menu-plugin.tsx b/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx similarity index 74% rename from app/soapbox/features/compose/editor/plugins/typeahead-menu-plugin.tsx rename to app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx index 3b5feef92..e70af4451 100644 --- a/app/soapbox/features/compose/editor/plugins/typeahead-menu-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/autosuggest-plugin.tsx @@ -7,6 +7,7 @@ */ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; +import clsx from 'clsx'; import { $getSelection, $isRangeSelection, @@ -22,6 +23,14 @@ import React, { useRef, useState, } from 'react'; +import ReactDOM from 'react-dom'; + +import { fetchComposeSuggestions } from 'soapbox/actions/compose'; +import { useAppDispatch, useCompose } from 'soapbox/hooks'; + +import AutosuggestAccount from '../../components/autosuggest-account'; + +import { getMentionMatch } from './mention-plugin'; export type QueryMatch = { leadOffset: number @@ -269,46 +278,95 @@ function useMenuAnchorRef( return anchorElementRef; } -export type TypeaheadMenuPluginProps = { - menuRenderFn: MenuRenderFn - triggerFn: TriggerFn - onOpen?: (resolution: Resolution) => void - onClose?: () => void -}; - export type TriggerFn = ( text: string, editor: LexicalEditor, ) => QueryMatch | null; -export function TypeaheadMenuPlugin({ - onOpen, - onClose, - menuRenderFn, - triggerFn, -}: TypeaheadMenuPluginProps): JSX.Element | null { +export type AutosuggestPluginProps = { + composeId: string + suggestionsHidden: boolean + setSuggestionsHidden: (value: boolean) => void +}; + +export function AutosuggestPlugin({ + composeId, + suggestionsHidden, + setSuggestionsHidden, +}: AutosuggestPluginProps): JSX.Element | null { + const { suggestions } = useCompose(composeId); + const dispatch = useAppDispatch(); + const [editor] = useLexicalComposerContext(); const [resolution, setResolution] = useState(null); + const [selectedSuggestion] = useState(0); const anchorElementRef = useMenuAnchorRef( resolution, setResolution, ); + const onSelectSuggestion: React.MouseEventHandler = (e) => { + e.preventDefault(); + + const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any); + + editor.update(() => { + + dispatch((_, getState) => { + const state = editor.getEditorState(); + const node = (state._selection as RangeSelection)?.anchor?.getNode(); + + const content = getState().accounts.get(suggestion)!.acct; + + node.setTextContent(`@${content} `); + node.select(); + }); + }); + }; + + const checkForMentionMatch = useCallback((text: string) => { + const matchArr = getMentionMatch(text); + + if (!matchArr) return null; + + dispatch(fetchComposeSuggestions(composeId, matchArr[0])); + + return { + leadOffset: matchArr.index, + matchingString: matchArr[0], + }; + }, []); + + const renderSuggestion = (suggestion: string, i: number) => { + const inner = ; + const key = suggestion; + + return ( +
+ {inner} +
+ ); + }; + const closeTypeahead = useCallback(() => { setResolution(null); - if (onClose && resolution !== null) { - onClose(); - } - }, [onClose, resolution]); + }, [resolution]); const openTypeahead = useCallback( (res: Resolution) => { setResolution(res); - if (onOpen && resolution === null) { - onOpen(res); - } }, - [onOpen, resolution], + [resolution], ); useEffect(() => { @@ -322,7 +380,7 @@ export function TypeaheadMenuPlugin({ return; } - const match = triggerFn(text, editor); + const match = checkForMentionMatch(text); if ( match !== null && @@ -350,16 +408,34 @@ export function TypeaheadMenuPlugin({ }; }, [ editor, - triggerFn, resolution, closeTypeahead, openTypeahead, ]); + useEffect(() => { + if (suggestions && suggestions.size > 0) setSuggestionsHidden(false); + }, [suggestions]); + return resolution === null || editor === null ? null : ( + anchorElementRef.current + ? ReactDOM.createPortal( +
+ {suggestions.map(renderSuggestion)} +
, + anchorElementRef.current, + ) + : null + } /> ); } diff --git a/app/soapbox/features/compose/editor/plugins/floating-link-editor-plugin.tsx b/app/soapbox/features/compose/editor/plugins/floating-link-editor-plugin.tsx index e94d43b8f..112cc6577 100644 --- a/app/soapbox/features/compose/editor/plugins/floating-link-editor-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/floating-link-editor-plugin.tsx @@ -159,13 +159,13 @@ const FloatingLinkEditor = ({ return (
{isEditMode ? ( <> { diff --git a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx index e20ccffa4..c46cb8898 100644 --- a/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx +++ b/app/soapbox/features/compose/editor/plugins/mention-plugin.tsx @@ -8,55 +8,25 @@ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'; import { useLexicalTextEntity } from '@lexical/react/useLexicalTextEntity'; -import clsx from 'clsx'; -import React, { useCallback, useEffect, useState } from 'react'; -import ReactDOM from 'react-dom'; +import { useCallback, useEffect } from 'react'; -import { fetchComposeSuggestions } from 'soapbox/actions/compose'; -import { useAppDispatch, useCompose } from 'soapbox/hooks'; - -import AutosuggestAccount from '../../components/autosuggest-account'; import { $createMentionNode, MentionNode } from '../nodes/mention-node'; -import { TypeaheadMenuPlugin } from './typeahead-menu-plugin'; -import type { RangeSelection, TextNode } from 'lexical'; +import type { TextNode } from 'lexical'; const REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); -export const MentionPlugin: React.FC<{ - composeId: string - suggestionsHidden: boolean - setSuggestionsHidden: (value: boolean) => void -}> = ({ - composeId, suggestionsHidden, setSuggestionsHidden, -}): JSX.Element | null => { - const { suggestions } = useCompose(composeId); - const dispatch = useAppDispatch(); +export const getMentionMatch = (text: string) => { + const matchArr = REGEX.exec(text); + if (!matchArr) return null; + return matchArr; +}; + +export const MentionPlugin = (): JSX.Element | null => { const [editor] = useLexicalComposerContext(); - const [selectedSuggestion] = useState(0); - - const onSelectSuggestion: React.MouseEventHandler = (e) => { - e.preventDefault(); - - const suggestion = suggestions.get(e.currentTarget.getAttribute('data-index') as any); - - editor.update(() => { - - dispatch((_, getState) => { - const state = editor.getEditorState(); - const node = (state._selection as RangeSelection)?.anchor?.getNode(); - - const content = getState().accounts.get(suggestion)!.acct; - - node.setTextContent(`@${content} `); - node.select(); - }); - }); - }; - useEffect(() => { if (!editor.hasNodes([MentionNode])) { throw new Error('MentionPlugin: MentionNode not registered on editor'); @@ -67,13 +37,6 @@ export const MentionPlugin: React.FC<{ return $createMentionNode(textNode.getTextContent()); }, []); - const getMentionMatch = (text: string) => { - const matchArr = REGEX.exec(text); - - if (!matchArr) return null; - return matchArr; - }; - const getEntityMatch = useCallback((text: string) => { const matchArr = getMentionMatch(text); @@ -88,69 +51,11 @@ export const MentionPlugin: React.FC<{ }; }, []); - const checkForMentionMatch = useCallback((text: string) => { - const matchArr = getMentionMatch(text); - - if (!matchArr) return null; - - dispatch(fetchComposeSuggestions(composeId, matchArr[0])); - - return { - leadOffset: matchArr.index, - matchingString: matchArr[0], - }; - }, []); - useLexicalTextEntity( getEntityMatch, MentionNode, createMentionNode, ); - const renderSuggestion = (suggestion: string, i: number) => { - const inner = ; - const key = suggestion; - - return ( -
- {inner} -
- ); - }; - - useEffect(() => { - if (suggestions && suggestions.size > 0) setSuggestionsHidden(false); - }, [suggestions]); - - return ( - - anchorElementRef.current - ? ReactDOM.createPortal( -
- {suggestions.map(renderSuggestion)} -
, - anchorElementRef.current, - ) - : null - } - /> - ); + return null; };