JavaScript 多级联动浮动菜单

请到这里看09-08-18更新版本

类似的多级浮动菜单网上也很多实例,但大部分都是只针对一种情况或不够灵活,简单说就是做死了的。
所以我就想到做一个能够自定义菜单的,有更多功能的多级浮动菜单。
而其中的关键就是怎么根据自定义的菜单结构来生成新菜单,
关键中的难点就是怎么得到下级菜单结构和容器对象的使用。
理想的做法是每次有下级菜单时,从对象直接取得下级菜单结构,放到容器对象中,
并且容器能重用,而不是每次都重新生成。
但想了很久也想不到适合的做法,直到做了多级联动下拉菜单终于得到了灵感。
放弃了直接取得下级菜单结构,而是每次都从原来的菜单结构中获取当前需要的下级菜单结构。
容器对象也不是自动生成,而是由用户先定义好(后来也做到能自动生成了)。
放下了这些包袱后,后面的开发就顺畅了。


特点:
1.根据自定义菜单结构生成菜单;
2.多级联动功能;
3.自定义浮动位置(上下左右);
4.自定义延迟效果;
5.js控制编辑菜单;
6.可根据需要自动生产容器对象;

效果:












程序原理

程序是根据传统浮动菜单扩展而来,这里说一下几个比较关键或有用的地方:


【延时功能】

这个很多人都懂,就是设个setTimeout计时器,这里有两个计时器,分别是容器计时器和菜单计时器。
容器计时器的作用是鼠标移到容器外面时隐藏容器,难点是如何判断当前鼠标是不是在容器外面。
一般的方法是设个bool参数,mouseout时设为false,mouseover时设为true(or倒过来),再根据这个参数判断,
但这个方法在这个不行,经过容器里的菜单对象时会触发容器mouseout,
由于事件冒泡,菜单对象的mouseout也会触发容器的mouseout。
例如:

Code
<div style="height:100px; width:100px; background:#000000;" onmouseout="alert(2)">
<div style="height:50px; width:50px; background:#FF0000;" onmouseout="alert(1)">
</div>
</div>

这里推荐一个方法,使用contains(ff是compareDocumentPosition)方法。
这个方法是我做图片滑动展示效果时muxrwc教我的:

var  isIn  =   false , oT  =  Event(e).relatedTarget;
Each(oThis.Container, 
function (o, i){  if (o.contains  ?  o.contains(oT)  ||  o  ==  oT : o.compareDocumentPosition(oT)  &   16 ){ isIn  =   true ; } });

 

详细参考仿LightBox内容显示效果,而菜单计时器就没什么特别,就是用来设置菜单内容。


【浮动位置】

除了母菜单的容器是固定的,子菜单的容器都是绝对定位的,定位的关键就是取得适合的left和top值。
首先要取得上一级菜单的left和top值。
由于母菜单是相对定位的,要取它的绝对left和top值就必须逐层向上取值,并加起来:

while  (o.offsetParent) { o  =  o.offsetParent; iLeft  +=  o.offsetLeft; iTop  +=  o.offsetTop; }

取得上一级菜单的left和top值后,再进行相应的移位就可以了:

Code
switch (position.toLowerCase()) {
    
case "up" :
        iTop 
-= oContainer.offsetHeight;
        
break;
    
case "down" :
        iTop 
+= o.offsetHeight;
        
break;
    
case "left" :
        iLeft 
-= oContainer.offsetWidth;
        
break;
    
case "right" :
    
default :
        iLeft 
+= o.offsetWidth;
}

 

这里要注意,如果display为none的话会取不到offset值,
所以为了在隐藏的状态也能定位,就要用visibility来隐藏。
当然如果display可以先显示再定位,但这样会出现瞬间移动的现象,不建议。


【自动生成容器对象】

除了第一个容器对象,当发现容器不够时,会根据前一个容器来生成新容器。
开始时我想用cloneNode,但由于对象中有事件所以不能这样用,只能手动建一个。

首先根据前一个容器的tagName创建一个新容器,
为了使用相同的样式,复制cssText(这个也是muxrwc告诉我的)和className到新容器,
然后用IniContainer()函数设置一下就可以了:
Code
var oPre = this.Container[i-1], oNew = document.body.appendChild(document.createElement(oPre.tagName));
oNew.style.cssText 
= oPre.style.cssText; oNew.className = oPre.className;
this.IniContainer(this.Container[i] = oNew, true);

【多级联动】

联动的关键是如何得到子菜单结构和根据这个子菜单结构生成菜单对象。
先说说菜单结构,是类似这样的结构:
Code
[
    {
'txt''1' },
    {
'txt''2''position''down''menu': [
        {
'txt''2_1'},
        {
'txt''2_2'}
    ]}
]

 

