近期在知乎上看到这么一个帖子,题主说自己JavaScript都学完了,结果老师留的作业还是不会写,就是写一个日历的插件,结果楼下一堆大牛出现了,百度的阿里的纷纷站出来发表自己的看法,有人认为简单,有人认为其实细化不简单,于是出于好奇,自己也来动手写了一下。虽说现如今各种优秀的UI框架层出不穷,都会自带calendar和datepick这种日历相关的组件,而且无论是适配还是视觉效果都做得相当nice,可能都不会用到自己写的,但是还是打算动手,因为date对象也是js里面的重点。
第一版:纯js实现
通过切换月份和年份,来展示不同的日历页面,实际上是根据当前年月,来进行页面的重绘,所以页面渲染是一个函数,所需参数就是当前选中的年份和月份。
const render = (month, year) => {
};
render();
首先,日历除去首行day有几行?
一共6行,Math.ceil((31 - 1) / 7 + 1) = 6
三块组成:上月剩余,本月,下月开始
1,上月剩余
首先要知道本月1号是周几(x),然后从周日到x的天数就是上月剩余天数,从几号到几号,需要了解本月是几月,来推算出上月月末是几号,其实也就是上月有多少天。
2,本月
只需知道当月是几月,当月多少天,然后按顺序排。
3,下月开始
只需要知道下月1号是周几,然后42个数字还剩多少,从1排到最后就可以了
代码部分:
HTML:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Date Picker</title> <link rel="stylesheet" type="text/css" href="css/index.css"></link> </head> <body> <input id="textInput" class="textInput" type="text" placeholder="选择日期" /> <div id="datePicker" class="datePicker datePickerHide"> <div class="dateHeader"> <span id="yearLeft" class="left headerMid"><<</span> <span id="monthLeft" class="left headerMid page"><</span> <span id="changeDateHead" class="page"> <span id="changeYear" class="headerMid col"> <span id="dateYear"></span> <span>年</span> </span> <span id="changeMonth" class="headerMid col"> <span id="dateMonth"></span> <span>月</span> </span> </span> <span id="changeYearHead" class="page headerMid col" style="display: none"> <span id="changeYearFirst"></span> <span>年</span> <span>-</span> <span id="changeYearLast"></span> <span>年</span> </span> <span id="changeMonthHead" class="page headerMid col" style="display: none"> <span id="backChangeYearPage"></span> <span>年</span> </span> <span id="yearRight" class="right headerMid">>></span> <span id="monthRight" class="right headerMid page">></span> </div> <div class="dateMain"> <div id="firstPage" class="page firstPage" style="display: block"> <div class="dateDay"> <span>日</span> <span>一</span> <span>二</span> <span>三</span> <span>四</span> <span>五</span> <span>六</span> </div> <div id="dateBody" class="dateBody"></div> </div> <div id="secondPage" class="page secondPage" style="display: none"></div> <div id="thirdPage" class="page secondPage" style="display: none" onclick="chooseMonth()"> <span><em id="month-1" index='1'>1月</em></span> <span><em id="month-2" index='2'>2月</em></span> <span><em id="month-3" index='3'>3月</em></span> <span><em id="month-4" index='4'>4月</em></span> <span><em id="month-5" index='5'>5月</em></span> <span><em id="month-6" index='6'>6月</em></span> <span><em id="month-7" index='7'>7月</em></span> <span><em id="month-8" index='8'>8月</em></span> <span><em id="month-9" index='9'>9月</em></span> <span><em id="month-10" index='10'>10月</em></span> <span><em id="month-11" index='11'>11月</em></span> <span><em id="month-12" index='12'>12月</em></span> </div> </div> </div> </body> <script src="js/index.js"></script> </html>
js部分:
// 默认是当天 var chosenDate = new Date(), year = chosenDate.getFullYear(), month = chosenDate.getMonth() + 1, date = chosenDate.getDate(), lastDateId = '', // 挂到全局下去 lastYearId = '', lastMonthId = '', firstNum = 0; window.onload = function(){ renderFirstPage(year, month, date); // input框获取焦点时日历显示 var datePicker = getIdDom('datePicker'); getIdDom('textInput').onfocus = function(){ datePicker.className = 'datePicker datePickerShow'; } /* 以上是第一部分页面展示 */ /* 二级页面 */ var renderSecondPage = function(firstNum){ var lastNum = firstNum + 9; // 二级页面末尾数字 getIdDom('changeYearFirst').innerHTML = firstNum; getIdDom('changeYearLast').innerHTML = lastNum; var yearTemplate = ``; for (var i = firstNum; i < lastNum + 1; i++) { if (i == year) { // 当前选中年 yearTemplate += `<span><em id="year-${i}" onclick="chooseYear(this, ${i})" style="background-color: #39f;color: #fff">${i}</em></span>` } else { yearTemplate += `<span><em id="year-${i}" onclick="chooseYear(this, ${i})">${i}</em></span>` } } getIdDom('secondPage').innerHTML = yearTemplate; } var reRenderSecondPage = function(){ var yearStr = year.toString(); var yearLastLetter = yearStr[yearStr.length-1]; // 末尾数 firstNum = year - Number(yearLastLetter); // 二级页面首位数字 renderSecondPage(firstNum); } reRenderSecondPage(); getIdDom("backChangeYearPage").innerHTML = year; // 三级页面年份 // click事件集中写 // 上一年下一年,上一月下一月的点击事件 clickFn('yearLeft', function(){ if (getIdDom('changeYearHead').style.display != 'none') { // 此时是二级页面,选年份 if (firstNum - 10 < 1) { // 首位年份不能小于1 return } firstNum -= 10; renderSecondPage(firstNum); } else { if (year - 1 < 1) { // 年份不能小于1 return } year--; renderFirstPage(year, month, date); getIdDom("backChangeYearPage").innerHTML = year; reRenderSecondPage(); } }); clickFn('monthLeft', function(){ if (month < 2) { // 1月 month = 12; year--; } else { month--; } renderFirstPage(year, month, date) }); clickFn('yearRight', function(){ if (getIdDom('changeYearHead').style.display != 'none') { // 此时是二级页面,选年份 firstNum += 10; renderSecondPage(firstNum); } else { year++; renderFirstPage(year, month, date); getIdDom("backChangeYearPage").innerHTML = year; reRenderSecondPage(); } }); clickFn('monthRight', function(){ if (month > 11) { // 12月 month = 1; year++; } else { month++; } renderFirstPage(year, month, date) }); clickFn('changeYear', function(){ var pagesArr = Array.from(document.querySelectorAll('.page')); pagesArr.forEach(function(item){ item.style = 'display: none' }); reRenderSecondPage(); changeStyle('secondPage', 'display: block'); changeStyle('changeYearHead', 'display: inline-block'); }); // 点击年份切换至二级页面 clickFn('changeMonth', function(){ var pagesArr = Array.from(document.querySelectorAll('.page')); pagesArr.forEach(function(item){ item.style = 'display: none' }); if (lastMonthId !== '') { // 非第一次点击 getIdDom(lastMonthId).style = ""; } changeStyle("month-" + month, 'background-color: #39f;color: #fff'); lastMonthId = 'month-' + month; changeStyle('thirdPage', 'display: block'); changeStyle('changeMonthHead', 'display: inline-block'); }) clickFn('changeMonthHead', function(){ // 切回年份选择 var pagesArr = Array.from(document.querySelectorAll('.page')); pagesArr.forEach(function(item){ item.style = 'display: none' }); changeStyle('secondPage', 'display: block'); changeStyle('changeYearHead', 'display: inline-block'); reRenderSecondPage(); }) document.getElementsByTagName('html')[0].onclick = function(e){ // 这里模拟失去焦点事件 var name = e.target.nodeName; if (name == 'BODY' || name == 'HTML') { datePicker.className = 'datePicker datePickerHide'; } } } function getIdDom(id){ return document.getElementById(id) } // 根据id获取dom节点 function clickFn(id, fn) { getIdDom(id).onclick = fn; } // 封装一下click方法 function renderFirstPage(year, month, date = 1){ var datePage = []; // 最终展示页面的所有日期合集 // 第一部分,上月月末几天 // 首先要知道上月一共多少天 var getAlldays = (year, month) => { if (month <= 7) { // 1-7月 if (month % 2 === 0) { // 偶数月 if (month === 2) { // 2月特殊 if ((year % 400 == 0) || ( year % 4 == 0 && year % 100 != 0)) { // 闰年 var alldays = 29 } else { var alldays = 28 } } else { var alldays = 30 } } else { // 奇数月 var alldays = 31 } } else { // 8-12月 if (month % 2 === 0) { // 偶数月 var alldays = 31 } else { var alldays = 30 } } return alldays }; // alldays表示某年某月的总天数 var lastMonthAllDays = getAlldays(year, month - 1); // 上月天数 var chosenFirstMonthDay = new Date(year, month - 1, 1).getDay(); // 当月1号周几 var datePageTemplate = ``; var num = 0; for (var i = lastMonthAllDays - chosenFirstMonthDay + 1; i < lastMonthAllDays + 1; i++ ) { datePageTemplate += `<span id="lastMonth"><em id="last-${i}" onclick="chooseDate(this, 'last-${i}', ${year}, ${month}, ${i})">${i}</em></span>`; num++; } // 第二部分,当月总天数 var chosenMonthAllDays = getAlldays(year, month); // 当月天数 var time = new Date(); var a = time.getFullYear(), b = time.getMonth() + 1, c = time.getDate(); // 用来记录当天时间 for(var i = 1; i < chosenMonthAllDays + 1; i++) { var chosenDateObj = { year: year, month: month, date: i }; if (i === c && year === a && month === b) { // 今天日期高亮 datePageTemplate += `<span id="today" class="currentMonth"><em id="today-${i}" onclick="chooseDate(this, 'today-${i}', ${year}, ${month}, ${i})" class="today">${i}</em></span>`; } else { datePageTemplate += `<span id="currentMonth" class="currentMonth"><em id="cur-${i}" onclick="chooseDate(this, 'cur-${i}', ${year}, ${month}, ${i})">${i}</em></span>`; } num++; } // 第三部分,剩余天数 for (var i = 1; i < 43 - num; i++) { var chosenDateObj = { year: year, month: month, date: i }; datePageTemplate += `<span id="nextMonth"><em id="nex-${i}" onclick="chooseDate(this, 'nex-${i}', ${year}, ${month}, ${i})">${i}</em></span>`; } var templateString = `${datePageTemplate}`; var innerFn = function(id, content) { getIdDom(id).innerHTML = content }; innerFn('dateYear', year); innerFn('dateMonth', month); innerFn('dateBody', templateString); } function chooseDate(item, index, year, month, date) { event.stopPropagation(); if (lastDateId !== '') { // 非第一次点击 getIdDom(lastDateId).style = ""; } // 选中项样式改变,并且将input的日期修改 lastDateId = index; item.style = "background-color: #39f;color: #fff"; getIdDom("textInput").value = year + '-' + month + '-' + date; } function chooseYear(item, thisYear) { event.stopPropagation(); if (lastYearId !== '') { // 非第一次点击 if (getIdDom(lastYearId)) { // 存在已经跨页面了,但是id不存在了 getIdDom(lastYearId).style = ""; } } else { // 第一次点击 getIdDom('year-' + year).style = ""; } lastYearId = 'year-' + thisYear; year = thisYear; item.style = "background-color: #39f;color: #fff"; var pagesArr = Array.from(document.querySelectorAll('.page')); pagesArr.forEach(function(item){ item.style = 'display: none' }); if (lastMonthId !== '') { // 非第一次点击 getIdDom(lastMonthId).style = ""; } changeStyle("month-" + month, 'background-color: #39f;color: #fff'); lastMonthId = 'month-' + month; getIdDom("backChangeYearPage").innerHTML = year; changeStyle('changeMonthHead', 'display: inline-block'); changeStyle('thirdPage', 'display: block'); } function chooseMonth(){ var target = event.target; if (target.nodeName === 'EM') { // 表示当前点击的为em节点 if (lastMonthId !== '') { // 非第一次点击 getIdDom(lastMonthId).style = ""; } else { // 第一次点击 getIdDom('month-' + month).style = ""; } month = parseInt(target.innerHTML); lastMonthId = 'month-' + month; target.style = "background-color: #39f;color: #fff"; renderFirstPage(year, month, date); // 展示首页 var pagesArr = Array.from(document.querySelectorAll('.page')); pagesArr.forEach(function(item){ item.style = 'display: none' }); changeStyle('firstPage', 'display: block'); changeStyle(['changeDateHead', 'monthLeft', 'monthRight'], 'display: inline-block'); } } // 判断对象类型 function isType(type){ return function(obj){ return toString.call(obj) == '[object ' + type + ']'; } } // 改变display属性 function changeStyle(ids, styles){ var isString = isType('String'), isArray = isType('Array'); if (isString(ids)) { getIdDom(ids).style = styles; } else if (isArray(ids)) { ids.forEach(function(item){ getIdDom(item).style = styles; }) } }
css部分:
* { margin: 0; padding: 0; } *, :after, :before { box-sizing: border-box; } body { font-family: Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,\\5FAE\8F6F\96C5\9ED1,Arial,sans-serif; } .textInput { position: relative; display: block; } .datePicker { width: 216px; margin: 10px; color: #c3cbd6; border-radius: 4px; box-shadow: 0 1px 6px rgba(0,0,0,.2); transform-origin: center top 0px; transition: all .2s ease-in-out; position: absolute; left: 0px; top: 16px; } .datePickerHide { opacity: 0; } .datePickerShow { opacity: 1; } .dateHeader { height: 32px; line-height: 32px; text-align: center; border-bottom: 1px solid #e3e8ee; } .left, .right { display: inline-block; width: 20px; height: 24px; line-height: 26px; margin-top: 4px; text-align: center; cursor: pointer; color: #c3cbd6; -webkit-transition: color .2s ease-in-out; transition: color .2s ease-in-out; } .left { float: left; margin-left: 10px; } .right { float: right; margin-right: 10px; } .dateMain { margin: 10px; } .dateDay { line-height: normal; font-size: 0; letter-spacing:normal; } span { display: inline-block; text-align: center; font-size: 12px; } .dateDay span { line-height: 24px; width: 24px; height: 24px; margin: 2px; } .dateBody span { width: 28px; height: 28px; cursor: pointer; } .dateBody span em { display: inline-block; position: relative; width: 24px; height: 24px; line-height: 24px; margin: 2px; font-style: normal; border-radius: 3px; text-align: center; transition: all .2s ease-in-out; } .dateBody span.currentMonth { color: #657180; } .today:after { content: ''; display: block; width: 6px; height: 6px; border-radius: 50%; background: #39f; position: absolute; top: 1px; right: 1px; } .currentMonth em:hover { background-color: #e1f0fe; } .col { color: #657180; } .headerMid { cursor: pointer; } .headerMid:hover { color: #39f; } /* second */ .secondPage { width: 196px; font-size: 0; } .secondPage span { display: inline-block; width: 40px; height: 28px; line-height: 28px; margin: 10px 12px; border-radius: 3px; cursor: pointer; } .secondPage span em { display: inline-block; width: 40px; height: 28px; line-height: 28px; margin: 0; font-style: normal; border-radius: 3px; text-align: center; transition: all .2s ease-in-out; color: #657180; } .secondPage span em:hover { background-color: #e1f0fe; }
GitHub项目地址:https://github.com/Yanchenyu/DatePicker
项目写完了,但其实发现里面存在大量的DOM操作以及环境污染,这个对性能的损耗是相当大的,写得越多越发现状态难以管理,都只能挂到全局下去,所以打算再写一套React版本的。
end