Sniffing Internet Explorer via JavaScript

I’ve been reviewing bug submissions for the SWFObject project and was reminded of a big problem with SWFObject 2.2: the JavaScript technique it uses for detecting Internet Explorer does not work in Internet Explorer 9.

SWFObject 2.2 currently uses an IE detection technique proposed by Andrea Giammarchi in 2009:


var isIE = !+"v1";

It’s a hack that relied on Internet Explorer’s unique handling of vertical spaces (v). Now that it has stopped working, I decided to take a quick look at what others are doing.

One of my favorite approaches is Dean Edwards’ “sniff” technique, which takes advantage of Microsoft’s conditional compilation.


var isIE = /*@cc_on!@*/!1;

It’s 4 years old, but still works like a charm. The only problem is that some JavaScript compressors and optimizers (including YUI Compressor and Google Closure) have a hard time with the inline comment and strip it out. It makes the code difficult to maintain, because it requires editing post-compression.

One way to get around the compressor issue (as pointed out by a commenter on Dean Edwards’ post) is to wrap the conditional compilation statement in an eval() function:


var isIE = eval("/*@cc_on!@*/!1");

Since eval() is evil, I won’t use this approach.

One of the most long-standing methods of detecting Internet Explorer is to examine the userAgent string:


var isIE = /msie/gi.test(navigator.userAgent);

However, this isn’t foolproof, as most browsers allow you to change the userAgent string at will. For example, I changed Safari’s userAgent string to report itself as Internet Explorer 8, and /msie/gi.test(navigator.userAgent); returned true!

So far, the only test I’ve found that seems to fit the bill is the old navigator.appName test:


var isIE = navigator.appName === 'Microsoft Internet Explorer';

Is it perfect? Not a chance, but it has a lot of upside: it works in IE9, doesn’t get damaged when compressed, doesn’t rely on any hacky tricks (as fun as they might be), and isn’t affected by spoofed userAgent strings.

What are the Frameworks using?

jQuery uses userAgent sniffing, and includes a warning that it’s unreliable. MooTools uses a combination of properties from the navigator object, including navigator.userAgent and navigator.platform. It’s interesting to note that MooTools previously used feature detection to infer the browser brand, but changes introduced by Firefox 3.6 prompted the MooTools team to switch to a userAgent-based detection method instead. (Nicholas Zakas wrote an interesting blog post about MooTools’ prior detection technique.)
Dustin Diaz’s Bowser detection script uses userAgent exclusively.

Why sniff in the first place?

Before you get down on me for even talking about sniffing, relax, I agree with you. I much prefer feature detection to browser sniffing; this is especially important with our quickly changing browser landscape: HTML5, web storage, geolocation, etc. (Modernizr is a great tool for modern feature detection.)

But Internet Explorer always makes us do things we’re not quite comfortable doing. Sometimes it’s not very easy to detect IE’s support for a given issue. For example, IE is notorious for not allowing JavaScript developers to set the name attribute of form elements via setAttribute; it requires including the name in the createElement invocation. Craziness!

So we carry on. I use feature detection for just about everything, but once in a while I need a global “are you Internet Explorer?”, and browser sniffing fits the bill.

Providing the same UI across browsers

As a web designer and e-learning course developer, I often grapple with the notion of making my products appear the same in every browser. I’m not opposed to the idea that you should make your work look (almost) identical in every browser. After all, you’re promoting a specific design aesthetic and the last thing you want is for a browser to dictate what you can and can’t do, especially browsers you don’t even like. (I’m looking at you, IE6.)

But it’s never as simple as we’d like. Internet Explorer has that crazy CSS box model. Older versions of Firefox and Safari don’t support border-radius or RGBa.  Mac browsers use completely different scrollbars and form elements than Windows and Linux browsers. For that matter, scrollbars and form elements in Windows 2000 look completely different than XP, Vista, and Windows 7.

This is one of the reasons Flash has become so popular — it allows designers to standardize their RIA‘s UI elements across browsers.

The latest salvo in the war for controlling your browser’s look and feel is Jilion’s SublimeVideo, an HTML5 video playback system that ensures your video has the same controller across browsers. It bypasses the browser’s built-in controller in favor of Jilion’s (very slick) custom controller. Jilion’s blog states (emphasis mine):

We aim at delivering a modern and simple video-embedding solution for web developers that provides the same user experience and uniform UI across browsers.

Jilion’s work comes from the same school of thought that led designers/developers to change the look and feel of other built-in browser components such as radio buttons and checkboxes. (Guilty!)

While looking at Jilion’s fine handiwork, I was struck by a thought: If a person uses a particular browser regularly, they will be accustomed to that browser’s native controls — whether it’s for video or drop-down menus in forms — and might be thrown off by the custom controls. Maybe it isn’t such a good idea to create a homogeneous video controller for all browsers.

After all, why does a Firefox user need to have the same experience as a Chrome user? Why does an Opera user need to have the same experience as an Internet Explorer or Safari user? These people have probably never seen what a webpage looks like in a different browser, anyway. What benefit do site visitors really receive from “a uniform UI across browsers”?

