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.

📚 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

🔑 Key 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:

src/CalloutPlugin.ts
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:

src/index.ts
export { CalloutPlugin } from './CalloutPlugin';

3Use the Plugin

Import and use your plugin in your editor setup:

Web Component Usage
import { CalloutPlugin } from './src/index';

// In your HTML
<editora-editor
  plugins="[CalloutPlugin()]"
  toolbar-items="['bold', 'italic', 'callout']"
></editora-editor>
React Usage
import { EditoraEditor } from '@editora/react';
import { CalloutPlugin } from './src/index';

function MyEditor() {
  return (
    <EditoraEditor
      plugins={[CalloutPlugin()]}
      toolbarItems={['bold', 'italic', 'callout']}
    />
  );
}

4Add Styles (Callout.css)

src/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)

Use when: Creating elements that should be standalone blocks in the document

Inline Plugins

Elements that flow within text (examples: link, mention, emoji)

Use when: Creating elements that should appear within paragraphs or text

Mark Plugins

Text formatting marks (examples: bold, italic, highlight)

Use when: Creating text styling or formatting options

Command-Only Plugins

Plugins that provide functionality without creating new content types (examples: find/replace, undo/redo)

Use when: Creating utility features without new content types

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

index.html - 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

App.tsx - 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

  1. Install plugin: npm install my-callout-plugin
  2. Import plugin function: Import the plugin function from your package
  3. Call the function: Execute the plugin function to get the Plugin object
  4. Add to plugins array: Include the plugin in your editor's plugins array
  5. Add toolbar items: Include plugin toolbar items in toolbar-items
  6. Test: Verify the plugin works in your editor

⚡ Advanced Features

Keyboard Shortcuts

Add keyboard shortcuts to your plugin commands:

Adding Keyboard Shortcuts
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:

Configurable Plugin
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:

Plugin with State
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

src/index.ts
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

Include in your README.md:
  • 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

âš ī¸ Common Issues & Solutions:
  • Plugin not loading: Check that you're calling the plugin function: CalloutPlugin() not CalloutPlugin
  • 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

Plugin Function Test
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

❌ Don't:
  • Forget to call the plugin function: use plugins={[CalloutPlugin()]} not plugins={[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

Complete Plugin 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