上一个版本(第一版请看这里)基本实现了多级联动和浮动菜单的功能,但效果不是太好,使用麻烦还有些bug,实用性不高。 这次除了修改已发现的问题外,还对程序做了大幅调整和改进,使程序实用性更高,功能更强大。 主程序代码也从第一版的一百多行增加到三百多,我写的程序中也就拖拉缩放效果有这个数量了。
效果预览
菜单使用演示:
位置:
第四个 第三个 第二个 第一个 1秒 0.5秒 0.2秒 不延时
仿京东商城商品分类菜单:
仿右键菜单:
仿淘宝拼音索引菜单:
程序原理
程序最关键的地方是多级联动,先大概说明一下: 首先第一级的菜单元素整理好后,从他们开始,当某个菜单元素触发显示下级菜单时, 准备好下一级的容器元素,并把下一级的菜单元素放进去,再定位并显示容器元素。 里面的菜单元素又可以触发显示下级菜单,然后按上面的步骤执行下去。 这样一级一级的递推下去,形成多级的联动菜单。
程序说明
【容器对象】
在多级联动中,每一级都需要一个容器元素来存放菜单元素。 程序中每个容器元素都对应一个容器对象,用来记录该容器的相关信息。 容器对象的集合记录在程序的_containers属性中。
容器参数containers是程序实例化时的必要参数,它的结构如下:
[ 容器元素(id), { id: 容器元素(id), menu: 插入菜单元素(id) }, ]
首先如果containers不是数组的话,程序会自动转成单元素数组。 如果菜单插入的元素就是容器元素本身,可以直接用容器元素(id)作为数组元素。 否则应该使用一个对象结构,它包括一个id属性表示是容器元素(id)和一个menu属性表示菜单插入的元素(id)。
containers会在程序初始化时这样处理:
Code forEach(isArray(containers) ? containers : [containers], function (o, i){ var pos, menu; if ( o.id ) { pos = o.id; menu = o.menu ? o.menu : pos; } else { pos = menu = o; }; pos = $$(pos); menu = $$(menu); pos && menu && this .IniContainer( i, { Pos: pos, Menu: menu } ); }, this );
主要是生成一个容器对象,其中Pos属性是容器元素,Menu属性是插入菜单的元素。 然后传递索引和容器对象给IniContainer函数,对容器对象做初始化。
在IniContainer中,首先用ResetContainer重置容器对象可能在程序中设置过的属性。 再给容器元素添加事件:
Code addEvent( oContainer, " mouseover " , Bind( this , function (){ clearTimeout( this ._timerContainer); } ) ); addEvent( oContainer, " mouseout " , BindAsEventListener( this , function (e){ var elem = e.relatedTarget, isOut = Every( this ._containers, function (o){ return ! (Contains(o.Pos, elem) || o.Pos == elem); } ); if ( isOut ) { clearTimeout( this ._timerContainer); clearTimeout( this ._timerMenu); this ._timerContainer = setTimeout( Bind( this , this .Hide ), this .Delay ); }; }));
在mouseout时,先判断是否容器内部或容器之间触发,不是的话再用定时器执行Hide隐藏函数。 在Hide里面,主要是隐藏容器:
this .forEachContainer( function (o, i){ if ( i === 0 ) { this .ResetCss(o); } else { this .HideContainer(o); }; });
由于第一级容器一般是不自动隐藏的,只需要用ResetCss来重置样式。 其他容器会用HideContainer函数来处理隐藏:
var css = container.Pos.style; css.visibility = " hidden " ; css.left = css.top = " -9999px " ; this ._containers[container._index - 1 ]._active = null ;
其中_active属性是保存该容器触发下级菜单的菜单对象,在隐藏容器同时重置上一级容器的_active。
在mouseover时清除容器定时器,其实就是取消Hide执行。
之后是设置样式:
if ( index ) { var css = container.Pos.style; css.position = " absolute " ; css.display = " block " ; css.margin = 0 ; css.zIndex = this ._containers[index - 1 ].Pos.style.zIndex + 1 ; };
除了第一级容器外,都设置浮动需要的样式。
最后用_index属性记录索引,方便调用,并把容器对象插入到容器集合中:
container._index = index; this ._containers[index] = container;
这个索引很重要,它决定了容器是用在第几级菜单。
【菜单对象】
容器元素插入了菜单元素才算一个菜单。 程序中每个菜单元素都对应一个菜单对象,用来记录该菜单的相关信息。
程序初始化前,应该先创建好自定义菜单集合,它的结构是这样的:
[ { id: 1 , parent: 0 , txt: 元素内容 }, { id: 2 , parent: 1 , txt: 元素内容 }, ]
其中id是菜单的唯一标识,parent是父级菜单的id。 除了这两个关键属性外,还可以包括以下属性: rank:排序属性 elem:自定义元素 tag:生成标签 css:默认样式 hover:触发菜单样式 active: 显示下级菜单时显示样式 txt:菜单内容 fixedmenu:是否相对菜单定位(否则相对容器) fixed:定位对象 attribute:自定义Attribute属性 property:自定义Property属性
其中fixedmenu和fixed是用于下级容器定位的。 具体作用会在后面的分析中说明。
自定义菜单集合会保存在_custommenu属性中。 在程序初始化时会执行BuildMenu程序,根据这个_custommenu生成程序需要的_menus菜单对象集合。 BuildMenu是比较关键的程序,菜单的层级结构就是在这里确定,它由以下几步组成:
第一步,清除旧菜单对象集合的dom元素。 这一步后面“内存泄漏”会详细说明。
第二步,生成菜单对象集合。 为了能更有效率地获取指定id的菜单对象,_menus是以id作为字典关键字的对象。 首先创建带根菜单(id为“0”)对象的_menus:
this ._menus = { " 0 " : { " _children " : [] } };
然后整理_custommenu并插入到_menus中:
Code forEach( this ._custommenu, function (o) { var menu = DeepExtend( DeepExtend( {}, options ), o || {} ); if ( !! this ._menus[ menu.id ] ) { return ; }; menu._children = []; menu._index = - 1 ; this ._menus[menu.id] = menu; }, this );
其中菜单对象中包含对象属性,要用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;
接着把原有容器内菜单移到碎片对象中:
forEach( container._menus, function (o) { o._elem && this ._frag.appendChild(o._elem); }, this ); container._menus = [];
在第一版,菜单每次使用都会重新创建,新版改进后会把旧菜单元素保存到碎片对象中,要使用时再拿出来。
然后根据parent获取父菜单对象,并把父菜单的_children子菜单集合的插入到容器中:
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.txt; };
第一版并不能自定义元素,但考虑到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 = BindAsEventListener( this , this .HoverMenu, menu ); 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 if ( !! o.elem && o.elem.className ) { o.css = o.elem.className; } else if ( o.css === undefined ) { o.css = "" ; }; if ( o.hover === undefined ) { o.hover = o.css; }; if ( o.active === undefined ) { o.active = o.hover; };
可以看到,程序会优先使用自定义元素的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 <! DOCTYPE html > < html > < body > < div id = " t " > div < / div> < script > var o = document.getElementById( " t " ); if (o.attachEvent){ o.attachEvent( " onclick " , function (){alert( 2 )}); } else { o.addEventListener( " click " , function (){alert( 2 )}, false ); } document.body.appendChild(o.cloneNode( true )); < / script> < / body> < / html>
在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:
Code <! 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:我对原版的fixEvent做了些修改,方便独立调用。
【浮动定位】
容器的浮动定位用的是浮动定位提示效果中的定位方法。 在该文章中已经详细说明了如何获取指定的浮动定位坐标,这里做一些补充。
一般来说用getBoundingClientRect配合scrollLeft/scrollTop就能获得对象相对文档的位置坐标。 测试下面代码:
Code <! DOCTYPE html > < html > < body style = " padding:1000px 0; " > < div id = " t1 " style = " border:1px solid; width: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的用法和相对容器定位,难的地方还是在样式(具体参考代码)。
这几个例子都只是二级菜单,其实是有点杀鸡用牛刀了。
使用说明
实例化时,第一个必要参数是自定义容器对象:
new FixedMenu( " idContainer " );
第二个可选参数用来设置系统的默认属性,包括 属性: 默认值//说明 Menu: [],//自定义菜单集合 Delay: 200,//延迟值(微秒) Tag: "div",//默认生成标签 Css: undefined,//默认样式 Hover: undefined,//触发菜单样式 Active: undefined,//显示下级菜单时显示样式 Txt: "",//菜单内容 FixedMenu: true,//是否相对菜单定位(否则相对容器) Fixed: { Align: "clientleft", vAlign: "bottom" },//定位对象 Attribute: {},//自定义Attribute属性 Property: {},//自定义Property属性 onBeforeShow: function(){}//菜单显示时执行
其中包括菜单对象的默认属性,因此菜单对象属性名都是小写以示区分。
还提供了以下方法: Add:添加菜单,参数是菜单对象或菜单对象集合; Edit:修改菜单,找出对应id的菜单对象修改属性设置; Delete:删除菜单,参数是要删除菜单对象的id。 这些方法都会执行Ini初始化程序,效率较低,一般来说尽量不要使用。
程序源码
Code var FixedMenu = function (containers, options) { this ._timerContainer = null ; // 容器定时器 this ._timerMenu = null ; // 菜单定时器 this ._frag = document.createDocumentFragment(); // 碎片对象,保存菜单元素 this ._menus = {}; // 菜单对象 this ._containers = []; // 容器集合 this .SetOptions(options); this ._custommenu = this .options.Menu; this .Css = this .options.Css; this .Hover = this .options.Hover; this .Active = this .options.Active; this .Tag = this .options.Tag; this .Txt = this .options.Txt; this .FixedMenu = this .options.FixedMenu; this .Fixed = this .options.Fixed; this .Attribute = this .options.Attribute; this .Property = this .options.Property; this .onBeforeShow = this .options.onBeforeShow; this .Delay = parseInt( this .options.Delay) || 0 ; // 修正自定义容器 forEach(isArray(containers) ? containers : [containers], function (o, i){ // 自定义容器 id:定位元素 menu:插入菜单元素 var pos, menu; if ( o.id ) { pos = o.id; menu = o.menu ? o.menu : pos; } else { pos = menu = o; }; pos = $$(pos); menu = $$(menu); // 容器对象 Pos:定位元素 Menu:插入菜单元素 pos && menu && this .IniContainer( i, { Pos: pos, Menu: menu } ); }, this ); // 初始化程序 this .Ini(); }; FixedMenu.prototype = { // 设置默认属性 SetOptions: function (options) { this .options = { // 默认值 Menu: [], // 自定义菜单集合 Delay: 200 , // 延迟值(微秒) Tag: " div " , // 默认生成标签 Css: undefined, // 默认样式 Hover: undefined, // 触发菜单样式 Active: undefined, // 显示下级菜单时显示样式 Txt: "" , // 菜单内容 FixedMenu: true , // 是否相对菜单定位(否则相对容器) Fixed: { Align: " clientleft " , vAlign: " bottom " }, // 定位对象 Attribute: {}, // 自定义Attribute属性 Property: {}, // 自定义Property属性 onBeforeShow: function (){} // 菜单显示时执行 }; Extend( this .options, options || {} ); }, // 程序初始化 Ini: function () { this .Hide(); // 隐藏菜单 this .BuildMenu(); // 生成菜单对象 this .forEachContainer( this .ResetContainer); // 重置容器属性 this .InsertMenu( 0 , 0 ); // 显示菜单 }, // 根据自定义菜单对象生成程序菜单对象 BuildMenu: function () { // 清除旧菜单dom(包括自定义的) this .forEachMenu( function (o){ var elem = o._elem; if ( elem ) { // 防止dom内存泄漏 removeEvent( elem, " mouseover " , o._event ); elem.parentNode.removeChild(elem); o._elem = o.elem = null ; }; }); // 设置菜单默认值 var options = { id: 0 , // id rank: 0 , // 排序 elem: "" , // 自定义元素 tag: this .Tag, css: this .Css, hover: this .Hover, active: this .Active, txt: this .Txt, fixedmenu: !! this .FixedMenu, fixed: this .Fixed, attribute: this .Attribute, property: this .Property }; // 先定义"0"顶级菜单 this ._menus = { " 0 " : { " _children " : [] } }; // 整理自定义菜单并插入到程序菜单对象 forEach( this ._custommenu, function (o) { // 生成菜单对象(由于包含对象,要用深度扩展) var menu = DeepExtend( DeepExtend( {}, options ), o || {} ); // 去掉相同id菜单,同时排除了id为"0"的菜单 if ( !! this ._menus[ menu.id ] ) { return ; }; // 重置属性 menu._children = []; menu._index = - 1 ; this ._menus[menu.id] = menu; }, this ); // 建立树形结构 this .forEachMenu( function ( o, id, menus ) { if ( " 0 " === id ) { return ; }; // 顶级没有父级菜单 var parent = this ._menus[o.parent]; // 父级菜单不存在或者父级是自己的话,当成一级菜单 if ( ! parent || parent === o ) { parent = menus[o.parent = " 0 " ]; }; // 插入到父级菜单对象的_children中 parent._children.push(o); }); // 整理菜单对象 this .forEachMenu( function (o) { // 如果有自定义元素的话先放到碎片文档中 !! o.elem && ( o.elem = $$(o.elem) ) && this ._frag.appendChild(o.elem); // 修正样式,优先使用自定义元素的class if ( !! o.elem && o.elem.className ) { o.css = o.elem.className; } else if ( o.css === undefined ) { o.css = "" ; }; if ( o.hover === undefined ) { o.hover = o.css; }; if ( o.active === undefined ) { o.active = o.hover; }; // 对菜单对象的_children集合排序(先按rank再按id排序) o._children.sort( function ( x, y ) { return x.rank - y.rank || x.id - y.id; }); }); }, // 插入菜单 InsertMenu: function (index, parent) { var container = this ._containers[index]; // 如果是同一个父级菜单不用重复插入 if ( container._parent === parent ) { return ; }; container._parent = parent; // 把原有容器内菜单移到碎片对象中 forEach( container._menus, function (o) { o._elem && this ._frag.appendChild(o._elem); }, this ); // 重置子菜单对象集合 container._menus = []; // 把从父级菜单元素的子菜单对象集合获取的元素插入到容器 forEach( this ._menus[parent]._children, function ( menu, i ){ this .CheckMenu( menu, index ); // 检查菜单 container._menus.push(menu); // 加入到容器的子菜单集合,方便调用 container.Menu.appendChild(menu._elem); // 菜单元素插入到容器 }, this ); }, // 检查菜单 CheckMenu: function (menu, index) { // 索引保存到菜单对象属性中,方便调用 menu._index = index; // 如果菜单对象没有元素 if ( ! menu._elem ) { var elem = menu.elem; // 如果没有自定义元素的话创建一个 if ( ! elem ) { elem = document.createElement(menu.tag); elem.innerHTML = menu.txt; }; // 设置property Extend( elem, menu.property ); // 设置attribute var attribute = menu.attribute; for ( var att in attribute) { elem.setAttribute( att, attribute[att] ); }; // 设置样式 elem.className = menu.css; // 设置事件 menu._event = BindAsEventListener( this , this .HoverMenu, menu ); // 用于清除事件 addEvent( elem, " mouseover " , menu._event ); // 保存到菜单对象 menu._elem = elem; }; }, // 触发菜单 HoverMenu: function (e, menu) { var elem = menu._elem; // 如果是内部元素触发直接返回 if ( Contains( elem, e.relatedTarget ) || elem === e.relatedTarget ) { return ; }; clearTimeout( this ._timerMenu); // 可能在多个容器间移动,所以全部容器都重新设置样式 this .forEachContainer( function (o, i){ if ( o.Pos.visibility === " hidden " ) { return ; }; this .ResetCss(o); // 设置当前菜单为active样式 var menu = o._active; if ( menu ) { menu._elem.className = menu.active; }; }); // 设置当前菜单为触发样式 if ( this ._containers[menu._index]._active !== menu ) { elem.className = menu.hover; }; // 触发显示菜单 this ._timerMenu = setTimeout( Bind( this , this .ShowMenu, menu ), this .Delay ); }, // 显示菜单 ShowMenu: function (menu) { var index = menu._index, container = this ._containers[index], child = !! menu._children.length; // 隐藏不需要的容器 this .forEachContainer( function (o, i) { i > index && this .HideContainer(o); } ); // 重置当前容器_active container._active = null ; // 如果有子级菜单 if ( child ) { // 设置当前容器_active container._active = menu; // 显示下一级容器 index ++ ; // 设置索引 this .CheckContainer(index); // 检查容器 this .InsertMenu(index, menu.id); // 插入菜单 this .ShowContainer(menu); // 显示容器 }; // 重置当前容器的css this .ResetCss(container); // 设置当前菜单样式 menu._elem.className = child ? menu.active : menu.hover; }, // 初始化容器(索引, 容器元素) IniContainer: function (index, container) { var oContainer = container.Pos; // 重置属性 this .ResetContainer(container); // 添加事件 addEvent( oContainer, " mouseover " , Bind( this , function (){ clearTimeout( this ._timerContainer); } ) ); addEvent( oContainer, " mouseout " , BindAsEventListener( this , function (e){ // 先判断是否移出到所有容器之外 var elem = e.relatedTarget, isOut = Every( this ._containers, function (o){ return ! (Contains(o.Pos, elem) || o.Pos == elem); } ); if ( isOut ) { // 清除定时器并隐藏 clearTimeout( this ._timerContainer); clearTimeout( this ._timerMenu); this ._timerContainer = setTimeout( Bind( this , this .Hide ), this .Delay ); }; })); // 除了第一个容器外设置浮动样式 if ( index ) { var css = container.Pos.style; css.position = " absolute " ; css.display = " block " ; css.margin = 0 ; // 要后面的覆盖前面的 css.zIndex = this ._containers[index - 1 ].Pos.style.zIndex + 1 ; }; // 记录索引,方便调用 container._index = index; // 插入到容器集合 this ._containers[index] = container; }, // 检查容器 CheckContainer: function (index) { if ( index > 0 && ! this ._containers[index] ) { // 如果容器不存在,根据前一个容器复制成新容器,第一个容器必须自定义 var pre = this ._containers[index - 1 ].Pos // 用了新的添加事件方式,没有ie的cloneNode的bug ,container = pre.parentNode.insertBefore( pre.cloneNode( false ), pre ); // 清除id防止冲突 container.id = "" ; // 初始化容器 this .IniContainer( index, { Pos: container, Menu: container } ); }; }, // 显示容器 ShowContainer: function (menu) { var index = menu._index, container = this ._containers[index + 1 ].Pos, elem = menu.fixedmenu ? menu._elem : this ._containers[index].Pos, pos = GetRelative( elem, container, menu.fixed ); // 定位并显示容器 container.style.left = pos.Left + " px " ; container.style.top = pos.Top + " px " ; // 执行显示前事件 this .onBeforeShow(container, menu); container.style.visibility = " visible " ; }, // 隐藏容器 HideContainer: function (container) { // 设置隐藏 var css = container.Pos.style; css.visibility = " hidden " ; css.left = css.top = " -9999px " ; // 重置上一个菜单的触发菜单对象 this ._containers[container._index - 1 ]._active = null ; }, // 重置容器对象属性 ResetContainer: function (container) { container._active = null ; // 重置触发菜单 container._menus = []; // 重置子菜单对象集合 container._parent = - 1 ; // 重置父级菜单id }, // 隐藏菜单 Hide: function () { this .forEachContainer( function (o, i){ if ( i === 0 ) { // 如果是第一个重设样式和_active this .ResetCss(o); } else { // 隐藏容器 this .HideContainer(o); }; }); }, // 重设容器菜单样式 ResetCss: function (container) { forEach( container._menus, function (o, i){ o._elem.className = o.css; }, this ); }, // 历遍菜单对象集合 forEachMenu: function (callback) { for ( var id in this ._menus ) { callback.call( this , this ._menus[id], id, this ._menus ); }; }, // 历遍容器对象集合 forEachContainer: function (callback) { forEach( this ._containers, callback, this ); }, // 添加自定义菜单 Add: function (menu) { this ._custommenu = this ._custommenu.concat(menu); this .Ini(); }, // 修改自定义菜单 Edit: function (menu) { forEach( isArray( menu ) ? menu : [ menu ], function (o){ // 如果对应id的菜单存在 if ( o.id && this ._menus[o.id] ) { // 从自定义菜单中找出对应菜单,并修改 Every( this ._custommenu, function (m, i){ if ( m.id === o.id ) { this ._custommenu[i] = DeepExtend( m, o ); return false ; }; return true ; // 用Every可以跳出循环 }, this ); }; }, this ); this .Ini(); }, // 删除自定义菜单 Delete: function () { var ids = Array.prototype.slice.call(arguments); this ._custommenu = Filter( this ._custommenu, function (o){ return IndexOf(ids, o.id) === - 1 ; }); this .Ini(); } };
完整实例下载
转载请注明出处:http://www.cnblogs.com/cloudgamer/
Tag标签: JavaScript,fixed,菜单,多级联动,浮动定位,Menu,内存泄漏,京东商城,淘宝拼音索引