• JavaScript 多级联动浮动(下拉)菜单 (第二版)


    JavaScript 多级联动浮动(下拉)菜单 (第二版)

     

    上一个版本(第一版请看这里)基本实现了多级联动和浮动菜单的功能,但效果不是太好,使用麻烦还有些bug,实用性不高。
    这次除了修改已发现的问题外,还对程序做了大幅调整和改进,使程序实用性更高,功能更强大。


    效果预览

    菜单使用演示:

    自定义样式
    下拉菜单
    新加菜单+
    任意定位










    位置:

    仿京东商城商品分类菜单:

    图片动画
    图片效果
    动画效果
    系统其他
    系统效果
    其他效果

    仿window xp右键菜单:
     

    仿淘宝拼音索引菜单:








    程序原理

    程序最关键的地方是多级联动,先大概说明一下:
    首先第一级的菜单元素整理好后,从他们开始,当某个菜单元素触发显示下级菜单时,
    准备好下一级的容器元素,并把下一级的菜单元素放进去,再定位并显示容器元素。
    里面的菜单元素又可以触发显示下级菜单,然后按上面的步骤执行下去。
    这样一级一级的递推下去,形成多级的联动菜单。


    程序说明

    【容器对象】

    在多级联动中,每一级都需要一个容器元素来存放菜单元素。
    程序中每个容器元素都对应一个容器对象,用来记录该容器的相关信息。
    容器对象的集合记录在程序的_containers属性中。

    容器参数containers是程序实例化时的必要参数,它的结构如下:

    [
        容器元素(id),
        { id: 容器元素(id), menu: 插入菜单元素(id) },
        
    ]


    首先如果containers不是数组的话,程序会自动转成单元素数组。
    如果菜单插入的元素就是容器元素本身,可以直接用容器元素(id)作为数组元素。
    否则应该使用一个对象结构,它包括一个id属性表示是容器元素(id)和一个menu属性表示菜单插入的元素(id)。

    containers会在程序初始化时这样处理:

    Code


    主要是生成一个容器对象,其中pos属性是容器元素,menu属性是插入菜单的元素。
    然后传递索引和容器对象给_iniContainer函数,对容器对象做初始化。

    在_iniContainer中,首先用_resetContainer重置容器对象可能在程序中设置过的属性。
    再给容器元素添加事件:

    Code


    在mouseout时,先判断是否容器内部或容器之间触发,不是的话再用定时器执行hide隐藏函数。
    在hide里面,主要是隐藏容器:

    复制代码
    this._forEachContainer(function(o, i){
        
    if ( i === 0 ) {
            
    this._resetCss(o);
        } 
    else {
            
    this._hideContainer(o);
        };
    });
    复制代码


    由于第一级容器一般是不自动隐藏的,只需要用_resetCss来重置样式。
    其他容器会用_hideContainer函数来处理隐藏:

    $$D.setStyle( container.pos, { left: "-9999px", top: "-9999px", visibility: "hidden" } );
    this._containers[container._index - 1]._active = null;


    其中_active属性是保存该容器触发下级菜单的菜单对象,在隐藏容器同时重置上一级容器的_active。

    在mouseover时清除容器定时器,其实就是取消hide执行。

    之后是设置样式:

    if ( index ) {
        $$D.setStyle(container.pos, {
            position: 
    "absolute", display: "block", margin: 0,
            zIndex: 
    this._containers[index - 1].pos.style.zIndex + 1
        });
    };


    除了第一级容器外,都设置浮动需要的样式。

    最后用_index属性记录索引,方便调用,并把容器对象插入到容器集合中: 

    container._index = index;
    this._containers[index] = container;


    这个索引很重要,它决定了容器是用在第几级菜单。


    【菜单对象】

    容器元素插入了菜单元素才算一个菜单。
    程序中每个菜单元素都对应一个菜单对象,用来记录该菜单的相关信息。

    程序初始化前,应该先创建好自定义菜单集合,它的结构是这样的: 

    [
        { id: 
    1, parent: 0, html: 元素内容 },
        { id: 
    2, parent: 1, html: 元素内容 },
        
    ]


    其中id是菜单的唯一标识,parent是父级菜单的id。
    除了这两个关键属性外,还可以包括以下属性:
    rank:排序属性
    elem:自定义元素
    tag:生成标签
    css:默认样式
    hover:触发菜单样式
    active: 显示下级菜单时显示样式
    html:菜单内容
    relContainer:是否相对容器定位(否则相对菜单)
    relative:定位对象
    attribute:自定义Attribute属性
    property:自定义Property属性

    其中relContainer和relative是用于下级容器定位的。

    自定义菜单集合会保存在_custommenu属性中。
    在程序初始化时会执行_buildMenu程序,根据这个_custommenu生成程序需要的_menus菜单对象集合。
    _buildMenu是比较关键的程序,菜单的层级结构就是在这里确定,它由以下几步组成:

    第一步,清除旧菜单对象集合的dom元素。
    这一步后面“内存泄漏”会详细说明。

    第二步,生成菜单对象集合。
    为了能更有效率地获取指定id的菜单对象,_menus是以id作为字典关键字的对象。
    首先创建带根菜单(id为“0”)对象的_menus: 

    this._menus = { "0": { "_children": [] } };


    然后整理_custommenu并插入到_menus中:

    Code


    其中菜单对象中包含对象属性,要用deepextend深度扩展来复制属性。
    为确保id是唯一标识,会排除相同id的菜单,间接排除了id为“0”的菜单。
    在重置_children(子菜单集合)和_index(联级级数)之后,就可以插入到_menus中了。

    第三步,建立树形结构。
    菜单之间的关系是一个树形结构,程序通过id和parent来建立这个关系的(写过数据库分级结构的话应该很熟悉)。
    而第一版是把子类直接菜单写在菜单元素的menu属性中,形成类似多维数组的结构。
    比较这两个方法,第一版的优势在于定义菜单时就直接确立了关系,而新版还必须根据id和parent来判断增加代码复杂度。
    新版的优势是使用维护方便,灵活,级数越多就越体现出来,而第一版刚好相反。

    能不能结合这两个方法的优势呢?
    这里采用了一个折中的方法,在写自定义菜单对象时用的是新版的方法,然后程序初始化时把它转换成类多维数组结构。
    转换过程是这样的:
    首先根据parent找到父菜单对象:

    var parent = this._menus[o.parent];


    如果找不到父菜单对象或父菜单对象就是菜单对象本身的,当成一级菜单处理:

    if ( !parent || parent === o ) { parent = menus[o.parent = "0"]; };


    最后把当前菜单对象放到父菜单对象的_children集合中: 

    parent._children.push(o);


    这就把_menus变成了类多维数组结构,而且这个结构不会发生死循环。

    第四步,整理菜单对象集合。
    这步主要是整理_menus里面的菜单对象。
    首先,把自定义菜单元素放到碎片文档中:

    !!o.elem && ( o.elem = $$(o.elem) ) && this._frag.appendChild(o.elem);


    菜单元素是需要显示时才会处理的,这样可以防止在容器上出现未处理的菜单元素。

    然后是修正样式(详细看样式设置部分)。

    最后,对菜单对象的_children集合进行排序:

    o._children.sort(function( x, y ) { return x.rank - y.rank || x.id - y.id; });


    先按rank再按id排序,跟菜单对象定义的顺序是无关的。

    执行完BuildMenu程序之后,_menus菜单对象集合就建立好了。
    麻烦的是在每次修改_custommenu之后,都必须执行一次_buildMenu程序。


    【多级联动】

    容器对象和菜单对象都准备好了,下面就是如何利用它们来做程序的核心——多级联动效果了。

    多级联动包括以下步骤:

    第一步,准备一级容器。
    一级容器一般是显示状态的(也可以自己定义它的显示隐藏,像仿右键菜单那样)。

    第二步,向容器插入菜单。
    通过_insertMenu程序,可以向指定容器插入指定菜单,其中第一个参数是索引,第二个参数是父菜单id。

    在_insertMenu程序里面,先判断是否同一个父级菜单,是的话就返回不用重复操作了: 

    var container = this._containers[index];
    if ( container._parent === parent ) { return; };
    container._parent 
    = parent;


    接着把原有容器内菜单移到碎片对象中:

    $$A.forEach( container._menus, function(o) { o._elem && this._frag.appendChild(o._elem); }, this );


    在第一版,菜单每次使用都会重新创建,新版改进后会把旧菜单元素保存到碎片对象中,要使用时再拿出来。

    然后根据parent获取父菜单对象,并把父菜单的_children子菜单集合的插入到容器中:

    $$A.forEach(this._menus[parent]._children, function( menu, i ){
        
    this._checkMenu( menu, index );
        container._menus.push(menu);
        container.menu.appendChild(menu._elem);
    }, 
    this);

     
    这样整个菜单就准备好了。

    第三步,添加触发下级菜单事件。
    上面在把菜单插入到容器之前,会先用_checkMenu程序检查菜单对象。

    _checkMenu程序主要是检测和处理菜单元素。
    首先判断没有自定义元素,没有的话就创建一个:

    var elem = menu.elem;
    if ( !elem ) { elem = document.createElement(menu.tag); elem.innerHTML = menu.html; };


    第一版并不能自定义元素,但考虑到seo、渐进增强等,在新版加入了这个功能。
    但每次BuildMenu之后会把所有菜单元素包括自定义元素都清除,这个必须留意。

    然后分别设置property、attribute和className属性:

    $$.extend( elem, menu.property );
    var attribute = menu.attribute;
    for (var att in attribute) { elem.setAttribute( att, attribute[att] ); };
    elem.className 
    = menu.css;


    ps:关于property和attribute的区别请看这里的attribute/property部分

    然后是关键的一步,添加HoverMenu触发事件程序:

    menu._event = $$F.bindAsEventListener( this._hoverMenu, this, menu );
    $$E.addEvent( elem, 
    "mouseover", menu._event );

     
    处理后的元素会保存在菜单对象的_elem属性中。

    第四步,触发显示下级菜单事件。
    当触发了显示下级菜单事件,就会执行_hoverMenu程序。
    在_hoverMenu程序里面,主要是做一些样式设置,详细参考后面的样式设置部分。
    然后是用定时器准备执行_showMenu显示菜单程序。

    第五步,整理菜单容器。
    在_showMenu程序中,首先是隐藏不需要的容器:

    this._forEachContainer( function(o, i) { i > index && this._hideContainer(o); } );


    然后判断当前菜单是否有子菜单,当有子菜单时,先用_checkContainer程序检查下级菜单容器。
    _checkContainer程序主要是检查容器是否存在,不存在的话就自动添加一个:

    var pre = this._containers[index - 1].Pos
        ,container 
    = pre.parentNode.insertBefore( pre.cloneNode(false), pre );
    container.id 
    = "";


    其实就是用cloneNode复制前一个容器,注意要重置id防止冲突。
    虽然程序能自动创建菜单,但也要求至少自定义一个容器。

    第六步,显示菜单容器。
    在显示之前,先按第二步向容器插入菜单,最后就是执行_showContainer程序来定位和显示容器了。

    当下一个容器内的菜单触发显示下级菜单事件时,会显示下下级的菜单容器。
    程序就是这样一级一级递推下去,形成多级联级效果。


    【样式设置】

    样式设置也是一个重要的部分,不是说要弄出多炫的界面,而是如何使程序能最大限度地灵活地实现那些界面。

    菜单对象有三个样式相关的属性,分别是:
    css:默认样式
    hover:鼠标进入菜单时使用样式
    active:显示下级菜单时使用样式

    在_buildMenu程序中,会对这些样式属性进行整理:

    Code


    可以看到,程序会优先使用自定义元素的class,避免被程序设置的默认样式覆盖。
    空字符串也可能被用来清空样式,所以要用undefined来判断是否自定义了样式。

    程序中主要在两个地方设置样式:在鼠标移到菜单元素上时(_hoverMenu)和显示下级菜单时(_showMenu)。

    在_hoverMenu程序中,先对每个显示的容器设置一次样式:

    this._forEachContainer(function(o, i){
        
    if ( o.pos.visibility === "hidden" ) { return; };
        
    this._resetCss(o);
        
    var menu = o._active;
        
    if ( menu ) { menu._elem.className = menu.active; };
    });


    由于鼠标可能是在多个容器间移动,所以所有显示的容器都需要设置。
    用_resetCss重置容器样式后再设置有下级菜单的菜单的样式为active。
    为了方便获取,容器对象用一个_active属性来保存当前容器触发了下级菜单的菜单对象。

    然后是设置鼠标所在菜单的样式:

    if ( this._containers[menu._index]._active !== menu ) { elem.className = menu.hover; };


    为了优先设置active样式,在当前菜单不是容器的_active时才设置hover样式。

    在_showMenu程序中,首先把显示下级菜单的菜单对象保存到容器的_active属性。
    再用_resetCss重置当前容器样式,这个在同级菜单中移动时会有用。
    然后再根据当前菜单是否有下级菜单来设置样式为active或hover。


    【内存泄漏】

    上面“菜单对象”中说到清除旧菜单对象的dom元素,这个主要是为了防止内存泄漏。
    关于内存泄漏也有很多文章,这里推荐看看Douglas Crockford的“JScript Memory Leaks”和winter的“浏览器中的内存泄露”。

    下面说说我解决本程序内存泄漏的经过:
    首先,通过调用程序的Add和Delete数千次来测试是否有内存泄漏。
    怎么看出来呢?可以找些相关的工具来检测,或者直接看任务管理器的页面文件(pf)使用记录。
    结果发现,虽然每个元素都用removeChild移出了dom,但随着循环的次数增多,pf还是稳步上升。
    于是按照Memory Leaks中说的“we must null out all of its event handlers to break the cycles”去掉事件:

    removeEvent( elem, "mouseover", o._event );


    效果是有了,但不太理想,然后再逐一排除,发现原来是_elem属性还关联着元素,结果经过一些操作后,又把元素append到dom上,还重新创建了一个元素。

    于是在移除元素后,立即重置_elem和elem属性:

    o._elem = o.elem = null;


    内存泄漏就没有了,其实这里也不算是内存泄露了,而是程序设计有问题了。
    所以清除dom元素时必须注意:
    1,按照Douglas Crockford的建议,移除所有dom元素相关的事件函数;
    2,删除/重置所有关联dom元素的js对象/属性。


    【cloneNode的bug】

    在上面多级联动中说到,会用cloneNode复制容器,但cloneNode在ie中有一个bug:
    在ie用attachEvent给dom元素绑定事件,在cloneNode之后会把事件也复制过去。
    而用addEventListener添加的事件就不会,可以在ie和ff测试下面的代码: 

    Code


    在ie和ff点击第一个div都会触发alert,关键是第二个div,在ff不会触发,而ie就会。
    当然这个是不是bug还不清楚,或许attachEvent本来就是这样设计的也说不定。
    但第一版就是由于这个bug,而没有用cloneNode。

    在找解决方法之前,再扩展这个问题,看看直接添加onclick事件会不会有同样的bug。
    首先测试在元素里面添加onclick: 

    复制代码
    <!DOCTYPE html>
    <html>
    <body>
    <div id="t" onclick="alert(1)">div</div>
    <script>
    var o = document.getElementById("t");
    document.body.appendChild(o.cloneNode(
    true));
    </script>
    </body>
    </html>
    复制代码


    结果在ie和ff都会复制事件。

    再测试在js添加onclick: 


    <!DOCTYPE html>
    <html>
    <body>
    <div id="t">div</div>
    <script>
    var o = document.getElementById("t");
    o.onclick 
    = function(){alert(1)}
    document.body.appendChild(o.cloneNode(
    true));
    </script>
    </body>
    </html>


    结果在ie和ff都不会复制事件,看来只有attachEvent会引起这个bug。

    下面是解决方法:
    用John Resig在《精通JavaScript》推荐的Dean Edwards写的addEvent和removeEvent方法来添加/移除事件。
    它的好处就不用说了,而且它能在ie解决上面说到的cloneNode的bug。
    因为它的实现原理是在ie用onclick来绑定事件,而上面的测试也证明用onclick绑定的事件是不会被cloneNode复制的。

    ps:我对原版的方法做了些修改,方便调用。


    【浮动定位】

    容器的浮动定位用的是浮动定位提示效果中的定位方法。
    在该文章中已经详细说明了如何获取指定的浮动定位坐标,这里做一些补充。

    一般来说用getBoundingClientRect配合scrollLeft/scrollTop就能获得对象相对文档的位置坐标。
    测试下面代码:


    <!DOCTYPE html>
    <html>
    <body style="padding:1000px 0;">
    <div id="t1" style="border:1px solid; 100px; height:100px;"></div>
    <div id="t2"></div>
    <script>
    var $$ = function (id) {
        
    return "string" == typeof id ? document.getElementById(id) : id;
    };
    var b = 0;
    window.onscroll
    =function(){
     
    var t = $$("t1").getBoundingClientRect().top + document.documentElement.scrollTop;
     
    if( t != b ){ b = t; $$("t2").innerHTML += t + "<br>"; }
    }
    </script>
    </body>
    </html>


    在除ie8外的浏览器,t会保持在一个固定值,但在ie8却会在1008和1009之间变换(用鼠标一格一格滚会比较明显)。
    虽然多数时候还是标准的1008,但原来的效果可能就会被这1px的差距破坏(例如仿京东和仿淘宝的菜单)。
    ps:chrome和safari要把documentElement换成body。

    为了解决这个问题,只好在ie8的时候用回传统的offset来取值了(详细参考代码)。
    至于造成这个问题的原因还没弄清楚,各位有什么相关资料的记得告诉我哦。


    使用技巧

    在仿京东商城商品分类菜单中,实现了一个阴影效果。
    原理是这样的:
    底部是一个灰色背景层(阴影),里面放内容层,然后设置内容层相对定位(position:relative),并做适当的偏移(left:-3px;top:-3px;)。
    由于相对定位会保留占位空间,这样就能巧妙地做出了一个可自适应大小的背景层(阴影)。
    ps:博客园首页也做了类似的效果,但貌似错位有些严重哦。

    仿右键菜单效果并不支持opera,因为opera并没有类似oncontextmenu这样的事件,要实现的话会很麻烦。
    ps:如果想兼容opera的话,可以看看这篇文章“Opera下自定义右键菜单的研究”。
    注意,在oncontextmenu事件中要用阻止默认事件(preventDefault)来取消默认菜单的显示。
    这个效果还做了一个不能选择的处理,就是拖动它的内容时不会被选择。
    在ff中把样式-moz-user-select设为none就可以了,而ie、chrome和safari通过在onselectstart返回false来实现相同的效果。
    ps:css3有user-select样式,但貌似还没有浏览器支持。
    当然,还有很多不完善的地方,这里只是做个参考例子,就不深究了。

    仿淘宝拼音索引菜单主要体现了active的用法和相对容器定位,难的地方还是在样式(具体参考代码)。

    ps:这几个例子都只是二级菜单,其实可以自己用更简单的方法实现。


    使用说明

    实例化时,第一个必要参数是自定义容器对象: 

    new FixedMenu("idContainer");


    第二个可选参数用来设置系统的默认属性,包括
    属性:   默认值//说明
    menu:   [],//自定义菜单集合
    delay:   200,//延迟值(微秒)
    tag:   "div",//默认生成标签
    css:   undefined,//默认样式
    hover:   undefined,//触发菜单样式
    active:   undefined,//显示下级菜单时显示样式
    html:   "",//菜单内容
    relContainer: false,//是否相对容器定位(否则相对菜单)
    relative:  { align: "clientleft", vAlign: "bottom" },//定位对象
    attribute:  {},//自定义attribute属性
    property:  {},//自定义property属性
    onBeforeShow: function(){}//菜单显示时执行
    其中包括菜单对象的默认属性。

    还提供了以下方法:
    hide:隐藏菜单,隐藏所有菜单容器;
    add:添加菜单,参数是菜单对象或菜单对象集合;
    edit:修改菜单,找出对应id的菜单对象修改属性设置;
    del:删除菜单,参数是要删除菜单对象的id。
    其中后面三个编辑方法都会执行Ini初始化程序,效率较低,一般来说尽量不要使用。

    完整实例下载

    转载请注明出处:http://www.cnblogs.com/cloudgamer/

    如有任何建议或疑问,欢迎留言讨论。

    如果觉得文章不错的话,欢迎点一下右下角的推荐。

    程序中包含的js工具库CJL.0.1.min.js,原文在这里

     
    分类: Javascript
  • 相关阅读:
    收集一些.NET开发资源站点和部分优秀.NET开源项目
    对DataTable数据进行查询过滤
    转:CommandArgument 传多个值到另外页面的方法
    转:SqlServer中的datetime类型的空值和c#中的DateTime的空值的研究
    (转)SQL语句Select Case和If else
    .net 使用NPOI或MyXls把DataTable导出到Excel
    pycharm+PyQt5+python最新开发环境配置,踩坑过程详解
    sql server2008系统表详细说明sys.开头的表
    SqlServer中Sql查看存储过程
    SQL SERVER导入EXCEL文件:未在本地计算机上注册“Microsoft.ACE.OLEDB.12.0”提供程序
  • 原文地址:https://www.cnblogs.com/ayforver/p/3491901.html
Copyright © 2020-2023  润新知