WIP mentions plugin

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2023-03-11 00:17:54 +01:00
parent 84fa5bf333
commit f81b5d9aa8
4 changed files with 134 additions and 2 deletions

View file

@ -27,7 +27,7 @@ import { useAppDispatch, useFeatures } from 'soapbox/hooks';
import nodes from './nodes';
import FloatingLinkEditorPlugin from './plugins/floating-link-editor-plugin';
import FloatingTextFormatToolbarPlugin from './plugins/floating-text-format-toolbar-plugin';
import NewMentionsPlugin from './plugins/mention-plugin';
import { MentionPlugin } from './plugins/mention-plugin';
const StatePlugin = ({ composeId, autoFocus }: { composeId: string, autoFocus: boolean }) => {
const dispatch = useAppDispatch();
@ -55,6 +55,7 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
nodes,
theme: {
hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
mention: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
text: {
bold: 'font-bold',
code: 'font-mono',
@ -130,12 +131,12 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
/>
<HistoryPlugin />
<HashtagPlugin />
<MentionPlugin />
{features.richText && <LinkPlugin />}
{features.richText && floatingAnchorElem && (
<>
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
<NewMentionsPlugin />
</>
)}
<StatePlugin composeId={composeId} autoFocus={autoFocus} />

View file

@ -13,6 +13,8 @@ import { AutoLinkNode, LinkNode } from '@lexical/link';
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
import { MentionNode } from './nodes/mention-node';
import type { Klass, LexicalNode } from 'lexical';
const ComposeNodes: Array<Klass<LexicalNode>> = [
@ -24,6 +26,7 @@ const ComposeNodes: Array<Klass<LexicalNode>> = [
LinkNode,
HorizontalRuleNode,
HashtagNode,
MentionNode,
];
export default ComposeNodes;

View file

@ -0,0 +1,73 @@
/**
* 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';
export class MentionNode extends TextNode {
static getType(): string {
return 'mention';
}
static clone(node: MentionNode): MentionNode {
return new MentionNode(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.mention);
return element;
}
static importJSON(serializedNode: SerializedTextNode): MentionNode {
const node = $createMentionNode(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: 'mention',
};
}
canInsertTextBefore(): boolean {
return false;
}
isTextEntity(): true {
return true;
}
}
export function $createMentionNode(text = ''): MentionNode {
return $applyNodeReplacement(new MentionNode(text));
}
export function $isMentionNode(
node: LexicalNode | null | undefined,
): node is MentionNode {
return node instanceof MentionNode;
}

View file

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