知道json的应该都知道是什么了,js的一种对象结构:
txt是显示的内容,也可以是html,到时会innerHTML插入;
position是位置,可以是"right"(默认),"down","up","left",浮动位置会根据这个值来设置;
menu是下一级的菜单结构。
可以看出这类似一个n维数组,注意是类似。

那怎么根据这个菜单结构获得当前菜单的子菜单呢?
首先从菜单对象的onmouseover说起,
在菜单a的onmouseover中,要做的是重新设置菜单和重新设置样式(这个稍后再说)。
设置菜单还包括设置一个索引属性index来记录当前容器菜单的索引(容器第几个菜单),
这里有点取巧的是容器菜单的索引跟对应菜单结构中menu的索引是相同的(后面会用到),
而_index是当前容器的索引(第几个容器),同样这里的索引也可以用来指示当前菜单在第几级。
还要设置_onmenu为当前的菜单对象,它在取浮动位置时需要用到。
然后就可以用Set()程序来设置菜单了:

Code
oThis._timerMenu = setTimeout(function(){
    oContainer.index 
= i; oThis._onmenu = oMenu; oThis._index = index + 1;  oThis.Set();
}, oThis.Delay);


在Set()程序中第一部是先隐藏select,这是通用的做法了:

Each(document.getElementsByTagName( " select " ),  function (o){ o.style.visibility  =   " hidden " ; })

设置一个参数,作为容器集合的索引,这里可以直接从第二级开始,所以设i初始值为1。
用一个while来反复取子菜单结构(menu),直到没有子菜单(menu长度为0)或者取得了子菜单结构(_index==i)。
这里没有用for,因为我觉得while比较合适,或者for更好也说不定。
这里除了取得子菜单结构也要取得子菜单的定位(position)。
期间如果容器不够会自动添加。
取得了子菜单结构和定位后,就可以用SetContainer()设置下一级菜单容器了:
Code
var i = 1;
while (menu.length > 0) {
    
//获取子菜单结构和定位
    
    
//如果容器不够就根据前一个自动添加
    
    
//设置下一级菜单
    if(this._index == i++){ this.SetContainer(menu, position); break; }
}


程序SetContainer()用的技巧不多,首先对容器进行相关设置,在使用了SetMenu()来设置菜单对象,
然后是容器的定位和显示,最后隐藏不需要的容器,这部分就不说明了。

要说说的是SetMenu()程序,它的作用是根据菜单结构设置菜单对象并放到容器中。
根据菜单结构的每个元素创建一个菜单对象,innerHTML元素的txt属性,设置mouseover事件,最后appendChild到容器中。
这里比较重要的是mouseover事件,在mouseover事件中会重新设置菜单和重新设置样式,
当触发mouseover事件就回到一开头的“从菜单对象的onmouseover说起”(轮回!-_-)。
不知你晕不晕,反正刚开始时我是比较晕的了。


【焦点样式设置】

这里说的就是SetMenu()中重新设置样式的部分。
程序中可以看出鼠标指定的菜单和父菜单会用另外定义的样式来显示。
一般的做法是在mouseover和mouseout中设置样式,
但这里不行,因为有延时,当鼠标快速移动到另一个菜单,再移到原来的菜单上时,
样式就不会自动设回来,所以只好每次mouseover都重新设置每个容器的菜单的样式。
暂时还找不到更好的方法,有的话记得通知我哦o(_ _)o

Code
Each(oThis.Container, function(o, i){
    
if(i > index) return;
    Each(o.getElementsByTagName(oThis.Tag), 
function(o){ o.className = oThis.Class; });
    
if(i == index){
        oMenu.className 
= oThis.onClass;
    } 
else if(o.index >= 0) {
        o.getElementsByTagName(oThis.Tag)[o.index].className 
= oThis.onClass;
    } 
else return;
});


【扩展功能】

有这些属性可以设置:
Position: 默认位置(up,down,left,right);
Tag: 默认生成标签;
Class: 默认样式;
onClass: 焦点样式;
Delay: 延迟值(微秒);

暂时有这两个方法:
Add(menu):添加菜单,参数是一个菜单结构;
Delete(index):删除菜单,参数是菜单索引;

也可以直接修改_menu属性,怎么扩展就看各位的想象力了。


使用说明

参数1是一个容器集合:

var  arrContainer = [ " idMenu1 " ]; 

