Plate
PlateEditorsTemplates
GitHub16kGitHub
DiscordDiscord
    • Stream
    • Copilot
  • Comments
  • Discussion
  • Suggestions
    • Basic Blocks
      • Blockquote
      • Heading
      • Horizontal Rule
    • Callout
    • Code Block
    • Column
    • Date
    • Equation
    • Link
    • Media
    • MentionElement
    • Table
    • Table of Contents
    • Footnote
    • Details
  • Marks
    • Bold
    • Italic
    • Underline
    • Code
    • Highlight
    • Keyboard Input
    • Strikethrough
    • Subscript
    • Superscript
      • Font
      • Line Height
      • Text Align
    • Indent
    • List
      • Exit Break
      • Single Block
      • Trailing Block
    • Autoformat
    • Block Menu
    • Block Placeholder
    • Combobox
      • Emoji
      • MentionElement
      • Slash Command
    • Drag & Drop
    • Navigation Feedback
    • Linting
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • DOCX
    • HTML
    • Markdown

Slash Command

PreviousNext

Slash command menu for quick insertion of various content types.

PlusComboboxSlash Nodes
Loading…
EmojiDrag & Drop

On This Page

FeaturesKit usageInstallationAdd kitManual usageInstallationAdd pluginsUsagePlate PlusAPI Reference
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Quick access to various editor commands
  • Triggered by / character
  • Keyboard navigation and filtering
  • Customizable command groups and options
Report an issue

Kit usage

Installation

The fastest way to add slash command functionality is with the SlashKit, which includes a pre-configured SlashPlugin with its Plate UI popup.

