2011/03/27

onChange event for CKEditor

It's a somewhat frequent request to see people asking how they can get a notification whenever the content of CKEditor changes.

Some of them are too optimistic:

I've this code <textarea onchange="myfunction()"></textarea> and when I use CKEditor "myfunction" is never called

But most of the people can understand the difference between the original textarea and a CKEditor instance and ask what's the better way to get a notification whenever the content changes, isn't there any built in event for that?

The answer is that no, there's no default event fired whenever something changes, but as I will show here it's quite easy to extend the CKEditor API and generate such event.

Generating a new 'change' event

Although there's no event for "the content has changed" there's something very similar, and that's the Undo system with its saveSnapshot event; whenever something changes it will be called, so we can listen for that event and it will help us greatly with our goal:

       editor.on( 'saveSnapshot', function(e) { somethingChanged(); });

That will take care if the change is something being changed like appying bold or any other style, a new table, pasting, ... it should handle almost everything. But there's one thing that doesn't file a 'saveSnapshot' event, and that's the undo system itself. When Undo or Redo are executed they don't fire that event, so we must listen to them:

        editor.getCommand('undo').on( 'afterUndo', function(e) { somethingChanged(); });
        editor.getCommand('redo').on( 'afterRedo', function(e) { somethingChanged(); });

Ok, Why "something changed"?
Answer: because we are not really sure that something has changed and we aren't really interested to know exactly what has changed, only that something might have changed. Any plugin will fire a "saveSnapshot" before and after any change to work properly, so in our function we will merge all those calls and fire a single event:

        var timer;
        // Avoid firing the event too often
        function somethingChanged()
        {
            if (timer)
                return;

            timer = setTimeout( function() {
                timer = 0;
                editor.fire( 'change' );
            }, editor.config.minimumChangeMilliseconds || 100);
        }

This way the editor will fire a "change" event at most every 100 milliseconds, and you can use editor.checkDirty() to verify if it has really changed (that call might be a little expensive if you are working with big documents, so it's better to avoid calling it too often by using something like the minimum 100 miliseconds that I've added)

Extra checks

To trap every change (and ASAP to avoid delays updating any UI that it's interested in this event I added also a listener for the afterCommandExec (I don't remember now which situation made me add it and I didn't put any comment explaining it :-(  )

        editor.on( 'afterCommandExec', function( event )
        {
            if ( event.data.command.canUndo !== false )
                somethingChanged();
        } );

and also a listener for the keyboard (don't know right now why I didn't listen for the editor.on('key') event; maybe I forgot about it) and new code to handle drag&drop (I've proposed to enhance the Undo system that way in ticket 7422 with some improved code)

        editor.on( 'contentDom', function()
             {
                 editor.document.on( 'keydown', function( event )
                     {
                         // Do not capture CTRL hotkeys.
                         if ( !event.data.$.ctrlKey && !event.data.$.metaKey )
                             somethingChanged();
                     });

                     // Firefox OK
                 editor.document.on( 'drop', function()
                     {
                         somethingChanged();
                     });
                     // IE OK
                 editor.document.getBody().on( 'drop', function()
                     {
                         somethingChanged();
                     });
             });

Ready to use plugin

Here you can download a zip with the full plugin and install instructions onChange event for CKEditor.
Obviously it can be improved, but the important part was to get something working good enough not to win a code contest about the best and most beautiful code.

Note: If you wanna link the plugin from any other site, please link this post so people can get the new versions when they are released instead of linking directly to the zip that will be outdated.

Edit: version 1.1 3rd September 2011

I've fixed an issue with the 'afterUndo' and 'afterRedo' events: they're fired on their respective commands, not on the editor itself; now the Undo and Redo buttons should work correctly. I can't understand how I missed that the first time.

Following the suggestion in the comments, I've added detection for changes in source mode, by keyboard, drag and drop and also on "input" if supported.

Edit: version 1.2 18th September 2011

The new CKEditor 3.6.2 fired the 'saveSnapshot' event too many times, just changing the focus might fire it and generate bogus change events when nothing has changed. Filtered those extra events.

The keyboard listener has been adjusted to ignore movement and other special keyboard events.

Edit: version 1.3 22th December 2011

Avoid firing the event after the editor has been destroyed.

Edit:

I've published a demo showing an usage example.

Edit: version 1.4 7th September 2012

Don't fire events if the editor is readonly, thanks to Ulrich Gabor.
Included code to use Mutation Observers (I'll try to explain it when I have some time).

Edit: version 1.5 20th October 2012

Detect Cut and Paste for IE in source mode thanks to Jacki.

Edit: version 1.6 18th November 2012

Detect multibyte characters thanks to Wouter.

Edit: version 1.7 6th December 2012

Compatibility with Source mode in CKEditor 4.

Note: 15th December 2012

Although the current version doesn't work correctly with CKEditor 4, I don't plan any future update.

Edit: version 1.8 8th June 2013

Use setInterval fix by Roman Minkin.


Notes to self about the Code highlighter plugin used in WriteArea:

  • It doesn't remember the last used language reverting always to Java
  • It doesn't prefill the content with the currently selected text