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

51 comments:

Unknown said...

Thank you for this. I've been trying to get this to work for a few days now. Got it working with the plugin in minutes.

Ju said...

Thanks for the plugin! I find the solution for long time! But then may I know what is the "editor" object? i was thinking how to register the "on" event listener...

Alfonso said...

"editor" is the CKEditor instance where you want to set the change listener.

SebaZ said...

How to find or call CKE instance?

Alfonso said...

you can get it as the return value of the call to create it, and it's always a member of the CKEDITOR.instances object

SC said...

afterCommandExec will shoot just by clicking the "view source" button.
I added to event.data.name != 'source' prevent this.

if ( event.data.name != 'source' && event.data.command.canUndo !== false )
somethingChanged();

Chanjal said...

Thank you for theis.
I had to make the below modification in plugin.js to get the undo and redo trigger the event.

if ( event.data.command.canUndo !== false )
somethingChanged();

was changed to

if ( event.data.command.canUndo == false )
somethingChanged();

(The comparison operator was changed from '!==' to '==')

Thanks Again for the plugin...

Kae Verens said...

this wasn't firing in source mode.

I use the jQuery extension, so added this to my CKeditor config.js to get it to fire:

$('textarea.cke_source').live('keyup', function() {
$(this)
.closest('.cke_wrapper')
.parent()
.parent()
.prev()
.ckeditorGet()
.fire('change');
});

Alfonso said...

@SC
I've added something similar, but it still fires an event while going to source as internally a 'saveSnapshot' event is fired.
Just remember that the aim of the plugin is to notify that something might have changed.

@Chanjal
Those changes doesn't seem right, they might cause false positives while using commands that don't change the contents (like the About box). I've fixed instead the way that afterUndo and afterRedo listeners are attached, they belong to the commands, not to the editor.

@Kae Verens
Thanks for the suggestion, I've added detection for keyup, drop and input events for the textarea. That way people won't have to use any third party library.

Unknown said...

I found that a number of keys were triggering a change, including, shift, alt, left-arrow, page up, etc.

This was true in Firefox, Chrome, and IE 8.

I got the behavior I wanted by filtering for these keys by keyCode.

Did I miss something or does this need to be added to the plugin?

Alfonso said...

@Dan K

I didn't perform any filtering, but I agree that it's better. The new version 1.2 includes a check on the keycode, if you find that something is still not working correctly please let me know.

Thanks

Doug said...

I can't get the saveSnapshot event handler to be called.

This produces alerts when I type in the editor:

editorInstance.on('key', function(e)
{
alert('pressed key');
return false;
});

However, this produces no alerts:

editorInstance.on( 'saveSnapshot', function(e) { alert('saveSnapshot');
});

I am using CKEditor 3.6.1
What am I doing wrong?

Alfonso said...

If you use the plugin you just have to listen for the 'change' event.

I've explained all the things that it tries to check, but of course each one is aimed at a different reason for changes.

The saveSnapshot is not fired on each key press, but only when an Undo step is recorded.

Doug said...

Ok, I get it now. Thanks.

Carl Woodhouse said...

Great plugin - just what i needed; expanded it a bit to work with some fo the default dialogs such as "links"

// detect changes made by dialogs
editor.on('dialogHide', function (event)
{
somethingChanged();
});

Alfonso said...

Detecting the hiding of the dialogs to mark that as change events is wrong.

When a dialog changes anything in the content it should generate an undo step and that's detected in the current code.

If a dialog doesn't generate an undo step then that's a bug in the dialog that should be fixed, but the hide event will happen also when the dialog is canceled so there are no changes in the content.

Nick said...

How can I check if the change exists in another file than the plugin.js?

Alfonso said...

Get a reference to the editor object and then set a listener for the 'change' event just like in the example code.

Nick said...

The example code I got is like:

editor.on( 'change', function(e) { console.log(e) });

I need something like:

editor.on( 'change', function(e) {
// function
});

.. in my default jquery file.

I can't get the editor or CKEDITOR in there?

Alfonso said...

CKEDITOR is a global variable so you can use it in anywhere. jQuery isn't something magical, it's just a javascript library

And of course the "editor" variable will exist only if you store it in a global scope after creating the editor instance, but you can store it in any other way that you like or get it at the moment from CKEDITOR or with the jQuery adapter.

Nick said...

Great, now I get it! Thanks for the fast response and the plugin!

NEXUS/QM GmbH - Alexander Reifinger said...
This comment has been removed by the author.
NEXUS/QM GmbH - Alexander Reifinger said...

Thanks for the plugin that saved me a lot of work. I don't get it why there even is a need for that plugin, the bug in the CKEditor issue tracker to include such an event is 5 years old and still unresolved :-(.

