Menu

Plugin API

neikiri

🔌 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]

🔒 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

⚙️ 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