2009/09/08

CKEditor events

The new articles will be focused on documenting the events fired by CKEditor as currently there's no documentation besides the source code.

Events are the way to hook different pieces, the glue for the plugin architecture of CKEditor that allows to replace / add several parts without going nuts. In this regard, this doc page is a must read for anyone that really wants to understand the basics about how it works. If you don't read it then you'll be lost and you'll ask questions that have already been answered there.

Yes, that page is quite generic, so we have to looks at the API docs for the CKEDITOR.event information, here we can find out exactly the details about how those ideas have been implemented.

As a summary: an object might implement this Api, so you can listen to their events adding your listener. The syntax is similar to the standard DOM events, but it has some slight improvements like the ability to specify an object upon which the listener must be called without resorting to extra code and specify a priority for the listener (you might need this very few times, but it's nice that it's there).

As any other event system you can remove your listener, and if you need to, you can fire an event whenever you like, on an standard object, your own object, some standard event or your own event.

Something important must be remarked at this point: If you try to register a listener to a non-existing event you won't get any error, but of course it will never be executed. So don't try to put ckeditor.on('imageDeleted') ckeditor.on('fileAdded') or whatever. Unless you write the code to fire those events you are wasting your time. Why does the event system allows to do that: because you can create any event that you want, so the system just register your listeners, expecting that you know what you are doing and the event will be fired later by any other code.

So now we now the basic, let's look at the events of one object

CKEDITOR events

CKEDITOR is the main object of the whole API, and it can fire several events that are useful to do some basic and generic tasks

