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.

4 comments:

AGX said...

With chrome i get an error


Uncaught TypeError: Cannot read property 'get' of undefined

Just after IE 8 OK
get: function() {

AGX said...

With Firefox and IE this ROCKS
You saved my day!

Alfonso said...

Chrome shouldn't have raised that error. That means that finally they have added something, but it's not compatible with the code that I wrote.

I'll review it one of these days to check if now it's possible to make it compatible with Chrome (hey, they are finally adding print preview, so they might start adding existing useful things instead of inventing new features)

Alfonso said...

I've added a new version that includes support for Chrome (but Safari still doesn't work, they insist that speed is more important than features that can be used by developers)

http://alfonsoml.blogspot.com/2011/07/third-version-for-seamless-replacement.html