Recursing HTML Frames in Javascript

Sometimes you want to apply a javascript function to an entire page. Maybe you want to replace all of the images with their alt text, maybe you want to open all of the links in new windows, maybe you want to remove the target attributes from links so they don’t open new windows. Bookmarklets usually do something like this, as do Greasemonkey scripts, and sometimes page authors want these functions. It’s as simple as using a couple of document.getElementsByTagName()s and similar functions.

Unfortunately this is singularly unhelpful when dealing with frames-based pages. If your page consists of, say, two frames each of which contains another page and you want to enumerate the links in the child pages you can’t do a simple document.getElementsByTagName("a"). That’ll give you back an empty array, since the frameset page itself (usually) contains no such elements. You need to recurse through the frames and apply the function to every document in the frameset.

function recurseFrames(doc, callback)
{
	if (typeof callback != 'function') return;
	// apply to the current document:
	callback(doc);
	// recurse over the child documents:
	var frames = doc.getElementsByTagName("frame");
	for (var i = 0; i < frames.length; i++)
	{
		var d = frames[i].contentDocument;
		recurseFrames(d, callback);
	}
}

The above function takes two arguments: a document to recurse, and a function to apply to the document and all of its child documents. If you're calling this from a bookmarklet you can just pass the document object as the first argument, since it will refer to the top document in the heirarchy. If you're calling it from a page within the heirarchy you need to use top.document. Of course you don't have to recurse the whole tree, so you could give it any document object.

The function makes sure that the second argument is a function before it tries to do anything. Then it applies that function (the callback function) to the document. If it's a simple document (ie. no frames) then that's all that happens, but if there are child frames then the function applies itself to their documents too, and so on down the heirarchy.

This is about the simplest implementation of this sort of recursion. A number of extensions immediately suggest themselves. What if you want to return some value(s) from the function? Say you want to enumerate the links in the entire compound document. You could always edit it like this:

function recurseFrames(doc, callback)
{
	if (typeof callback != 'function') return;
	var result = {};
	// apply to the current document:
	result.push(callback(doc));
	// recurse over the child documents:
	var frames = doc.getElementsByTagName("frame");
	for (var i = 0; i < frames.length; i++)
	{
		var d = frames[i].contentDocument;
		var res = recurseFrames(d, callback);
		for (var j = 0; j < res.length; j++)
		{
			result.push(res[j]);
		}
	}
	return result;
}

No doubt there's a cleaner way to accomplish this result, but the intention is clear. Each call to recurseFrames() returns an array of the results of applying the callback function to its children (and itself), which is combined with the parent's results array. The final return value is a flat one-dimensional array with a single element per document. If you knew the result of the callback function was an array you could modify the function further to sow these arrays together rather than returning an array of arrays.

Another reasonable extension is to extend this function to include iframes. This would require altering the definition of the frames variable. It's a trivial extension and I leave it as an exercise for the reader.

CategoriesUncategorized