Advanced AJAX ListBox Component v0.4
Responding to special keyboard events to improve behavior.
Introduction
In my last article, we modified our ListBox to enforce a browser compatibility issue introduced by separating out horizontal scrolling from scroll state preservation. In this article, we're going to turn our attention to the interface user, and finally make this control beta-worthy.
Background
Let's review the requirements checklist we drew out in the second article:
- It is difficult or impossible to select multiple disparate items at once by using the SHIFT and CONTROL keys because their
onkeydown
events trigger the scrollbars to be repositioned based on the first item(s) selected. We should read thekeyCode
of the event, and bypass our scrolling code for "meta" key-presses like this. - The ability to scroll through the items using a mouse wheel depends on both the browser and the version used to load the page. We would like to make mouse scrolling a configurable option supported on all target browsers.
The vertical and horizontal scrollbar positions are only preserved across postbacks when the control'sHorizontalScrollEnabled
property is explicitly set totrue
. We should be able to preserve the scroll state even whenHorizontalScrollEnabled
isfalse
.- Our event handling strategy does not fully support Firefox 1.0, because some user interactions which change the scroll position do not execute the
_onContainerScroll
event handler. Additionally, if you ever try debugging this handler, you'll notice that IE can execute it up to four times for a single mouse click or keystroke. Dragging a scrollbar causes my CPU to briefly jump to 30% or higher, depending on how fast I scroll. Updating the hidden field this many times places an unnecessary load on the client computer's processor.
The biggest issue on our plate from a usability standpoint is #1. We'll start this out intuitively, then see why we need to do some tricks in order to mimic the normal ListBox behavior.
Shock and Awe
isMetaKeyPress : function(e)
{
if (e.keyCode == 16 // SHIFT key
|| e.keyCode == 17 // CONTROL key
)
return true;
return false;
}
,
_onContainerKeyDown : function(e)
{
if (!this.isMetaKeyPress(e)) {
// previous code all goes inside this conditional block
}
}
,
This alleviates part of the CONTROL and SHIFT key problem. This will add some flavor to the control, but now let's kick it up a notch...
BAM!
isMetaKeyPress : function(e)
{
if (e.keyCode == 8 // BACKSPACE key
|| e.keyCode == 9 // TAB key
|| e.keyCode == 16 // SHIFT key
|| e.keyCode == 17 // CONTROL key
|| e.keyCode == 18 // ALT key
|| e.keyCode == 19 // PAUSEBREAK key
|| e.keyCode == 20 // CAPSLOCK key
|| e.keyCode == 27 // ESC key
|| e.keyCode == 45 // INSERT key
|| e.keyCode == 91 // WINDOWS key
|| e.keyCode == 93 // CONTEXT key
|| e.keyCode == 112 // F1 key
|| e.keyCode == 113 // F2 key
|| e.keyCode == 114 // F3 key
|| e.keyCode == 115 // F4 key
// skip F5 key, since it reloads the page
|| e.keyCode == 117 // F6 key
|| e.keyCode == 118 // F7 key
|| e.keyCode == 119 // F8 key
|| e.keyCode == 120 // F9 key
|| e.keyCode == 121 // F10 key
|| e.keyCode == 122 // F11 key
|| e.keyCode == 123 // F12 key
|| e.keyCode == 127 // DELETE key
|| e.keyCode == 144 // NUMLOCK key
|| e.keyCode == 145 // SCROLLLOCK key
)
return true;
return false;
}
,
Because a normal ASP ListBox
won't scroll during these key-presses, this is a better match for those data-entry clerks with long, slippery fingernails.
Be on Your Best Behavior
With this code, we still have some problems. When selecting a large block of items from top to bottom (using the SHIFT key), the DIV
jumps back up to the top item selected when you click the mouse. This is because the mouse click triggers the onchange
event, and the SHIFT keyCode
is not captured in that event. We can get around this by adding additional cases where the updateListBoxScrollPosition()
function should not be called.
supressAutoScroll : function(e)
{
if (this.isMetaKeyPress(e))
return true;
var selectedItems = this.getSelectedItemCount(true);
if (selectedItems > 1)
return true;
return false;
}
,
getSelectedItemCount : function(breakOnMultiple)
{
var selectedItems = 0;
for (i = 0; i<this.get_element().options.length; i++)
{
if (this.get_element().options[i].selected == true)
selectedItems++;
if (breakOnMultiple && selectedItems > 1)
break;
}
return selectedItems;
}
,
_onChange : function(e)
{
if (this.get_element() && !this.get_element().disabled)
{
if (!this.supressAutoScroll(e)) {
updateListBoxScrollPosition(this.get_elementContainer(),
this.get_element(), null);
}
// rest of the code stays the same
}
}
,
_onContainerKeyDown : function(e)
{
if (!this.supressAutoScroll(e)) {
// change condition from isMetaKeyPress
// to supressAutoScroll above
}
}
,
What we did here was add two other helper functions. We've written the getSelectedItemCount
function in a way that it can be reused by a real get_selectedItemCount
property later, if need be. For the purposes of this requirement though, we only need to know if there's more than one item selected in the ListBox
. Because of this, we pass a parameter that will cause the counting to stop and return 2 when there are multiple items selected. We do this from supressAutoScroll
so that we can combine the two cases when automatic DIV
scrolling should be suppressed:
- When
_onContainerKeyDown
is executed in response to a meta key-press, and - When
_onChange
or_onContainerKeyDown
are executed while there are multiple items selected.
All that's left is to use the supressAutoScroll
function to perform a condition check before calling the updateListBoxScrollPosition
method that causes the auto-scroll behavior in the two event handlers.
Splitting Hairs
One thing about the normal ListBox
is that users can select an item, hold the SHIFT key, then hit another key to select multiple items. Our ListBox does this too, but the normal ListBox
will jump to show the newly selected item, whereas ours does not. Also, the PAGEUP and PAGEDOWN keys don't work because the ListBox has no internal scrollbars. They act just like the HOME and END keys (which do work as intended).
Though I'm sure it's possible to imitate these behaviors, we're getting to a point of diminishing returns here. Like I said in my first article, perfectionism is a disease. The current control will be intuitive enough for the vast majority of users. It is now possible to use the SHIFT and CONTROL keys to click-select multiple items without having the scroll position go all haywire, which is good enough to check this requirement off of the list.