mirror of https://github.com/fantasticit/think.git
improve slash menu extension
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;
|
isBlock?: boolean;
|
||||||
icon: React.ReactNode;
|
icon: React.ReactNode;
|
||||||
label: string;
|
label: string;
|
||||||
|
pinyin: string;
|
||||||
user?: IUser;
|
user?: IUser;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -55,12 +56,14 @@ export const COMMANDS: ICommand[] = [
|
||||||
{
|
{
|
||||||
icon: <IconTableOfContents />,
|
icon: <IconTableOfContents />,
|
||||||
label: '目录',
|
label: '目录',
|
||||||
|
pinyin: 'mulu',
|
||||||
action: (editor) => editor.chain().focus().setTableOfContents().run(),
|
action: (editor) => editor.chain().focus().setTableOfContents().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconTable />,
|
icon: <IconTable />,
|
||||||
label: '表格',
|
label: '表格',
|
||||||
|
pinyin: 'biaoge',
|
||||||
custom: (editor, runCommand) => (
|
custom: (editor, runCommand) => (
|
||||||
<Popover
|
<Popover
|
||||||
key="custom-table"
|
key="custom-table"
|
||||||
|
@ -94,6 +97,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconLayout />,
|
icon: <IconLayout />,
|
||||||
label: '布局',
|
label: '布局',
|
||||||
|
pinyin: 'buju',
|
||||||
custom: (editor, runCommand) => (
|
custom: (editor, runCommand) => (
|
||||||
<Popover
|
<Popover
|
||||||
key="custom-columns"
|
key="custom-columns"
|
||||||
|
@ -129,30 +133,35 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconCodeBlock />,
|
icon: <IconCodeBlock />,
|
||||||
label: '代码块',
|
label: '代码块',
|
||||||
|
pinyin: 'daimakuai',
|
||||||
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
action: (editor) => editor.chain().focus().toggleCodeBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconImage />,
|
icon: <IconImage />,
|
||||||
label: '图片',
|
label: '图片',
|
||||||
|
pinyin: 'tupian',
|
||||||
action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(),
|
action: (editor) => editor.chain().focus().setEmptyImage({ width: '100%' }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconAttachment />,
|
icon: <IconAttachment />,
|
||||||
label: '附件',
|
label: '附件',
|
||||||
|
pinyin: 'fujian',
|
||||||
action: (editor) => editor.chain().focus().setAttachment().run(),
|
action: (editor) => editor.chain().focus().setAttachment().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconCountdown />,
|
icon: <IconCountdown />,
|
||||||
label: '倒计时',
|
label: '倒计时',
|
||||||
|
pinyin: 'daojishi',
|
||||||
action: (editor) => createCountdown(editor),
|
action: (editor) => createCountdown(editor),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconLink />,
|
icon: <IconLink />,
|
||||||
label: '外链',
|
label: '外链',
|
||||||
|
pinyin: 'wailian',
|
||||||
action: (editor, user) =>
|
action: (editor, user) =>
|
||||||
editor.chain().focus().setIframe({ url: '', defaultShowPicker: true, createUser: user.id }).run(),
|
editor.chain().focus().setIframe({ url: '', defaultShowPicker: true, createUser: user.id }).run(),
|
||||||
},
|
},
|
||||||
|
@ -163,6 +172,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconFlow />,
|
icon: <IconFlow />,
|
||||||
label: '流程图',
|
label: '流程图',
|
||||||
|
pinyin: 'liuchengtu',
|
||||||
action: (editor, user) => {
|
action: (editor, user) => {
|
||||||
editor.chain().focus().setFlow({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
editor.chain().focus().setFlow({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||||
},
|
},
|
||||||
|
@ -171,6 +181,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconMind />,
|
icon: <IconMind />,
|
||||||
label: '思维导图',
|
label: '思维导图',
|
||||||
|
pinyin: 'siweidaotu',
|
||||||
action: (editor, user) => {
|
action: (editor, user) => {
|
||||||
editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
editor.chain().focus().setMind({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||||
},
|
},
|
||||||
|
@ -179,6 +190,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconMind />,
|
icon: <IconMind />,
|
||||||
label: '绘图',
|
label: '绘图',
|
||||||
|
pinyin: 'huitu',
|
||||||
action: (editor, user) => {
|
action: (editor, user) => {
|
||||||
editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
editor.chain().focus().setExcalidraw({ width: '100%', defaultShowPicker: true, createUser: user.id }).run();
|
||||||
},
|
},
|
||||||
|
@ -187,17 +199,20 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconMath />,
|
icon: <IconMath />,
|
||||||
label: '数学公式',
|
label: '数学公式',
|
||||||
|
pinyin: 'shuxuegongshi',
|
||||||
action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.id }).run(),
|
action: (editor, user) => editor.chain().focus().setKatex({ defaultShowPicker: true, createUser: user.id }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <IconStatus />,
|
icon: <IconStatus />,
|
||||||
label: '状态',
|
label: '状态',
|
||||||
|
pinyin: 'zhuangtai',
|
||||||
action: (editor, user) => editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.id }).run(),
|
action: (editor, user) => editor.chain().focus().setStatus({ defaultShowPicker: true, createUser: user.id }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconCallout />,
|
icon: <IconCallout />,
|
||||||
label: '高亮块',
|
label: '高亮块',
|
||||||
|
pinyin: 'gaoliangkuai',
|
||||||
action: (editor) => editor.chain().focus().setCallout().run(),
|
action: (editor) => editor.chain().focus().setCallout().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -207,6 +222,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconDocument />,
|
icon: <IconDocument />,
|
||||||
label: '文档',
|
label: '文档',
|
||||||
|
pinyin: 'wendang',
|
||||||
action: (editor, user) =>
|
action: (editor, user) =>
|
||||||
editor.chain().focus().setDocumentReference({ defaultShowPicker: true, createUser: user.id }).run(),
|
editor.chain().focus().setDocumentReference({ defaultShowPicker: true, createUser: user.id }).run(),
|
||||||
},
|
},
|
||||||
|
@ -214,6 +230,7 @@ export const COMMANDS: ICommand[] = [
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconDocument />,
|
icon: <IconDocument />,
|
||||||
label: '子文档',
|
label: '子文档',
|
||||||
|
pinyin: 'ziwendang',
|
||||||
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
action: (editor) => editor.chain().focus().setDocumentChildren().run(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -223,12 +240,14 @@ export const QUICK_INSERT_COMMANDS = [
|
||||||
{
|
{
|
||||||
icon: <IconTable />,
|
icon: <IconTable />,
|
||||||
label: '表格',
|
label: '表格',
|
||||||
|
pinyin: 'biaoge',
|
||||||
action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
action: (editor: Editor) => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
isBlock: true,
|
isBlock: true,
|
||||||
icon: <IconLayout />,
|
icon: <IconLayout />,
|
||||||
label: '布局',
|
label: '布局',
|
||||||
|
pinyin: 'buju',
|
||||||
action: (editor) => editor.chain().focus().insertColumns({ cols: 2 }).run(),
|
action: (editor) => editor.chain().focus().insertColumns({ cols: 2 }).run(),
|
||||||
},
|
},
|
||||||
...COMMANDS.slice(4),
|
...COMMANDS.slice(4),
|
||||||
|
|
|
@ -47,9 +47,9 @@ import { OrderedList } from 'tiptap/core/extensions/ordered-list';
|
||||||
import { Paragraph } from 'tiptap/core/extensions/paragraph';
|
import { Paragraph } from 'tiptap/core/extensions/paragraph';
|
||||||
import { Paste } from 'tiptap/core/extensions/paste';
|
import { Paste } from 'tiptap/core/extensions/paste';
|
||||||
import { Placeholder } from 'tiptap/core/extensions/placeholder';
|
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 { Scroll2Cursor } from 'tiptap/core/extensions/scroll-to-cursor';
|
||||||
import { SearchNReplace } from 'tiptap/core/extensions/search';
|
import { SearchNReplace } from 'tiptap/core/extensions/search';
|
||||||
|
import { EnSlashExtension, ZhSlashExtension } from 'tiptap/core/extensions/slash';
|
||||||
import { Status } from 'tiptap/core/extensions/status';
|
import { Status } from 'tiptap/core/extensions/status';
|
||||||
import { Strike } from 'tiptap/core/extensions/strike';
|
import { Strike } from 'tiptap/core/extensions/strike';
|
||||||
import { Subscript } from 'tiptap/core/extensions/subscript';
|
import { Subscript } from 'tiptap/core/extensions/subscript';
|
||||||
|
@ -176,7 +176,8 @@ export const CollaborationKit = [
|
||||||
Mind.configure({
|
Mind.configure({
|
||||||
getCreateUserId,
|
getCreateUserId,
|
||||||
}),
|
}),
|
||||||
QuickInsert,
|
EnSlashExtension,
|
||||||
|
ZhSlashExtension,
|
||||||
SearchNReplace,
|
SearchNReplace,
|
||||||
Status,
|
Status,
|
||||||
TableOfContents.configure({
|
TableOfContents.configure({
|
||||||
|
|
Loading…
Reference in New Issue