Cross-Platform DHTML

Dr. Dobb's Journal February 2001

Dealing with the new "next-version" browsers

By Charlie Ma

Being a Javascript programmer usually means you have to adroitly handle various browser compatibility issues, then walk the fine line between coding to the lowest common denominator browsers or else taking advantage of some of the newer features and risk alienating those using older browsers. All this adds up to convoluted code that's difficult — if not impossible — to maintain.

Things are so bad that even our clients have learned to curb their expectations. Over the past year, I have not had a single client ask for a web site that works with all browsers. They don't even ask for compatibility with most browsers. These days clients know to say "compatibility with 4.0 and above" or risk being laughed at by the very consultants they try to hire. And of course, by convention "4.0 and above" means only Internet Explorer and Netscape Navigator.

Those patient enough to have negotiated the nettling nonconformance between Navigator (Nav) and Internet Explorer (IE) have found rewards in huge billing rates and endless job opportunities. Given that both Microsoft and Netscape released Version 4.0 browsers in 1997, Javascript programmers have been getting fat for more than three years. Sure IE 5 has been out for nearly two of those three years, but for the most part, it's nicely backward compatible with IE 4 (to the point of having navigator.appVersion = 4.0), and you've probably stayed well away from the 5.0 features to maintain Navigator 4 compatibility.

All that's about to change. Netscape 6 is out, which has navigator.appVersion = 5.0, and it is not backward compatible. What's more, this new browser has an open-source cousin called "Mozilla." Recent trends have taught us that open-source projects have a way of picking up steam, and there's a good chance that within a couple of years, you'll find Mozilla as the dominant browser technology on nonMicrosoft platforms. So, even though Microsoft holds nearly 85 percent of the marketshare with IE4 and IE5, you cannot afford to ignore the new Netscape and Mozilla browsers.

This is good and bad news. Depending on how you've managed your old clients, they're either going to run back to you screaming for help, or scream bloody murder for the broken "4.0 and above" promise. The real news is that for the first time, there seems to be an emerging standard in client-side web scripting. Certainly the 5.0 features of IE 5 and Netscape 6 are more alike than different. Both Microsoft and Netscape have announced their resolution to support W3C's DOM specifications.

W3C expects to release three levels of DOM specifications.

At this writing, DOM Level 1 has been released, DOM Level 2 is at Candidate Recommendation stage, and Level 3 is a working draft.

If all goes well, Javascript programmers will soon be able to enjoy Java's level of platform independence. But first you have to get past that transitional period in which there is still a large enough population of 4.0 browser users requiring you to continue this browser compatibility dance. And if history is any indication, IE and Nav will maintain just enough subtle differences to keep you awake at night.

For the remainder of the article, I'll use the abbreviations in Table 1. This may cause some confusion as other developers (including some listed in the "References") refer to the new Netscape and Mozilla as "6.0 browsers." I tend to stick to what's indicated in navigator.appVersion, which would make both of these 5.0 browsers. The only exception is that I am calling Internet Explorer 5 a "5.0 browser" even though navigator.appVersion is 4.0.

Browser and Platform Detection

In order to write scripts to handle all of these browser versions, you first need to be able to detect browsers. Listing One is a simple script that does just that. I use this script in all the following examples via a script inclusion: <script src='listing_1.js'></script>

Some History

The Document Object Model (DOM) is the object model for the browser's document object. This object is available to client-side Javascript and presents an interface to all the elements in your HTML document. Prior to 4.0 browsers, the DOM API was much the same across IE and Nav. You used it to get to images via the document.images array, or forms via the document.forms array, and the like. That, along with some familiar methods like document.write(), was about the limit of the earlier DOM.

