原生JS实现日历组件

想要实现的效果

  • 点击日期选择框出现日历
  • 有个日期控制栏帮助选择日期, 包括年、月、日的选择和今天
  • 日历格子,初次点击日期选择框时显示此刻的日期,日历格子的日期应该包括这个月的所有天数,同时如果当月的1号不是周日,还应补全从周日到1号的天数。还要在这个月最后1号的后面补全到周六。
  • 日期控制栏和日历格子的日期还有选择框里的日期的变化要是同步的。

实现思路

为了组件的可复用性,需要用面向对象的思想。
每个日历组件都是一个日历对象,主要包括日期选择框,日期控制显示栏,还有日历格子,为了保持日期控制显示栏和日历格子日期同步变化,日期控制栏和日历里面的每个格子都应该包含一个Date属性,点击日历里的格子,将格子存的Date属性作为函数参数,调用函数改变日期控制栏显示的时间。同理,日期控制栏时间变化时,也将Date属性作为参数调用函数,函数重新绘制日历格子。
上码:

function Calendar(parentId) {
    this.parentElement = document.getElementById(parentId);
    this.init();
}
Calendar.prototype = {
    init: function() {
        this.contains = document.createElement("div");
        this.contains.onselectstart = function(){return false};   //让按钮点击时不会出现文字被选中的蓝色块
        this.dateInput = document.createElement("input");
        this.datePicker = document.createElement("div");
        this.showDateBar = document.createElement("div");
        this.dateBox = document.createElement("div");
        this.icon = document.createElement("i");
        this.contains.className = 'datepicker-container';
        this.dateInput.className = 'date-input';
        this.dateInput.readOnly = true;
        var parent = this;
        this.dateInput.onclick = function(event){
            parent.onDateInputClick(event);            //点击日期选择框时显示日历格子                
        };
        this.contains.onblur = function(){
            parent.datePicker.style.display = 'none';
        }
        this.datePicker.className = 'date-picker';
        this.datePicker.style.display = 'none';
        this.showDateBar.className = 'show-date';
        this.dateBox.className = 'date-box';
        this.icon.className = 'date-icon';
        this.icon.innerHTML = ''; //iconfont这里用的阿里图标,可以自行替换
        this.datePicker.appendChild(this.showDateBar);
        this.datePicker.appendChild(this.dateBox);
        this.contains.appendChild(this.dateInput);
        this.contains.appendChild(this.icon);
        this.contains.appendChild(this.datePicker);
        this.parentElement.appendChild(this.contains);  
    },
}

初始化日期控制栏:

