Skip to content

Commit

Permalink
feat(Mentions): fix copy paste and support history
Browse files Browse the repository at this point in the history
  • Loading branch information
Carrotzpc committed Jul 10, 2024
1 parent 9522d3a commit a7d91fa
Show file tree
Hide file tree
Showing 9 changed files with 101 additions and 117 deletions.
12 changes: 10 additions & 2 deletions src/Mentions/demos/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,22 @@ export default () => {
}}
defaultValue="👋,I'm {{1.zhang}}"
options={[
{ label: 'zhang', value: '1.zhang', icon: <Icon icon={Smile} /> },
{
label: 'zhang',
value: '1.zhang',
icon: <Icon icon={Smile} />,
},
{
label: 'luobo',
value: '2.luobo',
icon: <Icon icon={Carrot} />,
error: '选我触发错误样式',
},
{ label: 'yunti', value: '3.yunti', icon: <Icon icon={Cloud} /> },
{
label: 'yunti',
value: '3.yunti',
icon: <Icon icon={Cloud} />,
},
]}
preTriggerChars=".*"
triggers={['@']}
Expand Down
79 changes: 42 additions & 37 deletions src/Mentions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LexicalComposer } from '@lexical/react/LexicalComposer';
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
import type { EditorState } from 'lexical';
Expand All @@ -15,6 +16,7 @@ import {
} from './plugins/mention-node';
import { MentionPickerPlugin, type MentionPickerPluginProps } from './plugins/mention-picker';
import OnBlurBlock from './plugins/on-blur-or-focus-block';
import { MentionsConfigProvider } from './provider';
import { useStyles } from './style';
import type { AutoSize, MentionsOptionsMap } from './types';
import { textToEditorState } from './utils';
Expand Down Expand Up @@ -73,8 +75,8 @@ export const Mentions: React.FC<MentionsProps> = ({
onError: (error: Error) => {
throw error;
},
// eslint-disable-next-line react-hooks/exhaustive-deps
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);

Expand All @@ -97,42 +99,45 @@ export const Mentions: React.FC<MentionsProps> = ({

return (
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
<div className={cx(styles.wrapper, wrapperClassname)}>
<RichTextPlugin
ErrorBoundary={LexicalErrorBoundary}
contentEditable={
<ContentEditable
className={cx(
{
[styles.root]: true,
[styles.filled]: variant === 'filled',
[styles.borderless]: variant === 'borderless',
[styles.disabled]: disabled,
},
className
)}
style={style || {}}
/>
}
placeholder={
<div className={styles.placeholder}>
{placeholder || `输入 ${triggers.join(' 或 ')} 插入引用`}
</div>
}
/>
<MentionPickerPlugin
allowSpaces={allowSpaces}
onSelect={onSelect}
options={options}
preTriggerChars={preTriggerChars}
punctuation={punctuation}
triggers={triggers}
/>
<MentionNodePlugin optionsMap={optionsMap} />
<MentionNodePluginReplacement optionsMap={optionsMap} />
<OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
</div>
<MentionsConfigProvider value={{ optionsMap }}>
<div className={cx(styles.wrapper, wrapperClassname)}>
<RichTextPlugin
ErrorBoundary={LexicalErrorBoundary}
contentEditable={
<ContentEditable
className={cx(
{
[styles.root]: true,
[styles.filled]: variant === 'filled',
[styles.borderless]: variant === 'borderless',
[styles.disabled]: disabled,
},
className
)}
style={style || {}}
/>
}
placeholder={
<div className={styles.placeholder}>
{placeholder || `输入 ${triggers.join(' 或 ')} 插入引用`}
</div>
}
/>
<MentionPickerPlugin
allowSpaces={allowSpaces}
onSelect={onSelect}
options={options}
preTriggerChars={preTriggerChars}
punctuation={punctuation}
triggers={triggers}
/>
<MentionNodePlugin />
<MentionNodePluginReplacement />
<HistoryPlugin />
<OnChangePlugin onChange={handleEditorChange} />
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
</div>
</MentionsConfigProvider>
</LexicalComposer>
);
};
32 changes: 9 additions & 23 deletions src/Mentions/plugins/mention-node/component.tsx
Original file line number Diff line number Diff line change
@@ -1,47 +1,33 @@
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
import { mergeRegister } from '@lexical/utils';
import { Icon } from '@lobehub/ui';
import { Flex, Tooltip } from 'antd';
import { COMMAND_PRIORITY_EDITOR } from 'lexical';
import { CircleAlert } from 'lucide-react';
import React, { memo, useEffect, useState } from 'react';
import React, { memo, useEffect } from 'react';

import { useOptionsMap } from '@/Mentions/provider';

import { useSelectOrDelete } from '../../hooks';
import { MentionsOptionsMap } from '../../types';
import { MentionNode } from './node';
import { useStyles } from './style';
import { DELETE_MENTION_COMMAND, UPDATE_MENTIONS_OPTIONS } from './utils';
import { DELETE_MENTION_COMMAND } from './utils';

export interface MentionNodeComponentProps {
nodeKey: string;
variable: string;
optionsMap: MentionsOptionsMap;
}

export const MentionNodeComponent: React.FC<MentionNodeComponentProps> = memo(
({ nodeKey, variable, optionsMap = {} }) => {
({ nodeKey, variable }) => {
const optionsMap = useOptionsMap();
const [editor] = useLexicalComposerContext();
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_MENTION_COMMAND);
const [localMentionsOptionsMap, setLocalMentionsOptionsMap] =
useState<MentionsOptionsMap>(optionsMap);
const option = localMentionsOptionsMap?.[variable];
const option = optionsMap?.[variable];
const { styles } = useStyles({ isSelected, isError: !option || !!option.error });

useEffect(() => {
if (!editor.hasNodes([MentionNode]))
if (!editor.hasNodes([MentionNode])) {
throw new Error('MentionsNodePlugin: MentionNode not registered on editor');

return mergeRegister(
editor.registerCommand(
UPDATE_MENTIONS_OPTIONS,
(newOptionsMap: MentionsOptionsMap) => {
setLocalMentionsOptionsMap(newOptionsMap);

return true;
},
COMMAND_PRIORITY_EDITOR
)
);
}
}, [editor]);

const Item = (
Expand Down
24 changes: 5 additions & 19 deletions src/Mentions/plugins/mention-node/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@ import { mergeRegister } from '@lexical/utils';
import { $insertNodes, COMMAND_PRIORITY_EDITOR } from 'lexical';
import React, { memo, useEffect } from 'react';

import { MentionsOptionsMap } from '../../types';
import { $createMentionNode, MentionNode } from './node';
import {
CLEAR_HIDE_MENU_TIMEOUT,
DELETE_MENTION_COMMAND,
INSERT_MENTION_COMMAND,
UPDATE_MENTIONS_OPTIONS,
} from './utils';
import { CLEAR_HIDE_MENU_TIMEOUT, DELETE_MENTION_COMMAND, INSERT_MENTION_COMMAND } from './utils';

export * from './node';
export * from './replacement';
Expand All @@ -19,18 +13,11 @@ export * from './utils';
export interface MentionNodePluginProps {
onInsert?: () => void;
onDelete?: () => void;
optionsMap: MentionsOptionsMap;
}
export const MentionNodePlugin: React.FC<MentionNodePluginProps> = memo(
({ optionsMap, onInsert, onDelete }) => {
({ onInsert, onDelete }) => {
const [editor] = useLexicalComposerContext();

useEffect(() => {
editor.update(() => {
editor.dispatchCommand(UPDATE_MENTIONS_OPTIONS, optionsMap);
});
}, [editor, optionsMap]);

useEffect(() => {
if (!editor.hasNodes([MentionNode]))
throw new Error('MentionsNodePlugin: MentionNode not registered on editor');
Expand All @@ -39,9 +26,8 @@ export const MentionNodePlugin: React.FC<MentionNodePluginProps> = memo(
editor.registerCommand(
INSERT_MENTION_COMMAND,
(variable: string) => {
// eslint-disable-next-line unicorn/no-useless-undefined
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, undefined);
const mentionNode = $createMentionNode(variable, optionsMap);
editor.dispatchCommand(CLEAR_HIDE_MENU_TIMEOUT, null);
const mentionNode = $createMentionNode(variable);

$insertNodes([mentionNode]);
if (onInsert) onInsert();
Expand All @@ -60,7 +46,7 @@ export const MentionNodePlugin: React.FC<MentionNodePluginProps> = memo(
COMMAND_PRIORITY_EDITOR
)
);
}, [editor, onInsert, onDelete, optionsMap]);
}, [editor, onInsert, onDelete]);

return null;
}
Expand Down
33 changes: 6 additions & 27 deletions src/Mentions/plugins/mention-node/node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,34 @@ import { MentionNodeComponent } from './component';

export type SerializedNode = SerializedLexicalNode & {
variable: string;
optionsMap: MentionsOptionsMap;
};

export class MentionNode extends DecoratorNode<JSX.Element> {
__variable: string;
__optionsMap: MentionsOptionsMap;

static getType(): string {
return 'mention-node';
}

static clone(node: MentionNode): MentionNode {
return new MentionNode(node.__variable, node.__optionsMap);
return new MentionNode(node.__variable);
}

isInline(): boolean {
return true;
}

constructor(variable: string, optionsMap: MentionsOptionsMap, key?: NodeKey) {
constructor(variable: string, optionsMap?: MentionsOptionsMap, key?: NodeKey) {
super(key);

this.__variable = variable;
this.__optionsMap = optionsMap;
}

createDOM(): HTMLElement {
const div = document.createElement('div');
div.style.display = 'inline-flex';
div.style.alignItems = 'center';
div.style.verticalAlign = 'middle';
// div.style['align-items'] = 'center';
// div.style['vertical-align'] = 'middle';

return div;
}

Expand All @@ -49,18 +43,12 @@ export class MentionNode extends DecoratorNode<JSX.Element> {
}

decorate(): JSX.Element {
return (
<MentionNodeComponent
nodeKey={this.getKey()}
optionsMap={this.__optionsMap}
variable={this.__variable}
/>
);
return <MentionNodeComponent nodeKey={this.getKey()} variable={this.__variable} />;
}

static importJSON(serializedNode: SerializedNode): MentionNode {
// eslint-disable-next-line @typescript-eslint/no-use-before-define
const node = $createMentionNode(serializedNode.variable, serializedNode.optionsMap);
const node = $createMentionNode(serializedNode.variable);

return node;
}
Expand All @@ -70,7 +58,6 @@ export class MentionNode extends DecoratorNode<JSX.Element> {
type: 'mention-node',
version: 1,
variable: this.getVariable(),
optionsMap: this.getOptionsMap(),
};
}

Expand All @@ -79,21 +66,13 @@ export class MentionNode extends DecoratorNode<JSX.Element> {
return self.__variable;
}

getOptionsMap(): MentionsOptionsMap {
const self = this.getLatest();
return self.__optionsMap;
}

getTextContent(): string {
return `{{${this.getVariable()}}}`;
}
}

export function $createMentionNode(
variable: string,
workflowNodesMap: MentionsOptionsMap
): MentionNode {
return new MentionNode(variable, workflowNodesMap);
export function $createMentionNode(variable: string): MentionNode {
return new MentionNode(variable);
}

export function $isMentionNode(
Expand Down
8 changes: 3 additions & 5 deletions src/Mentions/plugins/mention-node/replacement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,17 @@ import type { TextNode } from 'lexical';
import { $applyNodeReplacement } from 'lexical';
import React, { memo, useCallback, useEffect } from 'react';

import { MentionsOptionsMap } from '../../types';
import { decoratorTransform } from '../../utils';
import { CustomTextNode } from '../custom-text/node';
import { $createMentionNode, MentionNode } from './node';
import { MENTION_REGEX } from './utils';

export interface MentionNodePluginReplacementProps {
optionsMap: MentionsOptionsMap;
onInsert?: () => void;
}

export const MentionNodePluginReplacement: React.FC<MentionNodePluginReplacementProps> = memo(
({ optionsMap, onInsert }) => {
({ onInsert }) => {
const [editor] = useLexicalComposerContext();

useEffect(() => {
Expand All @@ -29,9 +27,9 @@ export const MentionNodePluginReplacement: React.FC<MentionNodePluginReplacement
if (onInsert) onInsert();

const nodePathString = textNode.getTextContent().slice(2, -2);
return $applyNodeReplacement($createMentionNode(nodePathString, optionsMap));
return $applyNodeReplacement($createMentionNode(nodePathString));
},
[onInsert, optionsMap]
[onInsert]
);

const getMatch = useCallback((text: string) => {
Expand Down
6 changes: 3 additions & 3 deletions src/Mentions/plugins/mention-node/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,8 @@ export const useStyles = createStyles(
}
if (isSelected) {
return {
// background: token.colorInfoBgHover,
background: token.colorInfoBg,
background: token.colorInfoBgHover,
border: token.colorInfoBorder,
// color: token.colorInfoTextActive,
color: token.colorInfoText,
};
}
Expand All @@ -38,6 +36,8 @@ export const useStyles = createStyles(
const { background, border, color } = getColors();
return {
root: css`
user-select: none;
margin: 1px 2px;
padding: 0 4px;
Expand Down
Loading

0 comments on commit a7d91fa

Please sign in to comment.