How to use the Custom Toolbar interface

My company is trying to upgrade from October CMS 1.0 to 3.0.x, and I’m trying to restore a custom button functionality to the editor for pages. I getting the registration editor extension to work as in the README.md but the constructor never was executed. So I ended up adding an example this way, after poking around a bit in the view structure:

((fn)=> {
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, {once: true});
    else fn();
})(()=>{
    'use strict';
    const run = (state)=> {
        (((state.customData??={}).customToolbarSettingsButtons ??= {})['cms-page'] ??= []).push({
            icon: 'octo-icon-info'
            , button: 'Hello World'
            , popupTitle: 'Hello World'
            , properties: []
        });
    };
    const tryTryAgain = ()=>{
        let state;
        try{
            state = $.oc.editor.store.extensions.cms.state;
        } catch (e) {
            setTimeout(tryTryAgain);
            return;
        }
        run(state);
    };
    tryTryAgain();
});

This does produce a button and the modal

But I can’t see a way to have the apply button do anything. The handleCustomToolbarButton eats the resolve promise and I don’t see a way to get the promise on callback.

Is there a better way to use this? Is this a ready to consume interface?

Hi @rkgladson

Take a look at this blog post for more details:

https://octobercms.com/blog/post/introducing-cms-editor-extensibility-api

Thanks. Sadly this doesn’t fit my use case as it doesn’t appear to make a button that directly does an action. Kind of counter-intuitive to have a PHP only solution for a Front End Framework.

I’ve had to do the following:

((fn)=> {
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, {once: true});
    else fn();
})(()=>{
    'use strict';
    const decorate = (options)=> {
        /** @type {function}*/
        const {toolbarElements} = options.computed;
        const {onToolbarCommand} = options.methods;
        options.computed.toolbarElements = function (...args) {
            const elements =  toolbarElements.apply(this, args);
            elements.push( {
                type: 'button',
                icon: 'octo-icon-copy',
                label: 'My Custom Button',
                command: 'myCustomEvent'
            });
            return elements;
        };
        options.methods.onToolbarCommand = function (...args) {
            const [command, isHotKey] = args;
            onToolbarCommand.apply(this, args);
            if (command === 'myCustomEvent' ) {
                this.onMyCustomEvent();
            }

        };
        options.methods.onMyCustomEvent = function () {
            /** @this {DocumentComponentBase['methods'|'data'|'computed']}*/
            const {documentData} = this;
            const {fileName} = documentData;
            documentData.fileName = `significant-${fileName}`;
            this.saveDocument().catch(()=>{
                documentData.fileName = fileName;
            });
        }
    };
    const tryTryAgain = ()=>{
        let options;
        try{
            options = Vue.component('cms-editor-component-page-editor').options;
        } catch (e) {
            setTimeout(tryTryAgain);
            return;
        }
        decorate(options);
    };
    tryTryAgain();
});

Ideally there would be a better way than my bit banging the definition of Vue.Component’s definition, but I haven’t yet found an entry-point there.

Hope this helps anyone just looking for just a plain button.

Sadly this doesn’t seem to fit our use case, as I need to add custom front end events to the button itself. It also is somewhat counter-intuitive to have to PHP events effect the outcome of a front end framework.

I came up with a solution to decorating the Vue’s component. I made an attempt to modify in place the component. Ideally it wouldn’t hammer the setTimeout, but I don’t know of any front end event I can hook into it’s definition.

((fn)=> {
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', fn, {once: true});
    else fn();
})(()=>{
    const decorate = (component,options)=> {
        /** @type {function}*/
        const {toolbarElements} = options.computed;
        const {onToolbarCommand} = options.methods;
        options.computed.toolbarElements = function (...args) {
            const elements =  toolbarElements.apply(this, args);

            if (!this.documentData.fileName?.startsWith('custom-')) {
                elements.push({
                    type: 'button',
                    icon: 'octo-icon-copy',
                    label: 'My Custom Command',
                    command: 'myCustomCommand'
                });
            }
            return elements;

        }
        options.methods.onToolbarCommand = function (...args) {
            const [command, isHotKey] = args;
            onToolbarCommand.apply(this, args);
            if (command === 'myCustomCommand' ) {
                this.onMyCustomCommand();
            }
        };

        options.methods.onMyCustomCommand = function () {
            /** @this {DocumentComponentBase['props'|'methods'|'computed']&ReturnType.<typeof DocumentComponentBase['data']>}*/
            const {documentData, documentMetadata} = this;
            // Change any properties you need with this.$set
            this.saveDocument().then(()=>{
                    //Emit any events you need here to update, say the navigation
                },
                ()=>{
                    // Roll back any changes to object you need.
                });
        }

        // Redefine the component element
        Vue.component(component, options);
    };

    const tryTryAgain = (component, decorate)=>{
        let componentOptions;
        try {
            componentOptions = Vue.component(component).options;
        } catch (e) {
            setTimeout(()=>tryTryAgain(component, decorate));
            return;
        }
        decorate(component, componentOptions);
    };
    // You can do this for multiple component
    tryTryAgain('cms-editor-component-layout-editor', decorate);
    tryTryAgain('cms-editor-component-partial-editor', decorate);
});

This only seems to work part of the time. And I’m not sure why. It only works when the element is on page on load, but fails when it’s selected afterwards. I suspect it is redefining something, but my attempt to catch it to do so haven’t been that successful. And now that I am trying I’m unable to reproduce.