Lexical: Cleanup, allow ctrl+enter to submit

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-04-19 00:22:22 +02:00
parent e76a9ec8aa
commit 461a002be9
6 changed files with 57 additions and 30 deletions

View file

@ -275,6 +275,7 @@ const ComposeForm = <ID extends string>({ id, shouldCondense, autoFocus, clickab
eventDiscussion={!!event} eventDiscussion={!!event}
autoFocus={shouldAutoFocus} autoFocus={shouldAutoFocus}
hasPoll={hasPoll} hasPoll={hasPoll}
handleSubmit={handleSubmit}
onFocus={handleComposeFocus} onFocus={handleComposeFocus}
onPaste={onPaste} onPaste={onPaste}
/> />

View file

@ -9,7 +9,6 @@ LICENSE file in the /app/soapbox/features/compose/editor directory.
import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown'; import { $convertFromMarkdownString, $convertToMarkdownString } from '@lexical/markdown';
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin'; import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer'; import { LexicalComposer, InitialConfigType } from '@lexical/react/LexicalComposer';
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { ContentEditable } from '@lexical/react/LexicalContentEditable'; import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'; import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin'; import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin';
@ -20,33 +19,20 @@ import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'; import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import clsx from 'clsx'; import clsx from 'clsx';
import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical'; import { $createParagraphNode, $createTextNode, $getRoot } from 'lexical';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import { setEditorState } from 'soapbox/actions/compose';
import { useAppDispatch, useFeatures } from 'soapbox/hooks'; import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import nodes from './nodes'; import nodes from './nodes';
import { AutosuggestPlugin } from './plugins/autosuggest-plugin'; import AutosuggestPlugin from './plugins/autosuggest-plugin';
import DraggableBlockPlugin from './plugins/draggable-block-plugin'; import DraggableBlockPlugin from './plugins/draggable-block-plugin';
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin'; import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin'; import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
import { MentionPlugin } from './plugins/mention-plugin'; import MentionPlugin from './plugins/mention-plugin';
import StatePlugin from './plugins/state-plugin';
import { TO_WYSIWYG_TRANSFORMERS } from './transformers'; import { TO_WYSIWYG_TRANSFORMERS } from './transformers';
const StatePlugin = ({ composeId }: { composeId: string }) => {
const dispatch = useAppDispatch();
const [editor] = useLexicalComposerContext();
useEffect(() => {
editor.registerUpdateListener(({ editorState }) => {
dispatch(setEditorState(composeId, editorState.isEmpty() ? null : JSON.stringify(editorState.toJSON())));
});
}, [editor]);
return null;
};
interface IComposeEditor { interface IComposeEditor {
className?: string className?: string
composeId: string composeId: string
@ -54,6 +40,7 @@ interface IComposeEditor {
eventDiscussion?: boolean eventDiscussion?: boolean
hasPoll?: boolean hasPoll?: boolean
autoFocus?: boolean autoFocus?: boolean
handleSubmit?: () => void
onFocus?: React.FocusEventHandler<HTMLDivElement> onFocus?: React.FocusEventHandler<HTMLDivElement>
onPaste?: (files: FileList) => void onPaste?: (files: FileList) => void
placeholder?: JSX.Element | string placeholder?: JSX.Element | string
@ -66,6 +53,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
eventDiscussion, eventDiscussion,
hasPoll, hasPoll,
autoFocus, autoFocus,
handleSubmit,
onFocus, onFocus,
onPaste, onPaste,
placeholder, placeholder,
@ -154,7 +142,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
<div className={clsx('lexical relative', className)} data-markup> <div className={clsx('lexical relative', className)} data-markup>
<RichTextPlugin <RichTextPlugin
contentEditable={ contentEditable={
<div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste}> <div className='editor' ref={onRef} onFocus={onFocus} onPaste={handlePaste} onSubmit={() => alert('xd')}>
<ContentEditable <ContentEditable
className={clsx('mr-4 outline-none transition-[min-height] motion-reduce:transition-none', { className={clsx('mr-4 outline-none transition-[min-height] motion-reduce:transition-none', {
'min-fh-[40px]': condensed, 'min-fh-[40px]': condensed,
@ -193,7 +181,7 @@ const ComposeEditor = React.forwardRef<string, IComposeEditor>(({
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} /> <FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
</> </>
)} )}
<StatePlugin composeId={composeId} /> <StatePlugin composeId={composeId} handleSubmit={handleSubmit} />
</div> </div>
</LexicalComposer> </LexicalComposer>
); );

View file

@ -37,17 +37,17 @@ import { $createEmojiNode } from '../nodes/emoji-node';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input'; import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
export type QueryMatch = { type QueryMatch = {
leadOffset: number leadOffset: number
matchingString: string matchingString: string
}; };
export type Resolution = { type Resolution = {
match: QueryMatch match: QueryMatch
getRect: () => DOMRect getRect: () => DOMRect
}; };
export type MenuRenderFn = ( type MenuRenderFn = (
anchorElementRef: MutableRefObject<HTMLElement | null>, anchorElementRef: MutableRefObject<HTMLElement | null>,
) => ReactPortal | JSX.Element | null; ) => ReactPortal | JSX.Element | null;
@ -102,7 +102,7 @@ const startTransition = (callback: () => void) => {
}; };
// Got from https://stackoverflow.com/a/42543908/2013580 // Got from https://stackoverflow.com/a/42543908/2013580
export const getScrollParent = ( const getScrollParent = (
element: HTMLElement, element: HTMLElement,
includeHidden: boolean, includeHidden: boolean,
): HTMLElement | HTMLBodyElement => { ): HTMLElement | HTMLBodyElement => {
@ -142,7 +142,7 @@ const isTriggerVisibleInNearestScrollContainer = (
}; };
// Reposition the menu on scroll, window resize, and element resize. // Reposition the menu on scroll, window resize, and element resize.
export const useDynamicPositioning = ( const useDynamicPositioning = (
resolution: Resolution | null, resolution: Resolution | null,
targetElement: HTMLElement | null, targetElement: HTMLElement | null,
onReposition: () => void, onReposition: () => void,
@ -269,13 +269,13 @@ const useMenuAnchorRef = (
return anchorElementRef; return anchorElementRef;
}; };
export type AutosuggestPluginProps = { type AutosuggestPluginProps = {
composeId: string composeId: string
suggestionsHidden: boolean suggestionsHidden: boolean
setSuggestionsHidden: (value: boolean) => void setSuggestionsHidden: (value: boolean) => void
}; };
export const AutosuggestPlugin = ({ const AutosuggestPlugin = ({
composeId, composeId,
suggestionsHidden, suggestionsHidden,
setSuggestionsHidden, setSuggestionsHidden,
@ -469,3 +469,5 @@ export const AutosuggestPlugin = ({
/> />
); );
}; };
export default AutosuggestPlugin;

View file

@ -14,16 +14,16 @@ import { $createMentionNode, MentionNode } from '../nodes/mention-node';
import type { TextNode } from 'lexical'; import type { TextNode } from 'lexical';
export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i'); const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');
export const getMentionMatch = (text: string) => { const getMentionMatch = (text: string) => {
const matchArr = MENTION_REGEX.exec(text); const matchArr = MENTION_REGEX.exec(text);
if (!matchArr) return null; if (!matchArr) return null;
return matchArr; return matchArr;
}; };
export const MentionPlugin = (): JSX.Element | null => { const MentionPlugin = (): JSX.Element | null => {
const [editor] = useLexicalComposerContext(); const [editor] = useLexicalComposerContext();
useEffect(() => { useEffect(() => {
@ -58,3 +58,5 @@ export const MentionPlugin = (): JSX.Element | null => {
return null; return null;
}; };
export default MentionPlugin;

View file

@ -0,0 +1,33 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { KEY_ENTER_COMMAND } from 'lexical';
import { useEffect } from 'react';
import { setEditorState } from 'soapbox/actions/compose';
import { useAppDispatch } from 'soapbox/hooks';
interface IStatePlugin {
composeId: string
handleSubmit?: () => void
}
const StatePlugin = ({ composeId, handleSubmit }: IStatePlugin) => {
const dispatch = useAppDispatch();
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (handleSubmit) editor.registerCommand(KEY_ENTER_COMMAND, (event) => {
if (event?.ctrlKey) {
handleSubmit();
return true;
}
return false;
}, 1);
editor.registerUpdateListener(({ editorState }) => {
dispatch(setEditorState(composeId, editorState.isEmpty() ? null : JSON.stringify(editorState.toJSON())));
});
}, [editor]);
return null;
};
export default StatePlugin;

View file

@ -242,6 +242,7 @@ const ComposeEventModal: React.FC<IComposeEventModal> = ({ onClose }) => {
className='block w-full rounded-md border-gray-400 bg-white px-3 py-2 text-base text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm' className='block w-full rounded-md border-gray-400 bg-white px-3 py-2 text-base text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
composeId='compose-event-modal' composeId='compose-event-modal'
placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)} placeholder={intl.formatMessage(messages.eventDescriptionPlaceholder)}
handleSubmit={handleSubmit}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup