Skip to content

Plugin API

neikiri edited this page May 18, 2026 · 4 revisions

πŸ”Œ Plugin API

Extend Neiki's Editor with custom toolbar buttons, init hooks, and programmatic actions.


🧩 Overview

Plugins are registered globally via NeikiEditor.registerPlugin() before or after editor initialization. A plugin can:

  • Add a toolbar button with a custom SVG icon
  • Run an action when the button is clicked
  • Run an init function when the editor starts up

Note

Plugins are global β€” they are available to all editor instances on the page.


πŸ“ Registering a Plugin

NeikiEditor.registerPlugin({
    name: 'my-plugin',            // unique identifier (required)
    icon: '<svg viewBox="0 0 24 24">...</svg>',  // toolbar icon SVG
    tooltip: 'My Custom Action',  // tooltip on hover
    action: function(editor) {
        // Called when toolbar button is clicked
    },
    init: function(editor) {
        // Called once when the editor initializes
    }
});

Then include it in the toolbar config:

new NeikiEditor('#editor', {
    toolbar: ['bold', 'italic', '|', 'my-plugin', '|', 'moreMenu']
});

πŸ“Š Plugin Properties

Property Type Required Description
name string βœ… Unique identifier. Referenced in the toolbar array.
icon string ❌ SVG markup for the toolbar button
tooltip string ❌ Hover tooltip text
action function(editor) ❌ Handler called when the toolbar button is clicked
init function(editor) ❌ Handler called once when the editor initializes

Important

Plugin names must be unique. Registering a second plugin with the same name overwrites the first.


πŸ’‘ Plugin Examples

Word Counter Alert

Show a word and character count dialog:

NeikiEditor.registerPlugin({
    name: 'word-counter',
    icon: '<svg viewBox="0 0 24 24"><path d="M3 18h12v-2H3v2zM3 6v2h18V6H3zm0 7h18v-2H3v2z"/></svg>',
    tooltip: 'Show Word Count',
    action: function(editor) {
        const text = editor.getText();
        const words = text.trim().split(/\s+/).filter(Boolean).length;
        const chars = text.length;
        alert(`Words: ${words}\nCharacters: ${chars}`);
    }
});

Insert Timestamp

Insert current date/time at the cursor position:

NeikiEditor.registerPlugin({
    name: 'timestamp',
    icon: '<svg viewBox="0 0 24 24"><path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm.5-13H11v6l5.25 3.15.75-1.23-4.5-2.67V7z"/></svg>',
    tooltip: 'Insert Timestamp',
    action: function(editor) {
        const now = new Date();
        const formatted = now.toLocaleString();
        editor.insertHTML(`<span style="color:#6b7280;font-size:0.9em">[${formatted}]</span>&nbsp;`);
    }
});

Insert Styled Divider

NeikiEditor.registerPlugin({
    name: 'styled-divider',
    icon: '<svg viewBox="0 0 24 24"><path d="M2 12h4v1H2v-1zm6 0h4v1H8v-1zm6 0h4v1h-4v-1zm6 0h2v1h-2v-1z"/></svg>',
    tooltip: 'Insert Styled Divider',
    action: function(editor) {
        editor.insertHTML(
            '<div style="text-align:center;margin:1.5em 0;color:#d1d5db;letter-spacing:0.5em;font-size:1.2em">β€’ β€’ β€’</div>'
        );
    }
});

Clear All Formatting

Strip inline styles and attributes from content:

NeikiEditor.registerPlugin({
    name: 'clean-paste',
    icon: '<svg viewBox="0 0 24 24"><path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/></svg>',
    tooltip: 'Clean All Formatting',
    action: function(editor) {
        const text = editor.contentArea.innerText;
        editor.setContent('<p>' + text.split('\n\n').join('</p><p>') + '</p>');
    }
});

Auto-Capitalize Headings (init hook)

Run code on every input event using the init hook:

NeikiEditor.registerPlugin({
    name: 'auto-capitalize',
    init: function(editor) {
        editor.contentArea.addEventListener('input', function() {
            const headings = editor.contentArea.querySelectorAll('h1, h2, h3, h4, h5, h6');
            headings.forEach(function(h) {
                if (h.textContent.length > 0) {
                    const first = h.textContent.charAt(0);
                    if (first !== first.toUpperCase()) {
                        h.textContent = first.toUpperCase() + h.textContent.slice(1);
                    }
                }
            });
        });
    }
});

Caution

Be careful with init plugins that modify the DOM on every input β€” they can interfere with cursor position. Always test thoroughly and handle edge cases.


Character Limit

Enforce a maximum character count:

NeikiEditor.registerPlugin({
    name: 'char-limit',
    init: function(editor) {
        const MAX = 5000;
        editor.contentArea.addEventListener('input', function() {
            const text = editor.getText();
            if (text.length > MAX) {
                // truncate to limit
                const truncated = text.slice(0, MAX);
                editor.setContent('<p>' + truncated + '</p>');
                alert(`Character limit of ${MAX} reached.`);
            }
        });
    }
});

Export as Markdown (action plugin)

NeikiEditor.registerPlugin({
    name: 'export-markdown',
    icon: '<svg viewBox="0 0 24 24"><path d="M20.56 18H3.44C2.65 18 2 17.37 2 16.59V7.41C2 6.63 2.65 6 3.44 6h17.12C21.35 6 22 6.63 22 7.41v9.18c0 .78-.65 1.41-1.44 1.41zM9.5 16v-4.5l2.5 3 2.5-3V16H16V8h-1.5l-2.5 3-2.5-3H8v8h1.5zm-5-3v-2H6V9H4.5v2H3l2 2 2-2H5.5zm11.75.25L18 11.5h-1.25V9h-1.5v2.5H14l2.25 2.25z"/></svg>',
    tooltip: 'Copy as Markdown',
    action: function(editor) {
        // Simple HTML-to-markdown conversion
        let md = editor.getContent()
            .replace(/<h1>(.*?)<\/h1>/gi, '# $1\n')
            .replace(/<h2>(.*?)<\/h2>/gi, '## $1\n')
            .replace(/<h3>(.*?)<\/h3>/gi, '### $1\n')
            .replace(/<strong>(.*?)<\/strong>/gi, '**$1**')
            .replace(/<em>(.*?)<\/em>/gi, '*$1*')
            .replace(/<p>(.*?)<\/p>/gi, '$1\n\n')
            .replace(/<[^>]+>/g, '');

        navigator.clipboard.writeText(md.trim()).then(() => {
            alert('Copied as Markdown!');
        });
    }
});

πŸ“‹ Listing Registered Plugins

const plugins = NeikiEditor.getPlugins();

plugins.forEach(function(plugin) {
    console.log('Plugin:', plugin.name);
});

πŸ—οΈ Plugin Architecture

graph TD
    A[NeikiEditor Instance] --> B[Toolbar Area]
    A --> C[Content Area]
    A --> D[Status Bar]
    B --> E[Plugin Buttons]
    E --> F[word-counter]
    E --> G[timestamp]
    E --> H[custom-plugin]
    A --> I[Plugin Init Hooks]
    I --> J[auto-capitalize β†’ addEventListener]
    I --> K[char-limit β†’ addEventListener]
Loading

πŸ”’ Best Practices

Tip

Follow these guidelines for reliable, maintainable plugins:

  1. Use unique names β€” prefix with your project name to avoid collisions (e.g., myapp-word-counter)
  2. One plugin, one responsibility β€” keep actions focused and simple
  3. Prefer editor API methods β€” use editor.insertHTML(), editor.setContent(), etc. over manipulating contentArea directly
  4. Test with all 4 themes β€” ensure your plugin's UI looks correct in Light, Dark, Blue, and Dark Blue
  5. Handle empty selections β€” check editor.isEmpty() or selection state before operating
  6. Clean up in destroy β€” if your init adds event listeners outside the editor, remove them when needed
  7. Don't block the main thread β€” for heavy operations in action, use setTimeout or async patterns

πŸ”— Related Pages

βš™οΈ Configuration Editor configuration & toolbar array
πŸ”§ Toolbar Reference Built-in toolbar button identifiers
πŸ“‹ API Reference insertHTML, setContent, getContent and all other methods
🧩 Advanced Features Tables, images, themes, autosave

Clone this wiki locally