Up/Down/Left/Right keyboard navigation with jQuery?

Hailwood picture Hailwood · Jul 1, 2013 · Viewed 14.1k times · Source

I have a list of div's all with a set and equal height/width that are float:left so they sit next to each other and fold under if that parent is smaller than the combined with of the items.

Pretty standard. This is to create a list of the twitter bootstrap icons, it gives something like this: enter image description here

I have added next/previous keyboard navigation using the code below, however you will notice that the up/down arrow keys are mapped to call the left/right functions. What I have no idea how to do is to actually do the up/down navigation?

JsFiddle

(function ($) {
    $.widget("ui.iconSelect", {

        // default options
        options: {

        },

        $select: null,

        $wrapper: null,

        $list: null,

        $filter: null,

        $active: null,

        icons: {},

        keys: {
            left: 37,
            up: 38,
            right: 39,
            down: 40

        },

        //initialization function
        _create: function () {

            var that = this;
            that.$select = that.element;

            that.$wrapper = $('<div class="select-icon" tabindex="0"></div>');
            that.$filter = $('<input class="span12" tabindex="-1" placeholder="Filter by class name..."/>').appendTo(that.$wrapper);
            that.$list = $('<div class="select-icon-list"></div>').appendTo(that.$wrapper);


            //build the list of icons
            that.element.find('option').each(function () {
                var $option = $(this);
                var icon = $option.val();

                that.icons[icon] = $('<a data-class="' + icon + '"><i class="icon ' + icon + '"></i></a>');

                if ($option.is(':selected')) {
                    that.icons[icon].addClass('selected active');
                }

                that.$list.append(that.icons[icon]);
            });

            that.$wrapper.insertBefore(that.$select);
            that.$select.addClass('hide');



            that._setupArrowKeysHandler();
            that._setupClickHandler();
            that._setupFilter();
            that.focus('selected');
        },

        focus: function (type) {
            var that = this;
            if (that.$active === null || that.$active.length == 0) {
                if (type == 'first') {
                    that.$active = that.$list.find('a:visible:first');
                } else if (type == 'last') {
                    that.$active = that.$list.find('a:visible:last');
                } else if (type == 'selected') {
                    that.$active = that.$list.find('a.selected:visible:first');
                    that.focus('first');
                }
            }
            that.$active.addClass('active');
            var toScroll = ((that.$list.scrollTop() + that.$active.position().top)-that.$list.height()/2)+that.$active.height()/2;
            //that.$list.scrollTop((that.$list.scrollTop() + top)-that.$list.height()/2);
            that.$list.stop(true).animate({
                scrollTop: toScroll,
                queue: false,
                easing: 'linear'
            }, 200);

            if (type === 'selected') {
                return false;
            }

            that.$select.val(that.$active.data('class'));
            that.$select.trigger('change');

        },

        _setupArrowKeysHandler: function () {
            var that = this;

            that.$wrapper.on('keydown', function (e) {
                switch (e.which) {
                    case that.keys.left:
                        that.moveLeft();
                        break;
                    case that.keys.up:
                        that.moveUp();
                        break;
                    case that.keys.right:
                        that.moveRight();
                        break;
                    case that.keys.down:
                        that.moveDown();
                        break;
                    case 16:
                        return true;
                    case 9:
                        return true;
                    break;
                    default:
                        that.$filter.focus();
                        return true;
                }
                return false;
            });
        },

        _setupFilter: function(){
            var that = this;

            that.$filter.on('keydown keyup keypress paste cut change', function(e){
                that.filter(that.$filter.val());
            });
        },

        _setupClickHandler: function () {
            var that = this;
            that.$list.on('click', 'a', function () {
                that.$wrapper.focus();
                that.$active.removeClass('active');
                that.$active = $(this);
                that.focus('first');
            });
        },

        moveUp: function () {
            var that = this;
            return that.moveLeft();
        },

        moveDown: function () {
            var that = this;
            return that.moveRight();
        },

        moveLeft: function () {
            var that = this;
            that.$active.removeClass('active');
            that.$active = that.$active.prevAll(':visible:first');
            that.focus('last');
            return false;
        },

        moveRight: function () {
            var that = this;
            that.$active.removeClass('active');
            that.$active = that.$active.nextAll(':visible:first');
            that.focus('first');
            return false;
        },

        filter: function(word){
            var that = this;
            var regexp = new RegExp(word.toLowerCase());
            var found = false;
            $.each(that.icons, function(i, $v){
                found = regexp.test(i);
                if(found && !$v.is(':visible')){
                    $v.show();
                } else if(!found && $v.is(':visible')){
                    $v.hide();
                }
            });
        }

    });
})(jQuery);

Answer

mibbler picture mibbler · Jul 1, 2013

Perhaps something like this: http://jsfiddle.net/QFzCY/

var blocksPerRow = 4;

$("body").on("keydown", function(e){
    var thisIndex = $(".selected").index();
    var newIndex = null;
    if(e.keyCode === 38) {
        // up
       newIndex = thisIndex - blocksPerRow;
    }
    else if(e.keyCode === 40) {
        // down
        newIndex = thisIndex + blocksPerRow;       
    }
    if(newIndex !== null) { 
        $(".test").eq(newIndex).addClass("selected").siblings().removeClass("selected");   
    }    
 });

Basically, you set how many items there are in a row and then find the current index and subtract or add that amount to select the next element via the new index.

If you need to know how many blocks per row there are, you could do this:

var offset = null;
var blocksPerRow = 0;
$(".test").each(function(){
    if(offset === null) {
        offset = $(this).offset().top;
    }
    else if($(this).offset().top !== offset) {
        return false;
    }
    blocksPerRow++;
});

To deal with your 'edge' cases, you could do:

if(newIndex >= $(".test").length) {
    newIndex = $(".test").length - newIndex;
}