Introduction
In my last article, we added a server control property that allows us to take control over how the ListBox responds to mouse wheel events. In this article, we're going to take care of some anomalies, and fully enforce browser compatibility for the mouse wheel.
Background
Once again, let's review the requirements checklist we drew out in the second article. Now that we're getting down to the nitty gritty, we're going to break #4 out into two separate requirements:
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 the 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 scroll state event 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. - If you ever try debugging the
_onContainerScroll
handler, you'll notice 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.
Although it may seem that we've squashed #2, those of you who are keen on Firefox may have noticed the following quirks:
- In FF1, when
HorizontalScrollEnabled
== true
and MouseWheelScroll
== Enforce
, the ListBox does not respond to the mouse wheel while the cursor is positioned over the scrollbars. - In FF1.5 and FF2, when
HorizontalScrollEnabled
== true
and MouseWheelScroll
== Prevent
, the ListBox does respond to the mouse wheel while the cursor is positioned over the scrollbars. - In FF1, when the mouse wheel is used to scroll, the scroll state is not saved to the hidden field. Incidentally, this is the last outstanding issue for requirement #4.
The first two quirks surfaced because we didn't handle the DIV
container's DOMMouseScroll
event. We should tackle that issue first because it will ultimately affect the third.
Copy and Paste
Here is the reason why we gave the DIV
a _thisPrototype
field pointing to the prototype, in the last article. Now, we already have access to the client control's fields that are needed to compute the new scroll position when the DIV
receives the onscroll
event. Granted, I could've written the _onContainerMouseWheel
code differently than the _onMouseWheel
code, since the this
reference will always point to the DIV
. However, writing it this way gives us an opportunity to consolidate the code into a helper function later.
if (this.get_element().onmousewheel === undefined)
{
this.get_element().addEventListener(
'DOMMouseScroll', this._onMouseWheel, false);
if (this.get_requiresContainerScroll())
{
this.get_elementContainer().addEventListener(
'DOMMouseScroll', this._onContainerMouseWheel, false);
}
}
_onContainerMouseWheel : function(e)
{
var _this = this._thisPrototype;
if (_this.get_mouseWheelScroll() == false)
{
e.preventDefault();
return false;
}
else if (_this.get_mouseWheelScroll() == true)
{
var listBox = _this.get_element();
var container = this;
var scrollingElement = this;
var direction = (e.detail > 1) ? 1 : -1;
var stepSize = scrollingElement.scrollHeight / listBox.options.length;
var newScrollTop = scrollingElement.scrollTop + (stepSize * direction);
scrollingElement.scrollTop = newScrollTop;
e.preventDefault();
return false;
}
}
,
All of our target Firefox versions will now fully comply with the control's MouseWheelScroll
setting when the cursor is positioned over the scrollbars. Congratulations, we can now check off requirement #2!
Home Stretch
Since that was so easy, let's do something a little more challenging. By default, Firefox 1.0 does not fire an onscroll
event for either the ListBox or the DIV
container when the mouse wheel is used to scroll. This means the hidden field isn't updated with the latest positioning information. We can coax it into doing this, but first, let's examine the scenarios where it's necessary.
Remember, the default mouse wheel behavior for FF1 is to allow it to scroll the ListBox when horizontal scrolling is disabled and the ListBox has focus. This means that we'll have to register the _onMouseWheel
handler even when MouseWheelScroll
is NotSet
(a.k.a. _mouseWheelScroll
== null
).
_initializeEvents : function()
{
if (this.get_mouseWheelScroll() != null)
{
}
else if (!this.get_requiresContainerScroll()
&& this.get_scrollStateEnabled()
&& this.get_element().onmousewheel === undefined)
{
this.get_element().addEventListener(
'DOMMouseScroll', this._onMouseWheel, false);
}
}
,
Now, in the _onMouseWheel
handler, we can try to handle the case where we need to update the scroll state in Firefox when horizontal scrolling is disabled, scroll state is enabled, and MouseWheelScroll
is NotSet
:
_onMouseWheel : function(e)
{
var _this = this._thisPrototype
if (_this === undefined)
_this = this;
if (_this.get_mouseWheelScroll() == false)
{
}
else if (_this.get_mouseWheelScroll() == true)
{
}
else if (!_this.get_requiresContainerScroll()
&& _this.get_scrollStateEnabled()
&& _this.get_element().onmousewheel === undefined)
{
_this._updateScrollState();
}
}
,
I have to admit, it took me a little while to figure out why the above code wouldn't work. I kicked myself when I found out that this._thisPrototype
was returning undefined. I forgot that in _initializeUI()
, _thisPrototype
is only defined when MouseWheelScroll
is either Enforce
or Prevent
. We have to remove the conditional check in that function so that we can have access to the DIV
's _thisPrototype
when MouseWheelScroll
is NotSet
. However, in the cases where this.get_elementContainer()
returns null
, we can't add a field to it.
_initializeUI : function()
{
var listBox = this.get_element();
var container = this.get_elementContainer();
listBox._thisPrototype = this;
if (container != null)
{
container._thisPrototype = this;
}
}
,
Slide On In
The other case where we want to jimmy FF1 into doing the same thing is when MouseWheelScroll
equals Enforce
, but we have to do it in both the DIV
's and in the ListBox's mouse wheel handlers. The hidden field must be updated near the end of the conditional block, just after the scrollTop
property is set but before the return false
line:
_onContainerMouseWheel : function(e)
{
var _this = this._thisPrototype;
if (_this.get_mouseWheelScroll() == false)
{
}
else if (_this.get_mouseWheelScroll() == true)
{
scrollingElement.scrollTop = newScrollTop;
if (_this.get_scrollStateEnabled())
{
_this._updateScrollState();
}
e.preventDefault();
return false;
}
}
,
_onMouseWheel : function(e)
{
var _this = this._thisPrototype
if (_this === undefined)
_this = this;
if (_this.get_mouseWheelScroll() == false)
{
}
else if (_this.get_mouseWheelScroll() == true)
{
scrollingElement.scrollTop = newScrollTop;
if (this._thisPrototype != undefined
&& _this.get_scrollStateEnabled())
{
_this._updateScrollState();
}
e.preventDefault();
return false;
}
else if (!_this.get_requiresContainerScroll()
&& _this.get_scrollStateEnabled()
&& _this.get_element().onmousewheel === undefined)
{
}
}
,
...And for the first time in six articles, we can now say that our ListBox fully supports Firefox 1.0.
The Big Picture
No, I'm not going to write an article on how to deal with the fifth requirement. It would be boring, and no matter how inefficient the code currently is, it at least exhibits the behaviors we set out to achieve. Some of you may be sitting there, reading this, going through the source and demo, thinking to yourself, "Man, this guy wrote over a thousand lines of code for a control that I'll only use on one or two of my forms tops." And, you're right. My only answer is, if you think this is a lot of code, you should check out the ASP.NET AJAX Control Toolkit source. I actually added even more code to satisfy the fifth requirement for my production version of this control, but I'll leave that as an exercise for you to figure out on your own.
As an application specialist, my top priority is the users of the systems I create. Making things easier for them to do is what I do. Whenever possible though, I want to make things easier for myself as well. ASP.NET WebControls are great, and even though ASP.NET 2.0 added a lot of client functionalities to validators, buttons, etc., they're still, first and foremost, server controls. Meeting and exceeding user demands in web applications requires us to go beyond the server, into the client capabilities of the browsers they ultimately use.
Like I said in my first article, I'm Java and J2EE certified... but I must applaud Microsoft for this framework. Integrating a server control with configurable, cross-browser client capabilities opens a lot of opportunities to improve web application user interfaces quite rapidly. I wanted to learn even more about it, and they say the best way to learn something is to teach it to someone else. Hopefully, you learned as much as I did :)