Create Custom Plugins for Editora
Learn how to build and integrate custom plugins with Editora Rich Text Editor
This comprehensive guide will walk you through creating your own plugins to extend Editora's functionality. You'll learn the plugin architecture, build your first plugin, and integrate it into your editor.
đ Table of Contents
đ Understanding Plugin Architecture
What is a Plugin?
A plugin in Editora is a modular component that extends the editor's functionality. Plugins can:
- Add new toolbar buttons and commands
- Create custom node types in the document
- Handle keyboard shortcuts
- Provide UI components and interactions
- Integrate with external APIs
- Transform and validate content
Core Plugin Concepts
- ProseMirror: Editora is built on ProseMirror, a powerful editor framework
- Schema: Defines the structure of editable content (nodes and marks)
- Commands: Functions that modify editor state
- Plugins: Extend editor behavior with new features
- Providers: React components that manage plugin state and UI
Plugin Anatomy
Every Editora plugin consists of three main parts:
| Component | Purpose | Example |
|---|---|---|
| Node/Mark Definition | Defines how content is stored in the document | MyCustomNode.ts |
| Commands | Functions that insert/modify the custom element | insertMyNodeCommand |
| UI Provider | React component for UI and state management | MyNodeProvider.tsx |
đī¸ Plugin Project Structure
Creating a New Plugin Package
If you're building a plugin as part of the monorepo:
packages/plugins/my-custom-plugin/ âââ src/ â âââ MyCustomPlugin.ts (node definition) â âââ MyCustomProvider.tsx (React component) â âââ MyCustom.css (styles) â âââ index.ts (exports) âââ package.json âââ tsconfig.json âââ README.md
File Descriptions
- MyCustomPlugin.ts: Core plugin logic, node/mark schema, and commands
- MyCustomProvider.tsx: React component managing plugin UI and state
- MyCustom.css: Styling for rendered nodes and UI components
- index.ts: Exports the plugin components for external use
đ Creating Your First Plugin - Callout Box
Let's build a practical plugin that adds a Callout Box (alert/note box) to the editor. This plugin will help users create highlighted information boxes.
1Create Plugin Function (CalloutPlugin.ts)
Start by creating a plugin function that returns a Plugin object:
import { Plugin } from '@editora/core';
/**
* Callout Plugin - Native Implementation
*
* Adds callout boxes (alert/note boxes) to the editor.
* Supports info, warning, success, and error types.
*/
export const CalloutPlugin = (): Plugin => ({
name: 'callout',
// Define the callout node in the schema
nodes: {
callout: {
group: 'block',
atom: true,
attrs: {
type: { default: 'info' },
message: { default: '' }
},
parseDOM: [
{
tag: 'div.callout',
getAttrs(dom: HTMLElement) {
return {
type: dom.getAttribute('data-type') || 'info',
message: dom.textContent || ''
};
}
}
],
toDOM(node) {
return [
'div',
{
class: `callout callout-${node.attrs.type}`,
data-type: node.attrs.type
},
['p', node.attrs.message]
];
}
}
},
// Toolbar button configuration
toolbar: [
{
label: 'Callout Box',
command: 'insertCallout',
icon: 'đŦ'
}
],
// Commands for inserting callouts
commands: {
insertCallout: (type: 'info' | 'warning' | 'success' | 'error' = 'info') => {
const message = prompt('Enter callout message:');
if (!message) return false;
// Use document.execCommand for simplicity
// In production, manipulate EditorState directly
const calloutHtml = `<div class="callout callout-${type}" data-type="${type}"><p>${message}</p></div>`;
document.execCommand('insertHTML', false, calloutHtml);
return true;
}
},
// Keyboard shortcuts
keymap: {
'Mod-Shift-C': () => 'insertCallout'
}
});
2Export Plugin Function
Export your plugin function so it can be imported and used:
export { CalloutPlugin } from './CalloutPlugin';
3Use the Plugin
Import and use your plugin in your editor setup:
import { CalloutPlugin } from './src/index';
// In your HTML
<editora-editor
plugins="[CalloutPlugin()]"
toolbar-items="['bold', 'italic', 'callout']"
></editora-editor>
import { EditoraEditor } from '@editora/react';
import { CalloutPlugin } from './src/index';
function MyEditor() {
return (
<EditoraEditor
plugins={[CalloutPlugin()]}
toolbarItems={['bold', 'italic', 'callout']}
/>
);
}
4Add Styles (Callout.css)
/* Callout Box Styles */
.callout {
padding: 16px;
margin: 12px 0;
border-radius: 6px;
border-left: 4px solid;
background: #f9f9f9;
font-size: 14px;
line-height: 1.6;
}
.callout-info {
border-left-color: #0066cc;
background: #e3f2fd;
color: #0066cc;
}
.callout-warning {
border-left-color: #ff9800;
background: #fff3e0;
color: #ff9800;
}
.callout-success {
border-left-color: #4caf50;
background: #e8f5e9;
color: #4caf50;
}
.callout-error {
border-left-color: #f44336;
background: #ffebee;
color: #f44336;
}
.callout p {
margin: 0;
}
đ¯ Plugin Types & Examples
Block Plugins
Block-level elements that take up full width (examples: callout, quote, code block)
Inline Plugins
Elements that flow within text (examples: link, mention, emoji)
Mark Plugins
Text formatting marks (examples: bold, italic, highlight)
Command-Only Plugins
Plugins that provide functionality without creating new content types (examples: find/replace, undo/redo)
Popular Plugin Examples
| Plugin Type | Built-in Examples | Idea for Custom Plugin |
|---|---|---|
| Block | Table, Code Sample, Quote | Timeline, Timeline Event, Accordion |
| Inline | Link, Mention | Price Tag, Badge, Tooltip |
| Mark | Bold, Italic, Underline | Highlight Color, Font Style |
| Command | Find/Replace, Export | Word Count, Reading Time, Analytics |
đ Integrating Your Plugin
Method 1: Web Component
<script src="https://unpkg.com/@editora/core@1.0.1"></script>
<script src="https://unpkg.com/my-callout-plugin@1.0.0"></script>
<editora-editor
plugins="[window.MyCalloutPlugin.CalloutPlugin()]"
toolbar-items="['bold', 'italic', 'callout']"
></editora-editor>
Method 2: React Component
import React from 'react';
import { EditoraEditor } from '@editora/react';
import { CalloutPlugin } from 'my-callout-plugin';
export const App = () => {
return (
<EditoraEditor
plugins={[CalloutPlugin()]}
toolbarItems={['bold', 'italic', 'callout']}
/>
);
};
Step-by-Step Integration
- Install plugin:
npm install my-callout-plugin - Import plugin function: Import the plugin function from your package
- Call the function: Execute the plugin function to get the Plugin object
- Add to plugins array: Include the plugin in your editor's plugins array
- Add toolbar items: Include plugin toolbar items in toolbar-items
- Test: Verify the plugin works in your editor
⥠Advanced Features
Keyboard Shortcuts
Add keyboard shortcuts to your plugin commands:
export const keyboardShortcuts = {
'Ctrl-Shift-K': insertCalloutCommand('info', ''),
'Ctrl-Shift-W': insertCalloutCommand('warning', ''),
'Ctrl-Shift-S': insertCalloutCommand('success', '')
};
Plugin Configuration
Make your plugin configurable by accepting options:
export interface CalloutConfig {
types?: string[];
defaultType?: 'info' | 'warning' | 'success' | 'error';
showIcon?: boolean;
customStyles?: Record<string, string>;
}
export const CalloutPlugin = (config: CalloutConfig = {}): Plugin => ({
name: 'callout',
// Configurable node definition
nodes: {
callout: {
group: 'block',
atom: true,
attrs: {
type: { default: config.defaultType || 'info' },
message: { default: '' }
},
parseDOM: [
{
tag: 'div.callout',
getAttrs(dom: HTMLElement) {
return {
type: dom.getAttribute('data-type') || config.defaultType || 'info',
message: dom.textContent || ''
};
}
}
],
toDOM(node) {
return [
'div',
{
class: `callout callout-${node.attrs.type}`,
data-type: node.attrs.type
},
['p', node.attrs.message]
];
}
}
},
// Configurable toolbar
toolbar: config.showIcon !== false ? [
{
label: 'Callout Box',
command: 'insertCallout',
icon: 'đŦ'
}
] : [],
// Commands with config
commands: {
insertCallout: (type?: 'info' | 'warning' | 'success' | 'error') => {
const calloutType = type || config.defaultType || 'info';
const message = prompt('Enter callout message:');
if (!message) return false;
const calloutHtml = `<div class="callout callout-${calloutType}" data-type="${calloutType}"><p>${message}</p></div>`;
document.execCommand('insertHTML', false, calloutHtml);
return true;
}
},
// Keyboard shortcuts
keymap: {
'Mod-Shift-C': () => 'insertCallout'
}
});
Plugin State Management
Plugins can manage their own state and lifecycle:
export const CounterPlugin = (): Plugin => {
let count = 0;
return {
name: 'counter',
// Mark for text highlighting
marks: {
highlight: {
parseDOM: [{ tag: 'mark' }],
toDOM() { return ['mark', { class: 'highlight' }, 0]; }
}
},
// Commands that use state
commands: {
highlightText: () => {
count++;
document.execCommand('insertHTML', false, `<mark class="highlight">Highlighted ${count}</mark>`);
return true;
}
},
toolbar: [
{
label: 'Highlight',
command: 'highlightText',
icon: 'đī¸'
}
],
keymap: {
'Mod-Shift-H': () => 'highlightText'
}
};
};
đĻ Publishing Your Plugin
Step 1: Prepare Your Package
package.json - must include:"name": "my-callout-plugin""version": "1.0.0""main": "dist/index.js""module": "dist/index.mjs""types": "dist/index.d.ts""files": ["dist", "README.md"]"peerDependencies": { "@editora/core": "^1.0.0"}Step 2: Export Structure
export { CalloutPlugin } from './CalloutPlugin';
export type { CalloutConfig } from './CalloutPlugin';
Step 2: Build the Plugin
npm run build
Step 3: Publish to npm
# Login to npm
npm login
# Publish
npm publish --access public
Step 4: Create Documentation
- Installation instructions
- Basic usage example
- Configuration options
- Keyboard shortcuts
- License information
⨠Best Practices & Tips
Code Quality
- Use TypeScript for type safety
- Write unit tests for your plugin
- Follow the existing code style
- Document your code with JSDoc comments
- Handle edge cases and errors gracefully
Performance
- Memoize React components with React.memo
- Use useCallback for event handlers
- Avoid unnecessary re-renders
- Lazy load heavy dependencies
- Minimize CSS selectors complexity
User Experience
- Provide clear visual feedback
- Add keyboard shortcuts
- Include helpful tooltips
- Support undo/redo properly
- Mobile-friendly design
Debugging Tips
- Plugin not loading: Check that you're calling the plugin function:
CalloutPlugin()notCalloutPlugin - Node not appearing: Verify node is defined in the plugin's nodes property
- Command not working: Ensure command is defined in plugin's commands object
- Toolbar button missing: Check toolbar configuration and toolbar-items attribute
- Styles not applied: Import CSS and ensure class names match toDOM output
Testing Your Plugin
import { describe, it, expect } from 'vitest';
import { CalloutPlugin } from './CalloutPlugin';
describe('Callout Plugin', () => {
it('should create plugin object', () => {
const plugin = CalloutPlugin();
expect(plugin).toBeDefined();
expect(plugin.name).toBe('callout');
expect(plugin.nodes).toHaveProperty('callout');
expect(plugin.commands).toHaveProperty('insertCallout');
});
it('should accept configuration', () => {
const plugin = CalloutPlugin({ defaultType: 'warning' });
expect(plugin.nodes.callout.attrs.type.default).toBe('warning');
});
it('should have toolbar configuration', () => {
const plugin = CalloutPlugin();
expect(plugin.toolbar).toHaveLength(1);
expect(plugin.toolbar[0].command).toBe('insertCallout');
});
});
Common Mistakes to Avoid
- Forget to call the plugin function: use
plugins={[CalloutPlugin()]}notplugins={[CalloutPlugin]} - Hardcode styles - make them customizable through configuration
- Skip the Plugin interface - always return a properly typed Plugin object
- Ignore TypeScript warnings about the Plugin interface
- Skip testing plugin function instantiation before publishing
- Mix old ProseMirror patterns with the new Plugin interface
Plugin Development Resources
đ Quick Reference
Plugin File Template
// MyPlugin.ts
import { Node } from 'prosemirror-model';
export const MyPluginNode = {
name: 'my-plugin',
group: 'block', // or 'inline'
atom: true, // non-editable by default
attrs: {
// Define attributes
},
toDOM(node) {
return ['div', {}, 'Content'];
},
parseDOM: [
{ tag: 'div' }
]
};
export const insertMyPluginCommand = () => ({ tr, dispatch }) => {
const node = tr.doc.type.schema.nodes['my-plugin'].create();
if (dispatch) dispatch(tr.replaceRangeWith(tr.selection.$from.pos, tr.selection.$from.pos, node));
return true;
};
Common Command Patterns
| Task | Code Example |
|---|---|
| Insert Node | tr.replaceRangeWith(pos, pos, node) |
| Replace Selection | tr.replaceSelectionWith(node) |
| Wrap Selection | tr.wrap(range, schema.nodes.wrapper) |
| Get Current Node | state.doc.nodeAt(pos) |
| Get Selection | state.selection.$from / $to |
đ You're Ready to Build!
You now have everything you need to create custom plugins for Editora. Start by building your first plugin, test it thoroughly, and share it with the community.
Next steps:
- Choose a plugin idea that excites you
- Create the plugin package structure
- Build and test your plugin
- Publish to npm
- Share with the Editora community