一步步教你实现跨游览器的日期选择器
由于日历与日期选择器几乎是同一回事,日历部分的讲解我就不重复了,没有看过的朋友可以参看我这篇博文。不过日期选择器有一个难点,就是如何防止在IE6中被下拉开选择框盖住的问题。不但是日期选择器,所有弹出层都有这个问题。这个问题其实网上都有现成的答案注1,最流行的做法就是利用iframe元素了注2。iframe已IE6与IE7中罕有的能遮住select的windowless元素,我们的日期选择器的容器也是windowless元素,windowless元素可以通过z-index改变它们的层叠顺序。那么就先iframe盖住日期选择器可能出现的位置,然后再让日期选择器上出现就行了。由于是这部分代码是专为IE准备,我们也用了IE createElement() 的一个特殊用法,在创建元素的同时也创建其样式与属性,即:
var iframe = document.createElement("< iframe id='calendar_mask' style='left:"+l +";width:220px;filter:mask();position:absolute;top:"+t +"height:160px;z-index:1;'></ iframe >");//z-index不能为负数 |
讲解一下这段代码。id是为了后面我们销毁此元素准备的,width与height应该和日期选择器的尺寸差不多,大点也没所谓,就是不能小于它(防止select像幽灵一样穿墙而出!),top与left是为了准确定位于我们要填写的文本域的下方,为了应用这两个属性,我们就必须把其设置为定位元素,filter:mask()滤镜的作用是让iframe透明,通常大家都是用filter:alpha(opacity=0),没有什么差别。最后讲讲z-index了。这里IE有个bug——定位元素会产生一个新的stacking context注3,而不管你有没有显式地定义其z-index,并且从z-index的值为0开始。因此网上许多教程都以讹传讹,把z-index设为-1是行不通。这里我吃了不少苦头,大家得注意了。
为了方便我讲解, 我先把日期选择器的代码贴出来,它是基于我上一篇博文修改而来,还处于毛坯状态,我们一点点来改进吧。
<! doctype html> < html dir="ltr" lang="zh-CN"> < head > < meta charset="utf-8"/> < meta http-equiv="X-UA-Compatible" content="IE=Edge"> < title >跨游览器的JS日历</ title > < link rel="stylesheet" type="text/css" id="css" href="dateselector.css"> < script type="text/javascript" src="dateselector.js"></ stript > </ head > < body bgcolor="gray"> < div style="margin:40px;background:#D7FFF0;width:500px;height:400px"> < form > < input id="datepicker" value="1990-1-1" title="日历在俺右边" />< br /> < select > < option >看你能不能挡住我</ option > < option >看你能不能挡住我</ option > < option >看你能不能挡住我</ option > < option >看你能不能挡住我</ option > < option >看你能不能挡住我</ option > </ select > </ form > </ div > </ body > </ html > |
#jcalendar { width : 210px ; background : #E0ECF9 ; border : 1px solid #479AC7 ; float : left ; } #jcalendar span { float : left ; width : 210px ; height : 20px ; background : #479AC7 ; color : #f90 ; font-weight : bolder ; text-align : center ; } #jcalendar .week { background : #D5F3F4 ; color : gray ; } #jcalendar .current { background : #369 ; color : #fff ; } #jcalendar a { display : block ; float : left ; width : 30px ; height : 20px ; color : #000 ; line-height : 20px ; text-align : center ; text-decoration : none ; } #jcalendar tt { color : #000040 ; } #jcalendar .weekend { color : #f00 !important ; } #jcalendar a.day:hover { background : #99C3F6 ; } |
var Class = { create: function () { return function () { this .initialize.apply( this , arguments); } } } var extend = function (destination, source) { for ( var property in source) { destination[property] = source[property]; } return destination; } //******************************************** var Jcalendar = Class.create(); Jcalendar.prototype = { initialize: function (options){ this .setOptions(options); var $ = new Date(); this .drawCalendar($.getFullYear(),$.getMonth() + 1,$.getDate()); }, setOptions: function (options) { this .options = { //默认属性集中写在这里。 id: 'jcalendar_' + new Date().getTime(), text_id: null , //用于输入日期的文本域的ID parent_id: null //指定父节点 }; extend( this .options, options || {}); }, fillArray : function (year,month){ var f = new Date(year, month -1 ,1).getDay(), //求出当月的第一天是星期几 dates = new Date(year, month , 0).getDate(), //上个月的第零天就是今个月的最后一天 arr = new Array(42); //用来装载日期的数组,日期以‘xxxx-xx-xx’的形式表示 for ( var i = 0; i < dates ; i ++ ,f ++){ arr[f] = year + '-' + month + '-' + (i+1) ; } return arr; }, addToDom: function (calendar){ var parent = document.getElementById( this .options.parent_id) ||document.getElementsByTagName( 'body' )[0]; parent.insertBefore(calendar, null ); }, drawCalendar : function (year,month,date){ var $ = document,$$ = 'createElement' ,calendar = this .getHandle(), weeks = "日一二三四五六" .split( '' ), //日历第二行的内容,显示星期几 a = $[$$]( 'a' ), //日历的a元素,用于克隆 tt = $[$$]( "tt" ), //日历页眉的tt元素,用于克隆 thead = $[$$]( 'span' ), //日历页眉 fragment = $.createDocumentFragment(), //减少DOM刷新页面的次数 arr = this .fillArray(year,month), //保存当月的日期 tts = [], //用于保存tt元素的引用 text_id = this .options.text_id, //用于输入日期的文本域的ID ths = this ; //用于保存Jcalendar对象的实例的引用 if (calendar) { calendar.innerHTML = '' ; } else { calendar = $[$$]( 'div' ); //日历的容器元素 this .addToDom(calendar); //把日历加入DOM树中 calendar.setAttribute( 'id' , this .getId()); //设置ID } for ( var i = 0;i<4;i++){ //循环生成出个时间按钮。 var clone = tt.cloneNode( true ); //比重新createElement快 clone.onclick = ( function (index){ return function (){ //在闭包里绑定事件 ths.redrawCalendar(year,month,date,index) } })(i); tts[i] = clone; //保存引用 if (i==2) thead.appendChild($.createTextNode(year+ "年" +month+ "月" +date+ "日" )); thead.appendChild(clone); } tts[0].innerHTML = '<<' ; tts[1].innerHTML = ' <' ; tts[2].innerHTML = '> ' ; tts[3].innerHTML = '>>' ; fragment.appendChild(thead); for (i = 0;i <7;i++){ var th = a.cloneNode( true ); th.innerHTML = weeks[i]; th.className = 'week' ; fragment.appendChild(th); } for (i = 0;i <42;i++){ var td = a.cloneNode( true ); if (arr[i] == undefined ){ fragment.appendChild(td); } else { var html = arr[i].split( '-' )[2]; td.innerHTML = html; td.className = 'day' ; td.href = "javascript:void(0)" ; //为ie6准备的 (date && html == date)&&(td.className += ' current' ) ; (i%7 == 0 || i%7 == 6)&&(td.className += ' weekend' ) ; td.onclick = ( function (i){ return function (){ text_id &&($.getElementById(text_id).value = i); calendar.style.display = 'none' ; if (/msie|MSIE 6/.test(navigator.userAgent)){ var mask = document.getElementById( "calendar_mask" ); mask.parentNode.removeChild(mask); } } })(arr[i]); fragment.appendChild(td); } } calendar.appendChild(fragment); }, getId: function (){ //返回id return this .options.id; }, getHandle: function (){ //返回其容器,用于重设样式(显示或隐藏,定位等等) return document.getElementById( this .getId()); }, listenTo: function (id){ //对文本域进行监听 id = id || this .options.text_id; if (id && document.getElementById(id)){ var calendar = this .getHandle(), textfield = document.getElementById(id) calendar.style.display = 'none' ; textfield.onfocus = function (){ textfield.style.position = 'relative' ; var l = textfield.offsetLeft + 'px' , t = (textfield.clientHeight + textfield.offsetTop)+ 'px' ; if (/msie|MSIE 6/.test(navigator.userAgent)){ var iframe = document.createElement( "<iframe id='calendar_mask' style='left:" +l + ";width:300px;filter:mask();position:absolute;" + ";top:" +t + "height:160px;z-index:1;'></iframe>" ); //z-index不能为负数 textfield.parentNode.insertBefore(iframe,textfield); } with (calendar.style){ position= "absolute" ; display = 'block' ; zIndex = 100; left = l; top = t; } }; } }, redrawCalendar : function (year,month,date,index){ switch (index){ case 0 : //preyear year--; break ; case 1: //premonth month--; (month < 1) &&(year--,month = 12) ; break ; case 2: //nextmonth month++; (month > 12)&&(year++,month = 1) ; break ; case 3: //nextyear year++; break ; } this .drawCalendar(year,month,date); } } window.onload = function (){ new Jcalendar({ id: 'jcalendar' , text_id: 'datepicker' }).listenTo(); }; |
代码很长,去掉铺助函数,其主体部分还有146行,虽然有的一行就是定义一个变量,但变量太多也很影响效率,这是Pure DOM生成html元素的通病,虽然我们用了cloneNode与DocumentFragment提高渲染速度了……在IE过去最伟大的日子(IE5),它创造了一系列非常高效的方法——insertAdjacentElement, insertAdjacentHTML, insertAdjacentText, innerHTML, outerHTML, outerText, innerText, 还有上面createElement的那种方便用法……最常用的是innerHTML,几乎成为事实的标准,其他方法,最新的opera,chrome,safari都支持了!我们现在就是用innerHTML来改写我们的类。为了方便,我们还为document.ceateElement等操作DOM的方法定义了几个快捷方式,把它们弄成类的原型方法……
Jcalendar.prototype = { ID: function (id){ return document.getElementById(id) }, TN: function (tn){ return document.getElementsByTagName(tn) }, CE: function (s){ return document.createElement(s) }, //******************其他原型方法****************** |
这样drawCalenda就可以精简为:
drawCalendar : function (year,month,date){ var $ = this ,T= 'getElementsByTagName' , calendar = $.getHandle(), //日历的容器 weeks = "日一二三四五六" .split( '' ), //日历第二行的内容,显示星期几 arr = $.fillArray(year,month), //保存当月的日期 text_id = $.options.text_id; //用于输入日期的文本域的ID if (calendar) { calendar.innerHTML = '' ; } else { calendar = $.CE( 'div' ); $.addToDom(calendar); //把日历加入DOM树中 calendar.setAttribute( 'id' , $.options.id); //设置ID } calendar.innerHTML+= '<span><nobr><tt><<</tt><tt> <</tt>' + year+ '年' +month+ '月' +date + '日<tt>> </tt><tt>>></tt></nobr></span>' ; for ( var i = 0;i <7;i++){ calendar.innerHTML += '<a class="week">' +weeks[i]+ '</a>' ; } for (i = 0;i <42;i++){ calendar.innerHTML += (!arr[i]) ? '<a> </a>' :( '<a href="javascript:void(0)" title="' +arr[i]+ '" class="day' + ((arr[i].split( '-' )[2] == date)? ' current' : '' ) + ((i%7 == 0 || i%7 == 6)? ' weekend' : '' )+ '"><kbd>' +arr[i].split( '-' )[2]+ '</kbd></a>' ) } } |
这样不但看起来代码少很多,而且效率成几何级提高。另,注意两个地方,我们改写了重绘的方法,不是整个删除,而是保留div容器,然后设置其innerHTML为空,这样我们最初就只需创建一个DOM,其实元素都是用字符串拼接生成。在日历头部,我们把tt等元素全部放进nobr,这是因为在firefox3.5中,align为center的元素在频繁创建与删除时,内容会发生计算错误,导致折行现象……下面就是绑定事件,我们重新定义redrawCalendar方法;
redrawCalendar : function (year,month,date){ (month < 1) &&(year--,month = 12) ; (month > 12)&&(year++,month = 1) ; this .drawCalendar(year,month,date); } |
var tts = calendar[T]( "tt" ); tts[0].onclick = function (){ $.redrawCalendar(year-1,month,date);} tts[1].onclick = function (){ $.redrawCalendar(year,month-1,date);} tts[2].onclick = function (){ $.redrawCalendar(year,month+1,date);} tts[3].onclick = function (){ $.redrawCalendar(year+1,month,date);} var dates = calendar[T]( "kbd" ),j = dates.length; while (--j >= 0) { dates[j].onclick = function (){ var title = this .parentNode.getAttribute( "title" ); text_id &&($.ID(text_id).value = title); calendar.style.display = 'none' ; if (/msie|MSIE 6/.test(navigator.userAgent)){ var mask = $.ID( "calendar_mask" ); mask.parentNode.removeChild(mask); } } } |
现在绑定事件需要先在DOM树中找出其元素,由于可恨的IE6不支持getElementsByClassName,浪费了我们在a元素上的那么多类。为了得到表示日期的a元素(上面的title暗藏着我们需要赋给文本域),我们在a元素中内套一个kbd元素,这样我们就可以在它们上面绑定单击事件,然后向上查代其父节点的title属性值了!由于kbd为内联元素,我们需要很精确地点击在数字上才能触发事件,我们可以为它们添加一个样式扩大其点击范围。
#jcalendar kbd{ display : block ; font : normal 12px / 20px "Comic Sans MS" , "Microsoft YaHei" , sans-serif ; } |
我们观察redrawCalendar与drawCalendar,参数都一样,而redrawCalendar只是对其年月进行一些纠错计算而已,因此redrawCalendar是没有必要的!最后行为层代码修改如下:
var Class = { create: function () { return function () { this .initialize.apply( this , arguments); } } } var extend = function (destination, source) { for ( var property in source) { destination[property] = source[property]; } return destination; } var Jcalendar = Class.create(); Jcalendar.prototype = { initialize: function (options){ this .setOptions(options); var $ = new Date(); this .drawCalendar($.getFullYear(),$.getMonth() + 1,$.getDate()); }, setOptions: function (options) { this .options = { //默认属性集中写在这里。 id: 'jcalendar_' + new Date().getTime(), text_id: null , //用于输入日期的文本域的ID parent_id: null //指定父节点 }; extend( this .options, options || {}); }, ID: function (id){ return document.getElementById(id) }, TN: function (tn){ return document.getElementsByTagName(tn) }, CE: function (s){ return document.createElement(s)}, getHandle: function (){ return this .ID( this .options.id); }, fillArray : function (year,month){ //fill Array var f = new Date(year, month -1 ,1).getDay(), //求出当月的第一天是星期几 dates = new Date(year, month , 0).getDate(), //上个月的第零天就是今个月的最后一天 arr = new Array(42); //用来装载日期的数组,日期以‘xxxx-xx-xx’的形式表示 for ( var i = 0; i < dates ; i ++ ,f ++){ arr[f] = year + '-' + month + '-' + (i+1) ; } return arr; }, addToDom: function (calendar){ //add to dom tree var parent = this .ID( this .options.parent_id) || this .TN( 'body' )[0]; parent.insertBefore(calendar, null ); }, drawCalendar : function (year,month,date){ (month < 1) &&(year--,month = 12) ; (month > 12)&&(year++,month = 1) ; var $ = this ,T= 'getElementsByTagName' , calendar = $.getHandle(), //日历的容器 weeks = "日一二三四五六" .split( '' ), //日历第二行的内容,显示星期几 arr = $.fillArray(year,month), //保存当月的日期 text_id = $.options.text_id; //用于输入日期的文本域的ID if (calendar) { calendar.innerHTML = '' ; } else { calendar = $.CE( 'div' ); $.addToDom(calendar); //把日历加入DOM树中 calendar.setAttribute( 'id' , $.options.id); //设置ID } calendar.innerHTML+= '<span><nobr><tt><<</tt><tt> <</tt>' + year+ '年' +month+ '月' +date + '日<tt>> </tt><tt>>></tt></nobr></span>' ; for ( var i = 0;i <7;i++){ calendar.innerHTML += '<a class="week">' +weeks[i]+ '</a>' ; } for (i = 0;i <42;i++){ calendar.innerHTML += (!arr[i]) ? '<a> </a>' :( '<a href="javascript:void(0)" title="' +arr[i]+ '" class="day' + ((arr[i].split( '-' )[2] == date)? ' current' : '' ) + ((i%7 == 0 || i%7 == 6)? ' weekend' : '' )+ '"><kbd>' +arr[i].split( '-' )[2]+ '</kbd></a>' ) } var tts = calendar[T]( "tt" ); tts[0].onclick = function (){ $.drawCalendar(year-1,month,date); } tts[1].onclick = function (){ $.drawCalendar(year,month-1,date); } tts[2].onclick = function (){ $.drawCalendar(year,month+1,date); } tts[3].onclick = function (){ $.drawCalendar(year+1,month,date); } var dates = calendar[T]( "kbd" ),j = dates.length; while (--j >= 0) { dates[j].onclick = function (){ var title = this .parentNode.getAttribute( "title" ); text_id &&($.ID(text_id).value = title); calendar.style.display = 'none' ; if (/msie|MSIE 6/.test(navigator.userAgent)){ var mask = $.ID( "calendar_mask" ); mask.parentNode.removeChild(mask); } } } }, listenTo: function (id){ //对文本域进行监听 var $ = this ; id = id || $.options.text_id; if (id && $.ID(id)){ var calendar = $.getHandle(), textfield = $.ID(id); calendar.style.display = 'none' ; textfield.onfocus = function (){ textfield.style.position = 'relative' ; var l = textfield.offsetLeft + 'px' , t = (textfield.clientHeight + textfield.offsetTop)+ 'px' ; if (/msie|MSIE 6/.test(navigator.userAgent)){ var iframe = $.CE( "<iframe id='calendar_mask' style='left:" +l + ";width:300px;filter:mask();position:absolute;" + ";top:" +t + "height:160px;z-index:1;'></iframe>" ); //z-index不能为负数 textfield.insertAdjacentElement( 'afterEnd' ,iframe); } with (calendar.style){ position= "absolute" ; display = 'block' ; zIndex = 100; left = l; top = t; } }; } } } ; window.onload = function (){ new Jcalendar({ id: 'jcalendar' , text_id: 'datepicker' }).listenTo(); }; |
注1五种解决方法如下
- 修改select,不用标准select,而是自己用其他html元素模拟
- 修改你的div,使用iframe。
- 在div被显示的时候或者到达select所在位置时隐藏select
- 在div中或div的同一坐标上,用相同尺寸的iframe先遮挡一下,然后在iframe上显示div的内容。
- Object对象的优先度较高,可以挡住select框
<OBJECT id=aa style= "display:none;z-index:1000; position:absolute; top:0; left:0; width:152; height: 200;" type= "text/x-scriptlet" data= "about:<body><div style='position:absolute;left:0;top:0;width:152;height:200;font:14;color:white;background:black;border:1 solid black'>test</div>" ></OBJECT> <select><option>hellohellohellohello</select><button onclick=aa.style.display=aa.style.display== "none" ? "" : "none" >test</button> |
注2微软吃饱饭没有事做,把所有DHTML元素(许多都是IE私有的,如htc,ActiveXObject)分为两大类,windowed element与windowless element。
windowed element
- object元素
- ActiveX 控件
- Plug-ins
- Scriptlet控件
- select 元素
- IE5.1与IE4.0中的iframe元素
windowless element
- Windowless 的 ActiveX 控件
- IE5.5以后的iframe
- 大多数 DHTML 元素,如a,div,form,button,span,address……
windowed 元素会无视其容器,渲染在所有windowless元素之上。这是因为所有windowless元素都是渲染在一个MSHTML 平面上,而windowed元素都是一个个单独地渲染于独自的MSHTML平面上。也就是说,windowed元素独占一个MSHTML平面,windowless元素共用一个MSHTML平面。我们可以使用z-index改变同一个平面上的元素的层叠顺序,但是不能跨平面操纵它们。而所有windowed元素所在的平面都是位于那一个windowless元素平面的顶部!我们可以通过下面的方法找到MSHTML的踪迹:动态生成一个iframe,什么也不设置,打开IE8的开发人员工具,就像看到它所在的MSHTML。

在IE5中(IE4.0,iframe没有z-index属性不可用),iframe是windowed元素,如果iframe元素的z-index大于select的,那么它就位于select之上。如果它们都没有指定z-index或z-index相同,那么后出现的元素位于前面元素之上。
在IE5.5中,iframe变成了windowless元素,但它还是保留了windowed元素的某些特征,也就是说它既可以通过z-index来调整它与select的层叠顺序,也可以通过z-index也调整它与windowless元素(如div)的层叠顺序。换言之,它是个跨MSHTML平面的特殊元素,因此我就可以利用iframe元素做蒙板,用它来盖住select,然后再用div来盖住它。
注3根据W3C标准,每一个定位元素都归属于一个stacking context(与IE的MSHTMNL相仿)。根元素形成root stacking context,而其他的stacking context则由定位元素产生(此定位元素的z-index被定义一个非auto的z-index值),我们称之为local stacking context。定位子元素会以这个local stacking context为参考,用相同的规则设置此元素里面的子孙元素的stacking context。当stacking context一样的时候,就用z-index的值来决定怎样显示,如果z-index也相同(即stack level相同),则按照档中后来者居上的原则(back-to-front )的顺序来层叠。当任何一个元素层叠另一个包含在不同stacking context元素时,则会以stacking context的层叠级别(stack level)来决定显示的先后情况。也就是说,在相同的stacking context下才会用z-index来决定先后,不同时则由stacking context的z-index来决定。IE浏览器似乎给body元素默认了一个相对定位属性(position: relative)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义