drawShowDateBar: function(parentElement){
        var parent = this;
        var nowDate = new Date();
        parentElement.date = nowDate;
        var nowYear = nowDate.getFullYear();
        var nowMonth = nowDate.getMonth();
        var nowDay = nowDate.getDate();
        //showDateBar内容拼接
        var contentStr ='<div class="year-input"><span>'+nowYear+'年</span><i class="select-year-btn"></i><ul class="year-select-box" style="display : none">';
        for(var i=0;i<150;i++){
            contentStr+='<li>'+(i+1900)+'年</li>';
        }
        contentStr+='</ul></div>'
                    +'<div class="month-input"><i class="prev-month"></i><select class="months-options">'
        for(var i=0;i<12;i++){
            contentStr+='<option>'+(i+1)+'月</option>';
        }
        contentStr+='</select><i class="next-month"></i></div>'
                    +'<div class="day-input"><i class="prev-day"></i><select class="days-options"></select>'
                    +'<i class="next-day"></i></div>'
                    +'<button class="today-btn">今天</button>'
                    +'<div class="days-title">';
        var weekday = ['日', '一', '二', '三', '四', '五', '六'];
        for (var i = 0; i < 7; i++) {
            contentStr+='<span class="day-title">'+weekday[i]+'</span>';
        }
        contentStr+='</div>';
        parentElement.innerHTML = contentStr;
        this.changeShowDateBar(nowDate);   //插入到showTimeBar之后,初始化,传入的参数是现在的时间
        var yearInput = parentElement.firstChild;
        //年选择框点击显示和隐藏选择列表
        yearInput.onclick = function(){     //target和this的区别  target是触发事件的元素,this是处理事件的元素  
            var ul = this.lastChild;
            ul.style.display==='none'||ul.style.display==='none'? ul.style.display='inline-block':ul.style.display='none';
        };
        //为年选择下拉框绑定点击事件
        var yearSelectBox = yearInput.lastChild;
        var yearLi = yearSelectBox.children;
        for(var i=0;i<yearLi.length;i++){
            yearLi[i].onclick = function(){
                parent.showDateBar.date.setFullYear(this.innerText.slice(0,-1));
                parent.changeShowDateBar(parent.showDateBar.date);   //时间改变之后都要重新调用,因为不同年,不同月,某个月的天数不全一样
            };
        }
        //为month的前后按钮添加点击事件
        var monthInput = yearInput.nextSibling;
        monthInput.firstChild.onclick = function(){
            var monthOptions = this.nextSibling;
                if(monthOptions.selectedIndex>0){
                    parent.showDateBar.date.setMonth(--monthOptions.selectedIndex);
                }else{
                    monthOptions.selectedIndex = 11;
                    parent.showDateBar.date.setFullYear(parent.showDateBar.date.getFullYear()-1);
                    parent.showDateBar.date.setMonth(11);
                }
            parent.changeShowDateBar(parent.showDateBar.date);
        };
        monthInput.lastChild.onclick = function(){
            var monthOptions = this.previousSibling;
            if(monthOptions.selectedIndex<11){
                parent.showDateBar.date.setMonth(++monthOptions.selectedIndex);
            }else{
                monthOptions.selectedIndex = 0;
                parent.showDateBar.date.setFullYear(parent.showDateBar.date.getFullYear()+1);
                parent.showDateBar.date.setMonth(0);
            }
            parent.changeShowDateBar(parent.showDateBar.date);
            
        }
        monthInput.children[1].onchange = function(){
            parent.showDateBar.date.setMonth(this.selectedIndex);
            parent.changeShowDateBar(parent.showDateBar.date)
        };

        //为day的前后按钮添加点击事件
        var dayInput = monthInput.nextSibling;
        dayInput.firstChild.onclick = function(){
            var dayOptions = this.nextSibling;
            if(dayOptions.selectedIndex>0){
                parent.showDateBar.date.setDate(dayOptions.selectedIndex--);
            }else{
                parent.showDateBar.date.setMonth(parent.showDateBar.date.getMonth()-1);
                parent.showDateBar.date.setDate(parent.getDaysOfMonth(parent.showDateBar.date));
            }
            parent.changeShowDateBar(parent.showDateBar.date);
        };
        dayInput.lastChild.onclick = function(){
            var dayOptions = this.previousSibling;
            if(dayOptions.selectedIndex < dayOptions.length-1){
                dayOptions.selectedIndex++;
                parent.showDateBar.date.setDate(dayOptions.selectedIndex+1);    
            }else{
                parent.showDateBar.date.setDate(1);
                parent.showDateBar.date.setMonth(parent.showDateBar.date.getMonth()+1); 
            }
            parent.changeShowDateBar(parent.showDateBar.date);
        };
        dayInput.children[1].onchange = function(){
            parent.showDateBar.date.setDate(this.selectedIndex+1);
            parent.changeShowDateBar(parent.showDateBar.date)
        };
        //为今天按钮绑定点击事件
        var todayBtn = dayInput.nextSibling;
        todayBtn.onclick = function(){
            parent.drawPicker(new Date());
            parent.changeShowDateBar(new Date());
        }   
    },

drawShowDateBar函数为日期控制栏的年份、月份、和天的点击按钮设置了点击事件处理函数。还有选择下拉框变化的处理函数。
在日期控制栏初始化时,或者改变showDateBar的Date时,都会调用changeShowDateBar 函数。这个函数主要根据传入的日期改变日期控制栏“日”下拉栏的天数,因为每个月的天数不尽相同,所以要根据传入的日期来改变。会计算出传入的日期对应的月份有多少天,使用getDaysOfMonth函数计算。

