用XML和XSLT进行高级的Web UI设计(二)
编译:alpha2002
定制目录树的上下文菜单
本文要求读者熟悉JScript,MSXML,XSL/XSLT,DOM。
这是系列文章的第二篇,主要介绍如何在目录树中创建上下文菜单。本文中所使用的目录树结构就是我们在前面第一篇文章中所创建的目录树结构。在它的基础上引入上下文菜单特性。
在Windows应用程序中,窗口对象的上下文菜单无处不在,只要将鼠标指针移到某个应用程序的窗口对象上,然后单击右键就可以弹出该对象的上下文菜单。对于不同的应用程序,以及同一个应用程序中不同的窗口对象来说,其上下文菜单是不一样的。早期的IE浏览器没有让开发人员创建对象上下文菜单的能力。自从引入的文档对象模型(DOM)之后。使得开发人员在Web应用程序中创建上下文菜单成为可能。
本文将示范如何用XML和XSLT针对特定对象创建定制的上下文菜单,并且菜单的级数是没有限制的。其建立机制与树型目录的建立一样,通过特定的XSL风格页将定义好的目录树XML文件转换成满足要求的HTML推送给客户端浏览器(IE5.5+)显示。客户端负责处理所有对菜单的导航操作。
Windows中的上下文菜单可以适用于任何Web页面对象。本文所创建的菜单是针对我们在上一篇文章中所创建的目录树增加的新UI特性。
在上一篇文章里,我们曾提到过只要在描述目录树结构的XML文件的entity元素中加入一个 "oncontextmenu" 元素,然后在这个元素里请求一个定制的XML上下文菜单描述文件。便可以轻松实现目录树的上下文菜单。菜单的源代码可以从本文的链接下载。