If you change the default controls to match the look and feel of something your visitor has never seen before, you run the risk of creating confusion, distrust, or alienation. Even worse, if the controls are poorly made or conceived — and many are — you might make your site less usable. A cardinal sin.

The more I think about it, the real beneficiaries of a uniform UI across browsers aren’t the site visitors, but rather the designers who demand artistic control and the clients who insist the product looks the same everywhere, without understanding that it’s okay (even expected) to have some differences.

Personally, I realized 100% uniformity is an unnecessary hardship when I began adopting HTML5 and CSS3 features into pipwerks.com last year. These new features are still not supported in all browsers, and essentially forced me to give up IE6 support. (pipwerks.com uses Andy Clarke’s handy Universal IE6 stylesheet for IE6.)

I’ve come to grips with the notion that my site’s design won’t look the same in Opera 9 or IE7/8 because those browsers don’t support border-radius. My design is simple enough that the general impression is preserved across browsers, but people who use certain browsers will see a slightly less refined product. This is a-ok with me.

It should also be noted that growth in the mobile market appears to be shifting perceptions about consistency between browsers, too. Most major sites have a “mobile edition” which will look quite different than the standard website, and is most likely geared to look great on an iPhone and so-so in all other mobile devices.

HTML5 Video, minus Ogg

As you’ve probably read somewhere on the interwebs, HTML5 is bringing native video support to browsers. This will enable us to embed a video file directly in our HTML, much like a SWF or image.

Background

You may have also heard that there’s currently a big controversy over what kind of video files will be supported. The defacto standard is MP4/H.264, which is used by Adobe in their Flash video format, and by huge media sites like YouTube. Mozilla, the makers of Firefox, refuse to support the MP4/H.264 standard because it isn’t open-source and free from licensing constraints.

Turns out H.264 is not public domain. Although the company that owns the H.264 patent has temporarily agreed to waive royalty fees for the next decade or so, they reserve the right to charge fees later on. Mozilla says no way, we will only support a video format that is free from licensing issues and has no patent holders (because patent holders can decide to sue some day). Mozilla supports the completely free/open-source Ogg format.

Apple and Adobe, already knee-deep in MP4/H.264 with their Quicktime and Flash video products, vow to press on with H.264.  Google also supports H.264 because YouTube relies on it, and because Google’s new Chrome browser is based on the WebKit project, which has Apple as a main code contributor. In case you haven’t noticed, Apple, Adobe and Google have pretty much cornered the internet video market the past few years, so if they’re throwing their support behind H.264, you can count on it being around for a while. Not to mention that many mobile devices, including the iPhone and most Android phones, have hardware that is designed specifically to support H.264 video, enabling smoother playback and less battery drain than non-dedicated hardware.

(For what it’s worth, Opera is in agreement with Mozilla and supports Ogg. However, not many people seem to pay attention to Opera these days, so they don’t appear to have much influence in this race. Microsoft has endorsed H.264 with it’s upcoming IE9 browser, but it won’t be available for some time.)

The problem

Firefox and Opera are essentially forcing websites to offer two versions of each video: an Ogg version and an MP4 version. In my opinion — and the opinion of many others — this simply will not do. Providing two different video files is not realistic, Ogg’s quality is inferior to H.264, and many computers and mobile devices have direct hardware support for H.264 but not Ogg. In reality, without MP4 support, HTML5 video is rendered useless for most site developers in Firefox and Opera.

The most logical workaround is to code <video> elements to work for MP4 and have a Flash Player-based fallback for older browsers and browsers that only support Ogg. Since the <video> element is designed to allow for fallback content, just like the <object> element, we can do this:


<video id="myvideo" width="480" height="360" controls>
    <source src="/video/file.m4v" type="video/mp4"></source>
    <object data="flash-video-player.swf" type="application/x-shockwave-flash" width="480" height="360">  
        <param value="flash-video-player.swf" name="movie"/>  
        <param value="file=/video/file.m4v" name="flashvars"/>
    </object>
</video>

This works fine in Safari, Chrome, Internet Explorer and older versions of Firefox and Opera that don’t support the <video> element. However, Firefox 3.6 and Opera 10.5 do something very irritating: even though they KNOW their <video> doesn’t support “video/MP4”, they load the <video> element anyway. Which is … like … OMG so duh … because the video can’t possibly play!

If the <video> element is loaded, Firefox and Opera will never load the fallback <object> containing the Flash-based fallback.

Because this behavior cannot be fixed with markup, we’re forced find a scripted workaround (notice that we haven’t used a single bit of JavaScript yet). Thankfully, there’s a pretty straightforward solution: Delete the <video> element in Firefox and Opera.

A Solution

Here’s a simple script that will detect whether HTML 5 video is supported, and if it is, will check to see if MP4 is supported. If HTML5 video is supported but MP4 is NOT supported, the script deletes the specified <video> element but leaves the Flash fallback in its place.


var detectVideoSupport = function (){
    var detect = document.createElement('video') || false;
    this.html5 = detect && typeof detect.canPlayType !== "undefined";
    this.mp4 = this.html5 && (detect.canPlayType("video/mp4") === "maybe" || detect.canPlayType("video/mp4") === "probably");
    this.ogg = this.html5 && (detect.canPlayType("video/ogg") === "maybe" || detect.canPlayType("video/ogg") === "probably");
    return this;
};

