diff --git a/packages/client/src/tiptap/core/extensions/quick-insert.ts b/packages/client/src/tiptap/core/extensions/quick-insert.ts deleted file mode 100644 index 95692f83..00000000 --- a/packages/client/src/tiptap/core/extensions/quick-insert.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { Node } from '@tiptap/core'; -import { ReactRenderer } from '@tiptap/react'; -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 { insertMenuLRUCache, QUICK_INSERT_COMMANDS, transformToCommands } from 'tiptap/core/menus/commands'; -import { MenuList } from 'tiptap/core/wrappers/menu-list'; - -export const QuickInsertPluginKey = new PluginKey('quickInsert'); -const extensionName = 'quickInsert'; - -export const QuickInsert = Node.create({ - name: extensionName, - - priority: EXTENSION_PRIORITY_HIGHEST, - - addOptions() { - return { - HTMLAttributes: {}, - suggestion: { - char: '/', - pluginKey: QuickInsertPluginKey, - command: ({ editor, range, props }) => { - const { state, dispatch } = editor.view; - const { $head, $from, $to } = state.selection; - - // 删除快捷指令 - const end = $from.pos; - const from = $head.nodeBefore - ? end - $head.nodeBefore.text.substring($head.nodeBefore.text.indexOf('/')).length - : $from.start(); - - const tr = state.tr.deleteRange(from, end); - dispatch(tr); - - props?.action(editor, props.user); - insertMenuLRUCache.put(props.label); - editor?.view?.focus(); - }, - }, - }; - }, - - addProseMirrorPlugins() { - return [ - Suggestion({ - editor: this.editor, - ...this.options.suggestion, - }), - new Plugin({ - key: new PluginKey('evokeMenuPlaceholder'), - }), - ]; - }, - - addStorage() { - return { - rect: { - width: 0, - height: 0, - left: 0, - top: 0, - right: 0, - bottom: 0, - }, - }; - }, -}).configure({ - suggestion: { - items: ({ 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(QUICK_INSERT_COMMANDS, recentUsed), ...restCommands].filter( - (command) => !('title' in command) && command.label && command.label.startsWith(query) - ); - }, - render: () => { - let component; - let popup; - let isEditable; - - return { - onStart: (props) => { - isEditable = props.editor.isEditable; - if (!isEditable) return; - - component = new ReactRenderer(MenuList, { - props, - editor: props.editor, - }); - - popup = tippy('body', { - getReferenceClientRect: props.clientRect || (() => props.editor.storage[extensionName].rect), - appendTo: () => document.body, - content: component.element, - showOnCreate: true, - interactive: true, - trigger: 'manual', - placement: 'bottom-start', - }); - }, - - onUpdate(props) { - if (!isEditable) return; - - component.updateProps(props); - - props.editor.storage[extensionName].rect = props.clientRect(); - - popup[0].setProps({ - getReferenceClientRect: props.clientRect, - }); - }, - - onKeyDown(props) { - if (!isEditable) return; - - if (props.event.key === 'Escape') { - popup[0].hide(); - return true; - } - return component.ref?.onKeyDown(props); - }, - - onExit() { - if (!isEditable) return; - popup[0].destroy(); - component.destroy(); - }, - }; - }, - }, -}); diff --git a/packages/client/src/tiptap/core/extensions/slash.ts b/packages/client/src/tiptap/core/extensions/slash.ts new file mode 100644 index 00000000..f380d9a1 --- /dev/null +++ b/packages/client/src/tiptap/core/extensions/slash.ts @@ -0,0 +1,143 @@ +import { Node } from '@tiptap/core'; +import { ReactRenderer } from '@tiptap/react'; +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 { insertMenuLRUCache, QUICK_INSERT_COMMANDS, transformToCommands } from 'tiptap/core/menus/commands'; +import { MenuList } from 'tiptap/core/wrappers/menu-list'; + +const createSlashExtension = (char: string) => { + const extensionName = `quickInsert-${char}`; + const extensionPluginKey = new PluginKey('quickInsert'); + + const slashExtension = Node.create({ + name: extensionName, + + priority: EXTENSION_PRIORITY_HIGHEST, + + addOptions() { + return { + HTMLAttributes: {}, + suggestion: { + char: char, + pluginKey: extensionPluginKey, + command: ({ editor, range, props }) => { + const { state, dispatch } = editor.view; + const { $head, $from, $to } = state.selection; + + // 删除快捷指令 + const end = $from.pos; + const from = $head.nodeBefore + ? end - $head.nodeBefore.text.substring($head.nodeBefore.text.indexOf(char)).length + : $from.start(); + + const tr = state.tr.deleteRange(from, end); + dispatch(tr); + + props?.action(editor, props.user); + insertMenuLRUCache.put(props.label); + editor?.view?.focus(); + }, + }, + }; + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + ...this.options.suggestion, + }), + new Plugin({ + key: new PluginKey('evokeMenuPlaceholder'), + }), + ]; + }, + + addStorage() { + return { + rect: { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + }; + }, + }).configure({ + suggestion: { + items: ({ 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(QUICK_INSERT_COMMANDS, recentUsed), ...restCommands].filter( + (command) => + !('title' in command) && + ((command.label && command.label.startsWith(query)) || (command.pinyin && command.pinyin.startsWith(query))) + ); + }, + render: () => { + let component; + let popup; + let isEditable; + + return { + onStart: (props) => { + isEditable = props.editor.isEditable; + if (!isEditable) return; + + component = new ReactRenderer(MenuList, { + props, + editor: props.editor, + }); + + popup = tippy('body', { + getReferenceClientRect: props.clientRect || (() => props.editor.storage[extensionName].rect), + appendTo: () => document.body, + content: component.element, + showOnCreate: true, + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + }); + }, + + onUpdate(props) { + if (!isEditable) return; + + component.updateProps(props); + props.editor.storage[extensionName].rect = props.clientRect(); + popup[0].setProps({ + getReferenceClientRect: props.clientRect, + }); + }, + + onKeyDown(props) { + if (!isEditable) return; + + if (props.event.key === 'Escape') { + popup[0].hide(); + return true; + } + return component.ref?.onKeyDown(props); + }, + + onExit() { + if (!isEditable) return; + popup[0].destroy(); + component.destroy(); + }, + }; + }, + }, + }); + + return slashExtension; +}; + +export const EnSlashExtension = createSlashExtension('/'); +export const ZhSlashExtension = createSlashExtension('、'); diff --git a/packages/client/src/tiptap/core/menus/commands.tsx b/packages/client/src/tiptap/core/menus/commands.tsx index 1cdd7624..b37218c8 100644 --- a/packages/client/src/tiptap/core/menus/commands.tsx +++ b/packages/client/src/tiptap/core/menus/commands.tsx @@ -30,6 +30,7 @@ type IBaseCommand = { isBlock?: boolean; icon: React.ReactNode; label: string; + pinyin: string; user?: IUser; }; @@ -55,12 +56,14 @@ export const COMMANDS: ICommand[] = [ { icon: , label: '目录', + pinyin: 'mulu', action: (editor) => editor.chain().focus().setTableOfContents().run(), }, { isBlock: true, icon: , label: '表格', + pinyin: 'biaoge', custom: (editor, runCommand) => ( , label: '布局', + pinyin: 'buju', custom: (editor, runCommand) => ( , label: '代码块', + pinyin: 'daimakuai', action: (editor) => editor.chain().focus().toggleCodeBlock().run(), }, { isBlock: true, icon: , label: '图片', + pinyin: 'tupian', action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(), }, { isBlock: true, icon: , label: '附件', + pinyin: 'fujian', action: (editor) => editor.chain().focus().setAttachment().run(), }, { isBlock: true, icon: , label: '倒计时', + pinyin: 'daojishi', action: (editor) => createCountdown(editor), }, { isBlock: true, icon: , label: '外链', + pinyin: 'wailian', action: (editor, user) => editor.chain().focus().setIframe({ url: '', defaultShowPicker: true, createUser: user.id }).run(), }, @@ -163,6 +172,7 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '流程图', + pinyin: 'liuchengtu', action: (editor, user) => { editor.chain().focus().setFlow({ width: '100%', defaultShowPicker: true, createUser: user.id }).run(); }, @@ -171,6 +181,7 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '思维导图', + pinyin: 'siweidaotu', action: (editor, user) => { editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run(); }, @@ -179,6 +190,7 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '绘图', + pinyin: 'huitu', action: (editor, user) => { editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run(); }, @@ -187,17 +199,20 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '数学公式', + pinyin: 'shuxuegongshi', action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.id }).run(), }, { icon: , label: '状态', + pinyin: 'zhuangtai', action: (editor, user) => editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.id }).run(), }, { isBlock: true, icon: , label: '高亮块', + pinyin: 'gaoliangkuai', action: (editor) => editor.chain().focus().setCallout().run(), }, { @@ -207,6 +222,7 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '文档', + pinyin: 'wendang', action: (editor, user) => editor.chain().focus().setDocumentReference({ defaultShowPicker: true, createUser: user.id }).run(), }, @@ -214,6 +230,7 @@ export const COMMANDS: ICommand[] = [ isBlock: true, icon: , label: '子文档', + pinyin: 'ziwendang', action: (editor) => editor.chain().focus().setDocumentChildren().run(), }, ]; @@ -223,12 +240,14 @@ export const QUICK_INSERT_COMMANDS = [ { icon: , label: '表格', + pinyin: 'biaoge', action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), }, { isBlock: true, icon: , label: '布局', + pinyin: 'buju', action: (editor) => editor.chain().focus().insertColumns({ cols: 2 }).run(), }, ...COMMANDS.slice(4), diff --git a/packages/client/src/tiptap/editor/collaboration/kit.ts b/packages/client/src/tiptap/editor/collaboration/kit.ts index c3f2e831..0d8573ff 100644 --- a/packages/client/src/tiptap/editor/collaboration/kit.ts +++ b/packages/client/src/tiptap/editor/collaboration/kit.ts @@ -47,9 +47,9 @@ import { OrderedList } from 'tiptap/core/extensions/ordered-list'; import { Paragraph } from 'tiptap/core/extensions/paragraph'; import { Paste } from 'tiptap/core/extensions/paste'; import { Placeholder } from 'tiptap/core/extensions/placeholder'; -import { QuickInsert } from 'tiptap/core/extensions/quick-insert'; import { Scroll2Cursor } from 'tiptap/core/extensions/scroll-to-cursor'; import { SearchNReplace } from 'tiptap/core/extensions/search'; +import { EnSlashExtension, ZhSlashExtension } from 'tiptap/core/extensions/slash'; import { Status } from 'tiptap/core/extensions/status'; import { Strike } from 'tiptap/core/extensions/strike'; import { Subscript } from 'tiptap/core/extensions/subscript'; @@ -176,7 +176,8 @@ export const CollaborationKit = [ Mind.configure({ getCreateUserId, }), - QuickInsert, + EnSlashExtension, + ZhSlashExtension, SearchNReplace, Status, TableOfContents.configure({