When Dynamic HTML was first introduced, a lot of the excitement it generated with its new features was eclipsed by the realization that a dynamic web page in NN 4 wouldn't work in IE4 and vice versa. A lot of finger pointing and blame-apportioning went on. This was aimed at the two main browser vendors for creating their versions of the language in advance/defiance of the standards, at the book publishers for encouraging this diversity by explaining the two methods of Dynamic HTML as almost two separate languages, at W3C for failing to agree to a standard method before the browsers were released, and at the web page developers themselves for daring to use two separate sets of code. It isn't really fair to blame anyone for this though.
The heart of the matter was that Netscape had created a <layer> tag as a method of implementing dynamic content. Allegedly, it had initially received positive sounds from the W3C standards committee that its method of Dynamic HTML would be adopted as the standard. Unfortunately, as the rumor goes, Netscape's method was ditched when the standards authority found out that in fact the general methods proposed by Microsoft were easier, more intuitive, and more workable. By then Netscape had almost completed its browser and was irreversibly committed to releasing it.
Whether we choose to blame one particular side or the other is irrelevant, because ultimately Dynamic HTML became a browser-specific language, and the Microsoft method became the de facto way of using it. This isn't to say that Microsoft's implementation was perfect. Netscape's latest browsers, NN 6 and 7, were written under the direction of the Open Source group, Mozilla, by an army of volunteers, and goes a long way toward reversing the browser-specific pitfalls that have dogged Dynamic HTML for a long time. In short. it brings NN up to date with the standards, and in many cases NN surpasses IE's compliance with them.
As described, the DOM is a standard that provides a generic set of properties, methods, and events for any language and therefore any browser to address the HTML document and manipulate it. What we're now going to do is go back to some examples from the previous chapter and demonstrate how to translate them into something that uses only DOM standards and methods, and, if this isn't possible, into something that provides a functioning dynamic page that can be viewed on two types of browsers without requiring two completely separate sets of code.
This isn't intended to be an entirely exhaustive guide as to how to get our page working in both browsers, but by examining four examples, we hope to cover a lot of the common ground between the two.
Our first example takes the animated text example from the previous chapter. In fact, we saw three versions of this example in the last chapter: one that worked on IE 4+, one that worked on NN 4.x, and one that worked on both IE 4+ and NN 4+. The last example made use of browser-specific code to accommodate the different browser types. Here we will rewrite the example for DOM-compliant browsers, with no browser-specific code necessary.
Start up a text editor and type the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <script language=JavaScript type="text/javascript"> var paraOneLeft = 100; var paraTwoLeft = 400; var switchDirection = false; function window_onload() { window.setInterval("moveParas()",50); } function moveParas() { var paraOne = document.getElementById("paraOneid"); var paraTwo = document.getElementById("paraTwoid"); if (switchDirection == false) { paraOneLeft++; paraTwoLeft--; if (paraOneLeft == 400) { switchDirection = true; } } else { paraOneLeft--; paraTwoLeft++; if (paraOneLeft == 100) { switchDirection = false; } } paraOne.style.left = paraOneLeft + 'px'; paraTwo.style.left = paraTwoLeft + 'px'; } </script> </head> <body language=JavaScript type="text/javascript" onload="return window_onload()"> <P STYLE="position:absolute;left:100px;top:30px" ID="paraOneid">Para 1</P> <P STYLE="position:absolute;left:400px;top:10px" ID="paraTwoid">Para 2</P> </body> </html>
Save this as animate.htm. Then open this in a browser and observe the animated text. (See Figures 13-15, 13-16, and 13-17.)
The code relies on two functions to make it work. The first window_onload() function has not changed from our previous examples. It is connected to the onload event handler given in the <body> tag, which fires when the window is opened. It is used to start a timer, which calls the second function, moveParas(), at intervals of 50 milliseconds.
Before we call the window_onload() function, we set three global variables, two of which initialize the starting positions of our moving text, and the other of which sets a variable that will be used to decide on the direction of scrolling.
var paraOneLeft = 100; var paraTwoLeft = 400; var switchDirection = false; function window_onload() { window.setInterval("moveParas()",50); }
The second function is the one that does the animation and is the one we have made changes to in this example. We start by using the getElementById() method of the document object to return a reference to both paragraph elements on the screen. This is the main difference between this example and the ones we saw before.
function moveParas() { var paraOne = document.getElementById("paraOneid"); var paraTwo = document.getElementById("paraTwoid");
The next part of the function should be familiar. If we have no need to reverse the direction of the scrolling (that is, the pieces of text haven't reached the boundaries we set of 100 and 400), we can add one to the variable that contains the left position of the first piece of text, and subtract one from the variable that contains the left position of the second piece of text. Note that this doesn't amend the style properties themselves as yet; it just updates the variable they will be referencing.
if (switchDirection == false) { paraOneLeft++; paraTwoLeft--;
If we've reached 400 pixels with the first paragraph, we need to set our switchDirection variable to reflect this. This variable ensures that the scrolling direction will switch on the next call of the function.
if (paraOneLeft == 400) { switchDirection = true; } }
If we've switched direction, the else statement comes into play. We subtract one from our left position variable for our first piece of text and add one to the second piece of text's left position variable.
else { paraOneLeft--; paraTwoLeft++;
We then again run a check to see whether the paragraphs have reached the boundaries we imposed, and if so we reverse direction again.
if (paraOneLeft == 100) { switchDirection = false; } }
It's left to the last two lines to actually animate the text by assigning the current value of whatever's in the paraOneLeft and paraTwoLeft variables, which were recently incremented and decremented by one to alter the style.left properties.
paraOne.style.left = paraOneLeft + 'px'; paraTwo.style.left = paraTwoLeft + 'px'; }
Thus our example becomes an infinite loop, because the paragraphs are shuttled back and forth across the screen within specified limits they can't go beyond.
The next example is also adapted from one in the previous chapter. Recall the innertext_IE.htm example in which we supplied the user with a text box and two buttons that allowed the user to retrieve or change the text written in the page. The example we give here is slightly more complex than that example, and needs extra coding to ensure that it is DOM compliant.
Start up a text editor and type the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <script language=JavaScript type="text/javascript"> function bttnAdd_onClick() { var elemPara = document.getElementById("Para1"); var elemTxtBox = document.getElementById("textarea1"); var oldTxtNode = elemPara.firstChild; var oldText = oldTxtNode.nodeValue; var newText = oldText + elemTxtBox.value; var newTxtNode = document.createTextNode(newText); elemPara.replaceChild(newTxtNode, oldTxtNode); } function bttnNew_onClick() { var elemPara = document.getElementById("Para1"); var elemTxtBox = document.getElementById("textarea1"); var newText = elemTxtBox.value; var newTxtNode = document.createTextNode(newText); var elemNew = document.createElement("P"); elemNew.appendChild(newTxtNode); document.body.insertBefore(elemNew, elemTxtBox); } function bttnRem_onClick() { var elemTxtBox = document.getElementById("textarea1"); var elemLastPara = elemTxtBox.previousSibling; document.body.removeChild(elemLastPara); } </script> </head> <body> <P id="Para1"> Some text... </P> <textarea cols=30 id="textarea1" rows=3></textarea> <BR> <input type="button" value="Add Text" name="bttnAdd" onclick="bttnAdd_onClick()"> <input type="button" value="New Paragraph" name="bttnNew" onclick="bttnNew_onClick()"> <input type="button" value="Remove Paragraph" name="bttnRem" onclick="bttnRem_onClick()"> </body> </html>
Save this as textnode.htm and open it in a browser. Type some text into the textarea, as shown in Figure 13-18.
If we click the Add Text button, we see our text added to the textarea in the page. Click the New Paragraph button to see our text appear as a new paragraph, as shown in Figure 13-19.
Click the Remove Paragraph button and the second occurrence of our text disappears again, as shown in Figure 13-20.
Note |
Note with this example that errors will occur if we try to remove or add text to a paragraph that has previously already been removed. |
In this example we have three functions that do all the work, each connected to one of the three buttons in the form. The first function is activated if we click the Add Text button.
function bttnAdd_onClick() { var elemPara = document.getElementById("Para1"); var elemTxtBox = document.getElementById("textarea1"); var oldTxtNode = elemPara.firstChild; var oldText = oldTxtNode.nodeValue; var newText = oldText + elemTxtBox.value; var newTxtNode = document.createTextNode(newText); elemPara.replaceChild(newTxtNode, oldTxtNode); }
It starts by creating two variables elemPara and elemTxtBox, which contain references to the <P> element and the <textarea> control element. Next we create two new variables, oldTxtNode and oldText, that store a reference to the text node that is a child of the paragraph node, and the text itself. This is so that when we add the new text, we don't delete the old. The next two lines create our new text, first by adding the old text to the contents of the <textarea> control, and then by creating a new text node with this new text.
On its own this does nothing; we still have to connect the new text node back to our tree structure. We do this with the replaceChild() method of the paragraph node object, which takes two arguments: one a reference to the old text node and the other the text node we want to replace it with.
The second function is activated when somebody clicks the New Paragraph button.
function bttnNew_onClick() { var elemPara = document.getElementById("Para1"); var elemTxtBox = document.getElementById("textarea1"); var newText = elemTxtBox.value; var newTxtNode = document.createTextNode(newText); var elemNew = document.createElement("P"); elemNew.appendChild(newTxtNode); document.body.insertBefore(elemNew, elemTxtBox); }
It works in a similar way to the first function. First it creates variables that store references to the <P> element and the <textarea> control. This time we don't have to worry about the existing contents of the <P> element because we are creating a new element. We create two new variables: newText, which stores the value supplied by the user in the <textarea> control, and newTxtNode, in which we create a new text node with the contents of the <textarea> control.
Next we create a new <P> element using the document object's createElement() method. We append the new text node to it using the appendChild() method, which just takes one argument, a reference to our new text node. Finally, we use the insertBefore() method to insert our <P> element onto the web page. The insertBefore() method takes two arguments: The first is the element we wish to insert, a new <P> element, and the second is the <textarea> control. It inserts a new child node before the node stipulated in the second argument of the method. This way the new <P> element is inserted after the first <P> element at the top, and before the <textarea> control.
The last function is the one that removes the latest paragraph on the web page and is activated when someone clicks the Remove Paragraph button. It is a lot simpler than the previous two functions.
function bttnRem_onClick() { var elemTxtBox = document.getElementById("textarea1"); var elemLastPara = elemTxtBox.previousSibling; document.body.removeChild(elemLastPara); }
We need to identify only one element: the <textarea> control. We store the reference to it in the elemTxtBox variable. We then use the previousSibling property in the next line of code to navigate back one in our tree structure to the previous element. If we haven't added any paragraphs to the page, this will be the first one created in the HTML page. We store a reference to this node in the variable elemLastPara. We then supply this variable to the removeChild() method, which removes the child node from the web page.
The next example we will consider is, again, one we already looked at in the previous chapter. This is where a piece of text has its content and color changed as the mouse pointer rolls over it. We'll re-create the example, but using DOM methods and properties.
Let's open up a text editor once more and type the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <script language=JavaScript type="text/javascript"> function myPara_onmouseout() { var myPara = document.getElementById("DynamicText"); myPara.firstChild.nodeValue = "Roll your mouse over my text"; myPara.style.color = "Black" } function myPara_onmouseover() { var myPara = document.getElementById("DynamicText"); myPara.firstChild.nodeValue = "Wow that feels good!!!"; myPara.style.color = "Red" } </script> </head> <body> <P id="DynamicText" onmouseout="return myPara_onmouseout()" onmouseover="return myPara_onmouseover()"> Roll your mouse over my text </P> </body> </html>
Save this as rollover.htm and start up the page in a browser. The page shown in Figure 13-21 opens.
Moving the mouse cursor over the text produces the result shown in Figure 13-22.
This example is very straightforward. If the onmouseover event handler is fired, meaning that the mouse pointer has moved over the <P> element, we set the <P> element's child node's node value (in other words the text of the <P> element) to equal "Wow that feels good!!!", and change the paragraph's style.color to be red.
function myPara_onmouseover() { var myPara = document.getElementById(DynamicText) myPara.firstChild.nodeValue = "Wow that feels good!!!"; myPara.style.color = "Red" }
When the mouse pointer rolls outside the target text, we set the <P> element's child node's node value to be equal to "Roll your mouse over my text" and the paragraph's style.color to be black.
function myPara_onmouseout() { var myPara = document.getElementById(DynamicText) myPara.firstChild.nodeValue = "Roll your mouse over my text"; myPara.style.color = "Black" }
That's all there is to this example.
We have left the most ambitious of our examples until last. This is where we provide a dynamic menu system that works in Internet Explorer 5.5 and 6, and Netscape Navigator 6 and 7. Up until now, all of the examples in this chapter have worked on both IE and Netscape Navigator without requiring any separate coding. This is where it changes. Previously, this example was coded to exploit only IE's capabilities or only NN 4.x's capabilities. However, now that NN 6 and 7 and IE 5.5 and 6 support much of the DOM, a cross-browser version is possible, but it does still require some amendments to be made first.
Everything that the dynamic menu example does in the IE- and NN-specific versions is theoretically achievable by using only properties and methods specified in the DOM. In fact, we're doing nothing special or out of the ordinary in this example.
However, it isn't all plain sailing. Just because both browsers now broadly support the DOM doesn't mean that they both support it to the letter. Sometimes one browser supports a set of methods or properties created in an earlier version of the browser that haven't been included in the DOM, or they might both support a particular feature of the DOM, but offer slightly different ways of implementing it. In this example, we'll try to create a version that does the following:
Uses as few as lines of browser-dependent code as possible
Offers identical functionality in both browsers
Uses the DOM way of accessing elements on the page
When translating the code, we came across five main areas in the program that needed to be fixed to get the example to work on both browsers. Let's look at them now before we create the example.
As we have seen, the id attribute is the preferred way in the DOM of identifying a particular occurrence of an element on the page. However, IE makes the actual object available directly via its id attribute, while Netscape Navigator doesn't. This means that to get access to the elements, we have to use our old friend, the getElementById() method of the document object once more. This method is available to both IE and NN and is used in preference to IE's ability to directly reference the id attribute in script, which we saw in the last chapter.
Note |
There is also a non-DOM-specified getElementByName() method that works in the same way as the getElementById() method and can be used in IE 5 only. However, since you are meant to identify elements with the id attribute and not the NAME attribute in the DOM, we won't be using this. We mention it for reference purposes only. |
IE is a lot more relaxed about making the event object available globally. When connecting an event handler to a function, you don't have to pass the event object as an argument; it's already available to that function as though it had been globally created. In Netscape, and indeed the DOM level 2 standard, to be able to make use of the properties and methods of the event object, we have to pass the event object as an argument to the function.
In the IE-specific version of this example, we can use the event object within a function, without any extra code. It works as follows without any arguments and will return img in the alert() statement.
<img id="Picture 1" src="link.jpg" onmouseover="menudisplay()"> <script language=JavaScript type="text/javascript"> function menudisplay() { alert(event.tagName); } </script>
However, this code wouldn't pass any parameters to the function in Netscape Navigator, and consequently the alert() statement wouldn't display any value. We have to pass the event object explicitly. The following code assigns an onmouseover event handler to an <img> tag and also passes the event object as an argument to the function. Then we can make use of the properties and methods within the function as follows:
<img id="Picture 1" src="link.jpg" onmouseover="menudisplay(event)"> <script language=JavaScript type="text/javascript"> function menudisplay(e) { alert(e.tagName); } </script>
Quite a few examples of this are in our dynamic menu example, so we will have to translate each occurrence, as shown previously.
A lot of what made the event handling facilities in IE 4 superior to those in NN 4 came down to the fact that they were simpler to use due to the reasons already discussed in the previous chapter. However other problems arose due to the event object having different properties.
In NN 4 and the DOM standards, to determine which element on a web page generated an event, we have to use the target property of the event object. However, in IE, after an event has been generated, we can retrieve that same element by checking the contents of the srcElement property of the event object.
With IE, if a mouse pointer crosses elements, and we want to discover the element that the mouse pointer is moving into, we need to use the toElement property of the event object. Once again this property isn't specified in the DOM standards, and DOM and NN 6 require us to use the relatedTarget property instead.
Fortunately, both sets of properties (target and srcElement, and relatedTarget and toElement) work in the same way, despite their different names. This makes the job of writing separate lines of code for each browser fairly straightforward. For once it's IE 5.5/6 that is at fault, because it doesn't support the methods outlined in the DOM.
To get around this problem and to create a dual-browser version of the code, we need to perform a check every time one of these properties needs to be used. If the browser running the page is IE, no changes to our previous IE version of the page are needed. If, on the other hand, it's NN 6 that is browsing the code, we need to use the target or relatedTarget property instead.
One disappointment about IE's DHTML implementation was the fact that while everything was theoretically open to a scripting language, the actual text on a page wasn't considered a separate entity or object in the way the different elements were. You could access it and change it using the innerhtml and innerText properties. (Actually, the innerhtml property is also now supported in NN 6 and 7, although it isn't strictly in the DOM.) However, the text wasn't recognized in the script as an object.
NN rectifies this and follows the DOM's wording by adding text nodes to the object model. This means that every element that contains a piece of text, as we noted earlier, should make the text available as a text node. IE 4 doesn't do this, and although IE 5+ does add text nodes to the object model, there are still differences between the implementations of the two browsers. As we saw earlier in the chapter, NN seems to add a text node for every element, even if it only contains white space, whereas IE adds a text node only for elements with text, which is not white space.
Differences also exist between the way some of the event properties return information. In IE only HTML elements can raise events, but in NN 6 text nodes can also raise events. This causes a problem in our example. If we use the onmouseover event to generate an event over a piece of text, in IE it will still return the name of the element that contains the text; whereas NN might return the text node.
In our example, when we generate an onmouseover event in NN 6, we have to move up our tree structure to the parent node (the element that contains the text). This navigation up the tree is necessary because we can change the style of an element node, but not of a text node. We use the parentNode property to go up the tree here.
One last quirk with this example is that when we create a menu by placing a <div> tag containing a table on the page, we create the <div>, <table>, <TR>, and <TD> tags dynamically using JavaScript. However, according to the HTML 4.01 standard, tables should also contain headers (<thead>), bodies (<tbody>), and footers (<tfoot>) in a similar way to that in which an HTML document is divided into <head> and <body> tags. Ordinarily this shouldn't cause us any problems, but this one time in our example it does.
The HTML 4.01 standard proposes that when we create a table, we should create it as follows:
<table> <thead> <tr>Row</tr> </thead> <tbody> <TR> <TD>Cell Number 1</TD> </TR> <TR> <TD>Cell Number 2</TD> </TR> </tbody> <tfoot> </tfoot> </table>
Creating it as follows will have exactly the same effect:
<table> <TR> <TD>Cell Number 1</TD> </TR> <TR> <TD>Cell Number 2</TD> </TR> </table>
It's back to the well-formed HTML document requirement that we described earlier. When we dynamically create our tables without the header, body, and footer sections, NN 6 and 7, because they require a well-formed HTML document, add these tags to the code for us. This can be demonstrated if we add an alert() statement to the code returning the tag name of the element the mouse pointer is currently over. If we move over the image, it returns img. If we move over the menu, it returns table. Because we have a border around the table, this forms part of the <tbody> section, and when we move our mouse pointer over it, it will show tbody.
Now, you might remember from the previous chapter that the way we detect whether we need to display the menu is by checking to see whether we're over an image, or inside the <div>. This can cause problems in IE because the onmouseout event of the <div> can be raised when the mouse pointer passes inside the <div> and over the contained table elements. The way we worked around this in the previous IE version of the page cannot be replicated using the DOM, so we have to come up with a new strategy.
As we will see later, the new strategy depends on being able to determine whether the element the mouse pointer is over and the <div> to be hidden have similar id attributes. We do this by setting the start of the id attributes for the elements of the table to all be the same. Because the id attributes are set by hand in the program, when NN adds its own <tbody> element, it doesn't add any id attributes. This has the effect that whenever we move over a part of the menu that returns a <tbody> element, it makes the menu disappear.
This means to make our <table> well formed, we had to add some extra script. Specifically, we add the <tbody> element and an id attribute that starts in the same way as the other table element's id values, so that JavaScript doesn't think the <tbody> element is outside the menu!
We need to change these five areas to make our dynamic menu work across browsers. Now let's get on with it.
Let's start our preferred web page editor and type the following:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <script language=JavaScript type="text/javascript"> var ns = (navigator.appName.indexOf('Netscape')>-1); var ie = (navigator.appName.indexOf('Microsoft Internet Explorer')>-1); var woodMenuItems = new Array( ["Oak", "OakWood.htm", "Oak Timber"], ["Teak", "TeakWood.htm", "Teak Timber"], ["Pine", "PineWood.htm", "Pine Timber"], ["Yew", "YewWood.htm", "Yew Timber"]); var metalMenuItems = new Array( ["Steel", "SteelMetal.htm", "Steel Girders"], ["Copper", "CopperMetal.htm", "Copper Pipes"], ["Gold", "GoldMetal.htm", "Gold Ingots"]); var bricksMenuItems = new Array( ["StdHouse", "StdHousebricks.htm", "Standard House Brick"], ["LargeHouseBrick", "LargeHousebricks.htm", "Large House Bricks"], ["BreezeBlock", "BreezeBlock.htm", "Breeze Block"]); function createMenu(menuName, menuItems) { var divhtml = '<div ID="' + menuName + 'MenuDiv" CLASS="DivMenu"'; divhtml = divhtml + 'onmouseout="return hideMenu(this, ' + menuName.length + ',event)">'; var tablehtml = '<table border=0 cellspacing=1 cellpadding=1 ID="' + menuName + 'Table"><Tbody ID="' + menuName + 'TableBody">'; var tableRowhtml = ""; var rowCount; var totalNoRows = menuItems.length; for (rowCount = 0; rowCount < totalNoRows; rowCount++) { tableRowhtml = tableRowhtml + '<TR id="' + menuName + menuItems[rowCount][0] + 'TR"><TD id="' + menuName + menuItems[rowCount][0]; tableRowhtml = tableRowhtml + '" onclick="goPage(\'' + menuItems[rowCount][1] + '\')"'; tableRowhtml = tableRowhtml + 'class="TDMenu">' + menuItems[rowCount][2] + '</TD></TR>'; } return divhtml + tablehtml + tableRowhtml + '</tbody></table></div>'; } function showMenu(menuToShow, e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } var xPos = parseInt(srcElement.style.left); var yPos = parseInt(srcElement.style.top); menuToShow.style.left = xPos + (srcElement.width) menuToShow.style.top = yPos; } function hideMenu(menuToHide, menuidLength, e) { var toElementid; if (ns) { var mouseLastIn = e.relatedTarget; } else { var mouseLastIn = event.toElement; } if (mouseLastIn != null) { if (mouseLastIn.nodeType == 3) { mouseLastIn = mouseLastIn.parentNode; } toElementid = mouseLastIn.id; } else { return false; } if (typeof(toElementid) == "undefined") { toElementid = "UNDEF"; } toElementid = toElementid.substr(0,menuidLength); var divMenuid = menuToHide.id; divMenuid = divMenuid.substr(0,menuidLength); if (toElementid != divMenuid) { menuToHide.style.left = -200 + 'px'; menuToHide.style.top = -1000 + 'px'; } } function document_onmouseover(e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } if (srcElement.nodeType == 3) { srcElement = srcElement.parentNode; } if (srcElement.tagName=="TD") { srcElement.style.color = "white"; srcElement.style.backgroundColor = "darkblue" } } function document_onmouseout(e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } if (srcElement.nodeType == 3) { srcElement = srcElement.parentNode; } if (srcElement.tagName == "TD") { srcElement.style.color ="darkblue"; srcElement.style.backgroundColor = "darkorange"; } } function goPage(src) { window.location.href = src; } </script> <style type="text/css"> .DivMenu {position:absolute; left:-200px; top:-1000px; width:180px; z-index:100; background-color:darkorange; border: 4px groove lightgrey; } .TDMenu { color:darkblue; font-family:verdana; font-size:70%; width:100%; cursor:default; } </style> </head> <body onmouseover="document_onmouseover(event)" onmouseout="document_onmouseout(event)"> <script language=JavaScript type="text/javascript"> document.write(createMenu('Wood', woodMenuItems)) document.write(createMenu('Metal', metalMenuItems)) document.write(createMenu('Bricks', bricksMenuItems)) </script> <img id="WoodMenuImage" src="WoodButton.gif" style="position:absolute;left:10px;top:75px" onmouseover="return showMenu(document.getElementById('WoodMenuDiv'), event)" onmouseout="return hideMenu(document.getElementById('WoodMenuDiv'),4,event)"> <img id="MetalMenuImage" src="MetalButton.gif" style="position:absolute;left:10;top:115" onmouseover="return showMenu(document.getElementById('MetalMenuDiv'),event)" onmouseout="return hideMenu(document.getElementById('MetalMenuDiv'),5,event)"> <img id="BricksMenuImage" src="BricksButton.gif" style="position:absolute;left:10;top:155" onmouseover="return showMenu(document.getElementById('BricksMenuDiv'),event)" onmouseout="return hideMenu(document.getElementById('BricksMenuDiv'),6,event)"> </body> </html>
Note |
If the image files aren't available, the page won't work in NN 6/7 (although it does in IE 5.5/6). NN 6/7 won't display anything at all, not even a broken link symbol. You can get the images from the code download for the book. |
Save this as IE_NN_Menus.htm and open up this example in IE 5 or later and NN 6 or 7. See Figures 13-23 and 13-24.
Move the mouse pointer over the buttons and they both generate menus dynamically in an identical fashion.
We haven't changed the fundamental workings of this example in any way. All we've done is translate the troublesome browser-specific properties into something DOM-specific. Where this hasn't been possible, we've used a little browser-sniffing code to determine which browser we're using and then pursued an appropriate course of action for that browser.
Indeed, the first two lines of code check for the type of browser being used.
var ns = (navigator.appName.indexOf('Netscape')>-1); var ie = (navigator.appName.indexOf('Microsoft Internet Explorer')>-1);
We generate two variables: ns, which will contain a true value only if our browser is a Netscape browser, and ie, which will contain a true value only if the browser is Internet Explorer. However, this is all we need to change in our first section.
The array of menu elements is generated in virtually the same way as before. The only difference is in the shortcut way of writing the system of array elements.
Let's skip this and jump to the createMenu() function. Only two things have changed here.
In the <div> tag we have an onmouseout event handler to hide the menu when the user moves outside the <div> tag. We have added two parameters to the function called here. The first parameter is the length of the menu name. We will investigate the need for this later. Also, because we're going to want to check the event object after we've moved outside the <div> tag, we need to pass the event object explicitly, and so have added this as another parameter.
The second thing we have changed is that the function now dynamically generates a <tbody> tag to ensure that our document is well formed and adds id attributes to this and the <TR> tags.
function createMenu(menuName, menuItems) { var divhtml = '<div id="' + menuName + 'MenuDiv" class="DivMenu"'; divhtml = divhtml + 'onmouseout="return hideMenu(this, ' + menuName.length + ',event)">'; var tablehtml = '<table border=0 cellspacing=1 cellpadding=1 id="' + menuName + 'Table"><tbody id="' + menuName + 'TableBody">'; var tableRowhtml = ""; var rowCount; var totalNoRows = menuItems.length; for (rowCount = 0; rowCount < totalNoRows; rowCount++) { tableRowhtml = tableRowhtml + '<TR id="' + menuName + menuItems[rowCount][0] + 'TR"><TD id="' + menuName + menuItems[rowCount][0]; tableRowhtml = tableRowhtml + '" onclick="goPage(\'' + menuItems[rowCount][1] + '\')"'; tableRowhtml = tableRowhtml + 'class="TDMenu">' + menuItems[rowCount][2] + '</TD></TR>'; } return divhtml + tablehtml + tableRowhtml + '</tbody></table></div>'; }
The showMenu() function comes next and has to be changed because it previously made use of the IE-only property srcElement of the event object to return the element that generated an event. According to DOM level 2, you should use the target property to do this. However, while Netscape supports this property, IE doesn't. The ie and ns variables we created globally at the beginning of the script come into play and we need to make use of them. The function creates a variable called srcElement that contains the name of the HTML element that generated the event. The way this variable is set depends on the browser being used.
The only other change to the showMenu() function is that the event object is passed as a parameter instead of being globally available as it was with the IE-only version.
function showMenu(menuToShow, e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } var xPos = parseInt(srcElement.style.left); var yPos = parseInt(srcElement.style.top); menuToShow.style.left = xPos + (srcElement.width) menuToShow.style.top = yPos; return false; }
The hideMenu() function needs more drastic changes. It too, like the showMenu() function, suffers from the event object properties affliction, except that the problem is with the IE-specific property toElement, which records the element the mouse pointer has just moved onto. The DOM-compliant equivalent that is supported in NN is relatedTarget. Once again we have to supply conditional code that will check the ns variable and create a mouseLastIn variable that holds one or other of the properties.
function hideMenu(menuToHide, menuidLength, e) { var toElementid; if (ns) { var mouseLastIn = e.relatedTarget; } else { var mouseLastIn = event.toElement; }
The second problem that has to be dealt with here is one we mentioned earlier. The relatedTarget property in NN can return text nodes, whereas the toElement property in IE can return only element nodes. The menu code here has to check the id of the element that the mouse pointer is going to, but id values are only applicable to elements and not text nodes. As a result, if mouseLastIn is a text node, our code needs to go up the node hierarchy to the element containing the text. The DOM allows that with the parentNode property of every node.
To make sure this happens, we check the nodeType property of mouseLastIn. If it returns a value 3, we know onmouseout has returned a text node. We then move back up the tree structure to the HTML element that contains the text node and use that instead.
We then store the id attribute's value for the mouseLastIn node in the variable toElementid.
if (mouseLastIn != null) { if (mouseLastIn.nodeType == 3) { mouseLastIn = mouseLastIn.parentNode; } toElementid = mouseLastIn.id; } else { return false; }
Finally, we have to determine whether the start of the id attribute of the mouseLastIn node matches the start of the <div> tag's id. If so, we know that the mouse pointer has just moved onto a tag inside the <div> rather than outside the <div>, so we don't hide the menu.
We perform this check by cutting the first part of the id of mouseLastIn stored in the variable toElementid. We know how much of the id we have to cut due to the second parameter of the function, which gives the length of the menu name. We then do the same thing for the id attribute of the <div> tag stored in the variable menuToHide, which was passed as the first parameter of the function. Finally we compare the two values obtained, and if they do not match, we set the left and top style properties of the <div> so that it is placed off-screen and hidden.
if (typeof(toElementid) == "undefined") { toElementid = "UNDEF"; } toElementid = toElementid.substr(0,menuidLength); var divMenuid = menuToHide.id; divMenuid = divMenuid.substr(0,menuidLength); if (toElementid != divMenuid) { menuToHide.style.left = -200 + 'px'; menuToHide.style.top = -1000 + 'px'; } }
Let's now look at the document_onmouseover() function. Once again the IE-specific srcElement property is used in IE 5+, while NN uses the DOM-compliant property target. We've just added a conditional if statement that checks the contents of the ns variable and sets the srcElement variable correspondingly. We also have to check to see that our onmouseover event has generated an HTML element, rather than text node, and we do this by checking the nodeType property again. If it does equal 3, we hop one up the tree structure to the HTML element and use that instead.
function document_onmouseover(e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } if (srcElement.nodeType == 3) { srcElement = srcElement.parentNode; } if (srcElement.tagName=="TD") { srcElement.style.color = "white"; srcElement.style.backgroundColor = "darkblue" } }
Lastly, the document_onmouseout() function has to be changed in this way also.
function document_onmouseout(e) { if (ns) { var srcElement = e.target; } else { var srcElement = event.srcElement; } if (srcElement.nodeType == 3) { srcElement = srcElement.parentNode; } if (srcElement.tagName == "TD") { srcElement.style.color ="darkblue"; srcElement.style.backgroundColor = "darkorange"; } }
Note |
Note that in these two functions we check to see whether the tag the mouse pointer is over is a <TD> tag and, if so, we change the style of the tag's contents. In previous version of the example, we performed more vigorous checks to make sure that the <TD> tag was contained in our menu rather than in any other content on the page. |
While the example has been very large, we've not had to adjust that much code within it and consequently it hasn't really grown in size. In fact, only two problems prompted many changes. The first was that IE uses srcElement and toElement properties instead of target and relatedTarget properties, and the second was that NN 6 can return text nodes from its event object's target and relatedTarget properties, whereas IE can return only element nodes from the corresponding srcElement and toElement properties. If we're aware of these problems, the task of amending them really isn't too great. Hopefully, we're miles away from the problems caused by IE 4 and NN 4 and having to using the layers collection. The task of upgrading, despite our earlier warnings, has been fairly painless.