var replaceVideoWithObject = function (video_id){    
    if(!video_id){ return false; }
    var video = document.getElementById(video_id);
    if(video){
        var obj = video.getElementsByTagName("object")[0];
        if(obj){
            var obj_copy = obj.cloneNode(true);
            video.parentNode.insertBefore(obj_copy, video);
            video.parentNode.removeChild(video);
        }
    }
};

window.onload = function (){
    var video = detectVideoSupport();
    //Both Opera and Firefox support OGG but lack MP4 support
    if(video.ogg && !video.mp4){
        replaceVideoWithObject("myvideo");
    }
};
</script>

Functioning demo.

A few notes

Tested successfully in:

  • Windows XP: Firefox 3.0, Firefox 3.5.8, Internet Explorer 6, Internet Explorer 8, Google Chrome 4.1.2
  • Windows Vista: Internet Explorer 7
  • Mac OS X (10.6): Firefox 3.6, Safari 4.01, Chrome 5 (beta), Opera 10.1, Opera 10.5b

(Note: IE6, IE7 & IE8 give an unexplained “object required” error in the demo page, but everything works fine. I will investigate as time permits.)

This demo uses the JW Media Player as the Flash fallback video player, but you can use any Flash-based video player on your page.

This demo doesn’t do any Flash Player version detection to ensure the visitor has a supported version of Flash Player. If you need to add Flash Player version detection, you can use SWFObject to embed your Flash file.

Update April 3, 2010: This post was updated to add Opera 10.5 to the list of non-behaving browsers and remove Firefox user agent sniffing.

Viewing PDFs in a Browser on a Mac

As a Mac user, one of the more annoying issues I frequently encounter is funky PDF handling in Firefox and Safari. For instance:

  • Adobe doesn’t make a version of Adobe Reader that’s compatible with Firefox on Mac OS X
  • Adobe Reader is only supported in 32-bit versions of Safari on OS X (Snow Leopard ships with a 64-bit version of Safari)
  • Safari has built-in handling of PDFs, but if Adobe Reader is installed — whether it’s actually working in Safari or not — it will turn off Safari’s native PDF handling by default.

Here are some things you can do to get PDFs to display in your browser(s).

Restore Safari’s built-in PDF handling

If your Safari browser isn’t using OS X’s native PDF handling, Adobe Reader may be overriding it. Here’s how to fix it:

  1. Launch Adobe Reader
  2. Go to Preferences > Internet
  3. Deselect “Display PDF in browser using…”
  4. Click OK

Enable PDF support in Firefox

Mozilla has a handy reference for this topic.  Here’s what they say:

Adobe does not yet maintain a plugin for viewing PDF files within Firefox for computers with Mac OS X. To view PDFs in Firefox:

I’ve installed the Firefox PDF Plugin on my Mac (Firefox 3.6), and it works great. It uses the Mac’s built-in PDF handling, so it’s a very small plugin runs very quick.

Caveats and an editorial

Using the Mac’s built-in PDF support means you won’t be able to take advantage of some of Adobe Reader’s new features, such as scripting, portfolios, and SWF support.

I don’t know about you, but I prefer to use the PDF format for its traditional purpose: reading print-based documents online.  Plus, Adobe Reader currently requires 290MB of space on the hard drive. I can live without the new features and would prefer to use my 290MB of space for other things.

In general, I recommend avoiding Adobe Reader on a Mac because of its poor support in Firefox and Safari, its incredible bloat, and the seemingly daily announcements of major security vulnerabilities. Most of the security issues have been directly related to the scripting functionality and other new features. If a lite version of Adobe Reader were offered — one that removes the bloat, eschews these new features, and simply lets us view standard PDF documents — I’d be more than happy to use it.

Shameless plug

Need to embed a PDF in an HTML page? Try PDFObject. It’s free, tiny, and works much like SWFObject (the two projects are unrelated).

Eolas is at it again

Eolas Technologies is a company that manages licensing for patents.

Eolas seeks to return value to its shareholders by commercializing these technologies through strategic alliances, licensing and spin offs. (source)

The problem is that Eolas is generally regarded as a bully trying to enforce a patent (Patent 5,838,906) that many experts feel should not have been issued.

The 906 patent, received in 1998 by the University and licensed exclusively to Eolas, describes ways that a Web browser can use external applications. (source)

(The US Patent and Trademark Office has made notoriously bad decisions relating to Internet technologies, causing many to wonder if they truly even understand the patents they’ve granted. Blackboard vs Desire2Learn is a great example; Blackboard acted much like Eolas, and their patents were eventually nullified by the PTO.)

Eolas sued Microsoft in 1999 for violating Patent 5,838,906, and in a rare show of solidarity, the Web and Open Source communities — normally very anti-Microsoft groups — rallied to Microsoft’s side. This included Sir Tim Berners-Lee, the founder of the Internet. They pleaded with Eolas to release the patent into public domain for the greater good of the global community.

Anybody in the browser field that studies the technology will see that its a very fundamental and basic patent to the World Wide Web. (source)

