Javascript Events, the DOM, and Firefox/Gecko

OK, here’s the deal, as quick as I can put it, but with enough words that Google will correctly index this post and make this information easier to find for the next person who needs it.

Let’s say you’re developing a Firefox extension, and you need to look at the HTML of the page. You know that Firefox is constantly throwing event notifications — there’s the MouseEvent “click” when something is clicked, the KeyboardEvent “keyup” when a key is release, and so on.

So, you figure “well, I need the HTML of the page, so I’ll wait until it loads, then grab the source/parse the DOM tree/unleash flying monkeys”. You know that the style of

window.onload = the_function_name;

is bad, because it only lets one function receive the load event. So, you write some code like

gBrowser.addEventListener(“load”, the_function_name, true);

because this is what you want. You wait for the “load” event to fire, and then run your function. Test this code out, and it will work well-enough, most of the time. Sometimes, it lags, sometimes it doesn’t even fire at all. WTF? Well, I’ve been there.

Here’s the thing: the “load” event fires when loading is done. If you’re looking at the whole window/document, it’s not done loading until the whole document is loaded, including bandwith-and-time-heavy images. And these things can be slow to load, or hang entirely. This can seriously mess with the concept of “loading”. You don’t care about any of these things, you just want the HTML.

Well, Firefox is a very deep project, and there’s a few secret events that aren’t widely used or documented. If you want to know when the HTML is loaded — or more accurately, when the DOM Tree’s content is loaded — there’s a special event you can monitor: “DOMContentLoaded”

gBrowser.addEventListener(“DOMContentLoaded”, the_function_name, true);

This is a very reliable way of knowing when the browser has loaded only the page. The catch is that it’s Firefox-internal only. Which means it only works from an extension — you can’t use this in a web page. There are techniques for duplicating this functionality in other browsers, but that’s getting beyond this post.

Detecting page loads in a Firefox Extension

Detecting page loads is a useful ability. It’s easy enough to throw in a

gBrowser.addEventListener(“load”, function_name, true);

But, it causes extra events to get generated. There’s load events occurring in the browser that aren’t page loads. The trick to detecting real page loads (but not Back/Forward navigation when the page is still in memory — only page loads that have a server hit) is to look at the event’s target. In this example, “event” has been passed into the function specified in the addEventListener call above.

if(event.originalTarget instanceof HTMLDocument) {

will make sure that the load event came from a web page being loaded. You can even go a step further and filter out different types of HTMLDocuments.

if(event.originalTarget.defaultView.frameElement) {

will catch events that were triggered as the result of a frame or iframe. It’s an important step, because lots of advertising networks embed their ads in iframes, and you probably are more interested in the actual web page than the ads on that page.

Firefox extension debugging

One hugely important thing in coding is debugging. Unfortunately, a lot of Javascript debugging gets done via alert() calls. This gets awkward quickly, with the alerts affecting timing, and just being annoying if you have to dump large amounts of data out.

Firebug is a great development tool, and has a really handy logging interface that you can dump debugging info to. Just calling console.log(whatever) will dump it to the main Firebug interface as text that you can copy/paste, scroll through, etc.

If you’re developing a Firefox extension, this debugging capability is really useful. Except, calling console.log() doesn’t work, console isn’t defined for the browser, only for each window.

The trick? Call it directly from the Firebug extension object.

Firebug.Console.log()

Be sure to capitalize both Firebug and Console, and you’ll be good to go. In addition to having great capabilities for logging, the console will prevent your debugging messages from popping up to your users, in case you leave some code where it shouldn’t be.

By the way — if you found this helpful, check back here in a few days. I’ve submitted a presentation proposal to SXSW for Firefox extension development, where I’ve got tons of info for creating extensions for web applications. They collect votes from the community, and I’d like your support. Plus, if the presentation goes through, I’ll be collecting lots of my best tips and putting them online as a resource for the attendees. That means you’ll get all of them too, and you don’t have to go anywhere! 😀

Edit (2008-08-21: Added link for SXSW voting panel)

Finding text from a Firefox Extension

OK. Because this was confusing the hell out of me, I had to post about figuring it out.

Let’s say you’re a developer writing a Mozilla Firefox Extension that searches for text on a page (or rather, in a browser window). If you have a button that has the following functionality attached onclick:

var webBrowserFind = getBrowser().selectedBrowser.webBrowserFind;
webBrowserFind.searchString = TEXT_TO_SEARCH_FOR;
var result = webBrowserFind.findNext();

So, every time you click the button, it will search for the text in the current browser tab. Cool.

I did this, and it was working fine, until I wanted to find the second instance of the string on the page. I’d click it twice. Sometimes it would highlight the first instance again. Sometimes it would highlight the second instance. No real sign as to why, and as a kicker, it would never find past the first two instances of the string on the page.

Turns out that after a certain timeout, the nsIWebBrowserFind interface is going to reset which find instance it was at. This means that on the second click, it will find the first occurrence again. If you click twice within the timeout, you’ll get the second result. I didn’t see any mention of this in the documentation, and it’s not clear what the mechanism is that’s resetting the instance.

If you click really fast, you’ll get the third and fourth results. For once, getting frustrated and repeatedly clicking fast was the solution.

Hat tip to mfinkle and johnm of irc.mozilla.org#extdev for helping me find the nsIWebBrowserInterface in Seamonkey