1

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);
Hailwood
  • 89,623
  • 107
  • 270
  • 423
  • you could call the left/right function as often as many items are in each row - would be the easiest without even taking a look at your code ;) i will do that now... – luk2302 Jul 01 '13 at 12:16
  • @luk2302, Possible solution yes, but how do you count how many items are in the row? I guess I could do `$wrapper.width() mod $element.width()` but the whole approach seems rather hacky to me. I've been looking into http://www.zehnet.de/2010/11/19/document-elementfrompoint-a-jquery-solution/ but I cannot work out how to make the point relative to the wrapper... – Hailwood Jul 01 '13 at 12:19
  • what is wrong with using just the code you provided in the link. if you just paste the code and added your own, it should work: you can get the element above via `$.elementFromPoint($(that.$active).offset().left, $(that.$active).offset().top-10);` . For the below one you would have to add 10 + the height of one item itself, but thats fixed. I don´t know how to change the active-attribute in your code, therefore i don´t provide an answer, just a comment. – luk2302 Jul 01 '13 at 12:48

2 Answers2

2

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;
}
mibbler
  • 405
  • 4
  • 12
  • This seems good, But how would you deal with the "literally" edge cases, aka, if the user is on the bottom row, and presses down, we want to select the relative item on the top row, and same for going up? – Hailwood Jul 01 '13 at 12:32
  • Updated answer with your 'edge' case. – mibbler Jul 01 '13 at 12:36
  • One more edge case, as you can see from the demo, we also have a filter box, so if the user does has filtered the items, then we need to somehow exclude those... – Hailwood Jul 01 '13 at 12:43
  • To only count the filtered elements you can do your `blocksPerRow` count and other work only on those that are visible, e.g. `$(".select-icon-list").children(":visible")`. – mibbler Jul 01 '13 at 13:02
  • so, I could do `$icons = $(".select-icon-list").children(":visible")` and then instead of using `.eq(newIndex)` use `$icons[newIndex]` but how would I pull out the currently active items index from that array? will I need to loop over the array and compare every value in there with the selected item? – Hailwood Jul 01 '13 at 13:10
  • To begin with you still want to select all of the icons with `$icons = $(".select-icon-list");`. Then you would use `$icons.eq(newIndex)`. To work with only active items, you would do `$icons.filter(":visible")`, e.g. `$icons.filter(":visible").eq(newIndex)`. – mibbler Jul 01 '13 at 13:18
  • to get index of active elem just do `$('a.selected.active').index()` – sabithpocker Jul 01 '13 at 13:18
  • But they don't have the class of active. Or am I missing something? – mibbler Jul 01 '13 at 13:22
1
    moveUp: function () {
        var that = this;
        var index = $(this).index();
        var containerWidth = parseInt( $('.select-icon-list').innerWidth(), 10);
        var iconWidth = parseInt( $('.select-icon-list > a').width(), 10);
        var noOfCols = Math.floor( containerWidth / iconWidth );
        var newIndex = ( (index - noOfCols) < 0 ) ? index : (index - noOfCols);
        var elem = $('.select-icon-list > a')[index];
    },

Cache what ever remains static.

sabithpocker
  • 15,274
  • 1
  • 42
  • 75
  • This seems good, But how would you deal with the "literally" edge cases, aka, if the user is on the bottom row, and presses down, we want to select the relative item on the top row, and same for going up? – Hailwood Jul 01 '13 at 12:31
  • i am just preventing such edge cases wit `( (index - noOfCols) < 0 ) ? index : (index - noOfCols)` and remain on same item. – sabithpocker Jul 01 '13 at 12:56