Despite the overwhelming negative response from the Web and Open Source communities, Eolas would not relent and forced Microsoft to modify Internet Explorer in a way that broke functionality on over hundreds of millions of web pages. Microsoft eventually forked over tens of millions of dollars in a settlement that allowed them to restore the functionality they had been forced to remove.

Other companies and products have used the same technology for years without paying royalties — Mozilla Firefox and Opera being the most well-known — but were not sued by Eolas, who chose to focus on the deep-pocketed Microsoft. (An initial jury verdict in 2003 awarded Eolas $521 million, but an undisclosed settlement was reached in 2007 after the case went through several appeals.)

This week — a year and a half after settling with Microsoft — Eolas has gone on the attack again, filing suit against “Adobe, Amazon, Apple, Argosy Publishing (publisher of The Visible Body), Blockbuster, Citigroup, eBay, Frito-Lay, GoDaddy, J. C. Penney, JPMorgan Chase, ‘transactional’ adult entertainment provider New Frontier Media, Office Depot, Perot Systems, Playboy Enterprises, Rent-a-Center, Staples, Sun Microsystems, Texas Instruments, Yahoo, and YouTube.” (article)

For the record, Eolas was founded by former University of California, San Francisco (UCSF) staff, and the patent they’re suing others for violating was developed at UCSF in the 90s. http://en.wikipedia.org/wiki/Eolas

I work at UCSF and am ashamed of these lawsuits.

The University of California owns Patent 5,838,906 and has licensed it to Eolas. The Regents of University of California are therefore the driving force behind Eolas’ behavior. By extension, UC is a driving force behind one of the biggest and most unpopular disruptions the Internet has known. If UC is really interested in public interests and good will — not to mention good publicity — I hope Patent 5,838,906 will be released into the public domain.

On a side note, knowing the University of California is being hit hard with budget cuts, I wonder if this latest blitz of lawsuits is an attempt at making up for budget shortfalls?

Gotchas in Internet Explorer 8

Internet Explorer 8 (IE8) is at Release Candidate 1, which means it will be released very shortly. IE8 is a brand-new browser and will represent a considerable shift from IE7/IE6; it will follow standards more closely and will offer much improved CSS 2.1 support. However, because of some of these changes, it is also widely understood that IE8 might break websites that have relied on IE-specific hacks targeted at previous versions of Internet Explorer.

To their credit, the IE development team has been very candid about the changes and have posted a number of blogs and documents aimed at helping web developers prepare for IE8. I was looking over one such page and thought I’d point out what I consider to be some of the biggest ‘gotchas’ so far.

Setting Unsupported CSS Values

Trying to detect support for a specific CSS value through a JavaScript try/catch statement will no longer generate an exception, which means you can’t rely on JavaScript to detect support for specific CSS values anymore.

Assigning CSS values that were unsupported in IE7 but are supported in IE8 Standards Mode will not generate exceptions in IE8 Compatibility View. Some sites use these exceptions to determine if a particular value for a CSS property is supported or not.


try {
   elm.style.display = "table-cell";
} catch(e) {
   // This executes in IE7,
   // but not IE8, regardless of mode
}

Malformed HTML

IE8 will not be as forgiving of malformed HTML markup. This is a great new ‘feature’ in terms of ensuring people (and software) are less sloppy with their markup, but this will certainly cause many, many problems for hundreds of thousands of old, poorly written websites.

Parser error correction for malformed HTML has changed in IE8 Standards Mode. Pages depending on the way IE7 performs error correction may encounter issues as a result.


<ul>
    <li>1.1
        <ul>
            <li>1.1.1</li>
    </li> <!-- Closes 1.1 in IE8, but not IE7 -->
            <li>1.1.2</li>
        </ul>
    </li>
</ul> 

Working with an Element’s Class

Like the malformed HTML ‘feature’, this is another great improvement in IE that will also cause many, many headaches. You see, for years IE wouldn’t let developers use the standard setAttribute("class") method for specifying a class name via JavaScript. Instead, IE required developers to use the proprietary setAttribute("className"). This means that it became commonplace for scripts to check for IE then use class for non-IE browsers and className for IE. Now, you’ll still need to make that check for older versions of IE but find a way to use class for IE8. <sarcasm>This will be fun.</sarcasm>

Don’t get me wrong — I’m excited that IE will finally behave like other browsers in this regard — but it also means that so long as IE6 and IE7 are still around, we’ll have to jump through more hoops to handle class names.

In IE7, “className” had to be used as the attribute name to set and retrieve the class of an element. This has been fixed for standards-compliance in IE8 Standards Mode. Using the old approach will create an attribute named “className” that has no affect on the actual class assigned to an element.


return elm.getAttribute("className");

SOLUTION: Use the standardized name, “class”, instead of “className”.


return elm.getAttribute("class");

CSS Expressions

One of the common hacks for IE’s shortcoming with CSS support has been to use IE’s proprietary CSS expressions, which are basically JavaScript statements embedded in place of a CSS value. While this practice has been frowned upon by most in-the-know web developers, it still wound up being heavily utilized as an ‘easy fix’ type of hack.

