dean.edwards.name/weblog/

IE7.js gets an update

IE7.js is finally updated!

Here are the changes:

There is a demo page here:

http://ie7-js.googlecode.com/svn/test/index.html

And you can download it from here:

http://code.google.com/p/ie7-js/downloads/list

MSIE’s document.createStyleSheet() throws an error if there are more than 30 existing style sheets

I’m currently doing a round of bug fixing for IE7.js.

Bug #85 highlights a bug in Internet Explorer that I was unaware of:

<!doctype html>
<title>document.createStyleSheet() test</title>
<script>
for (var i = 0; i < 32; i++) {
  document.createStyleSheet(); // throws when i == 31
}
</script>

It doesn’t matter if the style sheets are created with JavaScript or not. Calling document.createStyleSheet() with 30 existing style sheets will throw the error:

Error: Invalid argument.

getElementsByTagName()

It turns out that document fragments do not implement the getElementsByTagName() method. But they do implement the Selectors API methods: querySelector/querySelectorAll.

Nobody cares about this except for people that write JavaScript selector engines. If you are one of those people then keep reading.

I can code around the absence of a lot of DOM methods but getElementsByTagName() seemed impossible to do without. So I had to fake it!

JavaScript performance

JavaScript is a nice enough scripting language but it is still a scripting language.

For high performance loops you should try to avoid the following:

  1. try/catch blocks
  2. function/method calls (even on native objects)
  3. accessing DOM properties (or anything that involves calling a complex getter/setter)

Please note that this is an ordered list. :)

The code

With all that in mind I wrote this simple implementation of getElementsByTagName() which I hope is fast enough for those edge cases where users execute DOM queries on document fragments:

function getElementsByTagName(node, tagName) {
  var elements = [], i = 0, anyTag = tagName === "*", next = node.firstChild;
  while ((node = next)) {
    if (anyTag ? node.nodeType === 1 : node.nodeName === tagName) elements[i++] = node;
    next = node.firstChild || node.nextSibling;
    while (!next && (node = node.parentNode)) next = node.nextSibling;
  }
  return elements;
}

Update: I’ve amended the original code based on the comments below.

Some things to consider:

Convert any colour value to hex in MSIE

The following function will convert any colour value (rgb, named colours, etc) to the hex equivalent in MSIE:

function toHex(color) {
  var body  = createPopup().document.body,
      range = body.createTextRange();
  body.style.color = color;
  var value = range.queryCommandValue("ForeColor");
  value = ((value & 0x0000ff) << 16) | (value & 0x00ff00) | ((value & 0xff0000) >>> 16);
  value = value.toString(16);
  return "#000000".slice(0, 7 - value.length) + value;
};

For other browsers you can use getComputedStyle() so that is already a solved problem.

Note: this method has been around for a while. I posted this adaptation because it does not suffer some of the drawbacks of the original.

If you are concerned about performance then you can always re-use the popup and range objects.

Back in the game

After some wrangling I’m back in control of the dean.edwards.name domain.

dean.edwards.name

Some of you may have noticed that my site is now hosted on a new domain:

http://deanedwards.me.uk/

The old domain got squatted. I whined about it on reddit at the time.

The squatter is currently hosting my copyrighted material. I wrote to the registrar to complain but haven’t received a reply (I should probably contact the hosting service too).

So, if you could update your backlinks/bookmarks etc that would be cool. I know that it’s my fault though. :-(

The old domain was actually worth quite a lot to me. It provided my email address which was the way that people contacted me for work. That email address has provided all of my work for the last five years. There’s no way I can replace it. I’ll just have to start again.

Update: I’m now in contact with the new owner of the domain and it looks like it may be returned to me. Fingers crossed!

Callbacks vs Events

Most of the major JavaScript libraries claim to support custom events in one form or another. For example, jQuery, YUI and Dojo all support a custom “document ready” event. However, the implementation of these custom events is always some form of callback system.

A callback system works by storing event handlers in an array. When the underlying event is detected the dispatch system loops through the array calling the callback functions in turn. So what’s wrong with that? Before I answer that, let’s look at some code.

Here is some simple code that uses the DOMContentLoaded event to perform two separate initialisations:

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
}, false);

document.addEventListener("DOMContentLoaded", function() {
  console.log("Init: 2");
}, false);

What do you expect to see in the console when the document is loaded?

