John Livermore
John Livermore

Reputation: 31333

Navigating a list with keystrokes

I am creating a list of items that I want the user to be able to interact with using the keyboard. So something like this...

<ul contenteditable="true">
    <li>Item 1</li>
    <li>Item 2</li>
    <li>Item 3</li>
</ul>

My understanding is in order to capture keydown/keyup events, I have to set the contenteditable attribute. This works, but now the contents of the li's are editable which I don't want. I simply need to capture keydown/keyup events.

How can I capture these events without making the content editable?

EDIT

Thanks to jumpingcode, the following works to keep it read-only...

$('ul').on('click', 'li', function (e) {
    e.preventDefault();
});

... but I am still left with a blinking cursor on the li. How do I get rid of that?

EDIT

Started bounty, need the following updates to the preventDefault answer below.

  1. How to get rid of blinking cursor?
  2. How to make non-selectable? I really only want to capture keydown events! If I can accomplish that without setting contenteditable that would be preferable.
  3. I can capture keydown at the document level, but then the question becomes where did the keydown eminate from? There could be several plugins in play, all needing to respond to the same event, but only when they are the active scope.

Upvotes: 3

Views: 159

Answers (2)

Carlos
Carlos

Reputation: 5072

As you said, you do not need to edit the list items, so there is no reason to use contenteditable.

What you need is an index that corresponds to the current active item of the list and functions for increment and decrement that index when an arrow is pressed. Here is a constructor function that implements the functionality you need:

var NavigableList = function (ul, selectedClass, activeClass) {
    var list   = null,
        index  = 0,
        walk   = function (step) {
            var walked = true;
            index += step;
            if (index < 0 || index === list.length) {
                index -= step;
                walked = false;
            }
            return walked;
        },
        active = false;
    this.active = function (state) {
        if (state !== active) {
            active = state;
            if (active) {
                ul.addClass(activeClass);
            } else {
                ul.removeClass(activeClass);
            }
        }
    };
    this.isActive = function () {
        return active;
    };
    this.down = function () {
        var previous = index,
            changed  = false;
        if (walk(1)) {
            $(list[previous]).removeClass(selectedClass);
            $(list[index]).addClass(selectedClass);
            changed = true;
        }
        return changed;
    };
    this.up = function () {
        var previous = index,
            changed  = false;
        if (walk(-1)) {
            $(list[previous]).removeClass(selectedClass);
            $(list[index]).addClass(selectedClass);
            changed = true;
        }
        return changed;
    };
    this.currentIndex = function () {
        return index;
    };
    this.currentElementx = function () {
        return $(list[index]);
    };
    ul = $(ul);
    list = ul.find('>li');
    selectedClass = selectedClass || 'current';
    activeClass = activeClass || 'active';
    $(ul).click(function (e) {
        this.active(true);
        NavigableList.activeList = this;
    }.bind(this));
    $(document).keydown(function(e) {
        var event = $.Event('change');
        if (this.isActive() && e.keyCode === 40) {
            if (this.down()) {
                event.selected = $(list[index]);
                event.index = index;
                ul.trigger(event);
            }
        } else if (this.isActive() && e.keyCode === 38) {
            if (this.up()) {
                event.selected = $(list[index]);
                event.index = index;
                ul.trigger(event);
            }
        }
    }.bind(this));
    $(list[index]).addClass(selectedClass);
};

Using this is quite easy:

var list = new NavigableList($('#list'), 'current', 'active'),
    list2 = new NavigableList($('#list2'));
$(document).click(function (e) {
    if (NavigableList.activeList) {
        switch (NavigableList.activeList) {
            case list:
                list2.active(false);
                break;
            case list2:
                list.active(false);
                break;
        }
    } else {
        list.active(false);
        list2.active(false);
    }
    NavigableList.activeList = null;
});

Also, I implemented a change event on the list that you can use this way:

$('#list').change(function (e) {
    console.log('List. Selected: ' + e.index);
});
$('#list2').change(function (e) {
    console.log('List 2. Selected: ' + e.index);
});

For cross-browser compatibility you'll need this before all Javascript code:

if (!Function.prototype.bind) {
  Function.prototype.bind = function (oThis) {
    if (typeof this !== "function") {
      throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");
    }
    var aArgs = Array.prototype.slice.call(arguments, 1), 
        fToBind = this, 
        fNOP = function () {},
        fBound = function () {
          return fToBind.apply(this instanceof fNOP && oThis
                                 ? this
                                 : oThis,
                               aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
  };
}

Here is a working demo.

Upvotes: 5

usernolongerregistered
usernolongerregistered

Reputation: 390

Try this.

<ul contenteditable="true">
   <li tabindex="1">Item 1</li>
   <li tabindex="2">Item 2</li>
   <li tabindex="3">Item 3</li>
</ul>

Tabindex will set the tab order of elements in your HTML.

Upvotes: 0

Related Questions