参数2是一个菜单结构:
Code
var menu = [
    {
'txt''1 <a href="http://shundebk.cn/">link</a>' },
    {
'txt''2 →''menu': [
        {
'txt''2_1'},
        {
'txt''2_2'}
    ]},
    {
'txt''3 →''menu': [
        {
'txt''3_1 ↑''position''up''menu': [
            {
'txt''3_1_1'},
            {
'txt''3_1_2'}
        ]},
        {
'txt''3_2'}
    ]},
    {
'txt''4 →''menu': [
        {
'txt''4_1 ↓''position''down''menu': [
            {
'txt''4_1_1 →''menu': [
                {
'txt''4_1_1_1'}
            ]}
        ]}
    ]},
    {
'txt''5 →''menu': [
        {
'txt''5_1 →''menu': [
            {
'txt''5_1_1 ↓''position''down''menu': [
                {
'txt''5_1_1_1 ←''position''left''menu': [
                    {
'txt''5_1_1_1_1'}
                ]}
            ]}
        ]}
    ]}
];

参数3是一些设置:
{ Delay:  200 , onClass:  " on "  }

实例化对象:
new  CascadeMenu(arrContainer, arrMenu, { Delay:  200 , onClass:  " on "  });


程序源码

Code
var $ = function (id) {
    
return "string" == typeof id ? document.getElementById(id) : id;
};

function addEventHandler(oTarget, sEventType, fnHandler) {
    
if (oTarget.addEventListener) {
        oTarget.addEventListener(sEventType, fnHandler, 
false);
    } 
else if (oTarget.attachEvent) {
        oTarget.attachEvent(
"on" + sEventType, fnHandler);
    } 
else {
        oTarget[
"on" + sEventType] = fnHandler;
    }
};

function Event(e){
    
var oEvent = document.all ? window.event : e;
    
if (document.all) {
        
if(oEvent.type == "mouseout") {
            oEvent.relatedTarget 
= oEvent.toElement;
        }
else if(oEvent.type == "mouseover") {
            oEvent.relatedTarget 
= oEvent.fromElement;
        }
        
        oEvent.stopPropagation 
= function() { this.cancelBubble = true; }
    }
    
return oEvent;
}

function Each(list, fun){
    
for (var i = 0, len = list.length; i < len; i++) { fun(list[i], i); }
};


var Class = {
  create: 
function() {
    
return function() {
      
this.initialize.apply(this, arguments);
    }
  }
}

Object.extend 
= function(destination, source) {
    
for (var property in source) {
        destination[property] 
= source[property];
    }
    
return destination;
}


