Extend Neiki's Editor with custom toolbar buttons, init hooks, and programmatic actions.
Plugins are registered globally via NeikiEditor.registerPlugin() before or after editor initialization. A plugin can:
[!NOTE]
Plugins are global — they are available to all editor instances on the page.
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']
});
| 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.
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 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> `);
}
});
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>'
);
}
});
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>');
}
});
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 withinitplugins that modify the DOM on every input — they can interfere with cursor position. Always test thoroughly and handle edge cases.
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.`);
}
});
}
});
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!');
});
}
});
const plugins = NeikiEditor.getPlugins();
plugins.forEach(function(plugin) {
console.log('Plugin:', plugin.name);
});
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]
[!TIP]
Follow these guidelines for reliable, maintainable plugins:
myapp-word-counter)editor.insertHTML(), editor.setContent(), etc. over manipulating contentArea directlyeditor.isEmpty() or selection state before operatinginit adds event listeners outside the editor, remove them when neededaction, 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 |