IE8 will no longer support CSS expressions. This will make it behave more like other browsers, but will cause problems for those who have relied on CSS expression hacks. Fortunately, it should be relatively easy to move your CSS expressions into your page’s JavaScript as suggested by Microsoft.

Support for CSS Expressions has been removed in IE8 Standards Mode.


/* CSS */
#main {
    background-color: expression(
        (new Date()).getHours()%2 ? "#000" : "#fff"
    );
}

SOLUTION: Refactor to utilize either improved CSS support or DHTML logic.


/* Script */
var elm = document.getElementById("main");
if((new Date()).getHours()%2) {
    elm.style.backgroundColor = "#000";
} else {
    elm.style.backgroundColor = "#fff";
} 

On the whole, I’m excited about the changes IE8 will bring, although it will undoubtedly require site revisions for anyone who uses JavaScript extensively in their projects.

You can read the original Microsoft blog post here.

Disabling those pesky pop-up blockers

As a web surfer I LOVE pop-up blockers. They keep my web surfing sane and somewhat clean (though there always seems to be a few Netflix pop-ups that get through the cracks).

However, as an e-learning course developer, pop-up blockers are a major pain in the rear end. Our courses almost always need to be launched in a pop-up window, and the pop-up windows are very frequently blocked. This certainly doesn’t help us when many of our end-users — the infamous and vaguely named “learner” — are often resistant to taking the course in the first place.

Well, I can’t help fight la resistance, but I can help configure those pesky pop-up blockers.

Here’s a list of common pop-up blockers with links to the manufacturer’s instructions for handling said blocker. Enjoy.

Oh, and in case you didn’t know, the universal default for bypassing a pop-up blocker is to hold “ctrl” while clicking a link that causes a pop-up, such as the old chestnut “start course”.

Toolbars with Pop-up Blockers

Browsers

Other blockers

Want to add more to the list?

I know I’ve left a few things off, such as Opera and the MSN toolbar. If you have the link for any of these, or if you’d like to add something else to this list, just post a comment. Thanks.

Dealing with Internet Explorer in your JavaScript Code

It’s almost the end of 2008, and thanks to the hard work of web standardistas, browser vendors, and JavaScript framework developers, cross-browser JavaScript code is much less of an issue than it used to be. Even Microsoft is feeling the love — the upcoming Internet Explorer 8 will be a (mostly) clean break from legacy Internet Explorer releases and will behave much more like Firefox, Safari (WebKit) and Opera. …And they rejoiced.

So why is it that when I look under the hood of some recently produced web pages (learning management systems, courses produced by e-learning rapid development tools, general web pages, etc.), the pages’ JavaScript often includes incredibly out-of-date and bad-practice Internet Explorer detection? Check out these samples I randomly copied:


_ObjBrowser.prototype.Init = function() {
    var $nBrowserChar    = "";
    var $nBrowserStart   = 0 ;
    if ( this.$strUA.indexOf("MSIE") >= 0 ){
        this.$nBrowser = BROWSER_IE ;
        this.$nBrowserVersion = "";
        $nBrowserStart   = this.$strUA.indexOf("MSIE")+5
        $nBrowserChar    = this.$strUA.charAt($nBrowserStart);
        while ( $nBrowserChar != ";" ){
            if ( ( $nBrowserChar >= '0' && $nBrowserChar <= '9' ) || $nBrowserChar == '.' )
                this.$nBrowserVersion += $nBrowserChar ;
            $nBrowserStart++;
            $nBrowserChar     = this.$strUA.charAt($nBrowserStart);
        };
        this.$nBrowserVersion = parseInt( parseFloat( this.$nBrowserVersion ) * 100 ) ;
    } else if ( this.$strUA.indexOf("Mozilla") >= 0 ){
        this.$nBrowser        = BROWSER_MOZILLA ;
        this.$nBrowserVersion = parseInt ( (this.$strUA.substring( this.$strUA.indexOf("/") + 1,  this.$strUA.indexOf("/") + 5  )) * 100 );
    }
};

or even these simpler yet equally problematic sniffers:

UserAgent detection

if (navigator.appName &&
    navigator.appName.indexOf("Microsoft") != -1 &&
    navigator.userAgent.indexOf("Windows") != -1 &&
    navigator.userAgent.indexOf("Windows 3.1") == -1) {
        //Do something
}

Bad object detection

if (document.all) {
    //Do something for Internet Explorer 4
} else if (document.layers) {
    //Do something for Netscape Navigator 4
} else {
    //Do something for other browsers
}

These examples are BAD BAD BAD. Why? Well, there are a million web articles explaining the topic, but I’ll give you a quick rundown.

You shouldn’t test for specific browsers, but for specific functionality instead

In most cases, browser detection is being used because the developer is making an assumption that a particular browser does or doesn’t have a specific feature. The problem is that browsers change over time, and detecting for a browser based on assumptions can come back to bite you. Even the prickly Internet Explorer gets updates from time to time. Case in point: The versions of IE6 in Windows 2000 and Windows XP have a different JavaScript engine (JScript version) than IE6 running in Windows XP Service Pack 3. This means a general IE6 detection script might not lead you down the path you expected.

If you test for features instead, your code will be more future-compatible and less likely to break.

