🎨 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.

View this page on GitHub

© 2026 InnovaStudio

This site is built with Site Builder Kit