Sunday, June 10, 2007

Incremental search using JavaScript

Aside from this blog, I am also the webmaster (and one of the contributors) on two photo blogs(one in English, the other in Dutch). I wrote the software for maintaining these photo blogs myself, because nothing at the time seemed to do exactly what I wanted. The upside of writing your own software is that it does exactly what I want. The downside is that if I want any new feature, I have to write it myself.

This weekend I added a simple incremental search feature to the administrative interface of the photo blogs. Nothing fancy, but just an easy way for the contributors to check for example when the last post on diaper cakes was or when we had the theme week on chocolate.

My solution is completely client-side using this JavaScript:

  • var TEXT_NODE = 3;
    var TIMER;
    var SKIP_ROW_COUNT = 2;
    var SEARCH_INPUT;

    function instrument(input, skipRows) {
    SEARCH_INPUT = input;

    if (skipRows) {
    SKIP_ROW_COUNT = skipRows;
    }

    // listen to changes to the input
    input.onkeyup = input.onchange = startTimer;
    }

    function startTimer() {
    // start (or re-start) the timer, only once it expires will we start the search
    if (TIMER) {
    clearTimeout(TIMER);
    }
    TIMER = setTimeout("search()", 500);
    }

    function search() {
    var term = SEARCH_INPUT.value.toLowerCase();
    var rows = document.getElementsByTagName("tr");
    // skip first few rows (optional author book, commands and header)
    var i;
    for (i = SKIP_ROW_COUNT; i < rows.length; i++) {
    var tr = rows[i];
    // hide row if it doesn't contain the search term
    tr.style.display = (term == "" || doesNodeContain(tr, term)) ? "" : "none";
    }
    }

    function doesNodeContain(node, term) {
    if (node.nodeType == TEXT_NODE && node.nodeValue.toLowerCase().indexOf(term) >= 0) return true;
    return true;
    }
    var child = node.firstChild;
    while (child) {
    if (doesNodeContain(child, term)) {
    return true;
    }
    child = child.nextSibling;
    }
    return false;
    }
As you can see it is pretty basic stuff. Set up some event handlers on the input, start a timer whenever the user enters some text and then do the search once the timeout expires. The search itself is done using some recursive DOM scanning. There's probably a cheaper way to do this and once I find that I'll update the script. But until then, this simple script will do just fine.

You hook this up in the HTML using:
  • <input onfocus="instrument(this, 2)" type="text">
The second argument to the instrument function is the number of rows from the top that the search should ignore. I tend to have some extra rows at the top that unfortunately don't all use th's, so this is the easiest way to exclude those from the search.

I know that putting inline JavaScript in the HTML is not very Web 2.0 and makes the site inaccessible. But since this is the administrative interface for the contributors and not the front end of the website, I think I can live with myself for not making it accessible.

3 comments:

Anonymous said...

Hi Frank:
(Hope you're still around)

I'm using your Incremental Search js code, but can't make it work with IE7 (altho it works with FF and Chrome).

Also, I wish to search thru the 2nd column of text (rather than *each* column)

Can you help ?

I'm at syntel@cox.net

Thanks,
-Mel Smith (an old guy)

Frank said...

Hey Mel,

limiting the columns search can be done by modifying the doesNodeContainFunction. It'll have to cater for the various tag names you might encounter, like "td", "th", etc.

If you can point me to a sample page that you want to search, I'll see what I can do.

Frank

Frank said...

Hi Mel,

Just change the search function to the one below:

function search() {
var shomsg = document.getElementById("ishomsg") ;
//shomsg.style.color="red" ;
//shomsh.style.background="white" ;
var term = SEARCH_INPUT.value.toLowerCase();
var rows = document.getElementsByTagName("tr");
// skip first few rows
for (var i = SKIP_ROW_COUNT; i < rows.length; i++) {
var tr = rows[i]; // hide row if it doesn't contain the search term
var tds = tr.getElementsByTagName("td");
if (!tds || tds.length < 2) continue;
var td = tds[1];
tr.style.display = (term == "" || doesNodeContain(td, term)) ? "" : "none";
}
//alert("finished loop") ;
//shomsg.style.color = "#80ff80"
//shomsg.style.background = "#80ff80"
//alert("finished searching") ;


Let me know if it works for you.

Frank