Anyway, my point is that drag and drop is not causing a change call, is there any way to include this?

Alfonso said...

I wouldn't expect that the default CKEditor provides this kind of event because it forces extra checks and it's too easy to be used wrongly and have a poorer performance.

With regards to your drag&drop problems, I guess that you're using a webkit browser because the plugin already includes code to handle it in Firefox and IE.

You're lucky because just today they have fixed the problem that I reported so at some point in the future you'll get a new version with this problem fixed.

As that bug (https://bugs.webkit.org/show_bug.cgi?id=57185) shows, and I've stated several times, if you want some bug to be fixed in webkit it doesn't matter if it's useful, it's available in any other browser or anything like that. The only thing that matters for a webkit bug is if it's available in a public test like happened with the Acid3 or in this bug has happened with the Microsoft tests.

During 8 months nobody cared about the ticket, but now just in 8 days it has been fixed because it did take away points in a Microsoft test.

NEXUS/QM GmbH - Alexander Reifinger said...

Thank you, do you have a little more specific time frame for "at some point in the future" :-)

Alfonso said...

I'm sorry but I don't follow the release schedule so closely.
I guess that maybe in 3 months it's available for Chrome users and a new version of Safari might be released around June, but I'm just guessing. If you check the release schedule of whatever browser you're using you might get a better idea about how often they push releases with the new features of webkit.

JLogan said...

Okay, I am late to the game here, but am using CKEditor in my ASP.Net and mixed ASP environment. My JS skills are weak, and while I got it all working including a file manager and custom plugins, I can't seem to test for when the editor had any changes. I put in your plugin but have no idea how to say something like "if CKEDITOR.Is_Dirty" or other test on the client side. Any ideas?

Alfonso said...

CKEDITOR is a reference to a global object that it's used as entry point for some methods like creation of instances.

You want to get a reference to your instance (as the object returned on creation, or getting it from the CKEDITOR.instances object) and then you can use the checkDirty method to see if the content has been modified.

This plugin generates a stream of events as the content is modified, but your description seems to point that you should use just that method.

JLogan said...

Thank you so much for your fast reply. Unfortunately the checkDirty ends up "undefined" even though the ckeditor object is defined. I should mention that I am using ASP.NET and the editor is on an ascx control on an aspx page. I have other successful plugins working here, but it seems like the onchange plugin is not loaded perhaps. Using CKEDITOR.on(....) does not get called somehow, and certainly my C# aspx pages have no clue with OnLoad event what that is. So I call checkDirty and it is undefined. How can I ensure that the onchange plugin is loaded?

Alfonso said...

checkDirty is a method of every CKEditor instance.
It's not related to this plugin and if you can't use it that means that you aren't using the correct reference to the editor instance.
Keep in mind that I'm always talking about javascript, if you try to do any of this with C# at the server then it will just not work. Maybe the control that you are using has some server property or method, but that's not related to this plugin.

JLogan said...

Thanks again. Yes, most of what I am doing is a mix of javascript and aspx C# pages. I agree that I am not using the "right" instance of the CKEditor when I call checkDirty because the error says that the method does not exist. I have other things to tackle that are going well, so I will have to leave this issue for a bit and revisit it later. It is on the client side - obviously - that I must check if the editor is used.

Krishna Karthik said...

Alfonso, Thank you.Now I am able to get the listeners registered on undo and redo events.

Jacki said...

Thanks so much for this plugin. We're using it and just updated to the latest version. I have already manually updated the file on my end, but it would be great if you could add 'cut' and 'paste' event listeners so that we can pick up changes from the CTRL-X, CTRL-V and context menu cut/paste in Source mode in IE. This is especially useful for doing any kind of character counts.

Alfonso said...

@Jacki
If you share your current code I'll add it much sooner as I won't have to think too much about what to test and if I'm missing other cases.

Jacki said...

Sure, in the last method that detects changes in source mode, I added this after the drop and input event listeners:

if (CKEDITOR.env.ie) {
editor.textarea.on( 'cut', somethingChanged);
editor.textarea.on( 'paste', somethingChanged);
}

Alfonso said...

Thanks, I've updated the plugin.

Unknown said...

Alfonso, thanks for this plugin, it helped me with the character/word counter I added in my application.

One problem with the plugin though, is that it does not detect multibyte characters. My application also supports Japanese and Chinese, and when entering in these languages, the change event was not fired, until when I hit backspace or Enter or something similar.