//计算一个月的天数
    getDaysOfMonth: function(primalDate) {
        var date = new Date(primalDate);  //要新建一个对象,因为会改变date
        var month = date.getMonth();
        var time = date.getTime();        //计算思路主要是month+1,相减除一天的毫秒数
        var newTime = date.setMonth(month + 1);
        return Math.ceil((newTime - time) / (24 * 60 * 60 * 1000));
    },
changeShowDateBar : function(date){
        var yearInput = this.showDateBar.firstChild;
        var monthInput = yearInput.nextSibling;
        var dayInput = monthInput.nextSibling;
        yearInput.firstChild.innerText = date.getFullYear()+'年';
        var monthsOptions = monthInput.firstChild.nextSibling;
        monthsOptions.selectedIndex = date.getMonth();
        var daysOptions = dayInput.firstChild.nextSibling;
        var days = this.getDaysOfMonth(date);
        var dayStr = '';
        for(var i=1;i<=days;i++){
            dayStr+='<option>'+i+'日</option>';
        }
        daysOptions.innerHTML = dayStr;
    //  console.log(date.toLocaleDateString()+'changeShowDateBar');
        daysOptions.selectedIndex = date.getDate()-1;
        this.drawPicker(date);
    },

在日期控制栏的Date变化后,日历格子的日期也应该要改变,显示的日期要和日期控制栏的保持一致。所以在changeShowDateBar函数结尾处调用drawPicker函数,重新绘制日历格子。

绘制日历格子的思路

drawPicker函数要根据传入的日期绘制日历格子。

  • 首先计算传入的日期月份的天数
  • 计算这个月1号是周几 。利用Date对象的date.setDate(1) //将天设置为1号 。date.getDay() //得到这天是周几
  • 如果1号不是周日,则补全周日到1号的天数。可以利用oldDate.setDate(-1) //设置日期为原来日期的上个月的最后一天。注意setDate是会改变当前日期的,并不是返回新的日期。
  • 从1号到这个月最后一天循环。
  • 补全最后一天到周六的天数

drawPicker函数:

drawPicker: function(primalDate) {
        var date = new Date(primalDate);  //要新建一个对象,因为会改变date
        var nowMonth = date.getMonth()+1;
        var nowDate = date.getDate();
        var spanContainer = [];
        var dateBox = this.dateBox;
        dateBox.innerHTML = '';
        var time = date.getTime();
        var days = this.getDaysOfMonth(date);  //计算出这个月的天数
        date.setDate(1);                       //将date的日期设置为1号
        var firstDay = date.getDay();          //知道这个月1号是星期几
        for (var i = 0; i < firstDay; i++) {   //如果1号不是周日(一周的开头),则在1号之前要补全
            var tempDate = new Date(date);
            tempDate.setDate(i - firstDay + 1);
            var span = document.createElement("span");
            span.className = "unshow";
            spanContainer.push({span : span, date : tempDate});
        }
        for (var i = 1; i <= days; i++) {       //1号到这个月最后1天
            var span = document.createElement("span");
            span.className = 'show';
            spanContainer.push({span : span, date : new Date(date)});
            date.setDate(i + 1);
        }
        for (var i = date.getDay(); i <= 6; i++) {  //在这个月最后一天后面补全
            var span = document.createElement("span");
            span.className = "unshow";
            spanContainer.push({span : span, date : new Date(date)});
            date.setDate(date.getDate()+1);
        }
        for(var i=0;i<spanContainer.length;i++){
            var spanBox = spanContainer[i];
            var span = spanBox.span;
            span.year = spanBox.date.getFullYear();  //为每个span元素添加表示时间的属性
            span.month = spanBox.date.getMonth() + 1;
            span.date = spanBox.date.getDate();
            span.innerText = spanBox.date.getDate();
            if(span.date === nowDate&&span.month === nowMonth)  //如果这个span的日期为与传入的日期匹配,设置类名为select
                span.className+=" select";
            var parent = this;
            span.onclick = function(){    //设置点击事件
                var target = event.target;
                var selected = target.parentElement.getElementsByClassName("select");
                for(var i=0 ;i<selected.length;i++){
                    selected[i].className = selected[i].className.replace(" select","");
                };
                target.className+=" select";
                parent.changeDate(target.year, target.month, target.date); 
                parent.changeShowDateBar(new Date(target.year, target.month-1, target.date));   
            };
            dateBox.appendChild(span);  //将span添加到dateBox中
        }
        this.changeDate(primalDate.getFullYear(), primalDate.getMonth()+1, primalDate.getDate())
        return;
    },
