Extending jQuery’s selector capabilities

I’m sure you all know that it’s possible to create plugins and extend various aspects of the jQuery JavaScript library but did you know you could also extend the capabilities of it’s selector engine?

Well, you can! For example, you might want to add a new ‘:inline’ selector which will return those elements that are displayed inline. Have a look:

$.extend($.expr[':'],{
    inline: function(a) {
        return $(a).css('display') === 'inline';
    }
});

Using the above code, when you want to select elements that are displayed inline you can simply include it within the selector:

$(':inline'); // Selects ALL inline elements
$('a:inline'); // Selects ALL inline anchors

That was a pretty simple example but I’m sure you can see the endless possibilites that this enables! And, creating a custom jQuery selector couldn’t really be simpler!

Loaded images selector

You might want to add a ‘loaded’ selector which will work with images, and will return those images that are loaded:

// Flag images upon load:
$('img').load(function(){
    $(this).data('loaded',true);
});
 
// Extend expr:
$.extend($.expr[':'],{
    loaded: function(a) {
        return $(a).data('loaded');
    }
});
 
// Example of usage:
alert( 'Images loaded so far: ' + $('img:loaded').size() );

Querying element data

jQuery’s ‘data’ function allows us to add special data to elements without having to pollute global variables or add invalid element attributes. One of the things that jQuery lacks is the capability to easily query elements according to their data. For example, one might decide to flag all elements added dynamically (with jQuery) as ‘dom’:

// New element:
$('<img/>')
    .data('dom', true) // Flag
    .appendTo('body'); // Append to DOM

Currently there’s no easy way to select all elements that have been flagged but what if we added a new ‘:data’ selector which could query such information?

Here’s how we would do it:

// Wrap in self-invoking anonymous function:
(function($){
 
    // Extend jQuery's native ':'
    $.extend($.expr[':'],{
 
        // New method, "data"
        data: function(a,i,m) {
 
            var e = $(a).get(0), keyVal;
 
            // m[3] refers to value inside parenthesis (if existing) e.g. :data(___)
            if(!m[3]) {
 
                // Loop through properties of element object, find any jquery references:
                for (var x in e) { if((/jQuery\d+/).test(x)) { return true; } }
 
            } else {
 
                // Split into array (name,value):
                keyVal = m[3].split('=');
 
                // If a value is specified:
                if (keyVal[1]) {
 
                    // Test for regex syntax and test against it:
                    if((/^\/.+\/([mig]+)?$/).test(keyVal[1])) {
                        return
                         (new RegExp(
                             keyVal[1].substr(1,keyVal[1].lastIndexOf('/')-1),
                             keyVal[1].substr(keyVal[1].lastIndexOf('/')+1))
                          ).test($(a).data(keyVal[0]));
                    } else {
                        // Test key against value:
                        return $(a).data(keyVal[0]) == keyVal[1];
                    }
 
                } else {
 
                    // Test if element has data property:
                    if($(a).data(keyVal[0])) {
                        return true;
                    } else {
                        // If it doesn't remove data (this is to account for what seems
                        // to be a bug in jQuery):
                        $(a).removeData(keyVal[0]);
                        return false;
                    }
 
                }
            }
 
            // Strict compliance:
            return false;
 
        }
 
    });
})(jQuery);

Usage

Now, selecting elements which have that ‘dom’ flag is really easy:

$(':data(dom)'); // All elements with 'dom' flag
$('div:data(dom)'); // All DIV elements with 'dom' flag
$(':not(:data(dom))'); // All elements WITHOUT 'dom' flag

The ‘:data’ extension also allows you to query by comparison, for example:

$(':data(ABC=123)'); // All elements with a data key of 'ABC' equal to 123

It also allows you to use regular expressions:

// Let's assume we have slightly varying data across a set of elements:
$('div').each(function(i){
    $(this).data('divInfo','index:' + i);
    // Will result in value being 'index:0', 'index:1', 'index:2' etc. etc.
});
 
// We can select all of those DIVs like this:
$('div:data(divInfo=/index:\\d+/)');
 
// Note: It's necessary to use non-literal notation when writing these
// regular expressions, so if you want to match a real backslash you'd
// have to use '\\\\'. Similarly if you want to test for all digit's
// you'd have to use \\d instead of \d.

