menu_book Navigation menu

JS Hooks (UI & Editor)

Use the global window.CMS object to register tools and extensions in the Admin Panel.

info
Note
Always enqueue your JS files via the PHP hook Hook::ADMIN_ENQUEUE_JS first.

1. Visual Builder (Editor.js)

Register custom blocks for the drag-and-drop editor.

Creating a Custom Editor.js Tool

Example: A Google Maps block (components/editorjs/editor-map.js).

JAVASCRIPT
(function() { 
    class MapTool {
        static get toolbox() {
            return {
                title: 'Google Map',
                icon: '<svg width="20" height="20" viewBox="0 0 24 24"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>'
            };
        }

        constructor({data, config, api}) {
            this.data = {
                lat: data.lat || '',
                lng: data.lng || '',
                zoom: data.zoom || 12
            };
            this.apiKey = config.apiKey || '';
            this.wrapper = undefined;
        }

        render() {
            this.wrapper = document.createElement('div');
            this.wrapper.classList.add('custom-map-tool');
            this.wrapper.style.padding = '15px';
            this.wrapper.style.border = '1px solid #eee';
            this.wrapper.style.backgroundColor = '#f9f9f9';

            this.wrapper.innerHTML = `
                <div style="font-weight:bold; margin-bottom:10px; color:#555;">📍 Map Settings</div>
                <div style="display:flex; gap:10px;">
                    <input type="text" class="cdx-input map-lat" placeholder="Latitude (e.g. 48.8566)" value="${this.data.lat}">
                    <input type="text" class="cdx-input map-lng" placeholder="Longitude (e.g. 2.3522)" value="${this.data.lng}">
                </div>
                <div style="margin-top:10px; font-size:0.8em; color:#888;">
                    API Key: ${this.apiKey ? 'Yes (Hidden)' : 'Not defined'}
                </div>
            `;
            return this.wrapper;
        }

        save(blockContent) {
            return {
                lat: blockContent.querySelector('.map-lat').value,
                lng: blockContent.querySelector('.map-lng').value,
                zoom: this.data.zoom
            };
        }
    }

    // Register tool in CMS
    window.CMS = window.CMS || {};
    window.CMS.editorTools = window.CMS.editorTools || {};

    window.CMS.editorTools.map = {
        class: MapTool,
        tunes: ['containerTune'],
        inlineToolbar: true,
        config: {
            apiKey: 'AIzaSyD...'
        }
    };
})();

2. Admin UI Form Field Renderers

The JS constructor exposes a global registry to let plugins render their own configuration UI for custom fields inside the Content Type / Form builders.

Entry point: window.CMS.fieldRenderers

JAVASCRIPT
// Register a renderer for a custom field type 'color_picker'
window.CMS.fieldRenderers['color_picker'] = function(fieldId, data) {
    // Return HTML string or DOM element
    return `<input type="color" id="${fieldId}" value="${data.value}">`;
};
JAVASCRIPT
document.addEventListener('DOMContentLoaded', () => {
    window.CMS.fieldRenderers['rating'] = function(fieldId, data) {
        // Return HTML string or DOM element
        return `
            <div class="form-container">
                <label>Maximum number of stars</label>
                <input type="number" class="form-control val-max-input" value="${data.validation_rules?.match(/max:(\d+)/)?.[1] || 5}">
                <input type="hidden" name="fields[${fieldId}][validation_rules]" class="validation-rules-final">
            </div>
        `;
    };
});

3. Media & Icons API

Interact dynamically with the built-in CMS managers.

MethodDescription
window.CMS.media.open(options)Open the Media Library modal. options: { multiple: bool, onSelect: func, trigger: HTMLElement }.
window.CMS.media.load(page)Force reload the media grid via AJAX.
window.CMS.icons.open(options)Open the Icon Picker modal. options: { onSelect: func }.
window.CMS.icons.registerProvider(type, name, provider)Register a new icon set (e.g. FontAwesome). provider must implement load() and render().

Registering an Icon Provider

Here is the implementation of fontawesome-provider.js to complement the PHP logic shown in Page 1:

JAVASCRIPT
window.CMS.icons.registerProvider('fontawesome', 'Font Awesome 7', {
    // Path to the JSON catalog of icons
    path: window.CMS.baseUrl + "assets/themes/mon-theme/assets/vendors/fontawesome/icons.json",

    // Load the icon list
    async load() {
        const response = await fetch(this.path);
        const jsonData = await response.json();
        
        return Object.keys(jsonData).map(iconName => ({
            name: iconName,
            styles: jsonData[iconName].styles 
        }));
    },

    // Render the icon in the selection grid
    render(iconData, callback) {
        const item = document.createElement('div');
        item.className = 'grid-icon-item';
    
        const style = iconData.styles.includes('solid') ? 'solid' : iconData.styles[0];
        const libraryKey = `fa-${style}`;          
        const iconClassName = `fa-${style} fa-${iconData.name}`;
    
        item.innerHTML = `
            <i class="${iconClassName}"></i>
            <div class="icon-name">${iconData.name}</div>
        `;
    
        // Click action: Send data to the CMS
        item.addEventListener('click', () => {
            const iconDataForForm = {
                html: `<i class="${iconClassName}"></i>`, 
                library: libraryKey,                      
                name: iconData.name                             
            };
            callback(iconDataForForm);
        });
    
        return item;
    }
});

4. Global Event Listeners

The CMS broadcasts specific events across the document when actions occur within the managers. You can listen to these from any JS extension.

JAVASCRIPT
// Triggered when a media is selected (Legacy Mode / no specific callback provided)
document.addEventListener('cms:media:selected', (e) => {
    const mediaData = e.detail;
    console.log('User selected an image:', mediaData.url);
});

// Triggered to force the media tool to reload its grid
document.addEventListener('cms:media:reload', (e) => {
    if (window.CMS.media) { window.CMS.media.load(1); }
});

// Triggered when an icon is selected (Legacy Mode)
document.addEventListener('cms:icon:selected', (e) => {
    const iconData = e.detail; // { library, name, html }
    console.log('User selected an icon:', iconData.name);
});

// Triggered specifically by the Editor.js Gallery Tool when media array is confirmed
document.addEventListener('cms:gallery:confirmed', (e) => {
    const mediaDataArray = e.detail;
    console.log('User confirmed a gallery selection containing:', mediaDataArray.length, 'items');
});

5.Translation Reference Drawer API

When editors localize content, Pragma CMS provides a side-by-side Translation Reference Drawer that loads the source language in a read-only sidebar. Clicking the "Copy Source" button automatically copies all source values to the working form on the left.

You can customize how data is rendered inside the drawer, override the global copying process, or fine-tune individual custom fields copying via dedicated hooks.

A. DataType Custom Renderers

By default, the drawer renders raw text or basic HTML strings. To support custom or structured data types (e.g., custom recipes, forms, menus), register a custom renderer inside the window.CMS.langReferenceDrawerRenderers registry.

JAVASCRIPT
/**
 * Register a renderer for the 'recipe' dataType
 *
 * @param {Object} data - The raw JSON data of the reference entity
 * @param {HTMLElement} refContent - The container element of the drawer's body
 * @param {Object} ui - Map of common UI DOM elements inside the drawer (refTitle, refSlug, etc.)
 */
window.CMS.langReferenceDrawerRenderers['recipe'] = function(data, refContent, ui) {
    let html = `
        <div class="ref-field-group">
            <label class="ref-field-label">Ingredients</label>
            <div class="ref-content-viewer" style="background: #f5f5f5; padding: 10px;">
                ${data.ingredients?.map(ing => `• ${ing.quantity} ${ing.title}`).join('<br>')}
            </div>
            <!-- Store raw JSON for the copy algorithm to fetch later -->
            <input type="hidden" data-original-name="preparation" value="${encodeURIComponent(JSON.stringify(data.preparation))}">
        </div>
    `;
    refContent.innerHTML = html;
};

B. DataType Custom Copiers

For complex entites where standard input-to-input mapping is insufficient, register a global copier inside window.CMS.langReferenceDrawerCopiers. Returning true lets the core execution complete post-copy routines (like re-indexing and sortable rebinding).

JAVASCRIPT
/**
 * Register a custom copier for the 'recipe' dataType
 *
 * @param {HTMLElement} drawer - The main drawer DOM element
 * @param {HTMLElement} refContent - The drawer's body container
 * @param {string} currentLangId - The ID of the working language (left)
 * @return {Promise<boolean>}- Resolve with true on successful execution to trigger standard plugin re-initialization
 */
window.CMS.langReferenceDrawerCopiers['recipe'] = async function(drawer, refContent, currentLangId){
    // Implement custom copy operations between refContent and the working form on the left
    const refPrepInput = refContent.querySelector('[data-original-name="preparation"]');
    const mainPrepInput = document.getElementById('hidden-preparation');

    if (refPrepInput && mainPrepInput) {
        mainPrepInput.value = decodeURIComponent(refPrepInput.value);
        // Destroy and reload EditorJS container
        await resetEditorJsContainer('editorjs-preparation', mainPrepInput.value);
    }
    return true;
};

C. Custom Field Copiers

While standard HTML inputs are mapped automatically, custom fields with complex JavaScript interfaces (e.g., interactive maps, sliders, color pickers) require a custom callback to synchronize their values and update their UI previews on the left.

Register custom field copiers inside the window.CMS.langReferenceDrawerFieldCopiers registry.

JAVASCRIPT
/**
 * Register a custom copier for a custom 'google_maps_address' field
 *
 * @param {HTMLElement} mainEl - The primary input element in the working form (left)
 * @param {HTMLElement} refEl - The read-only reference input element in the drawer (right)
 * @param {HTMLElement} mainContainer - The parent '.form-container' of the working field
 * @param {HTMLElement} refContainer - The parent '.form-container' inside the reference drawer
 */
window.CMS.langReferenceDrawerFieldCopiers['google_maps_address'] = function(mainEl, refEl, mainContainer, refContainer){
    // 1. Copy the raw database value
    mainEl.value = refEl.value;

    // 2. Dispatch events
    mainEl.dispatchEvent(new Event("change"));

    // 3. Update custom library UI
    const mapInstance = mainContainer.querySelector('.google-map-instance');
    if (mapInstance && mainEl.value) {
        const geoData = JSON.parse(mainEl.value);
        myPluginMapsLibrary.updateMarker(mapInstance, geoData.lat, geoData.lng);
    }
};

Interactive Elements Cleanup

If your custom copier clones raw HTML inside a nested repeater structure, ensure you strip any initialization flags so that the core re-initialization routine can safely bind events to your newly copied UI elements.

JAVASCRIPT
// Example of clearing initialization flags on a copied block before plugin rebinding
copiedContainer.querySelectorAll(".autocomplete-component").forEach(ac => {
    ac.removeAttribute("data-autocomplete-initialized");
});
copiedContainer.querySelectorAll(".link-field-component").forEach(link => {
    link.removeAttribute("data-link-initialized");
});