mirror of
https://github.com/docmost/docmost.git
synced 2026-05-14 20:54:07 +08:00
6046d04375
* feat(editor): show emoji name in suggestion list Replace the fixed-column emoji grid with a vertical list that displays each emoji alongside its :shortcode: name. This makes the picker more discoverable—users can see and learn shortcodes without prior knowledge. Changes: - EmojiList: switch from SimpleGrid/ActionIcon to UnstyledButton list rows showing emoji glyph + monospace 🆔 label - Navigation simplified to ArrowUp/ArrowDown (list has no columns) - Results capped at 8 items for a focused, scannable dropdown - CSS module: rename menuBtn -> menuItem, tighten padding * feat(editor): replace SearchIndex with name/id includes search Port the exact search algorithm from the original extension: - Build a flat index from @emoji-mart/data: { id, name (lowercase), native } - Filter with name.includes(q) || id.includes(q) — predictable, no keyword indirection - Results capped at 5 (same as extension) - Frequently-used emojis (sorted by usage) shown when query is empty - Remove emoji-mart init() / SearchIndex / getEmojiDataFromNative dependencies; index is built lazily and cached in memory - Remove unused GRID_COLUMNS constant * feat(editor): emoji picker with browse and search modes When the query is empty the picker shows a category bar with 8 tabs (people, nature, food…) and a scrollable emoji grid. Typing after ':' switches to a compact list that shows the glyph and :shortcode: side by side, making it easy to discover emoji names while you type. - Category data is loaded lazily from @emoji-mart/data and cached, so opening the picker more than once has no overhead - Grid keyboard nav: arrow keys move by cell/row, Enter picks - List keyboard nav: up/down through results, Enter picks - Mouse hover syncs the keyboard selection index in both modes - incrementEmojiUsage tracks picks so frequently used ones bubble up in future sessions * fix(editor): polish emoji picker copy and loading * feat: add emoji to slash command * Add keyboard support to emoji group navigation --------- Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
781 lines
21 KiB
TypeScript
781 lines
21 KiB
TypeScript
import {
|
|
IconBlockquote,
|
|
IconCaretRightFilled,
|
|
IconCheckbox,
|
|
IconCode,
|
|
IconH1,
|
|
IconH2,
|
|
IconH3,
|
|
IconInfoCircle,
|
|
IconList,
|
|
IconListNumbers,
|
|
IconMath,
|
|
IconMathFunction,
|
|
IconMovie,
|
|
IconMusic,
|
|
IconPaperclip,
|
|
IconFileTypePdf,
|
|
IconPhoto,
|
|
IconTable,
|
|
IconTypography,
|
|
IconMenu4,
|
|
IconCalendar,
|
|
IconAppWindow,
|
|
IconSitemap,
|
|
IconColumns3,
|
|
IconColumns2,
|
|
IconTag,
|
|
IconMoodSmile,
|
|
IconRotate2,
|
|
} 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 { uploadAudioAction } from "@/features/editor/components/audio/upload-audio-action.tsx";
|
|
import { uploadAttachmentAction } from "@/features/editor/components/attachment/upload-attachment-action.tsx";
|
|
import { uploadPdfAction } from "@/features/editor/components/pdf/upload-pdf-action.tsx";
|
|
import IconExcalidraw from "@/components/icons/icon-excalidraw";
|
|
import IconMermaid from "@/components/icons/icon-mermaid";
|
|
import IconDrawio from "@/components/icons/icon-drawio";
|
|
import { IconColumns4 } from "@/components/icons/icon-columns-4";
|
|
import { IconColumns5 } from "@/components/icons/icon-columns-5";
|
|
import {
|
|
AirtableIcon,
|
|
FigmaIcon,
|
|
FramerIcon,
|
|
GoogleDriveIcon,
|
|
GoogleSheetsIcon,
|
|
LoomIcon,
|
|
MiroIcon,
|
|
TypeformIcon,
|
|
VimeoIcon,
|
|
YoutubeIcon,
|
|
} from "@/components/icons";
|
|
|
|
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: "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", "ol"],
|
|
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", "file", "attachment"],
|
|
icon: IconPhoto,
|
|
command: ({ editor, range }) => {
|
|
editor.chain().focus().deleteRange(range).run();
|
|
|
|
// @ts-ignore
|
|
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.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.onchange = async () => {
|
|
if (input.files?.length) {
|
|
for (const file of input.files) {
|
|
const pos = editor.view.state.selection.from;
|
|
|
|
uploadImageAction(file, editor, pos, pageId);
|
|
}
|
|
}
|
|
|
|
input.remove();
|
|
};
|
|
input.click();
|
|
},
|
|
},
|
|
{
|
|
title: "Video",
|
|
description: "Upload any video from your device.",
|
|
searchTerms: ["video", "mp4", "media", "file", "attachment"],
|
|
icon: IconMovie,
|
|
command: ({ editor, range }) => {
|
|
editor.chain().focus().deleteRange(range).run();
|
|
|
|
// @ts-ignore
|
|
const pageId = editor.storage?.pageId;
|
|
if (!pageId) return;
|
|
|
|
// upload video
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "video/*";
|
|
input.multiple = true;
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.onchange = async () => {
|
|
if (input.files?.length) {
|
|
for (const file of input.files) {
|
|
const pos = editor.view.state.selection.from;
|
|
|
|
uploadVideoAction(file, editor, pos, pageId);
|
|
}
|
|
}
|
|
|
|
input.remove();
|
|
};
|
|
input.click();
|
|
},
|
|
},
|
|
{
|
|
title: "Audio",
|
|
description: "Upload any audio from your device.",
|
|
searchTerms: [
|
|
"audio",
|
|
"music",
|
|
"sound",
|
|
"mp3",
|
|
"media",
|
|
"file",
|
|
"attachment",
|
|
],
|
|
icon: IconMusic,
|
|
command: ({ editor, range }) => {
|
|
editor.chain().focus().deleteRange(range).run();
|
|
|
|
// @ts-ignore
|
|
const pageId = editor.storage?.pageId;
|
|
if (!pageId) return;
|
|
|
|
// upload audio
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "audio/*";
|
|
input.multiple = true;
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.onchange = async () => {
|
|
if (input.files?.length) {
|
|
for (const file of input.files) {
|
|
const pos = editor.view.state.selection.from;
|
|
|
|
uploadAudioAction(file, editor, pos, pageId);
|
|
}
|
|
}
|
|
|
|
input.remove();
|
|
};
|
|
input.click();
|
|
},
|
|
},
|
|
{
|
|
title: "Embed PDF",
|
|
description: "Upload and embed a PDF file.",
|
|
searchTerms: ["pdf", "document", "embed"],
|
|
icon: IconFileTypePdf,
|
|
command: ({ editor, range }) => {
|
|
editor.chain().focus().deleteRange(range).run();
|
|
|
|
// @ts-ignore
|
|
const pageId = editor.storage?.pageId;
|
|
if (!pageId) return;
|
|
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "application/pdf";
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.onchange = async () => {
|
|
if (input.files?.length) {
|
|
for (const file of input.files) {
|
|
const pos = editor.view.state.selection.from;
|
|
|
|
uploadPdfAction(file, editor, pos, pageId);
|
|
}
|
|
}
|
|
|
|
input.remove();
|
|
};
|
|
input.click();
|
|
},
|
|
},
|
|
{
|
|
title: "File attachment",
|
|
description: "Upload any file from your device.",
|
|
searchTerms: ["file", "attachment", "upload", "csv", "zip"],
|
|
icon: IconPaperclip,
|
|
command: ({ editor, range }) => {
|
|
editor.chain().focus().deleteRange(range).run();
|
|
|
|
// @ts-ignore
|
|
const pageId = editor.storage?.pageId;
|
|
if (!pageId) return;
|
|
|
|
// upload file
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = "";
|
|
input.multiple = true;
|
|
input.style.display = "none";
|
|
document.body.appendChild(input);
|
|
input.onchange = async () => {
|
|
if (input.files?.length) {
|
|
for (const file of input.files) {
|
|
const pos = editor.view.state.selection.from;
|
|
|
|
uploadAttachmentAction(file, editor, pos, pageId, true);
|
|
}
|
|
}
|
|
|
|
input.remove();
|
|
};
|
|
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).setDetails().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 (Whiteboard)",
|
|
description: "Draw and sketch excalidraw diagrams",
|
|
searchTerms: ["diagrams", "draw", "sketch", "whiteboard"],
|
|
icon: IconExcalidraw,
|
|
command: ({ editor, range }: CommandProps) =>
|
|
editor.chain().focus().deleteRange(range).setExcalidraw().run(),
|
|
},
|
|
{
|
|
title: "Date",
|
|
description: "Insert current date",
|
|
searchTerms: ["date", "today"],
|
|
icon: IconCalendar,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
const currentDate = new Date().toLocaleDateString("en-US", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertContent(currentDate)
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Status",
|
|
description: "Insert inline status badge.",
|
|
searchTerms: ["status", "badge", "label", "lozenge"],
|
|
icon: IconTag,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setStatus({ text: "", color: "gray" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Emoji",
|
|
description: "Insert emoji.",
|
|
searchTerms: ["emoji", "icon", "smiley", "emoticon", "symbol", "reaction"],
|
|
icon: IconMoodSmile,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor.chain().focus().deleteRange(range).insertContent(":").run();
|
|
},
|
|
},
|
|
{
|
|
title: "Subpages (Child pages)",
|
|
description: "List all subpages of the current page",
|
|
searchTerms: [
|
|
"subpages",
|
|
"child",
|
|
"children",
|
|
"nested",
|
|
"hierarchy",
|
|
"toc",
|
|
],
|
|
icon: IconSitemap,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor.chain().focus().deleteRange(range).insertSubpages().run();
|
|
},
|
|
},
|
|
{
|
|
title: "Synced block",
|
|
description: "Create a block that stays in sync across pages.",
|
|
searchTerms: [
|
|
"sync",
|
|
"synced",
|
|
"synced block",
|
|
"excerpt",
|
|
"transclusion",
|
|
"reusable",
|
|
"snippet",
|
|
],
|
|
icon: IconRotate2,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertTransclusionSource()
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "2 Columns",
|
|
description: "Split content into two columns.",
|
|
searchTerms: ["columns", "layout", "split", "side"],
|
|
icon: IconColumns2,
|
|
command: ({ editor, range }: CommandProps) =>
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertColumns({ layout: "two_equal" })
|
|
.run(),
|
|
},
|
|
{
|
|
title: "3 Columns",
|
|
description: "Split content into three columns.",
|
|
searchTerms: ["columns", "layout", "split", "triple"],
|
|
icon: IconColumns3,
|
|
command: ({ editor, range }: CommandProps) =>
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertColumns({ layout: "three_equal" })
|
|
.run(),
|
|
},
|
|
{
|
|
title: "4 Columns",
|
|
description: "Split content into four columns.",
|
|
searchTerms: ["columns", "layout", "split"],
|
|
icon: IconColumns4,
|
|
command: ({ editor, range }: CommandProps) =>
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertColumns({ layout: "four_equal" })
|
|
.run(),
|
|
},
|
|
{
|
|
title: "5 Columns",
|
|
description: "Split content into five columns.",
|
|
searchTerms: ["columns", "layout", "split"],
|
|
icon: IconColumns5,
|
|
command: ({ editor, range }: CommandProps) =>
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.insertColumns({ layout: "five_equal" })
|
|
.run(),
|
|
},
|
|
{
|
|
title: "Iframe embed",
|
|
description: "Embed any Iframe",
|
|
searchTerms: ["iframe"],
|
|
icon: IconAppWindow,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "iframe" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Airtable",
|
|
description: "Embed Airtable",
|
|
searchTerms: ["airtable"],
|
|
icon: AirtableIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "airtable" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Loom",
|
|
description: "Embed Loom video",
|
|
searchTerms: ["loom"],
|
|
icon: LoomIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "loom" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Figma",
|
|
description: "Embed Figma files",
|
|
searchTerms: ["figma"],
|
|
icon: FigmaIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "figma" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Typeform",
|
|
description: "Embed Typeform",
|
|
searchTerms: ["typeform"],
|
|
icon: TypeformIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "typeform" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Miro",
|
|
description: "Embed Miro board",
|
|
searchTerms: ["miro"],
|
|
icon: MiroIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "miro" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "YouTube",
|
|
description: "Embed YouTube video",
|
|
searchTerms: ["youtube", "yt", "media", "video"],
|
|
icon: YoutubeIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "youtube" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Vimeo",
|
|
description: "Embed Vimeo video",
|
|
searchTerms: ["vimeo"],
|
|
icon: VimeoIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "vimeo" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Framer",
|
|
description: "Embed Framer prototype",
|
|
searchTerms: ["framer"],
|
|
icon: FramerIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "framer" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Google Drive",
|
|
description: "Embed Google Drive content",
|
|
searchTerms: ["google drive", "gdrive"],
|
|
icon: GoogleDriveIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "gdrive" })
|
|
.run();
|
|
},
|
|
},
|
|
{
|
|
title: "Google Sheets",
|
|
description: "Embed Google Sheets content",
|
|
searchTerms: ["google sheets", "gsheets"],
|
|
icon: GoogleSheetsIcon,
|
|
command: ({ editor, range }: CommandProps) => {
|
|
editor
|
|
.chain()
|
|
.focus()
|
|
.deleteRange(range)
|
|
.setEmbed({ provider: "gsheets" })
|
|
.run();
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
export const getSuggestionItems = ({
|
|
query,
|
|
excludeItems,
|
|
}: {
|
|
query: string;
|
|
excludeItems?: Set<string>;
|
|
}): SlashMenuGroupedItemsType => {
|
|
const search = query.toLowerCase();
|
|
const filteredGroups: SlashMenuGroupedItemsType = {};
|
|
|
|
const fuzzyMatch = (query: string, target: string) => {
|
|
let queryIndex = 0;
|
|
target = target.toLowerCase();
|
|
for (const 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) => {
|
|
if (excludeItems?.has(item.title)) return false;
|
|
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.sort((a, b) => {
|
|
const aTitle = a.title.toLowerCase().includes(search) ? 0 : 1;
|
|
const bTitle = b.title.toLowerCase().includes(search) ? 0 : 1;
|
|
return aTitle - bTitle;
|
|
});
|
|
}
|
|
}
|
|
|
|
return filteredGroups;
|
|
};
|
|
|
|
export default getSuggestionItems;
|