wip
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
820ead9932
commit
7681134d7f
7 changed files with 169 additions and 24 deletions
|
@ -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} />
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
||||
|
|
71
app/soapbox/features/compose/editor/nodes/emoji-node.tsx
Normal file
71
app/soapbox/features/compose/editor/nodes/emoji-node.tsx
Normal 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 };
|
|
@ -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 };
|
||||
|
|
|
@ -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 &&
|
||||
|
|
61
app/soapbox/features/compose/editor/plugins/emoji-plugin.tsx
Normal file
61
app/soapbox/features/compose/editor/plugins/emoji-plugin.tsx
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue