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
    • Tabbable
    • Toolbar
    • Yjs
    • Multi SelectEditor
    • CSV
    • DOCX
    • HTML
    • Markdown

Toolbar

PreviousNext

Fixed and floating toolbars for your editor.

PlusFixed ToolbarFloating Toolbar
Loading…
TabbableYjs

On This Page

FeaturesKit usageInstallationAdd kitManual usageCreate pluginsCustomizing fixed toolbar buttonsCustomizing floating toolbar buttonsCreating custom buttonCreating mark buttonTurn into toolbar buttonInsert toolbar buttonPlate PlusPluginsFixedToolbarKitFloatingToolbarKit
Build your editor
Production-ready AI template and reusable components.
Get all-access

Features

  • Fixed Toolbar: Persistent toolbar above the editor's scrollable content
  • Floating Toolbar: Contextual toolbar that appears on text selection
  • Customizable Buttons: Add, remove, and reorder toolbar buttons
  • Responsive Design: Adapts to different screen sizes and content
  • Plugin Integration: Uses Plate plugins and copied UI components
Report an issue

Kit usage

Installation

The fastest way to add toolbar functionality is with the FixedToolbarKit and FloatingToolbarKit, which include pre-configured toolbar plugins along with their Plate UI components.

'use client';
 
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from 'platejs/media';
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  FontBackgroundColorPlugin,
  FontColorPlugin,
  useEditorReadOnly,
  definePlugin,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import {
  AllCommentsButton,
  CommentToolbarButton,
} from './comment-toolbar-button';
import { DetailsToolbarButton } from './details-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
  DEFAULT_COLORS,
  FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
  IndentToolbarButton,
  OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
  BulletedListToolbarButton,
  NumberedListToolbarButton,
  TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FixedToolbarButtons({
  children,
}: {
  children?: React.ReactNode;
} = {}) {
  const readOnly = useEditorReadOnly();
 
  return (
    <div className="flex w-full">
      {!readOnly && (
        <>
          <ToolbarGroup>
            <UndoToolbarButton />
            <RedoToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
            </AIToolbarButton>
          </ToolbarGroup>
 
          {children}
 
          <ToolbarGroup>
            <InsertToolbarButton />
            <TurnIntoToolbarButton />
            <FontSizeToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontColorPlugin}
              tooltip="Text color"
            >
              <BaselineIcon />
            </FontColorToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontBackgroundColorPlugin}
              tooltip="Background color"
            >
              <PaintBucketIcon />
            </FontColorToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AlignToolbarButton />
 
            <NumberedListToolbarButton />
            <BulletedListToolbarButton />
            <TodoListToolbarButton />
            <DetailsToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LinkToolbarButton />
            <TableToolbarButton />
            <EmojiToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MediaToolbarButton plugin={BaseImagePlugin} />
            <MediaToolbarButton plugin={BaseVideoPlugin} />
            <MediaToolbarButton plugin={BaseAudioPlugin} />
            <MediaToolbarButton plugin={BaseFilePlugin} />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LineHeightToolbarButton />
            <OutdentToolbarButton />
            <IndentToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MoreToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <div className="grow" />
 
      <ToolbarGroup>
        <MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
          <HighlighterIcon />
        </MarkToolbarButton>
        <CommentToolbarButton />
        <AllCommentsButton />
      </ToolbarGroup>
 
      <ToolbarGroup>
        <ModeToolbarButton />
      </ToolbarGroup>
    </div>
  );
}
 
export function FixedToolbar({
  className,
  ref,
  ...props
}: React.ComponentProps<typeof Toolbar>) {
  return (
    <Toolbar
      {...props}
      ref={ref}
      className={cn(
        'scrollbar-hide z-50 w-full shrink-0 justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
        className
      )}
      data-slot="fixed-toolbar"
    />
  );
}
 
export const FixedToolbarPlugin = definePlugin('fixedToolbar', {
  slots: {
    beforeContainer: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
'use client';
 
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from 'platejs/media';
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  FontBackgroundColorPlugin,
  FontColorPlugin,


































































































































































































'use client';
 
import { flip, offset, useDismiss, useInteractions } from '@floating-ui/react';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { AIChatPlugin } from 'platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  type EditableSiblingProps,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  definePlugin,
  useComposedRef,
  useEditorFocused,
  useEditorReadOnly,
  useEditorSelector,















































































































































































































































  • FixedToolbar: Renders a persistent toolbar above the editor
  • FixedToolbarButtons: Pre-configured button set for the fixed toolbar
  • FloatingToolbar: Renders a contextual toolbar on text selection
  • FloatingToolbarButtons: Pre-configured button set for the floating toolbar

Add kit

import { createEditor } from 'platejs/react';
import { FixedToolbarKit } from '@/components/editor/fixed-toolbar';
import { FloatingToolbarKit } from '@/components/editor/floating-toolbar';
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    ...FixedToolbarKit,
    ...FloatingToolbarKit,
  ],
});
import { createEditor } from 'platejs/react'









Manual usage

Create plugins

import { definePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
 
const fixedToolbarPlugin = definePlugin('fixedToolbar', {
  slots: {
    beforeContainer: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
const floatingToolbarPlugin = definePlugin('floatingToolbar', {
  slots: {
    afterEditable: FloatingToolbar,
  },
});
 
const editor = createEditor({
  plugins: [
    // ...otherPlugins,
    fixedToolbarPlugin,
    floatingToolbarPlugin,
  ],
});
import { definePlugin } from 'platejs/react';
import { FixedToolbar } from '@/components/editor/fixed-toolbar';
import { FixedToolbarButtons } from '@/components/editor/fixed-toolbar';
import { FloatingToolbar } from '@/components/editor/floating-toolbar';
 
const fixedToolbarPlugin = definePlugin('fixedToolbar', {
  slots: {
    beforeContainer: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
const floatingToolbarPlugin = definePlugin('floatingToolbar'











  • slots.beforeContainer: Renders FixedToolbar before the editor scroll container
  • slots.afterEditable: Renders FloatingToolbar as an overlay after the editor

Place the editor container in an EditorFrame. The fixed toolbar and scroll container then occupy separate rows inside the same bounded layout.

<EditorFrame className="h-[650px]">
  <EditorContainer>
    <Editor />
  </EditorContainer>
</EditorFrame>
<EditorFrame className="h-[650px]">
  <EditorContainer>
    <Editor />
  </EditorContainer>
</EditorFrame>

Customizing fixed toolbar buttons

The FixedToolbarButtons component contains the default set of buttons for the fixed toolbar.

'use client';
 
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from 'platejs/media';
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  FontBackgroundColorPlugin,



































































































































































































To customize it, you can edit components/editor/fixed-toolbar.tsx.

Customizing floating toolbar buttons

Similarly, you can customize the floating toolbar by editing components/editor/floating-toolbar.tsx.

'use client';
 
import { flip, offset, useDismiss, useInteractions } from '@floating-ui/react';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { AIChatPlugin } from 'platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  type EditableSiblingProps,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  definePlugin,
  useComposedRef,
  useEditorFocused,
  useEditorReadOnly,
  useEditorSelector,















































































































































































































































Creating custom button

This example shows a button that inserts custom text into the editor.

import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
 
export function CustomToolbarButton() {
  const editor = useEditor();
 
  return (
    <ToolbarButton
      onClick={() => {
        // Custom action
        editor.update((tx) => {
          tx.text.insert('Custom text');
        });
      }}
      tooltip="Custom Action"




Creating mark button

For toggling marks like bold or italic, you can use the MarkToolbarButton component. It simplifies the process by handling the toggle state and action automatically.

This example creates a "Bold" button.

import { BoldIcon } from 'lucide-react';
import { BoldPlugin } from 'platejs/react';
 
import { MarkToolbarButton } from '@/components/editor/mark-toolbar-button';
 
export function BoldToolbarButton() {
  return (
    <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
      <BoldIcon />
    </MarkToolbarButton>
  );
}
  • plugin: Specifies the mark plugin to toggle (for example, BoldPlugin).
  • tooltip: Provides a helpful tooltip for the button.
  • MarkToolbarButton reads mark state and handles toggling directly in the component.

Turn into toolbar button

The TurnIntoToolbarButton provides a dropdown menu for leaf formats and structural block actions.

'use client';
 
import {
  ChevronRightIcon,
  Columns3Icon,
  FileCodeIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  Heading4Icon,
  Heading5Icon,
  Heading6Icon,
  ListIcon,
  ListOrderedIcon,
  PilcrowIcon,
  QuoteIcon,
  SquareIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,
  BaseCodeBlockPlugin,
  BaseHeadingPlugin,
  BaseListPlugin,
  ElementApi,
  PathApi,
  SelectionApi,
  type Element,
  type Path,












































































































































































































































































































































































































































































































































































































Each item calls its feature directly. Keep mutually exclusive text, heading, and list formats in the radio group. Render wrappers and layouts as separate actions with their own selection checks.

const quote = editor.plugin(BaseBlockquotePlugin);
 
if (quote.installed) {
  editor.update((tx) => {
    tx.plugin(BaseBlockquotePlugin).toggle();
  });
}
const quote = editor.plugin(BaseBlockquotePlugin);
 
if (quote.installed) {
  editor.update((tx) => {
    tx.plugin

Insert toolbar button

The InsertToolbarButton provides a dropdown menu to insert various elements (blocks, lists, media, inline elements).

'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  FileCodeIcon,
  FilmIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  ImageIcon,
  Link2Icon,
  ListIcon,
  ListOrderedIcon,
  MinusIcon,
  PenToolIcon,
  PilcrowIcon,
  PlusIcon,
  QuoteIcon,
  RadicalIcon,
  SquareIcon,
  SuperscriptIcon,
  TableIcon,
  TableOfContentsIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,






































































































































































































































































































































































































































































































































































To add an insertable item, define its typed operation in getGroups. The local value identifies the menu item; it does not dispatch the edit.

{
  icon: <Heading2Icon />,
  label: 'Heading 2',
  value: 'heading-2',
  onSelect: () => {
    const heading = editor.plugin(BaseHeadingPlugin);
 
    if (!heading.installed || editor.read.view.isReadOnly()) return;
 
    editor.update((tx) => {
      insertBlock(tx, {
        matches: (block) =>
          !block.listType &&
          block.type === heading.schema.type &&







Plate Plus

  • Color picker
  • Mark as equation
  • Beautifully crafted UI
Get the code

Plugins

FixedToolbarKit

Plugin that renders a fixed toolbar before the editor scroll container.

Options

    Renders the fixed toolbar before the editor scroll container. Contains FixedToolbarButtons by default.

FloatingToolbarKit

Plugin that renders a floating toolbar that appears on text selection.

Options

    Renders the floating toolbar as an overlay after the editor. Contains FloatingToolbarButtons by default.

useEditorReadOnly,
definePlugin,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import {
AllCommentsButton,
CommentToolbarButton,
} from './comment-toolbar-button';
import { DetailsToolbarButton } from './details-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<DetailsToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
<AllCommentsButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar({
className,
ref,
...props
}: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
ref={ref}
className={cn(
'scrollbar-hide z-50 w-full shrink-0 justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
className
)}
data-slot="fixed-toolbar"
/>
);
}
export const FixedToolbarPlugin = definePlugin('fixedToolbar', {
slots: {
beforeContainer: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
usePluginStore,
useSelectionGeometry,
} from 'platejs/react';
import * as React from 'react';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { useFloatingRect } from '@/hooks/use-floating-rect';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
export function FloatingToolbar({
children,
...props
}: React.PropsWithChildren<EditableSiblingProps>) {
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0
);
if (hasNodeSelection) return null;
return (
<TextFloatingToolbar {...props}>
{children === undefined ? <FloatingToolbarButtons /> : children}
</TextFloatingToolbar>
);
}
function TextFloatingToolbar({
children,
editableRef,
}: React.PropsWithChildren<EditableSiblingProps>) {
const editorFocused = useEditorFocused();
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const selectionExpanded = useEditorSelector((innerEditor) =>
innerEditor.read.selection.isExpanded()
);
const selectionText = useEditorSelector((innerEditor2) =>
innerEditor2.read.text.string()
);
const selectionRange = useEditorSelector((innerEditor3) =>
innerEditor3.read.selection()
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (!innerEditor4.read.view.isFocused()) return true;
return previous;
}
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorFocused || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!readOnly &&
(!waitForCollapsedSelection || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const document = editableRef.current?.ownerDocument;
if (!document) return undefined;
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, [editableRef]);
if (!open) return null;
return (
<PositionedFloatingToolbar
editableRef={editableRef}
onOpenChange={(nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
}}
onOverlayOpenChange={setOwnedOverlayOpen}
>
{children}
</PositionedFloatingToolbar>
);
}
function PositionedFloatingToolbar({
children,
editableRef,
onOpenChange,
onOverlayOpenChange,
}: React.PropsWithChildren<EditableSiblingProps> & {
onOpenChange: (open: boolean) => void;
onOverlayOpenChange: (open: boolean) => void;
}) {
const geometry = useSelectionGeometry({ editableRef });
const floating = useFloatingRect(geometry?.boundingRect ?? null, {
open: true,
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
onOpenChange,
});
const dismiss = useDismiss(floating.context, {
escapeKey: false,
outsidePress: (event) => {
const Element =
floating.elements.floating?.ownerDocument.defaultView?.Element;
return (
!Element ||
!(event.target instanceof Element) ||
!event.target.closest('[class~="ignore-click-outside/toolbar"]')
);
},
});
const { getFloatingProps } = useInteractions([dismiss]);
const ref = useComposedRef(floating.refs.setFloating);
return (
<Toolbar
{...getFloatingProps()}
ref={ref}
onOverlayOpenChange={onOverlayOpenChange}
style={floating.style}
className="absolute z-50 scrollbar-hide max-w-[80vw] overflow-x-auto rounded-md border bg-popover p-1 whitespace-nowrap opacity-100 shadow-md print:hidden"
>
{children}
</Toolbar>
);
}
export const FloatingToolbarPlugin = definePlugin('floatingToolbar', {
slots: {
afterEditable: FloatingToolbar,
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
'use client';
 
import { flip, offset, useDismiss, useInteractions } from '@floating-ui/react';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { AIChatPlugin } from 'platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  type EditableSiblingProps,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  definePlugin,
  useComposedRef,
  useEditorFocused,
  useEditorReadOnly,
  useEditorSelector,
  usePluginStore,
  useSelectionGeometry,
} from 'platejs/react';
import * as React from 'react';
 
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { useFloatingRect } from '@/hooks/use-floating-rect';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FloatingToolbarButtons() {
  const readOnly = useEditorReadOnly();
 
  return (
    <>
      {!readOnly && (
        <>
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
              Ask AI
            </AIToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <TurnIntoToolbarButton />
 
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <InlineEquationToolbarButton />
 
            <LinkToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <ToolbarGroup>
        <CommentToolbarButton />
        <SuggestionToolbarButton />
 
        {!readOnly && <MoreToolbarButton />}
      </ToolbarGroup>
    </>
  );
}
 
export function FloatingToolbar({
  children,
  ...props
}: React.PropsWithChildren<EditableSiblingProps>) {
  const hasNodeSelection = useEditorSelector(
    (editor) => editor.read.selection.nodes().length > 0
  );
 
  if (hasNodeSelection) return null;
 
  return (
    <TextFloatingToolbar {...props}>
      {children === undefined ? <FloatingToolbarButtons /> : children}
    </TextFloatingToolbar>
  );
}
 
function TextFloatingToolbar({
  children,
  editableRef,
}: React.PropsWithChildren<EditableSiblingProps>) {
  const editorFocused = useEditorFocused();
  const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
  const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
  const selectionExpanded = useEditorSelector((innerEditor) =>
    innerEditor.read.selection.isExpanded()
  );
  const selectionText = useEditorSelector((innerEditor2) =>
    innerEditor2.read.text.string()
  );
  const selectionRange = useEditorSelector((innerEditor3) =>
    innerEditor3.read.selection()
  );
  const waitForCollapsedSelection = useEditorSelector(
    (innerEditor4, previous = false) => {
      if (!innerEditor4.read.selection.isExpanded()) return false;
      if (!innerEditor4.read.view.isFocused()) return true;
 
      return previous;
    }
  );
  const readOnly = useEditorReadOnly();
  const [dismissedSelection, setDismissedSelection] =
    React.useState<typeof selectionRange>(null);
  const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
    null
  );
  const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
  const open =
    selectionExpanded &&
    !!selectionText &&
    (editorFocused || ownedOverlayOpen) &&
    !isFloatingLinkOpen &&
    !isAIChatOpen &&
    !readOnly &&
    (!waitForCollapsedSelection || ownedOverlayOpen) &&
    mouseDownOpen !== false &&
    dismissedSelection !== selectionRange;
  const openStateRef = React.useRef(open);
 
  React.useEffect(() => {
    openStateRef.current = open;
  }, [open]);
 
  React.useEffect(() => {
    const document = editableRef.current?.ownerDocument;
 
    if (!document) return undefined;
 
    const onMouseUp = () => {
      setMouseDownOpen(null);
    };
    const onMouseDown = () => {
      setMouseDownOpen(openStateRef.current);
    };
 
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousedown', onMouseDown);
 
    return () => {
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, [editableRef]);
 
  if (!open) return null;
 
  return (
    <PositionedFloatingToolbar
      editableRef={editableRef}
      onOpenChange={(nextOpen) => {
        setDismissedSelection(nextOpen ? null : selectionRange);
      }}
      onOverlayOpenChange={setOwnedOverlayOpen}
    >
      {children}
    </PositionedFloatingToolbar>
  );
}
 
function PositionedFloatingToolbar({
  children,
  editableRef,
  onOpenChange,
  onOverlayOpenChange,
}: React.PropsWithChildren<EditableSiblingProps> & {
  onOpenChange: (open: boolean) => void;
  onOverlayOpenChange: (open: boolean) => void;
}) {
  const geometry = useSelectionGeometry({ editableRef });
  const floating = useFloatingRect(geometry?.boundingRect ?? null, {
    open: true,
    middleware: [
      offset(12),
      flip({
        fallbackPlacements: [
          'top-start',
          'top-end',
          'bottom-start',
          'bottom-end',
        ],
        padding: 12,
      }),
    ],
    placement: 'top',
    onOpenChange,
  });
  const dismiss = useDismiss(floating.context, {
    escapeKey: false,
    outsidePress: (event) => {
      const Element =
        floating.elements.floating?.ownerDocument.defaultView?.Element;
      return (
        !Element ||
        !(event.target instanceof Element) ||
        !event.target.closest('[class~="ignore-click-outside/toolbar"]')
      );
    },
  });
  const { getFloatingProps } = useInteractions([dismiss]);
  const ref = useComposedRef(floating.refs.setFloating);
 
  return (
    <Toolbar
      {...getFloatingProps()}
      ref={ref}
      onOverlayOpenChange={onOverlayOpenChange}
      style={floating.style}
      className="absolute z-50 scrollbar-hide max-w-[80vw] overflow-x-auto rounded-md border bg-popover p-1 whitespace-nowrap opacity-100 shadow-md print:hidden"
    >
      {children}
    </Toolbar>
  );
}
 
export const FloatingToolbarPlugin = definePlugin('floatingToolbar', {
  slots: {
    afterEditable: FloatingToolbar,
  },
});
 
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
;
import { FixedToolbarKit } from '@/components/editor/fixed-toolbar';
import { FloatingToolbarKit } from '@/components/editor/floating-toolbar';
const editor = createEditor({
plugins: [
// ...otherPlugins,
...FixedToolbarKit,
...FloatingToolbarKit,
],
});
, {
slots: {
afterEditable: FloatingToolbar,
},
});
const editor = createEditor({
plugins: [
// ...otherPlugins,
fixedToolbarPlugin,
floatingToolbarPlugin,
],
});
FontColorPlugin,
useEditorReadOnly,
definePlugin,
} from 'platejs/react';
import * as React from 'react';
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import {
AllCommentsButton,
CommentToolbarButton,
} from './comment-toolbar-button';
import { DetailsToolbarButton } from './details-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
DEFAULT_COLORS,
FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
IndentToolbarButton,
OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
BulletedListToolbarButton,
NumberedListToolbarButton,
TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FixedToolbarButtons({
children,
}: {
children?: React.ReactNode;
} = {}) {
const readOnly = useEditorReadOnly();
return (
<div className="flex w-full">
{!readOnly && (
<>
<ToolbarGroup>
<UndoToolbarButton />
<RedoToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
</AIToolbarButton>
</ToolbarGroup>
{children}
<ToolbarGroup>
<InsertToolbarButton />
<TurnIntoToolbarButton />
<FontSizeToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontColorPlugin}
tooltip="Text color"
>
<BaselineIcon />
</FontColorToolbarButton>
<FontColorToolbarButton
colors={DEFAULT_COLORS}
plugin={FontBackgroundColorPlugin}
tooltip="Background color"
>
<PaintBucketIcon />
</FontColorToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<AlignToolbarButton />
<NumberedListToolbarButton />
<BulletedListToolbarButton />
<TodoListToolbarButton />
<DetailsToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<LinkToolbarButton />
<TableToolbarButton />
<EmojiToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MediaToolbarButton plugin={BaseImagePlugin} />
<MediaToolbarButton plugin={BaseVideoPlugin} />
<MediaToolbarButton plugin={BaseAudioPlugin} />
<MediaToolbarButton plugin={BaseFilePlugin} />
</ToolbarGroup>
<ToolbarGroup>
<LineHeightToolbarButton />
<OutdentToolbarButton />
<IndentToolbarButton />
</ToolbarGroup>
<ToolbarGroup>
<MoreToolbarButton />
</ToolbarGroup>
</>
)}
<div className="grow" />
<ToolbarGroup>
<MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
<HighlighterIcon />
</MarkToolbarButton>
<CommentToolbarButton />
<AllCommentsButton />
</ToolbarGroup>
<ToolbarGroup>
<ModeToolbarButton />
</ToolbarGroup>
</div>
);
}
export function FixedToolbar({
className,
ref,
...props
}: React.ComponentProps<typeof Toolbar>) {
return (
<Toolbar
{...props}
ref={ref}
className={cn(
'scrollbar-hide z-50 w-full shrink-0 justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
className
)}
data-slot="fixed-toolbar"
/>
);
}
export const FixedToolbarPlugin = definePlugin('fixedToolbar', {
slots: {
beforeContainer: () => (
<FixedToolbar>
<FixedToolbarButtons />
</FixedToolbar>
),
},
});
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
'use client';
 
import {
  BaselineIcon,
  BoldIcon,
  Code2Icon,
  HighlighterIcon,
  ItalicIcon,
  PaintBucketIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import {
  BaseAudioPlugin,
  BaseFilePlugin,
  BaseImagePlugin,
  BaseVideoPlugin,
} from 'platejs/media';
import {
  BoldPlugin,
  CodePlugin,
  HighlightPlugin,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  FontBackgroundColorPlugin,
  FontColorPlugin,
  useEditorReadOnly,
  definePlugin,
} from 'platejs/react';
import * as React from 'react';
 
import { cn } from '@/lib/utils';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { AlignToolbarButton } from './align-toolbar-button';
import {
  AllCommentsButton,
  CommentToolbarButton,
} from './comment-toolbar-button';
import { DetailsToolbarButton } from './details-toolbar-button';
import { EmojiToolbarButton } from './emoji-toolbar-button';
import {
  DEFAULT_COLORS,
  FontColorToolbarButton,
} from './font-color-toolbar-button';
import { FontSizeToolbarButton } from './font-size-toolbar-button';
import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button';
import {
  IndentToolbarButton,
  OutdentToolbarButton,
} from './indent-toolbar-button';
import { InsertToolbarButton } from './insert-toolbar-button';
import { LineHeightToolbarButton } from './line-height-toolbar-button';
import { LinkToolbarButton } from './link-toolbar-button';
import {
  BulletedListToolbarButton,
  NumberedListToolbarButton,
  TodoListToolbarButton,
} from './list-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MediaToolbarButton } from './media-toolbar-button';
import { ModeToolbarButton } from './mode-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { TableToolbarButton } from './table-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FixedToolbarButtons({
  children,
}: {
  children?: React.ReactNode;
} = {}) {
  const readOnly = useEditorReadOnly();
 
  return (
    <div className="flex w-full">
      {!readOnly && (
        <>
          <ToolbarGroup>
            <UndoToolbarButton />
            <RedoToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
            </AIToolbarButton>
          </ToolbarGroup>
 
          {children}
 
          <ToolbarGroup>
            <InsertToolbarButton />
            <TurnIntoToolbarButton />
            <FontSizeToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontColorPlugin}
              tooltip="Text color"
            >
              <BaselineIcon />
            </FontColorToolbarButton>
 
            <FontColorToolbarButton
              colors={DEFAULT_COLORS}
              plugin={FontBackgroundColorPlugin}
              tooltip="Background color"
            >
              <PaintBucketIcon />
            </FontColorToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <AlignToolbarButton />
 
            <NumberedListToolbarButton />
            <BulletedListToolbarButton />
            <TodoListToolbarButton />
            <DetailsToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LinkToolbarButton />
            <TableToolbarButton />
            <EmojiToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MediaToolbarButton plugin={BaseImagePlugin} />
            <MediaToolbarButton plugin={BaseVideoPlugin} />
            <MediaToolbarButton plugin={BaseAudioPlugin} />
            <MediaToolbarButton plugin={BaseFilePlugin} />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <LineHeightToolbarButton />
            <OutdentToolbarButton />
            <IndentToolbarButton />
          </ToolbarGroup>
 
          <ToolbarGroup>
            <MoreToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <div className="grow" />
 
      <ToolbarGroup>
        <MarkToolbarButton plugin={HighlightPlugin} tooltip="Highlight">
          <HighlighterIcon />
        </MarkToolbarButton>
        <CommentToolbarButton />
        <AllCommentsButton />
      </ToolbarGroup>
 
      <ToolbarGroup>
        <ModeToolbarButton />
      </ToolbarGroup>
    </div>
  );
}
 
export function FixedToolbar({
  className,
  ref,
  ...props
}: React.ComponentProps<typeof Toolbar>) {
  return (
    <Toolbar
      {...props}
      ref={ref}
      className={cn(
        'scrollbar-hide z-50 w-full shrink-0 justify-between overflow-x-auto rounded-t-lg border-b border-b-border bg-background/95 p-1 backdrop-blur-sm supports-backdrop-blur:bg-background/60',
        className
      )}
      data-slot="fixed-toolbar"
    />
  );
}
 
export const FixedToolbarPlugin = definePlugin('fixedToolbar', {
  slots: {
    beforeContainer: () => (
      <FixedToolbar>
        <FixedToolbarButtons />
      </FixedToolbar>
    ),
  },
});
 
export const FixedToolbarKit = [FixedToolbarPlugin] as const;
usePluginStore,
useSelectionGeometry,
} from 'platejs/react';
import * as React from 'react';
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { useFloatingRect } from '@/hooks/use-floating-rect';
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
export function FloatingToolbarButtons() {
const readOnly = useEditorReadOnly();
return (
<>
{!readOnly && (
<>
<ToolbarGroup>
<AIToolbarButton tooltip="AI commands">
<WandSparklesIcon />
Ask AI
</AIToolbarButton>
</ToolbarGroup>
<ToolbarGroup>
<TurnIntoToolbarButton />
<MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
<BoldIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
<ItalicIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={UnderlinePlugin}
tooltip="Underline (⌘+U)"
>
<UnderlineIcon />
</MarkToolbarButton>
<MarkToolbarButton
plugin={StrikethroughPlugin}
tooltip="Strikethrough (⌘+⇧+M)"
>
<StrikethroughIcon />
</MarkToolbarButton>
<MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
<Code2Icon />
</MarkToolbarButton>
<InlineEquationToolbarButton />
<LinkToolbarButton />
</ToolbarGroup>
</>
)}
<ToolbarGroup>
<CommentToolbarButton />
<SuggestionToolbarButton />
{!readOnly && <MoreToolbarButton />}
</ToolbarGroup>
</>
);
}
export function FloatingToolbar({
children,
...props
}: React.PropsWithChildren<EditableSiblingProps>) {
const hasNodeSelection = useEditorSelector(
(editor) => editor.read.selection.nodes().length > 0
);
if (hasNodeSelection) return null;
return (
<TextFloatingToolbar {...props}>
{children === undefined ? <FloatingToolbarButtons /> : children}
</TextFloatingToolbar>
);
}
function TextFloatingToolbar({
children,
editableRef,
}: React.PropsWithChildren<EditableSiblingProps>) {
const editorFocused = useEditorFocused();
const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
const selectionExpanded = useEditorSelector((innerEditor) =>
innerEditor.read.selection.isExpanded()
);
const selectionText = useEditorSelector((innerEditor2) =>
innerEditor2.read.text.string()
);
const selectionRange = useEditorSelector((innerEditor3) =>
innerEditor3.read.selection()
);
const waitForCollapsedSelection = useEditorSelector(
(innerEditor4, previous = false) => {
if (!innerEditor4.read.selection.isExpanded()) return false;
if (!innerEditor4.read.view.isFocused()) return true;
return previous;
}
);
const readOnly = useEditorReadOnly();
const [dismissedSelection, setDismissedSelection] =
React.useState<typeof selectionRange>(null);
const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
null
);
const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
const open =
selectionExpanded &&
!!selectionText &&
(editorFocused || ownedOverlayOpen) &&
!isFloatingLinkOpen &&
!isAIChatOpen &&
!readOnly &&
(!waitForCollapsedSelection || ownedOverlayOpen) &&
mouseDownOpen !== false &&
dismissedSelection !== selectionRange;
const openStateRef = React.useRef(open);
React.useEffect(() => {
openStateRef.current = open;
}, [open]);
React.useEffect(() => {
const document = editableRef.current?.ownerDocument;
if (!document) return undefined;
const onMouseUp = () => {
setMouseDownOpen(null);
};
const onMouseDown = () => {
setMouseDownOpen(openStateRef.current);
};
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('mousedown', onMouseDown);
return () => {
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('mousedown', onMouseDown);
};
}, [editableRef]);
if (!open) return null;
return (
<PositionedFloatingToolbar
editableRef={editableRef}
onOpenChange={(nextOpen) => {
setDismissedSelection(nextOpen ? null : selectionRange);
}}
onOverlayOpenChange={setOwnedOverlayOpen}
>
{children}
</PositionedFloatingToolbar>
);
}
function PositionedFloatingToolbar({
children,
editableRef,
onOpenChange,
onOverlayOpenChange,
}: React.PropsWithChildren<EditableSiblingProps> & {
onOpenChange: (open: boolean) => void;
onOverlayOpenChange: (open: boolean) => void;
}) {
const geometry = useSelectionGeometry({ editableRef });
const floating = useFloatingRect(geometry?.boundingRect ?? null, {
open: true,
middleware: [
offset(12),
flip({
fallbackPlacements: [
'top-start',
'top-end',
'bottom-start',
'bottom-end',
],
padding: 12,
}),
],
placement: 'top',
onOpenChange,
});
const dismiss = useDismiss(floating.context, {
escapeKey: false,
outsidePress: (event) => {
const Element =
floating.elements.floating?.ownerDocument.defaultView?.Element;
return (
!Element ||
!(event.target instanceof Element) ||
!event.target.closest('[class~="ignore-click-outside/toolbar"]')
);
},
});
const { getFloatingProps } = useInteractions([dismiss]);
const ref = useComposedRef(floating.refs.setFloating);
return (
<Toolbar
{...getFloatingProps()}
ref={ref}
onOverlayOpenChange={onOverlayOpenChange}
style={floating.style}
className="absolute z-50 scrollbar-hide max-w-[80vw] overflow-x-auto rounded-md border bg-popover p-1 whitespace-nowrap opacity-100 shadow-md print:hidden"
>
{children}
</Toolbar>
);
}
export const FloatingToolbarPlugin = definePlugin('floatingToolbar', {
slots: {
afterEditable: FloatingToolbar,
},
});
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
'use client';
 
import { flip, offset, useDismiss, useInteractions } from '@floating-ui/react';
import {
  BoldIcon,
  Code2Icon,
  ItalicIcon,
  StrikethroughIcon,
  UnderlineIcon,
  WandSparklesIcon,
} from 'lucide-react';
import { AIChatPlugin } from 'platejs/ai/react';
import {
  BoldPlugin,
  CodePlugin,
  type EditableSiblingProps,
  ItalicPlugin,
  StrikethroughPlugin,
  UnderlinePlugin,
  definePlugin,
  useComposedRef,
  useEditorFocused,
  useEditorReadOnly,
  useEditorSelector,
  usePluginStore,
  useSelectionGeometry,
} from 'platejs/react';
import * as React from 'react';
 
import { ToolbarGroup, Toolbar } from '@/components/editor/toolbar';
import { useFloatingRect } from '@/hooks/use-floating-rect';
 
import { AIToolbarButton } from './ai-toolbar-button';
import { CommentToolbarButton } from './comment-toolbar-button';
import { InlineEquationToolbarButton } from './equation-toolbar-button';
import { linkPlugin } from './link';
import { LinkToolbarButton } from './link-toolbar-button';
import { MarkToolbarButton } from './mark-toolbar-button';
import { MoreToolbarButton } from './more-toolbar-button';
import { SuggestionToolbarButton } from './suggestion-toolbar-button';
import { TurnIntoToolbarButton } from './turn-into-toolbar-button';
 
export function FloatingToolbarButtons() {
  const readOnly = useEditorReadOnly();
 
  return (
    <>
      {!readOnly && (
        <>
          <ToolbarGroup>
            <AIToolbarButton tooltip="AI commands">
              <WandSparklesIcon />
              Ask AI
            </AIToolbarButton>
          </ToolbarGroup>
 
          <ToolbarGroup>
            <TurnIntoToolbarButton />
 
            <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)">
              <BoldIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={ItalicPlugin} tooltip="Italic (⌘+I)">
              <ItalicIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={UnderlinePlugin}
              tooltip="Underline (⌘+U)"
            >
              <UnderlineIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton
              plugin={StrikethroughPlugin}
              tooltip="Strikethrough (⌘+⇧+M)"
            >
              <StrikethroughIcon />
            </MarkToolbarButton>
 
            <MarkToolbarButton plugin={CodePlugin} tooltip="Code (⌘+E)">
              <Code2Icon />
            </MarkToolbarButton>
 
            <InlineEquationToolbarButton />
 
            <LinkToolbarButton />
          </ToolbarGroup>
        </>
      )}
 
      <ToolbarGroup>
        <CommentToolbarButton />
        <SuggestionToolbarButton />
 
        {!readOnly && <MoreToolbarButton />}
      </ToolbarGroup>
    </>
  );
}
 
export function FloatingToolbar({
  children,
  ...props
}: React.PropsWithChildren<EditableSiblingProps>) {
  const hasNodeSelection = useEditorSelector(
    (editor) => editor.read.selection.nodes().length > 0
  );
 
  if (hasNodeSelection) return null;
 
  return (
    <TextFloatingToolbar {...props}>
      {children === undefined ? <FloatingToolbarButtons /> : children}
    </TextFloatingToolbar>
  );
}
 
function TextFloatingToolbar({
  children,
  editableRef,
}: React.PropsWithChildren<EditableSiblingProps>) {
  const editorFocused = useEditorFocused();
  const isFloatingLinkOpen = !!usePluginStore(linkPlugin, 'mode');
  const isAIChatOpen = usePluginStore(AIChatPlugin, 'open');
  const selectionExpanded = useEditorSelector((innerEditor) =>
    innerEditor.read.selection.isExpanded()
  );
  const selectionText = useEditorSelector((innerEditor2) =>
    innerEditor2.read.text.string()
  );
  const selectionRange = useEditorSelector((innerEditor3) =>
    innerEditor3.read.selection()
  );
  const waitForCollapsedSelection = useEditorSelector(
    (innerEditor4, previous = false) => {
      if (!innerEditor4.read.selection.isExpanded()) return false;
      if (!innerEditor4.read.view.isFocused()) return true;
 
      return previous;
    }
  );
  const readOnly = useEditorReadOnly();
  const [dismissedSelection, setDismissedSelection] =
    React.useState<typeof selectionRange>(null);
  const [mouseDownOpen, setMouseDownOpen] = React.useState<boolean | null>(
    null
  );
  const [ownedOverlayOpen, setOwnedOverlayOpen] = React.useState(false);
  const open =
    selectionExpanded &&
    !!selectionText &&
    (editorFocused || ownedOverlayOpen) &&
    !isFloatingLinkOpen &&
    !isAIChatOpen &&
    !readOnly &&
    (!waitForCollapsedSelection || ownedOverlayOpen) &&
    mouseDownOpen !== false &&
    dismissedSelection !== selectionRange;
  const openStateRef = React.useRef(open);
 
  React.useEffect(() => {
    openStateRef.current = open;
  }, [open]);
 
  React.useEffect(() => {
    const document = editableRef.current?.ownerDocument;
 
    if (!document) return undefined;
 
    const onMouseUp = () => {
      setMouseDownOpen(null);
    };
    const onMouseDown = () => {
      setMouseDownOpen(openStateRef.current);
    };
 
    document.addEventListener('mouseup', onMouseUp);
    document.addEventListener('mousedown', onMouseDown);
 
    return () => {
      document.removeEventListener('mouseup', onMouseUp);
      document.removeEventListener('mousedown', onMouseDown);
    };
  }, [editableRef]);
 
  if (!open) return null;
 
  return (
    <PositionedFloatingToolbar
      editableRef={editableRef}
      onOpenChange={(nextOpen) => {
        setDismissedSelection(nextOpen ? null : selectionRange);
      }}
      onOverlayOpenChange={setOwnedOverlayOpen}
    >
      {children}
    </PositionedFloatingToolbar>
  );
}
 
function PositionedFloatingToolbar({
  children,
  editableRef,
  onOpenChange,
  onOverlayOpenChange,
}: React.PropsWithChildren<EditableSiblingProps> & {
  onOpenChange: (open: boolean) => void;
  onOverlayOpenChange: (open: boolean) => void;
}) {
  const geometry = useSelectionGeometry({ editableRef });
  const floating = useFloatingRect(geometry?.boundingRect ?? null, {
    open: true,
    middleware: [
      offset(12),
      flip({
        fallbackPlacements: [
          'top-start',
          'top-end',
          'bottom-start',
          'bottom-end',
        ],
        padding: 12,
      }),
    ],
    placement: 'top',
    onOpenChange,
  });
  const dismiss = useDismiss(floating.context, {
    escapeKey: false,
    outsidePress: (event) => {
      const Element =
        floating.elements.floating?.ownerDocument.defaultView?.Element;
      return (
        !Element ||
        !(event.target instanceof Element) ||
        !event.target.closest('[class~="ignore-click-outside/toolbar"]')
      );
    },
  });
  const { getFloatingProps } = useInteractions([dismiss]);
  const ref = useComposedRef(floating.refs.setFloating);
 
  return (
    <Toolbar
      {...getFloatingProps()}
      ref={ref}
      onOverlayOpenChange={onOverlayOpenChange}
      style={floating.style}
      className="absolute z-50 scrollbar-hide max-w-[80vw] overflow-x-auto rounded-md border bg-popover p-1 whitespace-nowrap opacity-100 shadow-md print:hidden"
    >
      {children}
    </Toolbar>
  );
}
 
export const FloatingToolbarPlugin = definePlugin('floatingToolbar', {
  slots: {
    afterEditable: FloatingToolbar,
  },
});
 
export const FloatingToolbarKit = [FloatingToolbarPlugin] as const;
>
<CustomIcon />
</ToolbarButton>
);
}
import { useEditor } from 'platejs/react';
import { CustomIcon } from 'lucide-react';
import { ToolbarButton } from '@/components/editor/toolbar';
 
export function CustomToolbarButton() {
  const editor = useEditor();
 
  return (
    <ToolbarButton
      onClick={() => {
        // Custom action
        editor.update((tx) => {
          tx.text.insert('Custom text');
        });
      }}
      tooltip="Custom Action"
    >
      <CustomIcon />
    </ToolbarButton>
  );
}
import { BoldIcon } from 'lucide-react'; import { BoldPlugin } from 'platejs/react'; import { MarkToolbarButton } from '@/components/editor/mark-toolbar-button'; export function BoldToolbarButton() { return ( <MarkToolbarButton plugin={BoldPlugin} tooltip="Bold (⌘+B)"> <BoldIcon /> </MarkToolbarButton> ); }
} from 'platejs';
import { BaseDetailsPlugin } from 'platejs/details';
import { BaseColumnPlugin } from 'platejs/layout';
import {
type Editor,
useEditor,
useEditorReadOnly,
useEditorSelector,
useSelectionFragmentProp,
} from 'platejs/react';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
type LeafFormat =
| 'decimal'
| 'disc'
| 'heading-1'
| 'heading-2'
| 'heading-3'
| 'heading-4'
| 'heading-5'
| 'heading-6'
| 'text'
| 'todo';
type LeafItem = {
icon: React.ReactNode;
label: string;
onSelect: () => void;
value: LeafFormat;
};
type StructuralActionState = {
active: boolean;
eligible: boolean;
};
type StructuralState = {
code: StructuralActionState;
columns: StructuralActionState;
details: StructuralActionState;
quote: StructuralActionState;
};
const structuralStateEqual = (
previous: StructuralState | null,
next: StructuralState
) =>
!!previous &&
previous.code.active === next.code.active &&
previous.code.eligible === next.code.eligible &&
previous.columns.active === next.columns.active &&
previous.columns.eligible === next.columns.eligible &&
previous.details.active === next.details.active &&
previous.details.eligible === next.details.eligible &&
previous.quote.active === next.quote.active &&
previous.quote.eligible === next.quote.eligible;
const readBlocks = (
editor: Editor,
mode: 'highest' | 'lowest'
): ReturnType<Editor['read']['nodes']['blocks']> => {
const selection = editor.read.selection();
if (
!selection ||
(SelectionApi.isText(selection) &&
!editor.read.selection.isValid(selection))
) {
return [];
}
return editor.read.nodes.blocks({ mode });
};
const isContiguousSiblingRun = (
entries: ReturnType<Editor['read']['nodes']['blocks']>
) => {
if (entries.length === 0) return false;
const paths = entries.map(([, path]) => path).toSorted(PathApi.compare);
const parent = PathApi.parent(paths[0]);
const firstIndex = paths[0].at(-1);
return (
firstIndex !== undefined &&
paths.every(
(path, index) =>
PathApi.equals(PathApi.parent(path), parent) &&
path.at(-1) === firstIndex + index
)
);
};
const hasWrapper = (
editor: Editor,
entry: ReturnType<Editor['read']['nodes']['blocks']>[number],
plugin: typeof BaseBlockquotePlugin | typeof BaseDetailsPlugin
) => {
const portal = editor.plugin(plugin);
return (
portal.installed &&
(entry[0].type === portal.schema.type ||
!!editor.read.nodes.above({ at: entry[1], type: plugin }))
);
};
const getStructuralState = (editor: Editor): StructuralState => {
const entries = readBlocks(editor, 'highest');
const leafEntries = readBlocks(editor, 'lowest');
const selectedEntries = editor.read.selection.nodes();
const hasSelection = entries.length > 0;
const hasContainerNodeSelection = selectedEntries.some(([, selectedPath]) =>
leafEntries.some(([, path]) => PathApi.isAncestor(selectedPath, path))
);
const quote = editor.plugin(BaseBlockquotePlugin);
const details = editor.plugin(BaseDetailsPlugin);
const code = editor.plugin(BaseCodeBlockPlugin);
const columns = editor.plugin(BaseColumnPlugin);
const quoteActive =
quote.installed &&
hasSelection &&
entries.every((entry) => hasWrapper(editor, entry, BaseBlockquotePlugin));
const detailsMembership = details.installed
? entries.map((entry) => hasWrapper(editor, entry, BaseDetailsPlugin))
: [];
const detailsActive = hasSelection && detailsMembership.every(Boolean);
const detailsMixed = detailsMembership.some(Boolean) && !detailsActive;
const codeMembership = code.installed
? entries.map(([node]) => !node.listType && node.type === code.schema.type)
: [];
const codeActive = hasSelection && codeMembership.every(Boolean);
const codeMixed = codeMembership.some(Boolean) && !codeActive;
const columnActive =
columns.installed &&
entries.length === 1 &&
(entries[0][0].type === columns.schema.type ||
!!editor.read.nodes.above({
at: entries[0][1],
type: BaseColumnPlugin,
}));
return {
code: {
active: codeActive,
eligible: code.installed && !codeMixed && isContiguousSiblingRun(entries),
},
columns: {
active: columnActive,
eligible:
columns.installed &&
(columnActive || (!hasContainerNodeSelection && entries.length === 1)),
},
details: {
active: detailsActive,
eligible:
details.installed &&
!detailsMixed &&
(detailsActive ||
(!hasContainerNodeSelection && isContiguousSiblingRun(entries))),
},
quote: {
active: quoteActive,
eligible: quote.installed && hasSelection,
},
};
};
const getDetailsPaths = (editor: Editor): Path[] => {
const details = editor.plugin(BaseDetailsPlugin);
if (!details.installed) return [];
const paths = editor.read.nodes
.blocks({ mode: 'highest' })
.flatMap(([node, path]) => {
if (node.type === details.schema.type) return [path];
const ancestor = editor.read.nodes.above({
at: path,
type: BaseDetailsPlugin,
});
return ancestor ? [ancestor[1]] : [];
});
return paths.filter(
(path, index) =>
paths.findIndex((candidate) => PathApi.equals(candidate, path)) === index
);
};
const getLeafFormat = (editor: Editor, node: Element): LeafFormat => {
const list = editor.plugin(BaseListPlugin);
if (list.installed && node.listType) {
if (node.listType === 'numbered') return 'decimal';
if (node.listType === 'task') return 'todo';
return 'disc';
}
const heading = editor.plugin(BaseHeadingPlugin);
if (heading.installed && node.type === heading.schema.type) {
switch (node.level) {
case 1:
case 2:
case 3:
case 4:
case 5:
case 6: {
return `heading-${node.level}`;
}
}
}
return 'text';
};
const isLeafFormatActive = (editor: Editor, value: LeafFormat) => {
const entries = readBlocks(editor, 'lowest');
return (
entries.length > 0 &&
entries.every(([node]) => getLeafFormat(editor, node) === value)
);
};
const getTurnIntoLabel = (value: LeafFormat | undefined) => {
switch (value) {
case 'disc': {
return 'Bulleted list';
}
case 'decimal': {
return 'Numbered list';
}
case 'todo': {
return 'To-do list';
}
case 'heading-1': {
return 'Heading 1';
}
case 'heading-2': {
return 'Heading 2';
}
case 'heading-3': {
return 'Heading 3';
}
case 'heading-4': {
return 'Heading 4';
}
case 'heading-5': {
return 'Heading 5';
}
case 'heading-6': {
return 'Heading 6';
}
case 'text': {
return 'Text';
}
default: {
return 'Mixed';
}
}
};
function TurnIntoLeafItem({
children,
icon,
value,
}: React.PropsWithChildren<{
icon: React.ReactNode;
value: LeafFormat;
}>) {
return (
<DropdownMenuRadioItem
className="min-w-[180px] pl-2 *:first:[span]:hidden"
value={value}
>
{icon}
{children}
</DropdownMenuRadioItem>
);
}
export function TurnIntoToolbarButton() {
const editor = useEditor();
const readOnly = useEditorReadOnly();
const [open, setOpen] = React.useState(false);
const leafValue = useSelectionFragmentProp({
defaultValue: 'text',
getProp: (node) =>
ElementApi.isElement(node) ? getLeafFormat(editor, node) : undefined,
}) as LeafFormat | undefined;
const structural = useEditorSelector(getStructuralState, {
equalityFn: structuralStateEqual,
});
const hasSelection = useEditorSelector(
(current) => readBlocks(current, 'highest').length > 0
);
const heading = editor.plugin(BaseHeadingPlugin);
const list = editor.plugin(BaseListPlugin);
const quote = editor.plugin(BaseBlockquotePlugin);
const details = editor.plugin(BaseDetailsPlugin);
const code = editor.plugin(BaseCodeBlockPlugin);
const columns = editor.plugin(BaseColumnPlugin);
const leafItems: LeafItem[] = [
{
icon: <PilcrowIcon />,
label: 'Text',
value: 'text',
onSelect: () => {
if (
editor.read.view.isReadOnly() ||
isLeafFormatActive(editor, 'text')
) {
return;
}
editor.update((tx) => {
if (editor.plugin(BaseListPlugin).installed) {
tx.plugin(BaseListPlugin).clear();
}
tx.blocks.reset();
});
},
},
];
if (heading.installed) {
(
[
['heading-1', 'Heading 1', <Heading1Icon key="heading-1" />, 1],
['heading-2', 'Heading 2', <Heading2Icon key="heading-2" />, 2],
['heading-3', 'Heading 3', <Heading3Icon key="heading-3" />, 3],
['heading-4', 'Heading 4', <Heading4Icon key="heading-4" />, 4],
['heading-5', 'Heading 5', <Heading5Icon key="heading-5" />, 5],
['heading-6', 'Heading 6', <Heading6Icon key="heading-6" />, 6],
] as const
).forEach(([value, label, icon, level]) => {
leafItems.push({
icon,
label,
value,
onSelect: () => {
const current = editor.plugin(BaseHeadingPlugin);
if (
!current.installed ||
editor.read.view.isReadOnly() ||
isLeafFormatActive(editor, value)
) {
return;
}
editor.update((tx) => {
if (editor.plugin(BaseListPlugin).installed) {
tx.plugin(BaseListPlugin).clear();
}
tx.blocks.set({
level,
type: current.schema.type,
});
});
},
});
});
}
if (list.installed) {
(
[
['disc', 'Bulleted list', <ListIcon key="disc" />, 'bulleted'],
[
'decimal',
'Numbered list',
<ListOrderedIcon key="decimal" />,
'numbered',
],
['todo', 'To-do list', <SquareIcon key="todo" />, 'task'],
] as const
).forEach(([value, label, icon, type]) => {
leafItems.push({
icon,
label,
value,
onSelect: () => {
if (
!editor.plugin(BaseListPlugin).installed ||
editor.read.view.isReadOnly() ||
isLeafFormatActive(editor, value)
) {
return;
}
editor.update((tx) => {
tx.plugin(BaseListPlugin).clear();
tx.blocks.reset();
tx.plugin(BaseListPlugin).toggle({ type });
});
},
});
});
}
const structuralItems = [
...(quote.installed
? [
{
...structural.quote,
icon: <QuoteIcon />,
label: 'Quote',
onSelect: () => {
const current = getStructuralState(editor).quote;
if (!current.eligible || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
if (editor.plugin(BaseListPlugin).installed) {
tx.plugin(BaseListPlugin).clear();
}
tx.plugin(BaseBlockquotePlugin).toggle();
});
},
},
]
: []),
...(details.installed
? [
{
...structural.details,
icon: <ChevronRightIcon />,
label: 'Details',
onSelect: () => {
const current = getStructuralState(editor).details;
if (!current.eligible || editor.read.view.isReadOnly()) return;
const portal = editor.plugin(BaseDetailsPlugin);
if (!portal.installed) return;
if (current.active) {
const paths = getDetailsPaths(editor);
if (paths.length === 0) return;
const selection = editor.read.selection();
const root = selection && SelectionApi.root(selection);
portal.update.unwrap({
at: SelectionApi.nodes(paths as [Path, ...Path[]], {
...(root ? { root } : {}),
}),
});
} else {
portal.update.wrap();
}
},
},
]
: []),
...(code.installed
? [
{
...structural.code,
icon: <FileCodeIcon />,
label: 'Code',
onSelect: () => {
const current = getStructuralState(editor).code;
const portal = editor.plugin(BaseCodeBlockPlugin);
if (
!portal.installed ||
!current.eligible ||
editor.read.view.isReadOnly()
) {
return;
}
portal.update.toggle();
},
},
]
: []),
...(columns.installed
? [
{
...structural.columns,
icon: <Columns3Icon />,
label: '3 columns',
onSelect: () => {
const current = getStructuralState(editor).columns;
const portal = editor.plugin(BaseColumnPlugin);
if (
!portal.installed ||
!current.eligible ||
editor.read.view.isReadOnly()
) {
return;
}
portal.update.toggle({ columns: 3 });
},
},
]
: []),
];
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<ToolbarButton
className="min-w-[125px]"
disabled={readOnly || !hasSelection}
pressed={open}
tooltip="Turn into"
isDropdown
>
{getTurnIntoLabel(leafValue)}
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="ignore-click-outside/toolbar min-w-0"
onFinalFocus={(event) => {
event.preventDefault();
editor.api.dom.focus();
}}
align="start"
>
<DropdownMenuRadioGroup
value={leafValue ?? ''}
onValueChange={(value) => {
const item = leafItems.find(
(candidate) => candidate.value === value
);
item?.onSelect();
}}
>
<ToolbarMenuGroup label="Turn into">
{leafItems.map((item) => (
<TurnIntoLeafItem
key={item.value}
icon={item.icon}
value={item.value}
>
{item.label}
</TurnIntoLeafItem>
))}
</ToolbarMenuGroup>
</DropdownMenuRadioGroup>
{structuralItems.length > 0 && (
<ToolbarMenuGroup label="Structure">
{structuralItems.map((item) => (
<DropdownMenuCheckboxItem
key={item.label}
checked={item.active}
className="min-w-[180px]"
disabled={!item.eligible}
onSelect={item.onSelect}
>
{item.icon}
{item.label}
</DropdownMenuCheckboxItem>
))}
</ToolbarMenuGroup>
)}
</DropdownMenuContent>
</DropdownMenu>
);
}
'use client';
 
import {
  ChevronRightIcon,
  Columns3Icon,
  FileCodeIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  Heading4Icon,
  Heading5Icon,
  Heading6Icon,
  ListIcon,
  ListOrderedIcon,
  PilcrowIcon,
  QuoteIcon,
  SquareIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,
  BaseCodeBlockPlugin,
  BaseHeadingPlugin,
  BaseListPlugin,
  ElementApi,
  PathApi,
  SelectionApi,
  type Element,
  type Path,
} from 'platejs';
import { BaseDetailsPlugin } from 'platejs/details';
import { BaseColumnPlugin } from 'platejs/layout';
import {
  type Editor,
  useEditor,
  useEditorReadOnly,
  useEditorSelector,
  useSelectionFragmentProp,
} from 'platejs/react';
import * as React from 'react';
 
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
  ToolbarButton,
  ToolbarMenuGroup,
} from '@/components/editor/toolbar';
 
type LeafFormat =
  | 'decimal'
  | 'disc'
  | 'heading-1'
  | 'heading-2'
  | 'heading-3'
  | 'heading-4'
  | 'heading-5'
  | 'heading-6'
  | 'text'
  | 'todo';
 
type LeafItem = {
  icon: React.ReactNode;
  label: string;
  onSelect: () => void;
  value: LeafFormat;
};
 
type StructuralActionState = {
  active: boolean;
  eligible: boolean;
};
 
type StructuralState = {
  code: StructuralActionState;
  columns: StructuralActionState;
  details: StructuralActionState;
  quote: StructuralActionState;
};
 
const structuralStateEqual = (
  previous: StructuralState | null,
  next: StructuralState
) =>
  !!previous &&
  previous.code.active === next.code.active &&
  previous.code.eligible === next.code.eligible &&
  previous.columns.active === next.columns.active &&
  previous.columns.eligible === next.columns.eligible &&
  previous.details.active === next.details.active &&
  previous.details.eligible === next.details.eligible &&
  previous.quote.active === next.quote.active &&
  previous.quote.eligible === next.quote.eligible;
 
const readBlocks = (
  editor: Editor,
  mode: 'highest' | 'lowest'
): ReturnType<Editor['read']['nodes']['blocks']> => {
  const selection = editor.read.selection();
 
  if (
    !selection ||
    (SelectionApi.isText(selection) &&
      !editor.read.selection.isValid(selection))
  ) {
    return [];
  }
 
  return editor.read.nodes.blocks({ mode });
};
 
const isContiguousSiblingRun = (
  entries: ReturnType<Editor['read']['nodes']['blocks']>
) => {
  if (entries.length === 0) return false;
 
  const paths = entries.map(([, path]) => path).toSorted(PathApi.compare);
  const parent = PathApi.parent(paths[0]);
  const firstIndex = paths[0].at(-1);
 
  return (
    firstIndex !== undefined &&
    paths.every(
      (path, index) =>
        PathApi.equals(PathApi.parent(path), parent) &&
        path.at(-1) === firstIndex + index
    )
  );
};
 
const hasWrapper = (
  editor: Editor,
  entry: ReturnType<Editor['read']['nodes']['blocks']>[number],
  plugin: typeof BaseBlockquotePlugin | typeof BaseDetailsPlugin
) => {
  const portal = editor.plugin(plugin);
 
  return (
    portal.installed &&
    (entry[0].type === portal.schema.type ||
      !!editor.read.nodes.above({ at: entry[1], type: plugin }))
  );
};
 
const getStructuralState = (editor: Editor): StructuralState => {
  const entries = readBlocks(editor, 'highest');
  const leafEntries = readBlocks(editor, 'lowest');
  const selectedEntries = editor.read.selection.nodes();
  const hasSelection = entries.length > 0;
  const hasContainerNodeSelection = selectedEntries.some(([, selectedPath]) =>
    leafEntries.some(([, path]) => PathApi.isAncestor(selectedPath, path))
  );
  const quote = editor.plugin(BaseBlockquotePlugin);
  const details = editor.plugin(BaseDetailsPlugin);
  const code = editor.plugin(BaseCodeBlockPlugin);
  const columns = editor.plugin(BaseColumnPlugin);
  const quoteActive =
    quote.installed &&
    hasSelection &&
    entries.every((entry) => hasWrapper(editor, entry, BaseBlockquotePlugin));
  const detailsMembership = details.installed
    ? entries.map((entry) => hasWrapper(editor, entry, BaseDetailsPlugin))
    : [];
  const detailsActive = hasSelection && detailsMembership.every(Boolean);
  const detailsMixed = detailsMembership.some(Boolean) && !detailsActive;
  const codeMembership = code.installed
    ? entries.map(([node]) => !node.listType && node.type === code.schema.type)
    : [];
  const codeActive = hasSelection && codeMembership.every(Boolean);
  const codeMixed = codeMembership.some(Boolean) && !codeActive;
  const columnActive =
    columns.installed &&
    entries.length === 1 &&
    (entries[0][0].type === columns.schema.type ||
      !!editor.read.nodes.above({
        at: entries[0][1],
        type: BaseColumnPlugin,
      }));
 
  return {
    code: {
      active: codeActive,
      eligible: code.installed && !codeMixed && isContiguousSiblingRun(entries),
    },
    columns: {
      active: columnActive,
      eligible:
        columns.installed &&
        (columnActive || (!hasContainerNodeSelection && entries.length === 1)),
    },
    details: {
      active: detailsActive,
      eligible:
        details.installed &&
        !detailsMixed &&
        (detailsActive ||
          (!hasContainerNodeSelection && isContiguousSiblingRun(entries))),
    },
    quote: {
      active: quoteActive,
      eligible: quote.installed && hasSelection,
    },
  };
};
 
const getDetailsPaths = (editor: Editor): Path[] => {
  const details = editor.plugin(BaseDetailsPlugin);
 
  if (!details.installed) return [];
 
  const paths = editor.read.nodes
    .blocks({ mode: 'highest' })
    .flatMap(([node, path]) => {
      if (node.type === details.schema.type) return [path];
 
      const ancestor = editor.read.nodes.above({
        at: path,
        type: BaseDetailsPlugin,
      });
 
      return ancestor ? [ancestor[1]] : [];
    });
 
  return paths.filter(
    (path, index) =>
      paths.findIndex((candidate) => PathApi.equals(candidate, path)) === index
  );
};
 
const getLeafFormat = (editor: Editor, node: Element): LeafFormat => {
  const list = editor.plugin(BaseListPlugin);
 
  if (list.installed && node.listType) {
    if (node.listType === 'numbered') return 'decimal';
    if (node.listType === 'task') return 'todo';
 
    return 'disc';
  }
 
  const heading = editor.plugin(BaseHeadingPlugin);
 
  if (heading.installed && node.type === heading.schema.type) {
    switch (node.level) {
      case 1:
      case 2:
      case 3:
      case 4:
      case 5:
      case 6: {
        return `heading-${node.level}`;
      }
    }
  }
 
  return 'text';
};
 
const isLeafFormatActive = (editor: Editor, value: LeafFormat) => {
  const entries = readBlocks(editor, 'lowest');
 
  return (
    entries.length > 0 &&
    entries.every(([node]) => getLeafFormat(editor, node) === value)
  );
};
 
const getTurnIntoLabel = (value: LeafFormat | undefined) => {
  switch (value) {
    case 'disc': {
      return 'Bulleted list';
    }
    case 'decimal': {
      return 'Numbered list';
    }
    case 'todo': {
      return 'To-do list';
    }
    case 'heading-1': {
      return 'Heading 1';
    }
    case 'heading-2': {
      return 'Heading 2';
    }
    case 'heading-3': {
      return 'Heading 3';
    }
    case 'heading-4': {
      return 'Heading 4';
    }
    case 'heading-5': {
      return 'Heading 5';
    }
    case 'heading-6': {
      return 'Heading 6';
    }
    case 'text': {
      return 'Text';
    }
    default: {
      return 'Mixed';
    }
  }
};
 
function TurnIntoLeafItem({
  children,
  icon,
  value,
}: React.PropsWithChildren<{
  icon: React.ReactNode;
  value: LeafFormat;
}>) {
  return (
    <DropdownMenuRadioItem
      className="min-w-[180px] pl-2 *:first:[span]:hidden"
      value={value}
    >
      {icon}
      {children}
    </DropdownMenuRadioItem>
  );
}
 
export function TurnIntoToolbarButton() {
  const editor = useEditor();
  const readOnly = useEditorReadOnly();
  const [open, setOpen] = React.useState(false);
  const leafValue = useSelectionFragmentProp({
    defaultValue: 'text',
    getProp: (node) =>
      ElementApi.isElement(node) ? getLeafFormat(editor, node) : undefined,
  }) as LeafFormat | undefined;
  const structural = useEditorSelector(getStructuralState, {
    equalityFn: structuralStateEqual,
  });
  const hasSelection = useEditorSelector(
    (current) => readBlocks(current, 'highest').length > 0
  );
  const heading = editor.plugin(BaseHeadingPlugin);
  const list = editor.plugin(BaseListPlugin);
  const quote = editor.plugin(BaseBlockquotePlugin);
  const details = editor.plugin(BaseDetailsPlugin);
  const code = editor.plugin(BaseCodeBlockPlugin);
  const columns = editor.plugin(BaseColumnPlugin);
  const leafItems: LeafItem[] = [
    {
      icon: <PilcrowIcon />,
      label: 'Text',
      value: 'text',
      onSelect: () => {
        if (
          editor.read.view.isReadOnly() ||
          isLeafFormatActive(editor, 'text')
        ) {
          return;
        }
 
        editor.update((tx) => {
          if (editor.plugin(BaseListPlugin).installed) {
            tx.plugin(BaseListPlugin).clear();
          }
          tx.blocks.reset();
        });
      },
    },
  ];
 
  if (heading.installed) {
    (
      [
        ['heading-1', 'Heading 1', <Heading1Icon key="heading-1" />, 1],
        ['heading-2', 'Heading 2', <Heading2Icon key="heading-2" />, 2],
        ['heading-3', 'Heading 3', <Heading3Icon key="heading-3" />, 3],
        ['heading-4', 'Heading 4', <Heading4Icon key="heading-4" />, 4],
        ['heading-5', 'Heading 5', <Heading5Icon key="heading-5" />, 5],
        ['heading-6', 'Heading 6', <Heading6Icon key="heading-6" />, 6],
      ] as const
    ).forEach(([value, label, icon, level]) => {
      leafItems.push({
        icon,
        label,
        value,
        onSelect: () => {
          const current = editor.plugin(BaseHeadingPlugin);
 
          if (
            !current.installed ||
            editor.read.view.isReadOnly() ||
            isLeafFormatActive(editor, value)
          ) {
            return;
          }
 
          editor.update((tx) => {
            if (editor.plugin(BaseListPlugin).installed) {
              tx.plugin(BaseListPlugin).clear();
            }
            tx.blocks.set({
              level,
              type: current.schema.type,
            });
          });
        },
      });
    });
  }
 
  if (list.installed) {
    (
      [
        ['disc', 'Bulleted list', <ListIcon key="disc" />, 'bulleted'],
        [
          'decimal',
          'Numbered list',
          <ListOrderedIcon key="decimal" />,
          'numbered',
        ],
        ['todo', 'To-do list', <SquareIcon key="todo" />, 'task'],
      ] as const
    ).forEach(([value, label, icon, type]) => {
      leafItems.push({
        icon,
        label,
        value,
        onSelect: () => {
          if (
            !editor.plugin(BaseListPlugin).installed ||
            editor.read.view.isReadOnly() ||
            isLeafFormatActive(editor, value)
          ) {
            return;
          }
 
          editor.update((tx) => {
            tx.plugin(BaseListPlugin).clear();
            tx.blocks.reset();
            tx.plugin(BaseListPlugin).toggle({ type });
          });
        },
      });
    });
  }
 
  const structuralItems = [
    ...(quote.installed
      ? [
          {
            ...structural.quote,
            icon: <QuoteIcon />,
            label: 'Quote',
            onSelect: () => {
              const current = getStructuralState(editor).quote;
 
              if (!current.eligible || editor.read.view.isReadOnly()) return;
 
              editor.update((tx) => {
                if (editor.plugin(BaseListPlugin).installed) {
                  tx.plugin(BaseListPlugin).clear();
                }
                tx.plugin(BaseBlockquotePlugin).toggle();
              });
            },
          },
        ]
      : []),
    ...(details.installed
      ? [
          {
            ...structural.details,
            icon: <ChevronRightIcon />,
            label: 'Details',
            onSelect: () => {
              const current = getStructuralState(editor).details;
 
              if (!current.eligible || editor.read.view.isReadOnly()) return;
 
              const portal = editor.plugin(BaseDetailsPlugin);
 
              if (!portal.installed) return;
 
              if (current.active) {
                const paths = getDetailsPaths(editor);
 
                if (paths.length === 0) return;
 
                const selection = editor.read.selection();
                const root = selection && SelectionApi.root(selection);
 
                portal.update.unwrap({
                  at: SelectionApi.nodes(paths as [Path, ...Path[]], {
                    ...(root ? { root } : {}),
                  }),
                });
              } else {
                portal.update.wrap();
              }
            },
          },
        ]
      : []),
    ...(code.installed
      ? [
          {
            ...structural.code,
            icon: <FileCodeIcon />,
            label: 'Code',
            onSelect: () => {
              const current = getStructuralState(editor).code;
              const portal = editor.plugin(BaseCodeBlockPlugin);
 
              if (
                !portal.installed ||
                !current.eligible ||
                editor.read.view.isReadOnly()
              ) {
                return;
              }
 
              portal.update.toggle();
            },
          },
        ]
      : []),
    ...(columns.installed
      ? [
          {
            ...structural.columns,
            icon: <Columns3Icon />,
            label: '3 columns',
            onSelect: () => {
              const current = getStructuralState(editor).columns;
              const portal = editor.plugin(BaseColumnPlugin);
 
              if (
                !portal.installed ||
                !current.eligible ||
                editor.read.view.isReadOnly()
              ) {
                return;
              }
 
              portal.update.toggle({ columns: 3 });
            },
          },
        ]
      : []),
  ];
 
  return (
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger>
        <ToolbarButton
          className="min-w-[125px]"
          disabled={readOnly || !hasSelection}
          pressed={open}
          tooltip="Turn into"
          isDropdown
        >
          {getTurnIntoLabel(leafValue)}
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent
        className="ignore-click-outside/toolbar min-w-0"
        onFinalFocus={(event) => {
          event.preventDefault();
          editor.api.dom.focus();
        }}
        align="start"
      >
        <DropdownMenuRadioGroup
          value={leafValue ?? ''}
          onValueChange={(value) => {
            const item = leafItems.find(
              (candidate) => candidate.value === value
            );
 
            item?.onSelect();
          }}
        >
          <ToolbarMenuGroup label="Turn into">
            {leafItems.map((item) => (
              <TurnIntoLeafItem
                key={item.value}
                icon={item.icon}
                value={item.value}
              >
                {item.label}
              </TurnIntoLeafItem>
            ))}
          </ToolbarMenuGroup>
        </DropdownMenuRadioGroup>
 
        {structuralItems.length > 0 && (
          <ToolbarMenuGroup label="Structure">
            {structuralItems.map((item) => (
              <DropdownMenuCheckboxItem
                key={item.label}
                checked={item.active}
                className="min-w-[180px]"
                disabled={!item.eligible}
                onSelect={item.onSelect}
              >
                {item.icon}
                {item.label}
              </DropdownMenuCheckboxItem>
            ))}
          </ToolbarMenuGroup>
        )}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
(BaseBlockquotePlugin).
toggle
();
});
}
BaseCodeBlockPlugin,
BaseHeadingPlugin,
BaseHorizontalRulePlugin,
BaseListPlugin,
BaseParagraphPlugin,
PLUGINS,
} from 'platejs';
import { BaseCodeDrawingPlugin } from 'platejs/code-drawing';
import { BaseDatePlugin } from 'platejs/date';
import { BaseDetailsPlugin } from 'platejs/details';
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { BaseFootnotePlugin } from 'platejs/footnote';
import { BaseColumnPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import { BaseImagePlugin, BaseMediaEmbedPlugin } from 'platejs/media';
import { type Editor, useEditor, useEditorReadOnly } from 'platejs/react';
import { BaseTablePlugin } from 'platejs/table';
import { BaseTocPlugin } from 'platejs/toc';
import * as React from 'react';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
ToolbarButton,
ToolbarMenuGroup,
} from '@/components/editor/toolbar';
import { linkPlugin } from './link';
type Item = {
icon: React.ReactNode;
onSelect: () => void;
value: string;
focusEditor?: boolean;
label?: string;
};
type Group = {
group: string;
items: Item[];
};
const canRestoreFocus = (
editor: Editor,
ownerDocument: Document | undefined,
activeElement: Element | null | undefined
) => {
const editable = editor.api.dom.editable();
const currentActiveElement = ownerDocument?.activeElement;
return (
editable?.isConnected === true &&
!editor.read.view.isReadOnly() &&
(!currentActiveElement ||
currentActiveElement === ownerDocument?.body ||
currentActiveElement === activeElement)
);
};
function getGroups(editor: Editor): Group[] {
const groups: Group[] = [];
const basicBlocks: Item[] = [];
const lists: Item[] = [];
const media: Item[] = [];
const advancedBlocks: Item[] = [];
const inline: Item[] = [];
const paragraph = editor.plugin(BaseParagraphPlugin);
const heading = editor.plugin(BaseHeadingPlugin);
const list = editor.plugin(BaseListPlugin);
const table = editor.plugin(BaseTablePlugin);
const codeBlock = editor.plugin(BaseCodeBlockPlugin);
const blockquote = editor.plugin(BaseBlockquotePlugin);
const horizontalRule = editor.plugin(BaseHorizontalRulePlugin);
const details = editor.plugin(BaseDetailsPlugin);
const image = editor.plugin(BaseImagePlugin);
const mediaEmbed = editor.plugin(BaseMediaEmbedPlugin);
const toc = editor.plugin(BaseTocPlugin);
const column = editor.plugin(BaseColumnPlugin);
const equation = editor.plugin(BaseEquationPlugin);
const excalidraw = editor.plugin(BaseExcalidrawPlugin);
const codeDrawing = editor.plugin(BaseCodeDrawingPlugin);
const link = editor.plugin(linkPlugin);
const date = editor.plugin(BaseDatePlugin);
const footnote = editor.plugin(BaseFootnotePlugin);
const inlineEquation = editor.plugin(BaseInlineEquationPlugin);
if (paragraph.installed) {
basicBlocks.push({
icon: <PilcrowIcon />,
label: 'Paragraph',
value: PLUGINS.paragraph,
onSelect: () => {
const current = editor.plugin(BaseParagraphPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseParagraphPlugin).insert({}, { select: true });
});
},
});
}
if (heading.installed) {
(
[
['heading-1', 'Heading 1', <Heading1Icon key="heading-1" />, 1],
['heading-2', 'Heading 2', <Heading2Icon key="heading-2" />, 2],
['heading-3', 'Heading 3', <Heading3Icon key="heading-3" />, 3],
] as const
).forEach(([value, label, icon, level]) => {
basicBlocks.push({
icon,
label,
value,
onSelect: () => {
const current = editor.plugin(BaseHeadingPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseHeadingPlugin).insert({ level }, { select: true });
});
},
});
});
}
if (table.installed) {
basicBlocks.push({
icon: <TableIcon />,
label: 'Table',
value: PLUGINS.table,
onSelect: () => {
const current = editor.plugin(BaseTablePlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseTablePlugin).insert({}, { select: true });
});
},
});
}
if (codeBlock.installed) {
basicBlocks.push({
icon: <FileCodeIcon />,
label: 'Code',
value: PLUGINS.codeBlock,
onSelect: () => {
const current = editor.plugin(BaseCodeBlockPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseCodeBlockPlugin).insert({}, { select: true });
});
},
});
}
if (blockquote.installed) {
basicBlocks.push({
icon: <QuoteIcon />,
label: 'Quote',
value: PLUGINS.blockquote,
onSelect: () => {
const current = editor.plugin(BaseBlockquotePlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseBlockquotePlugin).insert({}, { select: true });
});
},
});
}
if (horizontalRule.installed) {
basicBlocks.push({
icon: <MinusIcon />,
label: 'Divider',
value: PLUGINS.horizontalRule,
onSelect: () => {
const current = editor.plugin(BaseHorizontalRulePlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseHorizontalRulePlugin).insert({}, { select: true });
});
},
});
}
if (list.installed) {
(
[
['disc', 'Bulleted list', <ListIcon key="disc" />, 'bulleted'],
[
'decimal',
'Numbered list',
<ListOrderedIcon key="decimal" />,
'numbered',
],
['todo', 'To-do list', <SquareIcon key="todo" />, 'task'],
] as const
).forEach(([value, label, icon, type]) => {
lists.push({
icon,
label,
value,
onSelect: () => {
if (
!editor.plugin(BaseListPlugin).installed ||
editor.read.view.isReadOnly()
) {
return;
}
editor.update((tx) => {
tx.plugin(BaseListPlugin).insert({ type }, { select: true });
});
},
});
});
}
if (details.installed) {
lists.push({
icon: <ChevronRightIcon />,
label: 'Details',
value: PLUGINS.details,
onSelect: () => {
const current = editor.plugin(BaseDetailsPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseDetailsPlugin).insert({}, { select: true });
});
},
});
}
if (image.installed) {
media.push({
focusEditor: false,
icon: <ImageIcon />,
label: 'Image',
value: PLUGINS.image,
onSelect: () => {
const current = editor.plugin(BaseImagePlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
const editable = editor.api.dom.editable();
const ownerDocument = editable?.ownerDocument;
const activeElement = ownerDocument?.activeElement;
void current.api
.insertUrl(
// oxlint-disable-next-line no-alert -- This copied menu owns its URL input policy.
() => window.prompt('Enter the URL of the image'),
{ select: true }
)
.then((inserted) => {
if (
inserted &&
canRestoreFocus(editor, ownerDocument, activeElement)
) {
editor.api.dom.focus();
}
})
.catch((error: unknown) => {
console.error('Could not insert the image URL.', error);
});
},
});
}
if (mediaEmbed.installed) {
media.push({
focusEditor: false,
icon: <FilmIcon />,
label: 'Embed',
value: PLUGINS.mediaEmbed,
onSelect: () => {
const current = editor.plugin(BaseMediaEmbedPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
const editable = editor.api.dom.editable();
const ownerDocument = editable?.ownerDocument;
const activeElement = ownerDocument?.activeElement;
void current.api
.insertUrl(
// oxlint-disable-next-line no-alert -- This copied menu owns its URL input policy.
() => window.prompt('Enter the URL of the embed'),
{ select: true }
)
.then((inserted) => {
if (
inserted &&
canRestoreFocus(editor, ownerDocument, activeElement)
) {
editor.api.dom.focus();
}
})
.catch((error: unknown) => {
console.error('Could not insert the embed URL.', error);
});
},
});
}
if (toc.installed) {
advancedBlocks.push({
icon: <TableOfContentsIcon />,
label: 'Table of contents',
value: PLUGINS.toc,
onSelect: () => {
const current = editor.plugin(BaseTocPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseTocPlugin).insert({}, { select: true });
});
},
});
}
if (column.installed) {
advancedBlocks.push({
icon: <Columns3Icon />,
label: '3 columns',
value: 'action_three_columns',
onSelect: () => {
const current = editor.plugin(BaseColumnPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseColumnPlugin).insert({ columns: 3 }, { select: true });
});
},
});
}
if (equation.installed) {
advancedBlocks.push({
focusEditor: false,
icon: <RadicalIcon />,
label: 'Equation',
value: PLUGINS.equation,
onSelect: () => {
const current = editor.plugin(BaseEquationPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseEquationPlugin).insert({}, { select: true });
});
},
});
}
if (excalidraw.installed) {
advancedBlocks.push({
icon: <PenToolIcon />,
label: 'Excalidraw',
value: PLUGINS.excalidraw,
onSelect: () => {
const current = editor.plugin(BaseExcalidrawPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseExcalidrawPlugin).insert({}, { select: true });
});
},
});
}
if (codeDrawing.installed) {
advancedBlocks.push({
icon: <Code2 />,
label: 'Code Drawing',
value: PLUGINS.codeDrawing,
onSelect: () => {
const current = editor.plugin(BaseCodeDrawingPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
editor.update((tx) => {
tx.plugin(BaseCodeDrawingPlugin).insert({}, { select: true });
});
},
});
}
if (link.installed) {
inline.push({
focusEditor: false,
icon: <Link2Icon />,
label: 'Link',
value: PLUGINS.link,
onSelect: () => {
const current = editor.plugin(linkPlugin);
if (!current.installed || editor.read.view.isReadOnly()) return;
current.store.set({ text: editor.read.text.string() });
current.api.show('insert', editor.id);
},
});
}
if (date.installed) {
inline.push({
icon: <CalendarIcon />,
label: 'Date',
value: PLUGINS.date,
onSelect: () => {
if (
!editor.plugin(BaseDatePlugin).installed ||
editor.read.view.isReadOnly()
) {
return;
}
editor.update((tx) => {
tx.plugin(BaseDatePlugin).insert({}, { select: true });
});
},
});
}
if (footnote.installed) {
inline.push({
icon: <SuperscriptIcon />,
label: 'Footnote',
value: 'action_footnote',
onSelect: () => {
if (
!editor.plugin(BaseFootnotePlugin).installed ||
editor.read.view.isReadOnly()
) {
return;
}
editor.update((tx) => {
tx.plugin(BaseFootnotePlugin).insert({}, { select: true });
});
},
});
}
if (inlineEquation.installed) {
inline.push({
focusEditor: false,
icon: <RadicalIcon />,
label: 'Inline Equation',
value: PLUGINS.inlineEquation,
onSelect: () => {
if (
!editor.plugin(BaseInlineEquationPlugin).installed ||
editor.read.view.isReadOnly()
) {
return;
}
editor.update((tx) => {
tx.plugin(BaseInlineEquationPlugin).insert({}, { select: true });
});
},
});
}
if (basicBlocks.length > 0) {
groups.push({ group: 'Basic blocks', items: basicBlocks });
}
if (lists.length > 0) groups.push({ group: 'Lists', items: lists });
if (media.length > 0) groups.push({ group: 'Media', items: media });
if (advancedBlocks.length > 0) {
groups.push({ group: 'Advanced blocks', items: advancedBlocks });
}
if (inline.length > 0) groups.push({ group: 'Inline', items: inline });
return groups;
}
export function InsertToolbarButton() {
const editor = useEditor();
const readOnly = useEditorReadOnly();
const [open, setOpen] = React.useState(false);
const groups = getGroups(editor);
return (
<DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
<DropdownMenuTrigger>
<ToolbarButton
disabled={readOnly || groups.length === 0}
pressed={open}
tooltip="Insert"
isDropdown
>
<PlusIcon />
</ToolbarButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
align="start"
>
{groups.map(({ group, items }) => (
<ToolbarMenuGroup key={group} label={group}>
{items.map(
({ focusEditor = true, icon, label, value, onSelect }) => (
<DropdownMenuItem
key={value}
className="min-w-[180px]"
finalFocus={
focusEditor
? () => {
if (!editor.read.view.isReadOnly()) {
editor.api.dom.focus();
}
}
: false
}
onSelect={onSelect}
>
{icon}
{label}
</DropdownMenuItem>
)
)}
</ToolbarMenuGroup>
))}
</DropdownMenuContent>
</DropdownMenu>
);
}
'use client';
 
import {
  CalendarIcon,
  ChevronRightIcon,
  Code2,
  Columns3Icon,
  FileCodeIcon,
  FilmIcon,
  Heading1Icon,
  Heading2Icon,
  Heading3Icon,
  ImageIcon,
  Link2Icon,
  ListIcon,
  ListOrderedIcon,
  MinusIcon,
  PenToolIcon,
  PilcrowIcon,
  PlusIcon,
  QuoteIcon,
  RadicalIcon,
  SquareIcon,
  SuperscriptIcon,
  TableIcon,
  TableOfContentsIcon,
} from 'lucide-react';
import {
  BaseBlockquotePlugin,
  BaseCodeBlockPlugin,
  BaseHeadingPlugin,
  BaseHorizontalRulePlugin,
  BaseListPlugin,
  BaseParagraphPlugin,
  PLUGINS,
} from 'platejs';
import { BaseCodeDrawingPlugin } from 'platejs/code-drawing';
import { BaseDatePlugin } from 'platejs/date';
import { BaseDetailsPlugin } from 'platejs/details';
import { BaseExcalidrawPlugin } from 'platejs/excalidraw';
import { BaseFootnotePlugin } from 'platejs/footnote';
import { BaseColumnPlugin } from 'platejs/layout';
import { BaseEquationPlugin, BaseInlineEquationPlugin } from 'platejs/math';
import { BaseImagePlugin, BaseMediaEmbedPlugin } from 'platejs/media';
import { type Editor, useEditor, useEditorReadOnly } from 'platejs/react';
import { BaseTablePlugin } from 'platejs/table';
import { BaseTocPlugin } from 'platejs/toc';
import * as React from 'react';
 
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from '@/components/editor/dropdown-menu';
import {
  ToolbarButton,
  ToolbarMenuGroup,
} from '@/components/editor/toolbar';
 
