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 aclassName
orid
attribute will not result in the appropriateDOMAttrModified
event
We’re open to suggestions. But best would be if some WebKit developer would fix the bug so we can throw this code away.