zoukankan      html  css  js  c++  java
  • 一步步教你实现跨游览器的日期选择器

    由于日历与日期选择器几乎是同一回事,日历部分的讲解我就不重复了,没有看过的朋友可以参看我这篇博文。不过日期选择器有一个难点,就是如何防止在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
                            +";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;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 {
        210px;
        background:#E0ECF9;
        border:1px solid #479AC7;
        float:left;
    }
    
    #jcalendar span {
        float:left;
        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;
        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 = '&lt;&lt;';
            tts[1].innerHTML = '&nbsp;&nbsp;&lt;';
            tts[2].innerHTML = '&gt;&nbsp;&nbsp;';
            tts[3].innerHTML = '&gt;&gt;';
            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
                            +";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>&lt;&lt;</tt><tt>&emsp;&lt;</tt>'+ year+'年'+month+'月'+date
                 +'日<tt>&gt;&emsp;</tt><tt>&gt;&gt;</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>&nbsp;</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>&lt;&lt;</tt><tt>&emsp;&lt;</tt>'+ year+'年'+month+'月'+date
                 +'日<tt>&gt;&emsp;</tt><tt>&gt;&gt;</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>&nbsp;</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
                            +";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五种解决方法如下

    1. 修改select,不用标准select,而是自己用其他html元素模拟
    2. 修改你的div,使用iframe。
    3. 在div被显示的时候或者到达select所在位置时隐藏select
    4. 在div中或div的同一坐标上,用相同尺寸的iframe先遮挡一下,然后在iframe上显示div的内容。
    5. Object对象的优先度较高,可以挡住select框
    <OBJECT id=aa style="display:none;z-index:1000; position:absolute; top:0; left:0; 152; height: 200;" type="text/x-scriptlet" data="about:<body><div style='position:absolute;left:0;top:0;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)

  • 相关阅读:
    Smobiler如何实现.net一键开发,ios和android跨平台运行
    使用Smobiler实现类似美团的界面
    疫情当下,企业系统如何快速实现移动化?
    Smobiler针对百度文字识别SDK动态编译与运行
    smobiler自适应不同手机分辨率
    仓库管理移动应用解决方案——C#开发的移动应用开源解决方案
    移动OA办公——Smobiler第一个开源应用解决方案,快来get吧
    react-native 标题随页面滚动显示和隐藏
    react-native 键盘遮挡输入框
    解决adb网络连接中出现的“由于目标计算机积极拒绝,无法连接”错误
  • 原文地址:https://www.cnblogs.com/rubylouvre/p/1534076.html
Copyright © 2011-2022 走看看