浮动层菜单

浮动层菜单

分类: 客户端技术 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/