Introduction
I’m currently working on contributing the
ListSearchExtender to the Microsoft ASP.AJAX Control Toolkit. This extender lets you search incrementally within a ListBox, so that if you type "mi" when focused on the ListBox you will go to the first item that starts with "mi" rather than the first item that starts with "m" and then the first item that starts with "i" which is the default behaviour.
I have a developer account on the
CodePlex site, and I’ve downloaded the AJAX Control Toolkit source. I figured that submitting the extender would be a couple of hours of works, mainly involving cleaning up the code, creating documentation, and creating test cases , all based on an article I put together previously which showed it working in Internet Explorer.
In reality it was more like a couple of months. Admittedly I’ve had a lot of stuff on my plate (such as my day job), but I also discovered that getting the extender to work in other browsers was far more work than I’d expected. This isn’t because Firefox, Safari and Opera are bad, but rather because my JavaScript code was bad, and the various browsers differ wildly in their implementation of keyboard related events, such as the KeyPress event.
For the last couple of years I’ve been working on corporate Intranet applications. Internet Explorer is mandated throughout the corporation. Now we all know that we should be writing JavaScript code that works in all browsers even if we know
that the end-users will all have IE. This is without doubt the “right” thing to do. But it’s not free. Researching and testing for cross-browser support would have been an expensive proposition for which there really wasn’t a business case.
Excuses aside, in this article I’m going to talk through some of the issues I discovered when making the Control Extender support multiple browsers. If you are a hardcore JavaScript developer then a lot of this may be familiar. But if you are like me, and JavaScript is just one of many technologies you are using, read on to learn from my mistakes.
Event Handling
The first issue I hit was that my event handling was broken. In one specific case I’d forgotten to use the ASP.NET AJAX event wrapper object, and instead I’d used the event object directly. Or rather that is what I’d tried to do, but the (non-standard) event object doesn’t exist in browsers such as Firefox. This is why it is wrapped behind the ASP.NET event wrapper (Sys.UI.DomEvent).
Old code:
_onKeyDown : function(e) {
...
if(event.keyCode == Sys.UI.Key.backspace) {
...
}
}
New code:
_onKeyDown : function(e) {
...
if(e.keyCode == Sys.UI.Key.backspace) {
...
}
}
InnerText and InnerHtml
In my original code, when the user gives the ListBox focus I created a DIV next to the SELECT with prompt text, such as “Type to search”:
The prompt text is configurable on the server side. I assigned the prompt text to the innerText property of the DIV, since that way even if the prompt text contained special characters such as “<” they would not cause the page’s markup to be disrupted. When the user starts typing to search for an item in the ListBox I replace the DIV’s contents with the text that they have typed, so that they can see what is being searched for:
Unfortunately the innerText property is totally nonstandard and is not supported by browsers such as Firefox. So I moved to using the innerHTML property instead. Unfortunately innerHTML is also totally nonstandard, but it is supported by Firefox, Opera and Safari. The moral of the story is to carefully choose your nonstandard code.
I still had the issue of dealing with special characters such as “<”. If you set your innerHTML to '<' then the browser expects it to be the start of a tag and doesn't display it.
In the end I decided to create a TextNode DOM object rather than using innerHTML. This is standards compliant, and although it is slower than using innerHTML, in my case this really isn’t an issue.
Old code:
// Add key that was pressed to the displayed DIV
promptDiv.innerText += ch;
New code:
// Add key that was pressed to the displayed DIV
promptDiv.innerHTML += ch;
Newest code:
// Add key that was pressed to the displayed DIV
var textNode = promptDiv.firstChild;
if(!textNode) {
textNode = document.createTextNode(ch);
promptDiv.appendChild(textNode);
} else {
textNode.nodeValue = += ch;
}
Inconsistent KeyPress events
When I implemented the extender for Internet Explorer, I found that special keys, such as the backspace key and arrow keys, were passed to the KeyDown event handler, but not to the KeyPress event handler. Consequently in order to handle the backspace (so that users can delete characters from the text for which they are searching), I signed up to receive the KeyDown event and processed backspaces there.
When testing in Firefox I discovered that not only was the Backspace causing a KeyDown event, but it was also causing a KeyPress event. This totally broke my assumptions about the kind of characters that would be passed to the KeyPress event -- I'd assumed that anything I received in a KeyPress could be appended to the promptDiv.
I soon discovered that it was safest to make absolutely no assumptions
with regards to how IE, FireFox, Opera and Safari implement support for the KeyDown, KeyPress and KeyUp events. The good news is that there is a standard in this area, but the bad news is that no browser implements it.
Nevertheless it was important for me to be able to detect when a user has hit a key such as an arrow key in the KeyPress event, so that it isn't added as a control character to the promptDiv.
In the end I found I had to code special cases for most of the browsers.
FireFox
FireFox does fire a keyPress for arrow keys. In order to detect them (and ignore them) I found I could look at the underlying (raw) event's keyCode property. If it is set then a key such as an arrow key was pressed, but it is not set for normal keys such as 'a':
_isNormalChar : function(e) {
if (Sys.Browser.agent == Sys.Browser.Firefox && e.rawEvent.keyCode) {
return false;
}
...
Opera
Opera on the other hand does set the raw' event's keyCode when a keyPress occurs, but it does this whether or not the key was a normal key (such as 'a') or an arrow key. The solution for Opera was to look at the another of the underlying raw event's properties, the which property:
...
if (Sys.Browser.agent == Sys.Browser.Opera && e.rawEvent.which == 0) {
return false;
}
...
Safari
Safari was the most interesting case of all. I found that it was using very strange codes for the arrow keys, in the range of 65000. I ended up with code like this:
...
if (e.charCode < Sys.UI.Key.space || e.charCode > 60000) {
return false;
}
...
Inconsistent event cancelling
Hitting BackSpace in Internet Explorer often takes you back to the previous page in your browser's history. I am sure I'm not the only one that has tried to delete some characters from a textbox and instead had the delightful experience of accidently going back several pages in my browsing history.
When the user hits BackSpace on a ListBox (rendered as a SELECT) I definitely don't want them to go back in their browsing history. Instead I want to delete a single character from the promptDiv. In order to stop the default behaviour I call a couple of methods on the event object:
...
if(e.keyCode == Sys.UI.Key.backspace) {
e.preventDefault();
e.stopPropagation();
...
These methods invoke corresponding methods on the underlying event objects, taking into account browser differences. This works well on IE, FireFox and Safari, but Opera ignores the request to prevent the default behaviour and instead still takes the user back to the previous page.
The only workaround I found for this was to modify the BackSpace key mapping under the options menu in Opera -- not particularly difficult, but not something that all users will want to do.
I also found that despite calling preventDefault and stopPropogation in FireFox, it still sometimes changes the selected option to be the first one that starts with the character that is typed. I found that I could reset the selected option to the correct one in the KeyUp event, but nevertheless it does look a little ugly as the selected option jumps momentarily.
Sometimes things just don't work
In Safari it appears that a DropDownList (rendered as a SELECT) fires no JavaScript key events at all. None. Not one. Which kind of makes it hard to implement the ListSearchExtender. The ListBox works just fine though. Not much you can do in this kind of situation.
Summary
ASP.NET AJAX and the ASP.NET AJAX Control Toolkit do a lot of work to hide browser inconsistencies from developers, but nevertheless there are still times when you'll need to go behind the scenes and examine the raw browser objects to code around differences.
The cool thing though is that now that I've done the hard work of making the ListSearchExtender support multiple browsers, you can simply drag the extender onto your ASP.NET canvas, hook it up to a ListBox, and not have to worry about it working in IE, FireFox, Opera and Safari. Look out for it in a future release of the ASP.NET AJAX Control Toolkit.
About the author
Damian Mehers is an independent consultant based out of Geneva, Switzerland, specializing in ASP.NET/C#/SQL Server. He has worked as a software developer for almost twenty years.He blogs at http://damianblog.com/ and can be reached at damian at atadore dot com.
Please login to rate or to leave a comment.