Things changed when the 4.0 browsers introduced Cascading Style Sheets (CSS) and layers. It is beyond the scope of this article to discuss CSS and layers in detail (see references for more information). Here, I will concentrate on absolutely positioned CSS elements that in Nav4 occupy their own layer. Developers use absolutely positioned CSS to place floating elements on the web page (see Example 1). The problem is that while each such element in IE4 is just an object in the DOM with some style attributes, each layer in Nav4 is like a mini web page with its own little DOM. This means that if you want to access the image object Img1 sitting in a CSS layer (which you've identified with id='L1'), you'd use document.images['Img1']for IE4, but document.layers['L1'].document.images['Img1'] for Nav4. It's easy to see that if you have layers within layers, you'd end up with scripts looking like: document.layers['layer1Id']......layers['layerNId'].document.images['Img1'].

The New DOM

W3C's Level 1 (DOM1) specification defines the object hierarchy and methods of navigating this hierarchy for the new DOM, which has been adopted by the 5.0 browsers.

DOM1 has a hierarchy of elements that are similar to that of Nav4's layers, but they are more generalized and called "nodes." Before launching into nodes, I need to distinguish between HTML "container tags" (which are all start and end tag pairs such as <form></form>, <p></p>, <body> </body>, and <html></html>, which wrap open and close tags around some content) and "noncontainer tags" (used without close tags and do not wrap around any content; these include <input>, <img>, <br>, and so on). Every tag in an HTML document is a node in the new DOM. Each container tag is the parentNode of the tags it contains, which together constitutes its childNodes array. The root node is the document object itself, and the leaf nodes are the noncontainer tags.

Unlike Nav4 layers, DOM1 offers some nice ways to walk through the DOM hierarchy as well as ways to access the nodes directly without having to step through the hierarchy at all. Table 2 lists some of these node properties and document methods. Using these interfaces, you can learn a lot about the DOM as implemented by 5.0 browsers.

Listing Two is Javascript you can use to walk the hierarchy. The function that does the bulk of the work is getChildObjects(), which takes a node as input (the other parameter is just to keep track of the indents for the display), and calls itself recursively to walk down, ultimately, to the leaf nodes beneath it. You can ignore the function prettyIndent(), which just makes everything come out nice and neat. The function showDOM() is the one you would actually call, which replaces your current document content with a large text area displaying the DOM hierarchy at the time showDOM() was called.

Example 2 uses Listing Two's scripts to display the DOM of a page similar to Example 1. Notice that I specified IDs for the script tags. This simply illustrates that you can place IDs in all HTML tags in 5.0 browsers. This isn't recommended in practice, as earlier browsers may not be able to handle extraneous IDs in the tags. Figure 1 shows the outputs of Example 2 produced by IE5 and Nav5.

Keep in mind that the DOM does not necessarily correlate to the source of the HTML (as displayed by the browser menu's view source). This can cause confusion. If you try to call showDOM() before the document finished loading, the script breaks. This makes sense since the document object only exists after the document has finished loading.

You can also try to invoke a script which alters the document content, then call showDOM() again. You'll then see the DOM in its new altered state (I'll do this in Example 3). This becomes a great debugging tool as you can try calling showDOM() at various states of your document and see how the DOM is responding to your scripts.

Another way to do this is to replace the newHTML =... and document.write... lines of the showDOM() function with alert(msg);, which will display the DOM diagram in an alert box. The alert box's lack of a scroll bar makes this method impractical for all but the simplest DOM. But you can call showDOM(objId) where objId is the ID of an HTML element. This returns only this node and the nodes beneath it.

In Figure 1, Nav5 and IE5 produced slightly different outputs. The most noticeable difference is the additional #text nodes peppered throughout the Nav5 DOM. A #text node is a leaf node that contains no HTML tags (that is, it is straight text). Consequently, Nav5 is really doing the right thing as it seems to have placed almost all occurrences of the newline character into a #text node, while IE5 seems to do so only haphazardly. In fact, except for the #text node that appears after the first <IMG> node, IE5 has completely ignored all newline characters. This inconsistency is important to keep in mind as it may affect the way you traverse the DOM hierarchy.

Another IE5 weirdness is the occurrence of a node for a nonexistent <TITLE> tag. Also in IE5, the <HTML> node does not return a parentNode; that's why its children aren't properly indented, making it appear as though <HEAD> and <BODY> are also child nodes of the document root. You can accommodate for this by uncommenting the else if (isIE5 ...) clause of the prettyIndent() function in Listing Two.

W3C's DOM specification defines various nodeTypes. Listing Two only distinguishes between Type 1, all HTML tags except CDATA and comments, and non-type 1, which include type 3 #text nodes among others. (You can read about all the node types in W3C's specification.) You can see from Figure 1 that Nav5 and IE5 treat comments differently. Nav5 observes the distinction and labels comments as type 8, while IE5 confusingly treats comments as just another HTML tag, with nodeName = '!'.