import { linkPlugin } from './link';
 
type Item = {
  icon: React.ReactNode;
  onSelect: () => void;
  value: string;
  focusEditor?: boolean;
  label?: string;
};
 
type Group = {
  group: string;
  items: Item[];
};
 
const canRestoreFocus = (
  editor: Editor,
  ownerDocument: Document | undefined,
  activeElement: Element | null | undefined
) => {
  const editable = editor.api.dom.editable();
  const currentActiveElement = ownerDocument?.activeElement;
 
  return (
    editable?.isConnected === true &&
    !editor.read.view.isReadOnly() &&
    (!currentActiveElement ||
      currentActiveElement === ownerDocument?.body ||
      currentActiveElement === activeElement)
  );
};
 
function getGroups(editor: Editor): Group[] {
  const groups: Group[] = [];
  const basicBlocks: Item[] = [];
  const lists: Item[] = [];
  const media: Item[] = [];
  const advancedBlocks: Item[] = [];
  const inline: Item[] = [];
  const paragraph = editor.plugin(BaseParagraphPlugin);
  const heading = editor.plugin(BaseHeadingPlugin);
  const list = editor.plugin(BaseListPlugin);
  const table = editor.plugin(BaseTablePlugin);
  const codeBlock = editor.plugin(BaseCodeBlockPlugin);
  const blockquote = editor.plugin(BaseBlockquotePlugin);
  const horizontalRule = editor.plugin(BaseHorizontalRulePlugin);
  const details = editor.plugin(BaseDetailsPlugin);
  const image = editor.plugin(BaseImagePlugin);
  const mediaEmbed = editor.plugin(BaseMediaEmbedPlugin);
  const toc = editor.plugin(BaseTocPlugin);
  const column = editor.plugin(BaseColumnPlugin);
  const equation = editor.plugin(BaseEquationPlugin);
  const excalidraw = editor.plugin(BaseExcalidrawPlugin);
  const codeDrawing = editor.plugin(BaseCodeDrawingPlugin);
  const link = editor.plugin(linkPlugin);
  const date = editor.plugin(BaseDatePlugin);
  const footnote = editor.plugin(BaseFootnotePlugin);
  const inlineEquation = editor.plugin(BaseInlineEquationPlugin);
 
  if (paragraph.installed) {
    basicBlocks.push({
      icon: <PilcrowIcon />,
      label: 'Paragraph',
      value: PLUGINS.paragraph,
      onSelect: () => {
        const current = editor.plugin(BaseParagraphPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseParagraphPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (heading.installed) {
    (
      [
        ['heading-1', 'Heading 1', <Heading1Icon key="heading-1" />, 1],
        ['heading-2', 'Heading 2', <Heading2Icon key="heading-2" />, 2],
        ['heading-3', 'Heading 3', <Heading3Icon key="heading-3" />, 3],
      ] as const
    ).forEach(([value, label, icon, level]) => {
      basicBlocks.push({
        icon,
        label,
        value,
        onSelect: () => {
          const current = editor.plugin(BaseHeadingPlugin);
 
          if (!current.installed || editor.read.view.isReadOnly()) return;
 
          editor.update((tx) => {
            tx.plugin(BaseHeadingPlugin).insert({ level }, { select: true });
          });
        },
      });
    });
  }
 
  if (table.installed) {
    basicBlocks.push({
      icon: <TableIcon />,
      label: 'Table',
      value: PLUGINS.table,
      onSelect: () => {
        const current = editor.plugin(BaseTablePlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseTablePlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (codeBlock.installed) {
    basicBlocks.push({
      icon: <FileCodeIcon />,
      label: 'Code',
      value: PLUGINS.codeBlock,
      onSelect: () => {
        const current = editor.plugin(BaseCodeBlockPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseCodeBlockPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (blockquote.installed) {
    basicBlocks.push({
      icon: <QuoteIcon />,
      label: 'Quote',
      value: PLUGINS.blockquote,
      onSelect: () => {
        const current = editor.plugin(BaseBlockquotePlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseBlockquotePlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (horizontalRule.installed) {
    basicBlocks.push({
      icon: <MinusIcon />,
      label: 'Divider',
      value: PLUGINS.horizontalRule,
      onSelect: () => {
        const current = editor.plugin(BaseHorizontalRulePlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseHorizontalRulePlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (list.installed) {
    (
      [
        ['disc', 'Bulleted list', <ListIcon key="disc" />, 'bulleted'],
        [
          'decimal',
          'Numbered list',
          <ListOrderedIcon key="decimal" />,
          'numbered',
        ],
        ['todo', 'To-do list', <SquareIcon key="todo" />, 'task'],
      ] as const
    ).forEach(([value, label, icon, type]) => {
      lists.push({
        icon,
        label,
        value,
        onSelect: () => {
          if (
            !editor.plugin(BaseListPlugin).installed ||
            editor.read.view.isReadOnly()
          ) {
            return;
          }
 
          editor.update((tx) => {
            tx.plugin(BaseListPlugin).insert({ type }, { select: true });
          });
        },
      });
    });
  }
 
  if (details.installed) {
    lists.push({
      icon: <ChevronRightIcon />,
      label: 'Details',
      value: PLUGINS.details,
      onSelect: () => {
        const current = editor.plugin(BaseDetailsPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseDetailsPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (image.installed) {
    media.push({
      focusEditor: false,
      icon: <ImageIcon />,
      label: 'Image',
      value: PLUGINS.image,
      onSelect: () => {
        const current = editor.plugin(BaseImagePlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        const editable = editor.api.dom.editable();
        const ownerDocument = editable?.ownerDocument;
        const activeElement = ownerDocument?.activeElement;
 
        void current.api
          .insertUrl(
            // oxlint-disable-next-line no-alert -- This copied menu owns its URL input policy.
            () => window.prompt('Enter the URL of the image'),
            { select: true }
          )
          .then((inserted) => {
            if (
              inserted &&
              canRestoreFocus(editor, ownerDocument, activeElement)
            ) {
              editor.api.dom.focus();
            }
          })
          .catch((error: unknown) => {
            console.error('Could not insert the image URL.', error);
          });
      },
    });
  }
 
  if (mediaEmbed.installed) {
    media.push({
      focusEditor: false,
      icon: <FilmIcon />,
      label: 'Embed',
      value: PLUGINS.mediaEmbed,
      onSelect: () => {
        const current = editor.plugin(BaseMediaEmbedPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        const editable = editor.api.dom.editable();
        const ownerDocument = editable?.ownerDocument;
        const activeElement = ownerDocument?.activeElement;
 
        void current.api
          .insertUrl(
            // oxlint-disable-next-line no-alert -- This copied menu owns its URL input policy.
            () => window.prompt('Enter the URL of the embed'),
            { select: true }
          )
          .then((inserted) => {
            if (
              inserted &&
              canRestoreFocus(editor, ownerDocument, activeElement)
            ) {
              editor.api.dom.focus();
            }
          })
          .catch((error: unknown) => {
            console.error('Could not insert the embed URL.', error);
          });
      },
    });
  }
 
  if (toc.installed) {
    advancedBlocks.push({
      icon: <TableOfContentsIcon />,
      label: 'Table of contents',
      value: PLUGINS.toc,
      onSelect: () => {
        const current = editor.plugin(BaseTocPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseTocPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (column.installed) {
    advancedBlocks.push({
      icon: <Columns3Icon />,
      label: '3 columns',
      value: 'action_three_columns',
      onSelect: () => {
        const current = editor.plugin(BaseColumnPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseColumnPlugin).insert({ columns: 3 }, { select: true });
        });
      },
    });
  }
 
  if (equation.installed) {
    advancedBlocks.push({
      focusEditor: false,
      icon: <RadicalIcon />,
      label: 'Equation',
      value: PLUGINS.equation,
      onSelect: () => {
        const current = editor.plugin(BaseEquationPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseEquationPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (excalidraw.installed) {
    advancedBlocks.push({
      icon: <PenToolIcon />,
      label: 'Excalidraw',
      value: PLUGINS.excalidraw,
      onSelect: () => {
        const current = editor.plugin(BaseExcalidrawPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseExcalidrawPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (codeDrawing.installed) {
    advancedBlocks.push({
      icon: <Code2 />,
      label: 'Code Drawing',
      value: PLUGINS.codeDrawing,
      onSelect: () => {
        const current = editor.plugin(BaseCodeDrawingPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        editor.update((tx) => {
          tx.plugin(BaseCodeDrawingPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (link.installed) {
    inline.push({
      focusEditor: false,
      icon: <Link2Icon />,
      label: 'Link',
      value: PLUGINS.link,
      onSelect: () => {
        const current = editor.plugin(linkPlugin);
 
        if (!current.installed || editor.read.view.isReadOnly()) return;
 
        current.store.set({ text: editor.read.text.string() });
        current.api.show('insert', editor.id);
      },
    });
  }
 
  if (date.installed) {
    inline.push({
      icon: <CalendarIcon />,
      label: 'Date',
      value: PLUGINS.date,
      onSelect: () => {
        if (
          !editor.plugin(BaseDatePlugin).installed ||
          editor.read.view.isReadOnly()
        ) {
          return;
        }
 
        editor.update((tx) => {
          tx.plugin(BaseDatePlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (footnote.installed) {
    inline.push({
      icon: <SuperscriptIcon />,
      label: 'Footnote',
      value: 'action_footnote',
      onSelect: () => {
        if (
          !editor.plugin(BaseFootnotePlugin).installed ||
          editor.read.view.isReadOnly()
        ) {
          return;
        }
 
        editor.update((tx) => {
          tx.plugin(BaseFootnotePlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (inlineEquation.installed) {
    inline.push({
      focusEditor: false,
      icon: <RadicalIcon />,
      label: 'Inline Equation',
      value: PLUGINS.inlineEquation,
      onSelect: () => {
        if (
          !editor.plugin(BaseInlineEquationPlugin).installed ||
          editor.read.view.isReadOnly()
        ) {
          return;
        }
 
        editor.update((tx) => {
          tx.plugin(BaseInlineEquationPlugin).insert({}, { select: true });
        });
      },
    });
  }
 
  if (basicBlocks.length > 0) {
    groups.push({ group: 'Basic blocks', items: basicBlocks });
  }
  if (lists.length > 0) groups.push({ group: 'Lists', items: lists });
  if (media.length > 0) groups.push({ group: 'Media', items: media });
  if (advancedBlocks.length > 0) {
    groups.push({ group: 'Advanced blocks', items: advancedBlocks });
  }
  if (inline.length > 0) groups.push({ group: 'Inline', items: inline });
 
  return groups;
}
 
export function InsertToolbarButton() {
  const editor = useEditor();
  const readOnly = useEditorReadOnly();
  const [open, setOpen] = React.useState(false);
  const groups = getGroups(editor);
 
  return (
    <DropdownMenu open={open} onOpenChange={setOpen} modal={false}>
      <DropdownMenuTrigger>
        <ToolbarButton
          disabled={readOnly || groups.length === 0}
          pressed={open}
          tooltip="Insert"
          isDropdown
        >
          <PlusIcon />
        </ToolbarButton>
      </DropdownMenuTrigger>
 
      <DropdownMenuContent
        className="flex max-h-[500px] min-w-0 flex-col overflow-y-auto"
        align="start"
      >
        {groups.map(({ group, items }) => (
          <ToolbarMenuGroup key={group} label={group}>
            {items.map(
              ({ focusEditor = true, icon, label, value, onSelect }) => (
                <DropdownMenuItem
                  key={value}
                  className="min-w-[180px]"
                  finalFocus={
                    focusEditor
                      ? () => {
                          if (!editor.read.view.isReadOnly()) {
                            editor.api.dom.focus();
                          }
                        }
                      : false
                  }
                  onSelect={onSelect}
                >
                  {icon}
                  {label}
                </DropdownMenuItem>
              )
            )}
          </ToolbarMenuGroup>
        ))}
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
block.level
===
2
,
insert: (options) => {
tx.plugin(BaseHeadingPlugin).insert({ level: 2 }, options);
},
});
});
},
}
{
  icon: <Heading2Icon />,
  label: 'Heading 2',
  value: 'heading-2',
  onSelect: () => {
    const heading = editor.plugin(BaseHeadingPlugin);
 
    if (!heading.installed || editor.read.view.isReadOnly()) return;
 
    editor.update((tx) => {
      insertBlock(tx, {
        matches: (block) =>
          !block.listType &&
          block.type === heading.schema.type &&
          block.level === 2,
        insert: (options) => {
          tx.plugin(BaseHeadingPlugin).insert({ level: 2 }, options);
        },
      });
    });
  },
}