Attack of the cloneNodes

So the solution to the bug I had yesterday was fixed with a call to element::cloneNode to avoid aliasing. This introduced, to my great consternation, another bug — some DOM nodes were reverting to their default value. Had I written this down in my (as yet hypothetical) bug journal, it might have become more clear. Instead, I slaved away in Firebug for a few hours without results.

Thinking about it clearly, the problem had to be in cloneNode. I ended up having to write the following recursive fix-up function:

JAVASCRIPT [Show Plain Code]:
  1. /**
  2. * List of all DOM event handler names.
  3. */
  4. var dom_events =
  5. ['onblur', 'onfocus', 'oncontextmenu', 'onload',
  6. 'onresize', 'onscroll', 'onunload', 'onclick',
  7. 'ondblclick', 'onmousedown', 'onmouseup', 'onmouseenter',
  8. 'onmouseleave', 'onmousemove', 'onmouseover',
  9. 'onmouseout', 'onchange', 'onreset', 'onselect',
  10. 'onsubmit', 'onkeydown', 'onkeyup', 'onkeypress',
  11. 'onabort', 'onerror']; // ondasher, onprancer, etc.
  12.  
  13. /**
  14. Fixes copy errors introduced by {@link element#cloneNode}, e.g. failure to copy classically-registered event handlers and the value property.
  15. @param {element} o The original DOM element
  16. @param {element} copy The result of o.cloneNode()
  17. @return {element} A modified copy with event handlers maintained
  18. */
  19. function fix_dom_clone(o, copy) {
  20.     if (!(dom_obj(o) && dom_obj(copy))) { return; }
  21.    
  22.     for (var i = 0;i < dom_events.length;i++) {
  23.         var event = dom_events[i];
  24.         if (event in o) { copy[event] = o[event]; }
  25.     }
  26.     if ('value' in o) { copy.value = o.value; }
  27.    
  28.     // recur
  29.     var o_kids = o.childNodes;
  30.     var c_kids = copy.childNodes;
  31.     for (i = 0;i < o_kids.length;i++) {
  32.         fix_dom_clone(o_kids[i], c_kids[i]);
  33.     }
  34. }

Oof. Unsurprisingly, there are a few efficiency issues.

My bug was weird and unexpected, and the W3C DOM level 2 spec doesn't allude to problems like this, but looking at a Mozilla bug report on the topic, it seems that the W3C DOM level 3 spec says that "[u]ser data associated to the imported node is not carried over". I guess if that's true for event handlers, it's also true for the value property. Oh well. I'd feel better about this irritating API "feature" if they said "associated with".

* Filed by Michael Greenberg on 2007-03-31 at 2:59pm under JavaScript
* 2 Comments

a weasel in a hat

2 Responses to “Attack of the cloneNodes”

  1. October 1st, 2007 | 10:49am

    Andreas Eibach pointed out that this definition was missing:

    /**
     * Determines whether o is a DOM object.
     *
     * @param o A value to test
     * @param {Boolean} True if o is a DOM object
     */
    function dom_obj(o) {
        return typeof o == 'object' && (o instanceof Node || o.nodeType > 0);
    }
    
  2. adriano
    April 16th, 2008 | 4:21am

    guy. you rock. seriously. thank you a lot

Leave a reply

Please submit only once. Comments are moderated and will not appear immediately.