Last week at Southwest Fox a number of people asked whether it’s possible to use jQuery to do selector searches based on element content. The basic functionality is provided through the :contains() filter function in jQuery which provide a case sensitive search of element content. If I want to find all elements in a set of table row that contain a certain bit of text I might do something like this:

$("#gdEntries>tbody>tr:contains(SQL)").addClass("selectionhighlight");

which in this case will highlight any row that contains the case sensitive SQL string:

HighlightCommands

Nice. This can be be pretty useful. For doing Interactive searches for example. If I add a dialog to the page and hook up a little bit of script code to fire on the keyup event I can dynamically select rows as you type matching the text typed into the textbox:

// Set up key search
$("#txtSearch").keyup(function() {
    $("#gdEntries>tbody>tr:not(:first-child):not(last-child)").removeClass("selectionhighlight");
    $("#gdEntries>tbody>tr:not(:first-child):not(last-child):contains(" + this.value + ")").addClass("selectionhighlight");
});

Talk about a cool effect. Try it out for yourself, it’s pretty neat how easy it is to search and select content interactively.

Got this idea from one of the plug-ins Dave Ward Posted, specifically jQuery quickSearch, but you can provide basic search similar to the plug-in with just these few lines of code.

Creating your own Custom Filters

One very cool feature about filters in the jQuery selector engine is that you can create your own. The :contains selector  works, but unfortunately the search performed is case sensitive. If you wanted to create a case insensitive version of :contains you can do so pretty easily either by replacing :contains or creating a new filter altogether.

Here’s a new filter implementation called :containsNoCase:

$.expr[":"].containsNoCase = function(el, i, m) {
    var search = m[3];
    if (!search) return false;
    return eval("/" + search + "/i").test($(el).text());
};  

A filter selector expression adds to the .expr object and an operator member (in this case the colon :). All filter expressions are defined on $.expr[“:”] as new functions (or strings that are eval’d). The function called receives 3 parameters: The matched element, an index of the element matched, and m which is an object that contains parse tokens as an array. element 0 is the full filter string including parameter, with token 1,2,3 being the :, filter name and parameter respectively. m[3] is the first parameter which in this filter is the string we’re searching for.

The code then uses an regEx to find the expression in the document using case insensitive matching and returns whether a match is found. The result should be true or false to indicate whether the element should be included in the jQuery matched set.

You can declare this code anytime after jQuery has been loaded in a page and then use it like this on the same code above:

$("#gdEntries>tbody>tr:containsNoCase(sql)").addClass("selectionhighlight");

Notice now I specify a lower case sql string which would have failed with plain :contains, but using :containsNoCase finds the match properly.

Here’s another useful filter I’ve used:

$.expr[":"].startsWith = function(el, i, m) {
    var search = m[3];        
    if (!search) return false;
    return eval("/^[/s]*" + search + "/i").test($(el).text());
};

which instead of finding all matched content only finds content that starts with a given string. This requires a more focused search to hopefully select individual elements rather than elements that contain other elements. In the example above I might want to find only the columns or the bold text inside of the columns to search for starting text:

$("#gdEntries>tbody>tr>td>b:startsWith(fix)").addClass("selectionhighlight");

But wait… there’s more.

While I was searching around today I also found out that you can extend the selection operators themselves. If I get really lazy and decide that :containsNoCase is really too wordy for my lazy typing fingers I can shortcut this by creating a custom selector operator, say $. The following code does just that:

$.expr[":"].containsNoCase = function(el, i, m) {
    var search = m[3];
    if (!search) return false;
    return eval("/" + search + "/i").test($(el).text());
};
jQuery.parse.push(/^\s*(\$)(\s*)(.*)$/);
jQuery.expr["$"] = jQuery.expr[":"].containsNoCase;

which lets you now call the code like this:

$("#gdEntries>tbody>tr $DevConnection").addClass("selectionhighlight");

Voila – custom query operator. You’ll want to be careful though not use operators that are already in use. $ works although I had worried that there would be overlap with the attribute ends with selector ([href=$_page.aspx]), but the two can co-exist. I suspect this has to do with the RegEx for the parse expression.

It’s pretty impressive what you can do with jQuery’s query engine and how extensible it is. If you’re interested how some of this works and what you can do with it, check out the jQuery source and look at how the base filters are implemented which gives a pretty intense glimpse of what’s possible with custom selectors.