In a recent update to Googlebar Lite, I made a number of improvements to the search term highlighting feature, fixing several bugs along the way. This feature uses the nsIFind
interface available in Firefox, which is poorly documented in my opinion. Unable to find any decent examples, I picked apart another extension I found that uses this interface, and I now better understand how it works. As such, I thought I'd provide an example of how to use this interface so that future developers won't have to dig down in the source like I did.
The following example will be simple, but we'll do some DOM modification along the way to spice things up. Note that this interface really only has one function that we care about: Find
. Here's what it looks like:
nsIFind::Find(text, searchRange, startPoint, endPoint)
The parameters to this function are simple:
- text: The text to search for
- searchRange: A DOM Range specifying the domain of the search
- startPoint: A DOM Range specifying the start point of the search
- endPoint: A DOM Range specifying the end point of the search
Note that both the startPoint
and endPoint
parameters are ranges. Technically, these ranges can have different start and end points themselves, which complicates things depending on which direction you are searching (forwards or backwards). However, this example will avoid that scenario to keep things simple. I will also not discuss how to handle searching within frames on a website, something I'll leave as an exercise to the reader (hint: the solution involves recursion). Let's dive right in to the code:
// TODO: Note that in practice, we should do some error checking on // the following three variables to make sure they are really available var win = window.content; // Get a reference to the window's content var doc = win.document; // Get a reference to the document var body = doc.body; // Get a reference to the body element var term = "Firefox"; // The term to find, hard-coded for this example // Create a highlighted span element which we will clone var span = doc.createElement("span"); span.setAttribute("style", "background: #FF0; color: #000; " + "display: inline !important; font-size: inherit !important;"); // Create our search range var searchRange = doc.createRange(); searchRange.selectNodeContents(body); // Create the start point var start = searchRange.cloneRange(); start.collapse(true); // Collapse to the beginning // Create the end point var end = searchRange.cloneRange(); end.collapse(false); // Collapse to the end // Create the finder instance var finder = Components.classes['@mozilla.org/embedcomp/rangefind;1'] .createInstance(Components.interfaces.nsIFind); // Perform the find operation while((start = finder.Find(term, searchRange, start, end))) { // Clone the highlighter node and surround our search results with it var hilitenode = span.cloneNode(true); start.surroundContents(hilitenode); // Collapse the starting range to its end point, so we don't find this // instance again the next time around the loop start.collapse(false); // Workaround for Firefox bug #488427 body.offsetWidth; }
There are a few sections of this code that are worth expanding on. As the "TODO" comment suggests, you should test to make sure that your references to the window's content actually exist. A simple if(!variable)
test will suffice. Next, when creating our highlighting span, you'll note that I set a few interesting style rules: display:inline
and font-size:inherit
, both of which have the !important
modifier applied to them. These rules help ensure that our inserted spans don't interfere too much with the existing page layout. I'm sure that additional rules could be added to make this even more battle hardened, but these are what I use in Googlebar Lite.
Next, when we create the search range, we use the selectNodeContents
function to populate the range object. In our example, we select the contents of the "body" tag, which is essentially all of the page's content. Our start and end points are created by cloning our search range, then using the collapse
function. Passing true
to this function will collapse the range to its start point, while passing false
will collapse to the end point.
Everything else should be fairly straightforward: we create an instance of the nsIFind
interface, call its Find
method, assigning our starting point to the result (so we don't repeatedly find the same instance of our search string). For each instance, we surround it with our "highlighted" span and we collapse the start point, again so we skip this instance the next time around the loop.
The last line of code in this function deserves some explanation. As you can see, we simply do a read on the offsetWidth
property of the body
element. This read essentially forces a reflow of the page, which flushes the changes we made to the DOM (inserting the our highlighted span). If we don't flush the changes by reflowing the page, the Find
method will skip to the next sibling element in the DOM. Bug 488427 has all the details, though (as of this writing) its currently closed and marked as "worksforme." Nevertheless, this problem still persists as of Firefox 7.0.1, and this simple fix acts as a nice workaround. Note that this read must appear between the time you insert your element and the time you call nsIFind.