Ckeditor5: Unable to cancel associated keystroke on calling cancel API

Created on 11 Sep 2018  ·  10Comments  ·  Source: ckeditor/ckeditor5

Is this a bug report or feature request? (choose one)

🐞 Bug report

💻 11.0.1

📋 Steps to reproduce

  1. Associate a keystroke callback function with editor using below code

    editor.keystrokes.set( "delete", this.deleteKeyHandler, { priority : "highest" } );
    
    private deleteKeyHandler(ev, cancel){
        cancel();
    }
    
  2. Press delete key in the editor

✅ Delete key should not work

❎ Delete key is working

utils bug

All 10 comments

cc @f1ames

I can confirm it doesn't work as expected (see this codepen to reproduce).

@kapilgupta77 Until we investigate this issue in more detail, if you only want to prevent delete action you can use:

editor.editing.view.document.on( 'delete', ( evt, data ) => {
    evt.stop();
}, { priority: 'highest' } );

as a workaround.

From what I see the KeystrokeHandler which is responsible for handling keystrokes listens to keydown events emitted from editor view document and re-emits them in its own _format_. The keydown event with keyCode 46 becomes new _keydown:46 event.

Then inside KeystrokeHandler.set() method (which is the same as editor.keystrokes.set) for delete key, the listener listening to _keydown:46 is set up. The callback provided in editor.keystrokes.set is executed correctly and cancel() function cancels event correctly. However, this is the different event than initial keydown event.

And the event triggering deletion is the initial keydown event, not the proxied one created by KeystorkeHandler. It seems to be the cause why deletion is performed even though the delete keystroke was canceled.

Hi @f1ames
Thank you for the confirmation. The workaround that you provided is working fine.

Regards
Kapil

How do I get the editor object or model object from the handler function. This is how am registering for callback on execution of a command
this.editor.editing.view.document.on( "delete", this.deleteKeyHandler, {priority: "high"}); deleteKeyHandler(evt, data){ //this.editor is not defined here }
How can I pass scope object to be command rather than document?

@kapilgupta77 That's the different question, but you can simply assign editor.model to a variable and use it inside your locally defined function:

const editorModel = this.editor.model;

function deleteKeyHandler( evt, data ) {
    // editorModel
}

this.editor.editing.view.document.on( 'delete', deleteKeyHandler, { priority: 'high' } );

or use scoping function/method:

this.editor.editing.view.document.on( 'delete', this.deleteKeyHandler(), { priority: 'high' } );

deleteKeyHandler() {
    const editorModel = this.editor.model;
    return ( evt, data ) => { // editorModel }
}

or a bind function which is more elegant:

this.editor.editing.view.document.on( 'delete', this.deleteKeyHandler.bind( this ), { priority: 'high' } );

deleteKeyHandler( evt, data ){
    // this.editor
}

Please remember that if you have separate issues they should be reported in a separate issues/tickets. For questions you can go to our gitter or Stack Overflow.

Hi @f1ames , I have noticed that the workaround that you provided does not work if I do not add following statement in my code

editor.keystrokes.set( "delete", (ev, cancel) => {
            cancel();
        }, { priority : "highest" } );

Without this line the workaround is not working in my plugin.

@kapilgupta77 Thanks for pointing that out.

To prevent deletion without editor.keystrokes.set( ... ) call for collapsed and non-collapsed selection you should use:

editor.editing.view.document.on( 'keydown', ( evt, data ) => {
    if ( data.keyCode == 8 || data.keyCode == 46 ) {
        data.stopPropagation();
        data.preventDefault();
        evt.stop();
    }
}, { priority: 'highest' } );

For technical explanation see below.


Now from the more technical point of view. As mentioned in a comment above, the KeystrokeHandler function is executed correctly and internally it calls:

nativeKeyEvt.preventDefault();
nativeKeyEvt.stopPropagation();

wrappedEvt.stop();

which means apart from stopping internal CKEditor delete event (or rather _keydown:46 which is not the same event), it also prevents native keydown event triggered by delete/backspace. When editor.keystrokes.set( ... ) call is removed, preventing native event needs to be added manually:

editor.editing.view.document.on( 'delete', ( evt, data ) => {
    data.stopPropagation();
    data.preventDefault();
    evt.stop();
}, { priority: 'highest' } );

If native event is not prevented it will cause mutations to be fired which are handled via separate mutation handler still resulting in content removal.

But that's only the part of the story. Since handling DOM modifications is quite complex there is one additional mechanism in a Typing plugin implemented to simplify mutations generated on content removal. Generally, when a key is pressed (and keydown fired) over non-collapsed selection, the selection content is removed. It happens on keydown event so preventing delete event will not stop this from happening.

To prevent removing content with delete/backspace for both collapsed and non-collapsed selection, you should use:

editor.editing.view.document.on( 'keydown', ( evt, data ) => {
    if ( data.keyCode == 8 || data.keyCode == 46 ) {
        data.stopPropagation();
        data.preventDefault();
        evt.stop();
    }
}, { priority: 'highest' } );

I'd add that the keystroke handler is not a tool to handle more complex scenarios. It's a simple keystroke => action mapping utility but for more complex scenarios you'd need to go "lower" to keydown or higher to things like the delete command which you can disable and which would have a similar behaviour. In fact, disabling the command may work even better in the future if the command is used consequently in all cases when some content needs to be deleted (see e.g. https://github.com/ckeditor/ckeditor5-typing/issues/105).

To disable a command you need to do this:

disableCommand( editor.commands.get( 'delete' ) );
disableCommand( editor.commands.get( 'forwardDelete' ) );

function disableCommand( cmd ) {
    cmd.on( 'set:isEnabled', forceDisable, { priority: 'highest' } );
    cmd.isEnabled = false;

    function forceDisable( evt ) {
        evt.return = false;
        evt.stop();
    }
}

Demo: https://jsfiddle.net/7Lrhxp0b/3/

This works pretty fine except non-collapsed selections. I guess that one of the mutation handlers kick in. It should use the delete command but perhaps doesn't (might be https://github.com/ckeditor/ckeditor5-typing/issues/105).

BTW, why do you want to disable the delete/backspace keys?

@Reinmar am disabling backspace/delete key to implement track changes behavior in CKEditor where if tracking is switched on by user, the deletion of text should wrap the deleted text in a HTML del element rather than removing it from the DOM. Similarly any new content added in editor should go inside an ins element.
So probably I cannot simply disable the 'delete' command as I need a callback when delete key is pressed.
In V4 I had developed this behavior by listening to 'keydown' event on the editable and by intercepting the cut/paste commands.

@f1ames your solution is working fine. Thank you

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Reinmar picture Reinmar  ·  3Comments

MCMicS picture MCMicS  ·  3Comments

MansoorJafari picture MansoorJafari  ·  3Comments

oleq picture oleq  ·  3Comments

devaptas picture devaptas  ·  3Comments