"loaded"
The first event is fired only if you have used the basic integration, obviously if you use the full code from the start as soon as your code is executed the core is ready (or you wouldn't be able to call CKEDITOR.on() ). Also, there's no editor sent in the eventInfo object.

"instanceCreated"
Each time an instance is created this event is fired, before it's configuration is initialized.

"instanceReady"
It signals that the initialization of an instance has finished.

"currentInstance"
Fired when an editor gets or loses focus. (After CKEDITOR.currentInstance has been updated. This is currently an undocumented property used only for the focus management ). No editor is sent in the eventInfo object. This is fired quite a lot, for example opening a dialog means that the editor loses focus, so the event is fired, and also when it's closed.

"dialogDefinition"
Fired upon creating the definition for a dialog. The event data are the dialog name and its definition. (this is the only event in CKEDITOR that sends some data in the eventInfo.data field). This event is fired when a dialog is launched for the first time in each CKEditor instance.

So that's all for the main CKEDITOR object. It's a short list, but enough to show a bug in my previous code to provide the seamless integration of textareas in CKEDITOR: Yes the CKEDITOR object doesn't fire any event when an instance is destroyed so that part of the code must be written like this:

// Hook each textarea with its editor
CKEDITOR.on('instanceCreated', function(e) {
 if (e.editor.element.getName()=="textarea")
 {
  var node = e.editor.element.$ ;

  // If the .nativeValue hasn't been set for the textarea try to do it now
  if (typeof(node.nativeValue) == "undefined")
  {
   // IE
   if (!document.__defineGetter__) return;
   // for Opera. Safari will fail
   if (!DefineNativeValue(node)) return;
  }

  node.editor = e.editor;

  // House keeping.
  e.editor.on('destroy', function(e) {
   if (node.editor)
    delete node.editor;
  });
 }
});

This just shows again that you must not trust anyone, test whatever you find and use it as a guideline, but always be ready to think that the person that wrote some example might not have tested under your same conditions, that some code has been changed meanwhile (for example I guess that the list of events of CKEDITOR might have increased in v3.2 for example, so if you read this article 2 years from now just think that I'm talking about the 3.0 version), or the code might be wrong because whoever wrote it was too drunk that day :-)

 

2009/09/06

Hooking textarea.value with CKEditor, part 2

Edit: this version is now obsolete, go here for the latest version.

Today I wanted to finish some tests and study better the problems that I found while trying to hook textarea.value with the contents of CKEditor.

As I thought the problem with Opera is just related to the use of the HTMLTextAreaElement.prototype, if the code is modified to work on each textarea then it works, so that only leaves out Safari&Chrome users as well as old IEs.

There are two bugs for Webkit, but it's impossible to know about any estimated about when they might get fixed: getter and setter doesn't work on native properties and lookupGetter and lookupSetter doesn't work in native properties.

Opera has several internal bugs tracking several issues with that code, but as Opera 10 has just been released I think that it might take a while to see a new version with the ability to work on the textarea prototype, but fortunately the workaround works for this code.

As I mentioned, there was also a problem in IE if you try to reuse a native property descriptor with another name. I've filed Object.defineProperty can't reuse directly the object returned by getOwnPropertyDescriptor but it can be either marked as invalid or remain there for quite a lot of time as there's no indication about when a new version of IE might be released.

So the new code that works in IE8 + FF3.5 + Op10 is this one: There's also a change to the event handling to fix a problem described in the events post.

// Modify the default methods in CKEDITOR.dom.element to use .nativeValue if it's available
CKEDITOR.dom.element.prototype.getValue = function()
{
    if (typeof(this.$.nativeValue) == "undefined")
        return this.$.value;

    return this.$.nativeValue;
}

CKEDITOR.dom.element.prototype.setValue = function( value )
{
    if (typeof(this.$.nativeValue) == "undefined")
        this.$.value = value;
    else
        this.$.nativeValue = value;

    return this;
}

// Hook each textarea with its editor
CKEDITOR.on('instanceCreated', function(e) {
 if (e.editor.element.getName()=="textarea")
 {
  var node = e.editor.element.$ ;

  // If the .nativeValue hasn't been set for the textarea try to do it now
  if (typeof(node.nativeValue) == "undefined")
  {
   // IE
   if (!document.__defineGetter__) return;
   // for Opera. Safari will fail
   if (!DefineNativeValue(node)) return;
  }

  node.editor = e.editor;

  // House keeping.
  e.editor.on('destroy', function(e) {
   if (node.editor)
    delete node.editor;
  });
 }
});


// This function alters the behavior of the .value property to work with CKEditor
// It also provides a new property .nativeValue that reflects the original .value
// It can be used with HTMLTextAreaElement.prototype for Firefox, but Opera needs to call it on a textarea instance
function DefineNativeValue(node)
{
    var originalGetter = node.__lookupGetter__("value");
    var originalSetter = node.__lookupSetter__("value");
    if (originalGetter && originalSetter)
    {
        node.__defineGetter__("value", function() {
                // if there's an editor, return its value
                if (this.editor) 
                    return this.editor.getData();
                // else return the native value
                return originalGetter.call(this);
                } 
            );
        node.__defineSetter__("value", function(data) { 
                // If there's an editor, set its value
                if (this.editor) this.editor.setData(data); 
                // always set the native value
                originalSetter.call(this, data)
                } 
            );

        node.__defineGetter__("nativeValue", function() {
                return originalGetter.call(this);
                } 
            );
        node.__defineSetter__("nativeValue", function(data) { 
                originalSetter.call(this, data)
                } 
            );
        return true
    }
    return false;
}

// Wrap this in an anonymous function
(function() {
if (Object.defineProperty)
{
    // IE 8  OK
    var originalValuepropDesc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
    Object.defineProperty(HTMLTextAreaElement.prototype, "nativeValue",
            {    
                get: function() { 
                    return originalValuepropDesc.get.call(this);
                },
                set: function(data) { 
                    originalValuepropDesc.set.call(this, data);
                } 
            } 
        );

    Object.defineProperty(HTMLTextAreaElement.prototype, "value",
            {    
                get: function() { 
                    // if there's an editor, return its value
                    if (this.editor) 
                        return this.editor.getData();
                    // else return the native value
                    return originalValuepropDesc.get.call(this);
                },
                set: function(data) { 
                    // If there's an editor, set its value
                    if (this.editor) this.editor.setData(data); 
                    // always set the native value
                    originalValuepropDesc.set.call(this, data);
                } 
            } 
        );
} 
    else if (document.__defineGetter__) 
{
    // FF 3.5 OK
    // Opera 10 fails for HTMLTextAreaElement.prototype, but it will work for each instance
    // Webkit fails: https://bugs.webkit.org/show_bug.cgi?id=12721
    if (!DefineNativeValue(HTMLTextAreaElement.prototype))
    {
        // We try to get the innerHTML getter for the body, if it works then getting the value for each textarea will work
        if (!document.body.__lookupGetter__("innerHTML"))
            alert("Safari and Chrome don't allow to read the originalGetter and Setter for the textarea value");
    }
} 
    else
{
    // detect IE8 in compatibility mode...
    if (document.documentMode)
        alert("The page is running in Compatibility Mode (" + document.documentMode + "). Fix that")
    else
        alert("Your version of IE is too old"); 
}
})();

Edit: this version is now obsolete, go here for the latest version.

2009/09/05

Seamless replacement of textareas with CKEditor

Edit: this version is now obsolete, go here for the latest version.

Everynow and then someone always pops up asking why they don't get the current contents of the editor if the read the value of the textarea. The answer is obvious: FCKeditor/CKEditor is a different object than the textarea so they must work with the editor, not with the textarea, but this might be more subtle when they are using some javascript framework to read all the form elements on form submit. In that situation the editor updates the value of its associated textarea using an event handler, it might run later than the read of the values and so the value that they get is the initial one.

What about making all of that automatic?

First step: textarea.value

In order to make it work we would need to use our own textarea.value property. In Firefox it's possible to do that since long ago, and IE 8 did bring this feature for us.

This would be the code to make it work for IE:

 var originalValuepropDesc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
Object.defineProperty(HTMLTextAreaElement.prototype, "value",
  {   
   get: function() {
    // if there's an editor, return its value
    if (this.editor)
     return this.editor.getData();
    // else return the native value
    return originalValuepropDesc.get.call(this);
   },
   set: function(data) {
    // If there's an editor, set its value
    if (this.editor) this.editor.setData(data);
    // always set the native value
    originalValuepropDesc.set.call(this, data);
   }
  }
 );

With this code we are modifying all the textareas in the page, if they have an .editor property associated, it will call it to read/write the data; if not, it will use the default system.

Believe it or not, that code is for IE, and it uses new ECMAScript accesor properties that so far no other browser has implemented, they have the first implementation and we should hope that the rest of the browsers write their parts when it's tested in Acid 4 or whatever.

For Firefox we can use this code:

    var originalGetter = HTMLTextAreaElement.prototype.__lookupGetter__("value");
    var originalSetter = HTMLTextAreaElement.prototype.__lookupSetter__("value");
        HTMLTextAreaElement.prototype.__defineGetter__("value", function() {
                // if there's an editor, return its value
                if (this.editor)
                    return this.editor.getData();
                // else return the native value
                return originalGetter.call(this);
                }
            );
        HTMLTextAreaElement.prototype.__defineSetter__("value", function(data) {
                // If there's an editor, set its value
                if (this.editor) this.editor.setData(data);
                // always set the native value
                originalSetter.call(this, data)
                }
            );

As you can see the idea is just the same, but the way to make it work is just different. It's a little nicer to use a single object as the property accessor, but it's not a big issue.

But for other browsers this fails. They have implemented the __defineGetter__ syntax, but Webkit (Safari & Chrome) doesn't allow to use getter/setters with native DOM properties, and in my first tests Opera seemed to work (at least partially) but failed with the final code. I'll try to review the Opera problem, I think that it might work to alter the properties of a DOM object, but not on the prototype. You  should read the follow-up post to get it working in Opera.

So we have the code for IE8 and Firefox that takes care to intercept the usage of any textarea.value, and is just linked to setting a property "editor" on that textarea DOM node. It could have been written in other ways, for example checking in CKEditor.instances for an instance matching the id or name of the textarea, but I just used this way.

Next step: avoid problems

Before writing the rest of the code, we need to think a little: when the form is submitted, or in general, whenever editor.updateElement() is called, it will set the textarea.value, but if we have it hooked to automatically update the editor we are wasting time, and what's worse, any selection and cursor position in the editor will be lost.

So it might be better to do a little modifications: besides altering textarea.value we can create a new property textarea.nativeValue that doesn't try to do anything fancy, it will call the original getter/setter without any relationship to CKEditor and we will be safe.

You might think that for IE you would need just to write a one line:

 Object.defineProperty(HTMLTextAreaElement.prototype, "nativeValue", originalValuepropDesc);

but that won't work, it just raises an error.
And even trying to use

   Object.defineProperty(HTMLTextAreaElement.prototype, "nativeValue", 
         {get:originalValuepropDesc.get, set:originalValuepropDesc.set});

in case that the object did include some special property that didn't allowed it to be reused also fails. So in the end the code is just like the one for the "value" property.

And then we do a little modification to the CKEditor routines so it tries to use that new property if it exist:

CKEDITOR.dom.element.prototype.getValue = function()
{
    if (typeof(this.$.nativeValue) == "undefined")
        return this.$.value;
    return this.$.nativeValue;
}
CKEDITOR.dom.element.prototype.setValue = function( value )
{
    if (typeof(this.$.nativeValue) == "undefined")
        this.$.value = value;
    else
        this.$.nativeValue = value;
    return this;
}

 Joining the pieces

So we got every textarea on the page with our new .nativeValue property and CKEditor is modified to use it instead of the original .value; what we need to finish this magic is to set the .editor property on the replaced textareas so they know which editor they must use. And that's easy:

CKEDITOR.on('instanceCreated', function(e) {
    if (e.editor.element.getName()=="textarea")
        e.editor.element.$.editor = e.editor;
});

of course we need to do some cleanup whenever the editor is destroyed:

CKEDITOR.on('destroy', function(e) {
    if (e.editor.element.$.editor)
        delete e.editor.element.$.editor;
});

And that's all that you need.!

Now whenever any javascript tries to read the .value of a textarea that is linked to a CKEditor instance you'll get the current value of CKEditor, and if you write that .value then it will set that to CKEditor at the same time, no need to change anything in the rest of your code!!!

All the code

You just need to put this code in the page after the CKEditor script and the magic is done. Using the automatic replacement described in the first article about CKEditor you can use it without any change to your code, no matter how it does work as long as you restrict your users to Firefox 3.5 or IE8. Edit: look at the follow-up post for the code to include Opera in the supported browsers.

CKEDITOR.dom.element.prototype.getValue = function()
{
    if (typeof(this.$.nativeValue) == "undefined")
        return this.$.value;

    return this.$.nativeValue;
}

CKEDITOR.dom.element.prototype.setValue = function( value )
{
    if (typeof(this.$.nativeValue) == "undefined")
        this.$.value = value;
    else
        this.$.nativeValue = value;

    return this;
}

// Hook each textarea with its editor
CKEDITOR.on('instanceCreated', function(e) {
    if (e.editor.element.getName()=="textarea")
        e.editor.element.$.editor = e.editor;
});

// House keeping.
CKEDITOR.on('destroy', function(e) {
    if (e.editor.element.$.editor)
        delete e.editor.element.$.editor;
});

// Wrap this in an anonymous function
(function() {
if (Object.defineProperty)
{
    // IE 8  OK
    var originalValuepropDesc = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value");
    Object.defineProperty(HTMLTextAreaElement.prototype, "nativeValue",
            {   
                get: function() {
                    return originalValuepropDesc.get.call(this);
                },
                set: function(data) {
                    originalValuepropDesc.set.call(this, data);
                }
            }
        );

    Object.defineProperty(HTMLTextAreaElement.prototype, "value",
            {   
                get: function() {
                    // if there's an editor, return its value
                    if (this.editor)
                        return this.editor.getData();
                    // else return the native value
                    return originalValuepropDesc.get.call(this);
                },
                set: function(data) {
                    // If there's an editor, set its value
                    if (this.editor) this.editor.setData(data);
                    // always set the native value
                    originalValuepropDesc.set.call(this, data);
                }
            }
        );
}
    else if (document.__defineGetter__)
{
     // FF 3.5 OK
     // Opera 10 fails
     // Webkit fails: https://bugs.webkit.org/show_bug.cgi?id=12721
    var originalGetter = HTMLTextAreaElement.prototype.__lookupGetter__("value");
    var originalSetter = HTMLTextAreaElement.prototype.__lookupSetter__("value");
    if (originalGetter && originalSetter)
    {
        HTMLTextAreaElement.prototype.__defineGetter__("value", function() {
                // if there's an editor, return its value
                if (this.editor)
                    return this.editor.getData();
                // else return the native value
                return originalGetter.call(this);
                }
            );
        HTMLTextAreaElement.prototype.__defineSetter__("value", function(data) {
                // If there's an editor, set its value
                if (this.editor) this.editor.setData(data);
                // always set the native value
                originalSetter.call(this, data)
                }
            );

        HTMLTextAreaElement.prototype.__defineGetter__("nativeValue", function() {
                return originalGetter.call(this);
                }
            );
        HTMLTextAreaElement.prototype.__defineSetter__("nativeValue", function(data) {
                originalSetter.call(this, data)
                }
            );
    }
        else
            alert("Opera and Safari doesn't allow to read the originalGetter and Setter for the textarea value");
}
    else
{
    // detect IE8 in compatibility mode...
    if (document.documentMode)
        alert("The page is running in Compatibility Mode (" + document.documentMode + "). Fix that")
    else
        alert("Your version of IE is too old");
}
})();

 So, what do you think?

 :-)

Edit: this version is now obsolete, go here for the latest version.

2009/09/04

Creating CKEditor instances with Javascript

This should be the final article about creating instances of CKEditor. The first one talked about the simple conversion of textareas and the second one about the new delayed loading feature.

This post will focus on how to create the editor in a more dynamical way (let's call that AJAX).

Replace or Append

One aspect that I didn't mention in the introduction to it, is that the replace function isn't restricted to textareas, as you can see in the examples, you can pass to it a DOM element, or an Id to be used with document.getElementById or a Name that matches a textarea in the page. So the first two options (DOM and Id) allow to use the editor with any kind of element (usually a div), not just textareas. A basic example is the divReplace.html sample (keep in mind that this is just a proof of concept, you will need to code quite a lot more in order to create a CMS with these features). The element being replaced isn't removed from the DOM, it just hidden with style="display:none; visibility:hidden" and the instance is populated with the innerHTML of that element.

Due to the usage of innerHTML and the way that it behaves in different browsers (specially IE) you might find that this isn't too useful, specially if you include any script in the source or any code that is processed at the server.

The appendTo function is the second option to dynamically create instances, and it also accepts as the first parameter a DOM element or an Id, but as the new editor will be created inside that element, it doesn't accept a name, and of course the element that you want to use as the parent must be a valid one (using it on a textarea is wrong and it won't work). An example to use this function is the ajax.html file.

Usually you will use appendTo with a newly created container, so it's empty, but just in case of doubt: appendTo obviously appends the CKEditor element as the last child of the container.

Notes about the creation

Whatever reference element that it's being used, it will be stored in editor.element (editor.element is a wrapper for the native DOM object that can be accessed in editor.element.$). If you are replacing a div or other non-form element, the Save button in the toolbar will be disabled. This should be obvious: its behavior is to update the reference element and then submit that form, if it can't submit the form then it can't save the data. You have two options in these situations: either remove the save button from your toolbar configuration, or create a mini-plugin that does take care to process the data in the way that you need.

The newly created instance will be named after the Id or Name of the replaced element, and if it doesn't have neither of those attributes or it isn't replacing an element, it will use an automatic counter. You can't use the .name property to rename an instance: the property will reflect the new value but it will remain with the old name in CKEDITOR.instances.

Both functions return the CKEditor object that has been created so you can keep working on it, but if you are using the fckeditor_basic.js integration, it might not be full featured until the full core is loaded and then it's really created. You can find this code in the ajax.html example to learn how to deal with this situation:

if ( editor.setData )
    editor.setData( html );
else
    CKEDITOR.on( 'loaded', function()
        {
            editor.setData( html );
        });

End of chapter

So I think that this covers all the current options related to creation of the instances. Of course, you could use directly the ckeditor constructor, but unless you are absolutely sure that you know what you are doing, you should avoid it.

In future releases you will have the option to use different plugins to some popular Javascript frameworks (the one in the works is for jQuery) so you can use CKEditor in a "native" way in that framework.

2009/09/01

Delayed loading of CKEditor

As I mentioned in the post explaining the way to create an CKEditor instance, now instead of including the normal ckeditor.js file, you can use ckeditor_basic.js file, a bootstrap that contains just the core functionality so the rest of the code can be loaded at a later time, avoiding delaying the initial load of the page.

You just need to replace

<head>
 ...
 <script type="text/javascript" src="/ckeditor/ckeditor.js"></script>
</head>

with

<head>
 ...
 <script type="text/javascript" src="/ckeditor/ckeditor_basic.js"></script>
</head>

By default (you could alter the packaged contents to remove/add plugins), ckeditor.js weights 260KiB, but ckeditor_basic.js is only 7KiB, so the advantage is that if you don't need to create an instance of CKEditor right from the start you can save those 260KiB in the initial load (of course you should have http compression enabled on the server, so the download size is smaller, but let's focus on the raw numbers to ease understand it)

