2026-04-13 19:21:42 +08:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useCallback, useMemo } from "react";
|
|
|
|
|
import type { JSX } from "react";
|
2025-11-23 10:55:04 +08:00
|
|
|
import {
|
|
|
|
|
SuggestionMenuController,
|
|
|
|
|
getDefaultReactSlashMenuItems,
|
|
|
|
|
type DefaultReactSuggestionItem,
|
|
|
|
|
} from "@blocknote/react";
|
2026-01-08 06:28:14 +08:00
|
|
|
import { filterSuggestionItems, type BlockNoteEditor } from "@blocknote/core";
|
2025-11-23 10:55:04 +08:00
|
|
|
import { useRouter } from "next/navigation";
|
2026-04-13 19:21:42 +08:00
|
|
|
import {
|
|
|
|
|
FileImage,
|
|
|
|
|
FilePlus2,
|
|
|
|
|
FileVideo,
|
|
|
|
|
ListTree,
|
|
|
|
|
Music,
|
|
|
|
|
Paperclip,
|
|
|
|
|
PilcrowSquare,
|
|
|
|
|
Play,
|
|
|
|
|
Spline,
|
|
|
|
|
Sparkles,
|
|
|
|
|
SquareCheckBig,
|
|
|
|
|
Table,
|
|
|
|
|
} from "lucide-react";
|
|
|
|
|
import type { CustomBlockSchema } from "../schema";
|
|
|
|
|
import { useImagePicker } from "@/components/media/image-picker-context";
|
|
|
|
|
import type { MediaKind, MediaSelection } from "@/types/media";
|
|
|
|
|
import { createOnlineTable } from "@/lib/online-table";
|
|
|
|
|
|
|
|
|
|
type Props = {
|
|
|
|
|
editor: BlockNoteEditor<CustomBlockSchema>;
|
|
|
|
|
currentDocumentId: string;
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
const matchKeywords = (query: string, aliases: string[]) => {
|
2026-04-13 19:21:42 +08:00
|
|
|
const lower = query.trim().toLowerCase();
|
|
|
|
|
if (!lower) return true;
|
|
|
|
|
return aliases.some((alias) => alias.toLowerCase().includes(lower));
|
2025-11-23 10:55:04 +08:00
|
|
|
};
|
|
|
|
|
|
2026-01-08 06:28:14 +08:00
|
|
|
function insertOrUpdateBlockForSlashMenuCompat(
|
|
|
|
|
editor: BlockNoteEditor<CustomBlockSchema>,
|
|
|
|
|
partialBlock: unknown,
|
|
|
|
|
) {
|
|
|
|
|
const cursor = editor.getTextCursorPosition();
|
|
|
|
|
const referenceBlock = cursor?.block ?? editor.topLevelBlocks[editor.topLevelBlocks.length - 1];
|
|
|
|
|
if (!referenceBlock) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const nextType = (partialBlock as { type?: unknown })?.type;
|
|
|
|
|
const shouldAppendParagraph = nextType === "mindmap";
|
|
|
|
|
|
|
|
|
|
const content = Array.isArray(referenceBlock.content) ? referenceBlock.content : [];
|
|
|
|
|
const text = content
|
|
|
|
|
.map((node) => (node && typeof (node as { text?: unknown }).text === "string" ? (node as { text: string }).text : ""))
|
|
|
|
|
.join("")
|
|
|
|
|
.trim();
|
|
|
|
|
|
|
|
|
|
const looksLikeSlashCommand = text === "" || text.startsWith("/");
|
|
|
|
|
|
|
|
|
|
if (referenceBlock.type === "paragraph" && looksLikeSlashCommand) {
|
|
|
|
|
// 兼容默认 slash menu 行为:将当前段落“就地替换”为目标块类型,避免插入后又被 slash 菜单逻辑清理掉
|
|
|
|
|
editor.updateBlock(referenceBlock, partialBlock as never);
|
|
|
|
|
if (shouldAppendParagraph) {
|
|
|
|
|
const inserted = editor.insertBlocks(
|
|
|
|
|
[{ type: "paragraph" } as never],
|
|
|
|
|
referenceBlock,
|
|
|
|
|
"after",
|
|
|
|
|
);
|
|
|
|
|
const paragraph = inserted[0];
|
|
|
|
|
if (paragraph) {
|
|
|
|
|
try {
|
|
|
|
|
editor.setTextCursorPosition(paragraph, "start");
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (shouldAppendParagraph) {
|
|
|
|
|
const inserted = editor.insertBlocks(
|
|
|
|
|
[partialBlock as never, { type: "paragraph" } as never],
|
|
|
|
|
referenceBlock,
|
|
|
|
|
"after",
|
|
|
|
|
);
|
|
|
|
|
const paragraph = inserted[1] ?? inserted[inserted.length - 1];
|
|
|
|
|
if (paragraph) {
|
|
|
|
|
try {
|
|
|
|
|
editor.setTextCursorPosition(paragraph, "start");
|
|
|
|
|
} catch {
|
|
|
|
|
// ignore
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
editor.insertBlocks([partialBlock as never], referenceBlock, "after");
|
|
|
|
|
}
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const GROUP_TRANSLATIONS: Record<string, string> = {
|
|
|
|
|
"Headings": "标题",
|
|
|
|
|
"Subheadings": "副标题",
|
|
|
|
|
"Basic blocks": "基础块",
|
|
|
|
|
"Advanced": "高级",
|
|
|
|
|
"Media": "媒体",
|
|
|
|
|
"Others": "其他",
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DEFAULT_ITEM_TRANSLATIONS: Record<
|
|
|
|
|
string,
|
|
|
|
|
{ title?: string; subtext?: string; group?: string; aliases?: string[] }
|
|
|
|
|
> = {
|
|
|
|
|
"Paragraph": { title: "正文", group: "基础块", aliases: ["zw", "paragraph", "body"] },
|
|
|
|
|
"Heading 1": { title: "主标题", group: "标题", aliases: ["biaoti", "bt1"] },
|
|
|
|
|
"Heading 2": { title: "大标题", group: "标题", aliases: ["biaoti", "bt2"] },
|
|
|
|
|
"Heading 3": { title: "中标题", group: "标题", aliases: ["biaoti", "bt3"] },
|
|
|
|
|
"Heading 4": { title: "小标题", group: "标题", aliases: ["biaoti", "bt4"] },
|
|
|
|
|
"Heading 5": { title: "极小标题", group: "标题", aliases: ["biaoti", "bt5"] },
|
|
|
|
|
"Heading 6": { title: "最小标题", group: "标题", aliases: ["biaoti", "bt6"] },
|
|
|
|
|
"Toggle Heading 1": { title: "可折叠主标题", group: "标题", aliases: ["toggle", "zd1"] },
|
|
|
|
|
"Toggle Heading 2": { title: "可折叠大标题", group: "标题", aliases: ["toggle", "zd2"] },
|
|
|
|
|
"Toggle Heading 3": { title: "可折叠中标题", group: "标题", aliases: ["toggle", "zd3"] },
|
|
|
|
|
"Quote": { title: "引用", group: "基础块", aliases: ["quote", "引用"] },
|
|
|
|
|
"Toggle List": { title: "折叠列表", group: "基础块", aliases: ["toggle list", "zd"] },
|
|
|
|
|
"Numbered List": { title: "数字列表", group: "基础块", aliases: ["ordered", "ol"] },
|
|
|
|
|
"Bullet List": { title: "符号列表", group: "基础块", aliases: ["ul", "list"] },
|
|
|
|
|
"Check List": { title: "任务列表", group: "基础块", aliases: ["todo", "checkbox"] },
|
|
|
|
|
"Code Block": { title: "代码块", group: "基础块", aliases: ["code", "pre"] },
|
|
|
|
|
"Table": { title: "表格", group: "高级", aliases: ["table", "biaoge"] },
|
|
|
|
|
"Image": { title: "插入图片", group: "媒体", subtext: "上传或引用图片资源", aliases: ["tupian", "image", "tp"] },
|
|
|
|
|
"Video": { title: "插入视频", group: "媒体", subtext: "上传或引用视频", aliases: ["shipin", "video", "sp"] },
|
|
|
|
|
"Audio": { title: "插入音频", group: "媒体", subtext: "上传或引用音频", aliases: ["yinpin", "audio", "yp"] },
|
|
|
|
|
"File": { title: "插入文件", group: "媒体", subtext: "上传附件并生成卡片", aliases: ["wenjian", "file", "wj"] },
|
|
|
|
|
"Emoji": { title: "插入表情", group: "其他", aliases: ["emoji", "biaoqing"] },
|
|
|
|
|
"Divider": { title: "分割线", group: "基础块", aliases: ["divider", "hr"] },
|
|
|
|
|
"Page Break": { title: "分页符", group: "基础块", aliases: ["page", "break"] },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const MEDIA_ICONS: Record<MediaKind, JSX.Element> = {
|
|
|
|
|
image: <FileImage className="h-4 w-4 text-[#2563eb]" />,
|
|
|
|
|
video: <FileVideo className="h-4 w-4 text-[#f97316]" />,
|
|
|
|
|
audio: <Music className="h-4 w-4 text-[#10b981]" />,
|
|
|
|
|
file: <Paperclip className="h-4 w-4 text-[#0f172a]" />,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const HEADING_PRESETS = [
|
|
|
|
|
{
|
|
|
|
|
level: 1,
|
|
|
|
|
title: "主标题",
|
|
|
|
|
subtext: "适合页面名称/顶层章节",
|
|
|
|
|
aliases: ["biaoti1", "h1", "level1"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
level: 2,
|
|
|
|
|
title: "大标题",
|
|
|
|
|
subtext: "用于章节逻辑层",
|
|
|
|
|
aliases: ["biaoti2", "h2", "level2"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
level: 3,
|
|
|
|
|
title: "中标题",
|
|
|
|
|
subtext: "用于小节和段落",
|
|
|
|
|
aliases: ["biaoti3", "h3", "level3"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
level: 4,
|
|
|
|
|
title: "小标题",
|
|
|
|
|
subtext: "更细的结构说明",
|
|
|
|
|
aliases: ["biaoti4", "h4", "level4"],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
level: 5,
|
|
|
|
|
title: "极小标题",
|
|
|
|
|
subtext: "适合脚注/补充说明",
|
|
|
|
|
aliases: ["biaoti5", "h5", "level5"],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const isHeadingDefaultItem = (item: DefaultReactSuggestionItem) => {
|
|
|
|
|
const maybeKey = (item as { key?: string }).key ?? "";
|
|
|
|
|
if (maybeKey && (maybeKey === "heading" || maybeKey.startsWith("heading_") || maybeKey.startsWith("toggle_heading"))) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
const title = item.title ?? "";
|
|
|
|
|
return title.includes("标题");
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
export const CustomSlashMenu = ({ editor, currentDocumentId }: Props) => {
|
|
|
|
|
const defaultItems = useMemo(() => getDefaultReactSlashMenuItems(editor), [editor]);
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const { openPicker } = useImagePicker();
|
|
|
|
|
|
|
|
|
|
const getItems = useCallback(
|
|
|
|
|
async (query: string) => {
|
2026-01-08 06:28:14 +08:00
|
|
|
// 注意:不要把 cursor/referenceBlock 在 getItems 阶段“捕获”后长期复用。
|
|
|
|
|
// Slash 菜单打开后,BlockNote 会持续更新光标与块对象;若使用陈旧引用,
|
|
|
|
|
// 可能出现插入块“瞬间出现又消失/不落库”的现象(尤其是插入自定义块时)。
|
2025-11-23 17:22:35 +08:00
|
|
|
const createTableItem: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "在线表格",
|
|
|
|
|
group: "高级",
|
|
|
|
|
aliases: ["online table", "bg", "表格"],
|
|
|
|
|
icon: <Table className="h-4 w-4 text-[#0f172a]" />,
|
|
|
|
|
onItemClick: async () => {
|
|
|
|
|
const documentId = currentDocumentId;
|
|
|
|
|
try {
|
|
|
|
|
const newTable = await createOnlineTable(documentId);
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "onlineTable",
|
|
|
|
|
props: { tableId: newTable.id, title: newTable.title },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2026-01-10 10:35:21 +08:00
|
|
|
window.dispatchEvent(new CustomEvent("online-table-saved", { detail: { tableId: newTable.id } }));
|
2025-11-23 17:22:35 +08:00
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Failed to create table:", error);
|
|
|
|
|
// TODO: 插入错误提示块
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
2025-12-28 22:05:37 +08:00
|
|
|
const createMindmapItem: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "思维导图",
|
|
|
|
|
group: "高级",
|
|
|
|
|
subtext: "插入可编辑导图(带顶栏工具)",
|
|
|
|
|
aliases: ["mindmap", "swdt", "导图"],
|
|
|
|
|
icon: <Spline className="h-4 w-4 text-[#2563eb]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "mindmap",
|
|
|
|
|
props: { docId: currentDocumentId },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-12-28 22:05:37 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
const createPageItem: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "嵌入页面",
|
|
|
|
|
group: "嵌入",
|
|
|
|
|
aliases: ["page", "ym", "子页面", "嵌入页面块"],
|
|
|
|
|
icon: <FilePlus2 className="h-4 w-4 text-[#2563eb]" />,
|
|
|
|
|
onItemClick: async () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
const cursor = editor.getTextCursorPosition();
|
|
|
|
|
const cursorBlock = cursor?.block as any;
|
|
|
|
|
const firstText =
|
|
|
|
|
Array.isArray(cursorBlock?.content) && cursorBlock.content.length > 0
|
|
|
|
|
? (cursorBlock.content[0] as any)?.text
|
|
|
|
|
: undefined;
|
2025-11-23 10:55:04 +08:00
|
|
|
const response = await fetch("/api/documents/create-child", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
parentId: currentDocumentId,
|
2026-01-08 06:28:14 +08:00
|
|
|
title: typeof firstText === "string" && firstText.trim() ? firstText : "未命名页面",
|
|
|
|
|
blocks: cursorBlock ? [cursorBlock] : [],
|
2025-11-23 10:55:04 +08:00
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!response.ok) return;
|
|
|
|
|
const { pageId, title } = await response.json();
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "pageReference",
|
2026-02-01 08:47:40 +08:00
|
|
|
props: { pageId, title, asChildPage: true },
|
2026-01-08 06:28:14 +08:00
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
router.refresh();
|
|
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const headingItems: DefaultReactSuggestionItem[] = HEADING_PRESETS.map((preset) => ({
|
|
|
|
|
title: preset.title,
|
|
|
|
|
group: "标题",
|
|
|
|
|
subtext: preset.subtext,
|
|
|
|
|
aliases: preset.aliases,
|
2025-11-23 10:55:04 +08:00
|
|
|
icon: <PilcrowSquare className="h-4 w-4 text-[#0f172a]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "heading",
|
|
|
|
|
props: { level: preset.level },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
},
|
|
|
|
|
}));
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const foldHeading: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "折叠标题",
|
|
|
|
|
group: "标题",
|
2025-11-23 10:55:04 +08:00
|
|
|
aliases: ["toggle", "zd", "fold"],
|
|
|
|
|
icon: <ListTree className="h-4 w-4 text-[#0f172a]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "heading",
|
|
|
|
|
props: { level: 2, isToggleable: true },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const advancedTodo: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "高级待办",
|
|
|
|
|
group: "待办",
|
|
|
|
|
subtext: "四态状态 · Alt 直接取消",
|
|
|
|
|
aliases: ["gjdblb", "todopro", "gaoji", "todo+"],
|
2025-11-23 10:55:04 +08:00
|
|
|
icon: <SquareCheckBig className="h-4 w-4 text-[#2563eb]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "advancedTodo",
|
|
|
|
|
props: { status: "todo" },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const progressMeter: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "进度条",
|
|
|
|
|
group: "进度",
|
|
|
|
|
subtext: "自动读取下方待办完成度",
|
|
|
|
|
aliases: ["jdt", "progress", "jindu"],
|
2025-11-23 10:55:04 +08:00
|
|
|
icon: <Sparkles className="h-4 w-4 text-[#f59e0b]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "progressMeter",
|
|
|
|
|
props: { percent: 0, auto: true },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const foldAdvancedTodo: DefaultReactSuggestionItem = {
|
|
|
|
|
title: "折叠高级待办",
|
|
|
|
|
group: "待办",
|
2025-11-23 10:55:04 +08:00
|
|
|
aliases: ["zdgjdb", "foldtodo"],
|
|
|
|
|
icon: <Play className="h-4 w-4 text-[#9f1239]" />,
|
|
|
|
|
onItemClick: () => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "advancedTodo",
|
|
|
|
|
props: { status: "todo" },
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
|
|
|
|
const customItems = [
|
|
|
|
|
...headingItems,
|
|
|
|
|
foldHeading,
|
|
|
|
|
createPageItem,
|
|
|
|
|
createTableItem,
|
|
|
|
|
createMindmapItem,
|
|
|
|
|
advancedTodo,
|
|
|
|
|
foldAdvancedTodo,
|
|
|
|
|
progressMeter,
|
|
|
|
|
].filter((item) => matchKeywords(query, item.aliases ?? []));
|
|
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
const insertMediaSelection = (selection: MediaSelection) => {
|
2026-01-08 06:28:14 +08:00
|
|
|
insertOrUpdateBlockForSlashMenuCompat(editor, {
|
|
|
|
|
type: "media",
|
|
|
|
|
props: {
|
|
|
|
|
fileUrl: selection.fileUrl,
|
|
|
|
|
thumbnailUrl: selection.thumbnailUrl ?? selection.fileUrl,
|
|
|
|
|
assetId: selection.assetId,
|
|
|
|
|
assetType: selection.assetType ?? "image",
|
|
|
|
|
fileName: selection.fileName ?? "",
|
|
|
|
|
fileSize: selection.fileSize ?? null,
|
|
|
|
|
mimeType: selection.mimeType ?? "",
|
|
|
|
|
ocrStatus: "idle",
|
|
|
|
|
},
|
|
|
|
|
content: [],
|
|
|
|
|
});
|
2025-11-23 10:55:04 +08:00
|
|
|
};
|
2026-04-13 19:21:42 +08:00
|
|
|
|
2025-11-23 10:55:04 +08:00
|
|
|
const handleMediaPick = (mediaType: MediaKind) => {
|
|
|
|
|
openPicker({
|
|
|
|
|
mediaType,
|
2026-02-01 08:47:40 +08:00
|
|
|
multiple: true,
|
2025-11-23 10:55:04 +08:00
|
|
|
onSelect: (selection) => {
|
|
|
|
|
insertMediaSelection({
|
|
|
|
|
...selection,
|
|
|
|
|
assetType: selection.assetType ?? mediaType,
|
|
|
|
|
});
|
2026-04-13 19:21:42 +08:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const localizedDefaults = defaultItems.map((item) => {
|
|
|
|
|
const translation = DEFAULT_ITEM_TRANSLATIONS[item.title];
|
|
|
|
|
const next: DefaultReactSuggestionItem = { ...item };
|
|
|
|
|
if (translation?.title) next.title = translation.title;
|
|
|
|
|
if (translation?.subtext) next.subtext = translation.subtext;
|
|
|
|
|
if (translation?.aliases) next.aliases = translation.aliases;
|
|
|
|
|
if (translation?.group) {
|
|
|
|
|
next.group = translation.group;
|
|
|
|
|
} else if (item.group && GROUP_TRANSLATIONS[item.group]) {
|
|
|
|
|
next.group = GROUP_TRANSLATIONS[item.group];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (["Image", "Video", "Audio", "File"].includes(item.title)) {
|
|
|
|
|
const mediaType = item.title.toLowerCase() as MediaKind;
|
|
|
|
|
next.icon = MEDIA_ICONS[mediaType];
|
|
|
|
|
next.group = translation?.group ?? "媒体";
|
|
|
|
|
next.subtext = translation?.subtext ?? next.subtext;
|
|
|
|
|
next.aliases = translation?.aliases ?? next.aliases;
|
|
|
|
|
next.onItemClick = () => handleMediaPick(mediaType);
|
|
|
|
|
}
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const sanitizedDefaults = localizedDefaults.filter(
|
|
|
|
|
(item) => !isHeadingDefaultItem(item) && item.title !== "表格",
|
|
|
|
|
);
|
|
|
|
|
const merged = [...customItems, ...sanitizedDefaults];
|
|
|
|
|
return filterSuggestionItems(merged, query);
|
|
|
|
|
},
|
|
|
|
|
[currentDocumentId, defaultItems, editor, openPicker, router],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return <SuggestionMenuController triggerCharacter="/" getItems={getItems} />;
|
|
|
|
|
}
|