Indexed Dropdown with jQuery

Did you note how difficult is to deal with highly populated dropdowns? More than 50 elements can be a real pain… Can we do something about it -specially when this “highly populated dropdown” is necessary-? Thanks to JavaScript/jQuery the answer is a big yes. How? Take a look to the demo and see which dropdown is more friendly to find… lets say: Chile.

Demo Download

Main Structure

Next image presents our target structure. input and image are the visible elements. content is our options container and index, our indexing element.

Now, lets code!

The jQuery Plugin

(function($) {

$.fn.indexedselect = function(options) {

var defaults = {
        defaultText: "Select…"
};
var opts = $.extend(defaults,options);

})(jQuery);

Since we want to customize our plugin -just a little bit by now-, we need to define options. We also need to define defaults since options is not mandatory. Finally, we “merge” both and save the result into opts.

Then, for each select element we apply…

var input = $(‘<input class="indexedselect_input" type="text"/>’);
input.css({width:$(this).outerWidth() + ‘px’,
                height:‘20px’,
                top:$(this).position().top + ‘px’,
                left:$(this).position().left + ‘px’}).
        val(opts.defaultText).
        insertAfter(this);

input.focus(function(){
        $(‘.indexedselect’).hide();
        indexedselect.show();
        content.css(‘height’, index.outerHeight() + ‘px’);
        $(input).select();
});

input.click(function(){
        this.select();
        return false;
});

input.keypress(function(){
        return false;
});

We start by creating input, the visible element, which is in charge of replacing our old dropdown and to show the current selected element.

Since our new element should behave like any dropdown element, we need to simulate this. Our first step into this matter, is to show available options when the new dropdown has the focus. To do this, we simply set the focus event of input to do exactly that.

Next two events, click and keypress, are there only for consistency -strictly only keypress event is needed-. They are in charge of avoiding any manual edition of the current selected option.

var hidden = $(‘<input type="hidden"/>’).insertAfter(input);
if($.trim($(this).attr(‘name’)) != ){
        hidden.attr(‘name’,$(this).attr(‘name’));
        $(this).attr(‘name’,)
}

Now, we need a way to receive our selected value in our server and this is hidden’s work. This element takes the name of the original select, so it will be sent as part of our form.

var img = $(‘<img class="img" src="./indexed_select.png"/>’).insertAfter(input);
img.css({position:‘absolute’,
                left:(img.position().left18) + ‘px’,
                top:(input.position().top + 1) + ‘px’});

img.mouseover(function(){
        $(this).attr(’src’,‘./indexed_select_hover.png’);
});

img.mouseout(function(){
        $(this).attr(’src’,‘./indexed_select.png’);
});

img.click(function(e){
        if(!$(indexedselect).is(‘:visible’)){
                $(‘.indexedselect’).hide();
                $(indexedselect).show();
                content.css(‘height’, indexedselect.find(‘.index’).height() + ‘px’);
                $(input).select();
        } else {
                $(‘.indexedselect’).hide();
        }
        return false;
});

Working again with consistency, img is added to be part of the dropdown element simulation. It acts like any show/hide toggle element when is clicked.

var indexedselect = $(‘<ul class="indexedselect">’ +
                                                ‘<li class="index">’ +
                                                        ‘<ul>’ +
                                                        ‘</ul>’ +
                                                ‘</li>’ +
                                                ‘<li class="content">’ +
                                                        ‘<ul>’ +
                                                        ‘</ul>’ +
                                                ‘</li>’ +
                                        ‘</ul>’).insertAfter(input);

Our options and indexes container is indexedselect. At this moment, we are only defining its internal structure.

var content = indexedselect.find(‘.content’);

content.
        css(‘width’, $(this).outerWidth() + ‘px’);

content.mouseover(function(e){
        if($(e.target).children().length == 0)
                $(e.target).addClass(‘hover’);
        return false;
});

content.mouseout(function(e){
        if($(e.target).children().length == 0)
                $(e.target).removeClass(‘hover’);
        return false;
});

content.click(function(e){
        input.val(String($.trim($(e.target).html())).replace(‘&amp;’,‘&’));
        hidden.val($(e.target).data(‘value’));
        indexedselect.hide();
        return false;
});

content is part of indexedselect and will store the options of the replaced element.

When any option is clicked, we set input equal to the visible value and hidden to the actual selected value -just like would be with any dropdown element-.

var index = indexedselect.find(‘.index’);

var iul = indexedselect.find(‘.index ul’);
var cul = indexedselect.find(‘.content ul’);

var indexes = Array();
$(this).find(‘option’).each(function(){
        var letter = String($(this).html()).substring(0,1).toLowerCase();
        if(!indexes[letter]){
                iul.append(‘<li>’ + letter + ‘</li>’);
                indexes[letter] = letter;
        }
        $(‘<li>’ + $(this).html() + ‘</li>’).
                data(‘value’,$(this).attr(‘value’)).
                appendTo(cul);
});

index.click(function(e){
        var selectedindex = String($.trim($(e.target).html())).toLowerCase();
        index.find(‘.selectedindex’).removeClass(’selectedindex’);
        content.find(‘.selectedindex’).removeClass(’selectedindex’);
        $(e.target).addClass(’selectedindex’);
        content.find(‘ul li’).each(function(){
                if(String($(this).html()).substring(0,1).toLowerCase() == selectedindex) {
                        $(this).addClass(’selectedindex’);
                        var divOffset = content.offset().top;
                        var pOffset = $(this).offset().top;
                        var pScroll = pOffset – divOffset – 1;
                        content.animate({scrollTop: ‘+=’ + pScroll + ‘px’}, 250);
                        return false;
                }
        });
        return false;
});

index.mouseover(function(e){
        if($(e.target).children().length == 0)
                $(e.target).addClass(‘hover’);
        return false;
});

index.mouseout(function(e){
        if($(e.target).children().length == 0)
                $(e.target).removeClass(‘hover’);
        return false;
});

indexedselect.css(‘top’, input.position().top + input.outerHeight() + ‘px’).
        css(‘left’, input.position().left + ‘px’).
        hide();

index is our help to highly populated dropdown elements. To initialize this element, we need to go option by option taking its first character. Once we have this character, we check whether is already included or not -this is done by checking the indexes variable-. Only if is not, we add it to the index element.

We want to avoid extra work, right? Well, since we are going option by option we also use this to initialize content. This is done by moving option’s visible html into content and linking the corresponding value by using jQuery’s data function. This way, we can set hidden to the correct selected value.

Click event triggers a searching process for the first coincidence with respect to the clicked index -so, is mandatory to order alphabetically all the option elements-. Once the first coincidence is found, we scroll down or up in order to make it visible.

indexedselect.css(‘top’, input.position().top + input.outerHeight() + ‘px’).
                                css(‘left’, input.position().left + ‘px’).
                                hide();

$(document).click(function() {
        $(‘.indexedselect’).hide();
});

$(this).css(‘visibility’,‘hidden’);

Finally, some minor things like setting initial position for our new element, setting hide function when we click outside our element and making not visible the initial dropdown -note the use of visibility instead of display. This is necessary to force the invisible element to still fill its space… try display and you will see-.

Final comments

We have made a very simple substitute for any dropdown element, but there is a lot of room for improvement. You can start with validations, for instance. You can also play with its design or reduce the number of limitations -like that all the options needs to be ordered-. Now, is up to you.

(function($) {

$.fn.indexedselect = function(options) {

        var defaults = {
                defaultText: "Select…"
        };
        var opts = $.extend(defaults,options);

        this.each(function(){
                var input = $(‘<input class="indexedselect_input" type="text"/>’);
                input.css({width:$(this).outerWidth() + ‘px’,
                                height:‘20px’,
                                top:$(this).position().top + ‘px’,
                                left:$(this).position().left + ‘px’}).
                        val(opts.defaultText).
                        insertAfter(this);

                input.focus(function(){
                        $(‘.indexedselect’).hide();
                        indexedselect.show();
                        content.css(‘height’, index.outerHeight() + ‘px’);
                        $(input).select();
                });

                input.click(function(){
                        this.select();
                        return false;
                });

                input.keypress(function(){
                        return false;
                });

                var hidden = $(‘<input type="hidden"/>’).insertAfter(input);
                if($.trim($(this).attr(‘name’)) != ){
                        hidden.attr(‘name’,$(this).attr(‘name’));
                        $(this).attr(‘name’,)
                }

                var img = $(‘<img class="img" src="./indexed_select.png"/>’).insertAfter(input);
                img.css({position:‘absolute’,
                                left:(img.position().left18) + ‘px’,
                                top:(input.position().top + 1) + ‘px’});

                img.mouseover(function(){
                        $(this).attr(’src’,‘./indexed_select_hover.png’);
                });
               
                img.mouseout(function(){
                        $(this).attr(’src’,‘./indexed_select.png’);
                });

                img.click(function(e){
                        if(!$(indexedselect).is(‘:visible’)){
                                $(‘.indexedselect’).hide();
                                $(indexedselect).show();
                                content.css(‘height’, indexedselect.find(‘.index’).height() + ‘px’);
                                $(input).select();
                        } else {
                                $(‘.indexedselect’).hide();
                        }
                        return false;
                });

                var indexedselect = $(‘<ul class="indexedselect">’ +
                                                        ‘<li class="index">’ +
                                                                ‘<ul>’ +
                                                                ‘</ul>’ +
                                                        ‘</li>’ +
                                                        ‘<li class="content">’ +
                                                                ‘<ul>’ +
                                                                ‘</ul>’ +
                                                        ‘</li>’ +
                                                ‘</ul>’).insertAfter(input);

                var content = indexedselect.find(‘.content’);

                content.
                        css(‘width’, $(this).outerWidth() + ‘px’);

                content.mouseover(function(e){
                        if($(e.target).children().length == 0)
                                $(e.target).addClass(‘hover’);
                        return false;
                });

                content.mouseout(function(e){
                        if($(e.target).children().length == 0)
                                $(e.target).removeClass(‘hover’);
                        return false;
                });

                content.click(function(e){
                        input.val(String($.trim($(e.target).html())).replace(‘&amp;’,‘&’));
                        hidden.val($(e.target).data(‘value’));
                        indexedselect.hide();
                        return false;
                });

                var index = indexedselect.find(‘.index’);

                var iul = indexedselect.find(‘.index ul’);
                var cul = indexedselect.find(‘.content ul’);

                var indexes = Array();
                $(this).find(‘option’).each(function(){
                        var letter = String($(this).html()).substring(0,1).toLowerCase();
                        if(!indexes[letter]){
                                iul.append(‘<li>’ + letter + ‘</li>’);
                                indexes[letter] = letter;
                        }
                        $(‘<li>’ + $(this).html() + ‘</li>’).
                                data(‘value’,$(this).attr(‘value’)).
                                appendTo(cul);
                });

                index.click(function(e){
                        var selectedindex = String($.trim($(e.target).html())).toLowerCase();
                        index.find(‘.selectedindex’).removeClass(’selectedindex’);
                        content.find(‘.selectedindex’).removeClass(’selectedindex’);
                        $(e.target).addClass(’selectedindex’);
                        content.find(‘ul li’).each(function(){
                                if(String($(this).html()).substring(0,1).toLowerCase() == selectedindex) {
                                        $(this).addClass(’selectedindex’);
                                        var divOffset = content.offset().top;
                                        var pOffset = $(this).offset().top;
                                        var pScroll = pOffset – divOffset – 1;
                                        content.animate({scrollTop: ‘+=’ + pScroll + ‘px’}, 250);
                                        return false;
                                }
                        });
                        return false;
                });

                index.mouseover(function(e){
                        if($(e.target).children().length == 0)
                                $(e.target).addClass(‘hover’);
                        return false;
                });

                index.mouseout(function(e){
                        if($(e.target).children().length == 0)
                                $(e.target).removeClass(‘hover’);
                        return false;
                });

                indexedselect.css(‘top’, input.position().top + input.outerHeight() + ‘px’).
                        css(‘left’, input.position().left + ‘px’).
                        hide();

                $(document).click(function() {
                        $(‘.indexedselect’).hide();
                });

                $(this).css(‘visibility’,‘hidden’);
        });
};

})(jQuery);

Category: jQuery, web design, web development

Tagged: , , ,

14 Responses

  1. JohnGalt says:

    This is a great jQuery tutorial. Adding a link at http://tutlist.com

  2. [...] plugin jQuery para transformar cualquier Dropdown en uno indexadodevness.com/2009/07/indexed-dropdown-with-jquery/ por dmoena hace pocos segundos [...]

  3. Miguel says:

    Liked the tutorial, as for using this… in the first dropdown i can just type ‘chi’ and there’s chile…way faster than using any index. This index + keyboard support ? :)

  4. David says:

    I like the idea. I think from a user interaction perspective, the feature would be a bit more discoverable if you automatically selected “A” after clicking on the dropdown. That’s a way to make it stand out a bit and show that it’s interactive.

  5. MediaWrox says:

    Why not add keybord support too :)

  6. [...] Dropdown ordenado alfabeticamente con jQuery, un complemento muy funcional para agregarle a tu sitio web si estás pensando en listar muchos items dentro de un select. Pueden ver un demo aquí. 0 # [...]

  7. dmoena says:

    @Miguel good point. I forgot that possibility

    @David excellent idea. thanks!

    @MediaWrox why not, right? thanks to all your comments, I will

  8. Davide says:

    I believe most browsers already have keyboard support for this built in. Click on the dropdown, hit a key on your keyboard and it will jump to that letter.

  9. [...] jQuery – Indexed Dropdown with jQuery – Tweeted by Elijah Manor [...]

  10. [...] jQuery – Indexed Dropdown with jQuery – Tweeted by Elijah Manor [...]

  11. Rodreego says:

    Very cool!!! never think in something like this

  12. [...] In: JQuery plugins 9 Jul 2009 Go to Source [...]

  13. TalonBauer says:

    It might be a good idea to edit this to index DIV or LI elements and scroll the page to those entries. Much like the iPhone / iPod contacts or music listings.

Leave a Reply