Type Editor / @type-editor/menu
@type-editor/menu
A refactored version of ProseMirror's prosemirror-menu module, providing building blocks for creating customizable editor menus and a ready-to-use menu bar implementation.
Note: This module exists primarily as an example of how you might approach adding a menu to a ProseMirror-based editor. For production use, you may want to fork and customize it to fit your specific requirements.
Installation
npm install @type-editor/menuOverview
This module provides:
- Menu elements: Building blocks for creating menu items, dropdowns, and submenus
- Menu bar plugin: A ready-to-use plugin that adds a menu bar above the editor
- MenuBarBuilder: A fluent API for building menu bars with groups and dropdowns
- Pre-built menu items: Ready-to-use menu items for common editing operations (formatting, lists, headings, images, links, and more)
- Helper functions: Utilities for building block-type and wrap commands
- Icons: A set of SVG icons for common editor actions
Styling
When using this module, make sure to include the menu stylesheet in your page:
import "@type-editor/menu/style/menu.css";Or link to it directly:
<link rel="stylesheet" href="node_modules/@type-editor/menu/style/menu.css" />Basic Usage
Creating a Menu Bar
import { EditorView } from "@type-editor/view";
import { EditorState } from "@type-editor/state";
import {
menuBarPlugin,
MenuItem,
Dropdown,
icons,
undoItem,
redoItem,
strongItem,
italicItem,
headingItem,
} from "@type-editor/menu";
// Define menu content as a nested array of menu elements
const menuContent = [
[undoItem, redoItem], // First group: undo/redo
[strongItem(), italicItem()], // Second group: formatting
[headingItem("1"), headingItem("2")], // Third group: headings
];
// Create the menu bar plugin
const menuPlugin = menuBarPlugin({
content: menuContent,
floating: false, // Set to true for a sticky menu bar
});
// Add the plugin to your editor state
const state = EditorState.create({
schema: mySchema,
plugins: [menuPlugin],
});Creating Custom Menu Items
import { MenuItem, icons } from "@type-editor/menu";
import { toggleMark } from "@type-editor/commands";
const boldItem = new MenuItem({
title: "Toggle bold",
icon: icons.strong,
run: toggleMark(schema.marks.strong),
enable: (state) => toggleMark(schema.marks.strong)(state),
active: (state) => {
const { from, $from, to, empty } = state.selection;
return empty
? !!schema.marks.strong.isInSet(state.storedMarks || $from.marks())
: state.doc.rangeHasMark(from, to, schema.marks.strong);
},
});Using Dropdowns
import { Dropdown, blockTypeItem } from "@type-editor/menu";
const headingDropdown = new Dropdown(
[
blockTypeItem(schema.nodes.heading, {
attrs: { level: 1 },
label: "Heading 1",
}),
blockTypeItem(schema.nodes.heading, {
attrs: { level: 2 },
label: "Heading 2",
}),
blockTypeItem(schema.nodes.paragraph, { label: "Paragraph" }),
],
{ label: "Type", title: "Change block type" },
);Using Submenu Dropdowns
import { DropdownSubmenu, wrapItem, icons } from "@type-editor/menu";
const wrapSubmenu = new DropdownSubmenu(
[
wrapItem(schema.nodes.blockquote, {
label: "Blockquote",
icon: icons.blockquote,
}),
wrapItem(schema.nodes.code_block, {
label: "Code Block",
icon: icons.code,
}),
],
{ label: "Wrap in..." },
);Using MenuBarBuilder
The MenuBarBuilder class provides a fluent API for constructing menu bars:
import {
MenuBarBuilder,
undoItem,
redoItem,
strongItem,
italicItem,
headingItem,
paragraphItem,
} from "@type-editor/menu";
const menuPlugin = new MenuBarBuilder(false, false) // (isLegacy, floating)
.addMenuGroup(undoItem, redoItem)
.addMenuGroup(strongItem(), italicItem())
.addDropDown({ title: "Block Type", label: "Type" }, [
paragraphItem(),
headingItem("1"),
headingItem("2"),
headingItem("3"),
])
.build();API Reference
MenuElement Interface
Any object that conforms to this interface can be used in a menu:
interface MenuElement {
render(view: EditorView): {
dom: HTMLElement;
update: (state: EditorState) => boolean;
};
}MenuItem Class
Creates a clickable menu item that executes a command.
new MenuItem(spec: MenuItemSpec)MenuItemSpec Properties
| Property | Type | Description |
|---|---|---|
run | (state, dispatch, view, event) => void | Required. The command to execute when clicked. |
select | (state) => boolean | If provided and returns false, the item is hidden. |
enable | (state) => boolean | If provided and returns false, the item is disabled. |
active | (state) => boolean | Returns true when the item should appear "active" (e.g., bold toggle when in bold text). |
icon | IconSpec | An icon specification for the item. |
label | string | Text label for the item (useful in dropdowns). |
title | string | (state) => string | Tooltip text. |
render | (view) => HTMLElement | Custom render function. |
class | string | Additional CSS class. |
css | string | Inline CSS styles. |
Dropdown Class
Creates a dropdown menu with a label and expandable content.
new Dropdown(
content: MenuElement[] | MenuElement,
options?: {
label?: string;
title?: string;
class?: string;
css?: string;
}
)DropdownSubmenu Class
Creates a submenu that expands to the right on hover.
new DropdownSubmenu(
content: MenuElement[] | MenuElement,
options?: {
label?: string;
title?: string;
class?: string;
css?: string;
}
)menuBarPlugin Function
Creates a plugin that adds a menu bar above the editor.
function menuBarPlugin(options: MenuBarOptions): Plugin;MenuBarOptions
| Property | Type | Default | Description |
|---|---|---|---|
content | MenuElement[][] | — | Required. Nested array of menu elements. Each inner array becomes a group separated by visual dividers. |
floating | boolean | false | When true, the menu bar sticks to the top of the viewport when scrolling. |
MenuBarBuilder Class
A fluent builder class for constructing menu bars with groups and dropdowns.
new MenuBarBuilder(isLegacy?: boolean, floating?: boolean)Constructor Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
isLegacy | boolean | false | When true, uses legacy dropdown rendering (DropdownLegacy). |
floating | boolean | false | When true, the menu bar sticks to the top of the viewport on scroll. |
Methods
| Method | Parameters | Return | Description |
|---|---|---|---|
addMenuGroup(...items) | MenuItem, Dropdown, DropdownLegacy, or arrays thereof | MenuBarBuilder | Adds a group of menu items. Items in the same group appear together without separators. |
addDropDown(options, ...items) | options: { title?: string, label?: string }, items: MenuItem[] or Dropdown | MenuBarBuilder | Adds a dropdown menu with the given title/label and items. |
build() | — | Plugin | Creates and returns the configured menu bar plugin. |
Example
import {
MenuBarBuilder,
undoItem,
redoItem,
strongItem,
italicItem,
codeItem,
headingItem,
paragraphItem,
bulletListItem,
orderedListItem,
} from "@type-editor/menu";
const menuPlugin = new MenuBarBuilder(false, false)
.addMenuGroup(undoItem, redoItem)
.addMenuGroup(strongItem(), italicItem(), codeItem())
.addDropDown({ title: "Block Type", label: "Type" }, [
paragraphItem(),
headingItem("1"),
headingItem("2"),
headingItem("3"),
])
.addDropDown({ title: "Lists", label: "Lists" }, [
bulletListItem(),
orderedListItem(),
])
.build();Pre-built Menu Items
The following menu item functions create ready-to-use MenuItem instances:
Text Formatting
| Function | Description | Parameters |
|---|---|---|
strongItem(title?, markType?) | Toggle bold/strong formatting. | title (default: 'Bold'), markType (default: schema.marks.strong) |
italicItem(title?, markType?) | Toggle italic/emphasis formatting. | title (default: 'Italic'), markType (default: schema.marks.em) |
codeItem(title?, markType?) | Toggle inline code formatting. | title (default: 'Code'), markType (default: schema.marks.code) |
strikethroughItem(title?, markType?) | Toggle strikethrough formatting. | title (default: 'Strikethrough'), markType (default: schema.marks.strikethrough) |
subscriptItem(title?, markType?) | Toggle subscript formatting. | title (default: 'Subscript'), markType (default: schema.marks.subscript) |
superscriptItem(title?, markType?) | Toggle superscript formatting. | title (default: 'Superscript'), markType (default: schema.marks.superscript) |
linkItem(title?, linkMarkType?, codeBlockNodeType?) | Insert or edit links. | title (default: 'Link'), linkMarkType (default: schema.marks.link) |
Block Types
| Function | Description | Parameters |
|---|---|---|
paragraphItem(title?, nodeType?, ulNodeType?, olNodeType?) | Change block to paragraph. | title (default: 'Paragraph'), nodeType (default: schema.nodes.paragraph) |
headingItem(level?, title?, nodeType?, codeBlockNodeType?) | Change block to heading (h1-h6). | level ('1'-'6', default: '1'), title (default: 'Heading {level}') |
codeBlockItem(title?, nodeType?, unwrapNodeType?) | Toggle code block. | title (default: 'Code Block'), nodeType (default: schema.nodes.code_block) |
blockquoteItem(title?, nodeType?) | Toggle blockquote wrap. | title (default: 'Blockquote'), nodeType (default: schema.nodes.blockquote) |
Lists
| Function | Description | Parameters |
|---|---|---|
bulletListItem(title?, ulNodeType?, olNodeType?) | Toggle bullet list. | title (default: 'Bullet List'), ulNodeType (default: schema.nodes.bullet_list) |
orderedListItem(title?, olNodeType?, ulNodeType?) | Toggle numbered list. | title (default: 'Numbered List'), olNodeType (default: schema.nodes.ordered_list) |
Alignment
| Function | Description | Parameters |
|---|---|---|
alignItem(align?, title?, codeBlockNodeType?, ulNodeType?, olNodeType?) | Set text alignment. | align ('left', 'right', 'center', 'justify', default: 'left'), title (default: align value) |
Media & Files
| Function | Description | Parameters |
|---|---|---|
imageItem(title?, imgType?, figureType?, codeBlockNodeType?) | Insert or edit images. | title (default: 'Image'), imgType (default: schema.nodes.image) |
fileUploadItem(title?, fileType?, codeBlockNodeType?) | Upload and attach files with thumbnail preview. | title (default: 'File Upload'), fileType (default: schema.marks.file) |
horizontalRuleItem(title?, nodeType?, codeBlockNodeType?) | Insert horizontal rule. | title (default: 'Horizontal Rule'), nodeType (default: schema.nodes.horizontal_rule) |
History & Structure
| Function | Description | Parameters |
|---|---|---|
undoItem | Undo the last action. | — |
redoItem | Redo the last undone action. | — |
joinUpItem | Join current block with the one above. | — |
liftItem | Lift content out of its parent. | — |
selectParentNodeItem | Select the parent node. | — |
Helper Functions
wrapItem
function wrapItem(
nodeType: NodeType,
options: Partial<MenuItemSpec> & { attrs?: Attrs },
): MenuItem;Creates a menu item that wraps the selection in the given node type (e.g., blockquote, list).
blockTypeItem
function blockTypeItem(
nodeType: NodeType,
options: Partial<MenuItemSpec> & { attrs?: Attrs },
): MenuItem;Creates a menu item that changes the current textblock to the given type (e.g., heading, paragraph).
renderGrouped
function renderGrouped(
view: EditorView,
content: MenuElement[][],
): { dom: DocumentFragment; update: (state: EditorState) => boolean };Renders a nested array of menu elements into a document fragment with separators between groups.
Icons
A set of SVG icons for common editor actions:
import { icons } from "@type-editor/menu";
// Available icons:
icons.join; // Join blocks
icons.lift; // Lift out of parent
icons.selectParentNode;
icons.undo;
icons.redo;
icons.strong; // Bold
icons.em; // Italic
icons.strikethrough; // Strikethrough
icons.code; // Inline code
icons.codeBlock; // Code block
icons.link; // Link
icons.bulletList;
icons.orderedList;
icons.blockquote;
icons.headings; // Generic heading
(icons.h1, icons.h2, icons.h3, icons.h4, icons.h5, icons.h6);
icons.paragraph;
icons.image;
icons.file; // File attachment
icons.horizontalRule;
icons.textBlock;
(icons.alignLeft, icons.alignCenter, icons.alignRight, icons.alignJustify);
icons.subscript;
icons.superscript;IconSpec Type
Icons can be specified in three ways:
type IconSpec =
| { path: string; width: number; height: number } // SVG path
| { text: string; css?: string } // Text character
| { dom: Node }; // Custom DOM nodePDF.js Worker Configuration
The fileUploadItem uses PDF.js to generate preview images for PDF files. By default, it uses a CDN-hosted worker file for maximum compatibility. However, for production environments, you should configure a local copy of the worker file:
import { GlobalWorkerOptions } from "pdfjs-dist";
// Set this before your editor initializes
GlobalWorkerOptions.workerSrc = "/path/to/pdf.worker.min.mjs";Option 1: Copy worker from node_modules
Copy the worker file from your node_modules to your public directory:
cp node_modules/pdfjs-dist/build/pdf.worker.min.mjs public/Then set the worker path:
GlobalWorkerOptions.workerSrc = "/pdf.worker.min.mjs";Option 2: Vite Configuration
If you're using Vite, you can configure it to copy the worker file automatically:
// vite.config.ts
import { defineConfig } from "vite";
import { copyFileSync } from "fs";
import { resolve } from "path";
export default defineConfig({
plugins: [
{
name: "copy-pdf-worker",
writeBundle() {
copyFileSync(
resolve("node_modules/pdfjs-dist/build/pdf.worker.min.mjs"),
resolve("dist/pdf.worker.min.mjs"),
);
},
},
],
});License
MIT
Modules
| Module | Description |
|---|---|
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
menu-items/image-item-plugins/create-handle-drop-finished-plugin | ‐ |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ | |
‐ |