Additionally, you can select elements on a basis of whether or not they have ANY data applied to them:

$(':data');       // All elements with data
$(':not(:data)'); // All elements without data

Some other examples:

  • :red

    // Check if color of element is red:
    $.extend($.expr[':'],{
        red: function(a) {
            return $(a).css('color') === 'red';
        }
    });
     
    // Usage:
    $('p:red'); // Select all red paragraphs
  • :childOfDiv

    // Check if element is a child of a div:
    $.extend($.expr[':'],{
        childOfDiv: function(a) {
            return $(a).parents('div').size();
        }
    });
     
    // Yes, I know this is exactly the same as $('div p')
    // This is just a demonstration! ;)
     
    // Usage:
    $('p:childOfDiv'); // Select all paragraphs that have a DIV as a parent
  • :width()

    // Check width of element:
    $.extend($.expr[':'],{
        width: function(a,i,m) {
            if(!m[3]||!(/^(<|>)\d+$/).test(m[3])) {return false;}
            return m[3].substr(0,1) === '>' ? 
                     $(a).width() > m[3].substr(1) : $(a).width() < m[3].substr(1);
        }
    });
     
    // Usage:
    $('div:width(>200)'); // Select all DIVs that have a width greater than 200px
     
    // Alternative usage:
    $('div:width(>200):width(<300)'); // Select all DIVs that have a width greater
                                      // than 200px but less than 300px
  • :biggerThan()

    // Check whether element is bigger than another:
    $.extend($.expr[':'],{
        biggerThan: function(a,i,m) {
            if(!m[3]) {return false;}
            return $(a).width() * $(a).height() > $(m[3]).width() * $(m[3]).height();
        }
    });
     
    // Usage:
    $('div:biggerThan(div#banner))'); // Select all DIVs that are bigger than #banner
     
    // Alternative usage: (something a little more complex)
    // (Making use of custom width() selector)
     
    // Select all DIVs with a width less than 600px but an overall
    // size greater than that of the first paragraph which has a 
    // size greater than img#header:
    $('div:width(<600):biggerThan(p:biggerThan(img#header):eq(0))');

Like I said, the possibilities are endless…

UPDATE

I’ve created a couple more examples, take a look:

  • :external

    // Check whether links are external:
    // (Only works with elements that have href):
    $.extend($.expr[':'],{
        external: function(a,i,m) {
            if(!a.href) {return false;}
            return a.hostname && a.hostname !== window.location.hostname;
        }
    });
     
    // Usage:
    $('a:external'); // Selects all anchors which link to external site/page
  • :inView

    // Check whether element is currently within the viewport:
    $.extend($.expr[':'],{
        inView: function(a) {
            var st = (document.documentElement.scrollTop || document.body.scrollTop),
                ot = $(a).offset().top,
                wh = (window.innerHeight && window.innerHeight < $(window).height()) ? window.innerHeight : $(window).height();
            return ot > st && ($(a).height() + ot) < (st + wh);
        }
    });
     
    // Usage:
    $('div:inView'); // Selects all DIV elements within the current viewport
     
    // Alternative Usage:
    if ( $('div#footer').is(':inView') ) {
        // Do stuff...
    }

UPDATE #2

I’ve created a plugin which makes it a little easier to add new ‘:’ selectors. Although, it’s not really a ‘plugin’, it’s just a function which resides under the jQuery namespace:

(function($){
    $.newSelector = function() {
        if(!arguments) { return; }
        $.extend($.expr[':'],typeof(arguments[0])==='object' ? arguments[0]
          : (function(){
              var newOb = {}; newOb[arguments[0]] = arguments[1];
              return newOb;
          })()
        );
    }
})(jQuery);

Creating a new selector with the ‘newSelector’ plugin:

// Method 1:
$.newSelector('big', function(elem){
    return $(elem).width() + $(elem).height() > 1000;
});
// Method 2:
$.newSelector({
    red: function(elem){
        return $(elem).css('color') === 'red';
    },
    yellow: function(elem){
        return $(elem).css('color') === 'yellow';
    },
    green: function(elem){
        return $(elem).css('color') === 'green';
    },
});