DMenu, DHTML 菜单






4.83/5 (32投票s)
2002 年 10 月 24 日
11分钟阅读

360559

3861
一个 DHTML 菜单,展示了结构、表现和功能的完全分离
摘要
一个严格分离了结构、表现和功能的深度 DHTML 菜单。展示了 HTML 结合 CSS 和 JavaScript 的强大之处。

要求
要查看演示并运行源代码:Internet Explorer 6(无需 ASP 或任何其他服务器端支持)。
要理解本文:需要具备基本的 HTML 和中等至高级水平的 CSS 和 JavaScript 技能。
更新
我更喜欢在文章开头通知大家更新
- (2002/10/25) 支持 Mozilla 1.0
非常感谢 Nikhil Dabas 提供了符合 W3C DOM 标准的 JavaScript 代码,这意味着该菜单现在可以在 Mozilla 和 Internet Explorer 中运行了。令人惊奇的是,CSS 本身只需要进行一个微调,就能让菜单在 Mozilla 中几乎完美地显示。该菜单在 Mozilla 中可能有些小问题(有时子菜单不会关闭,如果你能找出原因,给你点赞。我怀疑这与事件冒泡有关),但它足够可用。对于感兴趣的人来说,CSS 的微调只是简单地告诉菜单项
LI
元素宽度为 100%(实际上是 98%,因为 100% 会覆盖包含的UL
元素的边框)。我还将子菜单向上和向右移动了一点,以免覆盖包含的菜单(Nihkil 的好主意)。
引言
互联网上存在着无数的 DHTML 菜单示例。从复杂的跨浏览器示例到华丽的 DirectX 驱动的奇迹。本文演示的 DHTML 菜单并非这些,它所展示的是如何创建一个严格分离了结构、表现和功能的 DHTML 菜单。
这些三个方面的分离之所以重要,有以下原因:
- 向前和向后兼容性
通过不紧密耦合这三个方面,可以保证未来的浏览器至少能够很好地呈现内容和结构,使其可用。同样,在旧浏览器中,如果浏览器不支持高级 CSS 或 JavaScript,它至少支持内容和结构,因此再次确保了菜单的可用性。
- Accessibility
通过不将内容和结构与表现和功能混合在一起,辅助技术(如盲人文本阅读器)可以轻松访问和理解内容。搜索引擎也会比混合内容更好地索引结构良好的内容。
- 维护
在这种设置中添加或删除菜单项非常简单。其他设置可能需要额外的 CSS 类、额外的 JavaScript 和复杂的结构更改(更不用说理解所有这些了)。
- 技能运用
正如 ASP.NET 鼓励开发者专注于他们擅长的领域一样,这种设置也是如此。UI 专家可以修改 CSS 文件,而不会干扰正在处理 JavaScript 文件或正在处理 HTML 文件的内容专家的工作。
- 信息访问
网络就是向所有人提供信息的。无论您使用何种设备,网页至少应该足够易于访问,以便阅读内容并导航网站。本文所示的分离对于多种设备来说非常完美,因为它会根据访问页面的设备优雅地降级。例如,手持浏览器将忽略 JavaScript 和 CSS,只将菜单渲染为列表,使其可用且可访问。稍微进行一些设备检测也能进一步改进,因为有些设备试图呈现超出其能力范围的内容。
菜单
实际的菜单非常简单。它由三个部分组成:HTML(结构和内容)、CSS 文件(表现或样式)和 JavaScript 文件(功能或工作原理)。
菜单可以显示任意深度的子菜单,但如果深度过大,屏幕空间可能会成为实际问题。同样,每个菜单列表可以显示任意数量的菜单项,并在一个列表中包含任意数量的子菜单。
菜单的功能(onmouseover
和 onmouseout
事件)仅在 BODY
元素 `onload` 时应用。这样做是为了确保所有结构都在 JavaScript 尝试执行其操作之前加载。
您无需为每个子菜单定义 CSS 样式。子菜单的位置是自动的(并在下面的 CSS 部分进行了解释)。
HTML
完整的 HTML 在源文件中。
...
<body onload="initialiseMenu();">
<h1>DMenu: Sample Simple</h1>
<ul id="mainmenu">
<li><a href="about/">About Us</a></li>
<li><a href="about/">Articles</a>
<ul>
<li><a href="articles/">C#</a>
<ul>
<li><a href="articles/">C# Reference</a></li>
<li><a href="articles/">Tutorials</a></li>
<li><a href="articles/">Samples</a></li>
<li><a href="articles/">FAQ</a></li>
</ul>
</li>...
...</ul>
</li>
<li><a href="about/">Tools</a>
<ul>
<li><a href="articles/">Assemblers</a></li>
<li><a href="articles/">Disassemblers</a></li>
<li><a href="articles/">CP Utilities</a>
</ul>
</li>
<li><a href="contactus/">Contact Us</a></li>
</ul>
...
正如您所见,它看起来就像一个普通的 UL
(无序列表)结构。每个 LI
元素内部都有一个 A
元素,用于控制超链接。
根据 W3C 的规范,嵌套列表的正确方法如上所示,例如,子 UL
包含在 LI
元素内部,而不是外部。这对于 JavaScript 的正常工作也至关重要(即,如果您没有正确嵌套列表,JavaScript 将无法正常工作)。
您需要了解的唯一设置是 BODY
元素中的 onload="initialiseMenu();"
声明。如果您知道如何从 JavaScript 文件内部将 initialiseMenu
方法附加到 onload
事件,请给我发邮件,谢谢。
JavaScript
HTML 非常简单,没有什么新东西。但 JavaScript 相当有趣且有吸引力。我将逐行解释它。
function showSubMenu(){
var objThis = this;
for(var i = 0; i < objThis.childNodes.length; i++)
{
if(objThis.childNodes.item(i).nodeName == "UL")
{
objThis.childNodes.item(i).style.display = "block";
break;
}
}
}
这个方法非常简单,它在鼠标悬停在 LI
元素上时被调用。如您将在下面看到的,分配了 onmouseover
事件,但没有传递参数来指示显示哪个元素或哪个元素触发了事件。幸运的是,当您进行这种分配并且事件触发时,浏览器会保留一个触发事件的元素的引用,并通过 this
对象访问它。非常方便。
此时,您拥有 this
,您所要做的就是遍历子元素,直到找到 UL
元素。一旦找到,就将 style.display
属性值更改为 block
。
顺便说一句,childNodes
方法返回元素的所有子元素,包括文本节点,因此在使用索引引用子元素时要小心(谢谢 Nikhil!)。
function hideSubMenu()
{
var objThis = this;
for(var i = 0; i < objThis.childNodes.length; i++)
{
if(objThis.childNodes.item(i).nodeName == "UL")
{
objThis.childNodes.item(i).style.display = "none";
break;
}
}
}
与 showSubMenu
方法几乎相同,但它将 style.display
属性更改为 none
,从而有效地隐藏了元素。
function initialiseMenu()
{
var objLICollection = document.body.getElementsByTagName("LI");
for(var i = 0; i < objLICollection.length; i++)
{
var objLI = objLICollection[i];
for(var j = 0; j < objLI.childNodes.length; j++)
{
if(objLI.childNodes.item(j).nodeName == "UL")
{
objLI.onmouseover=showSubMenu;
objLI.onmouseout=hideSubMenu;
for(var j = 0; j < objLI.childNodes.length; j++)
{
if(objLI.childNodes.item(j).nodeName == "A")
{
objLI.childNodes.item(j).className = "hassubmenu";
}
}
}
}
}
}
initialiseMenu
是一个有趣的函数。它在文档的 body
`onload` 时调用。
首先,它获取文档 DOM 树中所有 LI
元素的完整集合。getElementsByTagName
是一个非常有用的方法,用于返回任何元素类型的节点集合。它也可以在几乎任何元素上使用(即,不仅仅是 body
),允许您限制获取的范围。例如,在此文章中,最好使用 document.body.all.mainmenu.getElementsByTagName("LI")
,这样就不会返回其他非菜单 LI
元素。
接下来,代码通过获取集合的长度 objLICollection.length
来循环遍历集合。
对于集合中的每个节点,它会遍历其子元素以查找 UL
元素。
如果找到,它会将 showSubMenu
方法赋值给当前 LI
元素的 onmouseover
事件,并将 hideSubMenu
方法赋值给 onmouseout
事件。在进行此赋值时,请确保不要在赋值后加上 ()
,这通常是方法调用时进行的。如果您这样做,则该方法会在实际赋值时运行(又一个吸取的教训)。
最后要做的是再次遍历子元素,并将 hasssubmenu
CSS 类赋值给子 A
元素。这只会显示箭头图像,以向用户指示菜单项具有子菜单。
CSS
CSS 很短但有时很棘手。我将逐一介绍每个相关类。
...
ul, li
{
font-size: small;
margin-top: 0px;
margin-right: 0px;
margin-bottom: 0px;
display: block;
}
...
给所有 UL
和 LI
元素设置 no margins,字体大小为 small,显示类型为 block。
...
ul
{
width: 130px;
border: solid 1px #333333;
border-top: solid 5px #333333;
border-right: solid 2px #333333;
}
...
给所有 UL
元素设置 130px 的宽度。然后为 UL
分配一个 1px 的实心深灰色边框,但覆盖右边框和上边框,使其宽度为 5px。您可以单独定义每个边框属性,但这是一种更快捷的方式。
...
ul li ul
{
display: none;
position: absolute;
}
...
ul li ul
是一个 CSS 父子选择器。基本上它的意思是“将以下样式应用于任何 UL,该 UL 是 LI 的子元素,而该 LI 是 UL 的子元素。” 这样可以防止顶层菜单 UL
被赋予样式(这很重要,否则顶层菜单 UL
将从一开始就被隐藏)。
display: none;
实际上表示不以任何方式显示该元素,并且该元素不占用任何空间。例如,visibility: hidden
并非如此。它使元素不可见,但元素仍然占用空间。
position: absolute;
将元素从文档的正常流程中移出,并将其“浮动”在其他相对或静态定位的元素之上。这样,当元素可见时,它不会将其他元素向下“推”到文档流程中。另外,值得注意的是,这个 absolute
值仍然与其容器相关。这使得子菜单能够自动定位。
...
li a
{
padding: 2px;
text-decoration: none;
color: #000000;
background-color: #ffffee;
width: 100%;
display: block;
border-bottom: dashed 1px #333333;
text-indent: 2px;
font-size: small;
}
...
这部分大部分是为了让菜单项看起来更漂亮。例如,虚线底部边框、背景色等。再次使用父子选择器:li a
。因此,样式将仅应用于作为 LI
元素子元素的 A
元素。
唯一重要的部分是 width: 100%;
属性,它告诉 A
元素在宽度方向上填满父元素的所有可用空间。这确保了鼠标悬停效果发生在整个菜单项上,而不仅仅是 A
元素的文本。
此外,text-indent
属性不常用,但当您想确保文本仅从边框缩进(也可以使用 padding
,但在某些情况下不一定有效)时,它非常有用。
...
li a:hover
{
background-color: #ffcc00;
font-weight: bold;
border-bottom: solid 1px #333333;
}
...
这只是定义了 hover
状态(鼠标悬停在元素上时)的样式。有一天(XHTML 2.0),这种功能(以及将 HREF
应用于任何元素!)将支持所有元素,这将非常棒。
...
li
{
float: left;
width: 98%;
}
...
这个看起来很简单,但非常关键。float
可以是您最好的朋友,也可以是您最糟糕的敌人。它的作用是允许元素字面意思上并排浮动,而不会换行。您可以向左和向右浮动。问题在于,当您希望其他元素停止浮动到某个元素旁边时。在这种情况下,您需要使用 clear: both
属性和值,它告诉该元素不允许任何其他元素与它在任何一侧浮动。
width: 98%;
告诉 LI
元素在宽度方向上填满所有可用空间。然而,100% 在 Mozilla 中不幸地会覆盖包含的 UL
的边框样式。98% 可以达到效果,并且在 Internet Explorer 中也很好。
我们快到结尾了。
...
a.hassubmenu
{
background-image: url(../img/lay_dmnuhassub.gif);
background-repeat: no-repeat;
background-position: 120px 7px;
}
...
还记得 JavaScript 中的 className
赋值吗?这就是分配给具有子菜单的 LI
元素的类。
它实际上所做的就是在元素的背景中显示 lay_dmnuhassub.gif 图形。background-repeat: no-repeat;
非常有用,因为您可以控制背景重复的平面(因此得名)。您可以指定 repeat-x
、repeat-y
、repeat
和 no-repeat
作为有效值。
background-position: 120px 7px;
也很有用,它允许您定位背景图形的起始位置。它是一个 x, y 坐标。
可能的改进和 Bug
- 预加载 hassubmenu 箭头图形,以防止每次显示子菜单时都重新加载它。如果有人有快速代码可以做到这一点,请随时分享。
- 尝试修复 Mozilla 中子菜单隐藏的 Bug。我假设这与事件冒泡有关。
- 将重要的 CSS 样式属性和值与纯粹的视觉呈现属性和值分开。这样,可以提供一个“基础” DHTML 菜单,然后应用样式(颜色、边框、背景等),而无需担心破坏菜单。
- 找出如何在不将脚本块移出 head 区域的情况下,从 JS 文件内部将 onload 事件处理程序附加到事件(虽然完全有效,但我对脚本块的位置很挑剔。主要是为了维护和理解)。
- 立即将所有用户浏览器升级到 Internet Explorer 6 或 Mozilla 1.0。;)
结论
好了,各位,这就是全部内容。总而言之,这是一个相当简单且优雅的 DHTML 菜单解决方案。
目前,DHTML 菜单非常适合具有前瞻性思维的网站内容,而对于旧浏览器或公司内部网(其中您可以控制用户使用的浏览器)则没有 DHTML 菜单。在未来,随着上述调整,这种 Web 代码将越来越受欢迎。
总而言之,我所驱动的概念是结构/内容、表现/样式和功能的完全分离。这对 Web 的发展至关重要,并且对于创建高效的开发和维护团队至关重要。没有它,我们将继续面临难以维护且不具备前瞻性兼容性的网站。