if(document.getElementById){
   //It is safe to use document.getElementById()
} else {
   //document.getElementbyId() is not supported in this browser
}

When hacks for Internet Explorer are required

Nowadays, most browsers offer roughly the same features and adhere to W3C standards. But, as we know, our friend Internet Explorer is… different. If you must use hacks custom code for Internet Explorer, know your options (and please, for the love of… something… don’t use ActiveX controls. Just don’t.).

Use cleaner IE detection

The old User-Agent sniffing approach has been abused for years. Internet Explorer 7 (Windows XP SP2) uses the following User-Agent to identify itself:

Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 5.1)

Yes, you read that correctly: IE declares itself a Mozilla browser. Opera’s User-Agent can be changed on the fly, allowing a user to set it to Opera, IE, or Mozilla! This all renders User-Agent detection useless. So what are your other IE detection options?

Conditional comments. Conditional comments are a proprietary Microsoft mechanism that is often used to include IE-specific CSS files in a page. For our discussion, you could conceivably use conditional comments to include a JavaScript file containing IE hacks in your page. You could also do a quick and dirty hack like this:


<!--[if IE]>
   <script type="text/javascript">
      var isIE = true;
   </script>
<![endif]-->

But, as you can see, it’s a clunky solution and isn’t inline with the other JavaScript, which makes it a less-than-ideal option.

Conditional compilation. This method is THE method of choice right now. Dean Edwards described this gem of an approach on his blog post Sniff. As far as I can tell, it is the cleanest and most reliable way to detect IE.

var isMSIE = /*@cc_on!@*/false;

Simple, isn’t it? No User-Agents, no 20 lines of conditional statements and string searches.

How does this code work? Internet Explorer uses JScript, a proprietary form of JavaScript (ECMAScript) developed by Microsoft. JScript has a feature called conditional compilation, which allows us to add JScript-specific JavaScript inside a specially formatted comment. Other JavaScript compilers will ignore this code, thinking it’s a comment, while JScript will recognize it and process it.

/*@cc_on begins the comment, and @*/ ends the comment. When any non-IE browser sees var isMSIE = /*@cc_on!@*/false;, it actually sees it as var isMSIE = <strong>false</strong>;. Internet Explorer’s JScript will add the exclamation point contained in the comment, meaning IE will see var isMSIE = <strong>!false</strong>;. Remember, an exclamation point reverses a boolean’s value, so !false is equivalent to true.

As Dean pointed out, there is an even simpler way to write this (though it’s slightly less legible):

var isMSIE/*@cc_on=1@*/;

If a variable is not assigned a value when it is declared, by default it will return ‘falsy‘ (null). Thus a non-IE browser will see this line as var isMSIE; while Internet Explorer’s JScript will see it as var isMSIE=1; (assigning a 1 makes the variable ‘truthy‘).

I prefer to shorten it a bit more by removing the MS from isMSIE, making it simply isIE. Note: it’s a best practice to give booleans a name that implies what the boolean stands for, usually in a quasi-question format. Using plain old var IE doesn’t tell us the variable is a boolean, and doesn’t tell us what question is being answered by the boolean’s value. var isIE does.


var isIE/*@cc_on=1@*/;
if(isIE){
   //Do something. I suggest downloading Firefox.  ^_^
}

Avoid forking your code for hacks whenever possible.

If your code can be written in a way that satisfies all browsers, do it that way, even if it veers to the left of official web standards — just be sure to document your decision in the comments. For example, using the standards-friendly setAttribute for specifying class names will fail in IE, while direct assignment will work in all major browsers.

//standards-friendly, but fails in IE
element.setAttribute("class", "myClass");

//direct assignment, works in all major browsers
element.className = "myClass";

In this case, I’m advocating using defacto standards that aren’t codified in the W3C standards, but are supported in every major browser. Some standardistas will balk, but this is a much easier way to maintain code; it’s a bad idea to use unnecessary conditional statements simply to prove that IE doesn’t always follow standards. We know, we don’t like it either, but get over it. If a non-standard but universally supported alternative is available, your extra “see I told you so” code will be pure bloat, increasing file size, bandwidth requirements, and browser processing requirements.

Don’t get me wrong: standards are important. Very important. But they only go so far; if we stuck to codified standards on principle, no one would be using AJAX today! That’s right, xmlhttprequest — the heart of AJAX — was created by Microsoft, then copied by other browsers. It is not an official standard, but it is universally supported, and is used by tens of millions of web site visitors every day.

If you’re not comfortable with non-standard code littered throughout your project, abstract it into a function so you can easily modify it later.

Old way:

element.className = "myClass";

Abstracted function:

function setClass(targetElement, name){
   /*
      not using setAttribute here because setAttribute
      won't work with class names in IE
   */
   targetElement.className = name;
}

setClass("element", "myClass");

Now whenever you need to set a class, you won’t worry about IE and can simply use your class function. If you decide to change how you assign the class name, you can just change it in the function and not have to dig around your code looking for every instance of direct assignment (className =).

Note: Many JavaScript frameworks, such as jQuery and MooTools, provide this type of support function for you. These frameworks protect you from needing to know about browser inconsistencies, and free you to focus on your application. If the above example were rewritten to use MooTools, it would simply be