图一 目录树上下文菜单运行例子
描述上下文菜单结构的XML 文件
与描述目录树结构的XML文件类似,这里所选则的上下文菜单格式能很好地适用于XSLT进行递归处理,从而满足和实现层次无限的上下文菜单的需要。
上下文菜单结构的XML文档有一个根元素"menu",此根元素下包含一个元素"entity"。然后在"entity"元素中定义"contents"子元素,整个上下文菜单的结构是通过在每一层entity元素的"contents"子元素中嵌套的entity元素来实现的。下面是"entity"元素中包含的关键所有元素或属性的清单。
名称
类型
描述
id
属性
上下文菜单或菜单选项的id号,唯一标示
description
元素
描述菜单或菜单选项的说明文本
onClick
元素
客户端触发onClick事件的函数名
image
元素
表示菜单选项的图像
contents
元素
包含entity子元素,其内容用以确定嵌套的entity元素是否有上下文子菜单
下面是描述上下文菜单结构的XML文档,当用户在目录树的某个节点单击右键,便会弹出在此节点定义菜单。每一个节点的上下文菜单可以是用不同的XML文件描述。当然,这些菜单可以是动态的,比如从数据库中读出记录进行处理。
<?xml version="1.0" encoding="gb2312"?>
<menu>
<entity id="c1">
<description>添加一期在线杂志</description>
<image>images/add_small.gif</image>
<contents>
<entity id="c2">
<description>一般文章</description>
<image>images/spacer.gif</image>
<contents>
<entity id="c2">
<description>数据库</description>
<image>images/spacer.gif</image>
<contents>
</contents>
</entity>
<entity id="c3">
<description>COM/COM+</description>
<image>images/spacer.gif</image>
<contents>
</contents>
</entity>
</contents>
</entity>
<entity id="c3">
<description>个人专栏</description>
<image>images/spacer.gif</image>
<contents>
<entity id="c2">
<description>wangjun专栏</description>
<image>images/spacer.gif</image>
<contents>
</contents>
</entity>
<entity id="c3">
<description>hangwire专栏</description>
<image>images/spacer.gif</image>
<contents>
</contents>
</entity>
</contents>
</entity>
</contents>
</entity>
</menu>
上面这个XML 文件名为"contextOnlineJnlFolder.xml",可以在下载代码中找到。下面我们来讨论如何在客户端浏览器中显示目录树结构中的上下文菜单。
XSLT 风格页
下面是应用到上下文菜单XML文档的标准的XSLT风格页:
<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dt="urn:schemas-microsoft-com:datatypes">
<xsl:template match="menu">
<div style="position: absolute;">
<div onselectstart="return false" ondragstart="return false">
<xsl:attribute name="STYLE">
position: absolute;
background-color: #6699cc;
border:1px solid #99ccff;
</xsl:attribute>
<table border="0" cellspacing="0" cellpadding="1">
<tr>
<td>
<table border="0" cellspacing="0" cellpadding="0">
<xsl:apply-templates select="entity"/>
</table>
</td>
</tr>
</table>
</div>
<xsl:apply-templates select="entity/contents"/>
</div>
</xsl:template>
<xsl:template match="entity">
<TR>
<xsl:attribute name="selected">false</xsl:attribute>
<xsl:attribute name="background">#6699cc</xsl:attribute>
<xsl:attribute name="light">#99ccff</xsl:attribute>
<xsl:attribute name="titlebar">#5389bc</xsl:attribute>
<xsl:attribute name="image">images/<xsl:value-of select="image"/></xsl:attribute>
<xsl:attribute name="imageOpen">images/<xsl:value-of select="imageOpen"/></xsl:attribute>
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
<xsl:attribute name="ONCLICK"><xsl:value-of select="onClick"/>;clean()</xsl:attribute>
<xsl:attribute name="ONMOUSEOVER">
contextHighlightRow(this);
<xsl:if test="contents/node()[count(child::*)>0]">
loadContextMenuSub(this)
</xsl:if>
</xsl:attribute>
<xsl:attribute name="ONMOUSEOUT">contextHighlightRow(this)</xsl:attribute>
<TD VALIGN="MIDDLE" ALIGN="CENTER" NOWRAP="true">
<xsl:attribute name="ONCLICK"><xsl:value-of select="@onmousedown"/></xsl:attribute>
<xsl:attribute name="STYLE">
background-color: #5389bc;
border-top:1px solid #5389bc;
border-bottom:1px solid #5389bc;
border-left:1px solid #5389bc;
padding-left: 4px;
padding-right: 4px;
padding-top: 4px;
padding-bottom: 3px;
cursor: default;
</xsl:attribute>
<IMG BORDER="0" HEIGHT="15" WIDTH="15">
<xsl:attribute name="SRC"><xsl:value-of select="image"/></xsl:attribute>
</IMG></TD>
<TD NOWRAP="true">
<xsl:attribute name="ONCLICK"><xsl:value-of select="@onmousedown"/></xsl:attribute>
<xsl:attribute name="STYLE">
font-family: Arial;
font-size: 11px;
font-weight: normal;
color: white;
background-color: #6699cc;
border-top: 1px solid #6699cc;
border-bottom: 1px solid #6699cc;
padding-top: 2px;
padding-bottom:2px;
padding-left: 6px;
padding-right: 8px;
cursor: default;
</xsl:attribute>
<xsl:value-of select="description"/></TD>
<TD VALIGN="middle" ALIGN="right" STYLE="padding-right: 6px;" NOWRAP="true">
<xsl:attribute name="ONCLICK"><xsl:value-of select="@onmousedown"/></xsl:attribute>
<xsl:attribute name="STYLE">
background-color: #6699cc;
border-top: 1px solid #6699cc;
border-bottom: 1px solid #6699cc;
border-right: 1px solid #6699cc;
padding-right: 5px;
</xsl:attribute>
<IMG BORDER="0" WIDTH="4">
<xsl:attribute name="SRC">
<xsl:choose>
<xsl:when test="contents/node()[count(child::*)>0]">
images/opensub.gif
</xsl:when>
<xsl:otherwise>
images/spacer.gif
</xsl:otherwise>
</xsl:choose>
</xsl:attribute>
</IMG></TD>
</TR>
</xsl:template>
<xsl:template match="contents">
<xsl:if test="count(child::*)>0">
<div onselectstart="return false" ondragstart="return false">
<xsl:attribute name="STYLE">
position: absolute;
background-color: #6699cc;
border:1px solid #99ccff;
display: none;
</xsl:attribute>
<xsl:attribute name="ID"><xsl:value-of select="../@id"/>Sub</xsl:attribute>
<table border="0" cellspacing="0" cellpadding="1">
<tr>
<td>
<table border="0" cellspacing="0" cellpadding="0">
<xsl:apply-templates select="entity"/>
</table>
</td>
</tr>
</table>
</div>
<xsl:apply-templates select="entity/contents"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