So I have looked in your code, and found out that the keycode I get is 229. According to http://stackoverflow.com/questions/875374/how-to-detect-multi-byte-characters-inputting-end-by-javascript, this is the keycode for multibyte characters, so I added it in your filter in the editor.document.on('keydown') handler. Hope to see this change back in your updates.

Alfonso said...

As I said to Jacki, if you post your change I can just copy it and publish a new version right away.

Otherways I have to understand better the problem to check that I can reproduce it and fix it according to your description.

Also, does this happens with all the browsers? I hoped that the events used for new ones should be able to catch all the unexpected cases.

Unknown said...

I don't see in the code which version I am using at the moment, but it is on line 114:

var keyCode = event.data.$.keyCode;
// Filter movement keys and related
if (keyCode==8 || keyCode == 13 || keyCode == 32 || ( keyCode >= 46 && keyCode <= 90) || ( keyCode >= 96 && keyCode <= 111) || ( keyCode >= 186 && keyCode <= 222) || keyCode == 229)
somethingChanged();


I only added the || keyCode == 229 part at the end of the if condition.

The keycode is sent out by the Input Method software, so I think it is not browser-related, and this fix should work for all browsers.

Giammin said...

I think I've found a bug.

if there are more than 1 ckeditor in inlinemode editing the change event is raised for all of them

Alfonso said...

Thanks, I didn't test v4 too much (it's a little depressing to look at that toolbar full of gray).

It seems that I have fixed it, but this time I'll try to test it more thoroughly before releasing a new version, so you'll have to wait a little.

Unknown said...

Is there some way to specify multiple handlers and each with different delay timing? I have a use case in which I have to handle the data immediately but in some cases I'd like to defer the handling of the data.

Appreciate if you can show us some ropes.

Thanks!

Unknown said...

Calvin, you can use setTimeout to defer the handling.

Immediate action:
editor.on('change', function(e) {
// do something immediately
});

Deferred action:
editor.on('change', function(e) {
setTimeout(function() {
// do something after 5 seconds
}, 5000);
});

The 5000 parameter is 5 seconds delay. You can change that to any other value in milliseconds.

Giammin said...

I resolved that bug using checkDirty method:

editor.on('change', function (e) {
if (editor.checkDirty()) {
//do something
} });

Unknown said...

Thank you very much for thus usefull plugin. It saved my project (and my ass...)

Taco

Corinna said...

Just in case anyone is trying to use this with CKEditor 4, note that there is a problem with the way the keydown event is handled. Whenever you call setData, the iframe's document is destroyed and recreated, and so the handler is lost. I fixed the problem by replacing the editor.document.on('keydown') listener with editor.on('key') and removing the lines within the handler referring to event.data.$.

See See http://blog-rat.blogspot.com/2011/06/ckeditor-keyup-event.html for more details for more details.

Alfonso said...

The keydown listener is assigned inside the 'contentDom' event, that it's fired whenever the iframe is recreated by switching to design. (at least it used to, if now it doesn't work in CKEditor 4 then I would say that it's a bug on their part as many other plugins will fail)

Corinna said...

Alfonso, it still works that way. The problem is, it's not the iframe that's being recreated, it's the document within the iframe. When that happens, all the event handlers attached to the document are lost. This happens every time you call editor.setData(). The event handlers are restored by switching the mode to source and back to wysiwyg, but obviously you don't want to have to do that every time you set the value of the editor with javascript. This is (apparently) easily solved by adding the handler to the editor rather than the document, but for some reason the event has to be "key" rather than "keydown." Again, see the blog I linked, it goes into more detail.

Alfonso said...

That blog doesn't really explain much. He just says that he tried to attach a keydown listener and that was the only way that he found to make it work. You can bet that he never heard about the "contentDom" event fired by the editor.

If now it turns out that calling setData can remove the listeners on the document and "contentDom" doesn't work then as I said this breaks much more than the simple keystroke handler (just look a few lines below and you'll see that drop events are detected in the same way, but so far CKEditor doesn't fire any event when something is dragged into it, so using listeners on the document is the only way to make them work)

Corinna said...

Ok, well color me super confused. After some more experimentation, trying to re-attach the key event on the "afterSetData" and "dataReady" events and having no success, I tested the drop events. It seems that, after calling setData, the drop events still work correctly, but the keydown event is gone and re-attaching it doesn't appear to work - though the keyup event can be re-attached successfully. I have no idea why it behaves this way. However, it does mean that the solution I mentioned about (changing it to editor.on('key')) is actually sufficient.

Maybe you can shed some light on the situation and come up with a better solution... but for now what I have appears to work.

Also, thanks for the plugin, it's been a huge lifesaver.

Alfonso said...

I've filed http://dev.ckeditor.com/ticket/10365 to report the bug with CKEditor 4