mirror of https://github.com/fantasticit/think.git
improve slash menu extension
This commit is contained in:
parent
7465ed6de3
commit
79ecf7c62e
|
@ -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();
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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('、');
|
|
@ -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: <IconTableOfContents />,
|
||||
label: '目录',
|
||||
pinyin: 'mulu',
|
||||
action: (editor) => editor.chain().focus().setTableOfContents().run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconTable />,
|
||||
label: '表格',
|
||||
pinyin: 'biaoge',
|
||||
custom: (editor, runCommand) => (
|
||||
<Popover
|
||||
key="custom-table"
|
||||
|
@ -94,6 +97,7 @@ export const COMMANDS: ICommand[] = [
|
|||
isBlock: true,
|
||||
icon: <IconLayout />,
|
||||
label: '布局',
|
||||
pinyin: 'buju',
|
||||
custom: (editor, runCommand) => (
|
||||
<Popover
|
||||
key="custom-columns"
|
||||
|
@ -129,30 +133,35 @@ export const COMMANDS: ICommand[] = [
|
|||
isBlock: true,
|
||||
icon: <IconCodeBlock />,
|
||||
label: '代码块',
|
||||
pinyin: 'daimakuai',
|
||||
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconImage />,
|
||||
label: '图片',
|
||||
pinyin: 'tupian',
|
||||
action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconAttachment />,
|
||||
label: '附件',
|
||||
pinyin: 'fujian',
|
||||
action: (editor) => editor.chain().focus().setAttachment().run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconCountdown />,
|
||||
label: '倒计时',
|
||||
pinyin: 'daojishi',
|
||||
action: (editor) => createCountdown(editor),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconLink />,
|
||||
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: <IconFlow />,
|
||||
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: <IconMind />,
|
||||
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: <IconMind />,
|
||||
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: <IconMath />,
|
||||
label: '数学公式',
|
||||
pinyin: 'shuxuegongshi',
|
||||
action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.id }).run(),
|
||||
},
|
||||
{
|
||||
icon: <IconStatus />,
|
||||
label: '状态',
|
||||
pinyin: 'zhuangtai',
|
||||
action: (editor, user) => editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.id }).run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconCallout />,
|
||||
label: '高亮块',
|
||||
pinyin: 'gaoliangkuai',
|
||||
action: (editor) => editor.chain().focus().setCallout().run(),
|
||||
},
|
||||
{
|
||||
|
@ -207,6 +222,7 @@ export const COMMANDS: ICommand[] = [
|
|||
isBlock: true,
|
||||
icon: <IconDocument />,
|
||||
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: <IconDocument />,
|
||||
label: '子文档',
|
||||
pinyin: 'ziwendang',
|
||||
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
||||
},
|
||||
];
|
||||
|
@ -223,12 +240,14 @@ export const QUICK_INSERT_COMMANDS = [
|
|||
{
|
||||
icon: <IconTable />,
|
||||
label: '表格',
|
||||
pinyin: 'biaoge',
|
||||
action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||
},
|
||||
{
|
||||
isBlock: true,
|
||||
icon: <IconLayout />,
|
||||
label: '布局',
|
||||
pinyin: 'buju',
|
||||
action: (editor) => editor.chain().focus().insertColumns({ cols: 2 }).run(),
|
||||
},
|
||||
...COMMANDS.slice(4),
|
||||
|
|
|
@ -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({
|
||||
|
|
Loading…
Reference in New Issue