var CascadeMenu = Class.create();
CascadeMenu.prototype 
= {
  
//初始化对象(容器集合, 菜单结构)
  initialize: function(arrContainer, arrMenu, options) {
    
if(arrContainer.length <= 0 || arrMenu.lenght <= 0return;
    
    
var oThis = this;
    
    
this._timerContainer = null;//容器定时器
    this._timerMenu = null;//菜单定时器
    this._onmenu = null;//当前菜单对象
    this._index = -1;//要设置容器的索引
    
    
this.Container = [];//容器集合
    this._menu = arrMenu;//菜单结构
    
    
this.SetOptions(options);
    
    
this.Position = this.options.Position || "right";
    
this.Delay = parseInt(this.options.Delay) || 0;
    
this.Class = this.options.Class || "";
    
this.onClass = this.options.onClass || this.Class;
    
this.Tag = this.options.Tag;
    
    
//设置容器
    Each(arrContainer, function(o, i){ oThis.IniContainer(oThis.Container[i] = (o = $(o)), i > 0); });
    
    
this.Ini();
  },
  
//设置默认属性
  SetOptions: function(options) {
    
this.options = {//默认值
        Position:    "right",//默认位置(up,down,left,right)
        Tag:        "div",//默认生成标签
        Class:        "",//默认样式
        onClass:    "",//焦点样式
        Delay:        0//延迟值(微秒)
    };
    Object.extend(
this.options, options || {});
  },
  
//初始化容器(容器集合, 是否子菜单容器)
  IniContainer: function(container, bChild) {
    
var oThis = this;
    addEventHandler(container, 
"mouseover"function(){ clearTimeout(oThis._timerContainer); });
    addEventHandler(container, 
"mouseout"function(e){
        
//是否在菜单之内
        var isIn = false, oT = Event(e).relatedTarget;
        Each(oThis.Container, 
function(o, i){ if(o.contains ? o.contains(oT) || o == oT : o.compareDocumentPosition(oT) & 16){ isIn = true; } });
        
//在菜单外隐藏
        if(!isIn){
            clearTimeout(oThis._timerContainer); clearTimeout(oThis._timerMenu);
            oThis._timerContainer 
= setTimeout(function(){ oThis.Hide(); }, oThis.Delay);
        }
    });
    
//重置索引
    container.index = -1;
    
//子菜单容器设置
    if (bChild) { container.style.position = "absolute"; container.style.visibility = "hidden"; }
  },
  
//初始化第一个容器
  Ini: function() {
    
this.Container[0].innerHTML = ""this._index = 0this.SetMenu(this._menu);
  },
  
//全局设置
  Set: function() {
    
//隐藏select
    Each(document.getElementsByTagName("select"), function(o){ o.style.visibility = "hidden"; })
    
    
var menu = this._menu;
    
//第一个不需要处理所以从1开始
    var i = 1;
    
while (menu.length > 0) {
        
//获取子菜单结构和定位
        var iC = this.Container[i-1].index, position = this.Position;
        
if(iC >= 0){
            
//这里要先取position再设menu
            position = menu[iC].position || this.Position; menu = menu[iC].menu || [];
        } 
else { menu = []; }
        
        
//如果容器不够就根据前一个自动添加
        if(!this.Container[i]){
            
var oPre = this.Container[i-1], oNew = document.body.appendChild(document.createElement(oPre.tagName));
            oNew.style.cssText 
= oPre.style.cssText; oNew.className = oPre.className;
            
this.IniContainer(this.Container[i] = oNew, true);
        }
        
        
//设置下一级菜单
        if(this._index == i++){ this.SetContainer(menu, position); break; }
    }
  },
  
//容器设置(菜单结构, 位置)
  SetContainer: function(menu, position) {
    
var oContainer = this.Container[this._index];
    
    
//设置容器
    oContainer.innerHTML = ""; oContainer.index = -1; oContainer.style.visibility = "hidden";
    
    
if(menu.length > 0){
        
//设置菜单
        this.SetMenu(menu);
        
        
//容器定位
        //offset取值会有偏差,要注意
        var o = this._onmenu, iLeft = o.offsetLeft, iTop = o.offsetTop;
        
//注意如果display为none的话取不到offset值,所以要用visibility
        switch (position.toLowerCase()) {
            
case "up" :
                iTop 
-= oContainer.offsetHeight;
                
break;
            
case "down" :
                iTop 
+= o.offsetHeight;
                
break;
            
case "left" :
                iLeft 
-= oContainer.offsetWidth;
                
break;
            
case "right" :
            
default :
                iLeft 
+= o.offsetWidth;
        }
        
while (o.offsetParent) { o = o.offsetParent; iLeft += o.offsetLeft; iTop += o.offsetTop; }
        oContainer.style.left 
= iLeft + "px"; oContainer.style.top = iTop + "px";
        oContainer.style.visibility 
= "visible";
    }
    
    
//隐藏不需要的容器
    for(var i = this._index + 1, len = this.Container.length; i < len; i++){ this.Container[i].style.visibility = "hidden";  }
  },
  
//菜单设置(菜单结构)
  SetMenu: function(menu) {
    
var oThis = this, index = this._index, oContainer = this.Container[index];
    Each(menu, 
function(o, i){
        
var oMenu = document.createElement(oThis.Tag);
        oMenu.innerHTML 
= o.txt;
        oMenu.onmouseover 
= function(){
            clearTimeout(oThis._timerMenu);
            
            
//重新设置菜单
            oThis._timerMenu = setTimeout(function(){
                oContainer.index 
= i; oThis._onmenu = oMenu; oThis._index = index + 1;  oThis.Set();
            }, oThis.Delay);
            
            
//重新设置样式
            //为解决设置延时后样式的问题每次都全部重新设置
            Each(oThis.Container, function(o, i){
                
if(i > index) return;
                Each(o.getElementsByTagName(oThis.Tag), 
function(o){ o.className = oThis.Class; });
                
if(i == index){
                    oMenu.className 
= oThis.onClass;
                } 
else if(o.index >= 0) {
                    o.getElementsByTagName(oThis.Tag)[o.index].className 
= oThis.onClass;
                } 
else return;
            });
        }
        oContainer.appendChild(oMenu);
    });    
  },
  
//隐藏菜单
  Hide: function() {
    
var oThis = this;
    
//除第一个外隐藏
    Each(this.Container, function(o, i){
        
if(i == 0){
            Each(o.getElementsByTagName(oThis.Tag), 
function(o, i){ o.className = oThis.Class; })
        } 
else { o.style.visibility = "hidden"; }
        o.index 
= -1;
    });
    
    
//显示select
    Each(document.getElementsByTagName("select"), function(o){ o.style.visibility = "visible"; })
  },
  
//添加菜单(一个菜单结构)
  Add: function(arrMenu) {
    
this._menu.push(arrMenu); this.Ini();
  },
  
//删除菜单
  Delete: function(index) {
    
if(index < 0 || index >= this._menu.length) return;
    
for(var i = index, len = this._menu.length - 1; i < len; i++){ this._menu[i] = this._menu[i + 1]; }
        
this._menu.pop(); this.Ini();
  }
};

下载完整实例

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值