原生JS实现日历组件的示例代码

作者:青柠时间:2026-10-22 08:38:37浏览:128

本文详细介绍如何使用原生JavaScript实现一个可复用的日历组件。通过面向对象思想封装日期选择框、控制栏及网格,利用Date对象API处理跨月跨年逻辑,实现日期同步显示与动态绘制。

日历组件设计目标

设计目标

  • 点击日期输入框时,展开日历面板。
  • 顶部设有日期控制栏,支持年、月、日的独立选择,并提供“今天”快捷按钮。
  • 日历网格需展示当前月份的所有天数。若当月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);
  },

控制栏日期变更后,需同步更新日历网格显示。因此,在changeShowDateBar函数末尾调用drawPicker,重新绘制日历格子,确保两者显示一致。

日历网格绘制逻辑

日历网格绘制逻辑

drawPicker函数依据传入日期生成日历网格。

  • 首先计算传入日期所在月份的天数。
  • 确定当月1号是星期几。通过Date对象的setDate(1)将日期设为1号,再调用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);
  },

最终效果展示

日历组件设计目标

界面样式较为简单。

关键问题与解决方案

开发过程中遇到的问题与解决方案

  • 网格绘制需处理首尾空白补全。将网格分为三部分:上月剩余天数、当月天数、下月起始天数。先计算1号是周几,绘制从周日到1号前的天数。
  • 当月部分从1号循环至月末。若月末不是周六,需补全至周六(判断date.getDay()<=6)。
  • 实现网格与控制栏的日期同步。绘制时为每个格子保存其代表的Date对象。点击格子时,调用changeShowDateBar并传入该Date,更新控制栏后重绘网格。
  • 解决每月天数不同导致“日”下拉框选项数量变化的问题。在changeShowDateBar中根据传入Date计算当月天数,动态生成对应数量的“日”选项。
  • 处理跨月与跨年逻辑。控制栏包含月份和日的上下切换按钮。判断当前是否为最后一月(日),若是,则年(月)加1,并将显示重置为首月(日),调用changeShowDateBar刷新。同理处理第一月(日)的情况。

Date对象API应用

使用的Date对象API

  • date.getFullYear() //获取年份
  • date.getMonth() //获取月份,范围0-11
  • date.getDate() //获取日期,范围1-31
  • date.getDay() //获取星期几,范围0-6
  • date.getTime()//获取时间戳(毫秒)
  • date.setFullYear(2017); //设置年份
  • date.setMonth(x) //设置月份。若x在0-11之间,对应x年1-12月;若超出范围,会自动进位或退位至相应年月。
  • 若传入负数,如-1,则跳转至上一年最后一月。
  • date.setDate(x) //逻辑同setMonth,自动处理月份变动。-1代表跳转至上月最后一天。
  • date.setTime()//根据时间戳设置日期

项目源码地址:https://github.com/wenkeShi/js-calendar

以上为本文全部内容,希望有助于学习,欢迎支持。

作者最新文章