mirror of https://github.com/fantasticit/think.git
tiptap: use shared commands
This commit is contained in:
parent
bfddd4cc6e
commit
a589db9978
|
@ -4,7 +4,7 @@ import Suggestion from '@tiptap/suggestion';
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
import { Plugin, PluginKey } from 'prosemirror-state';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
import { EXTENSION_PRIORITY_HIGHEST } from 'tiptap/core/constants';
|
||||||
import { QUICK_INSERT_ITEMS } from 'tiptap/core/menus/quick-insert';
|
import { insertMenuLRUCache, QUICK_INSERT_COMMANDS, transformToCommands } from 'tiptap/core/menus/commands';
|
||||||
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
import { MenuList } from 'tiptap/core/wrappers/menu-list';
|
||||||
|
|
||||||
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
export const QuickInsertPluginKey = new PluginKey('quickInsert');
|
||||||
|
@ -25,7 +25,8 @@ export const QuickInsert = Node.create({
|
||||||
const $from = state.selection.$from;
|
const $from = state.selection.$from;
|
||||||
const tr = state.tr.deleteRange($from.start(), $from.pos);
|
const tr = state.tr.deleteRange($from.start(), $from.pos);
|
||||||
dispatch(tr);
|
dispatch(tr);
|
||||||
props?.command(editor, props.user);
|
props?.action(editor, props.user);
|
||||||
|
insertMenuLRUCache.put(props.label);
|
||||||
editor?.view?.focus();
|
editor?.view?.focus();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -33,57 +34,26 @@ export const QuickInsert = Node.create({
|
||||||
},
|
},
|
||||||
|
|
||||||
addProseMirrorPlugins() {
|
addProseMirrorPlugins() {
|
||||||
const { editor } = this;
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Suggestion({
|
Suggestion({
|
||||||
editor: this.editor,
|
editor: this.editor,
|
||||||
...this.options.suggestion,
|
...this.options.suggestion,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: new PluginKey('evokeMenuPlaceholder'),
|
key: new PluginKey('evokeMenuPlaceholder'),
|
||||||
props: {
|
|
||||||
// decorations: (state) => {
|
|
||||||
// if (!editor.isEditable) return;
|
|
||||||
// const parent = findParentNode((node) => node.type.name === 'paragraph')(state.selection);
|
|
||||||
// if (!parent) {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// const decorations: Decoration[] = [];
|
|
||||||
// const isEmpty = parent && parent.node.content.size === 0;
|
|
||||||
// const isSlash = parent && parent.node.textContent === '/';
|
|
||||||
// const isTopLevel = state.selection.$from.depth === 1;
|
|
||||||
// const hasOtherChildren = parent && parent.node.content.childCount > 1;
|
|
||||||
// if (isTopLevel) {
|
|
||||||
// if (isEmpty) {
|
|
||||||
// decorations.push(
|
|
||||||
// Decoration.node(parent.pos, parent.pos + parent.node.nodeSize, {
|
|
||||||
// 'class': 'is-empty',
|
|
||||||
// 'data-placeholder': '输入 / 唤起更多',
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// if (isSlash && !hasOtherChildren) {
|
|
||||||
// decorations.push(
|
|
||||||
// Decoration.node(parent.pos, parent.pos + parent.node.nodeSize, {
|
|
||||||
// 'class': 'is-empty',
|
|
||||||
// 'data-placeholder': ` 继续输入进行过滤`,
|
|
||||||
// })
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// return DecorationSet.create(state.doc, decorations);
|
|
||||||
// }
|
|
||||||
// return null;
|
|
||||||
// },
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
}).configure({
|
}).configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: ({ query }) => {
|
items: ({ query }) => {
|
||||||
return QUICK_INSERT_ITEMS.filter((command) => command.key.startsWith(query));
|
const recentUsed = insertMenuLRUCache.get() as string[];
|
||||||
|
const restCommands = QUICK_INSERT_COMMANDS.filter((command) => {
|
||||||
|
return !('title' in command) && !('custom' in command) && !recentUsed.includes(command.label);
|
||||||
|
});
|
||||||
|
return [...transformToCommands(recentUsed), ...restCommands].filter(
|
||||||
|
(command) => !('title' in command) && command.label && command.label.startsWith(query)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
render: () => {
|
render: () => {
|
||||||
let component;
|
let component;
|
||||||
|
@ -130,9 +100,8 @@ export const QuickInsert = Node.create({
|
||||||
return component.ref?.onKeyDown(props);
|
return component.ref?.onKeyDown(props);
|
||||||
},
|
},
|
||||||
|
|
||||||
onExit(props) {
|
onExit() {
|
||||||
if (!isEditable) return;
|
if (!isEditable) return;
|
||||||
|
|
||||||
popup[0].destroy();
|
popup[0].destroy();
|
||||||
component.destroy();
|
component.destroy();
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,183 @@
|
||||||
|
import { Dropdown, Popover } from '@douyinfe/semi-ui';
|
||||||
|
import { IUser } from '@think/domains';
|
||||||
|
import { GridSelect } from 'components/grid-select';
|
||||||
|
import {
|
||||||
|
IconAttachment,
|
||||||
|
IconCallout,
|
||||||
|
IconCodeBlock,
|
||||||
|
IconCountdown,
|
||||||
|
IconDocument,
|
||||||
|
IconFlow,
|
||||||
|
IconImage,
|
||||||
|
IconLink,
|
||||||
|
IconMath,
|
||||||
|
IconMind,
|
||||||
|
IconStatus,
|
||||||
|
IconTable,
|
||||||
|
IconTableOfContents,
|
||||||
|
} from 'components/icons';
|
||||||
|
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
||||||
|
import { Editor } from 'tiptap/core';
|
||||||
|
|
||||||
|
import { createCountdown } from './countdown/service';
|
||||||
|
|
||||||
|
export type ITitle = {
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IBaseCommand = {
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
user?: IUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
type IAction = (editor: Editor, user?: IUser) => void;
|
||||||
|
|
||||||
|
export type ILabelRenderCommand = IBaseCommand & {
|
||||||
|
action: IAction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ICustomRenderCommand = IBaseCommand & {
|
||||||
|
custom: (editor: Editor, runCommand: (arg: { label: string; action: IAction }) => any) => React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ICommand = ITitle | ILabelRenderCommand | ICustomRenderCommand;
|
||||||
|
|
||||||
|
export const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
|
||||||
|
|
||||||
|
export const COMMANDS: ICommand[] = [
|
||||||
|
{
|
||||||
|
title: '通用',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconTableOfContents />,
|
||||||
|
label: '目录',
|
||||||
|
action: (editor) => editor.chain().focus().setTableOfContents().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconTable />,
|
||||||
|
label: '表格',
|
||||||
|
custom: (editor, runCommand) => (
|
||||||
|
<Popover
|
||||||
|
key="table"
|
||||||
|
showArrow
|
||||||
|
position="rightTop"
|
||||||
|
zIndex={10000}
|
||||||
|
content={
|
||||||
|
<div style={{ padding: 0 }}>
|
||||||
|
<GridSelect
|
||||||
|
onSelect={({ rows, cols }) => {
|
||||||
|
return runCommand({
|
||||||
|
label: '表格',
|
||||||
|
action: () => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(),
|
||||||
|
})();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Dropdown.Item>
|
||||||
|
<IconTable />
|
||||||
|
表格
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Popover>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconCodeBlock />,
|
||||||
|
label: '代码块',
|
||||||
|
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconImage />,
|
||||||
|
label: '图片',
|
||||||
|
// @ts-ignore
|
||||||
|
action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconAttachment />,
|
||||||
|
label: '附件',
|
||||||
|
action: (editor) => editor.chain().focus().setAttachment().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconCountdown />,
|
||||||
|
label: '倒计时',
|
||||||
|
action: (editor) => createCountdown(editor),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconLink />,
|
||||||
|
label: '外链',
|
||||||
|
action: (editor) => editor.chain().focus().setIframe({ url: '' }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '卡片',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconFlow />,
|
||||||
|
label: '流程图',
|
||||||
|
action: (editor) => {
|
||||||
|
editor.chain().focus().setFlow({ width: '100%' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconMind />,
|
||||||
|
label: '思维导图',
|
||||||
|
action: (editor) => {
|
||||||
|
// @ts-ignore
|
||||||
|
editor.chain().focus().setMind({ width: '100%' }).run();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconMath />,
|
||||||
|
label: '数学公式',
|
||||||
|
action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.name }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconStatus />,
|
||||||
|
label: '状态',
|
||||||
|
action: (editor, user) =>
|
||||||
|
editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.name }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconCallout />,
|
||||||
|
label: '高亮块',
|
||||||
|
action: (editor) => editor.chain().focus().setCallout().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '内容引用',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconDocument />,
|
||||||
|
label: '文档',
|
||||||
|
action: (editor) => editor.chain().focus().setDocumentReference().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <IconDocument />,
|
||||||
|
label: '子文档',
|
||||||
|
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const QUICK_INSERT_COMMANDS = [
|
||||||
|
...COMMANDS.slice(0, 1),
|
||||||
|
{
|
||||||
|
icon: <IconTable />,
|
||||||
|
label: '表格',
|
||||||
|
action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||||
|
},
|
||||||
|
...COMMANDS.slice(3),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const transformToCommands = (data: string[]) => {
|
||||||
|
return data
|
||||||
|
.map((label) => {
|
||||||
|
return COMMANDS.find((command) => {
|
||||||
|
if ('title' in command) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command.label === label;
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
};
|
|
@ -1,146 +1,14 @@
|
||||||
import { IconPlus } from '@douyinfe/semi-icons';
|
import { IconPlus } from '@douyinfe/semi-icons';
|
||||||
import { Button, Dropdown, Popover } from '@douyinfe/semi-ui';
|
import { Button, Dropdown } from '@douyinfe/semi-ui';
|
||||||
import { GridSelect } from 'components/grid-select';
|
|
||||||
import {
|
|
||||||
IconAttachment,
|
|
||||||
IconCallout,
|
|
||||||
IconCodeBlock,
|
|
||||||
IconCountdown,
|
|
||||||
IconDocument,
|
|
||||||
IconFlow,
|
|
||||||
IconImage,
|
|
||||||
IconLink,
|
|
||||||
IconMath,
|
|
||||||
IconMind,
|
|
||||||
IconStatus,
|
|
||||||
IconTable,
|
|
||||||
IconTableOfContents,
|
|
||||||
} from 'components/icons';
|
|
||||||
import { Tooltip } from 'components/tooltip';
|
import { Tooltip } from 'components/tooltip';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache';
|
|
||||||
import { useToggle } from 'hooks/use-toggle';
|
import { useToggle } from 'hooks/use-toggle';
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { Editor } from 'tiptap/core';
|
import { Editor } from 'tiptap/core';
|
||||||
import { Title } from 'tiptap/core/extensions/title';
|
import { Title } from 'tiptap/core/extensions/title';
|
||||||
import { useActive } from 'tiptap/core/hooks/use-active';
|
import { useActive } from 'tiptap/core/hooks/use-active';
|
||||||
|
|
||||||
import { createCountdown } from '../countdown/service';
|
import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands';
|
||||||
|
|
||||||
const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3);
|
|
||||||
|
|
||||||
const COMMANDS = [
|
|
||||||
{
|
|
||||||
title: '通用',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconTableOfContents />,
|
|
||||||
label: '目录',
|
|
||||||
action: (editor) => editor.chain().focus().setTableOfContents().run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconTable />,
|
|
||||||
label: '表格',
|
|
||||||
custom: (editor, runCommand) => (
|
|
||||||
<Popover
|
|
||||||
key="table"
|
|
||||||
showArrow
|
|
||||||
position="rightTop"
|
|
||||||
zIndex={10000}
|
|
||||||
content={
|
|
||||||
<div style={{ padding: 0 }}>
|
|
||||||
<GridSelect
|
|
||||||
onSelect={({ rows, cols }) => {
|
|
||||||
return runCommand({
|
|
||||||
label: '表格',
|
|
||||||
action: () => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(),
|
|
||||||
})();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Dropdown.Item>
|
|
||||||
<IconTable />
|
|
||||||
表格
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Popover>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconCodeBlock />,
|
|
||||||
label: '代码块',
|
|
||||||
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconImage />,
|
|
||||||
label: '图片',
|
|
||||||
action: (editor) => {
|
|
||||||
editor.chain().focus().setEmptyImage({ width: '100%' }).run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconAttachment />,
|
|
||||||
label: '附件',
|
|
||||||
action: (editor) => editor.chain().focus().setAttachment().run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconCountdown />,
|
|
||||||
label: '倒计时',
|
|
||||||
action: (editor) => createCountdown(editor),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconLink />,
|
|
||||||
label: '外链',
|
|
||||||
action: (editor) => editor.chain().focus().setIframe({ url: '' }).run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '卡片',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconFlow />,
|
|
||||||
label: '流程图',
|
|
||||||
action: (editor) => {
|
|
||||||
editor.chain().focus().setFlow({ width: '100%' }).run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconMind />,
|
|
||||||
label: '思维导图',
|
|
||||||
action: (editor) => {
|
|
||||||
editor.chain().focus().setMind({ width: '100%' }).run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconMath />,
|
|
||||||
label: '数学公式',
|
|
||||||
action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.name }).run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconStatus />,
|
|
||||||
label: '状态',
|
|
||||||
action: (editor, user) =>
|
|
||||||
editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.name }).run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconCallout />,
|
|
||||||
label: '高亮块',
|
|
||||||
action: (editor) => editor.chain().focus().setCallout().run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '内容引用',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconDocument />,
|
|
||||||
label: '文档',
|
|
||||||
action: (editor) => editor.chain().focus().setDocumentReference().run(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: <IconDocument />,
|
|
||||||
label: '子文档',
|
|
||||||
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
@ -153,14 +21,6 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
[recentUsed]
|
[recentUsed]
|
||||||
);
|
);
|
||||||
|
|
||||||
const transformToCommands = useCallback((data: string[]) => {
|
|
||||||
return data
|
|
||||||
.map((label) => {
|
|
||||||
return COMMANDS.find((command) => command.label && command.label === label);
|
|
||||||
})
|
|
||||||
.filter(Boolean);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const runCommand = useCallback(
|
const runCommand = useCallback(
|
||||||
(command) => {
|
(command) => {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -170,14 +30,14 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => {
|
||||||
toggleVisible(false);
|
toggleVisible(false);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
[editor, toggleVisible, transformToCommands, user]
|
[editor, toggleVisible, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!visible) return;
|
if (!visible) return;
|
||||||
insertMenuLRUCache.syncFromStorage();
|
insertMenuLRUCache.syncFromStorage();
|
||||||
setRecentUsed(transformToCommands(insertMenuLRUCache.get() as string[]));
|
setRecentUsed(transformToCommands(insertMenuLRUCache.get() as string[]));
|
||||||
}, [visible, transformToCommands]);
|
}, [visible]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|
|
@ -1,305 +0,0 @@
|
||||||
import { IconList, IconOrderedList } from '@douyinfe/semi-icons';
|
|
||||||
import { Space } from '@douyinfe/semi-ui';
|
|
||||||
import {
|
|
||||||
IconAttachment,
|
|
||||||
IconCallout,
|
|
||||||
IconCodeBlock,
|
|
||||||
IconCountdown,
|
|
||||||
IconDocument,
|
|
||||||
IconFlow,
|
|
||||||
IconHeading1,
|
|
||||||
IconHeading2,
|
|
||||||
IconHeading3,
|
|
||||||
IconHorizontalRule,
|
|
||||||
IconImage,
|
|
||||||
IconLink,
|
|
||||||
IconMath,
|
|
||||||
IconMind,
|
|
||||||
IconQuote,
|
|
||||||
IconStatus,
|
|
||||||
IconTable,
|
|
||||||
IconTableOfContents,
|
|
||||||
IconTask,
|
|
||||||
} from 'components/icons';
|
|
||||||
import { Editor } from 'tiptap/core';
|
|
||||||
|
|
||||||
import { createCountdown } from './countdown/service';
|
|
||||||
import { createOrToggleLink } from './link/service';
|
|
||||||
|
|
||||||
export const QUICK_INSERT_ITEMS = [
|
|
||||||
{
|
|
||||||
key: '标题1',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconHeading1 />
|
|
||||||
标题1
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '标题2',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconHeading2 />
|
|
||||||
标题2
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '标题1',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconHeading3 />
|
|
||||||
标题3
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '无序列表',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconList />
|
|
||||||
无序列表
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleBulletList().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '有序列表',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconOrderedList />
|
|
||||||
有序列表
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleOrderedList().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '任务列表',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconTask />
|
|
||||||
任务列表
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleTaskList().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '链接',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconLink />
|
|
||||||
链接
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => createOrToggleLink(editor),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '引用',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconQuote />
|
|
||||||
引用
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleBlockquote().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '分割线',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconHorizontalRule />
|
|
||||||
分割线
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setHorizontalRule().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '目录',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconTableOfContents />
|
|
||||||
目录
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setTableOfContents().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '表格',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconTable />
|
|
||||||
表格
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '代码块',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconCodeBlock />
|
|
||||||
代码块
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().toggleCodeBlock().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '图片',
|
|
||||||
label: () => (
|
|
||||||
<Space>
|
|
||||||
<IconImage />
|
|
||||||
图片
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setEmptyImage().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '附件',
|
|
||||||
label: () => (
|
|
||||||
<Space>
|
|
||||||
<IconAttachment />
|
|
||||||
附件
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setAttachment().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '倒计时',
|
|
||||||
label: () => (
|
|
||||||
<Space>
|
|
||||||
<IconCountdown />
|
|
||||||
倒计时
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => createCountdown(editor),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '外链',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconLink />
|
|
||||||
外链
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '流程图',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconFlow />
|
|
||||||
流程图
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setFlow({ width: '100%' }).run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '思维导图',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconMind />
|
|
||||||
思维导图
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setMind().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '数学公式',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconMath />
|
|
||||||
数学公式
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor, user) => {
|
|
||||||
console.log('user', user);
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setKatex({
|
|
||||||
defaultShowPicker: true,
|
|
||||||
createUser: user.name,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '状态',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconStatus />
|
|
||||||
状态
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor, user) => {
|
|
||||||
if (!user) return;
|
|
||||||
|
|
||||||
editor
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.setStatus({
|
|
||||||
defaultShowPicker: true,
|
|
||||||
createUser: user.name,
|
|
||||||
})
|
|
||||||
.run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '高亮块',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconCallout />
|
|
||||||
高亮块
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setCallout().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '文档',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconDocument />
|
|
||||||
文档
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setDocumentReference().run(),
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
key: '子文档',
|
|
||||||
label: (
|
|
||||||
<Space>
|
|
||||||
<IconDocument />
|
|
||||||
子文档
|
|
||||||
</Space>
|
|
||||||
),
|
|
||||||
command: (editor: Editor) => editor.chain().focus().setDocumentChildren().run(),
|
|
||||||
},
|
|
||||||
];
|
|
|
@ -1,15 +1,17 @@
|
||||||
|
import { Space } from '@douyinfe/semi-ui';
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core';
|
||||||
import cls from 'classnames';
|
import cls from 'classnames';
|
||||||
import { useUser } from 'data/user';
|
import { useUser } from 'data/user';
|
||||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
||||||
import scrollIntoView from 'scroll-into-view-if-needed';
|
import scrollIntoView from 'scroll-into-view-if-needed';
|
||||||
|
import { ILabelRenderCommand } from 'tiptap/core/menus/commands';
|
||||||
|
|
||||||
import styles from './index.module.scss';
|
import styles from './index.module.scss';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
||||||
items: Array<{ label: React.ReactNode | ((editor: Editor) => React.ReactNode) }>;
|
items: ILabelRenderCommand[];
|
||||||
command: any;
|
command: (command: ILabelRenderCommand) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||||
|
@ -18,12 +20,12 @@ export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
const selectItem = (index) => {
|
const selectItem = (index) => {
|
||||||
const item = props.items[index];
|
const command = props.items[index];
|
||||||
|
|
||||||
if (item) {
|
if (command) {
|
||||||
// @ts-ignore
|
// 注入用户信息
|
||||||
item.user = user; // 注入用户信息
|
command.user = user;
|
||||||
props.command(item);
|
props.command(command);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -72,15 +74,20 @@ export const MenuList: React.FC<IProps> = forwardRef((props, ref) => {
|
||||||
<div className={styles.items}>
|
<div className={styles.items}>
|
||||||
<div ref={$container}>
|
<div ref={$container}>
|
||||||
{props.items.length ? (
|
{props.items.length ? (
|
||||||
props.items.map((item, index) => (
|
props.items.map((item, index) => {
|
||||||
|
return (
|
||||||
<span
|
<span
|
||||||
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
|
className={cls(styles.item, index === selectedIndex ? styles['is-selected'] : '')}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(index)}
|
||||||
>
|
>
|
||||||
{typeof item.label === 'function' ? item.label(props.editor) : item.label}
|
<Space>
|
||||||
|
{item.icon}
|
||||||
|
{item.label}
|
||||||
|
</Space>
|
||||||
</span>
|
</span>
|
||||||
))
|
);
|
||||||
|
})
|
||||||
) : (
|
) : (
|
||||||
<div className={styles.item}>没有找到结果</div>
|
<div className={styles.item}>没有找到结果</div>
|
||||||
)}
|
)}
|
||||||
|
|
Loading…
Reference in New Issue