We who author leading-edge Web applications that must work on a broad spectrum of browser platforms can expect to play all ends against the middle for some time to come -- if not indefinitely. Standards efforts are always welcome in this space, but, as happens so often, the browser makers can and do develop new features and technologies much faster than standards bodies can study and adopt them.
It's happening again in the realm of Dynamic HTML (DHTML), a broad area that includes the ability to change more HTML page content and appearances on the fly than ever before. In this article, I'll focus on one fun aspect of DHTML: scripting the precise positioning and visibility of elements on a page. More specifically, I'll demonstrate techniques you can use to create a single DHTML page and scripts that accommodate the different object models in Netscape Communicator 4 (which includes Navigator 4) and Microsoft Internet Explorer 4.
Over and above this common standard, Netscape and Microsoft have applied these concepts to their own document object models and have developed different ways of conceptualizing these positionable entities. Netscape calls these items layers, and has even created a <LAYER> tag to facilitate adding such items into a document. Microsoft, on the other hand, refers to such items as styles. The challenge we scripters face is controlling Netscape's layers and Microsoft's styles to accomplish the same job.
Explaining the details of each of these objects extends way beyond the scope of this article, but suffice it to say that the vocabulary for scripted properties and methods of Netscape layers has little in common with that of Microsoft styles. These problems can be conquered with a bit of version checking in scripts or custom API creation, as I'll show later. In the meantime, let's look at how to create these objects so that both Navigator and Internet Explorer can use them.
In an effort to be respectful and diplomatic at the same time (if not downright weasely), I will hereafter refer to generic instances of these objects as positionable thingies.
<HTML> <HEAD> <STYLE TYPE="text/css"> <!-- #thingy1Name {attributes} #thingy2Name {attributes} ... --> </STYLE> </HEAD>The attributes for each item must include the position attribute. Table 1 shows the most common attributes you would set for a positionable thingy.
Attribute name | Values |
---|---|
position | absolute | relative |
left | Pixel position relative to outer element |
top | Pixel position relative to outer element |
visibility | visible | hidden | inherit |
z-index | Integer layer position in stack |
The syntax for these attributes is different from the normal HTML attribute settings you may be used to. All attributes for a given object are grouped together inside one set of curly braces. Attribute names are separated from their values by colons. And multiple attributes are delimited by semicolons. Therefore, to set an absolute-positioned item named myImage to a top-left position of 200, 250 and initially hide it, the syntax would be as follows:
#myImage {position:absolute; top:200; left:250; visibility:hidden}It may take you awhile to get over using the equal sign between attribute name and value, as you're accustomed to in HTML tags.
The other half of the formula is an HTML tag in the document that associates content with the positioning attributes named and defined in the <STYLE> tag. A convenient tag to use for this is the <SPAN>...</SPAN> tag set. In that tag you add an ID attribute that points to the name of the item properties defined earlier, as in this code:
<SPAN ID=myImage> [HTML content of any kind here] </SPAN>No matter where in the <BODY> section this <SPAN> tag occurs, it obeys the positioning attributes of the like-named definition in the <STYLE> tag.
document.layerObjectNameIn this syntax, layerObjectName is the name you've assigned to the layer. If you need to adjust the content of that layer, the reference must go one level deeper. For example, to access an image object that lives in a layer, the reference would be as follows:
document.layerObjectName.document.images["imageName"]In other words, you use the same approach for building references between nested layers and their documents as you do when building references between nested frames.
The Internet Explorer syntax, on the other hand, includes a way to globally reference all style objects inside the main document. A reference to the all property of the document can reach a uniquely named style object in the following manner:
document.all.styleObjectName.styleTo reach an image object that lives in one of these style objects, the reference doesn't have to specify the style object, but must point directly at the uniquely named image object:
document.all.imageNameBut getting back to positionable thingies: To make a property accessible by both browsers, you need to construct your references either along separate paths (based on the browser version) or by creating what I call platform equivalencies.
The "separate paths" methodology entails setting global variables as flags for each platform. For example, you would open your page's scripts with the following segment:
var isNav4, isIE4 if (navigator.appVersion.charAt(0) == "4") { if (navigator.appName == "Netscape") { isNav4 = true } else if (navigator.appVersion.indexOf("MSIE") != -1) { isIE4 = true } }With these variables in place, you can use them elsewhere to branch to the platform-specific references:
if (isNav4) { document.layer1.visibility = "hidden" document.layer2.visibility = "visible" } if(isIE4) { document.all.layer1.style.visibility = "hidden" document.all.layer2.style.visibility = "visible" }Or, by doing a little more work up front, you can set some global variables to comparable portions of both platform references, and assemble one reference that works for both platforms:
var range = "" // empty for Navigator 4 var styleObj = "" // ditto if (navigator.appVersion.charAt(0) == "4") { if (navigator.appVersion.indexOf("MSIE") != -1) { range = ".all" styleObj = ".style" } } ... eval("document" + range + ".layer1" + styleObj + ".visibility = 'hidden'") eval("document" + range + ".layer2" + styleObj + ".visibility = 'visible'")What you gain in fewer statements per property change, you lose in increased complexity of syntax -- you must make sure your eval() statement has all the dots in the right place. This "platform equivalency" methodology works best when your scripts make many changes to only a few objects, and those changes are to the properties that both Navigator and Internet Explorer have in common. Later in this article, you'll see an example where the two-path approach to property access works better.
That denominator encompasses three basic modifiable properties of positionable thingies:
Table 2 shows a selection of properties for the Navigator layer and Internet Explorer style. Items that are the same for both are shown in bold.
Navigator layer property | Comments | IE style
property |
---|---|---|
left | The Microsoft style object has a left property, but the value is a string including the unit of measure (for example, "20px"). To perform math on the left property of the style object, use the pixelLeft property. | pixelLeft |
top | The same situation as with left and pixelLeft. | pixelTop |
visibility | The Netscape layer object returns possible values of show, hide, and inherit; the Microsoft style object returns visible, hidden, and inherit. But the layer object can be set to the style object property value names without complaint. | visibility |
zIndex | 100 percent agreement on the integer values of this stacking order property. | zIndex |
background | Value is a URL of a background image. | background |
bgColor | Different property names, but the same color values, including Netscape plain-language names. | backgroundColor |
Unlike the Navigator layer object, the Internet Explorer style object does not have methods for moving the object. But both objects change their locations if you modify their respective positioning properties. One other important difference is that if a style object doesn't have one of its properties explicitly assigned in the <STYLE> tag, the property value is an empty string (at least in Internet Explorer 4 Preview 2); the layer object assigns default values to virtually all properties, whether they appear in the <STYLE> tag or not.
Dealing with these inconsistencies in a complex document can become tedious. Fortunately, you have one additional alternative for distancing yourself further from day-to-day concerns about platform inconsistencies: Generate your own set of APIs in JavaScript to take care of both platforms. Such an API consists of a function that stands in for each of the positioning tasks that your positionable thingies need on the page. You essentially create your own metalanguage for CSS-P for moving, hiding, showing, and restacking objects in your application. If you build a library of these functions, you can link it into any CSS-P page as an external .js file.
If you're using Navigator 4, you can view Fish Example 2 to see how the original version works; it uses the <LAYER> tag and the layer object properties and methods.
Now look at the CSS-P version, Fish Example 3. The full source code is also available. I've added a couple of extra touches, such as a Stop button and status bar messages that reveal a little more about what's going on as the fish swims.
Let me go over the source code section by section to reveal my design decisions along the way.
The first order of business is to create the positionable thingies by using standard CSS-style syntax:
<HTML> <HEAD> <TITLE>Swimming Fish</TITLE> <STYLE TYPE="text/css"> <!-- #bluepole {position:absolute; top:150; left:160; z-index:1;} #redpole {position:absolute; top:150; left:260; z-index:4;} #greenpole {position:absolute; top:150; left:360; z-index:2;} #fish {position:absolute; top:170; left:40; z-index:3;} #waiting {position:absolute; top:100; left:50;} #fishlink {position:absolute; top:100; left:10; visibility:hidden;} --> </STYLE> </HEAD>The objects include three colored poles, a fish object that displays different images depending on the swimming direction, a "Please wait" message that shows as images load, and an object containing the control buttons (initially hidden from view). Only the pole and fish objects need to have their z-index attributes set, because the other objects don't get involved with the stacking order part of this example.
In the next section, some script statements establish key global values for the entire page:
<SCRIPT LANGUAGE="JavaScript"> // set browser platform global vars var isNav4, isIE4 var range = "" // empty for Navigator 4 var styleObj = "" if (navigator.appVersion.charAt(0) == "4") { if (navigator.appName == "Netscape") { isNav4 = true } else { isIE4 = true range = ".all" styleObj = ".style" } }As you'll soon see, I need both the two-path and platform equivalency methods of handling the different platform syntax. The statements above cover me both ways, setting variables for platform flags (isNav4 and isIE4) and for the joint reference components (range and styleObj).
Some additional preloading of image objects and global variables follows:
// pre-cache both fish images and assign to objects // for easy image swapping later var fishForward = new Image() fishForward.src = "images/fish1.gif" var fishBackward = new Image() fishBackward.src = "images/fish2.gif" // global switch for fish direction var direction = "forward" // global ID for fish motion timeout var timeoutIDThere are only two fish images, each one an animated GIF. One faces to the right ("forward" in this application); the other faces left. A global value stores the direction in which the fish is currently swimming. Another holds the timeout ID for the repetitious calls to the function that inches the fish along its path.
The first function -- showForm() -- is called by the window's onLoad event handler:
// show 'waiting' message while images load // then hide it and show the animation buttons' layer/style function showForm() { // both browsers covered in each statement eval("document" + range + ".waiting" + styleObj + ".visibility = 'hidden'") eval("document" + range + ".fishlink" + styleObj + ".visibility = 'visible'") // commented out is the two-path method for comparison // if (isNav4) { // document.waiting.visibility = "hidden" // document.fishlink.visibility = "visible" // } // if(isIE4) { // document.all.waiting.style.visibility = "hidden" // document.all.fishlink.style.visibility = "visible" // } return false; }The sole job of this function is to swap the visibility of the waiting and fishlink (button) objects after all the images load. I demonstrate the platform equivalency method of addressing the objects in both platforms. Because the visibility property is the same on both platforms, this approach works well here. So that you can better understand what's going on, I also include (commented out) the two-path method of doing the same action. This method may use many more lines of code, but to me it is much more readable and maintainable.
Next comes the master function that moves the fish when the user clicks the "Move the Fish" button:
// start that fish swimmin' function moveFish3() { // cancel current ID so repeated invocations // don't trigger multiple timeouts clearTimeout(timeoutID) // set status bar message showFishLayer() // Nav4 returns integers for left; // IE4 returns "nnpx". Must use pixelLeft in IE4 if (isNav4) { var position = document.fish.left } if (isIE4) { var position = document.all.fish.style.pixelLeft } if (direction == "forward") { moveForward(position) } else { moveBackward(position) } // call this function again after 10 ms to nudge fish timeoutID = setTimeout("moveFish3()", 10); return; }The moveFish3() function's first task is to clear any existing timeoutID value. Failure to do so will cause additional clicks of the button to start what look like separate timeout threads, causing the fish to swim faster and faster as more instances of the setTimeout() statement per second invoke this function.
Next I call the showFishLayer() function that displays information about the current path in the status bar. (I'll discuss status bar messages in more detail later.)
Moving the fish requires setting its horizontal location to the right or left by 5 pixels, depending on the direction. To start the process, I must get the current position (since I won't be using the Netscape layer.moveBy() method here). As described earlier, the two platforms have different property names for the location values: left and pixelLeft. The platform equivalency method fails here, because one statement cannot conveniently be used for both platforms. Thus, I call upon the global platform flags set earlier to create two paths to set the position variable to the current setting.
Depending on the current direction (global variable), I then hand off processing to moveForward() or moveBackward(), passing the current position as a parameter. Once the desired routine has finished, the setTimeout() method sets a wake-up call to invoke this same moveFish3() function in 10 milliseconds.
The first function that actually moves the fish object shifts it to the right by 5 pixels:
// nudge fish to the right 5 pixels until // it reaches right edge; then turn him around function moveForward(position) { if (position < 450) { if (isNav4) { document.fish.left += 5 } if (isIE4) { document.all.fish.style.pixelLeft += 5 } } else { setPoles("new"); changeDirection(); } }I chose to change the Navigator layer's left property, but I could have also used the moveBy() method. For Internet Explorer 4, setting the pixelLeft property is the way to move a style horizontally. Regardless of platform, this motion works only while the position of the fish object is less than 450 pixels. If the position reaches that level, two functions change the z-order of the poles and direction of the fish (below).
Moving the fish in the opposite direction is very similar:
// nudge fish to the left 5 pixels until // it reaches left edge; then turn him around function moveBackward(position) { if (position > 10) { if (isNav4) { document.fish.left -= 5 } if (isIE4) { document.all.fish.style.pixelLeft -= 5 } } else { setPoles("reset"); changeDirection(); } }Adjustments to the position are negative values because the object needs to move to the left (a decreasing left position) -- until, that is, the position reaches 10, at which time the pole z-order is changed once again and fish direction swapped.
Changing direction requires changing a couple of settings:
// swap image in the mainFish layer/object and // change global direction value function changeDirection() { // different references for image in layer/style if (isNav4) { var fishImg = document.fish.document.images["mainFish"] } if (isIE4) { var fishImg = document.all.mainFish } if (direction == "forward") { direction = "backward"; fishImg.src = fishBackward.src; } else { direction = "forward"; fishImg.src = fishForward.src; } return; }I first get a reference to the image contained in the fish object. Notice that the Navigator reference follows the hierarchy through the layer to the document on the layer. Then, depending on the current setting of the direction global variable, the image is swapped in the image object, and the global value is changed.
Changing the pole order requires changing the zIndex property values for four objects. This is a good place to demonstrate the power of creating your own API for a particular positioning task. I define one function, setZIndex(), which takes care of all the platform dealings for the zIndex property in one place:
// cross-platform API for setting zIndex property function setZIndex(objectName, zOrder) { var theObj if (isNav4) { theObj = eval("document." + objectName) } if (isIE4) { theObj = eval("document.all." + objectName + ".style") } if (theObj) { theObj.zIndex = zOrder } }To change the order of each object, I invoke my API, passing the object name and the desired zIndex property value:
// change the stacking order of the poles and the fish function setPoles(type) { // call my cross-platform API for zIndex setZIndex("bluepole", ((type == "reset" ) ? 1 : 3)) setZIndex("redpole", ((type == "reset" ) ? 4 : 1)) setZIndex("greenpole", ((type == "reset" ) ? 2 : 4)) setZIndex("fish", ((type == "reset" ) ? 3 : 2)) return; }I had arbitrarily assigned a type to the two orders: reset (the original) and new (the order for backward swimming). I use that type (passed as a parameter to the setPoles() method) to help shrink the tests of all four objects' zIndex settings to simple conditional expressions.
The last two functions stop the fish and set the status bar, respectively:
// give him a rest function stopFish3() { clearTimeout(timeoutID) window.status = "" } // fish in front or behind red pole? function showFishLayer() { window.status = (direction == "forward") ? "Fish BEHIND red pole and IN FRONT OF others." : "Fish IN FRONT OF red pole and BEHIND others." } </SCRIPT>The message in the status bar shows the user what to look for in the ordering of pole and fish objects.
Now we come to the HTML that generates the elements on the page:
<BODY BGCOLOR="#ffffff" onload="showForm();"> <H1>Fish Example 3</H1> (For cross-platform CSS-P deployment) <SPAN ID=waiting> <H2>Please wait while the fish loads...</H2> </SPAN> <SPAN ID=fishlink> <FORM> <INPUT type=button value="Move the Fish" onClick="moveFish3(); return false;"> <INPUT type=button value="Stop the Fish" onClick="stopFish3(); return false;"> </FORM> </SPAN>The first two objects are contained by <SPAN> tags, with their ID attributes pointing to the corresponding positioning style attributes defined at the head of the document. One object contains nothing more than an <H2>-level block of text. When the page finishes loading the images, that object is hidden, and the second object, containing a form with two buttons, is shown.
The balance of the HTML creates the objects for the poles and fish:
<SPAN ID=bluepole> <IMG SRC=images/bluepole.gif> </SPAN> <SPAN ID=redpole> <IMG SRC=images/redpole.gif> </SPAN> <SPAN ID=greenpole> <IMG SRC=images/greenpol.gif> </SPAN> <SPAN ID=fish> <IMG NAME="mainFish" SRC=images/fish1.gif > </SPAN> </BODY> </HTML>The fish object displays the forward-facing image at the outset. The pole images could be listed in the HTML in any order, because their z-order is hard-coded by their position attributes in the head.
It is important to note that I changed the sequence of the HTML tags from what was in the original layer version of this example. One concern I had was how the appearance would degrade in non-DHTML browsers. The layout in the document looks much better in older browsers, although nothing nearly as intelligent as the positioned version (and the button scripts should be blocked for non-DHTML browsers, too).
Author and consultant Danny Goodman's twenty-fifth book is JavaScript Bible, published by IDG Books. His latest title, The Official Marimba Guide to Bongo, encompasses the Castanet technology that is built into Netscape Netcaster.