//日期框点击时显示日历
    onDateInputClick: function(event) {  
        var target = event.target;
        var value = target.value;
        var datePicker = this.datePicker;
        if(datePicker.style.display==='none'){   //这里必须要在js文件里将datePicker.style.display设置为none,如果是在css文件里设置为none,得到的display为""
            datePicker.style.display = 'block';
        }else{
            datePicker.style.display = 'none';
            return; 
        }
        if (!value) this.drawShowDateBar(this.showDateBar);  //绘制日历的显示栏 
    },
    changeDate : function(year, month, date){
        this.dateInput.value = year+"-"+(month<10?("0"+month):month)+"-"+(date<10?("0"+date):date);
    },

实现效果

calendar.png

有点丑......

实现中遇到的问题

  • 日历格子的绘制问题 。要补全1号前面到周日的天数,还要补全当月最后1号到周六的天数。日历格子的绘制可以分为3部分,当月前面、当月和当月后面的。要计算出1号是周几,然后将这周周日到1号的天数绘制。
    当月的日历从1号到最后1号循环绘制。补全最后1号到周六的天数(date.getDay()<=6)
  • 日历格子和日期控制栏显示的同步。在绘制时为每个日历格子单元保存其代表的Date。点击格子单元时,调用changeShowDateBar函数,将单元存的Date传入,改变日期控制栏显示的日期,然后重绘日历格子。
  • 每个月天数不同,出现的“日”选择框天数不同的问题。在changeShowDateBar函数里会根据传入的Date,计算当月有多少天,然后动态生成“日”选择框应有的天数。
  • 跨月,跨年的处理。在日期控制栏中,有月份和日的上下按钮,在处理跨月和跨年时,判断这月(日)是否为最后一月(日),若为,则日期控制栏的Date的年(月)加1,将显示的月(日)设为第一月(日),调用changeShowDateBar函数。同理判断是否为第一月(日)。

用到的Date API
date.getFullYear() //得到date的年份
date.getMonth() //得到月份 0-11
date.getDate() //得到日期 1-31的数字
date.getDay() // 得到这天是周几 0-6
date.getTime()// 得到date的时间戳 ms表示

date.setFullYear(2017); // 设置年份
date.setMonth(x) // 如果设置为0-11,则date为x年的1-12月,如果比11大,则会往前面推,会跳到x+([(n+1)/12])年的第(n+1)%12个月
如果为负数,例如-1则会调到上一年的最后一月去。
date.setDate(x) // 和setMonth是同理的,它会自动根据当月的天数,判断是否发生月份的变动。-1代表date跳到上月的最后一天
date.setTime()// 根据时间戳设置date

项目源码 求star,QAQ

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 上次给大家分享了《2017年最全的excel函数大全(5)——逻辑函数》,这次分享给大家日期和时间函数(上)。 D...
    幸福的耗子阅读 5,757评论 0 5
  • 本文发表至今已有一段时间,错别字多、文笔混乱、内容过于陈旧。本人建议读者不必细究,大概浏览即可,最新的开发指南还是...
    Oopsguy阅读 6,212评论 2 19
  • 临近毕业还有不到一个月了,b还是没有找到工作。 正午的阳光像块厚厚的棉垫披在他身上,没走出就业指导中心几步汗就落下...
    苏语阅读 826评论 4 5
  • 时光不老,我们不散,这是15年11月18日我们结伴游玩《烟台山》时,偶然间遇见的《理想书店》里写下的誓言。 16年...
    Smile旧颜阅读 206评论 0 0