WIP mentions plugin
Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
parent
84fa5bf333
commit
f81b5d9aa8
4 changed files with 134 additions and 2 deletions
|
@ -27,7 +27,7 @@ import { useAppDispatch, useFeatures } from 'soapbox/hooks';
|
||||||
import nodes from './nodes';
|
import nodes from './nodes';
|
||||||
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 NewMentionsPlugin from './plugins/mention-plugin';
|
import { MentionPlugin } from './plugins/mention-plugin';
|
||||||
|
|
||||||
const StatePlugin = ({ composeId, autoFocus }: { composeId: string, autoFocus: boolean }) => {
|
const StatePlugin = ({ composeId, autoFocus }: { composeId: string, autoFocus: boolean }) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
@ -55,6 +55,7 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
|
||||||
nodes,
|
nodes,
|
||||||
theme: {
|
theme: {
|
||||||
hashtag: 'hover:underline text-primary-600 dark:text-accent-blue hover:text-primary-800 dark:hover:text-accent-blue',
|
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: {
|
text: {
|
||||||
bold: 'font-bold',
|
bold: 'font-bold',
|
||||||
code: 'font-mono',
|
code: 'font-mono',
|
||||||
|
@ -130,12 +131,12 @@ const ComposeEditor = React.forwardRef<string, any>(({ composeId, condensed, onF
|
||||||
/>
|
/>
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
<HashtagPlugin />
|
<HashtagPlugin />
|
||||||
|
<MentionPlugin />
|
||||||
{features.richText && <LinkPlugin />}
|
{features.richText && <LinkPlugin />}
|
||||||
{features.richText && floatingAnchorElem && (
|
{features.richText && floatingAnchorElem && (
|
||||||
<>
|
<>
|
||||||
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
<FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} />
|
||||||
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
|
<FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} />
|
||||||
<NewMentionsPlugin />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<StatePlugin composeId={composeId} autoFocus={autoFocus} />
|
<StatePlugin composeId={composeId} autoFocus={autoFocus} />
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { AutoLinkNode, LinkNode } from '@lexical/link';
|
||||||
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
import { HorizontalRuleNode } from '@lexical/react/LexicalHorizontalRuleNode';
|
||||||
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
import { HeadingNode, QuoteNode } from '@lexical/rich-text';
|
||||||
|
|
||||||
|
import { MentionNode } from './nodes/mention-node';
|
||||||
|
|
||||||
import type { Klass, LexicalNode } from 'lexical';
|
import type { Klass, LexicalNode } from 'lexical';
|
||||||
|
|
||||||
const ComposeNodes: Array<Klass<LexicalNode>> = [
|
const ComposeNodes: Array<Klass<LexicalNode>> = [
|
||||||
|
@ -24,6 +26,7 @@ const ComposeNodes: Array<Klass<LexicalNode>> = [
|
||||||
LinkNode,
|
LinkNode,
|
||||||
HorizontalRuleNode,
|
HorizontalRuleNode,
|
||||||
HashtagNode,
|
HashtagNode,
|
||||||
|
MentionNode,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default ComposeNodes;
|
export default ComposeNodes;
|
||||||
|
|
73
app/soapbox/features/compose/editor/nodes/mention-node.tsx
Normal file
73
app/soapbox/features/compose/editor/nodes/mention-node.tsx
Normal 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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue