13

Is there a way to stop an remove operation in model.document.on('change') ?

I listen to change with this code:

model.document.on('change',(eventInfo,batch) => {
// My code here.
}

And it works fine, in so far as I do get and can inspect all changes. But there does not appear to be any way to reject the change.

I tried to call eventInfo.stop() and reset() on the differ. Both of these methods does stop the change, but always later results in a model-nodelist-offset-out-of-bounds: exception if I try to stop a remove operation.

What I am trying to do is to change how text delete works, so when the user delete text, instead of really deleting the text from the editor, I create a marker which marks which text have been "deleted" by the user. (For optional change control).

MTilsted
  • 5,425
  • 9
  • 44
  • 76
  • It looks like you are trying to implement track changes in CKEditor 5. We're going to publish such a plugin soon (next month most likely), feel free to contact us for more details (https://ckeditor.com/contact/) – Wiktor Walc Dec 10 '18 at 15:33
  • Will that plugin be available as a part of the standard ckeditor 5 package? (Or will it require the cloud version?) – MTilsted Dec 10 '18 at 15:36
  • It will be a commercial plugin, but it will not require cloud services to work - it will be possible to use it with a standard CKEditor package (without real-time collaboration, connection to cloud services etc.). The same soon will apply to the comments plugin - it will be possible to use it "offline" (without cloud services). – Wiktor Walc Dec 10 '18 at 15:42

2 Answers2

4

Instead of stopping the change, maybe you could save the data value after a change and "reset" to the previous value when a delete happens:

var oldData = '';
var editor = ClassicEditor
    .create(document.querySelector('#editor'))
    .then(editor => {
        editor.model.document.on('change',(eventInfo, batch) => {
           if(oldData.length > editor.getData().length) {
           // or use eventInfo.source.differ.getChanges() and check for type: "remove"
                editor.setData(oldData);
            }
            oldData = editor.getData();
        });
    })
    .catch( error => {
        console.error( error );
    });

Note instead of checking the data length, you could loop through the changes happened using eventInfo.source.differ.getChanges() and checking if there were changes of type "remove".

Jannes Botis
  • 11,154
  • 3
  • 21
  • 39
  • That might work, but this seems really expensive to do each time the user press the backspace button. I did implement an alternate hack where I create the needed markers, and then issue an undo operation each time the user delete something, to undo the delete. This works surprising well so far, but there must be a better way to do it. I think the information about exactly what is being deleted should be in the data structures someware, but I was unable to find that info and that part of ckeditor is really not documented. – MTilsted Dec 09 '18 at 22:49
4

An operation that was already applied (and the change event is fired after operations are applied) cannot be silently removed. They need to be either reverted (by applying new operations which will revert the state of the model to the previous one) or prevented before they actually get applied.

Reverting operations

The most obvious but extremely poorly performing solution is to use setData(). That will, however, reset the selection and may cause other issues.

A far more optimal solution is to apply reversed operations for each applied operation that you want to revert. Think like in git – you cannot remove a commit (ok, you can, but you'd have to do a force push, so you don't). Instead, you apply a reversed commit.

CKEditor 5's operations allow getting their reversed conterparts. You can then apply those. However, you need to make sure that the state of the model is correct after you do that – this is tricky because when working on such a low level, you lose the normalization logic which is implemented in the model writer.

Another disadvantage of this solution is that you will end up with empty undo steps. Undo steps are defined by batches. If you'll add to a batch operations which revert those which are already in it, that batch will be an empty undo step. Currently, I don't know of a mechanism which would allow removing it from the history.

Therefore, while reverting operations is doable, it's tricky and may not work flawlessly at the moment.

Preventing applying operations

This is a solution that I'd recommend. Instead of fixing an already changed model, make sure that it doesn't change at all.

CKEditor 5 features are implemented in the form of commands. Commands have their isEnabled state. If a command is disabled, it cannot be executed.

In CKEditor 5 even features such as support for typing are implemented in the form of commands (input, delete and forwardDelete). Hence, preventing typing can be achieved by disabling these commands.

Reinmar
  • 21,729
  • 4
  • 67
  • 78
  • @Rainmar I don't know if it changes anything, but it is always the entire batch I need to revert, not each operation. Which is why my current hack solution, just uses the undo operation. I can't disable the commands, because I need to know exactly what the command would have changed/deleted. Also: Many plugins can change the model, so I fear that your solution would result in me having to re-implement half the editor plugins. – MTilsted Jan 04 '19 at 20:57