- 浏览: 61022 次
- 性别:
- 来自: 顺德
最新评论
-
liuxf1122:
不支持最新的IE9、10和11!修改了也不行,不知如何是好啊! ...
JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布 -
gongyeye:
$$E.addEvent( this._container, ...
JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布 -
kingliu:
很好,很强大,学习了
JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布 -
lwkjob:
非常强大谢谢
图片延迟加载(按需加载)效果 -
zxh277100963:
判断ie版本 错误,我明细是IE8的判断出来是IE7
JavaScript 工具库:Cloudgamer JavaScript Library v0.1 发布
上一个版本(第一版请看这里)基本实现了多级联动和浮动菜单的功能,但效果不是太好,使用麻烦还有些bug,实用性不高。
这次除了修改已发现的问题外,还对程序做了大幅调整和改进,使程序实用性更高,功能更强大。
程序原理
程序最关键的地方是多级联动,先大概说明一下:
首先第一级的菜单元素整理好后,从他们开始,当某个菜单元素触发显示下级菜单时,
准备好下一级的容器元素,并把下一级的菜单元素放进去,再定位并显示容器元素。
里面的菜单元素又可以触发显示下级菜单,然后按上面的步骤执行下去。
这样一级一级的递推下去,形成多级的联动菜单。
程序说明
【容器对象】
在多级联动中,每一级都需要一个容器元素来存放菜单元素。
程序中每个容器元素都对应一个容器对象,用来记录该容器的相关信息。
容器对象的集合记录在程序的_containers属性中。
容器参数containers是程序实例化时的必要参数,它的结构如下:
[
容器元素(id),
{ id: 容器元素(id), menu: 插入菜单元素(id) },
]
首先如果containers不是数组的话,程序会自动转成单元素数组。
如果菜单插入的元素就是容器元素本身,可以直接用容器元素(id)作为数组元素。
否则应该使用一个对象结构,它包括一个id属性表示是容器元素(id)和一个menu属性表示菜单插入的元素(id)。
containers会在程序初始化时这样处理:
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重置容器对象可能在程序中设置过的属性。
再给容器元素添加事件:
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作为字典关键字的对象。
首先创建带根菜单(“0”id)对象的_menus:
this._menus = { "0": { "_children": [] } };
然后整理_custommenu并插入到_menus中:
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的菜单。
在重置_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程序中,会对这些样式属性进行整理:
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测试下面的代码:
<!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:
<!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就能获得对象相对文档的位置坐标。
测试下面代码:
<!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初始化程序,效率较低,一般来说尽量不要使用。
程序最关键的地方是多级联动,先大概说明一下:
首先第一级的菜单元素整理好后,从他们开始,当某个菜单元素触发显示下级菜单时,
准备好下一级的容器元素,并把下一级的菜单元素放进去,再定位并显示容器元素。
里面的菜单元素又可以触发显示下级菜单,然后按上面的步骤执行下去。
这样一级一级的递推下去,形成多级的联动菜单。
程序说明
【容器对象】
在多级联动中,每一级都需要一个容器元素来存放菜单元素。
程序中每个容器元素都对应一个容器对象,用来记录该容器的相关信息。
容器对象的集合记录在程序的_containers属性中。
容器参数containers是程序实例化时的必要参数,它的结构如下:
[
容器元素(id),
{ id: 容器元素(id), menu: 插入菜单元素(id) },
]
首先如果containers不是数组的话,程序会自动转成单元素数组。
如果菜单插入的元素就是容器元素本身,可以直接用容器元素(id)作为数组元素。
否则应该使用一个对象结构,它包括一个id属性表示是容器元素(id)和一个menu属性表示菜单插入的元素(id)。
containers会在程序初始化时这样处理:
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重置容器对象可能在程序中设置过的属性。
再给容器元素添加事件:
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作为字典关键字的对象。
首先创建带根菜单(“0”id)对象的_menus:
this._menus = { "0": { "_children": [] } };
然后整理_custommenu并插入到_menus中:
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的菜单。
在重置_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程序中,会对这些样式属性进行整理:
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测试下面的代码:
<!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:
<!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就能获得对象相对文档的位置坐标。
测试下面代码:
<!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初始化程序,效率较低,一般来说尽量不要使用。
发表评论
-
AlertBox 弹出层(信息提示框)效果
2010-10-11 11:06 1203弹出层就是相对文档或窗口定位的一个层,一般用来显示提示信息、广 ... -
JavaScript 图片3D展示空间(3DRoom)
2010-09-20 09:59 1345一般的平面效果,通过 ... -
ccs3/滤镜/canvas的图片旋转/缩放/翻转变换效果
2010-08-21 13:48 1649以前要实现图片的旋转或翻转,只能用ie的滤镜来实现,虽然can ... -
SlideView 图片滑动(扩展/收缩)展示效果
2010-07-30 08:52 1251滑动展示效果主要用在 ... -
ImageZoom 图片放大效果(扩展篇)
2010-04-16 21:32 1249上一篇ImageZoom已经对图片放大效果做了详细的分析,这次 ... -
ImageZoom 图片放大效果
2010-04-07 21:03 1266这个效果也叫放大镜效果,最早好像在ppg出现的,之后就有了很多 ... -
图片延迟加载(按需加载)效果
2010-03-11 21:13 2854之前在做一个图片浏览效果时,要看后面的小图必须等到前面的加载完 ... -
Lazyload 延迟加载(缓载)效果
2010-02-22 09:58 1550Lazyload是通过延迟加载 ... -
JavaScript 图片滑动展示效果
2008-05-13 01:34 1715看到jQuery实例:图片展示效果后,我也想拿来试试,但我不太 ... -
JavaScript 图片变换效果(ie only)
2008-05-23 12:02 2307仿照常见的那个图片变换flash做的效果,纯js。不过滤镜变换 ... -
JavaScript 图片滑动切换效果
2008-07-06 01:25 3187序一(08/07/06) 看到alibaba的一个图片切换效 ... -
JavaScript blog式日历控件
2008-08-23 00:25 1064近来要做一个记事本系统,想找一个合适的日历控件,但网上的都是那 ... -
JavaScript Table排序
2008-10-06 08:24 1082程序的实现的是在客户 ... -
JavaScript 日期联动选择器
2008-10-28 10:30 1252一个日期联动选择器, ... -
JavaScript 拖拉缩放效果
2008-12-03 09:07 788拖拉缩放效果,实现通过鼠标拖动来调整层的面积(宽高)大小。例如 ... -
JavaScript 滑动条效果
2008-12-24 08:27 2495这个滑动条(拖动条) ... -
JavaScript Tween算法及缓动效果
2009-01-06 09:17 1182Flash做动画时会用到Tween类,利用它可以做很多动画效果 ... -
JavaScript 颜色梯度和渐变效果
2009-03-11 08:24 1070近来看了Dean的“Convert any colour va ... -
JavaScript Table行定位效果
2009-05-18 14:02 1034近来有客户要求用table显示一大串数据,由于滚动后就看不到表 ... -
JavaScript 浮动定位提示效果
2009-07-07 08:23 1119本来想做一个集合浮动 ...
相关推荐
JavaScript多级联动浮动菜单 第二版,包含仿京东商城商品分类菜单、仿window xp右键菜单、仿淘宝拼音索引菜单,上一个版本基本实现了多级联动和浮动菜单的功能,但效果不是太好,使用麻烦还有些bug,实用性不高。...
JavaScript多级联动浮动菜单是一种常见的网页交互设计,用于创建具有层次结构的导航菜单。这种菜单在用户滚动页面时会保持固定在屏幕的某个位置,提供便捷的导航。在这个项目中,我们将深入探讨JavaScript如何实现...
总结,创建JavaScript自定义多级联动浮动菜单涉及到以下几个关键步骤: 1. 获取和操作DOM元素。 2. 添加事件监听器,响应用户交互。 3. 修改CSS样式以实现显示、隐藏和浮动效果。 4. 维护数据结构,实现菜单的联动。...
JavaScript(简称JS)是一种轻量级的解释型编程语言,广泛应用于网页和网络应用开发,实现客户端的...通过学习和理解这些知识点,你将能够创建自己的JavaScript多级联动浮动菜单,为网站或应用增加互动性和用户体验。
"javascript html js自定义多级联动浮动菜单,js制作菜单"这个标题所指的就是如何使用JavaScript、HTML和CSS技术来构建一个多级联动的浮动菜单。这种菜单在网页应用中常见于导航系统,能够提供层次分明的导航体验,...
1. **JavaScript/jQuery**:通过添加JavaScript或jQuery脚本,可以实现更复杂的互动效果,如动画过渡、延迟显示、多级下拉等。例如,使用jQuery的`.slideToggle()`方法可以实现平滑的展开和关闭效果。 2. **事件...
本篇文章将深入探讨如何使用Jquery实现多级联动菜单,以及`FloatMenu(2)`这个示例可能包含的内容。 首先,我们要理解什么是联动菜单。联动菜单,也称为下拉菜单或级联菜单,通常是指当用户选择某个菜单项时,会弹出...
总的来说,四级联动菜单是网页开发中的一种实用技术,通过HTML、CSS和JavaScript的配合,能够在多种现代浏览器上提供流畅的多级导航体验,但需要注意对旧版浏览器(如IE8及以下)的兼容性问题。在实际应用中,开发者...
同时,为了实现联动效果,需要在JavaScript或者jQuery中添加事件监听器,如点击事件,当用户选择某一菜单项时,动态加载对应的子菜单。 4. JavaScript/jQuery处理:在客户端,我们可以利用JavaScript或者jQuery库来...
本主题关注的是一个基于 jQuery 的纵向滑动菜单,它提供了二级或三级的联动效果,适用于构建层次分明的导航结构。 一、jQuery 纵向滑动菜单的基本概念 纵向滑动菜单是一种常见的网页导航组件,通常垂直布局,用户...
3. 多级联动:根据省份筛选城市,当选择省份时,只显示该省份的城市,减少用户的选择压力。 4. 键盘操作支持:使弹出层支持键盘导航,如Tab键切换焦点,Enter键确认选择,Esc键关闭弹出层。 四、实际应用 在网页...
在三级联动中,一级选择会触发二级下拉框的更新,二级选择则会触发三级下拉框的更新。 3. **数据结构**:为了实现联动效果,通常需要有一个包含省、市、区的数据结构,这可以是JSON对象或者数组。每个层级的数据应...
5. **层和框架的特效**:JavaScript可以控制页面上的浮动层(div)或框架(iframe)的显示、隐藏、移动和缩放,比如模态对话框的弹出与关闭。使用`getBoundingClientRect()`可以获取元素的位置信息,`style`属性则...
1. **全国省市县无刷新多级关联菜单.txt**:这是一个实现省市区三级联动的菜单,无需页面刷新即可展示各级别地区信息。用户在选择一个省份时,相应的市和县会自动更新,这种功能在地址输入或地理位置选择的场景中...
7. **Menu**:菜单组件可以创建多级下拉菜单,用于组织功能项和快捷操作。 8. **其他组件**:还包括按钮(button)、提示(tooltip)、日期选择器(datebox)、时间选择器(timespinner)等组件,以及动画和辅助...
jQuery Easy UI 提供了栅格布局(Layout)和浮动布局(Box),可以帮助开发者快速创建响应式和自适应的页面结构。 4. **组件间的交互**: 通过事件监听和回调函数,可以实现组件间的交互。例如,当点击表格的一行...
05 js练习之二级联动 06 jquery以及jquery对象介绍 07 jquery选择器 08 jquery的查找筛选器 09 jquery练习之左侧菜单 第44章 01 jquery属性操作之html,text,val方法 02 jquery循环方法和attr,prop方法 03 jquery...
弹出层是在网页主内容之上显示的一个浮动窗口,它可以用来展示详细信息、提示、表单、图片或其他互动组件。其主要目的是在不中断用户对主要页面浏览的情况下,提供一个临时的交互空间,增加用户的沉浸感和操作便捷...