'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  LightbulbIcon,
  ListIcon,
  ListOrdered,
  PenToolIcon,
  PilcrowIcon,
  Quote,
  RadicalIcon,
  SparklesIcon,
  Square,
  SuperscriptIcon,
  Table,
  TableOfContentsIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,
  BaseCodeBlockPlugin,
  BaseHeadingPlugin,
  BaseListPlugin,
  BaseParagraphPlugin,
  PLUGINS,
  type PluginTransaction,
} from 'platejs';
import { AIChatPlugin } from 'platejs/ai/react';
import { BaseCalloutPlugin } from 'platejs/callout';
import type { ComboboxState } from 'platejs/combobox';
import { BaseDatePlugin } from 'platejs/date';
import { BaseDetailsPlugin, BaseDetailsSummaryPlugin } from 'platejs/details';
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { BaseFootnotePlugin } from 'platejs/footnote';
import { BaseColumnGroupPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import {
  type EditableSiblingProps,
  type Editor,
  definePlugin,
  useEditor,
} from 'platejs/react';
import { BaseTablePlugin } from 'platejs/table';
import { BaseTocPlugin } from 'platejs/toc';
import * as React from 'react';
 
import {
  InlineCombobox,
  InlineComboboxContent,
  InlineComboboxEmpty,
  InlineComboboxGroup,
  InlineComboboxGroupLabel,
  InlineComboboxItem,
} from './inline-combobox';
 
type Item = {
  icon: React.ReactNode;
  isAvailable: (editor: Editor) => boolean;
  value: string;
  onClick?: (editor: Editor) => void;
  onSelect?: (editor: Editor, tx: PluginTransaction) => boolean | void;
  focusEditor?: boolean;
  keywords?: string[];
  label?: string;
};
 
type Group = {
  group: string;
  items: Item[];
};
 
const isTitle = (editor: Editor, tx: PluginTransaction) => {
  const summary = editor.plugin(BaseDetailsSummaryPlugin);
 
  return (
    summary.installed && tx.nodes.block()?.[0].type === summary.schema.type
  );
};
 
const upsertHeading = (
  editor: Editor,
  tx: PluginTransaction,
  level: 1 | 2 | 3
) => {
  const heading = tx.plugin(BaseHeadingPlugin);
 
  return isTitle(editor, tx)
    ? heading.setLevel(level)
    : heading.upsert({ level }, { select: true });
};
 
const groups: Group[] = [
  {
    group: 'AI',
    items: [
      {
        focusEditor: false,
        icon: <SparklesIcon />,
        isAvailable: (editor) => editor.plugin(AIChatPlugin).installed,
        value: 'AI',
        onClick: (editor) => {
          const ai = editor.plugin(AIChatPlugin);
 
          if (!ai.installed || editor.read.view.isReadOnly()) return;
 
          ai.api.show();
        },
      },
    ],
  },
  {
    group: 'Basic blocks',
    items: [
      {
        icon: <PilcrowIcon />,
        isAvailable: (editor) => editor.plugin(BaseParagraphPlugin).installed,
        keywords: ['paragraph'],
        label: 'Text',
        value: PLUGINS.paragraph,
        onSelect: (editor, tx) =>
          isTitle(editor, tx)
            ? tx.blocks.reset({ unset: ['level'] })
            : tx.plugin(BaseParagraphPlugin).upsert({}, { select: true }),
      },
      {
        icon: <Heading1Icon />,
        isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
        keywords: ['title', 'h1'],
        label: 'Heading 1',
        value: 'heading-1',
        onSelect: (editor, tx) => upsertHeading(editor, tx, 1),
      },
      {
        icon: <Heading2Icon />,
        isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
        keywords: ['subtitle', 'h2'],
        label: 'Heading 2',
        value: 'heading-2',
        onSelect: (editor, tx) => upsertHeading(editor, tx, 2),
      },
      {
        icon: <Heading3Icon />,
        isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
        keywords: ['subtitle', 'h3'],
        label: 'Heading 3',
        value: 'heading-3',
        onSelect: (editor, tx) => upsertHeading(editor, tx, 3),
      },
      {
        icon: <ListIcon />,
        isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
        keywords: ['unordered', 'ul', '-'],
        label: 'Bulleted list',
        value: 'disc',
        onSelect: (_editor, tx) =>
          tx
            .plugin(BaseListPlugin)
            .upsert({ type: 'bulleted' }, { select: true }),
      },
      {
        icon: <ListOrdered />,
        isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
        keywords: ['ordered', 'ol', '1'],
        label: 'Numbered list',
        value: 'decimal',
        onSelect: (_editor, tx) =>
          tx
            .plugin(BaseListPlugin)
            .upsert({ type: 'numbered' }, { select: true }),
      },
      {
        icon: <Square />,
        isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
        keywords: ['checklist', 'task', 'checkbox', '[]'],
        label: 'To-do list',
        value: 'todo',
        onSelect: (_editor, tx) =>
          tx.plugin(BaseListPlugin).upsert({ type: 'task' }, { select: true }),
      },
      {
        icon: <ChevronRightIcon />,
        isAvailable: (editor) => editor.plugin(BaseDetailsPlugin).installed,
        keywords: ['collapsible', 'expandable'],
        label: 'Details',
        value: PLUGINS.details,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseDetailsPlugin).upsert({}, { select: true }),
      },
      {
        icon: <Code2 />,
        isAvailable: (editor) => editor.plugin(BaseCodeBlockPlugin).installed,
        keywords: ['```'],
        label: 'Code Block',
        value: PLUGINS.codeBlock,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseCodeBlockPlugin).upsert({}, { select: true }),
      },
      {
        icon: <Table />,
        isAvailable: (editor) => editor.plugin(BaseTablePlugin).installed,
        label: 'Table',
        value: PLUGINS.table,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseTablePlugin).upsert({}, { select: true }),
      },
      {
        icon: <Quote />,
        isAvailable: (editor) => editor.plugin(BaseBlockquotePlugin).installed,
        keywords: ['citation', 'blockquote', 'quote', '>'],
        label: 'Blockquote',
        value: PLUGINS.blockquote,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseBlockquotePlugin).upsert({}, { select: true }),
      },
      {
        icon: <LightbulbIcon />,
        isAvailable: (editor) => editor.plugin(BaseCalloutPlugin).installed,
        keywords: ['note'],
        label: 'Callout',
        value: PLUGINS.callout,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseCalloutPlugin).upsert({}, { select: true }),
      },
    ],
  },
  {
    group: 'Advanced blocks',
    items: [
      {
        icon: <TableOfContentsIcon />,
        isAvailable: (editor) => editor.plugin(BaseTocPlugin).installed,
        keywords: ['toc'],
        label: 'Table of contents',
        value: PLUGINS.toc,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseTocPlugin).upsert({}, { select: true }),
      },
      {
        icon: <Columns3Icon />,
        isAvailable: (editor) => editor.plugin(BaseColumnGroupPlugin).installed,
        label: '3 columns',
        value: 'action_three_columns',
        onSelect: (_editor, tx) =>
          tx
            .plugin(BaseColumnGroupPlugin)
            .upsert({ columns: 3 }, { select: true }),
      },
      {
        focusEditor: false,
        icon: <RadicalIcon />,
        isAvailable: (editor) => editor.plugin(BaseEquationPlugin).installed,
        label: 'Equation',
        value: PLUGINS.equation,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseEquationPlugin).upsert({}, { select: true }),
      },
      {
        icon: <PenToolIcon />,
        isAvailable: (editor) => editor.plugin(BaseExcalidrawPlugin).installed,
        keywords: ['excalidraw'],
        label: 'Excalidraw',
        value: PLUGINS.excalidraw,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseExcalidrawPlugin).upsert({}, { select: true }),
      },
    ],
  },
  {
    group: 'Inline',
    items: [
      {
        icon: <CalendarIcon />,
        isAvailable: (editor) => editor.plugin(BaseDatePlugin).installed,
        keywords: ['time'],
        label: 'Date',
        value: PLUGINS.date,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseDatePlugin).insert({}, { select: true }),
      },
      {
        icon: <SuperscriptIcon />,
        isAvailable: (editor) => editor.plugin(BaseFootnotePlugin).installed,
        keywords: ['citation', 'fn', 'footnote', '[^]'],
        label: 'Footnote',
        value: 'action_footnote',
        onSelect: (_editor, tx) =>
          tx.plugin(BaseFootnotePlugin).insert({}, { select: true }),
      },
      {
        focusEditor: false,
        icon: <RadicalIcon />,
        isAvailable: (editor) =>
          editor.plugin(BaseInlineEquationPlugin).installed,
        label: 'Inline Equation',
        value: PLUGINS.inlineEquation,
        onSelect: (_editor, tx) =>
          tx.plugin(BaseInlineEquationPlugin).insert({}, { select: true }),
      },
    ],
  },
];
 