When is loaded the rest of ckeditor?

At any time you can call CKEDITOR.loadFullCore() to get the full functionality ready. Before the page is loaded, you can set in CKEDITOR.loadFullCoreTimeout the number of seconds to wait before the core is automatically loaded, the default value is 0, meaning that it won't be loaded automatically.

If you try to create an instance of CKEditor before the Full Core has been loaded, then it will be loaded at that time, so the user will have to wait a little to get the full core and then the editor will be created.

If you are using the automatic replacement of textareas with class ckeditor (or defined with replaceClass), then if no matching textareas are found the core won't be loaded. The same thing happens if you use replaceAll.

How to use this info:

If you know that the page will always use some instance of CKEditor right from the start, then just forget about this, it won't help you, it's an extra http request and 7KiB downloaded that are wasted.

If you expect that the full functionality of CKEditor won't be used most of the time, you can use ckeditor_basic.js and then let code take care of loading the rest only when it's needed.

If you know that you don't need it at page load, but that usually an instance will be created shortly after, you can set some seconds in the CKEDITOR.loadFullCoreTimeout property.

If you are able to know that after some command the user will need CKEditor, you can load the full core at that time (remember that this method destroys itself after it has been called, so you must always use it detecting its presence as it's in the example):

// Check if the full core code has been loaded and load it.
if ( CKEDITOR.loadFullCore )
    CKEDITOR.loadFullCore();

But these are just some guidelines, you should be the one that knows your app, how it's being used, and of course remember that browsers have caches, so those 260KiB aren't so terrifying after you realize that on second load it's a hit in the cache and in fact it might be better from a point of view to use the full core in most situations. On the other hand, you might want to merge the basic code into the rest of the javascript of the page, and then it's just a little increase in the size of that javascript and CKEditor will load itself only when it's needed

Caution with the filenames

The loadFullCore just tries to load the file CKEDITOR.basePath + 'ckeditor.js', so if you want to use this boostrap technique you must leave the name as is, also if you try to embed the file into some system (like Asp.Net the WebResource.axd) then it will also fail, and there is no provision to make it work in those situations.

If you have renamed the ckeditor.js (if you don't use this bootstrap system) or ckeditor_basic.js (or have merged into other file), you might need also to do some adjustments, like setting CKEDITOR_BASEPATH (because the detection of CKEditor.basePath is based on the name "ckeditor"), and some times provide a CKEDITOR_GETURL implementation for the getUrl method. But this is too much for this post, someone that uses it might explain it better.

So if you find that something isn't working, be sure to use the default filenames, and if you can't then remember that you'll need to specify some data by yourself.