Firefox 3 .pageX / .pageY bug

Firefox 3 has a bug. It's somewhat obscure, and it probably doesn't affect you, but it did affect me.

It also affected the Microsoft Virtual Earth MapControl, so if you use the VE MapControl for a mashup, your Firefox 3 users are going to be affected by this issue.

The issue? It has to do with a mouse event's .pageX and .pageY properties. The values of those properties are supposed to report the "page" position of the mouse cursor, relative to the upper-left corner of the HTML page. You can think of it as the absolute (x,y) of the mouse cursor.

The nice thing about .pageX/Y is that serves as an easy way to get the mouse position, regardless of how much (or even if) the browser window is scrolled.

Contrast .pageX/Y with .clientX/Y: the latter reports (x,y) coordinates relative to the upper-left corner of the browser window, so these values will be affected by browser scroll position.

Anyway, at this point, you're probably thinking that I'm going to tell you that Firefox 3 reports the wrong values for .pageX/Y, and that that's the bug. It's not that simple though (again, I admit this is a somewhat obscure bug).

Most of the time, Firefox 3 reports the right values for .pageX/Y, but there is one case in which it doesn't.

Manually Dispatching Events in Javascript

A while back, I wrote a post about Re-routing events in Javascript. In it, I explain how to create an event and initialize it with myEvt.initMouseEvents(..) (which takes a boat-load of parameters).

Of all those parameters sent into .initMouseEvents(..), 4 are of particular interest: .screenX, .screenY, .clientX, and .clientY.

Notice that .pageX and .pageY are not in the list. You don't get the ability to pass in .pageX/Y when manually creating/dispatching an event in Javascipt. Presumably, the values for these properties are computed based on .clientX/Y and the browser's scroll position.

So for example, if you initialize a MouseEvent with .clientX = 200 and .clientY = 300, and if the browser is scrolled 50 x 75, then your event's .pageX and .pageY values will be 250 and 375 respectively.

Oops! I mean, that's what the values should be.

The Bug

Firefox 2 actually gets it right, but Firefox 3 doesn't.

When you're manually dispatching a MouseEvent in Firefox 3, the .pageX/Y property values will always be equal to the .clientX/Y values, regardless of browser scroll position.

As I previously mentioned, this affects the VE MapControl, so it's easy to see the issue in action. Just use Firefox 3 and point your browser to http://maps.live.com. Now make your browser window small enough to cause scroll bars to appear. Scroll the browser window by some amount (doesn't matter how much) and try panning the map. You should see the map "jump" during the initial pan. The amount it "jumps" is going to be equal to the amount by which the browser window is scrolled.

The Fix

Microsoft has a work-around for this issue that is going to be released with the next version of Virtual Earth, but if you're a mashup dev using the VE MapControl, I've got a solution that you can use right now, and it goes like this:

(function()
{
    var mouseEvt;
    if (typeof document.createEvent !== 'undefined')
    {
        mouseEvt = document.createEvent('MouseEvents');
    }
    if (mouseEvt && mouseEvt.__proto__ && mouseEvt.__proto__.__defineGetter__)
    {
        mouseEvt.__proto__.__defineGetter__('pageX', function()
        {
            return this.clientX + window.pageXOffset;
        });
        mouseEvt.__proto__.__defineGetter__('pageY', function()
        {
            return this.clientY + window.pageYOffset;
        });
    }
})();

Just include the preceding code snippet anywhere in your HTML page. The code is pretty straight-forward and just relies on the fact that Firefox (and others) have the ability to define getters through the use of .__defineGetter__(..).

The .pageX/Y properties are read only, but it turns out that the browser will let you redefine these getters, thereby overriding their return logic. Since the .clientX/Y properties appear to always report the correct value, all we have to do redefine the .pageX/Y getters to use a combination of .clientX/Y + .pageX/YOffset.

In other words, we've redefined .pageX/Y to always return the position of the event (in our case, the mouse cursor since we're dealing with MouseEvents), relative to the upper-left corner of the browser window + the scrolled position of the browser -- what it should have been all along!

This code doesn't hurt IE, as it won't execute in that browser, and it doesn't hurt Firefox 2 or other browsers that understand the code either, because the logic we've defined for the getters is going to be essentially the same as what the browser would have done natively anyway.

I'd like to file a bug to Mozilla on this issue, but I haven't a clue as to how one goes about doing that. If anyone knows (or wants to do it for me), feel free to speak up!

Enjoy the fix!

Comments welcome.