Mutation events: what happen?



Lon Boonen 08 Oct 2009 09:00:00 GMT 200910080900

Engineering



Since typlab is all about exploring new ways of creating and consuming online content we figured our software might want to keep track of what’s happening inside a document.

All modern browsers have support for W3C’s mutation events. Safari, Chrome, FireFox and Opera all do them. But not all do all of them.

Notably WebKit fails to fire DOMAttrModified events when an attribute is changed. It does however fire the DOMSubtreeModified event after an attribute is modified. So at least that gives us something to work with until the good folks at WebKit squash the bug.

Here is how we fixed the lack of DOMAttrModified. First we need to detect whether the fix is needed:

var attrModifiedWorks = false;

var listener = function(){ attrModifiedWorks = true; };

document.documentElement.addEventListener("DOMAttrModified", listener, false);

document.documentElement.setAttribute("___TEST___", true);

document.documentElement.removeAttribute("___TEST___", true);

document.documentElement.removeEventListener("DOMAttrModified", listener, false);

The code is straightforward. Add an attribute and have a listener register the firing of the subsequent DOMAttrModified event. If the event is not fired our repair code kicks in:

if (!attrModifiedWorks)

{

Next we store and override HTMLElement.setAttribute:

HTMLElement.prototype.__setAttribute = HTMLElement.prototype.setAttribute



HTMLElement.prototype.setAttribute = function(attrName, newVal)

{

  var prevVal = this.getAttribute(attrName);

  this.__setAttribute(attrName, newVal);

  newVal = this.getAttribute(attrName);

  if (newVal != prevVal)

  {

    var evt = document.createEvent("MutationEvent");

    evt.initMutationEvent(

      "DOMAttrModified",

      true,

      false,

      this,

      prevVal || "",

      newVal || "",

      attrName,

      (prevVal == null) ? evt.ADDITION : evt.MODIFICATION

    );

    this.dispatchEvent(evt);

  }

}

The new code fetches the current value of the attribute, soon to become the previous value. It then proceeds to set the attribute using the original setAttribute method that we stored. We don’t know whether that method does fancy stuff to the new attribute value, so, just to be sure, we fetch the new value by calling getAttribute once again.

If and only if the new and previous value differ we proceed to dispatch the appropriately initialised mutation event.

This covers added and modified attributes. But it won’t help us with removed attributes. For those we can override the removeAttribute method of HTMLElement:

HTMLElement.prototype.__removeAttribute = HTMLElement.prototype.removeAttribute;

HTMLElement.prototype.removeAttribute = function(attrName)

{

  var prevVal = this.getAttribute(attrName);

  this.__removeAttribute(attrName);

  var evt = document.createEvent("MutationEvent");

  evt.initMutationEvent(

    "DOMAttrModified",

    true,

    false,

    this,

    prevVal,

    "",

    attrName,

    evt.REMOVAL

  );

  this.dispatchEvent(evt);

}

This concludes our fix for the lack of DOMAttrModified in WebKit.

Is this fix perfect? Nope. Some known issues:

  • a DOMSubtreeModified event is fired before instead of after the (artificial)DOMAttrModified event
  • assigning a value to an attribute will not trigger our setAttribute method. Most noticeably assigning a value to a className or id attribute will not result in the appropriate DOMAttrModified event

We’re open for suggestions. But best would be if some WebKit developer would fix the bug so we can throw this code away.

Engineering