mgreenblog

posts by category about this blog

Attack of the `cloneNode`s

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:

/**
 * List of all DOM event handler names.
 */
var dom_events = [
  "onblur",
  "onfocus",
  "oncontextmenu",
  "onload",
  "onresize",
  "onscroll",
  "onunload",
  "onclick",
  "ondblclick",
  "onmousedown",
  "onmouseup",
  "onmouseenter",
  "onmouseleave",
  "onmousemove",
  "onmouseover",
  "onmouseout",
  "onchange",
  "onreset",
  "onselect",
  "onsubmit",
  "onkeydown",
  "onkeyup",
  "onkeypress",
  "onabort",
  "onerror",
]; // ondasher, onprancer, etc.

/**
 Fixes copy errors introduced by {@link element#cloneNode}, e.g. failure to copy classically-registered event handlers and the value property.

 @param {element} o The original DOM element
 @param {element} copy The result of o.cloneNode()
 @return {element} A modified copy with event handlers maintained
*/
function fix_dom_clone(o, copy) {
  if (!(dom_obj(o) && dom_obj(copy))) {
    return;
  }

  for (var i = 0; i < dom_events.length; i++) {
    var event = dom_events[i];
    if (event in o) {
      copy[event] = o[event];
    }
  }
  if ("value" in o) {
    copy.value = o.value;
  }

  // recur
  var o_kids = o.childNodes;
  var c_kids = copy.childNodes;
  for (i = 0; i < o_kids.length; i++) {
    fix_dom_clone(o_kids[i], c_kids[i]);
  }
}

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".

Michael Greenberg (mike@weaselhat.com)

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);
}