Just to be fair, I included a <TEXTAREA> tag in Example 2 to show that IE5 correctly (in my opinion) placed the enclosed text as a #text node, while Nav5 placed the text in the value attribute (as well as the defaultvalue attribute). Yet even though IE5 doesn't put the text into <TEXTAREA>'s value attribute, you can still retrieve the text string from IE5 via document.getElementById('txtarea').value.

You can spend all day using the showDOM() function from Listing Two to see how IE5 and Nav5 build the DOM and document all the little differences. I suggest you do just that, but for now I'll move on to the new DOM and its impact on your existing DHTML scripts.

DHTML and the New DOM

Just when you've gotten used to handling just two browsers (IE4 and Nav4), you now have to deal with four (IE4, IE5, Nav4, and Nav5). So if you've been using lots of if statements to handle all the browsers in your script, you'd better think twice. Using this method to handle four different browsers will make the script needlessly complicated.

Go back to Example 1. A common thing to do with floating layers is to show and hide them, move them around, change the z-index, and so on. But you've already seen how different IE4 and Nav4 can be in terms of accessing the layer/CSS object. Now there's the addition of IE5 and Nav5. Fortunately, 5.0 browsers are more alike than different, and for the most part, they can be treated similarly. But this continues to complicate the script. To place layer L1 10 pixels from the left edge of the browser window, you need to execute Example 4.

This is cumbersome to do every time you want to move an absolutely positioned element. Imagine if you're implementing a DHTML navigation menu widget with several levels of drop down menus. Writing code like Example 4 to handle all the CSS manipulation (show, hide, change z-index, and do the image rollovers) can quickly get out of control.

Listing Three accomplishes this goal using object-oriented Javascript. The constructor function CSSObject() and the method prototypes wrap some of the browser dependencies with respect to CSS and layers within a standard interface. Once you've instantiated an object using cssobj=new CSSObject(css_layer_id);, you can access all the DHTML features via the object's methods without worrying about browser versions. Notice that you use document.getElementById() to get the layer element from IE5 and Nav5, and the document.all collection to get the element from IE4. But Nav4 presents a special problem. While you would like to get the element from document.layers, it only has a collection of the top-level layers of the document. A Nested child layer can only be retrieved from the layers collection of its parent layer. Therefore, you define the function getNav4Layer(), which recursively steps through this layers hierarchy to retrieve the desired layer element.

Retrieving the layer from the document.all collection also works for IE5. But you should stick to the standard API defined by W3C as much as possible, and that means using document.getElementById().

Example 3 is an HTML document that creates two floating layers, and clicking the Move 1 or Move 2 buttons will move them to the right at 10 pixel increments. As this illustrates, using the functions in Listing Three dramatically reduces the code's complexity. Another obvious advantage to this is that if another DHTML-capable browser comes along, you only need to edit Listing One to detect it and Listing Three to handle it. Your HTML (such as that in Example 3) should never need to change.

Many programmers have taken advantage of Nav4's open(), close(), and write() functions to write HTML to a layer element. IE4's equivalent is to set the innerHTML property of a CSS-layer element. With IE4, however, this works only if you had defined your CSS-layer with the <DIV> tag (as opposed to the nearly equivalent <SPAN> tag). This was fixed with IE5, which correctly implements innerHTML for both <DIV> and <SPAN>. However, innerHTML does not work at all for IE4 on the Macintosh. Likely, most people who use this property have decided that IE4 users on the Mac constitute a negligible population. I don't agree, but enough people are using innerHTML to dynamically alter a page's content so that you should show how this can also be done with Nav5.

