Navigation Tree 1.1 实现详解
Navigation Tree 1.1是一个仿照http://msdn.microsoft.com/library/ 左侧目录树(如图
所示,下称MSDN Library目录树)实现的居于XML的、客户端驱动的目录树组件。Navigation Tree 1.0 写于2002年12月,当初的目的主要是为了获得与MSDN Library目录树相同的效果。1.1的更新版本完成于2003年1月20日,更新后的Navigation Tree 1.1功能支持动态加载目录结点,也修正了1.0里面一些按键事件的响应错误。Navigation Tree 1.1的表现MSDN Library目录树更加一致。
Navigation Tree分为四个部分:结点描述文件(XML)、格式转换文件(XSL)、JavaScript文件和CSS样式表。四部分的具体实现如下:
结点描述文件(XML):
描述树结点信息和结点之间的关系。
NavigationTree.xml
<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet type="text/xsl" href="NavigationTree.xsl" ?>
<Tree>
<TreeNode Title="Node1" Href="http://www.china.com">
<TreeNode Title="Node11" Href="http://msdn.microsoft.com" />
<TreeNode Title="Node12" Href="about:blank" Target="_blank">
<TreeNode Title="Node121" Href="mailto:s_yzzhou@stu.edu.cn" />
<TreeNode Title="Node122" Href="javascript:alert('Hello,World');" Target="_self" />
<TreeNode Title="Node123" Href="http://ebook.stu.edu.cn" />
</TreeNode>
</TreeNode>
<TreeNode Title="Node2" Target="_blank">
<TreeNode Title="Node21" Target="_self" />
<TreeNode Title="Node22" Target="newFrame" />
</TreeNode>
<TreeNode Title="Node3" NodeSrc="NavigationTree.xml" />
<TreeNode Title="Node4" Href="http://ebook.stu.edu.cn" Target="_blank" />
<TreeNode Title="Node5" />
</Tree>
格式转换文件(XSL):
用于格式化结点描述文件。
NavigationTree.xsl
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/Tree">
<html>
<head>
<title>NavigationTree</title>
<link rel="stylesheet" type="text/css" href="NavigationTree.css" />
<script language="javascript" src="NavigationTree.js" />
</head>
<body onselectstart="OnBodySelectStart();" onkeydown="OnBodyKeyDown();" topmargin="0" leftmargin="0" marginheight="0" marginwidth="0" bgcolor="#f1f1f1" text="#000000">
<nobr>
<div class="NavigationTree" type="root">
<div type="nodecollection">
<xsl:apply-templates select="TreeNode" />
</div>
</div>
</nobr>
</body>
</html>
</xsl:template>
<xsl:template match="TreeNode">
<div type="node">
<!--Image-->
<span class="clsSpace" type="img">
<xsl:choose>
<xsl:when test="TreeNode or @NodeSrc">
<xsl:attribute name="onclick">OnImgClick(this)</xsl:attribute>
<xsl:attribute name="style">cursor:hand</xsl:attribute>
<span class="clsCollapse">+</span>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="style">cursor:default</xsl:attribute>
<span class="clsLeaf">.</span>
</xsl:otherwise>
</xsl:choose>
</span>
<!--Label-->
<span class="clsLabel" type="label" onclick="OnItemClick(this)" onmouseover="OnItemMouseOver(this)" onmouseout="OnItemMouseOut(this)" onmousedown="OnItemMouseDown(this)" onmouseup="OnItemMouseUp(this)">
<xsl:attribute name="id">
<xsl:value-of select="@NodeId" />
</xsl:attribute>
<xsl:attribute name="title">
<xsl:value-of select="@Title" />
</xsl:attribute>
<xsl:choose>
<xsl:when test="@Href">
<xsl:attribute name="style">cursor:hand;</xsl:attribute>
<a>
<xsl:choose>
<xsl:when test="@Target">
<xsl:attribute name="target">
<xsl:value-of select="@Target" />
</xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="target">fraContent</xsl:attribute> <!--Change the target frame name to appropriate value-->
</xsl:otherwise>
</xsl:choose>
<xsl:attribute name="tabindex">-1</xsl:attribute>
<xsl:attribute name="href">
<xsl:value-of select="@Href" />
</xsl:attribute>
<xsl:value-of select="@Title" />
</a>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="style">cursor:default;</xsl:attribute>
<xsl:value-of select="@Title" />
</xsl:otherwise>
</xsl:choose>
</span>
<!--Child nodes-->
<xsl:if test="TreeNode or @NodeSrc">
<div class="hidden" type="nodecollection">
<xsl:choose>
<xsl:when test="@NodeSrc">
<xsl:attribute name="nodesrc">
<xsl:value-of select="@NodeSrc" />
</xsl:attribute>
<div type="node">
<span class="clsSpace" type="img" style="cursor:default">
<span class="clsLeaf">.</span>
</span>
<span class="clsUnavailable">Loading...</span> <!--Change the suggestive string to your language-->
</div>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="TreeNode" />
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:if>
</div>
</xsl:template>
</xsl:stylesheet>
JavaScript文件:
对文档事件进行处理。
NavigationTree.js
// Modify templatePath to the specified xsl file as you need
var templatePath="NavigationTree.xsl";
// Suggestive string for loading failure, Modify it to your language
var failureMsg="Failed to load child nodes.";
// Default window status message, Change it to your language
var defaultStatusMsg="Navigation Tree 1.1 -- [Kerry Chou]";
// Global variant for storing last clicked node item
var lastClickedNode=null;
//////////////////////////////////////////////////
/*
Beginning of event handlers
*/
// OnImgClick(obj)
// Handler of onclick event on img portion of a node
function OnImgClick(obj)
{
var node=GetNode(obj);
if(node){
if(!HasChildren(node)){
var label=GetNodeLabel(node);
if(label){
OnItemMouseDown(label);
OnItemClick(label);
OnItemMouseUp(label);
}
return;
}
if(IsExpanded(node)){
CollapseNode(node);
}else{
ExpandNode(node);
}
}
}
// OnItemClick(obj)
// Handler of onclick event on label portion of a node
function OnItemClick(obj)
{
var node=GetNode(obj);
var a=GetElement(obj,"A",null);
if(window.event.srcElement.tagName.toUpperCase()=="SPAN"){
NavigateNode(node);
}else{
document.body.focus();
}
if(node){
if(HasChildren(node)){
if(IsExpanded(node)){
CollapseNode(node);
}else{
ExpandNode(node);
}
}
}
}
// OnItemMouseDown(obj)
// Handler of onmousedown event
function OnItemMouseDown(obj)
{
if(obj){
obj.className="clsMouseDown";
}
if(lastClickedNode&&GetNodeLabel(lastClickedNode)!=obj){
GetNodeLabel(lastClickedNode).className="clsCurrentNoFocus";
}
}
// OnItemMouseUp(obj)
// Handler of onmouseup event
function OnItemMouseUp(obj)
{
if(GetNodeLabel(lastClickedNode)!=obj){
if(lastClickedNode){
GetNodeLabel(lastClickedNode).className="";
}
obj.className="clsCurrentHasFocus";
lastClickedNode=GetNode(obj);
}else{
obj.className="clsCurrentHasFocus";
}
}
// OnItemMouseOver(obj)
// Handler of onmouseover event
function OnItemMouseOver(obj)
{
if(obj&&obj!=GetNodeLabel(lastClickedNode)){
obj.className="clsMouseOver";
}
var a=GetElement(obj,"A",null);
if(a){
window.status=a.href;
}
}
// OnItemMouseOut(obj)
// Handler of onmouseout event
function OnItemMouseOut(obj)
{
if(obj&&obj!=GetNodeLabel(lastClickedNode)){
obj.className="";
}
window.status=defaultStatusMsg;
}
// OnBodyKeyDown()
// Handler of keydown event
function OnBodyKeyDown()
{
var blnRetVal = false
var objLI;
if (window.event.ctrlKey == false && window.event.altKey == false)
{
window.event.cancelBubble = true;
window.event.returnValue = false;
switch (window.event.keyCode)
{
case 9 : // tab key
if (window.event.shiftKey == true)
{
MovePrevious();
}
else
{
MoveNext();
}
break;
case 13 : // enter key
objLI = window.event.srcElement.parentElement;
if(IsExpanded(lastClickedNode)){
CollapseNode(lastClickedNode);
}else{
ExpandNode(lastClickedNode);
}
break;
case 37 : // left key
MoveLeft()
break;
case 38 : // up key
MoveUp()
break;
case 39 : // right key
MoveRight()
break;
case 40 : // down key
MoveDown()
break;
case 188 : // "<" key
MovePrevious();
break;
case 190 : // ">" key
MoveNext();
break;
default :
window.event.cancelBubble = false;
window.event.returnValue = true;
blnRetVal = true;
break;
}
}
else
{
window.event.cancelBubble = false;
window.event.returnValue = true;
blnRetVal = true;
}
return blnRetVal;
}
// Handler of onselectstart event
// Disable selection
function OnBodySelectStart()
{
window.event.cancelBubble = true;
window.event.returnValue = false;
return false;
}
/*
End of event handlers
*/
////////////////////////////////////////////////////////
// Retrieve the parent document object with specified tag name.
function GetParentObject(obj,tag)
{
if(obj){
obj=obj.parentElement;
while(obj&&obj.tagName.toUpperCase()!=tag.toUpperCase()){
obj=obj.parentElement;
}
return obj;
}
return null;
}
// Retrieve the parent document object with specified tag name and type.
function GetParentElement(obj,tag,type)
{
var parent=GetParentObject(obj,tag);
while(parent&&!CheckItemType(parent,type)){
parent=GetParentObject(parent,tag);
}
return parent;
}
// Retrieve the child document object with specified tag name and type.
function GetElement(parentObj,tag,type)
{
if(type==null){
type="";
}
if(parentObj!=null){
var children=parentObj.children;
if(children!=null){
for(var i=0;i<children.length;++i){
if(type==""){
if(CheckItemType(children(i),type)){
return children(i);
}
}else{
if(children(i).tagName.toUpperCase()==tag &&
CheckItemType(children(i),type)){
return children(i);
}
}
}
}
}
return null;
}
// Retrieve object's type if it has
function GetItemType(obj)
{
if(obj){
if(obj.type){
return obj.type;
}
}
return "";
}
// Verify object's type
function CheckItemType(obj,type)
{
if(type==null){
type="";
}
return GetItemType(obj).toUpperCase()==type.toUpperCase();
}
// Retrieve node item with a specified object within the node
function GetNode(obj)
{
if(obj){
if(CheckItemType(obj,"NODE")){
return obj;
}
if(CheckItemType(obj,"LABEL")||CheckItemType(obj,"IMG")){
while(obj&&CheckItemType(obj,"NODE")==false){
obj=GetParentObject(obj,"DIV");
}
return obj;
}
}
return null;
}
// Retrieve children container
function GetChildrenContainer(node)
{
node=GetNode(node);
if(node){
var parent=GetElement(node,"DIV","nodecollection");
return parent;
}
return null;
}
// Retrieve container in which the specified node resides in
function GetParentContainer(node)
{
node=GetNode(node);
if(node){
var container=GetParentElement(node,"DIV","nodecollection");
return container;
}
return null;
}
// Retrieve the label of the specified node
function GetNodeLabel(node)
{
var node=GetNode(node);
if(node){
return GetElement(node,"SPAN","LABEL");
}
return null;
}
// Retrieve the img of the specified node
function GetNodeImg(node)
{
var node=GetNode(node);
if(node){
return GetElement(node,"SPAN","IMG");
}
return null;
}
// Retrieve the parent node
function GetParentNode(node)
{
node=GetNode(node);
if(node){
var parent=GetParentElement(node,"DIV","nodecollection");//nodecollection
if(parent){
parent=GetParentElement(parent,"DIV","node");//parent's node
return parent;
}
}
return null;
}
// Retrieve first child node
function GetChildNode(node)
{
node=GetNode(node);
if(node){
var container=GetChildrenContainer(node);
if(container){
return GetElement(container,"DIV","node");
}
}
return null;
}
// Retrieve last child node
function GetLastChildNode(node)
{
node=GetNode(node);
if(node){
var container=GetChildrenContainer(node);
var children=container.children;
if(children.length>0){
return children(children.length-1);
}
}
return null;
}
// Retrieve next sibling node
function GetNextSibling(node)
{
node=GetNode(node);
if(node){
var container=GetParentContainer(node);
if(container){
var children=container.children;
if(children&&children.length>0){
var foundCurrent=false;
for(var i=0;i<children.length;++i){
if(foundCurrent){
return children(i);
}
if(children(i)==node){
foundCurrent=true;
}
}
}
}
}
return null;
}
// Retrieve previous sibling node
function GetPreviousSibling(node)
{
node=GetNode(node);
if(node){
var container=GetParentContainer(node);
if(container){
var children=container.children;
if(children&&children.length>0){
var previous=null;
for(var i=0;i<children.length;++i){
if(children(i)==node){
return previous;
}
previous=children(i);
}
}
}
}
return null;
}
// Expand the specified node if it has children
function ExpandNode(node)
{
node=GetNode(node);
if(node&&HasChildren(node)){
var img=GetNodeImg(node);
if(img){
img=GetElement(img,"SPAN","");
if(img&&img.className.toUpperCase()!="CLSLEAF"){
img.innerText="-";
img.className="clsExpand";
}
}
var container=GetChildrenContainer(node);
if(container){
container.className="shown";
}
if(container.nodesrc!=null){
try{
var source = new ActiveXObject("Msxml2.DOMDocument");
source.async = false;
source.resolveExternals = false;
source.load(container.nodesrc);
// Load style sheet.
var stylesheet = new ActiveXObject("Msxml2.DOMDocument");
stylesheet.async = false;
stylesheet.resolveExternals = false;
stylesheet.load(templatePath);
var nodes=stylesheet.selectNodes("//xsl:template");
for(var i=0;i<nodes.length;++i){
if(nodes(i).getAttribute("match")=="TreeNode"){
// "TreeNode" template found
}else{
// delete the other templates
stylesheet.documentElement.removeChild(nodes(i));
}
}
// Fill a div tag with the result of the transform
container.innerHTML = source.transformNode(stylesheet);
container.nodesrc=null;
}catch(err){
container.innerText=failureMsg;
return;
}
}
}
}
// Collapse the specifed node if it has children
function CollapseNode(node)
{
node=GetNode(node);
if(node&&HasChildren(node)){
var img=GetNodeImg(node);
if(img){
img=GetElement(img,"SPAN","");
if(img&&img.className.toUpperCase()!="CLSLEAF"){
img.innerText="+";
img.className="clsCollapse";
}
}
var container=GetChildrenContainer(node);
if(container){
container.className="hidden";
}
}
}
// Check whether a specified node has children nodes
function HasChildren(node)
{
node=GetNode(node);
if(node){
var container=GetChildrenContainer(node);
return container!=null;
}
return false;
}
// Check whether a specified node is expanded
function IsExpanded(node)
{
node=GetNode(node);
if(node){
var container=GetChildrenContainer(node);
if(container){
if(container.className.toUpperCase()=="HIDDEN"){
return false;
}
if(container.className.toUpperCase()=="SHOWN"){
return true;
}
}
}
return false;
}
// Check wether a specified node is collapsed
function IsCollapsed(node)
{
node=GetNode(node);
if(node){
var container=GetChildrenContainer(node);
if(container){
if(container.className.toUpperCase()=="HIDDEN"){
return true;
}
if(container.className.toUpperCase()=="SHOWN"){
return false;
}
}
}
return false;
}
// Open resource associated with a node
function NavigateNode(node)
{
if(node){
var label=GetNodeLabel(node);
if(label){
var a=GetElement(label,"A","");
if(a){
window.open(a.href,a.target);
}
}
}
}
// Select specifed node
function SelectNode(node)
{
node=GetNode(node);
if(node!=lastClickedNode&&lastClickedNode){
OnItemMouseDown(GetNodeLabel(node));
OnItemMouseUp(GetNodeLabel(node));
}
NavigateNode(node);
}
// Move to previous sibling node when '<' key is pressed or 'shift+tab' pressed
function MovePrevious()
{
if(lastClickedNode){
var previous=GetPreviousSibling(lastClickedNode);
if(previous){
SelectNode(previous);
}else{
var parent=GetParentNode(lastClickedNode)
if(parent){
SelectNode(parent);
}
}
}
}
// Move to first child node or next visible node when '>' key is pressed or 'tab' pressed
function MoveNext()
{
if(lastClickedNode){
var b=HasChildren(lastClickedNode);
if(b){
ExpandNode(lastClickedNode);
var child=GetChildNode(lastClickedNode)
SelectNode(child);
}else{
var next=GetNextSibling(lastClickedNode);
if(next){
SelectNode(next);
}else{
var parent=GetParentNode(lastClickedNode);
while(parent){
next=GetNextSibling(parent);
if(next){
SelectNode(next);
break;
}
parent=GetParentNode(parent);
}
}
}
}
}
// Move to parent node
function MoveLeft()
{
if(lastClickedNode){
if(IsExpanded(lastClickedNode)){
CollapseNode(lastClickedNode);
}else{
var parent=GetParentNode(lastClickedNode);
if(parent){
SelectNode(parent);
}
}
}
}
// Move to first child node
function MoveRight()
{
if(lastClickedNode){
if(IsCollapsed(lastClickedNode)){
ExpandNode(lastClickedNode);
}else{
var child=GetChildNode(lastClickedNode);
if(child){
SelectNode(child);
}
}
}
}
// Move to last visible node
function MoveUp()
{
if(lastClickedNode){
var previous=GetPreviousSibling(lastClickedNode);
if(previous){
while(HasChildren(previous)&&IsExpanded(previous)){
previous=GetLastChildNode(previous);
}
if(previous){
SelectNode(previous);
}
}else{
var parent=GetParentNode(lastClickedNode);
if(parent){
SelectNode(parent);
}
}
}
}
// Move to next visible node
function MoveDown()
{
if(lastClickedNode){
if(HasChildren(lastClickedNode)&&IsExpanded(lastClickedNode)){
var child=GetChildNode(lastClickedNode);
if(child){
SelectNode(child);
}
}else{
var next=GetNextSibling(lastClickedNode);
if(next){
SelectNode(next);
}else{
var parent=GetParentNode(lastClickedNode);
while(parent){
next=GetNextSibling(parent);
if(next){
SelectNode(next);
break;
}
parent=GetParentNode(parent);
}
}
}
}
}
///////////////////////////////////////////
///////////////////////////////////////////
CSS样式表:
此样式表即为MSDN Library目录树的样式表。
BODY
{
font-family: Verdana;
cursor: default;
font-size: 9pt;
}
tbody
{
font-size: 9pt;
}
.NavigationTree
{
padding: 4px 5px;
}
.Navigation IMG
{
position: relative;
cursor: hand;
top: -2px;
margin: 0px;
padding: 0px;
}
.NavigationTree A
{
color: black;
text-decoration: none;
}
span
{
padding: 2px 3px;
position: relative;
display: inline;
top: -2px;
height: 17px;
border: solid 1px #f1f1f1;
}
span.clsLabel
{
}
SPAN.clsSpace
{
font-family: verdana;
position: relative;
padding: 3px 2px;
top: 0px;
width: 17px;
margin: 0px;
cursor: hand;
overflow: hidden;
}
span.clsSpace span
{
position: relative;
width: 11px;
height: 11px;
border: solid 1px black;
background-color: #ffffff;
}
SPAN.clsCollapse
{
line-height: 6px;
font-size: 9px;
overflow: hidden;
padding: 1px;
}
SPAN.clsExpand
{
padding-left: 1px;
overflow: hidden;
line-height: 3px;
font-size: 13px;
padding-top: 3px;
}
SPAN.clsLeaf
{
overflow: visible;
font-size: 9px;
line-height: 3px;
padding: 1px 0px 0px 3px;
}
SPAN.clsMouseOver
{
background-color: #CCCCCC;
border: 1px solid #999999;
}
SPAN.clsMouseDown
{
background-color: #999999;
border: 1px solid #999999;
}
SPAN.clsCurrentHasFocus
{
background-color: #FFFFFF;
border: 1px solid #999999;
}
SPAN.clsCurrentNoFocus
{
background-color: #F1F1F1;
border: 1px solid #999999;
}
A
{
color: black;
text-decoration: none;
}
span.clsUnavailable
{
height: 0px;
border: none;
color: #888888;
}
.hidden
{
display: none;
}
.shown
{
display: block;
margin-left: 15px;
}
NavigationTree 1.1的运行结果如下图所示:
NavigationTree 1.1运行环境:
由于客户端脚本里使用到了MSXML2,所以浏览器环境必须为IE5.01或以上版本。而其他浏览器版本的兼容性问题可以使用服务器端脚本来解决。一下ASP.NET代码判断客户端浏览器版本:
<script language="C#" runat="server">
string treeurl="NavigationTree.xml";
private void Page_Load(object sender, System.EventArgs e)
{
if(Request.Browser.MajorVersion<=5&&Request.Browser.MinorVersion<0.01){
treeurl=treeurl.Replace("xml","aspx");
}
Response.Redirect(treeurl);
}
</script>
Bug Reporting:
S_yzzhou@stu.edu.cn