export function SlashCombobox({ editableRef }: EditableSiblingProps) {
  const editor = useEditor();
  const availableGroups = groups
    .map((group) => ({
      ...group,
      items: group.items.filter((item) => item.isAvailable(editor)),
    }))
    .filter((group) => group.items.length > 0);
 
  return (
    <InlineCombobox editableRef={editableRef} plugin={SlashPlugin}>
      <InlineComboboxContent>
        <InlineComboboxEmpty>No results</InlineComboboxEmpty>
 
        {availableGroups.map(({ group, items }) => (
          <InlineComboboxGroup key={group}>
            <InlineComboboxGroupLabel>{group}</InlineComboboxGroupLabel>
 
            {items.map(
              ({
                focusEditor,
                icon,
                keywords,
                label,
                value,
                onClick,
                onSelect,
              }) => (
                <InlineComboboxItem
                  key={value}
                  value={value}
                  onClick={() => {
                    onClick?.(editor);
                  }}
                  onSelect={(tx) => onSelect?.(editor, tx)}
                  label={label}
                  focusEditor={focusEditor}
                  group={group}
                  keywords={keywords}
                >
                  <div className="mr-2 text-muted-foreground">{icon}</div>
                  {label ?? value}
                </InlineComboboxItem>
              )
            )}
          </InlineComboboxGroup>
        ))}
      </InlineComboboxContent>
    </InlineCombobox>
  );
}
 
export const SlashPlugin = definePlugin('slashCommand', {
  editOnly: true,
  initialState: (): ComboboxState => ({
    maxQueryLength: 75,
    queryPattern: null,
    trigger: '/',
    triggerPreviousCharPattern: /^\s?$/,
    triggerQuery: (editor) => {
      const codeBlock = editor.plugin(BaseCodeBlockPlugin);
 
      return (
        !codeBlock.installed ||
        !editor.read.nodes.some({
          type: codeBlock.schema.type,
        })
      );
    },
  }),
});
 
export const SlashKit = [
  SlashPlugin.configure({ slots: { afterEditable: SlashCombobox } }),
];
'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  LightbulbIcon,
  ListIcon,
  ListOrdered,
  PenToolIcon,
  PilcrowIcon,
  Quote,
  RadicalIcon,
  SparklesIcon,
  Square,
  SuperscriptIcon,
  Table,
  TableOfContentsIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,
  BaseCodeBlockPlugin,
  BaseHeadingPlugin,
  BaseListPlugin,
  BaseParagraphPlugin,
  PLUGINS,































































































































































































































































































































































  • SlashCombobox: Renders the slash command popup with predefined options