The bottom of Listing Three defines a write method for the CSSObject class. Notice the complexity of doing this in Nav5 versus the relative simplicity of accomplishing the same thing in Nav4 and IE5. All the methods used in the code are standard W3C DOM methods except for range.createContextualFragment(), which is a Netscape original. This method does as the name suggests — it takes a string and creates a document fragment by parsing the HTML content. The rest of the code just selects the proper place to put the fragment, removes the current content, and appends the newly parsed fragment as children nodes.

In Example 5, I also implemented the function changeLayer(), which takes a CSSObject and invokes the write() method to change the content of the CSS layer. This function can be accessed via the Change 1 and Change 2 links. Examine the DOM (via the showDOM() link) before and after changing the layers' contents. (Incidentally, Erik Arvidsson wrote a white paper, available at http://webfx.eae.net/dhtml/mozInnerHTML/mozInnerHTML.shtml, which shows how you can add the innerHTML and outerHTML properties to Nav5 HTML elements.)

Lastly, Listing Four shows two Javascript functions you can use to retrieve image and form objects by name from a document without worrying about the browser version. All browsers discussed thus far use the document.images and document.forms collections proper, with the frustrating exception of Nav4, which requires you to recursively examine each layer for the desired object.

However, even Listing Four doesn't present a satisfying solution. Since W3C and 5.0 browsers are moving toward retrieving HTML elements by their IDs, I'd like to start doing this for forms and images. But neither IE4 nor Nav4 can retrieve images and forms via IDs, so you aren't ready to abandon names. And to pass both name and ID to the functions just so you can use getElementById for Nav5 and IE5 would seem needlessly self indulgent.

DHTML Site Strategies

At this point, all the pieces are in place for you to start thinking about a cross-browser strategy. Here are the steps:

1. You should remove all extraneous spaces and newline characters because you can no longer assume browsers will ignore them.

2. Given that these browsers are moving towards identifying elements by IDs rather than names, you may be inclined to start adding IDs to all your HTML tags. Don't do this. In fact adding IDs to your HTML may cause pages to break on browsers not knowing what to do with this additional attribute. For example this

<script id='xyz' language='javascript' src='source file url'></script>

breaks on Nav4, though interestingly the following does not.

<script id='xyz' language='javascript'>
... some javascript...
</script>

The behavior is erratic enough that you would need to do some serious testing to document how each browser behaves with each HTML tag when the ID attribute is added. (This would be a great, although tedious, task for an intern with nothing better to do.) For now I'd suggest using IDs carefully and only on tags when you know it is safe (<DIV> and <SPAN>, for instance).

3. Whenever possible, wrap browser differences within an object or function that presents a uniform API as I did with CSSObject in Listing Three and getImageByName() and getFormByName() in Listing Four.

4. Finally, keep an eye on browser usage statistics and start phasing in more advanced features of the new DOM as soon as older, problematic browsers (like IE4 on the Mac and Nav4) drop off the map.

The timeline is tight. Nav 5 (commercially packaged as Netscape 6) is practically out the door. So start fixing your web sites, or start thinking of a good excuse to give your clients.

References

Andrew, Scott. "DHTML First Aid for the 6.0 Browsers," http://www.scottandrew.com/index.php?today/dhtml_w3c.html.

Andrew, Scott. "Scripting for the 6.0. Browsers," http://www.scottandrew.com/index.php?dom/index.html.

Arvidsson, Erik. "innerHTML for Mozilla," http://webfx.eae.net/dhtml/mozInnerHTML/mozInnerHTML.shtml.

Goodman, Danny. "Getting Ready for the W3C DOM," http://developer.netscape.com/viewsource/goodman_cross/goodman_cross.htm.

Goodman, Danny. Dynamic HTML, O'Reilly & Associates, 1998.

W3C DOM Level 1 and Level 2 Specifications, http://w3c.org/DOM/.

DDJ

Listing One

// in html examples this bit of code is referred to as file listing_1.js
// Detect browser version
var isNav5 = false;
var isNav4 = false;
var isNav = false;
var isIE4 = false;
var isIE5 = false;
var isIE = false;
var isWin = false;
var isMac = false;

if(navigator.appName=="Netscape") {
    isNav=true;
    if(parseInt(navigator.appVersion) == 4) isNav4=true;
    if(parseInt(navigator.appVersion) == 5) isNav5=true;
}
else if(navigator.appName=="Microsoft Internet Explorer") {
    isIE = true;
    if(navigator.appVersion.indexOf("MSIE 4") != -1) isIE4=true;
    if(navigator.appVersion.indexOf("MSIE 5") != -1) isIE5=true;
}
// Detect OS platform
if (navigator.platform.indexOf("Win") != -1 ) isWin = true;
if (navigator.platform.indexOf("Mac") != -1) isMac = true;

Back to Article

Listing Two

// in html examples this bit of code is referred to as file listing_2.js
// This function recursively steps through the node heirarchy
// and puts the structure in msg.
function getChildObjects(obj, formatIndent) {
    var msg = '';
    if (formatIndent) {
        msg = formatIndent;
    }

   if (obj.nodeType == 1) {
        // nodeType==1 for all HTML tags other than comments and CDATA.
                msg += "&lt;" + obj.nodeName;
        for (var j=0; j<obj.attributes.length; j++) {
            if (obj.attributes.item(j).nodeValue) {
               msg += " " + obj.attributes.item(j).nodeName 
                   +  x"='" + obj.attributes[j].nodeValue + "'";
            }
        }
        msg += "&gt;";
    }
    else {
        // W3C's DOM1 spec provides other nodeTypes for text, CDATA, entity 
        // reference, etc.  Not all of these are implemented by the browsers.
        msg += obj.nodeName;
    }
    msg += " nodeType: " + obj.nodeType + "\n";
    for (var i=0; i<obj.childNodes.length; i++) {
        nextIndent = prettyIndent(obj, formatIndent);
        msg += getChildObjects(obj.childNodes[i], nextIndent);
    }
    return msg;
}
// does nothing other than manage the indents for output
function prettyIndent(obj, formatIndent) {
    if (formatIndent && formatIndent.length>3 && obj.parentNode) {
        nextIndent = formatIndent.substring(0, formatIndent.length-4);
        nextIndent += (obj == obj.parentNode.lastChild)? 
                                            '      +-- ' : '|     +-- ' ;
    }
//  uncomment to fix problem caused by <HTML> node not returning a parentNode.
//  else if (isIE5 && obj.nodeName == 'HTML') {
//      nextIndent = '         +-- ';
//  }
    else {
        nextIndent = '   +-- ';
    }
    return nextIndent;
}
// calls getChildObjects and writes the returned msg to the
// text area of a new html page.
function showDOM(domObjId) {
    var obj = (domObjId)? document.getElementById(domObjId) : document;
    var msg = getChildObjects(obj);
    newHTML = "<HTML><BODY><form><textarea rows=30 cols=120'>" + msg + 
                                     "</textarea></form></BODY></HTML>"
    document.write(newHTML);
}

Back to Article

Listing Three

