Where does Ajax come into play with web site navigation? You can use it to get the list of data for a submenu, build a hierarchical list for use as breadcrumbs on the page, or create a navigation tree. In short, you can apply Ajax in many creative ways for web site navigation. I am sure that by the time this book hits the shelves, developers will be implementing a few more navigation techniques that use Ajax. That is part of the beauty of building Web 2.0 applications—they can always change.
An important part of an Ajax web application is the way the user gets from one place to another within its pages. This is site navigation in its simple terms, and it can take many forms, from plain navigation bars with text links to complicated file menus. Site navigation can also take the form of tabs separating content within the application, or links on the bottom of a page that take the user back to the top of the page. Whatever form it takes, site navigation must serve one purpose: to take the user somewhere else in the application.
One of the most popular navigation techniques is the menu, whether it is a navigation menu or a navigation bar. Menus are lists of links for use within the application that are put in some kind of logical grouping. The CSS that is applied to these menus determines how they will look. First, we will look at some simple navigation menus and bars and then we will discuss how to apply Ajax to them to make them more interactive.
A simple navigation bar can have a variety of looks based on the CSS that is applied to it. The bar is built with lists to represent the menu. The old way of creating a menu looked like the following XHTML markup:
<a href="file/">File</a> <a href="edit/">Edit</a> <a href="view/">View</a> <a href="insert/">Insert</a> <a href="format/">Format</a> <a href="table/">Table</a> <a href="tools/">Tools</a> <a href="window/">Window</a> <a href="help/">Help</a>
This old method of building a navigation bar was inflexible, and making it minimally presentable using style rules was difficult. Years ago, this is what developers used for menu bars, but today they use a much better method to create a navigation system. They use XHTML lists for the basic structure and then style them into whatever type of navigation bar they want. Figure 7-1 shows several examples of navigation bars with different CSS associated with them.
All of the navigation bars in Figure 7-1 use the same XHTML list; the difference in styles is in the CSS associated with each of them:
<div id="navigationMenu"> <ul id="menuList"> <li id="active"> <a id="current" href="file/" accesskey="F" hreflang="en" tabindex="1"> File </a> </li><li> <a href="edit/" accesskey="F" hreflang="en" tabindex="2"> Edit </a> </li><li> <a href="view/" accesskey="E" hreflang="en" tabindex="3"> View </a> </li><li> <a href="insert/" accesskey="I" hreflang="en" tabindex="4"> Insert </a> </li><li> <a href="format/" accesskey="M" hreflang="en" tabindex="5"> Format </a> </li><li> <a href="table/" accesskey="A" hreflang="en" tabindex="6"> Table </a> </li><li> <a href="tools/" accesskey="T" hreflang="en" tabindex="7"> Tools </a> </li><li> <a href="window/" accesskey="W" hreflang="en" tabindex="8"> Window </a> </li><li> <a href="help/" accesskey="H" hreflang="en" tabindex="9"> Help </a> </li> </ul> </div>
Adding the accesskey,
hreflang
, and tabindex
in the <link>
elements satisfies the
following Web Accessibility Initiative-Web Content Accessibility
Guidelines (WAI-WCAG) 1.0 guidelines:
Priority 1 checkpoint 4.1: Clearly identify changes in the natural language of a document’s text and any text equivalents (e.g., captions).
Priority 3 checkpoint 9.4: Create a logical tab order through links, form controls, and objects.
Priority 3 checkpoint 9.5: Provide keyboard shortcuts to important links (including those in client-side image maps), form controls, and groups of form controls.
Menu #1 is styled with the following CSS rules:
#menuList { background-color: #396; color: #fff; list-style-type: none; margin: 0; padding: .3em 0; text-align: center; } #menuList li { display: inline; padding: 0 .5em; } #menuList li a { background-color: transparent; color: #fff; padding: .1em .5em; text-decoration: none; } #menuList li a:hover { background-color: #0c0; color: #fff; }
There is no real trick to making a navigation menu with lists.
You will notice that the list-style-type
of the <ul>
element is set to none
and the display
of all <li>
elements is set to inline
. These are the two CSS rules that
create the horizontal list, and everything else is styling the
navigation menu to whatever look is desired.
Menu #2 is styled with the following CSS rules:[5]
#menuList { background-color: #396; border-top: 1px solid #063; color: #fff; list-style: none outside; margin: 0; padding: 0; text-align: center; } #menuList li { background-color: #000; bottom: .75em; color: #fff; display: inline; line-height: 1.2em; margin: 0 3px 0 0; padding: 4px 0; position: relative; } #menuList li a { background-color: #090; border: 1px solid #fff; bottom: 2px; color: #fff; display: inline; height: 1em; margin: 0; padding: 3px 5px; position: relative; right: 2px; text-decoration: none; } #menuList li a:hover { background-color: #0c0; bottom: 1px; color: #fff; position: relative; right: 1px; } #menuList li#active { background-color: #396; bottom: 13px; color: #fff; display: inline; margin: 0 4px; padding: 0; position: relative; } #menuList #active a { background-color: #396; border-bottom: none; border-left: 1px solid #063; border-right: 1px solid #063; border-top: 1px solid #063; bottom: 0; color: #fff; cursor: text; margin: 0; padding: 2px 7px 0 7px; position: relative; right: 0; }
The second menu shows off some of what you can do with a
little CSS and an XHTML list. The trick here is to move all of the
<li>
elements up from the
baseline, which we achieved with these two rules: position: relative;
and bottom: .75em;
. We then offset the
<a>
elements from this
<li>
base to create the
shadow effect using the right
and
bottom
rules on #menuList li a
.
Menu #3 is styled with the following CSS rules:[6]
#menuList { background-color: #396; border-bottom: 1px solid #063; border-top: 1px solid #063; color: #fff; list-style-type: none; margin-left: 0; padding: .3em 0; text-align: center; } #menuList li { border-top: 1px solid #ccc; display: inline; margin: 0; } #menuList li a { background-color: #ada; border-left: .5em solid #9b9; color: #000; padding: .1em .5em .1em .75em; text-decoration: none; } #menuList li a:hover { background-color: #0c0; color: #fff; border-color: #fff; }
Like menu #1, this menu also has a simple design. We create
the effect of having almost a pseudoindicator on each menu item via
the simple use of a border: border-left:
.5em solid #9b9;
. The rest of the changes needed for
styling this menu concern changing background colors like the other
menu bars.
The good thing about creating a menu bar using CSS is that it will degrade nicely into a simple list of links for browsers that cannot handle the CSS—screen readers, Lynx, and so on. This is important from an accessibility point of view, and is something developers should strive for.
Menu #3 could (arguably) be more of a navigation bar with buttons, rather than a simple menu navigation bar. The variety of effects available to implement with CSS makes it hard to distinguish between a menu and a navigation bar, as you will see later in this chapter.
Navigation bars with buttons are similar to the simple navigation bars we discussed in the preceding section. The main difference is in the look of the individual navigation elements, or links. Buttons are not flat to the navigation bar. Instead, they will appear to be raised off of or sunk into the navigation bar. When CSS is not used, you can instead employ the method of using images that look like buttons.
You can use a single image to represent all of the buttons in your application by adding text on top of the image as part of the XHTML, or you can use a different image to represent each function in the application. Figure 7-2 shows examples of different types of button and image navigation bars. The first navigation bar shows all the images used as buttons for a good bit of functionality. The second navigation bar shows image buttons we are all familiar with—a browser’s navigation (in this case, Firefox). The third navigation bar shows some of the button navigation in Hotmail, which adds images as part of the buttons.
The simplest way to create buttons and not actually use the
<input>
element with
type
equal to button
is to put stylized borders around
the element that will provide the link. Normally, the easiest
element to use is the <a>
element, because you already have the navigation built into it. In
other words, you do not have to use a JavaScript technique to
navigate to the button’s destination.
For example, look at the following CSS rules:
#menuList li a { background-color: #4a7; border: 2px solid; border-color: #cec #464 #575 #dfd; color: #fff; padding: .1em .5em; text-decoration: none; } #menuList li a:hover { background-color: #396; color: #fff; border-color: #575 #dfd #cec #464; }
Figure 7-3 shows
what these CSS rules would produce on an <a>
element.
Internet Explorer does not natively support :hover
on any element other than
<a>
. It is a World Wide
Web Consortium (W3C) standard that says that the :hover
pseudoclass should be available to
all XHTML elements, and :hover
does work appropriately in all
other modern browsers.
The trick is in the colors used for the button’s four
borders. The desired effect is to create a beveled edge for the
button. It would be just as easy to use the outset
and inset
values on a border-style
rule. If I had done that,
however, I would have no control over how the browser rendered the
bevels. Internet Explorer, Mozilla, and other browsers render the
inset
and outset
borders in different ways. This,
of course, comes down to personal preferences. The following CSS
may suit a developer:
#menuList li a { background-color: #4a7; border-style: outset; color: #fff; padding: .1em .5em; text-decoration: none; } #menuList li a:hover { background-color: #396; color: #fff; border-style: inset; }
Figure 7-4 shows a final working navigation bar with buttons. This navigation bar uses the same XHTML list as the previous examples, and the following CSS rules:
#menuList { background-color: #396; color: #fff; list-style-type: none; margin: 0; padding: .3em 0; text-align: center; } #menuList li { display: inline; padding: 0 .5em; } #menuList li a { background-color: #4a7; border: 2px solid; border-color: #cec #464 #575 #dfd; color: #fff; padding: .1em .5em; text-decoration: none; } #menuList li a:hover { background-color: #396; color: #fff; border-color: #575 #dfd #cec #464; }
For a final touch to our CSS buttons, we will add images to them to better represent their functionality. Figure 7-5 shows what one of these buttons looks like.
We will build this button using the following CSS rule:
#menuList li a { background: no-repeat 3px 4px url('save.png'), background-color: #4a7; border: 2px solid; border-color: #cec #464 #575 #dfd; color: #fff; padding: .1em .5em .15em 1.5em; text-decoration: none; }
The addition is background:
no-repeat 3px 4px url(save.png);
. In addition, we
increased the padding
of the
<a>
element to
accommodate the image. All we need to do now is create a small
image for each button on the navigation bar, and add the
corresponding CSS. To do this, we need to give each button a
unique id
attribute to which to
attach the proper image, as the following code illustrates:
#menuList li a { background-color: #4a7; border: 2px solid; border-color: #cec #464 #575 #dfd; color: #fff; padding: .1em .5em .15em 1.5em; text-decoration: none; } #menuList li a#save { background: no-repeat 3px 4px url('save.png'), } #menuList li a#saveAll { background: no-repeat 3px 4px url('saveall.png'), } #menuList li a#cancel { background: no-repeat 3px 4px url('cancel.png'), }
What if, instead of using CSS buttons, you wanted to have images represent your navigation bar’s buttons? Putting on an image is easy, but how do you make it change when the mouse moves over it to indicate to the user that the button is pressed?
The old way to do this was to use JavaScript to change the
image when certain MouseEvent
s
occur. Example 7-1
shows the JavaScript needed to accomplish this.
Example 7-1. rollover.js: JavaScript to handle image rollovers
/** * Example 7-1, rollover.js: JavaScript to handle image rollovers. */ /* Preload the images for a faster rollover */ if (document.images) { /* This represents the save image when active */ var saveImg_on = new Image( ); saveImg_on.src = 'saveImg_on.png'; /* This represents the save image when inactive */ var saveImg_off = new Image( ); saveImg_off.src = 'saveImg_off.png'; } /** * This function, turnImageOn, is called when there is a mouseover event on the * affected element and sets the /src/ attribute to the "on" image. * * @param {String} p_id The id attribute for the affected image. */ function turnImageOn(p_id) { document.getElementById(p_id).src = eval(p_id + '_on.src'), } /** * This function, turnImageOff, is called when there is a mouseout event on the * affected element and sets the /src/ attribute to the "off" image. * * @param {String} p_id The id attribute for the affected image. */ function turnImageOff(p_id) { document.getElementById(p_id).src = eval(p_id + '_off.src'), }
The JavaScript function turnImageOn( )
is called on all mouseover
events attached to an image
button to change the image. The function turnImageOff( )
is called on all
mouseout
events attached to the
image to return the button to the original image. All we need now
are images named with the _on
and _off
extensions for the
JavaScript to work. Here is an example:
<div id="navigationMenu"> <ul id="menuList"> <li> <a href="save/" accesskey="S" hreflang="en" tabindex="1" onmouseover="turnImageOn('saveImg')," onmouseout="turnImageOff('saveImg'),"> <img id="saveImg" src="saveImg_off.png" alt="Save" title="Save" /> </a> </li><li> <a href="saveall/" accesskey="A" hreflang="en" tabindex="2" onmouseover="turnImageOn('saveAllImg')," onmouseout="turnImageOff('saveAllImg'),"> <img id="saveAllImg" src="saveAllImg_off.png" alt="Save All" title="Save All" /> </a> </li><li> <a href="cancel/" accesskey="C" hreflang="en" tabindex="3" onmouseover="turnImageOn('cancelImg')," onmouseout="turnImageOff('cancelImg'),"> <img id="cancelImg" src="cancelImg_off.png" alt="Cancel" title="Cancel" /> </a> </li> </ul> </div>
If JavaScript were turned off in the browser, the image buttons would simply not change on mouse movements, and the image link would take them to the destination.
By using CSS, however, you can still change the images for browsers that do not have scripting capabilities. The CSS for this technique is:
a div#saveImg { background: no-repeat url('saveImg_off.png'), height: 20px; width: 50px; } a div#saveImg:hover { background: no-repeat url('saveImg_on.png'), } a div#saveAllImg { background: no-repeat url('saveAllImg_off.png'), height: 20px; width: 80px; } a div#saveAllImg:hover { background: no-repeat url('saveAllImg_on.png'), } a div#cancelImg { background: no-repeat url('cancelImg_off.png'), height: 20px; width: 65px; } a div#cancelImg:hover { background: no-repeat url('cancelImg_on.png'), }
The CSS rules apply to the following XHTML:
<div id="navigationMenu"> <ul id="menuList"> <li> <a href="save/" accesskey="S" hreflang="en" tabindex="1"> <div id="saveImg"> </div> </a> </li><li> <a href="saveall/" accesskey="A" hreflang="en" tabindex="2"> <div id="saveAllImg"> </div> </a> </li><li> <a href="cancel/" accesskey="C" hreflang="en" tabindex="3"> <div id="cancelImg"> </div> </a> </li> </ul> </div>
As mentioned earlier, Internet Explorer does not natively
support :hover
on elements
other than <a>
. For this
reason, instead of using the CSS that will work for all other
browsers, we must use this:
a div#saveDiv { height: 20px; width: 50px; } a#saveImg { background: no-repeat url('saveImg_off.png'), } a#saveImg:hover { background: no-repeat url('saveImg_on.png'), } a div#saveAllDiv { height: 20px; width: 80px; } a#saveAllImg { background: no-repeat url('saveAllImg_off.png'), } a#saveAllImg:hover { background: no-repeat url('saveAllImg_on.png'), } a div#cancelDiv { height: 20px; width: 65px; } a#cancelImg { background: no-repeat url('cancelImg_off.png'), } a#cancelImg:hover { background: no-repeat url('cancelImg_on.png'), }
The workaround is to change the <a>
element to suit our needs as
an image. Specifically, it needs a <div>
element to hold the size of
the image that will be the <a>
element’s background-image
. Then :hover
will work correctly and we’ll get
the desired effect. Of course, our XHTML must change as
well:
<div id="navigationMenu"> <ul id="menuList"> <li> <a id="saveImg" href="save/" accesskey="S" hreflang="en" tabindex="1"> <div id="saveDiv"> </div> </a> </li><li> <a id="saveAllImg" href="saveall/" accesskey="A" hreflang="en" tabindex="2"> <div id="saveAllDiv"> </div> </a> </li><li> <a id="cancelImg" href="cancel/" accesskey="C" hreflang="en" tabindex="3"> <div id="cancelDiv"> </div> </a> </li> </ul> </div>
You can read about a different workaround for this Internet
Explorer issue in Peter Nederlof’s excellent article, “be gone
evil scriplets!” (http://www.xs4all.nl/~peterned/hovercraft.html).
This article discusses a hack to get Internet Explorer to behave
as other browsers do with :hover
.
It’s easy to create drop-down menus with a combination of CSS and JavaScript. But for this application, we want a drop-down menu that we can create with only CSS. This kind of menu would be faster, as it would require no parsing of scripting code, and it would also degrade the way we want it to.
First, let’s make our menu a little more complicated, because
a drop-down menu should handle nested menus without a hitch, right?
The new menu will be using id
and
class
identifiers different from
the other menus to enable the drop-down part of the menu:
<div id="navigationMenu"> <ul id="topMenu"> <li class="sub"> <a href="file/" accesskey="F" hreflang="en" tabindex="1">File</a> <ul> <li><a href="open/" hreflang="en" tabindex="2">Open</a></li> <li><a href="save/" hreflang="en" tabindex="3">Save</a></li> <li><a href="saveall/" hreflang="en" tabindex="4">Save All</a></li> <li class="sub"><a href="export/" hreflang="en" tabindex="5"> <span class="rightArrow">▶</span>Export</a> <ul> <li> <a href="text/" hreflang="en" tabindex="6"> Export as Text </a> </li><li> <a href="html/" hreflang="en" tabindex="7"> Export as HTML </a> </li> </ul> </li> <li> <a href="http://www.google.com/" accesskey="X" hreflang="en" tabindex="8"> Exit </a> </li> </ul> </li> <li class="sub"> <a href="edit/" accesskey="E" hreflang="en" tabindex="9">Edit</a> <ul> <li><a href="copy/" tabindex="10">Copy</a></li> <li><a href="cut/" tabindex="11">Cut</a></li> <li><a href="paste/" tabindex="12">Paste</a></li> </ul> </li> <li class="sub"> <a href="file/" accesskey="N" hreflang="en" tabindex="13"> Find </a> </li> </ul> </div>
The menu still uses XHTML lists, which will degrade nicely in browsers that cannot support the CSS rules. Now we need our CSS for the drop-down menu. Example 7-2 shows the CSS required to give us a working drop-down menu.
Example 7-2. A CSS solution to drop-down menus
/* * Example 7-2, A CSS solution to drop-down menus */ ul#topMenu { background-color: #bbb; border: 2px solid; border-color: #ede #777 #888 #ddd; color: #000; font: 1em Arial, sans-serif; list-style-type: none; padding: 6px; text-align: left; } ul#topMenu li { display: inline; padding-right: 1em; position: relative; } ul#topMenu li a { background-color: transparent; border-color: 1px solid #bbb; color: #000; cursor: default; left: 0px; margin: 1px; padding: 2px 2px; position: relative; text-decoration: none; top: 0px; z-index: 1000000; } ul#topMenu li a:hover { background-color: #bbb; border-color: #888 #ddd #ede #777; color: #000; left: 0px; top: 0px; } ul#topMenu li:hover > ul { background-color: #bbb; border: 2px solid; border-color: #ede #777 #888 #ddd; color: #000; display: block; left: 1em; padding: 2px; position: absolute; width: 8em; z-index: 1000001; } ul#topMenu ul > li { display: block; margin: 0; padding: 0; } ul#topMenu ul > li a { border: none; display: block; text-decoration: none; } ul#topMenu ul > li a:hover { background-color: #33a; color: #fff; } ul#topMenu li:hover > ul li:hover > ul { left: 100%; top: 0; z-index: 1000002; } ul#topMenu ul { display: none; } .rightArrow { float: right; }
Now it’s time for the caveats. Yes, there are always caveats in the world of standards compliance. Example 7-2 will not work in Internet Explorer because Internet Explorer does not support the CSS2 rules that are used to make this work. The best solution for getting drop-down menu support that is fully cross-browser-compliant—other than lobbying the world to drop Internet Explorer—is to use CSS in combination with JavaScript.
XHTML lists are the best method for building a menu structure simply because browsers that do not support the CSS and JavaScript thrown at them can still use the underlying structure to present navigation to the user. We will use this principle throughout the rest of this book to make Ajax application controls, widgets, content, and so on a little bit more accessible. Example 7-3 shows the XHTML we can use to make the menu structure that we will enhance through CSS and JavaScript.
Example 7-3. filemenu.html: The basic structure for a file menu
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <title> Example 7-3, filemenu.html: The basic structure for a file menu </title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta name="author" content="Anthony T. Holdener, III (ath3)" /> <meta http-equiv="imagetoolbar" content="no" /> <style type="text/css"> body { background-color: #fff; color: #000; font: 1em Georgia, serif; font-size: 12px; margin: 0; padding: 0; } </style> <link rel="stylesheet" type="text/css" media="screen" href="filemenu.css" /> <!-- The Prototype library must be the first script to load, since it is used by the other scripts to be loaded --> <script type="text/javascript" src="prototype.js"> </script> <script type="text/javascript" src="browser.js"> </script> <script type="text/javascript" src="filemenu.js"> </script> </head> <body> <div id="bodyContent"> <div id="fileMenu"> <ul id="navMenu" class="fileMenuBar"> <li> <a href="file/" class="fileMenuButton" accesskey="F" hreflang="en" tabindex="1" onclick="return menu.buttonClick(event, 'fileSub')," onmouseover="menu.buttonMouseover(event, 'fileSub'),"> File </a> <ul id="fileSub" class="fileMenuChild" onmouseover="menu.fileMenuMouseover(event)"> <li> <a href="file/open/" class="fileMenuItem" hreflang="en" tabindex="2" onmouseover= "menu.fileMenuItemMouseover(event);"> Open </a> </li> <li><div class="fileMenuItemSep"></div></li> <li> <a href="file/save/" class="fileMenuItem" hreflang="en" tabindex="3" onmouseover= "menu.fileMenuItemMouseover(event);"> Save </a> </li><li> <a href="file/saveall/" class="fileMenuItem" hreflang="en" tabindex="4" onmouseover= "menu.fileMenuItemMouseover(event);"> Save All </a> </li> <li><div class="fileMenuItemSep"></div></li> <li> <a href="file/export/" class="fileMenuItem" hreflang="en" tabindex="5" onclick="return false;" onmouseover= "menu.fileMenuItemMouseover(event, 'exportSubSub'),"> <span class="fileMenuItemText">Export</span> <span class="fileMenuItemArrow">▶</span> </a> <ul id="exportSubSub" class="fileMenuChild"> <li> <a href="file/export/text/" class="fileMenuItem" hreflang="en" tabindex="6" onmouseover= "menu.fileMenuItemMouseover(event);" > Export as Text </a> </li><li> <a href="file/export/html/" class="fileMenuItem" hreflang="en" tabindex="7" onmouseover= "menu.fileMenuItemMouseover(event);" > Export as HTML </a> </li> </ul> </li> <li><div class="fileMenuItemSep"></div></li> <li> <a href="http://www.google.com/" class="fileMenuItem" hreflang="en" tabindex="8" onmouseover= "menu.fileMenuItemMouseover(event);"> Exit </a> </li> </ul> </li><li> <a href="edit/" class="fileMenuButton" accesskey="E" hreflang="en" tabindex="9" onclick="return menu.buttonClick(event, 'editSub')," onmouseover= "menu.buttonMouseover(event, 'editSub'),"> Edit </a> <ul id="editSub" class="fileMenuChild" onmouseover="menu.fileMenuMouseover(event)"> <li> <a href="edit/copy/" class="fileMenuItem" hreflang="en" tabindex="10" onmouseover= "menu.fileMenuItemMouseover(event);"> Copy </a> </li><li> <a href="edit/cut/" class="fileMenuItem" hreflang="en" tabindex="11" onmouseover= "menu.fileMenuItemMouseover(event);"> Cut </a> </li><li> <a href="edit/paste/" class="fileMenuItem" hreflang="en" tabindex="12" onmouseover= "menu.fileMenuItemMouseover(event);"> Paste </a> </li> </ul> </li><li> <a href="find/" class="fileMenuButton" accesskey="N" hreflang="en" tabindex="13"> Find </a> </li> </ul> </div> <h1>This is a File Menu example</h1> </div> </body> </html>
This structure is similar to the drop-down menu example from
the preceding section. The differences are in the calling of methods
from the menu
object on the
click, mouseover
, and mouseout MouseEvent
s. We’ll discuss these
methods in more detail in a minute, but first we must style the menu
to look like a file menu instead of nested XHTML lists.
Example 7-4 provides the CSS rules for the file menu. These rules attempt to make a file menu that looks like the one you find in Windows applications. It is easy enough to change these rules to match your style needs.
Example 7-4. filemenu.css: The CSS styles for a Windows-like file menu
/* * Example 7-4, filemenu.css: The CSS styles for a Windows-like file menu */ /* * This is the container for the menu itself, and it creates a little buffer space * under the menu to keep things from looking too crowded */ #fileMenu { padding-bottom: 1em; } /* * The file menu should have a standard sans-serif font for itself and all of * its children */ ul.filemenuBar, ul.filemenuBar a.fileMenuButton, ul.fileMenuChild, ul.fileMenuChild a.fileMenuItem { font: 1em Arial, sans-serif; } /* * This is the menu bar itself, with the colors and borders attempting to replicate * the theme from the default Windows environment */ ul.fileMenuBar { background-color: #bbb; border: 2px solid; border-color: #ede #777 #888 #ddd; color: #000; list-style-type: none; margin: 0; padding: 6px; text-align: left; } ul.fileMenuBar li { display: inline; padding-right: 1em; } ul.fileMenuBar a.fileMenuButton { background-color: transparent; border: 1px solid #bbb; color: #000; cursor: default; left: 0px; margin: 1px; padding: 2px 2px; position: relative; text-decoration: none; top: 0px; z-index: 1000000; } /* Highlight the choice the mouse is over */ ul.fileMenuBar a.fileMenuButton:hover { background-color: transparent; border-color: #ede #777 #888 #ddd; color: #000; } /* * Indent any choice that is selected or that the mouse is over if another * choice is selected */ ul.fileMenuBar a.fileMenuButtonActive, ul.fileMenuBar a.fileMenuButtonActive:hover { background-color: #bbb; border-color: #888 #ddd #ede #777; color: #000; left: 0px; top: 0px; } /* Define all of the children of the menu bar */ ul.fileMenuChild { background-color: #bbb; border: 2px solid; border-color: #ede #777 #888 #ddd; color: #000; display: none; left: 0px; padding: 1px; position: absolute; top: 6px; z-index: 1000001; } /* * Here is the one hack that was necessary simply because IE does not render * the drop-down menus with enough space, so a default width is set here. * This number can be anything the developer wants/needs for a width that will * accommodate all of the text lengths in the drop downs. */ ul.fileMenuChild li { display: block; padding: 0; width: 10em; } /* * IE will ignore this rule because it does not recognize the > in the rule. * This sets the width back to an auto value for other browsers. */ ul.fileMenuChild > li { width: auto; } ul.fileMenuChild a.fileMenuItem { color: #000; cursor: default; display: block; padding: 1px 1em; text-decoration: none; white-space: nowrap; } /* Highlight the choices in the child menus */ ul.fileMenuChild a.fileMenuItem:hover, ul.fileMenuChild a.fileMenuItemHighlight { background-color: #000; color: #fff; } ul.fileMenuChild a.fileMenuItem span.fileMenuItemArrow { margin-right: -0.75em; } /* * Create the separator bars in the menus. Once again, IE does not render this * quite right, as it adds more margin underneath the bar than it should. */ ul.fileMenuChild div.fileMenuItemSeperator { border-bottom: 1px solid #ddd; border-top: 1px solid #777; margin: 2px; }
There is nothing extraordinary about any of the CSS rules in the example, and this CSS should be 100 percent cross-browser-compliant. (I tried to avoid CSS2 rules whenever I could, but sometimes it is just necessary because of Internet Explorer.)
CSS2 is not implemented as completely in Internet Explorer as it is in other browsers. Because Internet Explorer currently has the largest market share among the available browsers, using too much of this standard could be problematic.
This file menu example uses the Prototype library as its base, as you saw in the script elements from Example 7-3. After the Prototype library is loaded, a file that contains code for browser detection is loaded. It is shown in Example 7-5.
Example 7-5. browser.js: Code for browser detection
/** * @fileoverview Example 7-5, browser.js: Code for browser detection * * This file, browser.js, contains the Browser object, which can be used for browser * detecting on the client. */ /** * This object, Browser, allows the developer to check the user's client against * specific browser clients. Currently, the following checks are supported: * - isIE (is the browser an Internet Explorer browser) * - isMoz (is the browser a Mozilla-based browser) * - isOpera (is the browser an Opera browser) * - isSafari (is the browser a Safari browser) * - isOther (is the browser an unknown browser) */ var Browser = { /** * This variable stores the browser's agent. * @private */ _agent: navigator.userAgent.toLowerCase( ), /** * This variable stores the browser's version/ * @private */ _version: navigator.appVersion.toLowerCase( ), /** * This variable stores whether the browser is an Internet Explorer browser * or not. */ isIE: false, /** * This variable stores whether the browser is a Mozilla-based browser or not. */ isMoz: false, /** * This variable stores whether the browser is an Opera browser or not. */ isOpera: false, /** * This variable stores whether the browser is a Safari browser or not. */ isSafari: false, /** * This variable stores whether the browser is some unknown browser or not. */ isOther: false, /** * This method, initialize, sets the boolean members of the class to their * appropriate values based on the values of the /_agent/ and /_version/ members. * * @member Browser * @constructor */ initialize: function( ) { this.isOpera = (this._agent.indexOf('opera') != -1); this.isIE = ((this._agent.indexOf('mac') != -1) && (this._version.indexOf('msie') != -1)); this.isOther = (this._agent.indexOf('konqueror') != -1); this.isSafari = ((this._agent.indexOf('safari') != -1) && (this_.agent.indexOf('mac') != -1)); this.isIE = ((this._version.indexOf('msie') != -1) && !this.isOpera && !(this._agent.indexOf('mac') != -1) && !this.isOther && !this.isSafari); this.isMoz = (!this.isOther && !this.isSafari && navigator.product && (navigator.product.toLowerCase( ) == 'gecko')); this.isOther = (!this.isIE && !this.isMoz && !this.isOpera && !this.isSafari); } }; /* use Prototype's cross-browser event handling methods for ease of use. */ try { /* * Call the initialize method of the Browser object when the load event * fires in the document */ Event.observe(document, 'load', Browser.initialize, false); } catch (ex) {}
Finally, there is the JavaScript for all of the menu manipulation, shown in Example 7-6.
Example 7-6. filemenu.js: Code for manipulating the file menu
/** * @fileoverview Example 7-6, filemenu.js: Code for manipulating the file menu * * This file, filemenu.js, contains the fileMenu object which is used to create * instances of a file menu on the page. */ /* Create a new class using Prototype's Class object */ var fileMenu = Class.create( ); /** * This object, fileMenu, creates the functionality for a file menu on the page. */ fileMenu.prototype = { /** * This member, _menu, holds the id of this file menu. * @private */ _menu: null, /** * This member, _activeButton, holds the element that is currently active. * @private */ _activeButton: null, /** * This method, initialize, is the constructor for the class. Any members * that need to be initialized should be here. * * @member fileMenu * @constructor * @param {String} p_element The element that represents the file menu. */ initialize: function(p_element) { /* * Currently unused, but nice to have for multiple instances of * the object */ this._menu = p_element; }, /** * This member, pageMousedown, is called on every mousedown event on the page * and determines if the menu should be reset based on where the user clicks on * the page. * * @member fileMenu * @param {Object} e The event that called the method. * @return Returns false so that no other event will be triggered. * @type Boolean * @see #getContainerWith * @see #resetButton */ pageMousedown: function(e) { var target = null; /* Is the file menu active? */ if (!this._activeButton) return; /* Is the client Internet Explorer? */ if (Browser.isIE) target = window.event.srcElement; else target = (e.target.tagName ? e.target : e.target.parentNode); /* Is the event target the active button? */ if (this._activeButton == target) return; /* Is the target not part of the file menu? */ if (!this.getContainerWith(target, 'UL', 'fileMenuChild')) { this.resetButton(this._activeButton); this._activeButton = null; } return (false); }, /** * This method, buttonClick, is called when the user clicks on one of the * buttons that are on the main menu bar. It determines if where the user * clicked on the menu bar is the active button, or if it is a different button * and another button's drop-down menus may need to be cleaned up and reset. * * @member fileMenu * @param {Object} e The event that called the method. * @param {String} p_fileMenuId The id of the file menu that is being used. * @return Returns false so that no other event will be triggered. * @type Boolean * @see #fileMenuInit * @see #resetButton * @see #depressButton * @see #buttonMouseover */ buttonClick: function(e, p_fileMenuId) { var button = null; /* Is the client Internet Explorer? */ if (Browser.isIE) button = window.event.srcElement; else button = e.currentTarget; /* Blur the focus of the button here to remove the annoying outline */ button.blur( ); /* Is this button part of the file menu already? */ if (!button.fileMenu) { button.fileMenu = $(p_fileMenuId); /* Is this button already initialized? */ if (!button.fileMenu.isInitialized) this.fileMenuInit(button.fileMenu); } /* Is there an active button already? */ if (this._activeButton) this.resetButton(this._activeButton); /* Is the button already activated? */ if (button != this._activeButton) { this.depressButton(button); this._activeButton = button; } else this._activeButton = null; return (false); }, /** * This member, buttonMouseover, is called on a mouseover event on a button on * the main menu bar of the file menu. If a different button was already active, * then activate the current one instead. * * @member fileMenu * @param {Object} e The event that called the method. * @param {String} p_fileMenuId The id of the file menu that is being used. * @see #buttonClick */ buttonMouseover: function(e, p_fileMenuId) { var button = null; /* Is the client Internet Explorer? */ if (Browser.isIE) button = window.event.srcElement; else button = e.currentTarget; /* Should this button be activated? */ if (this._activeButton && this._activeButton != button) this.buttonClick(e, p_fileMenuId); }, /** * This method, depressButton, is called on a buttonClick when a new drop-down * menu needs to be activated and positioned. * * @member fileMenu * @param {Object} p_button The button that has been pressed. * @see #getPageOffsetLeft * @see #getPageOffsetTop * @see Element#addClassName * @seee Element#setStyle * @see #buttonClick */ depressButton: function(p_button) { var x, y; /* * Make the button look depressed (no, not sad) and show the drop down * associated with it */ $(p_button).addClassName('fileMenuButtonActive'), /* Position any associated drop down under the button and display it */ x = this.getPageOffsetLeft(p_button); y = this.getPageOffsetTop(p_button) + p_button.offsetHeight; /* Is the client Internet Explorer? */ if (Browser.isIE) { x −= p_button.offsetWidth; y += p_button.offsetParent.clientTop; } $(p_button).setStyle({ left: x + 'px', top: y + 'px', display: 'block' }); }, /** * This method, resetButton, does what it says; it resets the button, closing * all submenus. * * @member fileMenu * @param {Object} p_button The button that has been pressed. * @see #closeSubFileMenu * @see Element#removeClassName * @see Element#setStyle * @see #pageMousedown * @see #buttonClick */ resetButton: function(p_button) { $(p_button).removeClassName('fileMenuButtonActive'), /* Does the button have a file menu? */ if (p_button.fileMenu) { this.closeSubFileMenu(p_button.fileMenu); $(p_button).setStyle({ display: 'none' }); } }, /** * This method, fileMenuMouseover, is called on a mouseover MouseEvent over any * of the drop-down menus in the file menu bar. Its main purpose is to close * submenus when they should no longer be active. * * @member fileMenu * @param {Object} e The event that called the method. * @see #getContainerWith * @see #closeSubFileMenu * @see Element#hasClassName */ fileMenuMouseover: function(e) { var fileMenu; /* Is the client Internet Explorer? */ if (Browser.isIE) fileMenu = this.getContainerWith(window.event.srcElement, 'UL', 'fileMenuChild'), else fileMenu = e.currentTarget; /* Does this menu have submenus? */ if (fileMenu.activeItem && ($(fileMenu.activeItem).hasClassName('fileMenuButton') && $(fileMenu.parentNode.firstChild).hasClassName('fileMenuItem'))) this.closeSubFileMenu(fileMenu); }, /** * This method, fileMenuItemMouseover, is called when there is a mouseover event * on one of the menu items that has a submenu attached to it. The method * calculates the position where the submenu should be placed in relation to the * menu item of the event. * * @member fileMenu * @param {Object} e The event that called the method. * @param {String} p_fileMenuId The id of the file menu that is being used. * @see #getContainerWith * @see #closeSubFileMenu * @see #fileMenuInit * @see #getPageOffsetLeft * @see #getPageOffsetTop * @see Element#hasClassName * @see Element#addClassName * @see Element#setStyle */ fileMenuItemMouseover: function(e, p_fileMenuId) { var item, fileMenu, x, y; /* Is the client Internet Explorer? */ if (Browser.isIE) item = this.getContainerWith(window.event.srcElement, 'A', 'fileMenuItem'), else item = e.currentTarget; fileMenu = this.getContainerWith(item, 'UL', 'fileMenuChild'), /* Does the file menu have an active item? */ if (fileMenu.activeItem) this.closeSubFileMenu(p_fileMenuId); /* Is there a file menu id? */ if (p_fileMenuId) { fileMenu.activeItem = item; /* Does the class name already exist? */ if (!$(item).hasClassName('fileMenuItemHighlight')) $(item).addClassName('fileMenuItemHighlight'), /* Has the sub file menu been attached already? */ if (item.subFileMenu == null) { item.subFileMenu = $(p_fileMenuId); /* Has the sub file menu already been initialized? */ if (!item.subFileMenu.isInitialized) this.fileMenuInit(item.subFileMenu); } /* Calculate the x and y positions where the submenu should be placed */ x = this.getPageOffsetLeft(item) + item.offsetWidth; y = this.getPageOffsetTop(item); /* Is the client Opera? */ if (Browser.isOpera) { x = item.offsetWidth; y = item.offsetTop; } /* Is the client Internet Explorer? */ if (Browser.isIE) { x -= (this._activeButton.offsetWidth * 2); y -= this._activeButton.offsetHeight; } var maxX, maxY; /* Is the client Internet Explorer? */ if (Browser.isIE) { maxX = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft) + (document.documentElement.clientWidth != 0 ? document.documentElement.clientWidth : document.body.clientWidth); maxY = Math.max(document.documentElement.scrollTop, document.body.scrollTop) + (document.documentElement.clientHeight != 0 ? document.documentElement.clientHeight : document.body.clientHeight); } /* Is the client Opera? */ if (Browser.isOpera) { maxX = document.documentElement.scrollLeft + window.innerWidth; maxY = document.documentElement.scrollTop + window.innerHeight; } /* Is the client Mozilla? */ if (Browser.isMoz) { maxX = window.scrollX + window.innerWidth; maxY = window.scrollY + window.innerHeight; } maxX -= item.subFileMenu.offsetWidth; maxY -= item.subFileMenu.offsetHeight; /* Is the x coordinate bigger than the maximum it can be? */ if (x > maxX) x = Math.max(0, x - item.offsetWidth - item.subFileMenu.offsetWidth + (menu.offsetWidth - item.offsetWidth)); y = Math.max(0, Math.min(y, maxY)); /* Show the submenu */ $(item).setStyle({ left: x + 'px', top: y + 'px', display: 'block' }); /* Is the client Internet Explorer? */ if (Browser.isIE) window.event.cancelBubble = true; else e.stopPropagation( ); } }, /** * This method, closeSubFileMenu, hides the submenu from the user and then * turns off all references to the submenu and that it was active. * * @member fileMenu * @param {Object} p_fileMenu The file menu that is to be closed. * @see #resetButton * @see #fileMenuMouseover * @see #fileMenuItemMouseover */ closeSubFileMenu: function(p_fileMenu) { /* Does the file menu not exist or is it not active? */ if (!p_fileMenu || !p_fileMenu.activeItem) return; /* Does the file menu have an active sub file menu? */ if (p_fileMenu.activeItem.subFileMenu) { this.closeSubFileMenu(p_fileMenu.activeItem.subFileMenu); $(p_fileMenu.activeItem.subFileMenu).setStyle({ display: none }); p_fileMenu.activeItem.subFileMenu = null; } $(p_fileMenu.activeItem).removeClassName('fileMenuItemHighlight'), p_fileMenu.activeItem = null; }, /** * This method, fileMenuInit, goes through a submenu and associates all of * the children to the menu as a part of the menu. It also sets up the * offset sizes of the submenu for positioning of the menu. * * @member fileMenu * @param {Object} p_fileMenu The file menu that is to be closed. * @see Element#setStyle * @see Element#hasClassName * @see #buttonClick * @see #fileMenuItemMouseover */ fileMenuInit: function(p_fileMenu) { var itemList, spanList, textElement, arrowElement, itemWidth, w, dw; /* Is the client Internet Explorer? */ if (Browser.isIE) { $(p_fileMenu).setStyle({ lineHeight: '2.5ex' }); spanList = p_fileMenu.getElementsByTagName('span'), /* Loop through the <span> elements */ for (var i = 0, il = spanList.length; i < il; i++) /* Does the <span> element have the class name? */ if ($(spanList[i]).hasClassName('fileMenuItemArrow')) { $(spanList[i]).setStyle({ fontFamily: 'Webdings' }); spanList[i].firstChild.nodeValue = '4'; } } itemList = p_fileMenu.getElementsByTagName('a'), /* Does the itemList have any <a> elements? */ if (itemList.length > 0) itemWidth = itemList[0].offsetWidth; else return; /* Loop through the <a> elements */ for (var i = 0, il = itemList.length; i < il; i++) { spanList = itemList[i].getElementsByTagName('span'), textElement = null; arrowElement = null; /* Loop through the <span> elements */ for (var j = 0, jl = spanList.length; j < jl; j++) { /* Does the <span> element have the class name? */ if ($(spanList[j]).hasClassName('fileMenuItemText')) textElement = spanList[j]; /* Does the <span> element have the class name? */ if ($(spanList[j]).hasClassName('fileMenuItemArrow')) arrowElement = spanList[j]; } } /* Do the /textElement/ and /arrowElement/ exist? */ if (textElement && arrowElement) { $(textElement).setStyle({ paddingRight: (itemWidth - (textElement.offsetWidth + arrowElement.offsetWidth)) + 'px' }); /* Is the client Opera? */ if (Browser.isOpera) $(arrowElement).setStyle({ marginRight: '0' }); } /* Is the client Internet Explorer? */ if (Browser.isIE) { w = itemList[0].offsetWidth; $(itemList[0]).setStyle({ width: w + 'px' }); dw = itemList[0].offsetWidth - w; w −= dw; $(itemList[0]).setStyle({ width: w + 'px' }); } p_fileMenu.isInitialized = true; }, /** * This method, getContainerWith, finds the element with a given tag and * class name and returns the discovered element or or null. * * @member fileMenu * @param {Object} p_element The element to check. * @param {String} p_tagname The tag name to look for. * @param {String} p_className The class name to look for. * @return Returns the discovered element, if found, or null. * @type Object * @see Element#hasClassName * @see #pageMousedown * @see #fileMenuMouseover * @see #fileMenuItemMouseover */ getContainerWith: function(p_element, p_tagname, p_className) { /* Traverse the element tree while there are elements */ while (p_element) { /* Does the element have the correct tag name and class name? */ if (p_element.tagName && p_element.tagName == p_tagname && $(p_element).hasClassName(p_className)) return (p_element); p_element = p_element.parentNode; } return (p_element); }, /** * This method, getPageOffsetLeft, returns the left offset of the element * in relation to the page. * * @member fileMenu * @param {Object} p_element The element to get the offset from. * @return Returns the left page offset of the element. * @type Integer * @see #getPageOffsetLeft * @see #depressButton * @see #fileMenuItemMouseover * @see #fileMenuItemMouseover */ getPageOffsetLeft: function(p_element) { var x; x = p_element.offsetLeft; /* Is the client not Mozilla and does the element have a parent offset? */ if (!Browser.isMoz && p_element.offsetParent) x += this.getPageOffsetLeft(p_element.offsetParent); return (x); }, /** * This method, getPageOffsetTop, returns the top offset of the element in * relation to the page. * * @member fileMenu * @param {Object} p_element The element to get the offset from. * @return Returns the left page offset of the element. * @type Integer * @see #getPageOffsetLeft * @see #depressButton * @see #fileMenuItemMouseover * @see #fileMenuItemMouseover */ getPageOffsetTop: function(p_element) { var y; y = p_element.offsetTop; /* Is the client not Mozilla and does the element have a parent offset? */ if (!Browser.isMoz && p_element.offsetParent) y += this.getPageOffsetTop(p_element.offsetParent); return (y); } }; /* Create a new instance of the fileMenu class (this calls fileMenu.initialize( )) */ var menu = new fileMenu('fileMenu'), try { /* * Set an event listener on the document for all mousedown events and * have the system call the /pageMousedown( )/ method, binding it to the menu * object that was just created. This allows for the creation of multiple * file menus, if there is ever a need. */ Event.observe(document, 'mousedown', menu.pageMousedown.bind(menu), true); } catch (ex) {}
I have checked the XHTML, CSS, and JavaScript for this file menu and they work with Opera 9.01+, Firefox 1.5+, and Internet Explorer 6+. Netscape Browser 8.1 has problems rendering submenus from the drop-down menus with the proper CSS rules applied to them. Figure 7-6 shows how this file menu would normally appear in the browser.
Creating a file menu that functions like those that most users are comfortable with in Windows applications is a good step toward providing a seamless transition from the desktop to Ajax web applications. But now we need some Ajax for the file menu, right? If this file menu was a bit more complicated, it would add some size to the page download, which adversely affects download times. Slowing down application speed is never a good thing.
A way to reduce this file size is to load only the main part
of the menu, and load the other parts of the menu when the user
clicks to activate them. Here is where we can use some Ajax. By
adding additional event listeners to the click MouseEvent
, we can grab the
necessary drop-down and submenus that are requested. That way,
once they have been loaded the first time, there is no need to
load them again, providing the speed we want from our
application.
First, we need to alter the XHTML page, filemenu.html, like this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <head> <title>Alteration offilemenu.html</title> <meta http-equiv="content-type" content="text/html; charset=utf-8" /> <meta name="author" content="Anthony T. Holdener, III (ath3)" /> <meta http-equiv="imagetoolbar" content="no" /> <style type="text/css"> body { background-color: #fff; color: #000; font: 1em Georgia, serif; font-size: 12px; margin: 0; padding: 0; } </style> <link rel="stylesheet" type="text/css" media="screen" href="filemenu.css" /> <script type="text/javascript" src="prototype.js"> </script> <script type="text/javascript" src="browser.js"> </script> <script type="text/javascript" src="filemenu.js"> </script> <script type="text/javascript" src="loadmenu.js"> </script> </head> <body> <div id="bodyContent"> <div id="fileMenu"> <ul id="navMenu" class="fileMenuBar"> <li> <a href="file/" class="fileMenuButton" accesskey="F" hreflang="en" tabindex="1" onclick="loadMenu('fileSub'), return menu.buttonClick(event, 'fileSub')," onmouseover="loadMenu('fileSub'), menu.buttonMouseover(event, 'fileSub'),"> File </a> <ul id="fileSub" class="fileMenuChild" onmouseover="menu.fileMenuMouseover(event)"></ul> </li> <li> <a href="edit/" class="fileMenuButton" accesskey="E" hreflang="en" tabindex="9" onclick="loadMenu('editSub'), return menu.buttonClick(event, 'editSub')," onmouseover="loadMenu('editSub'), menu.buttonMouseover(event, 'editSub'),"> Edit </a> <ul id="editSub" class="fileMenuChild" onmouseover="menu.fileMenuMouseover(event)"></ul> </li> <li> <a href="find/" class="fileMenuButton" accesskey="N" hreflang="en" tabindex="13"> Find </a> </li> </ul> </div> <h1>This is a File Menu example</h1> </div> </body> </html>
All of the submenu data has been removed, making this download a little bit smaller. As I said, the benefits come when you have larger and more complex file menus in your application. Nothing changes with the CSS rules in filemenu.css, or in the filemenu.js JavaScript file. The other change, you may have noticed, is that we added another JavaScript file to handle the Ajax, shown here:
/** * This function, loadMenu, calls the server via an /XMLHttpRequest/ for a new * menu if one has not already been called for with the passed /p_id/ parameter. * * @param {String} p_id The id of the container to hold the menu. */ function loadMenu(p_id) { /* Is the /innerHTML/ blank? */ if ($(p_id).innerHTML == '') new Ajax.Request('get_sub_menu.php', { method: 'post', parameters: 'id=' + p_id, onSuccess: function(xhrResponse) { $(p_id).innerHTML = xhrResponse.responseText; }, onFailure: function(xhrResponse) { $(p_id).innerHTML = xhrResponse.statusText; } }); }
The only assumption this JavaScript makes is that the
get_sub_menu.php file called
on the server must return formatted XHTML that can be directly
inserted into the innerHTML
of
the submenu. For example, the response for loadMenu('editSub'),
would look like
this:
<li> <a href="edit/copy/" class="fileMenuItem" hreflang="en" tabindex="10" onmouseover="menu.fileMenuItemMouseover(event);"> Copy </a> </li><li> <a href="edit/cut/" class="fileMenuItem" hreflang="en" tabindex="11" onmouseover="menu.fileMenuItemMouseover(event);"> Cut </a> </li><li> <a href="edit/paste/" class="fileMenuItem" hreflang="en" tabindex="12" onmouseover="menu.fileMenuItemMouseover(event);"> Paste </a> </li>
The other thing to notice with the loadMenu( )
function is that it first
checks whether the submenu being requested has already been
loaded. This keeps the application from repeatedly calling the
Ajax function and bogging down the server with unwanted
requests.
One last thought, and then we will be done with menus. To take the file menu example fully to the Web 2.0 level, we could set some CSS rules to make the submenus slightly less opaque. This will also enhance the idea that the web application can do everything (and sometimes more) than its desktop counterpart can do.
One good way to separate related content on your site is to use tabs. You can create tabs simply by using CSS to style an XHTML list, or you can make them with images. Images were always the way to create tabs in a classic web development environment, but I will show you a newer spin on the image tab technique. The goal of all tab navigation, as with menu navigation, is to allow the tabs to degrade with browsers that do not support the CSS techniques that are used.
The simplest way to build tabs is to add a little style to an XHTML list. Starting to notice a theme yet? By not using JavaScript, the developer makes his application more accessible to users. Obviously, once we throw Ajax into the mix, we will use JavaScript; however, even if the Ajax fails, the tabs might still work separately.
Figure 7-7 shows a few examples of what tabs can look like using CSS and XHTML lists. These tabs are no more complicated than the first menus I showed earlier in this chapter.
Both of the lists in Figure 7-7 use the following XHTML list as their underlying structure:
<div id="tabMenu"> <ul id="tabList"> <li id="active"> <a href="xhtml/" id="current" accesskey="H" hreflang="en" tabindex="1"> XHTML </a> </li><li> <a href="css/" accesskey="C" hreflang="en" tabindex="2">CSS</a> </li><li> <a href="js/" accesskey="J" hreflang="en" tabindex="3">JavaScript</a> </li><li> <a href="dom/" accesskey="D" hreflang="en" tabindex="4">DOM</a> </li><li> <a href="xml/" accesskey="X" hreflang="en" tabindex="5">XML</a> </li> </ul> </div>
Tab #1 is created using the following CSS rules:
#tabList { border-bottom: 1px solid #787; font: bold 1em Arial, sans-serif; margin-left: 0; padding: 3px 0 3px 1em; } #tabList li { display: inline; list-style: none; margin: 0; } #tabList li a { background-color: #bfb; border: 1px solid #787; border-bottom: none; margin-left: 3px; padding: 3px .5em; text-decoration: none; } #tabList li a:link { color: #484; } #tabList li a:visited { color: #676; } #tabList li a:hover { background-color: #ada; border-color: #272; color: #000; } #tabList li a#current, #tabList li a#current:hover { background: white; border-bottom: 1px solid #fff; color: #000; cursor: default; }
This first tab navigation is very simple in nature. The trick
is in switching colors based on where the mouse is, and changing the
borders along with it. Making the top border of #current
white gives the illusion that it
is part of the rest of the page, while the other tabs sit behind
it.
Tab #2 uses the following CSS rules:[7]
#tabMenu { background: #7a7; border-top: 1px solid #333; height: 2.5em; padding: 0; } #tabList { display: block; font: 1em Arial, sans-serif; margin-top: -1px; padding: 0 0 0 1em; } #tabList li { float: left; list-style: none; } #tabList a { background-color: #cfc; border: 1px solid #aca; border-top: 1px solid #333; color: #000; display: block; margin: 0; padding: 1px 6px; text-decoration: none; } #tabList a:hover { background-color: #9b9; border: 1px solid #333; color: #333; padding: 1px 6px; } #tabList li a#current { background: #fff; border: 1px solid #333; border-top: 1px solid #fff; cursor: default; } #tabList li#active { border-bottom: 2px solid #777; border-right: 2px solid #777; }
With this tab navigation, I wanted to give the illusion of
depth by dropping a shadow on the active tab. To do this, I had to
float the individual <li>
elements to the left, and then shift the entire <ul>
element up one pixel. I created
the tabs in much the same way as I did in tab #1. I created the
shadow by putting right and bottom borders on the #active li
element. The floating nature of
the <li>
elements allowed
the border of the #active
element
to be visible from underneath.
What’s frustrating when creating tabs using CSS is that all tabs are rectangular. Until CSS3 style rules become a recommendation and we can create curves using CSS, we have only one option: images.
I told you before that we will not be able to use images for
tabs in the same way we did in the earlier days of web design. One
option is to use a technique similar to the one we used for our
image navigation bar: creating multiple images and changing them
using :hover
in CSS. Primarily we
want to avoid having to rely on JavaScript to change the images (the
old rollover technique).
We will create all views of our tab in one image, and clip out everything but the part of the image we want for any given tab state. It sounds much more complicated than it is. Figure 7-8 shows an example of a tab with all states in one image.
First we need to set up our XHTML structure as follows:
<div id="tabMenu"> <ul id="tabList"> <li> <aid="xhtml"
class="selected" href="xhtml/" accesskey="H" hreflang="en" tabindex="1"> XHTML </a> </li><li> <aid="css"
href="css/" accesskey="C" hreflang="en" tabindex="2">CSS</a> </li><li> <aid="js"
href="js/" accesskey="J" hreflang="en" tabindex="3"> JavaScript </a> </li><li> <aid="dom"
href="dom/" accesskey="D" hreflang="en" tabindex="4">DOM</a> </li> <li> <aid="xml"
href="xml/" accesskey="X" hreflang="en" tabindex="5">XML</a> </li> </ul> </div>
This is almost the same XHTML list as before, but this time
the id
attribute was added to
each <a>
element. To
utilize our image correctly, we need to know the size of an
individual tab state. In this case, it is 27 pixels high and 95
pixels wide. The CSS rules for this technique look like this:
#tabMenu { border-bottom: 1px solid #700; padding: 1em 1em 0 1em; margin: 0; } #tabList { height: 26px; margin: -1px; overflow: hidden; padding: 0px; } #tabList a { background: top left no-repeat url('tabs.png'), background-position: 0 -54px; color: #fff; float: left; margin-right: 5px; overflow: hidden; padding: 6px 0px; text-align: center; text-decoration: none; width: 95px; } #tabList a:hover { background-position: 0 -27px; color: #ccc; } #tabList a:active, #tabList a.selected { background-position: 0 0px; color: #000; cursor: default; }
The <ul>
element
#tabList
is set to a height of 26
pixels (not 27 pixels, because we want the image to meet up with the
bottom horizontal line). Then the links in the #tabList
are set. The <a>
element has its background set
with the tab image, and then it is positioned down to the bottom of
the three tabs. The width
of the
tab is set to 95 pixels here, and the other important rule is to set
the overflow
to hidden
. Because we’re doing this, the user
can view only one part of the image at any time.
For the a:hover
, the
background-position
is shifted
down 27 pixels, which is to the middle tab in the image. Finally, in
the a:active
and a.selected
rules, the background-position
is set to the top of
the image. Everything but the top tab is hidden (clipped) from view.
Figure 7-9 shows how these tabs would
look.
Now, it is one thing to build tabbed navigation—or menu bars, for that matter—but they need to do something. Browsers that do not support the CSS to build the tabs can still follow the link to navigate the application. This is also the case for users who have JavaScript turned off. For the rest of us, we need these tabs to be more functional.
I will refer to the area in which the data is placed or viewed
as the tab content. These
<div>
elements contain
everything the developer wants the user to see when the user selects
one of the navigation tabs. Therefore, we need to change the
display
of the element in
relation to the tab that the user clicks. But first things first; we
need to construct the tab content. For example:
<div id="tabContents"> <div id="xhtmlContent"> <h1>XHTML</h1> <!-- more xhtml content here --> </div> <div id="cssContent"> <h1>Cascading Style Sheets (CSS)</h1> <!-- more css content here --> </div> <div id="jsContent"> <h1>JavaScript</h1> <!-- more js content here --> </div> <div id="domContent"> <h1>The DOM</h1> <!-- more dom content here --> </div> <div id="xmlContent"> <h1>XML</h1> <!-- more xml content here --> </div> </div>
This structure part is easy enough, but next you need to have a CSS rule to hide all of the tab content sections from view. Something like this would work:
#tabContents div { display: none; }
This hides all of the <div>
elements that are contained in
the #tabContents div
. We will
rely on JavaScript to highlight one of the tabs when the page loads.
Example 7-7 shows
this JavaScript code for making the tabs dynamically functional.
This JavaScript requires the Prototype library to be loaded in order
to work.
Example 7-7. tabs.js: The JavaScript for dynamic tab content
/** * @fileoverview Example 7-7, tabs.js: The JavaScript for dynamic tab content. * * This file, tabs.js, contains the tabNavigation object which is used to create * instances of a tab navigation system on the page. */ /* Create a new class using Prototype's Class object */ var tabNavigation = Class.create( ); /** * This object, tabNavigation, provides the developer with the means of * creating a tabbed navigation system on the page. */ tabNavigation.prototype = { /** * This member, _tabs, holds the id of the tabbed navigation. * @private */ _tabs: null, /** * This member, _previousTab, holds the id of the last tab clicked by the user. * @private */ _previousTab: null, /** * This member, initialize, is the constructor for the class. Any members that * need to be initialized are done here, as well as any other initial * housecleaning. * @member tabNavigation * @constructor * @param {String} p_id The id of the element that represents the tabbed * navigation. * @param {String} p_startTab The id of the starting tab element. * @see #expandTab */ initialize: function(p_id, p_startTab) { this._tabs = p_id; /* Get a list of link elements found in the tab list */ var tabLinks = $(this._tabs).getElementsByTagName('a'), /* Add a click event to all of the link elements */ for (var i = tabLinks.length - 1; i >= 0;) tabLinks[i--].setAttribute('onclick', 'return tabs.expandTab(this.id);'), /* Expand the starting tab */ this.expandTab(p_startTab); }, /** * This method, expandTab, is called on all click MouseEvents associated with * the tab navigation and hides the contents of the previous tab before showing * the contents of the current tab. * * @member tabNavigation * @param {String} p_linkId The id of the tab to expand. * @return Returns false so that no other events fire after this one. * @type Boolean * @see #highlightTab * @see Element#setStyle */ expandTab: function(p_linkId) { var catId; this.highlightTab(p_linkId); /* Is there a previous tab selected */ if (this._previousTab) $(this._previousTab).setStyle( { display: 'none' }); catId = p_linkId + 'Content'; $(catId).setStyle({ display: 'block' }); this._previousTab = catId; return (false); }, /** * This member, highlightTab, is called from the expandTab method and removes the * CSS rule for highlighting on all of the tabs and then sets it on the current * tab. * * @member tabNavigation * @param {String} p_linkId The id of the tab to expand. */ highlightTab: function(p_linkId) { var tabLinks = $(this._tabs).getElementsByTagName('a'), /* Loop through the list of <a> elements */ for (var i = tabLinks.length - 1; i >= 0;) $(tabLinks[i--]).removeClassName('selected'), $(p_linkId).addClassName('selected'), } }; var tabs; try { /* * Set an event listener on the document for the load event and have the system * create a new instance of the tabNavigation class (this calls * tabNavigation.initialize( )). This allows for the creation of multiple file * menus, if there is ever a need. */ Event.observe(window, 'load', function( ) { tabs = new tabNavigation('tabList', 'xhtml'), }, true); } catch(ex) {}
Separating the content of the page into tabbed sections is a start to satisfying the following WAI-WCAG 1.0 guideline:
Priority 2 checkpoint 12.3: Divide large blocks of information into more manageable groups where natural and appropriate.
This exposes tab content that is associated with a tab in the navigation. For now, let’s just assume that the content was already there at page load. We will discuss Ajax solutions to this in Chapter 8.
We have discussed the major navigation aids—menus and tabs—but plenty of other navigational components can appear in a web application. Think of a site that you believe has good navigation, i.e., it’s easy to navigate and find things in it because it is organized and gives you the tools needed for navigation. Did you think of a site that used a tree of links or vertical links? Maybe the site broke a page into smaller chunks with page links, what I call paged navigation. More than likely, the site had some simple tools such as breadcrumbs and in-site links.
A good site will have some combination of navigational components to provide smooth and easy navigation. Users never want to feel lost in an application, and any visual cue (breadcrumbs, a tree of links) is welcomed.
Breadcrumbs are visual aids that help the user keep track of where he is and how he got there. Figure 7-10 shows an example of breadcrumbs found on Amazon.com.
These breadcrumbs link back to the preceding sections relative to the user’s current location. In this case, the user is under the JavaScript section, and the links take him back to Scripting & Programming, Web Development, Computers & Internet, and Books.
Breadcrumbs are simple to create using XHTML lists and a little CSS. Consider the following:
<div id="breadContainer"> <ul id="breadList"> <li><a href="xhtml/" hreflang="en" tabindex="1">XHTML</a></li> <li><a href="css/" hreflang="en" tabindex="2">CSS</a></li> <li><a href="js/" hreflang="en" tabindex="3">JavaScript</a></li> <li><a href="dom/" hreflang="en" tabindex="4">DOM</a></li> <li><a href="xml/" hreflang="en" tabindex="5">XML</a></li> </ul> </div>
We can style this list into a list of breadcrumbs, but that is not what we want to concentrate on. How is the list created? If the entire page is loaded, any server-side script can generate the correct XHTML list to display the breadcrumbs. What happens, however, when the entire page is not refreshed? This book is about Ajax, after all.
First, let’s be complete in our discussion of building breadcrumbs. You can style the preceding list into breadcrumbs using the following CSS rules:
#breadContainer { margin-left: 10px; } #breadList { list-style: none; margin: 0; padding: 0; } #breadList li { display: inline; margin: 0; padding: 0; } #breadList li:before { content: " 0BB 020"; } #breadList li:first-child:before { content: ""; } /* * The following is an ugly IE hack that I wish I didn't have to do, but IE and * CSS don't mix well yet */ /* This rule is for all IE browsers*/ * html #breadList li { border-left: 1px solid black; margin: 0 0.4em 0 -0.4em; padding: 0 0.4em 0 0.4em; } /* Win IE browsers - hide from Mac IE*/ * html #breadList { height: 1%; } * html #breadList li { display: block; float: left; } /* End the hide from Mac*/ /* This rule is for Mac IE 5*/ * html #breadList li:first-child { border-left: 0; }
Fat Erik 5 wrote this CSS, and you can find it on Listamatic
(http://css.maxdesign.com.au/listamatic/index.htm).
The only problem with this CSS is that it does not work in Internet
Explorer because IE does not support many pseudoselectors, and to be
more specific, it does not support the :first-child
pseudoselector. All we can do
is hope that Internet Explorer 8 (or whatever the next fix/version
of IE is called) fixes this dilemma.
As you can see from the preceding example, the cross-browser solution is to use a CSS hack to get Internet Explorer to produce something between the elements. The left border of the element does the trick in this case. It isn’t pretty, but then again, not much about Internet Explorer’s CSS2 support is pretty.
The other thing you might have noticed is that the solution to
putting something between elements is to use the content
property to include the characters