Lexical: Cleanup, allow ctrl+enter to submit
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
e76a9ec8aa
commit
461a002be9
6 changed files with 57 additions and 30 deletions
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
33
app/soapbox/features/compose/editor/plugins/state-plugin.tsx
Normal file
33
app/soapbox/features/compose/editor/plugins/state-plugin.tsx
Normal 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;
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue