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 the keyCode
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's HorizontalScrollEnabled
property is explicitly set to true
. We should be able to preserve the scroll state even when HorizontalScrollEnabled
is false
.- 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
|| e.keyCode == 17
)
return true;
return false;
}
,
_onContainerKeyDown : function(e)
{
if (!this.isMetaKeyPress(e)) {
}
}
,
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
|| e.keyCode == 9
|| e.keyCode == 16
|| e.keyCode == 17
|| e.keyCode == 18
|| e.keyCode == 19
|| e.keyCode == 20
|| e.keyCode == 27
|| e.keyCode == 45
|| e.keyCode == 91
|| e.keyCode == 93
|| e.keyCode == 112
|| e.keyCode == 113
|| e.keyCode == 114
|| e.keyCode == 115
|| e.keyCode == 117
|| e.keyCode == 118
|| e.keyCode == 119
|| e.keyCode == 120
|| e.keyCode == 121
|| e.keyCode == 122
|| e.keyCode == 123
|| e.keyCode == 127
|| e.keyCode == 144
|| e.keyCode == 145
)
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);
}
}
}
,
_onContainerKeyDown : function(e)
{
if (!this.supressAutoScroll(e)) {
}
}
,
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.