Files
docmost-ryan/apps/client/src/features/editor/components/slash-menu/menu-items.ts
2024-09-03 09:17:00 +10:00

388 lines
11 KiB
TypeScript

import {
IconBlockquote,
IconCaretRightFilled,
IconCheckbox,
IconCode,
IconH1,
IconH2,
IconH3,
IconInfoCircle,
IconList,
IconListNumbers,
IconListTree,
IconMath,
IconMathFunction,
IconMovie,
IconPaperclip,
IconPhoto,
IconTable,
IconTypography,
IconMenu4
} from "@tabler/icons-react";
import {
CommandProps,
SlashMenuGroupedItemsType,
} from "@/features/editor/components/slash-menu/types";
import { uploadImageAction } from "@/features/editor/components/image/upload-image-action.tsx";
import { uploadVideoAction } from "@/features/editor/components/video/upload-video-action.tsx";
import { uploadAttachmentAction } from "@/features/editor/components/attachment/upload-attachment-action.tsx";
import IconExcalidraw from "@/components/icons/icon-excalidraw";
import IconMermaid from "@/components/icons/icon-mermaid";
import IconDrawio from "@/components/icons/icon-drawio";
const CommandGroups: SlashMenuGroupedItemsType = {
basic: [
{
title: "Text",
description: "Just start typing with plain text.",
searchTerms: ["p", "paragraph"],
icon: IconTypography,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run();
},
},
{
title: "To-do list",
description: "Track tasks with a to-do list.",
searchTerms: ["todo", "task", "list", "check", "checkbox"],
icon: IconCheckbox,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleTaskList().run();
},
},
{
title: "Heading 1",
description: "Big section heading.",
searchTerms: ["title", "big", "large"],
icon: IconH1,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 1 })
.run();
},
},
{
title: "Heading 2",
description: "Medium section heading.",
searchTerms: ["subtitle", "medium"],
icon: IconH2,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 2 })
.run();
},
},
{
title: "Heading 3",
description: "Small section heading.",
searchTerms: ["subtitle", "small"],
icon: IconH3,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 3 })
.run();
},
},
{
title: "Table of Contents",
description: "Create a table of contents.",
searchTerms: ["table", "contents", "list"],
icon: IconListTree,
command: ({ editor, range }: CommandProps) => {
editor
.chain()
.focus()
.deleteRange(range)
.toggleTableOfContents()
.run();
},
},
{
title: "Bullet list",
description: "Create a simple bullet list.",
searchTerms: ["unordered", "point", "list"],
icon: IconList,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleBulletList().run();
},
},
{
title: "Numbered list",
description: "Create a list with numbering.",
searchTerms: ["numbered", "ordered", "list"],
icon: IconListNumbers,
command: ({ editor, range }: CommandProps) => {
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
},
},
{
title: "Quote",
description: "Create block quote.",
searchTerms: ["blockquote", "quotes"],
icon: IconBlockquote,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleBlockquote().run(),
},
{
title: "Code",
description: "Insert code snippet.",
searchTerms: ["codeblock"],
icon: IconCode,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Divider",
description: "Insert horizontal rule divider",
searchTerms: ["horizontal rule", "hr"],
icon: IconMenu4,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setHorizontalRule().run(),
},
{
title: "Image",
description: "Upload any image from your device.",
searchTerms: ["photo", "picture", "media"],
icon: IconPhoto,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload image
const input = document.createElement("input");
input.type = "file";
input.accept = "image/*";
input.multiple = true;
input.onchange = async () => {
if (input.files?.length) {
for (const file of input.files) {
const pos = editor.view.state.selection.from;
uploadImageAction(file, editor.view, pos, pageId);
}
}
};
input.click();
},
},
{
title: "Video",
description: "Upload any video from your device.",
searchTerms: ["video", "mp4", "media"],
icon: IconMovie,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload video
const input = document.createElement("input");
input.type = "file";
input.accept = "video/*";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
uploadVideoAction(file, editor.view, pos, pageId);
}
};
input.click();
},
},
{
title: "File attachment",
description: "Upload any file from your device.",
searchTerms: ["file", "attachment", "upload", "pdf", "csv", "zip"],
icon: IconPaperclip,
command: ({ editor, range }) => {
editor.chain().focus().deleteRange(range).run();
const pageId = editor.storage?.pageId;
if (!pageId) return;
// upload file
const input = document.createElement("input");
input.type = "file";
input.accept = "";
input.onchange = async () => {
if (input.files?.length) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
if (file.type.includes("image/*")) {
uploadImageAction(file, editor.view, pos, pageId);
} else if (file.type.includes("video/*")) {
uploadVideoAction(file, editor.view, pos, pageId);
} else {
uploadAttachmentAction(file, editor.view, pos, pageId);
}
}
};
input.click();
},
},
{
title: "Table",
description: "Insert a table.",
searchTerms: ["table", "rows", "columns"],
icon: IconTable,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run(),
},
{
title: "Toggle block",
description: "Insert collapsible block.",
searchTerms: ["collapsible", "block", "toggle", "details", "expand"],
icon: IconCaretRightFilled,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleDetails().run(),
},
{
title: "Callout",
description: "Insert callout notice.",
searchTerms: [
"callout",
"notice",
"panel",
"info",
"warning",
"success",
"error",
"danger",
],
icon: IconInfoCircle,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).toggleCallout().run(),
},
{
title: "Math inline",
description: "Insert inline math equation.",
searchTerms: [
"math",
"inline",
"mathinline",
"inlinemath",
"inline math",
"equation",
"katex",
"latex",
"tex",
],
icon: IconMathFunction,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.setMathInline()
.setNodeSelection(range.from)
.run(),
},
{
title: "Math block",
description: "Insert math equation",
searchTerms: [
"math",
"block",
"mathblock",
"block math",
"equation",
"katex",
"latex",
"tex",
],
icon: IconMath,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setMathBlock().run(),
},
{
title: "Mermaid diagram",
description: "Insert mermaid diagram",
searchTerms: ["mermaid", "diagrams", "chart", "uml"],
icon: IconMermaid,
command: ({ editor, range }: CommandProps) =>
editor
.chain()
.focus()
.deleteRange(range)
.setCodeBlock({ language: "mermaid" })
.insertContent("flowchart LR\n" + " A --> B")
.run(),
},
{
title: "Draw.io (diagrams.net) ",
description: "Insert and design Drawio diagrams",
searchTerms: ["drawio", "diagrams", "charts", "uml", "whiteboard"],
icon: IconDrawio,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setDrawio().run(),
},
{
title: "Excalidraw diagram",
description: "Draw and sketch excalidraw diagrams",
searchTerms: ["diagrams", "draw", "sketch", "whiteboard"],
icon: IconExcalidraw,
command: ({ editor, range }: CommandProps) =>
editor.chain().focus().deleteRange(range).setExcalidraw().run(),
},
],
};
export const getSuggestionItems = ({
query,
}: {
query: string;
}): SlashMenuGroupedItemsType => {
const search = query.toLowerCase();
const filteredGroups: SlashMenuGroupedItemsType = {};
const fuzzyMatch = (query, target) => {
let queryIndex = 0;
target = target.toLowerCase();
for (let char of target) {
if (query[queryIndex] === char) queryIndex++;
if (queryIndex === query.length) return true;
}
return false;
};
for (const [group, items] of Object.entries(CommandGroups)) {
const filteredItems = items.filter((item) => {
return (
fuzzyMatch(search, item.title) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms &&
item.searchTerms.some((term: string) => term.includes(search)))
);
});
if (filteredItems.length) {
filteredGroups[group] = filteredItems;
}
}
return filteredGroups;
};
export default getSuggestionItems;