Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get the text from an Insert event in CKEditor 5?

I am trying to process an insert event from the CKEditor 5.

editor.document.on("change", (eventInfo, type, data) => {
  switch (type) {
    case "insert":
    console.log(type, data);
    break;
  }
});

When typing in the editor the call back is called. The data argument in the event callback looks like approximately like this:

{
  range: {
    start: {
      root: { ... },
      path: [0, 14]
    },
    end: {
      root: { ... },
      path: [0, 15]
    }
  }
}

I don't see a convenient way to figure out what text was actually inserted. I can call data.range.root.getNodeByPath(data.range.start.path); which seems to get me the text node that the text was inserted in. Should we then look at the text node's data field? Should we assume that the last item in the path is always an offset for the start and end of the range and use that to substring? I think the insert event is also fired for inserting non-text type things (e.g. element). How would we know that this is indeed a text type of an event?

Is there something I am missing, or is there just a different way to do this all together?

like image 374
Michael MacFadden Avatar asked Jan 23 '18 02:01

Michael MacFadden


People also ask

What are events in CKEditor?

An event is triggered when we paste some content in to CKEditor or paste from dialog (paste as Word, plain text). An event is triggered when editor content is focus or blur. Maximize and minimize We can catch the editor when maximized or minimized by the event given below.

How do I save data in CKEditor 4?

A dedicated Save plugin for CKEditor 4 is available, too. It provides the button, which fires the save event, but it currently works only for classic editor placed inside the <form> element. The following samples are available for getting and saving data in CKEditor 4:

What is the CKEditor API?

CKEditor is a pure JavaScript component and it does not offer anything more than JavaScript methods and events to access the data so that you could save it on the server. The CKEditor JavaScript API makes it easy to retrieve and control the data.

What's new in CKEditor 4?

Whenever a change is made in the editor, CKEditor 4 fires the change event. This makes additional features like auto-saving really easy to develop. The following example shows how to listen to the change event and print the total number of bytes to the console: A dedicated Save plugin for CKEditor 4 is available, too.


1 Answers

First, let me describe how you would do it currently (Jan 2018). Please, keep in mind that CKEditor 5 is now undergoing a big refactoring and things will change. At the end, I will describe how it will look like after we finish this refactoring. You may skip to the later part if you don't mind waiting some more time for the refactoring to come to an end.

EDIT: The 1.0.0-beta.1 was released on 15th of March, so you can jump to the "Since March 2018" section.

Until March 2018 (up to 1.0.0-alpha.2)

(If you need to learn more about some class API or an event, please check out the docs.)

Your best bet would be simply to iterate through the inserted range.

let data = '';

for ( const child of data.range.getItems() ) {
    if ( child.is( 'textProxy' ) ) {
        data += child.data;
    }
}

Note, that a TextProxy instance is always returned when you iterate through the range, even if the whole Text node is included in the range.

(You can read more about stringifying a range in CKEditor5 & Angular2 - Getting exact position of caret on click inside editor to grab data.)

Keep in mind, that InsertOperation may insert multiple nodes of a different kind. Mostly, these are just singular characters or elements, but more nodes can be provided. That's why there is no additional data.item or similar property in data. There could be data.items but those would just be same as Array.from( data.range.getItems() ).

Doing changes on Document#change

You haven't mentioned what you want to do with this information afterwards. Getting the range's content is easy, but if you'd like to somehow react to these changes and change the model, then you need to be careful. When the change event is fired, there might be already more changes enqueued. For example:

  • more changes can come at once from collaboration service,
  • a different feature might have already reacted to the same change and enqueued its changes which might make the model different.

If you know exactly what set of features you will use, you may just stick with what I proposed. Just remember that any change you do on the model should be done in a Document#enqueueChanges() block (otherwise, it won't be rendered).

If you would like to have this solution bulletproof, you probably would have to do this:

  1. While iterating over data.range children, if you found a TextProxy, create a LiveRange spanning over that node.
  2. Then, in a enqueueChanges() block, iterate through stored LiveRanges and through their children.
  3. Do your logic for each found TextProxy instance.
  4. Remember to destroy() all the LiveRanges afterwards.

As you can see this seems unnecessarily complicated. There are some drawbacks of providing an open and flexible framework, like CKE5, and having in mind all the edge cases is one of them. However it is true, that it could be simpler, that's why we started refactoring in the first place.

Since March 2018 (starting from 1.0.0-beta.1)

The big change coming in 1.0.0-beta.1 will be the introduction of the model.Differ class, revamped events structure and a new API for big part of the model.

First of all, Document#event:change will be fired after all enqueueChange blocks have finished. This means that you won't have to be worried whether another change won't mess up with the change that you are reacting to in your callback.

Also, engine.Document#registerPostFixer() method will be added and you will be able to use it to register callbacks. change event still will be available, but there will be slight differences between change event and registerPostFixer (we will cover them in a guide and docs).

Second, you will have access to a model.Differ instance, which will store a diff between the model state before the first change and the model state at the moment when you want to react to the changes. You will iterate through all diff items and check what exactly and where has changed.

Other than that, a lot of other changes will be conducted in the refactoring and below code snippet will also reflect them. So, in the new world, it will look like this:

editor.document.registerPostFixer( writer => {
    const changes = editor.document.differ.getChanges();

    for ( const entry of changes ) {
        if ( entry.type == 'insert' && entry.name == '$text' ) {
            // Use `writer` to do your logic here.
            // `entry` also contains `length` and `position` properties.
        }
    }
} );

In terms of code, it might be a bit more of it than in the first snippet, but:

  1. The first snippet was incomplete.
  2. There are a lot fewer edge cases to think about in the new approach.
  3. The new approach is easier to grasp - you have all the changes available after they are all done, instead of reacting to a change when other changes are queued and may mess up with the model.

The writer is an object that will be used to do changes on the model (instead of Document#batch API). It will have methods like insertText(), insertElement(), remove(), etc.

You can check model.Differ API and tests already as they are already available on master branch. (The internal code will change, but API will stay as it is.)

like image 123
Szymon Cofalik Avatar answered Sep 18 '22 16:09

Szymon Cofalik