图二 应用XSLT风格页显示的上下文菜单
用户端的操作
为了让目录树的上下文菜单按照我们思路运行,还需要在客户端做一些工作:例如加载菜单、加载子菜单、加亮菜单选项、清除打开的菜单等功能……。归纳起来就是下面这些脚本函数,这些函数的代码都在context.js文件中。
loadContextMenu 加载上下文菜单
loadContextMenuSub 加载上下文子菜单
contextHighlightRow 加亮菜单项
clean 清除打开的菜单项
returnContainer 辅助函数
这些函数的源代码如下:
var appState = new applicationState()
function applicationState() {
this.contextMenu = null
}
function loadContextMenu(path) {
var xmlDoc
var xslDoc
var contextMenu
if(path != "") {
xmlDoc = new ActiveXObject(''''Microsoft.XMLDOM'''')
xmlDoc.async = false;
xslDoc = new ActiveXObject(''''Microsoft.XMLDOM'''')
xslDoc.async = false;
xmlDoc.load(path)
xslDoc.load("context/context.xsl")
if(appState.contextMenu != null) appState.contextMenu.removeNode(true)
document.body.insertAdjacentHTML("beforeEnd", xmlDoc.documentElement.transformNode(xslDoc))
contextMenu = document.body.childNodes(document.body.childNodes.length-1)
contextMenu.style.left = window.event.x
contextMenu.style.top = window.event.y
appState.contextMenu = contextMenu
window.event.cancelBubble = true
}
}
function loadContextMenuSub(obj) {
var contextMenu
var parentMenu
parentMenu = returnContainer(obj)
contextMenu = document.all[obj.id + "Sub"]
contextMenu.style.display = "block"
contextMenu.style.top = obj.offsetTop + parentMenu.style.pixelTop
contextMenu.style.left = obj.offsetWidth + parentMenu.style.pixelLeft
parentMenu.subMenu = contextMenu
}
function contextHighlightRow(obj) {
var parentMenu
var subMenu
var i
parentMenu = returnContainer(obj)
if(obj.selected == "false") {
for(i=0; i < obj.childNodes.length; i++) {
obj.childNodes(i).style.borderTop = "1px solid white"
obj.childNodes(i).style.borderBottom = "1px solid white"
if(obj.childNodes(i).cellIndex == 0) {
obj.childNodes(i).style.borderLeft = "1px solid white"
}
else if (obj.childNodes(i).cellIndex == obj.cells.length-1) {
obj.childNodes(i).style.borderRight = "1px solid white"
}
}
if(parentMenu.subMenu != null && parentMenu != parentMenu.subMenu) {
subMenu = parentMenu.subMenu
while(subMenu != null) {
subMenu.style.display = "none"
subMenu = subMenu.subMenu
}
}
obj.selected = "true"
}
else {
for(i=0; i < obj.childNodes.length; i++) {
if(i == 0) {
obj.childNodes(i).style.borderTop = "1px solid " + obj.titlebar
obj.childNodes(i).style.borderBottom = "1px solid " + obj.titlebar
}
else {
obj.childNodes(i).style.borderTop = "1px solid " + obj.background
obj.childNodes(i).style.borderBottom = "1px solid " + obj.background
}
if(obj.childNodes(i).cellIndex == 0) {
obj.childNodes(i).style.borderLeft = "1px solid " + obj.titlebar
}
else if (obj.childNodes(i).cellIndex == obj.cells.length-1) {
obj.childNodes(i).style.borderRight = "1px solid " + obj.background
}
}
obj.selected = "false"
}
}
function clean() {
var contextMenu
// remove and destroy context menu
if(appState.contextMenu != null) {
contextMenu = appState.contextMenu.removeNode(true)
contextMenu = null
}
}
function returnContainer(container) {
while(container.tagName != "DIV") {
container = container.parentNode
}
return container
}
菜单操作是很多Windows应用程序不可或缺的UI特性。希望通过本文提供的技术能进一步提高你的Web应用程序界面的质量。我们在下一篇文章中将继续上下文菜单的内容,讨论如何真正实现目录树上下文菜单的各种操作,如插入、修改、删除以及更改菜单项的名称等。(待续)