// in html examples this bit of code is referred to as file listing_3.js
// taks a layer id and recursively steps through Nav4's
// layer heirarchy.  Returns the layer with the specified id.
function getNav4Layer(layerId, parent) {
    var objLayer;
    var parentObj = (parent)? parent : document;
    for (var i=0; i<parentObj.layers.length && !objLayer; i++) {
        if(parentObj.layers[i].id == layerId) {
            objLayer = parentObj.layers[i];
        }
        else {
            objLayer = getNav4Layer(layerId, parentObj.layers[i]);
        }
    }
    return objLayer;
}
// object constructor for CSSObject.
function CSSObject(obj)  {
    if (isIE5 || isNav5)  {
        this.name = obj;
        this.elem = document.getElementById(obj);
        this.css = this.elem.style;
    }
    else if(isNav4)  {
        this.name = obj;
        this.elem = getNav4Layer(obj, 0);
        this.css = this.elem;
    }
    else if(isIE4)  {
        this.name = obj;
        this.elem = document.all[obj];
        this.css = this.elem.style;
    }
}
// ----------- defines the moveBy method of CSSObject -----------------------
function moveByNav(x, y) {
    this.css.left = parseInt(this.css.left) + x;
    this.css.top = parseInt(this.css.top) + y;
}
function moveByIE(x, y) {
    this.css.pixelLeft += x;
    this.css.pixelTop += y;
}
if (isNav4 || isNav5)       CSSObject.prototype.moveBy = moveByNav
else if (isIE4 || isIE5)    CSSObject.prototype.moveBy = moveByIE;
// ----------- defines the moveTo method of CSSObject -----------------------
function moveToNav(x, y) {
    this.css.left = x;
    this.css.top = y;
}
function moveToIE(x, y) {
    this.css.pixelLeft = x;
    this.css.pixelTop = y;
}
if (isNav4 || isNav5)       CSSObject.prototype.moveTo = moveToNav
else if (isIE4 || isIE5)    CSSObject.prototype.moveTo = moveToIE;
// ----------- defines the write method of CSSObject -----------------------
// What follows only works with IE4 on Win (for css objects defined with <div> 
// and not <span>), IE5 on both Win and Mac, Nav4, and Nav5.
function HTMLWriteNav4(html) {
    this.css.document.open();
    this.css.document.write(html);
    this.css.document.close();
}
function HTMLWriteIE5(html) {
    this.elem.innerHTML = html;
}
function HTMLWriteNav5(html) {
    var rng = document.createRange();
    rng.selectNodeContents(this.elem);
    rng.deleteContents();
    var htmlFrag = rng.createContextualFragment(html);
    this.elem.appendChild(htmlFrag);
}
if (isNav4)         CSSObject.prototype.write = HTMLWriteNav4
else if (isNav5)    CSSObject.prototype.write = HTMLWriteNav5
else if (isIE5 || (isIE4 && isWin))     
                    CSSObject.prototype.write = HTMLWriteIE5;

Back to Article

Listing Four

// in html examples this bit of code is referred to as file listing_4.js
// returns document's image object of given name.
// For Nav4 getImgObjNav4 is called
function getImageByName(imgName) {
    if (isIE4 || isIE5 || isNav5) {
        return document.images[imgName];
    }
    else if (isNav4) {
        return getImgObjNav4(imgName);
    }
}
// returns document's form object of given name.
// For Nav4 getFormObjNav4 is called
function getFormByName(formName) {
    if (isIE4 || isIE5 || isNav5) {
        return document.forms[formName];
    }
    else if (isNav4) {
        return getFormObjNav4(formName);
    }
}
// steps throug Nav4's layer heirarchy recursively
// to get the image object with given name
function getImgObjNav4(imgName, parent) {
    var objImage;
    var parentObj = (parent)? parent : document;
    objImage = parentObj.images[imgName];
    if (!objImage) {;
        for (var i=0; i<parentObj.layers.length && !objImage; i++) {
            objImage = getImgObjNav4(imgName, parentObj.layers[i].document);
        }
    }
    return objImage;
}
// steps throug Nav4's layer heirarchy recursively
// to get the form object with given name
function getFormObjNav4(formName, parent) {
    var objForm;
    var parentObj = (parent)? parent : document;
    objForm = parentObj.forms[formName];
    if (!objForm) {;
        for (var i=0; i<parentObj.layers.length && !objForm; i++) {
            objForm = getFormObjNav4(formName, parentObj.layers[i].document);
        }
    }
    return objForm;
}

Back to Article