Plugin Development Guide
Welcome to the comprehensive Plugin Development Guide for On-Codemerge, a flexible and extensible web editor designed for seamless plugin integration. This guide covers everything you need to know to create powerful plugins that extend the editor's functionality.
Table of Contents
- Plugin Architecture
- Basic Plugin Structure
- Plugin Interface
- Core API Reference
- UI Components
- Event System
- Command Pattern
- Services
- Styling
- Best Practices
- Advanced Features
- Testing
- Examples
Plugin Architecture
On-Codemerge uses a modular plugin architecture where each plugin is a self-contained module that can be loaded independently. The core system provides a rich API for plugins to interact with the editor, manage UI components, handle events, and execute commands.
Key Components
- HTMLEditor: The main editor instance that manages plugins and provides core functionality
- Plugin Interface: Standard interface that all plugins must implement
- PluginManager: Manages plugin registration, lifecycle, and hotkeys
- Event System: Pub/sub system for communication between plugins and editor
- Command Pattern: For executing and undoing operations
- UI Components: Reusable components for creating interfaces
Basic Plugin Structure
Every plugin follows a standard structure:
import './style.scss';
import './public.scss';
import type { Plugin } from '../../core/Plugin';
import type { HTMLEditor } from '../../core/HTMLEditor';
export class MyPlugin implements Plugin {
name = 'my-plugin';
version = '1.0.0';
hotkeys = [
{
keys: 'Ctrl+Shift+M',
description: 'My Plugin Action',
command: 'my-plugin-action',
icon: '🔧'
}
];
private editor: HTMLEditor | null = null;
initialize(editor: HTMLEditor): void {
this.editor = editor;
this.setupEventListeners();
this.addToolbarButton();
}
destroy(): void {
this.cleanup();
this.editor = null;
}
private setupEventListeners(): void {
// Setup event listeners
}
private addToolbarButton(): void {
// Add toolbar button
}
private cleanup(): void {
// Cleanup resources
}
}
Plugin Interface
The Plugin
interface defines the contract that all plugins must follow:
export interface Plugin {
version?: string;
name: string;
hotkeys?: Hotkey[];
initialize: (editor: HTMLEditor) => void;
destroy?: () => void;
}
interface Hotkey {
icon: string;
keys: string;
description: string;
command: string;
}
Interface Properties
- name: Unique identifier for the plugin
- version: Plugin version (optional)
- hotkeys: Array of keyboard shortcuts (optional)
- initialize: Called when plugin is registered with editor
- destroy: Called when plugin is removed (optional)
Core API Reference
HTMLEditor Methods
Content Management
// Get current HTML content
const html = editor.getHtml();
// Set HTML content
await editor.setHtml('<p>New content</p>');
// Insert content at cursor
editor.insertContent('<span>Inserted text</span>');
// Insert text at cursor
editor.insertTextAtCursor('Plain text');
// Subscribe to content changes
const unsubscribe = editor.subscribeToContentChange((content) => {
console.log('Content changed:', content);
});
Selection Management
// Get selection container
const container = editor.getContainer();
// Get inner container
const innerContainer = editor.getInnerContainer();
// Save cursor position
const position = editor.saveCursorPosition();
// Restore cursor position
editor.restoreCursorPosition(position);
// Ensure editor has focus
editor.ensureEditorFocus();
Plugin Management
// Register a plugin
editor.use(new MyPlugin());
// Get all plugins
const plugins = editor.getPlugins();
// Get specific plugin
const plugin = plugins.get('my-plugin');
// Get hotkeys from all plugins
const hotkeys = editor.getHotkeys();
Text Formatting
// Get text formatter
const formatter = editor.getTextFormatter();
// Apply text styles
formatter?.toggleStyle('bold');
formatter?.toggleStyle('italic');
formatter?.toggleStyle('underline');
formatter?.toggleStyle('strikethrough');
// Check if style is applied
const isBold = formatter?.hasClass('bold');
// Apply alignment
formatter?.toggleStyle('alignLeft');
formatter?.toggleStyle('alignCenter');
formatter?.toggleStyle('alignRight');
formatter?.toggleStyle('alignJustify');
Localization
// Set locale
await editor.setLocale('ru');
// Get current locale
const locale = editor.getLocale();
// Translate text
const translated = editor.t('key', { param: 'value' });
Selector Service
The Selector service provides advanced selection and range management:
// Get selector instance
const selector = editor.getSelector();
// Save current selection
selector?.saveSelection();
// Restore saved selection
const range = selector?.getSelection();
// Check if selection is inside editor
const isInside = selector?.isSelectionInsideEditor();
// Get selected text
const selectedText = selector?.getSelectedText();
// Get selected elements
const selectedElements = selector?.getSelectedElements();
UI Components
Toolbar Buttons
Create toolbar buttons using the utility function. The toolbar is obtained through the editor's getToolbar()
method, which internally retrieves it from the ToolbarPlugin:
import { createToolbarButton } from '../ToolbarPlugin/utils';
private addToolbarButton(): void {
const toolbar = this.editor?.getToolbar();
if (toolbar) {
const button = createToolbarButton({
icon: '🔧',
title: 'My Plugin',
onClick: () => this.handleClick(),
disabled: false
});
toolbar.appendChild(button);
}
}
Important: Always use this.editor?.getToolbar()
. The editor's method ensures that the toolbar is obtained from the ToolbarPlugin instance, which may not always be present. If ToolbarPlugin is not loaded, getToolbar()
will return null
.
Popup Manager
Create modal dialogs with the PopupManager:
import { PopupManager, type PopupItem } from '../../core/ui/PopupManager';
private createPopup(): void {
const popup = new PopupManager(this.editor!, {
title: 'My Plugin Settings',
className: 'my-plugin-popup',
closeOnClickOutside: true,
items: [
{
type: 'input',
id: 'name',
label: 'Name',
placeholder: 'Enter name',
value: ''
},
{
type: 'checkbox',
id: 'enabled',
label: 'Enable feature',
value: true
},
{
type: 'list',
id: 'type',
label: 'Type',
options: ['Option 1', 'Option 2', 'Option 3'],
value: 'Option 1'
}
],
buttons: [
{
label: 'Save',
variant: 'primary',
onClick: () => this.handleSave()
},
{
label: 'Cancel',
variant: 'secondary',
onClick: () => popup.hide()
}
]
});
// Show popup
popup.show();
// Get values
const name = popup.getValue('name');
const enabled = popup.getValue('enabled');
// Set values
popup.setValue('name', 'New value');
// Hide popup
popup.hide();
}
Popup Item Types
Available popup item types:
- input: Text input
- textarea: Multi-line text input
- checkbox: Boolean checkbox
- list: Dropdown select
- radio: Radio button group
- number: Numeric input
- range: Slider input
- color: Color picker
- file: File upload
- date: Date picker
- time: Time picker
- datetime-local: Date and time picker
- password: Password input
- email: Email input
- url: URL input
- custom: Custom HTML content
- button: Action button
- divider: Visual separator
- progress: Progress bar
- loader: Loading spinner
- text: Read-only text
Context Menus
Create context menus for right-click actions:
import { ContextMenu } from '../../core/ui/ContextMenu';
private createContextMenu(): void {
const contextMenu = new ContextMenu(this.editor!, {
items: [
{
label: 'Action 1',
icon: '🔧',
onClick: () => this.handleAction1()
},
{
label: 'Action 2',
icon: '⚙️',
onClick: () => this.handleAction2()
},
{
type: 'divider'
},
{
label: 'Submenu',
icon: '📁',
submenu: [
{
label: 'Sub Action 1',
onClick: () => this.handleSubAction1()
},
{
label: 'Sub Action 2',
onClick: () => this.handleSubAction2()
}
]
}
]
});
// Show context menu at position
contextMenu.show(x, y);
}
Notifications
The editor provides a built-in notification system for showing user feedback. All notification methods are available through the editor instance:
// Show a basic notification (info type by default)
this.editor.showNotification('Operation completed successfully');
// Show different types of notifications
this.editor.showSuccessNotification('Data saved successfully');
this.editor.showErrorNotification('Failed to save data');
this.editor.showWarningNotification('Please check your input');
this.editor.showInfoNotification('Processing your request...');
// Custom notification with specific duration and type
this.editor.showNotification(
'Custom message',
'success', // 'success' | 'error' | 'warning' | 'info'
5000 // duration in milliseconds (default: 3000)
);
Notification Types
- Success: Green notification for successful operations
- Error: Red notification for errors and failures
- Warning: Yellow notification for warnings and cautions
- Info: Blue notification for informational messages
Notification Features
- Auto-dismiss: Notifications automatically disappear after the specified duration
- Multiple positions: Notifications can appear in different screen positions
- Dark mode support: Notifications adapt to the current theme
- Responsive design: Notifications work well on all screen sizes
- Non-blocking: Notifications don't interfere with user interaction
Best Practices
// Good: Clear, actionable messages
this.editor.showSuccessNotification('File uploaded successfully');
this.editor.showErrorNotification('Please check your internet connection');
// Good: Use appropriate notification types
try {
await this.saveData();
this.editor.showSuccessNotification('Data saved successfully');
} catch (error) {
this.editor.showErrorNotification('Failed to save data: ' + error.message);
}
// Good: Provide context for warnings
if (unsavedChanges) {
this.editor.showWarningNotification('You have unsaved changes');
}
// Good: Inform about long operations
this.editor.showInfoNotification('Processing your request...');
// ... perform operation
this.editor.showSuccessNotification('Request completed');
Integration with Plugin Actions
export class MyPlugin implements Plugin {
// ... other plugin code
private handleSave(): void {
try {
// Perform save operation
this.saveData();
this.editor?.showSuccessNotification('Data saved successfully');
} catch (error) {
this.editor?.showErrorNotification('Failed to save data');
}
}
private handleDelete(): void {
if (this.confirmDeletion()) {
this.deleteData();
this.editor?.showSuccessNotification('Item deleted');
}
}
private handleValidation(): void {
if (!this.validateData()) {
this.editor?.showWarningNotification('Please check your input');
return;
}
// Continue with valid data
}
}
Event System
The editor uses a powerful event system for communication between plugins and the editor.
Listening to Events
// Listen to editor events
this.editor.on('content-change', (content) => {
console.log('Content changed:', content);
});
// Listen to custom events
this.editor.on('my-custom-event', (data) => {
console.log('Custom event:', data);
});
// Listen to keyboard events
this.editor.on('keydown', (event) => {
console.log('Key pressed:', event.key);
});
// Listen to selection changes
this.editor.on('selectionchange', (event) => {
console.log('Selection changed');
});
Triggering Events
// Trigger custom events
this.editor.triggerEvent('my-custom-event', { data: 'value' });
// Trigger with multiple arguments
this.editor.triggerEvent('plugin-action', arg1, arg2, arg3);
Common Events
- content-change: Fired when content changes
- selectionchange: Fired when selection changes
- keydown: Fired on keyboard input
- drag-start: Fired when drag operation starts
- drag-end: Fired when drag operation ends
- drag-enter: Fired when element enters drop zone
- drag-over: Fired when element is over drop zone
- drag-leave: Fired when element leaves drop zone
- drag-drop: Fired when element is dropped
Unsubscribing from Events
// Store unsubscribe function
const unsubscribe = this.editor.on('content-change', handler);
// Later, unsubscribe
unsubscribe();
// Or unsubscribe all handlers for an event
this.editor.off('content-change');
Command Pattern
The Command pattern is used for executing and undoing operations. Commands encapsulate actions and can be executed, undone, and redone.
Creating Commands
import type { Command } from '../../../core/commands/Command';
export class MyCommand implements Command {
private editor: HTMLEditor;
private data: any;
constructor(editor: HTMLEditor, data: any) {
this.editor = editor;
this.data = data;
}
execute(): void {
// Execute the command
this.performAction();
}
private performAction(): void {
// Implementation
}
}
Using Commands
// Execute command
const command = new MyCommand(this.editor, data);
command.execute();
// Or trigger via event system
this.editor.triggerEvent('execute-command', command);
Command Examples
// Insert content command
export class InsertContentCommand implements Command {
private content: string;
private range: Range;
constructor(editor: HTMLEditor, content: string, range: Range) {
this.content = content;
this.range = range.cloneRange();
}
execute(): void {
const fragment = document.createRange().createContextualFragment(this.content);
this.range.deleteContents();
this.range.insertNode(fragment);
}
}
// Delete content command
export class DeleteContentCommand implements Command {
private range: Range;
private deletedContent: DocumentFragment;
constructor(editor: HTMLEditor, range: Range) {
this.range = range.cloneRange();
}
execute(): void {
this.deletedContent = this.range.extractContents();
}
}
Services
HTML Formatter
Format and manipulate HTML content:
import { HTMLFormatter } from '../../core/services/HTMLFormatter';
const formatter = new HTMLFormatter();
// Format HTML
const formatted = formatter.format(html);
// Minify HTML
const minified = formatter.minify(html);
// Validate HTML
const isValid = formatter.validate(html);
Text Formatter
Apply text formatting and styles:
const formatter = editor.getTextFormatter();
// Apply styles
formatter?.toggleStyle('bold');
formatter?.toggleStyle('italic');
formatter?.toggleStyle('underline');
formatter?.toggleStyle('strikethrough');
// Apply colors
formatter?.setTextColor('#ff0000');
formatter?.setBackgroundColor('#ffff00');
// Apply font
formatter?.setFontFamily('Arial');
formatter?.setFontSize('16px');
// Check styles
const isBold = formatter?.hasClass('bold');
const isItalic = formatter?.hasClass('italic');
Locale Manager
Handle internationalization:
import { LocaleManager } from '../../core/services/LocaleManager';
const localeManager = new LocaleManager();
// Initialize with locale
await localeManager.initialize('ru');
// Set locale
await localeManager.setLocale('en');
// Get current locale
const locale = localeManager.getCurrentLocale();
// Get loaded locales
const locales = localeManager.getLoadedLocales();
// Translate text
const translated = localeManager.t('key', { param: 'value' });
Styling
SCSS Structure
Each plugin should have its own styles:
// style.scss - Internal styles
.my-plugin {
&-button {
// Button styles
}
&-popup {
// Popup styles
}
}
// public.scss - Public styles (for users)
.my-plugin-public {
// Public styles
}
CSS Classes
Use consistent naming conventions:
// Plugin-specific classes
.my-plugin {
// Main plugin container
}
.my-plugin-button {
// Button styles
}
.my-plugin-button.active {
// Active state
}
.my-plugin-button:hover {
// Hover state
}
.my-plugin-button:disabled {
// Disabled state
}
Theme Support
Support both light and dark themes:
.my-plugin {
background: var(--editor-bg);
color: var(--editor-text);
border: 1px solid var(--editor-border);
}
// Dark theme
[data-theme="dark"] .my-plugin {
background: var(--editor-bg-dark);
color: var(--editor-text-dark);
border-color: var(--editor-border-dark);
}
Best Practices
Plugin Structure
- Keep plugins focused: Each plugin should have a single responsibility
- Use TypeScript: Leverage type safety for better development experience
- Follow naming conventions: Use consistent naming for classes, methods, and files
- Document your code: Add JSDoc comments for public methods
- Handle errors gracefully: Always handle potential errors and edge cases
Performance
- Lazy initialization: Initialize heavy components only when needed
- Event cleanup: Always unsubscribe from events in destroy method
- DOM manipulation: Minimize DOM queries and cache references
- Debounce events: Use debouncing for frequent events like resize or scroll
Accessibility
- Keyboard navigation: Ensure all functionality is accessible via keyboard
- Screen readers: Add proper ARIA labels and roles
- Focus management: Manage focus properly in modals and popups
- Color contrast: Ensure sufficient color contrast for text
Error Handling
initialize(editor: HTMLEditor): void {
try {
this.editor = editor;
this.setupComponents();
} catch (error) {
console.error('Failed to initialize plugin:', error);
// Handle initialization error
}
}
private setupComponents(): void {
if (!this.editor) {
throw new Error('Editor not available');
}
// Setup components
}
Advanced Features
Hotkeys
Register keyboard shortcuts for your plugin:
export class MyPlugin implements Plugin {
hotkeys = [
{
keys: 'Ctrl+Shift+M',
description: 'My Plugin Action',
command: 'my-plugin-action',
icon: '🔧'
},
{
keys: 'Alt+M',
description: 'Another Action',
command: 'my-plugin-another',
icon: '⚙️'
}
];
initialize(editor: HTMLEditor): void {
this.editor = editor;
// Listen to hotkey events
this.editor.on('my-plugin-action', () => {
this.handleAction();
});
this.editor.on('my-plugin-another', () => {
this.handleAnotherAction();
});
}
}
File Handling
Handle file uploads and drag-and-drop:
private setupFileHandling(): void {
this.editor.on('file-drop', (files: FileList) => {
this.handleFiles(files);
});
this.editor.on('paste', (event: ClipboardEvent) => {
const files = event.clipboardData?.files;
if (files) {
this.handleFiles(files);
}
});
}
private async handleFiles(files: FileList): Promise<void> {
for (const file of Array.from(files)) {
if (file.type.startsWith('image/')) {
await this.handleImage(file);
} else if (file.type.startsWith('video/')) {
await this.handleVideo(file);
}
}
}
Resizable Elements
Make elements resizable:
import { ResizableElement } from '../../utils/ResizableElement';
private makeResizable(element: HTMLElement): void {
const resizable = new ResizableElement(element, {
minWidth: 100,
minHeight: 50,
maxWidth: 800,
maxHeight: 600,
onResize: (width, height) => {
console.log('Resized to:', width, height);
}
});
}
Custom Components
Create reusable components:
export class MyComponent {
private element: HTMLElement;
constructor(options: any) {
this.element = this.createElement(options);
}
private createElement(options: any): HTMLElement {
const element = document.createElement('div');
element.className = 'my-component';
// Build component structure
return element;
}
getElement(): HTMLElement {
return this.element;
}
show(): void {
this.element.style.display = 'block';
}
hide(): void {
this.element.style.display = 'none';
}
destroy(): void {
this.element.remove();
}
}
Testing
Unit Testing
Test your plugin components:
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { MyPlugin } from './MyPlugin';
import { HTMLEditor } from '../../core/HTMLEditor';
describe('MyPlugin', () => {
let editor: HTMLEditor;
let plugin: MyPlugin;
let container: HTMLElement;
beforeEach(() => {
container = document.createElement('div');
editor = new HTMLEditor(container);
plugin = new MyPlugin();
});
afterEach(() => {
plugin.destroy();
editor.destroy();
});
it('should initialize correctly', () => {
plugin.initialize(editor);
expect(plugin.name).toBe('my-plugin');
});
it('should handle events', () => {
plugin.initialize(editor);
// Test event handling
});
});
Integration Testing
Test plugin integration with editor:
describe('MyPlugin Integration', () => {
it('should work with other plugins', () => {
const editor = new HTMLEditor(container);
editor.use(new ToolbarPlugin());
editor.use(new MyPlugin());
// Test integration
});
});
Examples
Simple Button Plugin
import './style.scss';
import type { Plugin } from '../../core/Plugin';
import type { HTMLEditor } from '../../core/HTMLEditor';
import { createToolbarButton } from '../ToolbarPlugin/utils';
export class SimpleButtonPlugin implements Plugin {
name = 'simple-button';
private editor: HTMLEditor | null = null;
private button: HTMLElement | null = null;
initialize(editor: HTMLEditor): void {
this.editor = editor;
this.addButton();
}
private addButton(): void {
const toolbar = this.editor?.getToolbar();
if (toolbar) {
this.button = createToolbarButton({
icon: '🔧',
title: 'Simple Action',
onClick: () => this.handleClick()
});
toolbar.appendChild(this.button);
}
}
private handleClick(): void {
this.editor?.insertTextAtCursor('Button clicked!');
}
destroy(): void {
this.button?.remove();
this.editor = null;
}
}
Modal Plugin
import './style.scss';
import type { Plugin } from '../../core/Plugin';
import type { HTMLEditor } from '../../core/HTMLEditor';
import { PopupManager } from '../../core/ui/PopupManager';
import { createToolbarButton } from '../ToolbarPlugin/utils';
export class ModalPlugin implements Plugin {
name = 'modal';
private editor: HTMLEditor | null = null;
private popup: PopupManager | null = null;
initialize(editor: HTMLEditor): void {
this.editor = editor;
this.createPopup();
this.addButton();
}
private createPopup(): void {
this.popup = new PopupManager(this.editor!, {
title: 'Modal Plugin',
items: [
{
type: 'input',
id: 'text',
label: 'Enter text',
placeholder: 'Type something...'
},
{
type: 'checkbox',
id: 'enabled',
label: 'Enable feature',
value: true
}
],
buttons: [
{
label: 'OK',
variant: 'primary',
onClick: () => this.handleOK()
},
{
label: 'Cancel',
variant: 'secondary',
onClick: () => this.popup?.hide()
}
]
});
}
private addButton(): void {
const toolbar = this.editor?.getToolbar();
if (toolbar) {
const button = createToolbarButton({
icon: '📋',
title: 'Open Modal',
onClick: () => this.popup?.show()
});
toolbar.appendChild(button);
}
}
private handleOK(): void {
const text = this.popup?.getValue('text');
const enabled = this.popup?.getValue('enabled');
console.log('Modal values:', { text, enabled });
this.popup?.hide();
}
destroy(): void {
this.popup?.destroy();
this.editor = null;
}
}
Event-Driven Plugin
import './style.scss';
import type { Plugin } from '../../core/Plugin';
import type { HTMLEditor } from '../../core/HTMLEditor';
export class EventPlugin implements Plugin {
name = 'event-plugin';
private editor: HTMLEditor | null = null;
private unsubscribe: (() => void)[] = [];
initialize(editor: HTMLEditor): void {
this.editor = editor;
this.setupEventListeners();
}
private setupEventListeners(): void {
// Listen to content changes
this.unsubscribe.push(
this.editor.on('content-change', (content) => {
console.log('Content changed:', content);
})
);
// Listen to selection changes
this.unsubscribe.push(
this.editor.on('selectionchange', () => {
console.log('Selection changed');
})
);
// Listen to keyboard events
this.unsubscribe.push(
this.editor.on('keydown', (event) => {
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
this.handleSave();
}
})
);
}
private handleSave(): void {
const content = this.editor?.getHtml();
console.log('Saving content:', content);
// Implement save logic
}
destroy(): void {
// Unsubscribe from all events
this.unsubscribe.forEach(unsub => unsub());
this.unsubscribe = [];
this.editor = null;
}
}
This comprehensive guide covers all aspects of plugin development for On-Codemerge. Use these examples and best practices to create powerful, maintainable plugins that extend the editor's functionality.