The more browser brands, versions, and operating systems you wish to support with your DHTML applications, the greater your challenge to write one code base that works with them all. Before undertaking any project intended for more than a single browser, you must make difficult decisions about not only which browsers to support but also how users of other browsers will be treated by your site. Consumer-oriented e-commerce sites, for example, can rarely afford to turn away even a small percentage of potential customers because the visitors' browsers don't measure up to a lofty design. Specialized sites that are not as concerned about competitive pressures may choose to require browsers of a certain minimum functionality to pass beyond the home page.
Another important question to ask yourself about your design goals is whether the DHTML features of your site add value to the content that is otherwise accessible to all, or those features are essential to the site design. For example, a DHTML-assisted hierarchical menu system adds value by speeding direct access to a nested area of your site, yet users of DHTML-challenged browsers can still reach those areas (albeit with more clicks and intermediate stops en route), and search engine web crawlers will pursue the links. Conversely, if navigation absolutely requires DHTML powers, some visitors will be locked out. Similarly, search engine web crawlers, which don't execute scripts, will not know to follow links that are rendered only by script. This could reduce the chances that deeper pages of your site will be catalogued and indexed.
Regardless of the approach you use to accommodate multiple browsers, it will at some point entail code branching or other equalization tricks that are dependent upon the scriptable features of the browser. Back when the matrix of browser versions was small, it was common practice to use browser "sniffing" with the aid of information gleaned from the navigator.userAgent and related properties. But with the matrix growing ever larger, it's time to examine object detection as a more workable solution.
DHTML developers from the Version 4 browser days who employed browser version sniffing found themselves in trouble at some point as new browser versions came on the scene. Consider the following typical global variable declaration from the era of IE 4 and Navigator 4:
// code unknowingly doomed to failure var isNav, isIE; if (parseInt(navigator.appVersion) >= 4) { isNav = (navigator.appName == "Netscape"); isIE = (navigator.appName.indexOf("Microsoft") != -1); }
Hereafter, various functions would branch their code, with browser-specific code in each branch. Unforeseen at this time, subsequent versions of Netscape abandoned the layer object—usually the primary need for branching in the first place. As a result, Netscape 6 (whose appVersion reports 5) attempted to execute code that it could not handle. On the IE variable side of things, two potential problems loomed, depending on how much IE4-ness the author ascribed to browsers following that branch. For one, the Macintosh does not implement most IE/Windows-only features. Second, the default preference settings of the Opera browser cause it to identify itself as IE, yet this does not assure compatibility with IE scripts.
Trying to compensate for all browsers past, present, and future requires a huge version sniffing library plus a crystal ball about future browser version numbering and naming. Even attempting such forecasting won't take into account new browsers that crop up, some of which will be built upon very capable existing engines, such as the Mozilla engine. Building branched code based on browser version is a losing battle. A better approach is to branch based on the capabilities of the browser. That's what object detection does.
Object detection is a shortcut name for a technique that verifies the existence of an object, property, or method before using it in a script. The technique isn't new. Scripts that control image rollovers have been using it for years by testing for the presence of the document.images array before acting on an image object:
if (document.images) { // act on image objects here }
All object models that implement img elements as objects support the document.images array. In older browsers, the expression document.images evaluates to undefined, which causes the if condition to fail, so the nested statements don't run. Thus, the scripter is freed from worrying about which browser supports the image object.
Implementing object detection on a broader scale can free you from the complexities of today's browser sniffing. For example, a function that switches a style property can work in both the IE 4 and W3C DOM browsers, but requires different referencing syntax for the element. The following function sets the fontWeight style property of an element to bold:
function emBolden(elemID) { var elem; if (document.all) { elem = document.all(elemID); } else if (document.getElementById) { elem = document.getElementById(elemID); } if (elem && elem.style && elem.style.fontWeight) { elem.style.fontWeight = "bold"; } }
A local variable, elem, is initialized as a null value. The if/else construction looks for the two element reference types that I know have a chance of supporting the style property. The test for document.all is like the earlier example of document.images. Less well-known is that object methods are exposed in most browsers as properties, whose existence can be tested in a similar fashion.
To protect additional script statements from the case of both if/else conditions failing, the balance of the function verifies that elem has a value assigned to it. The tripartite condition is overkill for this specific application, because you can make an educated and safe assumption that any browser that supports either document.all or document.getElementById( ) also supports not only the style property of elements, but also the very common fontWeight style property. But the example is here to demonstrate how to go about verifying the existence of a property when the object or intermediate property may not exist. In the above example, you cannot test simply for the existence of elem.style.fontWeight. A "one-dot" evaluation rule applies to JavaScript, whereby every reference up to the rightmost dot must evaluate successfully in order for the interpreter to see whether the last reference succeeds or fails. If you were to test for the existence of elem.style.fontWeight by itself, and elem was not a valid reference, the script interpreter generates a script error. Evaluation tests of an if condition are conducted from left to right. If any one of the ANDed expressions fails, the condition immediately fails (short circuits), and no further evaluations occur, leaving your browser free from script errors there.
Some browsers, especially Opera and some older IE and Netscape versions, may require more help in evaluating conditional expressions. For these browsers, a value of undefined does not necessarily convert to false (although the ECMA specification says it should). To obtain the same result, you can use the typeof operator to inspect the data type of the object or property:
if (elem && (typeof elem.style != "undefined")) {...}
A value of null does correctly evaluate to false for all browsers, so the first test is fine the way it is. If elem exists, the string returned by the typeof operator gets compared against undefined. If the data type is anything other than undefined, processing continues (the test for fontWeight is not shown here for the sake of brevity).
The typeof operator also helps in those cases when a property exists and its value (perhaps its default value) is either an empty string or zero. Both of these values would cause the conditional expression to evaluate to false, even though the property exists. By making sure the property value is either a particular data type or anything other than undefined, your condition more accurately reports the presence of the property.
Object detection doesn't solve every compatibility problem, and requires having at hand a good reference of currently-supported DHTML features (such as this book). There are even times, particularly when designing around known bugs in earlier browsers, when browser sniffing is appropriate on a small scale. Yet for a great many scripts, object detection can not only ease implementation of incompatible syntax, but also allow older browsers to degrade gracefully. You can read more about object detection techniques and strategies in an article at http://www.oreillynet.com/pub/a/javascript/synd/2001/10/23/ob_detect.html.
Whether you elect to use object detection, browser version sniffing, or the mix of the two, you have a choice of several cross-browser deployment strategies: page branching, internal branching, common denominator design, and custom API development. Additional choices you'll make include whether you wish to deny page access to older browsers, provide multiple paths for browsers of different capabilities, or provide just one path that enhances the experience for DHTML features of your design yet degrades gracefully for those browsers without the latest doodads. The following sections describe some of the more popular strategies for accommodating multiple browsers.
Web pages that use absolute-positioned elements degrade poorly when displayed in older browsers. The positioned elements do not appear where their attributes call for them, and, even worse, the elements render themselves from top to bottom in the browser window in the order in which they appear in the HTML file. Also, any elements that are to be hidden when the page loads appear in the older browsers in their source code order. To prevent users of older browsers from seeing visual gibberish, you should have a plan in place to direct users of non-DHTML-capable browsers to pages containing less flashy content or instructions on how to view your fancy pages. A server-side CGI program can perform this redirection by checking the USER_AGENT environment variable sent by the client at connect-time and redirecting different HTML content to each browser brand or version.
Alternatively, you can do the same branching strictly via client-side scripting. Depending on the amount of granularity you wish to establish for different browser brands and versions at your site, you have many branching techniques to choose from. All these techniques are based on a predominantly blank page that has some scripted intelligence behind it to automatically handle JavaScript-enabled browsers. Any script-enabled browser can execute a script that looks into the visitor's browser version and loads the appropriate starter page for that user. Example 2-1 shows one example of how such a page accommodates both scripted and unscripted browsers.
<html> <head> <title>MegaCorp On The Web</title> <script language="JavaScript" type="text/javascript"> <!-- if (document.images) { if (document.getElementById) { window.location.replace("startW3C_DHTML.html"); } else { window.location.replace("startRollover_DHTML.html"); } } else { window.location.href = "startPlainScripted.html"; } //--> </script> <meta http-equiv="REFRESH" content="1;URL=http://www.megacorp.com/startUnscripted.html"> </head> <body> <center> <a href="startUnscripted.html"> <img src="images/megaCorpLogo.gif" height="60" width="120" border="0" alt="MegaCorp Home Page"></a> </center> </body> </html>
The script portion of Example 2-1 provides three possible branches, depending on the browser level. If the browser version supports even the simplest W3C DOM feature (referencing elements via the document.getElementById( ) method), the user is immediately directed to a new start page that assumes that minimum capability. Using location.replace keeps the index page out of the browser history so the Back button works as expected. For a browser lacking the W3C DOM support, but fitted for image objects, the script directs that user to a start page that is wired for image rollovers as the maximum amount of DHTML. Notice the check for W3C DOM support is nested within the document.images check. This sequence offers a bit of insurance against the oldest scriptable browsers that might choke on a test for an undefined expression and also lack the typeof operator. Any other scriptable browser navigates to a start page that knows at least simple scripting that is completely backward-compatible.
For browsers that either don't have JavaScript built in or have JavaScript turned off, a <meta> tag refreshes this page after one second by loading a starter page for unscripted browsers. Even though page refreshing is not an official usage for the <meta> tag, a great many browsers support it. For "barebones" browsers that may not recognize scripting or <meta> tags (including Lynx and browsers built into a lot of handheld devices), a simple image link leads to the unscripted starter page. Users of these browsers will have to "click" on this link to enter the content portion of the web site.
Example 2-1 is an extreme example. It assumes that the web application has as many as four different paths for four different classes of visitor. This may seem like a good idea at first, but it seriously complicates the maintenance chores for the application in the future. Modified with fewer branches, the technique of Example 2-1 provides a way to filter access between W3C DOM DHTML-capable browsers and all the rest.
As an alternative to the page-branching technique, you can use the iframe element and CSS features of recent browsers to replace the regular HTML content of a page with the content from an entirely different page. With this technique, the address and document title of the original page remain visible in the browser. A search-engine robot arriving at the page can read and analyze the same barebones content that visitors with non-DHTML browsers see. But visitors with DHTML-enabled browsers (or whatever filter mechanism you desire) see only the enhanced page. Bookmarks and page pointers passed around the community refer to a single URI.
Example 2-2 demonstrates what you need to add to an existing page to overlay an enhanced page for W3C DOM-capable browsers.
<html> <head> <title>MegaCorp on the Web</title> <script language="JavaScript" type="text/javascript"> <!-- var isW3C = (document.getElementById) ? true : false; if (isW3C) { document.write("<style type='text/css'>body {margin:0px; padding:0px; overflow:"); document.write("hidden}#preW3C {display:none}</style>"); } // additional regular page scripts can go here //--> </script> </head> <body> <span id="preW3C"> regular body content goes here </span> <script type="text/javascript"> if (isW3C) { document.write("<iframe frameborder='0' vspace='0' hspace='0' marginwidth='0'"); document.write("marginheight='0' height='100%' width='100%'"); document.write("src='startW3C_DHTML.html'></iframe>"); } </script> </body> </html>
When a page containing the Example 2-2 enhancements loads into a W3C DOM-enhanced browser (that is, one that knows the document.getElementById( ) method), a script writes a two-rule style sheet for the page. The first rule erases all margins and scrollbars from the body element; the second rule prevents the span element that wraps all regular body content from rendering itself. The same browser versions that write the new style sheet rules also add an iframe element to the page. Attributes of the <iframe> tag assure that it fills the entire content region of the browser window (with the exception of Opera, which leaves a harmless blank area along the right edge).
The iframe acts as a self-contained window for a separate page containing the DHTML-enhanced content. You can decide whether links from this page should target the same frame (the default action) or replace the original page (by targeting _top).
You have two HTML pages to maintain with this scheme. But it also means that you have a static page to fall back on while you experiment with DHTML features. Plus, the search engines will still find your key data, while your DHTML-enhanced page can use all kinds of dynamic content creation techniques if you like.
Instead of creating separate documents for each browser class, you can use JavaScript to write browser-specific content for a page within a single document. There was more call for this when Navigator 4's <layer> tag behaved better than positioned div elements. A script could use document.write( ) to add a <layer> tag and its attributes for Navigator 4 and a <div> tag for IE. But even today, you may need to write different content for different browsers. For example, a corporate web designer may find that one set of style sheet rules works well for Windows browsers, while Mac browsers behave better with variations in font specifications. The following script fragment from a head script links in one external CSS file for Mac browsers and another file for all others:
var isMac = navigator.userAgent.indexOf("Mac") != -1; if (isMac) { document.write("<link rel='stylesheet' type='text/css'"); document.write("src='styles/corpMac.css'>"); } else { document.write("<link rel='stylesheet' type='text/css'"); document.write("src='styles/corpDefault.css'>"); }
From a maintenance point of view, the ideal DHTML page is one that uses a common denominator of syntax that all supported browsers interpret and render identically. You can achieve some success with this approach if you target W3C DOM-capable browsers, but you must be very careful in selecting standards-based syntax that is implemented identically in all such browsers. Because some of these standards were little more than working drafts as the supposedly compatible browsers were released to the world, the implementations are not consistent across the board.
DHTML feature sets that you can use as starting points for a common denominator approach are the standards for CSS1 and CSS-P. Tread carefully in CSS2, unless you are targeting only the latest browsers and have verified support for your features in those browsers. When you peruse developer documentation from browser vendors, it is often impossible to gauge whether a feature is a company's proprietary extension that adheres to the spirit, but not the letter, of a standard. Just because a feature is designated as "compatible with CSS" does not mean that it is actually in the published recommendation. Refer to the reference chapters in Part II of this book for term-by-term browser and standard support.
You are likely to encounter situations in which the same style sheet syntax is interpreted or rendered slightly differently in various browser versions, especially those prior to IE 6 and Netscape 6 when the page's <!DOCTYPE> points to a recent DTD (as explained in Chapter 8). This is one reason why it is vital to test even recommended standards on as many browser platforms as possible. When an incompatibility occurs, there is probably a platform-specific solution that makes the result look and behave the same in both browsers. To achieve this parity, you'll need to use internal branching for part of the page's content. This is still a more maintainable solution than creating an entirely separate page for each browser.
Thanks to the similarities in syntactical support for scripted CSS properties in both the IE 4 and W3C DOMs, scripts that must support the basics of these two DOMs need to reconcile only the element-reference and event-model idiosyncrasies. Scripters who also lived through the Navigator 4 DOM era experienced a far more difficult time reconciling the differences. The more DHTML DOMs you wish to support, the greater the need to use internal branching—preferably through object detection—for your application to work seamlessly across platforms.
Once you go to the trouble of writing scripts that perform internal branching, you might prefer to avoid doing it again for the next document. Modern browsers allow JavaScript to load libraries of script functions (files named with the .js extension) into any HTML document you like. You can therefore create your own meta language for scripted DHTML operations by writing a set of functions that have terminology you design. Place the functions in a library file and rely on them as if they were part of your scripting vocabulary. The language and function set you create is called an application programming interface—an API. Example 2-3 shows a small portion of a sample DHTML API library for DOMs that adhere to the IE 4 and W3C DOM element-referencing schemes.
// Convert object name string or object reference // into a valid object reference function getStyleObject(obj) { var styleObj; if (typeof obj == "string") { if (document.getElementById) { styleObj = document.getElementById(obj).style; } else if (document.all) { styleObj = document.all[obj].style; } } else if (obj.style) { styleObj = obj.style; } return styleObj; } // Positioning an object at a specific pixel coordinate function shiftTo(obj, x, y) { var styleObj = getStyleObject(obj); if (styleObj) { styleObj.left = x + "px"; styleObj.top = y + "px"; } }
The getStyleObject( ) function of Example 2-3 is an all-purpose function that returns a reference to the style property of an element object that is passed originally as either a string of the object's ID or a ready-to-go object reference. When the incoming object name is passed as a string, the string becomes an argument for document.getElementById( ) or an index to the document.all array, based on which form is supported by the browser. A browser that supports both reference types executes only the first. In contrast, when the incoming parameter is already an object reference, it goes through one more validation to guarantee that it has a style property before that property is retrieved. Notice that for a string value, browsers that don't support either of the preferred element referencing methods assign null to the value to be returned; the same goes for an incoming object value that doesn't have a style property. This null value plays a role in every function that invokes this getStyleObject( ) function.
The shiftTo( ) function in Example 2-3 doesn't have a lot to do. But by invoking getStyleObject( ) and validating the existence of the element object it is called upon to move, it helps other browsers, such as Navigator 4, degrade gracefully when it reacts to events triggering the element move. It's true that Navigator 4 can move an element (via different syntax for both the layer element reference and the movement action), but this API chooses to bypass support for that browser version.
Building an API along these lines lets you raise the common denominator of DHTML functionality for your applications. You free yourself from limits that would be imposed by adhering to 100% syntactical compatibility. In Chapter 4, I present a more complete custom API that smooths over potentially crushing CSS-P incompatibilities (including backward compatibility with Navigator 4 to assist readers who adopted the API from the first edition).
Copyright © 2003 O'Reilly & Associates. All rights reserved.