2009/11/29

CKEditor is Fast!

Yep, CKEditor is fast and this is not just a marketing slogan, but a fact. I'm stating this as the reply to this question:

I want to fire some event every time I replace <div id="editor"><div> element pressing "Create Editor" button:

  editor = CKEDITOR.appendTo('editor');
  editor.on('pluginsLoaded', function(ev) {
    …
  });


Everything is OK on first replacement of div. But when I destroy editor with "Remove Editor" button:

  editor.destroy();

and create it once again, event function is not executed.

The problem here is that the second time the CKEDITOR.appendTo is called, all the files (core, plugin, configuration, ...) are already loaded in memory, so the creation process is almost synchronous; as soon as the call is finished, the instance is almost ready and the only event you can attach the second time is the "instanceReady", the rest of events already have been executed by that time. (and that instanceReady is fired later due to the delay loading the iframe for editing and its initialization, maybe if the editor is configured to start in source mode that event is also fired synchronously. Correction, no. Starting with source mode still fires the instanceReady so it isn't related to iframes initialization, but other asynchronous task).

So we can't attach the event using editor.on() because as I said this is too late, the events have already been fired the second time. In this situation you can opt to use maybe the instance "instanceReady" because as I said this seems to be fired always. But we are playing a game with some risk: if the initalization is further optimized in the future avoiding those little milliseconds of delay that event will be fired as well before we get a change to listen to them.

Honestly I didn't speak about the events of a CKEditor instance, and this might be a little hidden topic as the official documentation doesn't explain it for the moment, but there are two ways to listen for the events of an instance.

The first method is what you can read above and everybody is used to in other environments and frameworks instance.on( eventName, callback ). But by the time we get a hold of instance the event has already been fired. Despite that we could use another workaround using the CKEDITOR.instanceCreated event. We can set a listener for that event and due to being assigned to an object that always exists, it will be always fired whenever an instance is created. So this means that any listener that we set on and editor instance inside a CKEDITOR.instanceCreated listener should then be fired even if the process of creation is synchronous. So this is a more promising solution because no matter how fast the computer, browser or CKEditor is optimized, as long as the event remains there we can use it to hook into any new instance and set our listeners.

But the second method is more appropiate for this situation, and it's based on the ability of creating events listener at creation time. It's based on this code from editor.js

                    // Register the events that may have been set at the instance
                    // configuration object.
                    if ( instanceConfig.on )
                    {
                        for ( var eventName in instanceConfig.on )
                        {
                            editor.on( eventName, instanceConfig.on[ eventName ] );
                        }
                    }

So it states that if we pass an "on" object in the configuration object at creation time, the events defined there will be assigned to our editor. And you can indeed try it:

    editor = CKEDITOR.appendTo( 'editor' , {  on:{'pluginsLoaded':function(e) { console.log('instance config::' + e.name)} } });

That event will be fired no matter if it's the first, second or Nth time that the editor is created. You have to be very careful about the syntax as it's easy to make a little mistake with some extra parenthesis or a missing bracket but the error console should warn you in that case.

6 comments:

sebbie said...

Brilliant tip although worth to explicitly mention that second time "pluginsLoaded" is loaded before editor is initialized (so for example any global variable holding editor instance is not available). Just use "this" inside method to access editor reliably.

Alfonso said...

Yes, obviously the event is fired before the assignment to the global "editor" variable is made in the example, so you can't use that inside the code.

But instead of "this", I would use the editor passed as part of the event data:

editor = CKEDITOR.appendTo( 'editor' , {
on:{'pluginsLoaded': function(e) { console.log("event " + e.name + ' fired for the editor ' + e.editor.name)} } });

That way seems far more generic and easier to me. You can use as the listener the method of another object, you don't have to care about global variables, the data is there for you to use it!

ptoly said...

When I try this snippit:
editor = CKEDITOR.appendTo( 'editor' ,
{
on:
{
'pluginsLoaded': function(e)
{
console.log("event " + e.name + ' fired for the editor ' + e.editor.name)
}
}
});

I get this in FireBug:
uncaught exception: [CKEDITOR.editor.appendTo] The element with id "null" was not found.

Alfonso said...

Does your page work with just
editor = CKEDITOR.appendTo( 'editor')

If it fails, then you have a more basic problem (and it seems that the exception code doesn't provide the correct data)

ptoly said...

Ah, it was obvious and I should have noticed it right off myself.

The editor instance was 'editor1' not simply 'editor'.

Apologies for my newbie blunder.

Great tips and articles by the way.

Alfonso said...

The incorrect error message has been fixed: ticket 4764

Thanks to Garry and Fred for the quick response.