element.addClass("myClass");

There are many ways to approach the problem, and all without using nasty browser detection scripts. It pays to know your options.

Use abstractions for forking code

Getting back to IE detection, if you aren’t using a framework and need to handle browser inconsistencies on your own, it’s a good idea to use support functions. These can encapsulate the browser inconsistencies and isolate them from the rest of your code. This makes your code more maintainable and readable.

Case in point: Internet Explorer will not allow you to dynamically assign a name value to an <input> element. This requires an IE-specific hack.

You would normally use the following W3C-compliant code:

var input = document.createElement("input");
input.setAttribute("name", "myInput");

In Internet Explorer, we’re forced to do this:

var input = document.createElement("<input name='myInput'>");

In your project, you’d have to use conditional statements each time you need to create an input element.

var isIE/*@cc_on=1@*/;

if(isIE){
   var input1 = document.createElement("<input name='myInput1'>");
} else {
   var input1 = document.createElement("input");
   input.setAttribute("name", "myInput1");
}
input1.setAttribute("id", "myInput1");

//Later on in your code...
if(isIE){
   var input2 = document.createElement("<input name='myInput2'>");
} else {
   var input2 = document.createElement("input");
   input.setAttribute("name", "myInput2");
}
input2.setAttribute("id", "myInput2");

What a pain. The best way to deal with this is to wrap the code in a function like so

function createInput(parentElement, id){

   //Make sure everything is supported. Error-checking is divine.
   if(!parentElement || !id || !document.createElement || !parentElement.createElement){ return false; }

   var isIE/*@cc_on=1@*/;
   var input;

   if(isIE) {
      input = parentElement.createElement("<input name='" +id +"'>");
   } else {
      input = parentElement.createElement("input");
      input.setAttribute("name", id);
   }

   input.setAttribute("id", id);
   return input;

}

As you can see, the code is now easily reusable throughout our project, contains better support for IE detection, and also contains feature detection ensuring our function won’t throw any errors. To use the code in the project, you’d simply write:

var input1 = createInput(document, "myInput1");
var input2 = createInput(document, "myInput2");

In closing

I hope you’ve found this post helpful, and I especially hopes it helps spread the word about using best practices and cleaner code techniques. I’ll be the first to say I’m no programming expert, so feel free to add a comment if you have suggestions for improvements. 🙂

PS: If you have to write a conditional statement, follow Crockford’s advice and wrap it in curly braces.

A cross-browser JavaScript prompt

You might be looking at the title of this entry and say “Well, DUH! JavaScript’s prompt() is already cross-browser!”

While working on a project earlier today, I discovered a nasty little problem… Internet Explorer v7 (IE7) disables prompt() by default! This means you can’t rely on prompt() being available in IE7 when building your online applications.

In most cases, prompt() should be avoided altogether. Maybe Microsoft was right in disabling prompt(), since so many malicious sites take advantage of it. However, I had a legitimate use for it today, and was very irked to find out it won’t work in IE7. (I develop in Firefox and IE6, which might explain to some of you why I’m late to the party with discovering this limitation in IE7.)

After googling for a while — yes, I use google as a verb — it appears quite a few people have developed workarounds for the prompt() limitation in IE7. Most of them were bulky or required a bit too much hacking for my tastes. There were a few nice examples [link no longer available] out there, but in the end, I decided to make my own workaround using Microsoft’s proprietary showModalDialog function.

Microsoft’s showModalDialog allows the developer to load an external HTML file into a generated window and give it focus. The user can’t click back to the original document until they’ve closed the modal window, just like a prompt(), alert(), or confirm(). The generated window behaves much like a pop-up window (it can be sized, show/hide scrollbars, etc.), but to my knowledge, modal windows aren’t blocked by pop-up blockers.

I’ll admit that using an external HTML page feels like a big disadvantage compared to a simple prompt(). However, one potential advantage showModalWindow provides is the ability to style the faux prompt window; in IE, the prompt window has always looked and behaved a little different from any of the other dialog windows (alert, confirm). Now we can have it visually fit in with the rest of the family! Due to this styling issue, and to make my life easier, I decided to use showModalWindow on all versions of IE, not just IE7.

Native IE6 prompt:
Native IE6 prompt

Native Firefox 2 prompt:
Native Firefox 2 prompt

New faux prompt, as seen in IE6:
Faux prompt, IE6

New faux prompt, as seen in IE7 (WinXP):
New faux prompt, as seen in IE7 (WinXP)

Important note: The IE7 modal window will forcibly display the address and status bars if being called by a server (not localhost) which isn’t listed in that user’s list of ‘Trusted Sites.’ This is annoying, but still better than having no prompt() at all. IE6 forcibly displays the status bar, but not the address bar.

Example

Before I dig into the code, here’s a functioning example. There are two files involved in this hack: the main HTML page calling the prompt, and the faux prompt HTML page.

The traditional prompt()

For those of you new to using prompts, here’s an example of a traditional prompt in action:


window.onload = function (){
   var name = prompt("Please enter your name", "");
   if(name){
      alert("You entered '" +name +"'.");
   }
}

Return values

