We’ve seen how JavaScript can be used to dynamically update images in the page in response to user actions. But if you consider that almost all parts of the page are scriptable in modern browsers, you’ll realize that manipulating images is only the tip of the iceberg. Given browser support, you can update not just images but also text and other content enclosed in tags, particularly <<div>>s, embedded objects, forms, and even the text in the page. You’re not just limited to changing content, either. Because most objects expose their CSS properties, you can change appearance and layout as well.
Three technologies come together to provide these features: (X)HTML provides the structural foundation of content, CSS contributes to its appearance and placement, and JavaScript enables the dynamic manipulation of both of these features. This combination of technologies is often referred to as Dynamic HTML, or DHTML for short, particularly when the effect created appears to make the page significantly change its structure. We start first with the traditional example of DHTML, positioned regions, and address how developers have addressed the troublesome cross-browser issues they have encountered. Once we have clearly demonstrated the problems with this approach to DHTML, we will present DOM Standard–oriented DHTML with a smattering of Internet Explorer details where appropriate.
For many, a major perceived downside of DHTML is that, because traditional object models are so divergent, doing anything non-trivial requires careful implementation with cross-browser issues in mind. Even when the interfaces by which DHTML is realized are uniform, browsers are notorious for interpreting standards in slightly different ways, so you’ll need to carefully test your scripts to ensure their behavior is as desired in the browsers used by your demographic. In this section we provide a brief example of the cross-browser headache by exploring how to create simple DHTML effects with positioned regions that work in both standards-aware and non-standards-aware browsers. Hopefully, the inconvenience of the workarounds and various arcane issues presented will encourage readers to spend time focusing on the standards-oriented DHTML that follows this section.
Given how important positioning is for DHTML, we present here a brief review of the related CSS. CSS positioning is generally controlled with the combination of the position, top, bottom, right, and left properties. Table 15-2 lists these and other relevant properties.
CSS Property |
Description |
---|---|
position |
Defines the type of positioning used for an element: static (default), absolute, relative, fixed, or inherit. Most often absolute is used to set the exact position of an element regardless of document flow. |
Top |
Defines the position of the object from the top of the enclosing region. For most objects, this should be from the top of the content area of the browser window. |
Left |
Defines the position of the object from the left of the enclosing region, most often the left of the browser window itself. |
height |
Defines the height of an element. With positioned items, a measure in pixels (px) is often used, though others like percentage (%) are also possible. |
width |
Defines the width of an element. With positioned items, a measure in pixels (px) is often used. |
Clip |
A clipping rectangle like clip: rect (top right bottom left) can be used to define a subset of content that is shown in a positioned region as defined by the rectangle with upper-left corner at (left,top) and bottom-right corner at (right,bottom). Note that the pixel values of the rectangle are relative to the clipped region and not the screen. |
visibility |
Sets whether an element should be visible. Possible values include hidden, visible, and inherit. |
z-index |
Defines the stacking order of the object. Regions with higher z-index number values stack on top of regions with lower numbers. Without z-index, the order of definition defines stacking, with last object defined the highest up. |
There are three primary types of positioning. An element with static positioning is placed where it would normally occur in the layout of the document (also called flow positioning). An element with relative positioning is positioned at the offset given by top, bottom, left, and/or right from where it would normally occur in the layout. That is, the document is laid out and then elements with relative positioning are offset from their position by the indicated amount. The final type of positioning is absolute, meaning the element is not laid out as a normal part of the document but is positioned at the indicated offset with respect to its parent (enclosing) element.
Note |
CSS2 also supports the idea of fixed positioning, which allows an object to stay pegged to a particular location regardless of window scrolling. However, it is not supported in IE6 or before and should be avoided. |
Absolutely positioned elements not contained within any other elements (save the <<body>>) are easy to move about the page in a dynamic way using JavaScript because their enclosing element is the entire document. So any coordinates assigned to their positional properties become their position on the page. We can also hide positioned regions by setting their visibility, change their size by setting their height and width values, and even change their content using the commonly supported innerHTML property or resorting to DOM methods as discussed in Chapter 10. However, while it sounds easy in practice, there are many different ways positioned objects are accessed with JavaScript in browsers.
Netscape 4 did not provide excellent support for CSS1. However, it does support the <<layer>> tag, which provides the equivalent of positioned regions in style sheets. For example,
<<layer name="test" pagex="100" pagey="100" width="100" height="50" bgcolor="#ffff99">> This is a layer! <</layer>>
produces the same region as
<<div id="test" style="position: absolute; top: 100px; left: 100px; width: 100px; height: 50px; background-color: #ffff99;">> This is a layer! <</div>>
Based on the preceding example, you might guess that you then have to include both <<div>> and <<layer>> tags in a document in order to achieve proper layout across browsers. Fortunately, just before release, Netscape 4 adopted support for positioned <<div>> tags. Note though that this support is actually through a mapping between <<div>> regions and Layer objects. In fact, to access a positioned <<div>> object under Netscape 4, you use the layers[] collection. To demonstrate this, consider that to access a region defined by
<<div id="region1" style="position: absolute; top: 100px; left: 100px; width: 100px; height: 100px; background-color: #ffff99;">> I am positioned! <</div>>
we would use document.layers['region1']. However, once accessed, we cannot unfortunately modify the style property of a region. Yet we can modify important values such as position, size, or visibility under Netscape 4. For example, to change the visibility we would use document.layers['region1'].visibility and set the property to either hide or show. The various modifiable aspects of a positioned region map actually map directly to the properties of the Layer object. The most commonly used properties for this object are shown in Table 15-3.
Property |
Description |
---|---|
background |
The URL of the background image for the layer. |
bgColor |
The background color of the layer. |
Clip |
References the clipping region object for the layer. This object has properties top, right, bottom, and left that correspond to normal CSS clipping rectangles as well as width and height, which can be used similarly to normal width and height properties in CSS. |
document |
A reference to the Document object of the current layer. |
Left |
The x-coordinate position of the layer. |
name |
The name of the layer. |
pageX |
The x-coordinate of the layer relative to the page. |
pageY |
The y-coordinate of the layer relative to the page. |
Src |
The URL to reference the layer's content when it is not directly set within the <layer> tag itself. |
Top |
The y-coordinate position of the layer. |
visibility |
Reference to the current visibility of the layer. Values of show and hide for <layer> are equivalent to visible and hidden under CSS. Later versions of Netscape 4 map the two values so either can be used. |
window |
Reference to the Window object containing the layer. |
X |
The x-coordinate value for the layer. |
Y |
The y-coordinate value for the layer. |
zIndex |
Holds the stacking order of the layer. |
Of course, <<layer>> is an extremely proprietary tag and is not supported outside Netscape 4. In fact, in the 6.x (and later) release of the browser, Netscape removed support for this tag. We’ll see in the next few sections how Internet Explorer and DOM-compatible browsers access positioned regions.
As mentioned in Chapter 9, Internet Explorer exposes all objects in a page via the all[] collection. So to access a positioned region defined by
<<div id="region1" style="position: absolute; top: 100px; left: 100px; width: 100px; height: 100px; background-color: #ffff99;">> I am positioned! <</div>>
under Internet Explorer 4 and greater, you would use document.all['region1'] or document.all.region1 or simply region1. Once the particular object in question was accessed we could manipulate its presentation using the Style object. For example, to set the background color of the region to orange as set by the CSS property background-color, we would use document.all['region1'].style.backgroundColor = 'orange' or simply region1.style .backgroundColor='orange'. To set visibility, we would use region1.style.visibility and set the value to either visible or hidden.
The style property to JavaScript property mapping was presented in Chapter 10, but recall once again that in general you take a hyphenated CSS property and uppercase the first letter of the hyphen-separated terms, so the CSS property text-indent becomes textIndent under IE and DOM-compatible JavaScript. The next section shows a slight variation to the scheme presented here since the standard DOM supports different syntax to access a positioned region. Fortunately, since Internet Explorer 5 and beyond, we can really use either syntax interchangeably.
Access to positioned regions under a DOM-compliant browser is pretty much nearly as easy as using Internet Explorer’s all[] collection with <<div>> tags. The primary method would be to use the document.getElementById() method. Given our sample region specified with a <<div>> called “region1”, we would use document.getElementById('region1') to retrieve the region and then we can set its visibility or other style-related properties via the Style object in a similar fashion to Internet Explorer. For example, to change visibility of an object to hidden we use document.getElementById('region1').style.visibility='hidden'. Of course the question then begs: how do we get and set style properties related to layer positioning in the same way across all browsers? The next section presents one possible solution to this challenge.
As we have just seen, as well as in many other examples in the book, significant differences exist in technology support between the popular Web browsers, particularly those that are not up to date with standards. For some developers, authoring for one browser (Internet Explorer) or the standard (DOM) has seemed the best way to deal with these differences. But sometimes one must address cross-browser compatibility head-on and write markup and script that works under any browser capable of producing the intended result. This section explores this approach by creating a sample cross-browser layer library. While it is by no means the only way to implement such a library, it does illustrate common techniques used for such tasks.
From the previous sections, we can see that for layer (content region) positioning and visibility we will need to support three different technologies:
Netscape 4 proprietary <<layer>> tags
Internet Explorer 4+ all[] collections with positioned <<div>> tags
DOM-compatible browsers with positioned <<div>> tags
Given these tractable requirements, we can create a suite of JavaScript routines to change visibility and move, modify, size, and set the contents of positioned regions in major browsers fairly easily.
The first thing such a library needs to do is identify the browser of the current user. The easiest way to do this is by looking at the Document object. If we see a layers[] collection, we know the browser supports Netscape 4 layers. We can look at the all[] collection to sense if the browser supports Internet Explorer’s all[] collection syntax. Last, we can look for our required DOM method getElementById() to see if we are dealing with a DOM-aware browser. The following statements show how to set some variables indicating the type of browser we are dealing with:
var layerobject = ((document.layers) ? (true) : (false)); var dom = ((document.getElementById) ? (true) : (false)); var allobject = ((document.all) ? (true) : (false));
Once we know what kind of layer-aware browser we are dealing with, we might define a set of common functions to manipulate the layers. We define the following layer functions to handle common tasks:
function hide(layerName) { } function show(layerName) { } function setX(layerName, x) { } function setY(layerName, y) { } function setZ(layerName, zIndex) { } function setHeight(layerName, height) { } function setWidth(layerName, width) { } function setClip(layerName, top, right, bottom, left) { } function setContents( ) { }
These are just stubs that we will fill out shortly, but first we will need one special routine in all of them to retrieve positioned elements by name, since each approach does this slightly differently.
function getElement(layerName, parentLayer) { if(layerobject) { parentLayer = (parentLayer) ? parentLayer : self; layerCollection = parentLayer.document.layers; if (layerCollection[layerName]) return layerCollection[layerName]; /* look through nested layers */ for (i=0; i << layerCollection.length;) return(getElement(layerName, layerCollection[i++])); } if (allobject) return document.all[layerName]; if (dom) return document.getElementById(layerName); }
Notice the trouble that the possibility of nested <<layer>> or <<div>> tags under Netscape causes. We have to look through the nested layers recursively until we find the object we are looking for or until we have run out of places to look.
Once a positioned element is accessed, we can then try to change its style. For example, to hide and show a positioned region we might write
function hide(layerName) { var theLayer = getElement(layerName); if (layerobject) theLayer.visibility = 'hide'; else theLayer.style.visibility = 'hidden'; } function show(layerName) { var theLayer = getElement(layerName); if (layerobject) theLayer.visibility = 'show'; else theLayer.style.visibility = 'visible'; }
The other routines are similar and all require the simple conditional detection of the browser objects to work in all capable browsers.
Of course, there are even more issues than what has been covered so far. For example, under older Opera browsers, we need to use the pixelHeight and pixelWidth properties to set the height and width of a positioned region. In order to detect for the Opera browser, we use the Navigator object to look at the user-agent string, as discussed in Chapter 17. Here we set a Boolean value to indicate whether we are using Opera by trying to find the substring “opera” within the user-agent string.
opera = (navigator.userAgent.toLowerCase().indexOf('opera') != -1);
Once we have detected the presence of the browser, we can write cross-browser routines to set height and width, as shown here:
/* set the height of layer named layerName */ function setHeight(layerName, height) { var theLayer = getElement(layerName); if (layerobject) theLayer.clip.height = height; else if (opera) theLayer.style.pixelHeight = height; else theLayer.style.height = height+"px"; } /* set the width of layer named layerName */ function setWidth(layerName, width) { var theLayer = getElement(layerName); if (layerobject) theLayer.clip.width = width; else if (opera) theLayer.style.pixelWidth = width; else theLayer.style.width = width+"px"; }
The same situation occurs for positioning with Opera, as it requires the use of pixelLeft and pixelTop properties rather than simply left and top to work. See the complete library for the function for setting position that is similar to the previous example.
We must also take into account some special factors when we write content to a layer. Under Netscape 4, we use the Document object methods like write() to rewrite the content of the layer. In Internet Explorer and most other browsers, we can use the innerHTML property. However, under a strictly DOM-compatible browser, life is somewhat difficult, since we would have to delete all children from the region and then create the appropriate items to insert. Because of this complexity and the fact that most DOM-supporting browsers also support innerHTML, we punt on this feature. This leaves Opera versions prior to Opera 7, though we wrote the code in such a manner that simply nothing happens rather than an error message being displayed.
function setContents(layerName, content) { var theLayer = getElement(layerName); if (layerobject) { theLayer.document.write(content); theLayer.document.close(); return; } if (theLayer.innerHTML) theLayer.innerHTML = content; }
We skipped discussion of a few routines, but their style and usage follow the ones already presented. The complete layer library is presented here:
/* layerlib.js: Simple Layer library with basic compatibility checking */ /* detect objects */ var layerobject = ((document.layers) ? (true) : (false)); var dom = ((document.getElementById) ? (true) : (false)); var allobject = ((document.all) ? (true) : (false)); /* detect browsers */ opera=navigator.userAgent.toLowerCase().indexOf('opera')!=-1; /* return the object for the passed layerName value */ function getElement(layerName,parentLayer) { if(layerobject) { parentLayer = (parentLayer)? parentLayer : self; layerCollection = parentLayer.document.layers; if (layerCollection[layerName]) return layerCollection[layerName]; /* look through nested layers */ for(i=0; i << layerCollection.length;) return(getElement(layerName, layerCollection[i++])); } if (allobject) return document.all[layerName]; if (dom) return document.getElementById(layerName); } /* hide the layer with id = layerName */ function hide(layerName) { var theLayer = getElement(layerName); if (layerobject) theLayer.visibility = 'hide'; else theLayer.style.visibility = 'hidden'; } /* show the layer with id = layerName */ function show(layerName) { var theLayer = getElement(layerName); if (layerobject) theLayer.visibility = 'show'; else theLayer.style.visibility = 'visible'; } /* set the x-coordinate of layer named layerName */ function setX(layerName, x) { var theLayer = getElement(layerName); if (layerobject) theLayer.left=x; else if (opera) theLayer.style.pixelLeft=x; else theLayer.style.left=x+"px"; } /* set the y-coordinate of layer named layerName */ function setY(layerName, y) { var theLayer = getElement(layerName); if (layerobject) theLayer.top=y; else if (opera) theLayer.style.pixelTop=y; else theLayer.style.top=y+"px"; } /* set the z-index of layer named layerName */ function setZ(layerName, zIndex) { var theLayer = getElement(layerName); if (layerobject) theLayer.zIndex = zIndex; else theLayer.style.zIndex = zIndex; } /* set the height of layer named layerName */ function setHeight(layerName, height) { var theLayer = getElement(layerName); if (layerobject) theLayer.clip.height = height; else if (opera) theLayer.style.pixelHeight = height; else theLayer.style.height = height+"px"; } /* set the width of layer named layerName */ function setWidth(layerName, width) { var theLayer = getElement(layerName); if (layerobject) theLayer.clip.width = width; else if (opera) theLayer.style.pixelWidth = width; else theLayer.style.width = width+"px"; } /* set the clipping rectangle on the layer named layerName defined by top, right, bottom, and left */ function setClip(layerName, top, right, bottom, left) { var theLayer = getElement(layerName); if (layerobject) { theLayer.clip.top = top; theLayer.clip.right = right; theLayer.clip.bottom = bottom; theLayer.clip.left = left; } else theLayer.style.clip = "rect("+top+"px "+right+"px "+" "+bottom+"px "+left+"px )"; } /* set the contents of layerName to passed content*/ function setContents(layerName, content) { var theLayer = getElement(layerName); if (layerobject) { theLayer.document.write(content); theLayer.document.close(); return; } if (theLayer.innerHTML) theLayer.innerHTML = content; }
We might save this library as “layerlib.js” and then test it using an example document like the one that follows here. If you want to avoid a lot of typing, make sure to visit the support site at www.javascriptref.com.
<<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">> <<html xmlns="http://www.w3.org/1999/xhtml">> <<head>> <<title>>Cross-browser Layer Tester<</title>> <<meta http-equiv="content-type" content="text/html; charset=utf-8" />> <<script type="text/javascript" src="layerlib.js">><</script>> <</head>> <<body>> <<div id="region1" style="position: absolute; top: 10px; left: 300px; width: 100px; height: 100px; background-color: #ffff99; z-index: 10;">> I am positioned! <</div>> <<div id="region2" style="position: absolute; top: 10px; left: 275px; width: 50px; height: 150px; background-color:#33ff99; z-index: 5;">> Fixed layer at z-index 5 to test z-index <</div>> <<br />><<br />><<br />><<br />><<br />><<br />> <<hr />> <<form name="testform" id="testform" action="#" method="get">> Visibility: <<input type="button" value="show" onclick="show('region1');" />> <<input type="button" value="hide" onclick="hide('region1');" />> <<br />><<br />> x: <<input type="text" value="300" name="x" id="x" size="4" />> <<input type="button" value="set" onclick="setX('region1',document.testform.x.value);" />> y: <<input type="text" value="10" name="y" id="y" size="4" />> <<input type="button" value="set" onclick="setY('region1',document.testform.y.value);" />> z: <<input type="text" value="10" name="z" id="z" size="4" />> <<input type="button" value="set" onclick="setZ('region1',document.testform.z.value);" />> <<br />><<br />> Height: <<input type="text" value="100" name="height" id="height" size="4" />> <<input type="button" value="set" onclick="setHeight('region1',document.testform.height.value);" />> Width: <<input type="text" value="100" name="width" id="width" size="4" />> <<input type="button" value="set" onclick="setWidth('region1',document.testform.width.value);" />> <<br />><<br />> Clipping rectangle: <<br />> top: <<input type="text" value="0" name="top" id="top" size="4" />> left: <<input type="text" value="0" name="left" id="left" size="4" />> bottom: <<input type="text" value="100" name="bottom" id="bottom" size="4" />> right: <<input type="text" value="100" name="right" id="right" size="4" />> <<input type="button" value="set" onclick="setClip('region1',document.testform.top.value, document.testform.right.value, document.testform.bottom.value, document.testform.left.value);" />> <<br />><<br />> <<input type="text" name="newcontent" id="newcontent" size="40" value="I am positioned!" />> <<input type="button" value="set content" onclick="setContents('region1',document.testform.newcontent.value);" />> <</form>> <</body>> <</html>>
Note |
If you type this example into an HTML document, be sure to fix the line wrapping: neither attribute values nor string literals in JavaScript are permitted to span multiple lines. |
A rendering of the library and example in action is shown in Figure 15-3.
Playing around with this script, you will find that you might encounter problems under Netscape 4 if you position the layer to cover the form elements in the page. You also may encounter a resize bug that causes the page to lose layout on window resize. The first problem is generally not solvable, but we can solve the latter problem by adding a somewhat clunky fix that reloads the page every time it is resized. It is presented here for readers to add to their library as a fix for this strictly Netscape 4 problem.
/* Reload window in Nav 4 to preserve layout when resized */ function reloadPage(initialload) { if (initialload==true) { if ((navigator.appName=="Netscape") && (parseInt(navigator.appVersion)==4)) { /* save page width for later examination */ document.pageWidth=window.innerWidth; document.pageHeight=window.innerHeight; /* set resize handler */ onresize=reloadPage; } } else if (innerWidth!=document.pageWidth || innerHeight!=document.pageHeight) location.reload(); } /* call function right away to fix bug */ reloadPage(true);
In the final examination, the harsh reality of DHTML libraries like the one presented here is that minor variations under Macintosh browsers and the less common JavaScript-aware browsers (such as Opera) can ruin everything. The perfect application of cross-browser DHTML is certainly not easily obtained, and significant testing is always required. The next section explores standards-oriented DHTML, which should soon provide at least some relief from cross-browser scripting headaches.