Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-04-01 13:40:15 +02:00
parent 820ead9932
commit 7681134d7f
7 changed files with 169 additions and 24 deletions

View file

@ -15,6 +15,7 @@ import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary';
import { HashtagPlugin } from '@lexical/react/LexicalHashtagPlugin';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import clsx from 'clsx';
@ -28,6 +29,7 @@ import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import nodes from './nodes';
import { AutosuggestPlugin } from './plugins/autosuggest-plugin';
import DraggableBlockPlugin from './plugins/draggable-block-plugin';
import { EmojiPlugin } from './plugins/emoji-plugin';
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
import { MentionPlugin } from './plugins/mention-plugin';
@ -141,9 +143,11 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
/>
<HistoryPlugin />
<HashtagPlugin />
<EmojiPlugin />
<MentionPlugin />
<AutosuggestPlugin composeId={composeId} suggestionsHidden={suggestionsHidden} setSuggestionsHidden={setSuggestionsHidden} />
{features.richText && <LinkPlugin />}
{features.richText && <ListPlugin />}
{features.richText && floatingAnchorElem && (
<>
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />

View file

@ -14,6 +14,7 @@ import { ListItemNode, ListNode } from '@lexical/list';
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { EmojiNode } from './nodes/emoji-node';
import { MentionNode } from './nodes/mention-node';
import type { Klass, LexicalNode } from 'lexical';
@ -29,6 +30,7 @@ const ComposeNodes: Array<Klass<LexicalNode>> = [
ListNode,
HorizontalRuleNode,
HashtagNode,
EmojiNode,
MentionNode,
];

View file

@ -0,0 +1,71 @@
/**
* 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 { addClassNamesToElement } from '@lexical/utils';
import { $applyNodeReplacement, TextNode } from 'lexical';
import type {
EditorConfig,
LexicalNode,
NodeKey,
SerializedTextNode,
} from 'lexical';
class EmojiNode extends TextNode {
static getType(): string {
return 'emoji';
}
static clone(node: EmojiNode): EmojiNode {
return new EmojiNode(node.__text, node.__key);
}
constructor(text: string, key?: NodeKey) {
super(text, key);
}
createDOM(config: EditorConfig): HTMLElement {
const element = super.createDOM(config);
addClassNamesToElement(element, config.theme.emoji);
return element;
}
static importJSON(serializedNode: SerializedTextNode): EmojiNode {
const node = $createEmojiNode(serializedNode.text);
node.setFormat(serializedNode.format);
node.setDetail(serializedNode.detail);
node.setMode(serializedNode.mode);
node.setStyle(serializedNode.style);
return node;
}
exportJSON(): SerializedTextNode {
return {
...super.exportJSON(),
type: 'emoji',
};
}
canInsertTextBefore(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
const $createEmojiNode = (text = ''): EmojiNode => $applyNodeReplacement(new EmojiNode(text).setMode('token'));
const $isEmojiNode = (
node: LexicalNode | null | undefined,
): node is EmojiNode => node instanceof EmojiNode;
export { EmojiNode, $createEmojiNode, $isEmojiNode };

View file

@ -16,7 +16,7 @@ import type {
SerializedTextNode,
} from 'lexical';
export class MentionNode extends TextNode {
class MentionNode extends TextNode {
static getType(): string {
return 'mention';
@ -62,12 +62,10 @@ export class MentionNode extends TextNode {
}
export function $createMentionNode(text = ''): MentionNode {
return $applyNodeReplacement(new MentionNode(text));
}
const $createMentionNode = (text = ''): MentionNode => $applyNodeReplacement(new MentionNode(text));
export function $isMentionNode(
const $isMentionNode = (
node: LexicalNode | null | undefined,
): node is MentionNode {
return node instanceof MentionNode;
}
): node is MentionNode => node instanceof MentionNode;
export { MentionNode, $createMentionNode, $isMentionNode };

View file

@ -26,11 +26,17 @@ import React, {
import ReactDOM from 'react-dom';
import { fetchComposeSuggestions } from 'soapbox/actions/compose';
import AutosuggestEmoji from 'soapbox/components/autosuggest-emoji';
import { useAppDispatch, useCompose } from 'soapbox/hooks';
import AutosuggestAccount from '../../components/autosuggest-account';
import { getMentionMatch } from './mention-plugin';
import { MENTION_REGEX } from './mention-plugin';
import type { AutoSuggestion } from 'soapbox/components/autosuggest-input';
const EMOJI_REGEX = new RegExp('(^|$|(?:^|\\s))([:])([a-z\\d_-]+([:]?))', 'i');
export type QueryMatch = {
leadOffset: number
@ -73,7 +79,7 @@ function getQueryTextForSearch(editor: LexicalEditor): string | null {
const state = editor.getEditorState();
const node = (state._selection as RangeSelection)?.anchor?.getNode();
if (node && node.getType() === 'mention') return node.getTextContent();
if (node && (node.getType() === 'mention' || node.getType() === 'text')) return node.getTextContent();
return null;
}
@ -278,11 +284,6 @@ function useMenuAnchorRef(
return anchorElementRef;
}
export type TriggerFn = (
text: string,
editor: LexicalEditor,
) => QueryMatch | null;
export type AutosuggestPluginProps = {
composeId: string
suggestionsHidden: boolean
@ -324,12 +325,12 @@ export function AutosuggestPlugin({
});
};
const checkForMentionMatch = useCallback((text: string) => {
const matchArr = getMentionMatch(text);
const checkForMatch = useCallback((text: string) => {
const matchArr = MENTION_REGEX.exec(text) || EMOJI_REGEX.exec(text);
if (!matchArr) return null;
dispatch(fetchComposeSuggestions(composeId, matchArr[0]));
dispatch(fetchComposeSuggestions(composeId, matchArr[0]?.trim()));
return {
leadOffset: matchArr.index,
@ -337,9 +338,17 @@ export function AutosuggestPlugin({
};
}, []);
const renderSuggestion = (suggestion: string, i: number) => {
const inner = <AutosuggestAccount id={suggestion} />;
const key = suggestion;
const renderSuggestion = (suggestion: AutoSuggestion, i: number) => {
let inner;
let key;
if (typeof suggestion === 'object') {
inner = <AutosuggestEmoji emoji={suggestion} />;
key = suggestion.id;
} else {
inner = <AutosuggestAccount id={suggestion} />;
key = suggestion;
}
return (
<div
@ -380,7 +389,7 @@ export function AutosuggestPlugin({
return;
}
const match = checkForMentionMatch(text);
const match = checkForMatch(text);
if (
match !== null &&

View file

@ -0,0 +1,61 @@
/**
* 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';
import { useLexicalTextEntity } from '@lexical/react/useLexicalTextEntity';
import { useCallback, useEffect } from 'react';
import { $createEmojiNode, EmojiNode } from '../nodes/emoji-node';
import type { TextNode } from 'lexical';
const REGEX = new RegExp('ggfafsdasdf(^|$|(?:^|\\s))([:])([a-z\\d_-]+([:]))', 'i');
export const getEmojiMatch = (text: string) => {
const matchArr = REGEX.exec(text);
if (!matchArr) return null;
return matchArr;
};
export const EmojiPlugin = (): JSX.Element | null => {
const [editor] = useLexicalComposerContext();
useEffect(() => {
if (!editor.hasNodes([EmojiNode])) {
throw new Error('EmojiPlugin: EmojiNode not registered on editor');
}
}, [editor]);
const createEmojiNode = useCallback((textNode: TextNode): EmojiNode => {
return $createEmojiNode(textNode.getTextContent());
}, []);
const getEntityMatch = useCallback((text: string) => {
const matchArr = getEmojiMatch(text);
if (!matchArr) return null;
const emojiLength = matchArr[3].length + 1;
const startOffset = matchArr.index + matchArr[1].length;
const endOffset = startOffset + emojiLength;
return {
end: endOffset,
start: startOffset,
};
}, []);
useLexicalTextEntity<EmojiNode>(
getEntityMatch,
EmojiNode,
createEmojiNode,
);
return null;
};

View file

@ -15,10 +15,10 @@ import { $createMentionNode, MentionNode } from '../nodes/mention-node';
import type { TextNode } from 'lexical';
const REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');
export const MENTION_REGEX = new RegExp('(^|$|(?:^|\\s))([@])([a-z\\d_-]+(?:@[^@\\s]+)?)', 'i');
export const getMentionMatch = (text: string) => {
const matchArr = REGEX.exec(text);
const matchArr = MENTION_REGEX.exec(text);
if (!matchArr) return null;
return matchArr;