Ckeditor5: Disabling a button after it's been added to the toolbar

Created on 22 Mar 2017  路  10Comments  路  Source: ckeditor/ckeditor5

Hello,

I apologise if this is a question with an obvious answer, but I searched and couldn't find what I needed.

I'd like to disable and enable certain buttons (they can stay there but it would be great if they can be greyed out and not respond to input) at will that reside in the toolbar after they have been added and the editor is up and running. Is there any way to do this?

I know that it was possible in CKEDITOR 4, for example as this plugin does:
http://hectorguo.com/CKEditor-Markdown-Plugin/

(Basically I am trying to emulate the behaviour of this plugin in my app. Convert to and from markdown and disable other buttons while markdown mode is active)

Kind regards,

Dean

question

Most helpful comment

To disable buttons, you need to disable commands related to these buttons.

In CKEditor 4 we had a problem with multiple plugins managing the state of commands. It was always unclear what should be the state when I want to re-enable command. Didn't any other plugin disable it in the meantime too? Maybe it should stay disabled because you can not use this button in this context? When should the state change?

This is why in CKEditor 5 you do not disable/enable commands directly. Instead, whenever something happens what could change the state of command (selection change or your markdown mode is turned on/off) command.refreshState(); should be called. This method fire 'refreshState' event, which let every plugin say if the feature should be disabled or not.

It means that you should do something like:

const disableCallback = ( evt, data ) => {
    if( markdownMode ) {
        data.isEnabled = false;
        evt.stop();
    }
};

editor.commands.forEach( ( command ) => {
    command.on( 'refreshState' , disableCallback );
    command.refreshState();
} );

If anybody asks for the state in the markdown mode, the state will be disabled, but as soon as markdownMode is false you don't disable commands and let other plugins decide what is the proper state.

Event may be stopped byevt.stop(), so other callbacks won't be able to change command state. But they don't have to, so callbacks can interact each other and override.

All 10 comments

Hey Dean!

Most like CKEditor 4, the toolbar buttons are usually linked to "commands". Therefore, changes on the state of the commands will be reflect in the UI.

We played a bit here with this and the following solution seems to work for disabling all commands:

const disableCallback = ( evt, data ) => {
    data.isEnabled = false;
    evt.stop();
};

editor.commands.forEach( ( command ) => {
    command.on( 'refreshState', disableCallback );
    command.refreshState();
} );

Then, to re-enable them:

editor.commands.forEach( ( command ) => {
    command.off( 'refreshState', disableCallback );
    command.refreshState();
} );

I hope this will work for your case ;)

To disable buttons, you need to disable commands related to these buttons.

In CKEditor 4 we had a problem with multiple plugins managing the state of commands. It was always unclear what should be the state when I want to re-enable command. Didn't any other plugin disable it in the meantime too? Maybe it should stay disabled because you can not use this button in this context? When should the state change?

This is why in CKEditor 5 you do not disable/enable commands directly. Instead, whenever something happens what could change the state of command (selection change or your markdown mode is turned on/off) command.refreshState(); should be called. This method fire 'refreshState' event, which let every plugin say if the feature should be disabled or not.

It means that you should do something like:

const disableCallback = ( evt, data ) => {
    if( markdownMode ) {
        data.isEnabled = false;
        evt.stop();
    }
};

editor.commands.forEach( ( command ) => {
    command.on( 'refreshState' , disableCallback );
    command.refreshState();
} );

If anybody asks for the state in the markdown mode, the state will be disabled, but as soon as markdownMode is false you don't disable commands and let other plugins decide what is the proper state.

Event may be stopped byevt.stop(), so other callbacks won't be able to change command state. But they don't have to, so callbacks can interact each other and override.

In case you haven't seen it yet I am posting recent topic about custom data processors: https://github.com/ckeditor/ckeditor5/issues/417. There is also a link to our experimental Markdown Data Processor which might be interesting for you.

@fredck & @pjasiun: I had to reopen the ticket unfortunately. It seems that it works fine if I add a timeout like this:

editor.commands.forEach( ( command ) => {
        command.on( 'refreshState', disableCallback );
        command.refreshState();
} );

setTimeout(function(){
    editor.commands.forEach( ( command ) => {
         command.off( 'refreshState', disableCallback );
         command.refreshState();
     } );
}, 3000);

However a slightly more advanced version like below (in which I want the markdown button to stay active and activate/deactivate the other buttons depending on if the mode is active) does not do anything the second time I click it.

Thus, the buttons stay disabled. I have verified that the 'else if' statement works fine, it does move to that part of the code, yet it just does not trigger a UI change:

// Disable editing buttons other than Markdown Mode
const disableCallback = ( evt, data ) => {
    data.isEnabled = false;
    evt.stop();
};

if (!store.markdownMode.isActive){
    editor.commands.forEach( ( command ) => {
        if (command.constructor.name !== 'ToMarkdownCommand') {
            command.on( 'refreshState', disableCallback );
            command.refreshState();
        }
    } );

    store.markdownMode.isActive = true;

} else if (store.markdownMode.isActive) {
    editor.commands.forEach( ( command ) => {
        if (command.constructor.name !== 'ToMarkdownCommand') {
             command.off( 'refreshState', disableCallback );
             command.refreshState();
        }
     } );

    store.markdownMode.isActive = false;
}

Am I making a silly mistake here?

@szymonkups: Appreciated! I'm happy with the library I am using now to convert, but I will keep this in mind for the future for sure.

The code looks good, so maybe it's something outside? E.g., are you sure that the code isn't executed twice? On the first run it would disable the commands, change store.markdownMode.isActive to true which somehow may trigger the same callback to be called again, but this time the second if() would be executed, reverting the results of the first run.

@Reinmar: Yes, I got the following 2 logs when I pressed 2 times, so no issues in that regard I think:

tomarkdowncommand.js?f36e:50 MarkdownMode is not active and button was pressed, so making Markdown mode active and disabling all other buttons.

tomarkdowncommand.js?f36e:64 MarkdownMode is active and button was pressed, so making Markdown mode inactive and enabling all other buttons.

It seems that it works fine if I add a timeout like this:

I believe you should not need to use any timeout, but you should add listeners in the proper moment of the editor initialization when all commands will be ready.

You can use promise returned by the editor class:

ClassicEditor.create( document.querySelector( '#editor' ), {
    plugins: [ Enter, Typing, Paragraph, Undo, Heading, Bold, Italic ],
    toolbar: [ 'headings', 'bold', 'italic', 'undo', 'redo' ]
} ).then( editor => {
    // Your code goes here...
} )
.catch( err => {
    console.error( err.stack );
} );

If you add listeners in the plugin, I recommend you use pluginsReady event:

export default class CommandsDisabler extends Plugin {
    init() {
        this.editor.on( 'pluginsReady', () => {
            // Your code goes here...
        } );
    }
}

@pjasiun I mentioned the timeout just to indicate that if I put the timeout, the code does work in the sense that I press on the button -> they are disabled -> timeout runs out -> they are enabled again. I want to get rid of the timeout of course, and get a system where I can toggle between the two states (as I tried to do above).

I tried adding your pluginsReady code in my plugin files (tried it in the main plugin file, the command file and the engine file) but in each case I either get an error or it just doesn't do anything at all unfortunately.

Maybe it's more convenient if I upload what I have in gists:

  1. Main plugin file: https://gist.github.com/deanvaessen/4155026043262679049cc70932822bac
  2. Plugin engine file: https://gist.github.com/deanvaessen/b874fa78523ff9ce7a2ad27bd69a1f8f
  3. Command file: https://gist.github.com/deanvaessen/cf21114145150863259c70ce6f5254a7

(I am structuring it this way because that's the way I saw other plugins do it).

Maybe I'm making a mistake with how the plugin is built up?

Hi @deanvaessen,

I've looked at the code you provided and I think that the issue may lay here: https://gist.github.com/deanvaessen/cf21114145150863259c70ce6f5254a7#file-tomarkdowncommand-js-L44-L47

In this part of code, you create a new callback every time your command is executed. Because of that command.off() cannot correctly unbind the callback, because at that point disableCallback holds a different value.

The solution would be to move disableCallback to higher scope (for example after class definition). Let me know if that helped!

(BTW. You could probably check command != this here: https://gist.github.com/deanvaessen/cf21114145150863259c70ce6f5254a7#file-tomarkdowncommand-js-L53).

@scofalik: Thanks a ton! That did it. I moved it outside the class and also added your command != this suggestion, works cleaner that way. I agree :)

A big thank you to the rest also who helped me get my request sorted out. Much appreciated!

Was this page helpful?
0 / 5 - 0 ratings