A key feature of prompt() is that it returns the value of the textfield as a string. If nothing was typed into the text field, prompt() returns either null or false, depending on your browser. If the user clicks ‘cancel’, prompt() returns false.

Our faux prompt for IE needs to work the same way. As luck would have it, showModalWindow allows us to specify return values.

Prompt text

Another key feature of prompt() is the ability to display your own text in the prompt, such as “Please enter your name.” To make our lives easier, our faux prompt should work the same way, using the same syntax.

Our IE-specific function: iePrompt()

In all its glory:


function iePrompt(str){
   var settings = "dialogWidth: 290px; dialogHeight: 160px; center: yes; edge: raised; scroll: no; status: no;";
   return window.showModalDialog("iePrompt.html", str, settings);
}

Let’s break it down.


function iePrompt(str){

Since we can’t use the traditional prompt(), we need to create a new, similar function. Just as prompt allows you to specify what text will display by passing a string,

prompt("This text will display in my prompt","")

we want to be able to pass a string in our new function:

iePrompt("This text will display in my prompt")


var settings = "dialogWidth: 290px; dialogHeight: 160px; center: yes; edge: raised; scroll: no; status: no;";

IE’s showModalDialog allows you to style the window much like a pop-up window. The settings are entered as a single string. For convenience and readability, I placed all the settings in a variable named “settings”. You can tweak these settings to suit your own needs. A list of optional parameters can be found here.

Important note: The height of the modal window is set here using dialogHeight; if the display text (in this case “Please enter your name”) wraps to the next line, the window will NOT expand to fit it. If you’re working with long strings, you’ll need to test the height of your modal window, or develop your own sizing routine. Also note that in IE6, dialogHeight referred to the height of the entire dialog window. In IE7, the model was changed, and dialogHeight now refers ONLY to the height of the content. See this IEBlog for more info.

return window.showModalDialog("iePrompt.html", str, settings);

As mentioned earlier, showModalWindow can return a value the same way prompt() returns a value. We want our iePrompt() function to pass the value returned by showModalWindow.

  • iePrompt.html is the name/path of the HTML file containing a form that mimics prompt().
  • str is the text that will be displayed in the prompt.
  • settings is the variable containing the settings string we defined earlier.

The iePrompt.html file

I don’t want to spend too much time explaining the HTML file, as it’s a simple HTML file containing a wee bit of CSS for styling, and a very tiny form. There are some important elements to note, though.

The submit function


function formSubmit(){
   var str = document.getElementById("promptText").value;
   if(str){  
      window.returnValue = str;
   } else {  
      window.returnValue = false;
   }
   window.close();
}

In order to get the showModalDialog function to return a value, we have to make our form return a value. To do this, we have to specifically use window.returnValue. The code is set up to conditionally send either the content of the promptText field, or the value false, just like a normal prompt.

After returning the value to the page that called the prompt, we need to close the modal window by calling window.close.


function formCancel(){
   window.returnValue = false;
   window.close();
}

Prompts also have cancel buttons. We want our modal window to have a cancel button that behaves just like a prompt. To do this, we set up a function that returns the value false and closes the modal window.


window.onload = function (){
   var str = window.dialogArguments;
   if(str){
      document.getElementsByTagName("label")[0].innerHTML = str;
   }
}

This bit of code grabs the display text value being passed by iePrompt and dynamically inserts it into a label on our form. We’re passing the argument using the dialogArguments property of the modal window generated by showModalDialog.


<body onbeforeunload="formCancel();">

Lastly, we need to take into account the fact that many people choose to click the dialog window’s close button (X) instead of OK or Cancel. Using onbeforeunload allows us to call formCancel() if the user decides to close the window.

And that’s it! We now have a functioning prompt for IE.

Putting it into action

Now that we have our custom IE prompt, we need to integrate it with non-IE browsers. Fortunately, this is a pretty simple task.

Determining if the browser is IE

Rather than use some elaborate browser-sniffing technique, we’ll stick to supported feature detection, which is considered a best-practice by the DOM scripting crowd. This is easily accomplished by checking for window.showModalDialog. If the return value is true, the browser is a flavor of Internet Explorer (v4 and higher), and supports our new prompt method. If the return value is false, the browser isn’t IE and should be able to use a traditional prompt().


function cbPrompt(str){
   try {
      if(window.showModalDialog){ return iePrompt(str); }
         else { return prompt(str, ""); }
   } catch (e) { 
         return false; 
   } 	
}

I decided to use a try/catch statement for future expandability (being able to add extra functionality, such as using confirm() to validate the text entered by the user. You could rewrite it to use simple if/else statements.

I wrapped the logic into a new function called cbPrompt (“cb” meaning cross-browser). Now anytime I need a prompt, I simply call cbPrompt instead of prompt():


window.onload = function (){
   var name = cbPrompt("Please enter your name");
   if(name){
      alert("You entered '" +name +"'.");
   }
}

As you can see, it’s almost exactly the same as using the traditional prompt:


window.onload = function (){
   var name = prompt("Please enter your name", "");
   if(name){
      alert("You entered '" +name +"'.");
   }
}

Just remember that you have to ensure the iePrompt.html file is present and that the path defined in the iePrompt function is accurate.

Here’s a fully-functional cross-browser example.