🎨 Custom Editors (Advanced UI)
When you need more than simple form fields—like sortable lists, dynamic item management, or live previews—use custom editors.
What Custom Editors Enable
- Sortable lists (drag & drop reordering)
- Dynamic item management (add/remove/duplicate)
- Collapsible sections (accordion-style editors)
- Live previews (image thumbnails, color swatches)
- Complex layouts (multi-column forms, tabs)
- Custom interactions (anything you can build with JavaScript!)
The openContentEditor Function
editor: {
openContentEditor: function(element, builder, onChange) {
// Build your custom UI
const container = document.createElement('div');
// ... add your custom UI elements ...
return container; // Must return a DOM element
}
}
Parameters
1. element - The Plugin Element
The DOM element containing your plugin content.
openContentEditor: function(element, builder, onChange) {
// Access plugin content
const cards = element.querySelectorAll('.card');
// Modify plugin content
element.appendChild(newElement);
existingElement.remove();
}
2. builder - ContentBuilder/ContentBox Instance
Access builder features like file pickers:
openContentEditor: function(element, builder, onChange) {
// Open file picker for images
builder.openFilePicker('image', (url) => {
imageElement.src = url;
onChange();
});
// Open file picker for videos
builder.openFilePicker('video', (url) => {
videoElement.src = url;
onChange();
});
// Available types: 'image', 'video', 'audio', 'file'
}
Available File Picker Types:
'image'- Image files only'video'- Video files only'audio'- Audio files only'file'- All file types'media'- Image and Video files
3. onChange - Change Notification Callback
Call this whenever plugin content changes:
openContentEditor: function(element, builder, onChange) {
input.addEventListener('input', () => {
element.querySelector('h1').textContent = input.value;
onChange(); // Required!
});
}
When to call onChange:
- After modifying element content
- After adding/removing elements
- After reordering elements
- After any change that should be saved
Note: You can also call onChange with a delay for better performance with frequent updates:
let timer;
input.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(() => {
element.textContent = input.value;
onChange();
}, 300); // Wait 300ms after user stops typing
});
Return Value
Must return a DOM element containing your custom UI:
openContentEditor: function(element, builder, onChange) {
const container = document.createElement('div');
// Build your UI inside container
container.appendChild(titleInput);
container.appendChild(descriptionTextarea);
container.appendChild(addButton);
return container; // Required!
}
Quick Start Example
Let's build a Card List editor where users can add, remove, and edit cards:
export default {
name: 'card-list',
displayName: 'Card List',
version: '1.0.0',
settings: {
columns: {
type: 'number',
label: 'Columns',
default: 3,
min: 1,
max: 6
}
},
editor: {
openContentEditor: function(element, builder, onChange) {
const container = document.createElement('div');
const cards = element.querySelectorAll('.card');
// Create editor for a single card
const createCardEditor = (card, index) => {
const editor = document.createElement('div');
editor.style.cssText = 'border: 1px solid #ddd; padding: 12px; margin-bottom: 10px; border-radius: 4px;';
// Title input
const titleInput = document.createElement('input');
titleInput.type = 'text';
titleInput.value = card.querySelector('h3')?.textContent || '';
titleInput.placeholder = 'Card title';
titleInput.style.cssText = 'margin-bottom: 8px;';
titleInput.addEventListener('input', () => {
card.querySelector('h3').textContent = titleInput.value;
onChange();
});
// Description textarea
const descInput = document.createElement('textarea');
descInput.value = card.querySelector('p')?.textContent || '';
descInput.placeholder = 'Card description';
descInput.style.cssText = 'height: 140px; margin-bottom: 8px;';
descInput.addEventListener('input', () => {
card.querySelector('p').textContent = descInput.value;
onChange();
});
// Delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = (e) => {
e.preventDefault();
card.remove();
editor.remove();
onChange();
};
editor.appendChild(titleInput);
editor.appendChild(descInput);
editor.appendChild(deleteBtn);
return editor;
};
// Create editor for each card
cards.forEach((card, index) => {
container.appendChild(createCardEditor(card, index));
});
// Add "New Card" button
const addBtn = document.createElement('button');
addBtn.textContent = '+ Add Card';
addBtn.onclick = (e) => {
e.preventDefault();
const newCard = document.createElement('div');
newCard.className = 'card';
newCard.innerHTML = `
<img src="https://placehold.co/600x400?text=New+Card" alt="">
<h3>New Card</h3>
<p>Card description</p>
`;
element.appendChild(newCard);
const editor = createCardEditor(newCard, container.children.length);
container.insertBefore(editor, addBtn);
onChange();
};
container.appendChild(addBtn);
return container;
}
},
mount: function(element, options) {
element.style.display = 'grid';
element.style.gridTemplateColumns = `repeat(${options.columns || 3}, 1fr)`;
element.style.gap = '16px';
return {};
}
};
💡 Note: Standard HTML elements (input, textarea, button, select) are automatically styled by ContentBuilder/ContentBox. You only need custom styles for special elements like image previews or custom layouts.