Well, you should see this (or something like it):

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

The point is, both functions are executed. You get an error in the first function but it does not stop the second function from executing.

The Problem

Now let’s look at some code based on a callback system. I’ll pick on jQuery because it’s the most popular:

$(document).ready(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

$(document).ready(function() {
  console.log("Init: 2");
});

What do we see in the console?

Init: 1

Error: DOES_NOT_EXIST is not defined

The problem is clear. Callback systems are brittle. If any of the callback functions throw an error then the subsequent callbacks are not executed. In reality, this means that a poorly written plugin can prevent other plugins from initialising.

Dojo suffers exactly the same problem as jQuery. The YUI library takes a slightly different approach. It wraps a try/catch around its dispatch mechanism. The downside is that your errors occur silently:

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

YAHOO.util.Event.onDOMReady(function() {
  console.log("Init: 2");
});

Produces:

Init: 1

Init: 2

Perfect initialisation! Nothing to worry about here! Except for the error that you don’t see.

So what’s the solution?

The Solution

The solution is to use a hybrid of a callback system and real event dispatch. We can trigger a fake event and from within that event, run the callback function. Each event handler has its own execution context. If an error occurs in our fake event then it won’t affect our callback system.

That sounds a bit complicated. I’ll illustrate with some code.

var currentHandler;

if (document.addEventListener) {
  document.addEventListener("fakeEvents", function() {
    // execute the callback
    currentHandler();
  }, false);

  var dispatchFakeEvent = function() {
    var fakeEvent = document.createEvent("UIEvents");
    fakeEvent.initEvent("fakeEvents", false, false);
    document.dispatchEvent(fakeEvent);
  };
} else { // MSIE

  // I'll show this code later
}

var onLoadHandlers = [];
function addOnLoad(handler) {
  onLoadHandlers.push(handler);
};

onload = function() {
  for (var i = 0; i < onLoadHandlers.length; i++) {
    currentHandler = onLoadHandlers[i];
    dispatchFakeEvent();
  }
};

Now we’ll use the code above to attach our two troublesome event handlers:

addOnLoad(function() {
  console.log("Init: 1");
  DOES_NOT_EXIST++; // this will throw an error
});

addOnLoad(function() {
  console.log("Init: 2");
});

OK. Let’s run the code above and see what we get:

Init: 1

Error: DOES_NOT_EXIST is not defined

Init: 2

Perfect! Just what we want. Both event handlers are executed and we also get a message telling us about the error in the first handler. Great!

But what about Internet Explorer I hear you ask (I have good hearing). MSIE does not support the standard event dispatch system. It has its own method; fireEvent but that only works with real events (e.g. click).

Rather than explain the solution in words, here is the code:

var currentHandler;

if (document.addEventListener) {

  // We've seen this code already

} else if (document.attachEvent) { // MSIE

  document.documentElement.fakeEvents = 0; // an expando property

  document.documentElement.attachEvent("onpropertychange", function(event) {
    if (event.propertyName == "fakeEvents") {
      // execute the callback
      currentHandler();
    }
  });

  dispatchFakeEvent = function(handler) {
    // fire the propertychange event
    document.documentElement.fakeEvents++;
  };
}

A similar approach except that we use the proprietary propertychange event as the trigger.

Summary

I’ve shown a very simple example of how to use the uderlying event system to fire custom events. Library authors should be capable of seeing how this can be extended to fully support cross-browser custom events.

Update

Some commenters have suggested using setTimeout. Here is my response to that:

For this particular example, a timer will work fine. This is just an example to illustrate the technique. The real usefulness of this is for other custom events. Most libraries implement custom events using a callback system. As I illustrated, callback systems are brittle. Dispatching events with timers will work to a degree but it is not how a real event system works. In a real system, events are dispatched sequentially. There are other concerns, like cancelling an event and stopping the event from bubbling. This would be impossible with timers.

The important thing is that I’ve demonstrated a technique for wrapping a callback system in a real event dispatch system. That gives you the ability to fire real custom events in MSIE. If you are building an event system based on event delegation then this technique may also be interesting to you.

Update 2

It seems that Prototype uses an almost identical trick to fire custom events in MSIE:

http://andrewdupont.net/2009/03/24/link-dean-edwards/

Two steps forward one step back

Safari 3.1 fails the Acid2 test:

http://bugs.webkit.org/show_bug.cgi?id=13693