Add kit

import { createEditor } from 'platejs/react';
import { SlashKit } from '@/components/editor/slash';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    ...SlashKit,
  ],
});
import { createEditor } from 'platejs/react';
import { SlashKit } from '@/components/editor/slash';
 
const editor = createEditor




Manual usage

Installation

pnpm add platejs
npx shadcn@latest add @plate/slash
pnpm add platejs
npx shadcn@latest add @plate/slash

Add plugins

The copied slash file defines SlashPlugin, which holds only the trigger policy. Mount the popup in its afterEditable slot:

import { createEditor } from 'platejs/react';
 
import { SlashCombobox, SlashPlugin } from '@/components/editor/slash';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    SlashPlugin.configure({ slots: { afterEditable: SlashCombobox } }),
  ],
});
import { createEditor } from 'platejs/react';
 
import { SlashCombobox, SlashPlugin } from '@/components/editor/slash'






Its state is a ComboboxState. A / opens the menu at the start of a line or after a space, and triggerQuery keeps a / typed in a code block as plain text. Edit SlashPlugin in the copied file to change either.

Usage

How to use slash commands:

  1. Type / at the start of a line or after a space to open the slash menu
  2. Keep typing to filter options, or use the arrow keys to navigate
  3. Press Enter, press Tab or click to run an option; it replaces the typed / and query
  4. Press Escape to close the menu and keep the typed text

Available options include:

  • Text blocks (paragraphs, headings)
  • Lists (bulleted, numbered, to-do)
  • Advanced blocks (tables, code blocks, callouts)
  • Inline elements (dates, equations)

Use keywords to quickly find options. For example, type '/ul' for Bulleted List or '/h1' for Heading 1.

Plate Plus

  • Extended set of slash menu options like "Ask AI"
  • Trigger slash menu by click the + button on the left gutter
  • Item groups
  • Beautifully crafted UI
Get the code

API Reference

