浮动层菜单
分类: 客户端技术 2009-01-16 16:49:00 阅读(2593)
最近做了一个通用的浮动层菜单替代函数,可以方便地把传统的 SELECT 单选菜单替换成时尚的浮动层菜单。
源码
DivMenu.css
a.divmenu, a.divmenu:hover {
padding: 1px 1px 1px 3px;
border: 1px solid #9999CC;
color:#000;
text-decoration:none;
cursor:default;
}
/*提示文字条上的文字样式,a 是行间元素,需要 display:inline-block 才能在程序中给它设置宽度 */
a.divmenu span.menuhint {
display:inline-block;
}
/*提示文字条上的箭头*/
a.divmenu span.menuarrow {
color: #000;
}
/*鼠标悬停时的样式
IE6处理CSS伪类:hover的Bug,在 IE 6 中必须让 a:hover 和 a 有明确的不同定义,才能让 a:hover span 生效,所以可根据实际情况加个不影响效果的新定义。
*/
a.divmenu:hover
{
color:#fff;
border: 1px solid #33f;
background-color: #36f;
}
/*IE 6 中不灵,解决办法见上面 a.divmenu:hover */
a.divmenu:hover span {
color: #fff;
}
/*浮动菜单层的样式。设成 display:inline-block; 好让这个层的宽度能自动适应内部文字,而不是全窗宽度。但是 IE 中不灵,还是全窗宽度,display:table-cell; 也不行。*/
.divmenu_panel {
position: absolute;
display: none;
z-index: 9999;
width: 100px;
border: 1px solid #999999;
background-color: #fff;
}
/*菜单层中替代原有选项的链接的样式。用 cursor:default; 是为了恢复和传统菜单一样的鼠标指针,而不是用链接时的手形指针。 display:block; 让 a 自成一行,不用再写其它标记了。*/
.divmenu_panel a {
cursor:default;
display:block;
color:#000;
text-decoration:none;
}
.divmenu_panel a:hover {
color:#399;
background-color: #f99;
background-color: #BBDDFF;
color: #3366FF;
}
/*选项分组带的样式*/
.divmenu_group {
display:block;
background-color: #9cf;
}
DivMenu.js
//因为替换出来的链接上要使用全局的事件处理函数,所以这个程序想用完全匿名函数还不太方便,先定义一个全局对象吧
//先把一个简单的注册事件处理函数的对象封装在这里
var DivMenu =
{
//兼容 IE 和 FF 的事件注册。这个对象里没用到注销事件,先不写了。
add : function(element, eventType, handler)
{
if (document.addEventListener)
{
element.addEventListener(eventType, handler, false);
}
else if (document.attachEvent)
{
element.attachEvent("on" + eventType, handler);
};
},
//取页面元素相对窗口的绝对位置
getAbsPoint : function (e)
{
var x = e.offsetLeft;
var y = e.offsetTop;
while(e = e.offsetParent)
{
x += e.offsetLeft;
y += e.offsetTop;
}
return {"x": x, "y": y};
},
//show 的这个参数是最初 SELECT 表单项的 id 如 city,而找位置要找替换 SELECT 的 a 元素 如 city_a,它显示出的菜单层则是 city_div
show : function (sourceId)
{
var sourceobj = document.getElementById(sourceId+'_a');
var panelId = "divmenu_panel";
var panel= document.getElementById(panelId);
if (!panel)
{
panel= document.createElement("div");
panel.className = "divmenu_panel";
panel.id=panelId;
document.body.appendChild(panel);
}
panel.innerHTML = document.getElementById(sourceId+'_div').innerHTML;
//FF 中还得再加上位于层上时要显示此层,否则会在移到菜单层上时又隐藏了。到目前不需要 FF 中使用 contains 方法了,所以把前面页给 FF 定义的 contains 方法也注释掉。
//panel.setAttribute('onmouseover',"DivMenu.show('" + sourceId +"');");
//和发起元素左对齐
var xy = this.getAbsPoint(sourceobj);
panel.style.left = xy.x + "px";
//纵向加发起元素的高度
panel.style.top = (xy.y + sourceobj.offsetHeight) + "px";
panel.style.display = "block";
if (navigator.appName == "Microsoft Internet Explorer")
{
//为解决 IE6 下 SELECT 挡住 DIV 层的问题,加个 IFRAME 层
var iframeId = "divmenu_panel_iframe";
var iframe_dom = document.getElementById(iframeId);
if(!iframe_dom) //不存在 自动生成 iframe
{
var tmpIframeDom = document.createElement("IFRAME");
tmpIframeDom.id = iframeId;
document.body.appendChild(tmpIframeDom);
iframe_dom = document.getElementById(iframeId);
iframe_dom.src = "about:blank"; //javascript:void(0); about:blank
iframe_dom.style.position = "absolute";
iframe_dom.style.scrolling = "no";
iframe_dom.style.frameBorder = 0;
//iframe_dom.style.backgroundColor = "#ff0000"; //加个背景色只为调试用
}
//隐藏层的方式 hide() 会把此 IFRAME 设置成 display = "none",所以每次显示菜单层时要再把 IFRAME 设置成 display = "block"。
iframe_dom.style.display = "block";
//再定成和菜单层一样的宽和高及坐标
iframe_dom.style.width = panel.offsetWidth;
iframe_dom.style.height = panel.offsetHeight;
iframe_dom.style.top = panel.style.top;
iframe_dom.style.left = panel.style.left;
//原来是 panel.style.zIndex - 1,但我这里取不到 panel.style.zIndex 值,只好手工定成 9998 吧,反正比 panel.style.zIndex 小1即可
iframe_dom.style.zIndex = 9998;
}
},
//隐藏菜单
hide : function()
{
if (document.getElementById('divmenu_panel'))
{
document.getElementById('divmenu_panel').style.display = "none";
}
//为解决 IE6 下 SELECT 挡住 DIV 层的问题,加个 IFRAME 层,隐藏菜单时还得把这个 IFRAME 也隐藏了
if (navigator.appName == "Microsoft Internet Explorer")
{
var iframeId = "divmenu_panel_iframe";
var iframe_dom = document.getElementById(iframeId);
if(iframe_dom) //确实存在时再隐藏
{
iframe_dom.style.display = "none";
}
}
},
//选中某项时,把替代 select 的给 hidden 项的赋上值,再把 a 中的文本换掉
setInput : function(itemid, hint, val)
{
var formitem = document.getElementById(itemid);
formitem.value = val;
var nodeHint = document.getElementById(itemid+'_a');
//一个 a 元素形如 <a id="purpose_a" class="divmenu" href='javascript:DivMenu.show("purpose")'><span style="width: 6em;" class="menuhint">写字楼</span><span>▼<input type="hidden" id="purpose" name="purpose" class="" value=""/></span></a>,替换显示文字是把 a 的第1个子节点的文本换了,所以是
var nodeHint = nodeHint.firstChild; //取第1子节点,这时 nodeHint 是 <span style="width: 6em;" class="menuhint">写字楼</span> 了
var newHint = document.createTextNode(hint); //用新的显示文字创建文本节点
nodeHint.replaceChild(newHint, nodeHint.firstChild); //用 nodeHint 替换掉它的第1子节点,即“写字楼”这个文本节点
},
//计算字符串的字节长度
byteLength : function (s)
{
var len = 0;
for (i = 0; i < s.length; i++)
{//根据字符编码决定给总字节数加1或2,问题是 charCodeAt 返回的是 Unicode 编码,究竟哪个 Unicode 编码范围是只计作1个字节的呢,基本上是 \x00-\xff,即 0 - 255 这些字符是1字节的。其它还有不少讲究,比如听说还有3字节的字符,但大多不会在用户输入时写的出。
len += (s.charCodeAt(i) < 256) ? 1:2;
}
return len;
},
//菜单变换的函数
menuTransform : function ()
{
var debug = '';
//所有菜单存在一个数组里
var menuSelects = [];
var arrMenu = document.getElementsByTagName('select');
for (var i=0; i<arrMenu.length; i++)
{
//只把单选菜单替换,所以找那些是单选的菜单
//http://alex.zybar.net/javascript/IE/IE 确实不支持 hasAttribute() 和 hasAttributes() 这2个 DOM Level 2 的方法,可以用 attributes.length > 0 替代 hasAttributes(), getAttribute(attrName) != null 替代 hasAttribute(attrName)。但是对于 multiple 这样没有值的属性,它不存在时 FF 返回的是 null 而 IE 返回的是 false,(它存在时 FF 返回空字符串,IE 返回 true)只好再凑合一下,还得用严格等于===,否则还不灵。对于 multiple 或 multiple="multiple" 都可行
var getMultiple = arrMenu[i].getAttribute('multiple');
if ((null === getMultiple) || (false === getMultiple))
{
var nodesOption = arrMenu[i].childNodes;
var menuName = arrMenu[i].getAttribute('name');
//整个菜单的提示条,用于最初替换菜单的 a 元素内的文字
var strHint='';
var valSelected = null;
var objOptions = [];
//当前菜单的信息存为一个对象
var objSelect = {};
for (var j=0; j<nodesOption.length; j++)
{
//通常在选项组内的选项不会用来作提示条,所以这选项组内的循环就不处理提示条了
if ('OPTGROUP'== nodesOption[j].nodeName)
{
objOptions.push({'hint':nodesOption[j].getAttribute('label'), 'value':null, 'type':'OPTGROUP'});
var nodesGroup = nodesOption[j].childNodes;
for (var k = 0;k<nodesGroup.length;k++)
{
if ('OPTION'== nodesGroup[k].nodeName)
{
var hintThis = nodesGroup[k].firstChild.data;
var valueThis = nodesGroup[k].getAttribute('value');
var selectedThis = nodesOption[j].getAttribute('selected');
if (('selected' == selectedThis) || (true === selectedThis))
{
strHint = hintThis;
valSelected = valueThis;
}
objOptions.push({'hint':hintThis, 'value':valueThis, 'type':'OPTION'});
}
}
}
else if ('OPTION'== nodesOption[j].nodeName)
{
var hintThis = nodesOption[j].firstChild.data;
var disabledThis = nodesOption[j].getAttribute('disabled');
var selectedThis = nodesOption[j].getAttribute('selected');
var valueThis = nodesOption[j].getAttribute('value');
//alert(selectedThis);
//值为空字符串的应该算作选项,而不是提示条,所以条件里不加 || ('' == valueThis)
if (('disabled' == disabledThis) || (true === disabledThis) || (null == valueThis))
{
strHint = hintThis;
}
else
{
//如果有默认选中项,把它的显示文字作提示条
if (('selected' == selectedThis) || (true === selectedThis) || ('' === selectedThis))
{
strHint = hintThis;
valSelected = valueThis;
}
objOptions.push({'hint':hintThis, 'value':valueThis, 'type':'OPTION'});
}
}
}
//如果菜单没有定义用作提示语的的选项,就把第一个选项作为提示语
if ('' == strHint)
{
strHint = objOptions[0].hint;
}
//而菜单层则直接生成节点添加到整个文档上
var nodeDiv = document.createElement('div');
nodeDiv.id=menuName+'_div';
//生成菜单层备用,设置成不可见的样式
nodeDiv.style.display='none';
//用于组装 innerHTML 的临时字符串
var str = '';
//先记下提示条的字节数,下面组装选项时再逐个比较选项显示文字的字节数,取最大值,最终确定提示条 a 的宽度
var strLength = DivMenu.byteLength(strHint);
for (n=0; n<objOptions.length;n++)
{
if ('OPTGROUP' == objOptions[n].type)
{
str += '<div class="divmenu_group">'+objOptions[n].hint+'</div>';
}
else if('OPTION' == objOptions[n].type)
{
str += '<a href="javascript:DivMenu.setInput("'+menuName+'", "'+objOptions[n].hint+'", "'+objOptions[n].value+'")">'+objOptions[n].hint+'</a>';
}
//找出显示文字中最长的字符串,来定 a 的宽度。JavaScript 数字符数时一个中文算1个字符,但是中文宽度比西文宽度大得多,所以通过数字数来定宽度不好。试直接比较 a 和创建的菜单层的宽度,二者取大值即可。但直接获取 ComputedStyle 也不灵,这些菜单 div 都是隐藏和绝对定位的,它的宽度IE 中ComputedStyle返回的是 auto,而 FF 返回的是整个窗口的宽度。但实际上,除了等宽型字体,每个西文字符的宽度也不一样,所以很难通过字符数来确定其宽度,传统 SELECT 是浏览器取到所有 option 的文字后解析并呈现的,而现在替换它成为 a 和 div 两个元素,确实有些麻烦。现在采用的是比较保险的做法,算出最大字符数 strLength ,然后设 width:'+strLength*0.6+'em,其中 0.6 只是经验系数,直接 width:'+strLength+'em过于宽了,再按 60% 缩小。
var thisLength = DivMenu.byteLength(objOptions[n].hint);
if (thisLength > strLength)
{
strLength = thisLength;
}
}
nodeDiv.innerHTML = str;
document.body.appendChild(nodeDiv);
//做替代 select 的 a 节点,并把它存在替换数组里,将用这个包括隐藏 input 的 a 替换掉原有的 select 节点
var nodeHint = document.createElement('a');
nodeHint.id=menuName+'_a';
nodeHint.className='divmenu';
nodeHint.setAttribute('href', 'javascript:DivMenu.show("'+menuName+'")');
str = '<span class="menuhint" style="width:'+strLength*0.6+'em">'+strHint+'</span><span class="menuarrow">▼<input type="hidden" name="'+menuName+'" id="'+menuName+'"'+((null == valSelected)? '':' value="'+valSelected+'"')+'></span>';
nodeHint.innerHTML = str;
//先把新旧节点存在对象数组里,以备后面统一替换
var menuthis = {'node':arrMenu[i],'nodeNew':nodeHint};
menuSelects.push(menuthis);
}
}
for (var i=0; i<menuSelects.length; i++)
{
menuSelects[i].node.parentNode.replaceChild(menuSelects[i].nodeNew, menuSelects[i].node);
}
}
};
//在文档加载完毕后执行上述菜单变换的函数
DivMenu.add(window, 'load', DivMenu.menuTransform);
//传统菜单在点开时是占住焦点的,再在文档空白处点击一下才会收起菜单,所以完全重现传统菜单的使用习惯,也点一下文档再隐藏菜单
DivMenu.add(document, 'click', DivMenu.hide);
DivMenu.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312" />
<title>浮动层菜单</title>
<script type="text/javascript" src="DivMenu.js"></script>
<link href="DivMenu.css" rel="stylesheet" type="text/css" />
</head>
<body>
<h1>浮动层菜单</h1>
<p>这是一个通用的浮动层菜单替代函数,可以方便地把传统的 SELECT 单选菜单替换成时尚的浮动层菜单。</p>
<h3>使用说明</h3>
<p>即仅需把附件中的 JS 文件和 CSS 文件引用到网页里,如:</p>
<pre><script type="text/javascript" src="DivMenu.js"></script>
<link href="DivMenu.css" rel="stylesheet" type="text/css" /> </pre>
<h4><a href="DivMenu.zip">下载源码打包</a></h4>
<p>其中 DivMenu.js 是经 Javascript compressor 压缩的精简版本,供生成实际使用。DivMenu_develop.js 是带详细注释的开发版,供学习研究。使用时可以参考 DivMenu.css 文件中的注释,自行修改样式定义。</p>
<p>JavaScript 程序已经实现免打扰(unobtrusive),即引用此 JS 文件的 HTML 文件无需任何其它调整。但还未实现完全匿名(anonymous),意思是此程序仍然创建了全局变量和若干网页节点,已经尽量做到少增加全局变量和节点,但使用时仍请注意变量命名冲突。此程序仅增加一个全局变量“DivMenu”,增加的网页节点比较多,对应替换掉的每个 SELECT 节点增加两个节点,名字分别是 SELECT 节点的 name 值加“_a”后缀和“_div”后缀,例如原有某 SELECT 节点的名字是“menu”,则新增的两个节点名字是“menu_a”和“menu_div”。</p>
<h3>功能说明</h3>
<form id="classic" method="post" action="">
<div> 城市
<select name="city">
<option disabled="disabled">请选择城市</option>
<option value="beijing">北京</option>
<option value="tianjin">天津</option>
<optgroup label="河北省">
<option value="shijiazhuang">石家庄</option>
<option value="tangshan">唐山</option>
</optgroup>
<optgroup label="浙江省">
<option value="suzhou">苏州</option>
<option value="hangzhou">杭州</option>
<option value="ningbo">宁波</option>
</optgroup>
</select>
类型
<select name="purpose">
<option>请选择类型</option>
<option value="house" selected="selected" >住宅</option>
<option value="economic">经济适用房</option>
<option value="villa">别墅</option>
<option value="building">写字楼</option>
<option value="shop">商铺</option>
</select>
价格
<select name="price">
<option selected="selected" disabled="disabled">请选择价格</option>
<option value="0-1000">零到1000</option>
<option value="1000-2000">1000-2000</option>
<option value="2000-3000">2000-3000</option>
<option value="3000-4000">3000-4000</option>
<option value="4000-5000">4000-5000</option>
<option value="5000-6000">5000-6000</option>
<option value="6000-7000">6000-7000</option>
<option value="7000-8000">7000-8000</option>
<option value="8000-9000">8000-9000</option>
<option value="9000-10000">9000-10000</option>
<option value="" selected="selected">不限</option>
</select>
<select name="nohint">
<option value="0">零</option>
<option value="1000-2000">1000-2000</option>
<option value="2000-3000">2000-3000</option>
<option value="3000-4000">3000-4000</option>
<option value="4000-5000">4000-5000</option>
<option value="5000-6000">5000-6000</option>
<option value="6000-7000">6000-7000</option>
<option value="7000-8000">7000-8000</option>
<option value="8000-9000">8000-9000</option>
<option value="9000-10000">9000-10000</option>
<option value="10000-11000">10000-11000</option>
</select>
多选菜单不转换,多选菜单中的提示文字只能用 disabled="disabled" 来实现了。
<select name="multi" multiple="multiple">
<option disabled="disabled">若要选择多个,请住 Ctrl 键再选择</option>
<option value="house">多选1</option>
<option value="economic">多选2</option>
<option value="villa">多选3</option>
<option value="building">多选4</option>
<option value="shop">多选5</option>
</select>
放一个多选菜单,用来演示多选菜单不会被替换,以及在 IE6 中浮动层可以正常显示在其它 SELECT 以上了。 </div>
</form>
以上是个实用的范例,几组经典的 SELECT 选择菜单,第一个是复杂带分组的,提示文字用 disabled="disabled" 的第一个 option 实现,后一个是简单的,提示文字用的是没有 value 属性的那个 option。实践发现 FF 可以正常处理 disabled="disabled" 或者仅 disabled 的 option,让它可以不可选的状态,而 IE 则无视任何disabled="disabled" 和 disabled,所以估计通常 IE 里的菜单想用提示文字都会用没有 value 属性或者 value=""的那个 option。可以看它的源码就是原来的 SELECT,不需要任何改动。
<p>基本思路是把文档中原有的 SELECT 菜单转换成隐藏型的 INPUT 表单项,用 A 元素来做菜单的提示文字条和选项条,用 DIV 层做菜单,各个选项也用 A 元素来替代,点击时把值赋给隐藏型的 INPUT 表单项。</p>
<p>主要功能或限制包括:</p>
<ol>
<li>所有使用习惯遵循传统的 SELECT 菜单。</li>
<li>只有单选的 SELECT 菜单会被替换,多选菜单维持不变,因为通常多选菜单也不用浮动层菜单来替代。</li>
<li>支持各种 SELECT 元素的特性,如把 disabled 的或者没有 value 的 option 作为提示文字条;用 optgroup 分组的选项也会在新菜单中分组。并且专门针对 IE 6 中 SELECT 挡住 DIV 层的 BUG 进行了修正。</li>
<li>支持默认选中项。</li>
<li>几乎支持普通菜单的所有功能,但是不支持额外的交互功能,如 JavaScript 实时创建的 Option 选项、联动选项等,比如选一个城市区县会相应变化这类的功能是不支持的。</li>
<li>全部样式都用 CSS 定义,字号使用相对大小,无需改动即可适应多数网页。</li>
<li>菜单位置和尺寸均设置成灵活的,可适应任意的网页排版和布局。做的时候考虑的是要能在替代的 A 元素中显示时,放得下最宽的选项文字,因而没有直接取原有 SELECT 框的宽度。因此菜单尺寸可能比原有的 SELECT 宽,使用时请注意。</li>
</ol>
<p>欢迎试用,并提出意见建议,共用探讨改进。</p>
</body>
</html>
使用说明
即仅需把上述的 JS 文件和 CSS 文件引用到网页里,如:
其中 DivMenu.js 是经 Javascript compressor 压缩的精简版本,供生成实际使用。DivMenu_develop.js 是带详细注释的开发版,供学习研究。使用时可以参考 DivMenu.css 文件中的注释,自行修改样式定义。
JavaScript 程序已经实现免打扰(unobtrusive),即引用此 JS 文件的 HTML 文件无需任何其它调整。但还未实现完全匿名(anonymous),意思是此程序仍然创建了全局变量和若干网页节点,已经尽量做到少增加全局变量和节点,但使用时仍请注意变量命名冲突。此程序仅增加一个全局变量“DivMenu”,增加的网页节点比较多,对应替换掉的每个 SELECT 节点增加两个节点,名字分别是 SELECT 节点的 name 值加“_a”后缀和“_div”后缀,例如原有某 SELECT 节点的名字是“menu”,则新增的两个节点名字是“menu_a”和“menu_div”。
功能说明
基本思路是把文档中原有的 SELECT 菜单转换成隐藏型的 INPUT 表单项,用 A 元素来做菜单的提示文字条和选项条,用 DIV 层做菜单,各个选项也用 A 元素来替代,点击时把值赋给隐藏型的 INPUT 表单项。
主要功能或限制包括:
1. 所有使用习惯遵循传统的 SELECT 菜单。
2. 只有单选的 SELECT 菜单会被替换,多选菜单维持不变,因为通常多选菜单也不用浮动层菜单来替代。
3. 支持各种 SELECT 元素的特性,如把 disabled 的或者没有 value 的 option 作为提示文字条;用 optgroup 分组的选项也会在新菜单中分组。并且专门针对 IE 6 中 SELECT 挡住 DIV 层的 BUG 进行了修正。
4. 支持默认选中项。
5. 几乎支持普通菜单的所有功能,但是不支持额外的交互功能,如 JavaScript 实时创建的 Option 选项、联动选项等,比如选一个城市区县会相应变化这类的功能是不支持的。
6. 全部样式都用 CSS 定义,字号使用相对大小,无需改动即可适应多数网页。
7. 菜单位置和尺寸均设置成灵活的,可适应任意的网页排版和布局。做的时候考虑的是要能在替代的 A 元素中显示时,放得下最宽的选项文字,因而没有直接取原有 SELECT 框的宽度。因此菜单尺寸可能比原有的 SELECT 宽,使用时请注意。
原文链接: https://www.snowpeak.fun/cn/article/detail/menu_by_floating_layer/