diff --git a/packages/client/src/tiptap/core/extensions/quick-insert.ts b/packages/client/src/tiptap/core/extensions/quick-insert.ts index 92b8224c..03832587 100644 --- a/packages/client/src/tiptap/core/extensions/quick-insert.ts +++ b/packages/client/src/tiptap/core/extensions/quick-insert.ts @@ -4,7 +4,7 @@ import Suggestion from '@tiptap/suggestion'; import { Plugin, PluginKey } from 'prosemirror-state'; import tippy from 'tippy.js'; 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'; export const QuickInsertPluginKey = new PluginKey('quickInsert'); @@ -25,7 +25,8 @@ export const QuickInsert = Node.create({ const $from = state.selection.$from; const tr = state.tr.deleteRange($from.start(), $from.pos); dispatch(tr); - props?.command(editor, props.user); + props?.action(editor, props.user); + insertMenuLRUCache.put(props.label); editor?.view?.focus(); }, }, @@ -33,57 +34,26 @@ export const QuickInsert = Node.create({ }, addProseMirrorPlugins() { - const { editor } = this; - return [ Suggestion({ editor: this.editor, ...this.options.suggestion, }), - new Plugin({ 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({ suggestion: { 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: () => { let component; @@ -130,9 +100,8 @@ export const QuickInsert = Node.create({ return component.ref?.onKeyDown(props); }, - onExit(props) { + onExit() { if (!isEditable) return; - popup[0].destroy(); component.destroy(); }, diff --git a/packages/client/src/tiptap/core/menus/commands.tsx b/packages/client/src/tiptap/core/menus/commands.tsx new file mode 100644 index 00000000..9212152f --- /dev/null +++ b/packages/client/src/tiptap/core/menus/commands.tsx @@ -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: , + label: '目录', + action: (editor) => editor.chain().focus().setTableOfContents().run(), + }, + { + icon: , + label: '表格', + custom: (editor, runCommand) => ( + + { + return runCommand({ + label: '表格', + action: () => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(), + })(); + }} + /> + + } + > + + + 表格 + + + ), + }, + { + icon: , + label: '代码块', + action: (editor) => editor.chain().focus().toggleCodeBlock().run(), + }, + { + icon: , + label: '图片', + // @ts-ignore + action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(), + }, + { + icon: , + label: '附件', + action: (editor) => editor.chain().focus().setAttachment().run(), + }, + { + icon: , + label: '倒计时', + action: (editor) => createCountdown(editor), + }, + { + icon: , + label: '外链', + action: (editor) => editor.chain().focus().setIframe({ url: '' }).run(), + }, + { + title: '卡片', + }, + { + icon: , + label: '流程图', + action: (editor) => { + editor.chain().focus().setFlow({ width: '100%' }).run(); + }, + }, + { + icon: , + label: '思维导图', + action: (editor) => { + // @ts-ignore + editor.chain().focus().setMind({ width: '100%' }).run(); + }, + }, + { + icon: , + label: '数学公式', + action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.name }).run(), + }, + { + icon: , + label: '状态', + action: (editor, user) => + editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.name }).run(), + }, + { + icon: , + label: '高亮块', + action: (editor) => editor.chain().focus().setCallout().run(), + }, + { + title: '内容引用', + }, + { + icon: , + label: '文档', + action: (editor) => editor.chain().focus().setDocumentReference().run(), + }, + { + icon: , + label: '子文档', + action: (editor) => editor.chain().focus().setDocumentChildren().run(), + }, +]; + +export const QUICK_INSERT_COMMANDS = [ + ...COMMANDS.slice(0, 1), + { + icon: , + 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); +}; diff --git a/packages/client/src/tiptap/core/menus/insert/index.tsx b/packages/client/src/tiptap/core/menus/insert/index.tsx index 55df4873..3dacb391 100644 --- a/packages/client/src/tiptap/core/menus/insert/index.tsx +++ b/packages/client/src/tiptap/core/menus/insert/index.tsx @@ -1,146 +1,14 @@ import { IconPlus } from '@douyinfe/semi-icons'; -import { Button, Dropdown, Popover } 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 { Button, Dropdown } from '@douyinfe/semi-ui'; import { Tooltip } from 'components/tooltip'; import { useUser } from 'data/user'; -import { createKeysLocalStorageLRUCache } from 'helpers/lru-cache'; import { useToggle } from 'hooks/use-toggle'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Editor } from 'tiptap/core'; import { Title } from 'tiptap/core/extensions/title'; import { useActive } from 'tiptap/core/hooks/use-active'; -import { createCountdown } from '../countdown/service'; - -const insertMenuLRUCache = createKeysLocalStorageLRUCache('TIPTAP_INSERT_MENU', 3); - -const COMMANDS = [ - { - title: '通用', - }, - { - icon: , - label: '目录', - action: (editor) => editor.chain().focus().setTableOfContents().run(), - }, - { - icon: , - label: '表格', - custom: (editor, runCommand) => ( - - { - return runCommand({ - label: '表格', - action: () => editor.chain().focus().insertTable({ rows, cols, withHeaderRow: true }).run(), - })(); - }} - /> - - } - > - - - 表格 - - - ), - }, - { - icon: , - label: '代码块', - action: (editor) => editor.chain().focus().toggleCodeBlock().run(), - }, - { - icon: , - label: '图片', - action: (editor) => { - editor.chain().focus().setEmptyImage({ width: '100%' }).run(); - }, - }, - { - icon: , - label: '附件', - action: (editor) => editor.chain().focus().setAttachment().run(), - }, - { - icon: , - label: '倒计时', - action: (editor) => createCountdown(editor), - }, - { - icon: , - label: '外链', - action: (editor) => editor.chain().focus().setIframe({ url: '' }).run(), - }, - { - title: '卡片', - }, - { - icon: , - label: '流程图', - action: (editor) => { - editor.chain().focus().setFlow({ width: '100%' }).run(); - }, - }, - { - icon: , - label: '思维导图', - action: (editor) => { - editor.chain().focus().setMind({ width: '100%' }).run(); - }, - }, - { - icon: , - label: '数学公式', - action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.name }).run(), - }, - { - icon: , - label: '状态', - action: (editor, user) => - editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.name }).run(), - }, - { - icon: , - label: '高亮块', - action: (editor) => editor.chain().focus().setCallout().run(), - }, - { - title: '内容引用', - }, - { - icon: , - label: '文档', - action: (editor) => editor.chain().focus().setDocumentReference().run(), - }, - { - icon: , - label: '子文档', - action: (editor) => editor.chain().focus().setDocumentChildren().run(), - }, -]; +import { COMMANDS, insertMenuLRUCache, transformToCommands } from '../commands'; export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { const { user } = useUser(); @@ -153,14 +21,6 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { [recentUsed] ); - const transformToCommands = useCallback((data: string[]) => { - return data - .map((label) => { - return COMMANDS.find((command) => command.label && command.label === label); - }) - .filter(Boolean); - }, []); - const runCommand = useCallback( (command) => { return () => { @@ -170,14 +30,14 @@ export const Insert: React.FC<{ editor: Editor }> = ({ editor }) => { toggleVisible(false); }; }, - [editor, toggleVisible, transformToCommands, user] + [editor, toggleVisible, user] ); useEffect(() => { if (!visible) return; insertMenuLRUCache.syncFromStorage(); setRecentUsed(transformToCommands(insertMenuLRUCache.get() as string[])); - }, [visible, transformToCommands]); + }, [visible]); return ( - - 标题1 - - ), - command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 1 }).run(), - }, - - { - key: '标题2', - label: ( - - - 标题2 - - ), - command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 2 }).run(), - }, - - { - key: '标题1', - label: ( - - - 标题3 - - ), - command: (editor: Editor) => editor.chain().focus().toggleHeading({ level: 3 }).run(), - }, - - { - key: '无序列表', - label: ( - - - 无序列表 - - ), - command: (editor: Editor) => editor.chain().focus().toggleBulletList().run(), - }, - - { - key: '有序列表', - label: ( - - - 有序列表 - - ), - command: (editor: Editor) => editor.chain().focus().toggleOrderedList().run(), - }, - - { - key: '任务列表', - label: ( - - - 任务列表 - - ), - command: (editor: Editor) => editor.chain().focus().toggleTaskList().run(), - }, - - { - key: '链接', - label: ( - - - 链接 - - ), - command: (editor: Editor) => createOrToggleLink(editor), - }, - - { - key: '引用', - label: ( - - - 引用 - - ), - command: (editor: Editor) => editor.chain().focus().toggleBlockquote().run(), - }, - - { - key: '分割线', - label: ( - - - 分割线 - - ), - command: (editor: Editor) => editor.chain().focus().setHorizontalRule().run(), - }, - - { - key: '目录', - label: ( - - - 目录 - - ), - command: (editor: Editor) => editor.chain().focus().setTableOfContents().run(), - }, - - { - key: '表格', - label: ( - - - 表格 - - ), - command: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), - }, - - { - key: '代码块', - label: ( - - - 代码块 - - ), - command: (editor: Editor) => editor.chain().focus().toggleCodeBlock().run(), - }, - - { - key: '图片', - label: () => ( - - - 图片 - - ), - command: (editor: Editor) => editor.chain().focus().setEmptyImage().run(), - }, - - { - key: '附件', - label: () => ( - - - 附件 - - ), - command: (editor: Editor) => editor.chain().focus().setAttachment().run(), - }, - - { - key: '倒计时', - label: () => ( - - - 倒计时 - - ), - command: (editor: Editor) => createCountdown(editor), - }, - - { - key: '外链', - label: ( - - - 外链 - - ), - command: (editor: Editor) => editor.chain().focus().setIframe({ url: '' }).run(), - }, - - { - key: '流程图', - label: ( - - - 流程图 - - ), - command: (editor: Editor) => editor.chain().focus().setFlow({ width: '100%' }).run(), - }, - - { - key: '思维导图', - label: ( - - - 思维导图 - - ), - command: (editor: Editor) => editor.chain().focus().setMind().run(), - }, - - { - key: '数学公式', - label: ( - - - 数学公式 - - ), - command: (editor: Editor, user) => { - console.log('user', user); - if (!user) return; - - editor - .chain() - .focus() - .setKatex({ - defaultShowPicker: true, - createUser: user.name, - }) - .run(); - }, - }, - - { - key: '状态', - label: ( - - - 状态 - - ), - command: (editor: Editor, user) => { - if (!user) return; - - editor - .chain() - .focus() - .setStatus({ - defaultShowPicker: true, - createUser: user.name, - }) - .run(); - }, - }, - - { - key: '高亮块', - label: ( - - - 高亮块 - - ), - command: (editor: Editor) => editor.chain().focus().setCallout().run(), - }, - - { - key: '文档', - label: ( - - - 文档 - - ), - command: (editor: Editor) => editor.chain().focus().setDocumentReference().run(), - }, - - { - key: '子文档', - label: ( - - - 子文档 - - ), - command: (editor: Editor) => editor.chain().focus().setDocumentChildren().run(), - }, -]; diff --git a/packages/client/src/tiptap/core/wrappers/menu-list/index.tsx b/packages/client/src/tiptap/core/wrappers/menu-list/index.tsx index c8b7c108..ca472376 100644 --- a/packages/client/src/tiptap/core/wrappers/menu-list/index.tsx +++ b/packages/client/src/tiptap/core/wrappers/menu-list/index.tsx @@ -1,15 +1,17 @@ +import { Space } from '@douyinfe/semi-ui'; import { Editor } from '@tiptap/core'; import cls from 'classnames'; import { useUser } from 'data/user'; import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'; import scrollIntoView from 'scroll-into-view-if-needed'; +import { ILabelRenderCommand } from 'tiptap/core/menus/commands'; import styles from './index.module.scss'; interface IProps { editor: Editor; - items: Array<{ label: React.ReactNode | ((editor: Editor) => React.ReactNode) }>; - command: any; + items: ILabelRenderCommand[]; + command: (command: ILabelRenderCommand) => void; } export const MenuList: React.FC = forwardRef((props, ref) => { @@ -18,12 +20,12 @@ export const MenuList: React.FC = forwardRef((props, ref) => { const [selectedIndex, setSelectedIndex] = useState(0); const selectItem = (index) => { - const item = props.items[index]; + const command = props.items[index]; - if (item) { - // @ts-ignore - item.user = user; // 注入用户信息 - props.command(item); + if (command) { + // 注入用户信息 + command.user = user; + props.command(command); } }; @@ -72,15 +74,20 @@ export const MenuList: React.FC = forwardRef((props, ref) => {
{props.items.length ? ( - props.items.map((item, index) => ( - selectItem(index)} - > - {typeof item.label === 'function' ? item.label(props.editor) : item.label} - - )) + props.items.map((item, index) => { + return ( + selectItem(index)} + > + + {item.icon} + {item.label} + + + ); + }) ) : (
没有找到结果
)}