APISourceUse
SlashPluginslash registryEdit-only plugin whose state holds the / trigger policy and the code-block check.
SlashKitslash registrySlashPlugin with SlashCombobox in its afterEditable slot.
SlashComboboxslash registryThe slash menu, built from the file's groups list. Edit that list to add, remove or reorder options.
type PluginTransaction,
} from 'platejs';
import { AIChatPlugin } from 'platejs/ai/react';
import { BaseCalloutPlugin } from 'platejs/callout';
import type { ComboboxState } from 'platejs/combobox';
import { BaseDatePlugin } from 'platejs/date';
import { BaseDetailsPlugin, BaseDetailsSummaryPlugin } from 'platejs/details';
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { BaseFootnotePlugin } from 'platejs/footnote';
import { BaseColumnGroupPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import {
type EditableSiblingProps,
type Editor,
definePlugin,
useEditor,
} from 'platejs/react';
import { BaseTablePlugin } from 'platejs/table';
import { BaseTocPlugin } from 'platejs/toc';
import * as React from 'react';
import {
InlineCombobox,
InlineComboboxContent,
InlineComboboxEmpty,
InlineComboboxGroup,
InlineComboboxGroupLabel,
InlineComboboxItem,
} from './inline-combobox';
type Item = {
icon: React.ReactNode;
isAvailable: (editor: Editor) => boolean;
value: string;
onClick?: (editor: Editor) => void;
onSelect?: (editor: Editor, tx: PluginTransaction) => boolean | void;
focusEditor?: boolean;
keywords?: string[];
label?: string;
};
type Group = {
group: string;
items: Item[];
};
const isTitle = (editor: Editor, tx: PluginTransaction) => {
const summary = editor.plugin(BaseDetailsSummaryPlugin);
return (
summary.installed && tx.nodes.block()?.[0].type === summary.schema.type
);
};
const upsertHeading = (
editor: Editor,
tx: PluginTransaction,
level: 1 | 2 | 3
) => {
const heading = tx.plugin(BaseHeadingPlugin);
return isTitle(editor, tx)
? heading.setLevel(level)
: heading.upsert({ level }, { select: true });
};
const groups: Group[] = [
{
group: 'AI',
items: [
{
focusEditor: false,
icon: <SparklesIcon />,
isAvailable: (editor) => editor.plugin(AIChatPlugin).installed,
value: 'AI',
onClick: (editor) => {
const ai = editor.plugin(AIChatPlugin);
if (!ai.installed || editor.read.view.isReadOnly()) return;
ai.api.show();
},
},
],
},
{
group: 'Basic blocks',
items: [
{
icon: <PilcrowIcon />,
isAvailable: (editor) => editor.plugin(BaseParagraphPlugin).installed,
keywords: ['paragraph'],
label: 'Text',
value: PLUGINS.paragraph,
onSelect: (editor, tx) =>
isTitle(editor, tx)
? tx.blocks.reset({ unset: ['level'] })
: tx.plugin(BaseParagraphPlugin).upsert({}, { select: true }),
},
{
icon: <Heading1Icon />,
isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
keywords: ['title', 'h1'],
label: 'Heading 1',
value: 'heading-1',
onSelect: (editor, tx) => upsertHeading(editor, tx, 1),
},
{
icon: <Heading2Icon />,
isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
keywords: ['subtitle', 'h2'],
label: 'Heading 2',
value: 'heading-2',
onSelect: (editor, tx) => upsertHeading(editor, tx, 2),
},
{
icon: <Heading3Icon />,
isAvailable: (editor) => editor.plugin(BaseHeadingPlugin).installed,
keywords: ['subtitle', 'h3'],
label: 'Heading 3',
value: 'heading-3',
onSelect: (editor, tx) => upsertHeading(editor, tx, 3),
},
{
icon: <ListIcon />,
isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
keywords: ['unordered', 'ul', '-'],
label: 'Bulleted list',
value: 'disc',
onSelect: (_editor, tx) =>
tx
.plugin(BaseListPlugin)
.upsert({ type: 'bulleted' }, { select: true }),
},
{
icon: <ListOrdered />,
isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
keywords: ['ordered', 'ol', '1'],
label: 'Numbered list',
value: 'decimal',
onSelect: (_editor, tx) =>
tx
.plugin(BaseListPlugin)
.upsert({ type: 'numbered' }, { select: true }),
},
{
icon: <Square />,
isAvailable: (editor) => editor.plugin(BaseListPlugin).installed,
keywords: ['checklist', 'task', 'checkbox', '[]'],
label: 'To-do list',
value: 'todo',
onSelect: (_editor, tx) =>
tx.plugin(BaseListPlugin).upsert({ type: 'task' }, { select: true }),
},
{
icon: <ChevronRightIcon />,
isAvailable: (editor) => editor.plugin(BaseDetailsPlugin).installed,
keywords: ['collapsible', 'expandable'],
label: 'Details',
value: PLUGINS.details,
onSelect: (_editor, tx) =>
tx.plugin(BaseDetailsPlugin).upsert({}, { select: true }),
},
{
icon: <Code2 />,
isAvailable: (editor) => editor.plugin(BaseCodeBlockPlugin).installed,
keywords: ['```'],
label: 'Code Block',
value: PLUGINS.codeBlock,
onSelect: (_editor, tx) =>
tx.plugin(BaseCodeBlockPlugin).upsert({}, { select: true }),
},
{
icon: <Table />,
isAvailable: (editor) => editor.plugin(BaseTablePlugin).installed,
label: 'Table',
value: PLUGINS.table,
onSelect: (_editor, tx) =>
tx.plugin(BaseTablePlugin).upsert({}, { select: true }),
},
{
icon: <Quote />,
isAvailable: (editor) => editor.plugin(BaseBlockquotePlugin).installed,
keywords: ['citation', 'blockquote', 'quote', '>'],
label: 'Blockquote',
value: PLUGINS.blockquote,
onSelect: (_editor, tx) =>
tx.plugin(BaseBlockquotePlugin).upsert({}, { select: true }),
},
{
icon: <LightbulbIcon />,
isAvailable: (editor) => editor.plugin(BaseCalloutPlugin).installed,
keywords: ['note'],
label: 'Callout',
value: PLUGINS.callout,
onSelect: (_editor, tx) =>
tx.plugin(BaseCalloutPlugin).upsert({}, { select: true }),
},
],
},
{
group: 'Advanced blocks',
items: [
{
icon: <TableOfContentsIcon />,
isAvailable: (editor) => editor.plugin(BaseTocPlugin).installed,
keywords: ['toc'],
label: 'Table of contents',
value: PLUGINS.toc,
onSelect: (_editor, tx) =>
tx.plugin(BaseTocPlugin).upsert({}, { select: true }),
},
{
icon: <Columns3Icon />,
isAvailable: (editor) => editor.plugin(BaseColumnGroupPlugin).installed,
label: '3 columns',
value: 'action_three_columns',
onSelect: (_editor, tx) =>
tx
.plugin(BaseColumnGroupPlugin)
.upsert({ columns: 3 }, { select: true }),
},
{
focusEditor: false,
icon: <RadicalIcon />,
isAvailable: (editor) => editor.plugin(BaseEquationPlugin).installed,
label: 'Equation',
value: PLUGINS.equation,
onSelect: (_editor, tx) =>
tx.plugin(BaseEquationPlugin).upsert({}, { select: true }),
},
{
icon: <PenToolIcon />,
isAvailable: (editor) => editor.plugin(BaseExcalidrawPlugin).installed,
keywords: ['excalidraw'],
label: 'Excalidraw',
value: PLUGINS.excalidraw,
onSelect: (_editor, tx) =>
tx.plugin(BaseExcalidrawPlugin).upsert({}, { select: true }),
},
],
},
{
group: 'Inline',
items: [
{
icon: <CalendarIcon />,
isAvailable: (editor) => editor.plugin(BaseDatePlugin).installed,
keywords: ['time'],
label: 'Date',
value: PLUGINS.date,
onSelect: (_editor, tx) =>
tx.plugin(BaseDatePlugin).insert({}, { select: true }),
},
{
icon: <SuperscriptIcon />,
isAvailable: (editor) => editor.plugin(BaseFootnotePlugin).installed,
keywords: ['citation', 'fn', 'footnote', '[^]'],
label: 'Footnote',
value: 'action_footnote',
onSelect: (_editor, tx) =>
tx.plugin(BaseFootnotePlugin).insert({}, { select: true }),
},
{
focusEditor: false,
icon: <RadicalIcon />,
isAvailable: (editor) =>
editor.plugin(BaseInlineEquationPlugin).installed,
label: 'Inline Equation',
value: PLUGINS.inlineEquation,
onSelect: (_editor, tx) =>
tx.plugin(BaseInlineEquationPlugin).insert({}, { select: true }),
},
],
},
];
export function SlashCombobox({ editableRef }: EditableSiblingProps) {
const editor = useEditor();
const availableGroups = groups
.map((group) => ({
...group,
items: group.items.filter((item) => item.isAvailable(editor)),
}))
.filter((group) => group.items.length > 0);
return (
<InlineCombobox editableRef={editableRef} plugin={SlashPlugin}>
<InlineComboboxContent>
<InlineComboboxEmpty>No results</InlineComboboxEmpty>
{availableGroups.map(({ group, items }) => (
<InlineComboboxGroup key={group}>
<InlineComboboxGroupLabel>{group}</InlineComboboxGroupLabel>
{items.map(
({
focusEditor,
icon,
keywords,
label,
value,
onClick,
onSelect,
}) => (
<InlineComboboxItem
key={value}
value={value}
onClick={() => {
onClick?.(editor);
}}
onSelect={(tx) => onSelect?.(editor, tx)}
label={label}
focusEditor={focusEditor}
group={group}
keywords={keywords}
>
<div className="mr-2 text-muted-foreground">{icon}</div>
{label ?? value}
</InlineComboboxItem>
)
)}
</InlineComboboxGroup>
))}
</InlineComboboxContent>
</InlineCombobox>
);
}
export const SlashPlugin = definePlugin('slashCommand', {
editOnly: true,
initialState: (): ComboboxState => ({
maxQueryLength: 75,
queryPattern: null,
trigger: '/',
triggerPreviousCharPattern: /^\s?$/,
triggerQuery: (editor) => {
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
return (
!codeBlock.installed ||
!editor.read.nodes.some({
type: codeBlock.schema.type,
})
);
},
}),
});
export const SlashKit = [
SlashPlugin.configure({ slots: { afterEditable: SlashCombobox } }),
];
({
plugins: [
// ...otherPlugins,
...SlashKit,
],
});
;
const editor = createEditor({
plugins: [
// ...otherPlugins,
SlashPlugin.configure({ slots: { afterEditable: SlashCombobox } }),
],
});