菜单应用是 Web 页面的点睛之笔。当用户在浏览器端右键单击的时候,浏览器会弹出自带的菜单,显示如“查看源代码”、“复制”、“粘贴”等可用菜单栏。通过使用浏览器自带 的菜单,用户可以方便的进行复制、粘贴等操作。然而很多时候,网站开发人员会考虑禁止用户通过浏览器自带的菜单进行以上操作,或者是希望用户使用开发人员 自定义菜单。一个简单的自定义菜单如下图所示:
自定义菜单的使用,可以方便用户快速定位到某个操作,增强了用户界面的交互性,提高用户体验。
Dojo 提供的菜单库,除实现了菜单的基本功能外,还加入对弹出式菜单、图标效果、键盘响应等功能的支持,方便了开发人员的菜单开发过程。本文将首先介绍 Dojo 菜单实现原理,并从创建最简单右键菜单入手,介绍右键菜单的静态和动态两种菜单创建方式,最后举例说明如何开发 Dojo 提供的上下文菜单、下拉式菜单、静态菜单三种菜单。
在默认状况下,用户在浏览器右键单击时,浏览器会触发 document.oncontextmemu 事件,浏览器会采用默认方式对事件进行处理,弹出浏览器自带的右键菜单。
实现自定义右键菜单的基本原理就是:菜单默认为隐藏;当 document.oncontextmemu 事件触发时,使用 JavaScript 操作菜单节点的 style 属性,显示该菜单;同时使用 JavaScript 侦听鼠标 onclick 事件,当该事件执行时,判断鼠标点击位置是否在菜单区域时,若没有,则通过操作菜单的 style 隐藏该菜单。
Dojo 实现右键菜单的方法也是采用了上面的原理,但 Dojo 封装了底层事件的处理方法,开发人员直接使用 Dojo 提供的简单 API 就能实现复杂的菜单。具体实现方式参见下文。
包括右键菜单在内,Dojo 提供了三种类型菜单:上下文菜单(右键菜单和弹出式菜单)、下拉式菜单、静态菜单。由于其他菜单使用和右键菜单使用方式基本相同,本文将从创建一个最简单的右键菜单开始讲解,然后分别介绍上述三种菜单的作用及创建方式。
在 Dojo 支持的上下文菜单、下拉式菜单、静态菜单三类菜单中,使用最为广泛的是“上下文菜单”中的右键菜单,一个最简单的右键菜单如下图所示:
用户在“Please Right-click On Me!”上右键单击,即可看到由 Cut、 Copy、 Paste 纵向三栏构成的右键菜单。可以看到,使用 Dojo 创建的“右键菜单”比较漂亮而且符合用户的使用习惯,下面采用“静态创建”和“动态创建”两种方式实现上述菜单:
与 Dojo 静态创建其他 Widget 类似,如果希望一个实体实现菜单的效果,需要在实体的标签里面加上 dojoType=” dijit.Menu” 属性。
静态创建菜单一般需要如下完整的步骤:
- 导入所需的 JavaScript 和 CSS 文件后,导入 Dojo 所需要的 dijit.Menu 、dijit.MenuItem 等模块。
- 静态创建菜单 Widget 及菜单的各个菜单项 Widget
- 将该菜单 Widget 静态绑定到现有的 DOM 节点。
<html> <head> <title>Menu Learn</title> <style type="text/css"> @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/resources/dojo.css"; @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dijit/themes/tundra/tundra.css" </style> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/dojo.xd.js" djConfig="isDebug:true,parseOnLoad:true"> </script> </head> <body class="tundra"> <script language="JavaScript" type="text/javascript"> dojo.require("dijit.Menu"); dojo.require("dijit.MenuItem"); dojo.require("dojo.parser"); </script> <!-- 创建菜单 Widget --> <div dojoType="dijit.Menu" id="menu_context_1" contextMenuForWindow="false" style="display: none;" targetNodeIds="show_context"> <div dojoType="dijit.MenuItem" >Cut</div> <div dojoType="dijit.MenuItem" >Copy</div> <div dojoType="dijit.MenuItem" >Paste</div> </div> <!-- 菜单 Widget 显示的节点 --> <div id="show_context">Please Right-click On Me!</div> </body> </html> |
dijit.Menu 是 Dojo 中菜单 Widget 的一种,可以理解为是菜单菜单项的容器,一个 dijit.Menu 通常有若干 dijit.MenuItem 组成,每一个 dijit.MenuItem 即为一条菜单项。
dijit.Menu 的 targetNodeIds 属性表示与该 Menu 绑定的目标 DOM 节点,即在该 DOM 节点上右击才会出现右键菜单。contextMenuForWindow 属性表示是否只有在窗体的任何地方右键单击才会打开菜单,如果该值为 true,用户在窗体的任何地方右击都会弹出该菜单,若该值为 false,只有在 targetNodeIds 对应的节点上右击才会弹出菜单。同时,因为右键菜单的在用户右键单击前是不显示的,因此该 Menu Widget 的 style 中 display 属性为 none。
在清单 1 中,通过在一些实体的标签里面加上相应的 Dojo 标签属性实现了 Menu Widget 创建。这种静态实现 Menu Widge 的方法简单明了。然而某些情况下,需要根据一些实际情况动态的生成 Menu Widge,或者动态的修改 Menu Widget 的某些属性。下面代码就是动态实现上述简单右键菜单的方法:
<html> <head> <title>Menu Learn</title> <style type="text/css"> @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/resources/dojo.css"; @import "http://ajax.googleapis.com/ajax/libs/dojo/1.4/dijit/themes/tundra/tundra.css" </style> <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/dojo/1.4/dojo/dojo.xd.js" djConfig="isDebug:true,parseOnLoad:true"> </script> </head> <body class="tundra"> <script language="JavaScript" type="text/javascript"> dojo.require("dijit.Menu"); dojo.require("dijit.MenuItem"); dojo.require("dojo.parser"); dojo.addOnLoad(function(){ <!-- 动态创建菜单 Widget,初始化时设置其属性 --> var menu = new dijit.Menu({targetNodeIds:["show_context"],id:"menu_context_2"}); <!-- 创建菜单项,并添加到菜单中 --> var item1 = new dijit.MenuItem({label:"Cut"}); var item2 = new dijit.MenuItem({label:"Copy"}); var item3 = new dijit.MenuItem({label:"Paste"}); menu.addChild(item1); menu.addChild(item2); menu.addChild(item3); <!-- 调用菜单 --> menu.startup(); } ) </script> <div id="show_context">Please Right-click On Me!</div> </body> </html> |
可以看到,与 Dojo 动态创建普通的 Widget 类似,创建 dojo.menu 的过程也可分为三步:
- 导入所需的 JavaScript 和 CSS 文件后,导入 Dojo 所需要的 dijit.Menu、dijit.MenuItem 等模块。
- 动态创建菜单 Widget,将该菜单 Widget 动态绑定到现有的某个目标 DOM 节点。
- 启动菜单。
需要特别注意的是: 在动态创建 dijit.Menu 的时候,dijit.Menu 的 targetNodeIds 属性是一个对象数组,而非特定的对象。
上下文菜单是最常见的菜单,一般会结合上下文环境使用,该菜单典型的应用是右键菜单和弹出式菜单。上章节设计的菜单即为最简单的右键菜单,而稍微复杂的上下文菜单都会有键盘响应、图标效果显示、自定义快捷键、分隔符、弹出式菜单、禁用菜单项、复选式菜单项等功能:
上图所示:键盘响应指用户可以通过 Dojo 已定义的快捷键对菜单进行操作,如使用“空格键”弹出子菜单;图标效果如上述菜单的“Cut”栏剪刀图标效果所示,而自定义快捷键则如“Cut”栏对应的 “Ctrl + X”快捷键;分隔符的作用如“Paste”栏下面的横线,将不同栏目组分隔开;“Paste”栏底色为灰色,即使点击也不会触发任何时间,即为禁用菜单栏 功能;弹出式菜单则是菜单中最经常用到的功能,用户点击 Popup Menu 时会弹出下一级菜单;而复选菜单的效果则如“Checked”栏所示,用户可以通过点击复选框表示选中该栏或取消选择。
下面的代码实现了上述功能:
<div dojoType="dijit.Menu" id="menu_context_3" contextMenuForWindow="true" style="display: none;"> <!-- 增加图标效果和快捷键 --> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconCut" onClick="console.log('nothing will happen,but you can implement it!')" accelKey="Ctrl+X">Cut</div> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconCopy" onClick="console.log('nothing will happen,but you can implement it!')" accelKey="Ctrl+C">Copy</div> <!-- 禁用该菜单项 --> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconPaste" onClick="console.log('nothing will happen,but you can implement it!')" disabled="true" accelKey="Ctrl+V">Paste</div> <div dojoType="dijit.MenuSeparator"></div> <!-- 弹出子菜单 --> <div dojoType="dijit.PopupMenuItem" id="popupMenuItem"> <span>Popup Menu</span> <div dojoType="dijit.Menu" id="submenu"> <div dojoType="dijit.MenuItem" id="submenu_item1" onClick="console.log('submenu_item1')">Submenu Item 1</div> <div dojoType="dijit.MenuItem" id="submenu_item2" onClick="console.log('submenu_item2')">Submenu Item 2</div> <div dojoType="dijit.PopupMenuItem" id="submenu_item3"> <span>Deeper Popup Menu Item</span> <div dojoType="dijit.Menu" id="submenu_item3submenu"> <div dojoType="dijit.MenuItem" onClick="console.log(' submenu_item3submenu_item1!')">Submenu Submenu Item 1</div> <div dojoType="dijit.MenuItem" onClick="console.log(' submenu_item3submenu_item2!')">Submenu Submenu Item 2</div> </div> </div> </div> </div> <!-- 弹出其他 Widget --> <div dojoType="dijit.PopupMenuItem"> <span>Different Popup</span> <div dojoType="dijit.ColorPalette"></div> </div> <div dojoType="dijit.MenuSeparator"></div> <!-- 复选菜单项 --> <div dojoType="dijit.CheckedMenuItem" checked="true" onChange="if(arguments[0]) console.log('checked'); else console.log('unchecked');"> Checked </div> </div> |
下面就各个功能对上述代码进行讲解:
键盘响应的功能是不需要开发人员实现的,Dojo 创建的 Menu 自身就已经具备了键盘响应的功能,Dojo 提供的键盘响应有:
功能 | 快捷键 |
---|---|
打开上下文菜单 | Windows:shift-f10 或者是在 FireFox 浏览器上右击 Macintosh: ctrl-space Safari 4 或 Mac: VO+shift+m (VO 一般是指 control+opton 组合键 ) |
遍历菜单 | ↑、↓方向键 |
弹出子菜单 | 空格、回车或是→方向键 |
关闭上下文菜单,或关闭当前子菜单返回上级菜单 | Esc 或者←方向键 |
关闭上下文菜单和所有子菜单 | Tab 键 |
dijit.MenuItem 的 iconClass 属性表示了菜单项而使用的 CSS,当菜单项引入该 CSS 后,该菜单项会添加图标效果。Dojo 提供了如 dijitEditorIconCut、dijitEditorIconCopy、dijitEditorIconPaste 等图标效果的 CSS 类。
dijit.MenuItem 的 accelKey 属性表示该菜单项对应的快捷键。需要特别注意的是:尽管菜单项上可以显示该快捷键文本,如上图的第一栏右边显示有“Ctrl+X”,然而当前 Dojo 版本 (1.4) 并没有提供捕捉和执行该快捷键事件的机制,即即使用户键盘输入“Ctrl+X”,也不会触发剪贴事件。
dijit.MenuSeparator 表示菜单菜单项之间的线,用于分割各个菜单项。
如果想使用弹出式菜单,会需要如下的代码结构:
<div dojoType="dijit.PopupMenuItem" id="popupMenuItem"> <span>Popup Menu</span> <div dojoType="dijit.Menu" id="submenu"> <div dojoType="dijit.MenuItem" id="submenu_item1"> Submenu Item 1 </div> ... </div> |
其中,PopupMenuItem 作用类似于 MenuItem,但是它可以显示下一级菜单或者其他 Widget。一般 PopupMenuItem 都会有两个子节点:显示该菜单项内容的静态文本的标签(一般是写在 span 里)和一个需要显示的 Widget,该 Widget 一般是 dijit.Menu,也可以是 dijit.ColorPalette(颜色选择框)等 Widget。
dijit.MenuItem 的 disabled 属性表示该菜单项是否可用,该属性默认值为“flase”,表示可用;如果该属性为 true,则该菜单项被禁用,即使点击该菜单项也不会触发点击事件。
dijit.CheckedMenuItem 表示复选菜单项,其 checked 属性标识了该菜单项是否被选中。checked 属性的默认值为 false,即未被选中,每次用户点击该菜单项,就会触发选中 / 取消选中的事件,菜单项状态就会在“checked”和“unchecked”之间进行切换。用户可以定义 onchange 函数,用于处理选中 / 取消选中该菜单项事件,onchange 函数接受的第一个参数即为 checked 属性的值。
需要说明的是,以上功能并非只有在“上下文菜单”中才有,“下拉式菜单”和“静态菜单栏”都具备相同的功能,使用的方法也一样。
下拉式菜单指的是点击某个按钮或者菜单项时,会纵向下拉弹出的菜单。Dojo 提供的下拉式菜单一般会绑定到 dijit.form.ComboButton,dijit.form.DropDownButton 或 dijit.MenuBar Widget 上,点击这些 Widget 或 Widget 的菜单项时,会弹出下拉式菜单。以 dijit.MenuBar 为例:MenuBar Widget 是经常用到的 Widget,它模拟实现了一个典型的菜单条,横向列出若干菜单选项,当点击某个菜单项时,会下拉弹出子菜单或其他 Widget。如下图所示:
上述功能可由清单 5 实现:
<!-- 下拉式菜单,即菜单条 --> <div id="menubar" dojoType="dijit.MenuBar"> <!-- 菜单条的菜单项 --> <div dojoType="dijit.PopupMenuBarItem" id="file"> <span>File</span> <!-- 菜单项下拉弹出子菜单 --> <div dojoType="dijit.Menu" id="fileMenu"> <div dojoType="dijit.MenuItem" id="new">New</div> <div dojoType="dijit.MenuItem" id="open">Open</div> <div dojoType="dijit.MenuSeparator" id="separator"></div> <div dojoType="dijit.MenuItem" id="save" iconClass="dijitEditorIconSave">Save</div> <div dojoType="dijit.PopupMenuItem" id="saveas"> <span>Save as</span> <div dojoType="dijit.Menu" id="subMenu"> <div dojoType="dijit.MenuItem">*.txt</div> <div dojoType="dijit.MenuItem">*.doc</div> </div> </div> </div> </div> <div dojoType="dijit.PopupMenuBarItem" id="edit"> <span>Edit</span> <div dojoType="dijit.Menu" id="editMenu"> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconCut" onClick="console.log('nothing will happen,but you can implement it!')" accelKey="Ctrl+X">Cut</div> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconCopy" onClick="console.log('nothing will happen,but you can implement it!')" accelKey="Ctrl+C">Copy</div> <div dojoType="dijit.MenuItem" iconClass="dijitEditorIcon dijitEditorIconPaste" onClick="console.log('nothing will happen,but you can implement it!')" disabled="true" accelKey="Ctrl+V">Paste</div> </div> </div> <div dojoType="dijit.PopupMenuBarItem" disabled="true"> <span>Disabled</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">If you see this,there is something wrong!</div> </div> </div> <!-- 不会执行 onclick 事件 --> <div dojoType="dijit.PopupMenuBarItem" onclick="console.log('no submenu,menu donot has any item,handle onclick event?');"> <span>Empty</span> <div dojoType="dijit.Menu"> </div> </div> <!-- 会执行 onclick 事件 --> <div dojoType="dijit.MenuBarItem" onclick="console.log('no submenu,I am MenuBarItem,handle onclick event?'); "> Please! </div> </div> |
一个 dijit.MenuBar Widget 由多个 dijit.PopupMenuBarItem 或 dijit.MenuBarItem Widget 组成:
- 示例的“File”菜单项就是由 dijit.PopupMenuBarItem Widget 实现的,当鼠标点击该菜单项时,菜单项会弹出一个子菜单或其他 Widget。同上下文菜单的 dijit.PopupMenuItem 类似,一个 dijit.PopupMenuBarItem Widget 会包含两个子节点:显示静态文本的标签(一般是写在 span 里)和一个需要显示的 Widget,该 Widget 一般是 digit.Menu Widget。
- dijit.MenuBarItem 也是菜单条的菜单项,它不支持下拉弹出 digit.Menu 或其他 Widget。与 dijit.PopupMenuBarItem 不同,当在该 dijit.MenuBarItem Widget 单击时,会触发 onclick 函数,这点可以通过“Empty”和“Please!”菜单项得到验证:
当点击“Empty”和“Please!”菜单项时,都不会弹出下拉菜单,两者显示效果看起来一样,但实际触发的事件却不同,观察 firebug 控制台,可以发现:“Empty”菜单栏被单击后,并没有向控制台进行输出,即并没有真正执行 onclick 函数;而“Please!”菜单栏被单击后,则向控制台进行输出。
静态菜单是静态定为到窗体某个位置的菜单,如下图所示:
静态菜单与上下文菜单的显示效果是一样的,然而,静态菜单会在网页加载完后固定显示于窗体某个位置,并且不会像上下文菜单一样会因鼠标事件的发生而消失或显示,它的典型应用为作为导航菜单显示在窗体的左侧,用户可以根据其菜单项进行信息的过滤和查找。
实现上述功能菜单的代码为:
<!-- 静态菜单,不需要绑定 DOM 节点,加载时直接显示在页面上 --> <div dojoType="dijit.Menu" id="navMenu"> <!-- 其他用法与上下文菜单相同 --> <div dojoType="dijit.PopupMenuItem"> <span>Africa</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">Egypt</div> <div dojoType="dijit.MenuItem">Kenya</div> <div dojoType="dijit.MenuItem">Sudan</div> </div> </div> <div dojoType="dijit.PopupMenuItem"> <span>Asia</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">China</div> <div dojoType="dijit.MenuItem">India</div> <div dojoType="dijit.MenuItem">Russia</div> <div dojoType="dijit.MenuItem">Mongolia</div> </div> </div> <div dojoType="dijit.PopupMenuItem"> <span>Europe</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">Germany</div> <div dojoType="dijit.MenuItem">France</div> <div dojoType="dijit.MenuItem">Spain</div> <div dojoType="dijit.MenuItem">Italy</div> </div> </div> <div dojoType="dijit.PopupMenuItem"> <span>North America</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">Mexico</div> <div dojoType="dijit.MenuItem">Canada</div> <div dojoType="dijit.MenuItem">United States of America</div> </div> </div> <div dojoType="dijit.PopupMenuItem"> <span>South America</span> <div dojoType="dijit.Menu"> <div dojoType="dijit.MenuItem">Brazil</div> <div dojoType="dijit.MenuItem">Argentina</div> </div> </div> <div dojoType="dijit.MenuSeparator"></div> <div dojoType="dijit.PopupMenuItem"> <span>Different Popup</span> <div dojoType="dijit.ColorPalette"></div> </div> <div dojoType="dijit.MenuSeparator"></div> <div dojoType="dijit.CheckedMenuItem" checked="true" onChange="if(arguments[0]) console.log('checked'); else console.log('unchecked');">Checked</div> |
可以看出,静态菜单的实现与上下文菜单的代码几乎是一样的,有区别的地方在于:
- 静态菜单的 style 中 display 取的是默认值 inline,因为它要求页面加载完就显示给用户。
- 静态菜单的 contextMenuForWindow 取的是默认值 false,避免用户在浏览器任意位置右键单击后重新加载该菜单。
- 静态菜单不需要绑定 targetNodeIds,不会因用户在特定节点右键单击后浏览器重新加载该菜单。
综上所述,Dojo 菜单库封装了底层 JavaScript 对右键单击事件的响应,提供了简洁的开发菜单的方法,开发人员使用 Dojo 可以快速开发出一个用户界面良好的菜单。
Dojo 提供 了上下文菜单(即右键菜单和弹出式菜单)、下拉式菜单、静态菜单三种菜单形式,静态、动态两种菜单创建形式,并支持图标效果显示、自定义快捷键、分隔符、 弹出式菜单、禁用菜单项、复选菜单项等多种功能,使得 Web 开发的菜单可以与传统桌面软件菜单相媲美,提高了用户体验。