在Web应用上创建一个下拉菜单,可以有多种方法。有些基于CSS来实现,有些基于JavaScript来实现。这两种方法各有优劣。基于CSS的实现只使用CSS技术,比较好掌握,但不容易应付比较复杂,如多级菜单的情况,而且往往还需要采用各种hacks来应付不同浏览器的怪癖。而基于JavaScript的实现,原则上还需要CSS来负责页面的表现,但使用JavaScript来与用户交互。应该说,CSS属于表现层,JavaScript属于行为层,遵循关注点分离的原则,像下拉菜单这种动态情况还应使用JavaScript来实现更为合适。
本文使用基于JavaScript的技术来实现了水平导航下拉菜单,支持3级菜单,但在本文的后面,您可以看到,本文所用的方法,原则上并不仅限于3级菜单。这个实现有以下特点:
使用DOM接口,在各种浏览器上均可获得很好支持。
采用无干扰模式,HTML页面简单明了。
支持多级菜单。
算法高效,封装性强。
下图是这种菜单的最终效果图。
要实现这个菜单,需要三个文件协同工作:index.html, style.css以及h_nav_menu.js。
先看index.html文件。
基于JavaScript的水平导航下拉菜单
Demo -- 基于JavaScript的水平导航下拉菜单
内容很简单,主要集中于定义菜单。其结构分为4大部分,分别为banner, navigation, mainContent及footer,分别对应于网站的横幅,导航栏,主文及页脚。
注意,这是一个非常干净的Web页面,其中没有混杂丝毫的JavaScript脚本及CSS。这是一种良好的无干扰编程模式,维护很方便。
让我们把注意力集中到navigation的设置上。严格来说,id为navigation的这个div不并属于菜单,我们更愿意将其看作是整个菜单的包装区域。有了这个包装区域,我们就很容易地与其它页面区域分离。此外,您在下面将看到,这个div实际上还帮助我们编写出了高效的JavaScript脚本。
在navigation之下,每个ul实际上都是每级菜单的开始标签,而其下面的每个li则是该级菜单的各个菜单项,li之下的a则定义了菜单项的链接地址及菜单项的文本内容。各级菜单的菜单项及文本如下:
可以看出,这是一个3级菜单。菜单项的HTML标签结构图如下:
如果要增加下一级菜单,可按上面的HTML标签结构图来操作。
下面是这个index.html文件在未应用任何CSS及JavaScript脚本时在浏览器中的显示情况。
像白开水一样,该耐心地给它化妆了。下面我们开始编辑style.css。先定义一些基本规则。
body {
margin: 0;
padding: 0;
}
#banner {
color: #0F0;
background-color: #999;
}
#banner h1 {
color: #9bf25f;
margin: 0;
padding: 10px;
}
#mainContent {
border-top: 1px solid #DDD;
}
#footer {
text-align: center;
color: #999;
background-color: #CCC;
}
此时的效果图:
除了导航栏,其它区域的安排已经基本到位。该加工导航栏了。
#navigation ul { margin: 0; padding: 0; } #navigation ul li { list-style-type: none; float: left; text-align: center; padding: 2px; border: 1px solid #000; }
#mainContent {
clear: left;
border-top: 1px solid #DDD;
}
将navagation的ul区的margin及padding设为0,去掉其原来的空间距离,需要时再重新指定。接着,在#navigation ul li选择器中将li前面的符号去掉,并将它们浮动起来,这样这些菜单项就可以水平排列。同时将文本居中,并设置padding为2px,以与边框保持一定距离。暂时设置一个像素为1的黑边,可帮助我们看清各元素的排列。在#mainContent中,用clear: left;来清除浮动的效果。效果如下图:
此时,很像是一个表格,且各级菜单都归位于各自的单元格中。
默认情况下,UL及LI均是block型的,但由于我们对LI进行了浮动,LI变成了inine型,因此,首页、产品、技术支持、关于我们所在的LI均自动地从左到右排列。但LI之下的UL依旧保留了block型。因此,产品下面有一个UL,属block型,只能排在产品所在的block中。而该UL下的三个LI,最新设备,其它设备,耗材,属inline型,从左到右排列。再往下,以此类推。block的面积是可变的,总会自动加宽加长来容纳存放其中的子block。这样就形成了上面的效果。
但对于菜单来讲,这不是我们所要的效果。只需给LI加上一个width, 就可实现菜单的效果。
#navigation ul li {
list-style-type: none;
float: left;
width: 150px;
text-align: center;
padding: 2px;
border: 1px solid #000;
}
限定了一个具体的宽度后,子菜单项不再拼命地在水平空间上去挤了。
我们注意到mainContent的内容被往下推开了。另外一个问题则是第一级菜单与其下级菜单并不对齐,下级菜单往右偏移了一点。所幸的是,这两个问题以同时解决。
#navigation ul li {
position: relative;
list-style-type: none;
float: left;
width: 150px;
text-align: center;
padding: 2px;
border: 1px solid #000;
}
#navigation ul ul { position: absolute; left: -1px; }
对LI设定了position: relative后,被该元素所包围的下级子元素将相对于该元素来设定位置。而position: absolute可以标签脱离原来的流布局,以精确定位。同时,取消流布局后,其原来所占的空间将被释放,因此,下面的mainContent的内容得以紧挨在第一级菜单之下,且不因第二级以下的菜单是否弹出而变化位置。因此,上面的设定的意思是,取消二级菜单的流布局,但根据第一级菜单的位置来设定下级菜单的相对位置。left: -1px将二级菜单的位置往左移动了一个像素,从而与第一级菜单对齐。效果图如下。
因为继承的原因,第三级菜单被限定在第一级菜单所在的block。而正常的情况应是第三级菜单出现在第二级菜单的右侧。由于每个菜单项的宽度已被设为150px,因此,我们只需将第三级菜单的位置往右推开150px就行了。
#navigation ul ul {
position: absolute;
left: -1px;
}
#navigation ul ul ul { top: -1px; left: 150px; }
从效果图看出,150px的距离在水平上不能完美地对齐。这是由于此距离还受到了各元素的border, margin, padding等因素的影响。没关系,与您一样,我的数学也很差,才不会费劲去计算这些数学题。但显而易见,增加此值,则菜单往右偏移。试着增大其值,此时,155px的值正好合适。因为第三级菜单是第二级菜单的子块,因此top: -1px将其稍稍上移,从而与第二级菜单水平对齐。
#navigation ul ul ul {
top: -1px;
left: 155px;
}
现在,各个菜单已经基本就位,只是菜单同时打开了,有点眼花缭乱。我们先关闭第二、第三级菜单。
#navigation ul ul {
position: absolute;
left: -1px;
display: none;
}
哈,终于又回到简洁的界面了。现在,我们准备将navigation往下推开一点,并设置其链接样式。
#banner h1 {
color: #9bf25f;
margin: 0;
padding: 10px;
}
#navigation { margin: 5px; } #navigation a { text-decoration: none; color: #666; } #navigation a:hover { color: green; }
鼠标悬停时,文本是淡淡的绿色。但还有一些缺点,当鼠标悬停于文本之外时,鼠标指针不会变成手型。而且,文本与边框距离太小了,应使用padding撑开一点。但,padding不会起作用。上述两个问题均是由于A是inline型而造成的。没关系,display: block可以将其转换为block型。
#navigation a {
text-decoration: none;
color: #666;
display: block; padding: 5px;
}
效果好了一些。该做什么了?看看图1,产品及技术支持旁边均有一个向下的三角形,明确地告诉用户,这些菜单项下面还有级联菜单。这是一个很好的值得大力推荐的界面设计,且实现起来一点都不难,即将相应的标签附上一个背景图片就行了。
#navigation {
margin: 5px;
}
#navigation .downMenu { background: url(../images/nav_down.gif) no-repeat 95% center; } #navigation .rightMenu { background: url(../images/nav_right.gif) no-repeat 95% center; }
LI及A标签均可设置背景图片,但如果在LI标签设置此背景图片,当鼠标悬停于链接时,如果设置鼠标悬停的背景颜色,此颜色将覆盖此背景图片。鉴于此,应在A标签设置背景。在产品及技术支持的A标签添加downMenu的class,即可立即实现这个很酷的功能。
class="downMenu">产品
……
class="downMenu">技术支持
但这种方法有一些问题,上面只是设置了2个downMenu,但第二级以下的菜单可能需要设置很多的rightMenu。如果将这些设置都直接放在HTML页面上,无疑会严重地污染页面的简洁,维护网页将成为一种苦难。如果我们能将这种设置往后推延到运行时才自动设置,该有多好!而这是JavaScript这个强大的脚本语言大有用武之地。
现在,暂时抵制住诱惑,先将上面HTML页面的class=”downMenu”删除掉。我们的战场开始转移到这次作战最重要的第三条战线:JavaScript。
创建并编辑h_nav_menu.js。
1 window.onload = initMenu;
2
3 function initMenu() {
4 var theUL = document.getElementById("navigation").getElementsByTagName("ul")[0];
5 var theULChilds = theUL.childNodes;
6
7 for (var i = 0; i < theULChilds.length; i++) {
8 if (theULChilds[i].tagName == "LI") {
9 var theLINode = theULChilds[i];
10 if (theLINode.getElementsByTagName("ul").length > 0) {
11 theLINode.firstChild.className = "downMenu";
12 }
13 }
14 }
15 }
要读取网页上的元素,只有在网页加载完毕之后才可进行。第1行,确保网页加载完毕之后才执行initMenu()函数。
第4行,在取得id为navigation的元素后,通过getElementsByTagName("ul")取得其下所有的UL元素。该函数所返回的列表包含了所有嵌套的多级的UL元素。但我们只关心第一个,因此,通过下标访问符[0]返回第一个UL元素,并存放于theUL变量中。
第5行,取得theUL的所有子节点。在DOM2中,childNodes是节点是一个属性,它返回相应对象的下一级的所有子节点。与getElementsByTagName不同的地方是,它只返回紧邻下一级的所有子节点。此外,应注意的是,不同的浏览器所返回的元素数量不一样。有些浏览器,如Mozilla Firefox 3,将节点之间的回车换行符也作为TextNode返回。而另外一些浏览器,如IE7,则不返回这些空白节点。
第7行,开始遍历所返回的子节点。第8行,我们只关心LI节点,因此,对于不同浏览器的返回结果,此行可统一地过滤掉其他类型的子节点。第10到第12行,如果有下级菜单,则将LI节点下第一个子节点的className属性设为downMenu。而LI节点的第一个子节点正好是A标签:
产品
这样,产品 在运行时就变成了,这样,CSS就可以为其加载背景图片了。
我常常惊奇于浏览器的这种即时功能。只需要简单地改变其中一个属性,浏览器就能立即感知,并能毫无闪烁地刷新页面,将变更后的结果立即反馈回来。在桌面应用中,要实现这种功能,必须编写多行代码才能实现。因此,这也是Web编程的一种极大乐趣。
马上存盘并刷新页面,看看是不是所有带有下级菜单的菜单都出现了那迷人的倒立三角形?而此时,我们的HTML页面依旧是那么的干净!
现在,该将二级菜单请出来了。最直接的思路是,通过第一级菜单项的A标签的onmouseover事件,弹出子菜单,而在第一级菜单的onmouseout事件,关闭所有子菜单。没错,这种方法适用于我们将鼠标从产品上方移到同级菜单项,如技术支持时。
但还有第二种情况,即在产品的子菜单弹出后,当我们的鼠标从产品上方移到子菜单上时,若按上面的思路,产品的mouseout事件被触发,此事件将关闭所有的下级菜单。这样,我们就面临了一个难题,即如何在我们将鼠标从所触发的第一级菜单移至弹出的第二级菜单时,依旧显示第二级菜单?
即使我们解决了上述的问题,还需要考虑到这种情况,当我们的鼠标从子菜单直接移开至其他地方后,如何通知浏览器关闭产品的下级菜单?因为我们知道,此时的鼠标已经不在产品上方,因而已经不能触发产品的onmouseout事件了。这可真是请神容易送神难。为解决这个问题,送您一句至理箴言:同在一片蓝天下,撑起大伞好乘凉。
我们来观察网页的结构。
产品
从表面上看,最新设备是产品的下级菜单,但A与UL均是LI的同级子菜单。当我们在产品所在A标签设置onmouseover, onmouseout事件时,它无法管到同级的UL标签。但是,A与UL的同一片蓝天是什么?LI标签。如果在LI标签上设置onmouseover, onmouseout事件,不就一切问题都解决了吗?当鼠标移至产品所在A标签的父标签LI之上,LI的onmouseover事件被触发,显示出下面的子菜单。当鼠标从产品移至各个子菜单上,由于它们同是该LI的子标签,此时鼠标还未移出该LI,故而LI的onmouseout事件还没有触发,因此就不会自动关闭子菜单。当鼠标从产品上方移至其它同级菜单项上方时,实际上也离开了产品所在的LI,其onmouseout事件触发,此时,可以放心地关闭产品的所有子菜单了。
7 for (var i = 0; i < theULChilds.length; i++) {
8 if (theULChilds[i].tagName == "LI") {
9 var theLINode = theULChilds[i];
10 if (theLINode.getElementsByTagName("ul").length > 0) {
11 setMouseActions(theLINode); 12 theLINode.firstChild.className = "downMenu";
13 }
14 }
15 }
16 }
17
18 function setMouseActions(node) { 19 node.onmouseover = function() { 20 this.getElementsByTagName("ul")[0].style.display = "block"; 21 }; 22 23 node.onmouseout = function() { 24 this.getElementsByTagName("ul")[0].style.display = "none"; 25 }; 26 }
第11行,第18行至第26行是新增内容。代码很容易理解,通过两个匿名函数,分别设置onmouseover及onmouseout事件,将其下属的第一个UL标签的display分别设为block及none,从而显示或隐藏下级菜单。
border显得太多了,且二级菜单是透明的,透出了下面的背景,之间的分隔线也因重复设置了border而有加粗的现象。现在就来解决这些问题。
#navigation ul li {
position: relative;
list-style-type: none;
float: left;
width: 150px;
text-align: center;
padding: 2px;
border: 1px solid #000;
}
#navigation ul ul {
position: absolute;
left: -1px;
display: none;
border-bottom: 1px solid #CCC;
}
#navigation ul ul li { border: 1px solid #CCC; border-bottom-width: 0; background: #EEE; text-align: left; }
先删除第一级菜单中border的设置。这样,子元素就不会从其继承border的属性。对于第二级菜单,我们只需为其设置border后,再将border-bottom-width设为0,即取消下边框,这样就不会有重复的边界线了。并将它们的背景色设为一个浅灰色,文本左对齐。最后,在上级选择器,即#navigation ul ul中为其补上最后一项所缺失的下边框。值得一提的是,通过上级选择器为下级选择器补画某个缺失的边框线是CSS设计中一个常见手法。如果所有浏览器都支持:first-child这种伪元素就不会这么辛苦了。
#navigation a:hover {
color: green;
background-color: #FFF;
}
之后,设置鼠标悬停的背景为白色,以突出显示当前所指向的菜单项。
到时候请出第三级菜单了。其实现原理与第二级菜单基本一致,因此,可通过一个递归方法遍历所有级别的菜单。
1 window.onload = initMenu;
2
3 function initMenu() {
4 assembleMenu(document.getElementById("navigation"));
5 }
6
7 function assembleMenu(uLParentNode) {
8 var theUL = uLParentNode.getElementsByTagName("ul")[0];
9 var theULChilds = theUL.childNodes;
10
11 for (var i=0; i
12 if (theULChilds[i].tagName == "LI") {
13 var theLINode = theULChilds[i];
14 if (hasNextLevelMenu(theLINode)) {
15 setMouseActions(theLINode);
16 theLINode.firstChild.className = uLParentNode.tagName == "DIV" ? "downMenu" : rightMenu";
17
18 assembleMenu(theLINode);
19 }
20 }
21 }
22
23 function hasNextLevelMenu(node) {
24 return node.getElementsByTagName("ul").length > 0;
25 }
26
27 function setMouseActions(node) {
28 node.onmouseover = function() {
29 this.getElementsByTagName("ul")[0].style.display = "block";
30 };
31
32 node.onmouseout = function() {
33 this.getElementsByTagName("ul")[0].style.display = "none";
34 };
35 }
36 }
除增加新功能外,上面的代码还对之前的代码进行了一些必要的改进。第4行取得navigation元素作为遍历的起始点。第14行将算法包装为一个有更易理解的名称的函数。第16行,因为整个菜单只有最初的标签是DIV,因此,可依此判决各个菜单项是否第一级,并为其设置相应的className。第18行,递归调用第7行的assembleMenu()函数,从而遍历并自动设置所有的带有下级菜单的菜单项。
在JavaScript中,函数可以嵌套函数,根据这一强大的语言性能,我们将之前的setMouseActions()函数,连同hasNextLevelMenu()函数,一起包装进assembleMenu()函数中,这样,所有功能都集中于一个函数,只需传入最外层的节点,剩下的就不需要我们操心了。
现在,还剩一些小问题。当鼠标指向其它耗材时,其相应的上级菜单项,耗材、产品的背景色均应为白色。而鼠标移开时,应恢复其原来的背景色。
function setMouseActions(node) {
node.onmouseover = function() {
this.getElementsByTagName("ul")[0].style.display = "block";
this.firstChild.style.backgroundColor= "#FFF";
};
node.onmouseout = function() {
this.getElementsByTagName("ul")[0].style.display = "none";
this.firstChild.style.backgroundColor= "#EEE";
};
}
新问题出现了。当鼠标从产品及其下级菜单移开时,产品的背景色为灰色,与其它同级菜单项不一致。
我们只需将A的背景改为该颜色就行了。
#navigation .downMenu {
background:
#EEE url(../images/nav_down.gif) no-repeat 95% center;
}
#navigation .rightMenu {
background:
#EEE url(../images/nav_right.gif) no-repeat 95% center;
}
#navigation a {
text-decoration: none;
color: #666;
display: block;
padding: 5px;
background-color: #EEE;
}
现在,所有的步骤都已经完成。下面是在IE7中显示的效果图。可以看出,在Firefox及IE7这两个主流浏览器上均运行得很好。
本文一开始就提到,这种实现并不仅限于3级菜单。当增加新级菜单时,只需修改相应的HTML页面及CSS,而JavaScript则不需要修改。
附:本文所用到的两个图片: