日历选择器

首先我们想一下我们平常看到的日历选择器,通常头部会有一个星期几表头,下面就是日期,而日期下面通常会有1号,1号前面应该有一些上个月的日期,而最后一天后面还应该有下一个月的日期。

对于日期相关处理,JavaScript 提供了 Date 对象。

getDate 获得一个月的第几天
getDay 获得一个星期的第几天 (0 - 6)
getMonth  获取月份 (0 - 11)
getFullYear 获取年份

而对于比较 Date 对象的构造器有一个非常有意思的地方,那就是越界了之后,Date 会帮你自动校正日期。

new Date()
Sun Apr 16 2017 13:25:50 GMT+0800 (中国标准时间)  // 当前日期  4/16 

new Date(2017, 4, 1)
Mon May 01 2017 00:00:00 GMT+0800 (中国标准时间)  // 日期 5/1 日

我们可以看到这里有一个陷阱,当我们给月份的参数是 4 的时候,其实它是五月份。

接下来我们来看一看日期的自动修正。

new Date(2017, 3, 0)
Fri Mar 31 2017 00:00:00 GMT+0800 (中国标准时间) // 3 月 31 日     

// 4 月份的第 0 日,就是 3 月份的最后一日 

new Date(2017, 3, 1)
Sat Apr 01 2017 00:00:00 GMT+0800 (中国标准时间) // 4 月 1 日

new Date(2017, 4, 0)
Sun Apr 30 2017 00:00:00 GMT+0800 (中国标准时间) // 4 月 30 日

// 5 月份的 0 日,就是 4 月份的最后一日

new Date(2017, 3, 31)
Mon May 01 2017 00:00:00 GMT+0800 (中国标准时间) // 5 月 1 日

4 月份只有 30 日, 当为 31 日的时候,变成了 5月份的 1 日

通过这些我们就可以获取当前月份的最后一天,以及上个月最后一天。

  1. 首先我们准备一个基本的静态页面
    <div class="field">
         <input type="text" value="" id="date-input" placeholder="请输入日期"/>
         <div class="date-select">
            <table>
                <caption>
                    <span id="prev-month"> < </span>
                    <small>2017 - 3</small>
                    <span id="next-month"> > </span>

                </caption>
                <tr>
                    <th>一</th>
                    <th>二</th>
                    <th>三</th>
                    <th>四</th>
                    <th>五</th>
                    <th>六</th>
                    <th>日</th>
                </tr>

                <tr>
                    <td>1</td>
                    <td>2</td>
                    <td>3</td>
                    <td>4</td>
                    <td>5</td>
                    <td>6</td>
                    <td>7</td>
                </tr>
            </table>
         </div>
    </div>
        *{padding: 0;margin: 0;}
        .field{
            width: 300px;
            margin: 20px auto;
            font-size: 12px;
            position: relative;
        }
        .field input{
            width: 100%;
            box-sizing: border-box;
            outline: none;
        }
        .date-select{
            background: #e8e8e8;
            color: #fff;
            cursor: pointer;
            transition: all .8s;
            position: absolute;
            width: 100%;
            z-index: 2;
            display: none;
        }

        .date-select table{
            width: 100%;
            border-collapse: collapse;
            text-align: center;
        }

        .date-select table caption:after{
            content: '';
            display: block;
            clear: both;
        }

        .date-select table caption{
            background: #999;
            line-height: 20px;
        }

        .date-select table th{
            background: #999;
        }

        .date-select table tr:first-child{
            line-height: 30px;
        }

        .date-select table tr td{
             border: 1px solid #f0f0f0;
        }

        .date-select table tr:not(:first-child){
            line-height: 25px;
        }

        .date-select table caption span#prev-month{
            float: left;
            padding-left: 10px;
        }
        .date-select table caption span#next-month{
            float: right;
            padding-right: 10px;
        }
        const prevMonthBtn = document.querySelector('#prev-month'),
              nextMonthBtn = document.querySelector('#next-month');

        const dateInput = document.querySelector('#date-input');

        const dateSelect = document.querySelector('.date-select');

        dateInput.onclick = () => {
            if(dateSelect.style.display == "block" ){
                dateSelect.style.display = "none";
                return;
            }
            dateSelect.style.display = "block";
        }

        prevMonthBtn.onclick = () => {
            console.log("prev");
        }

        nextMonthBtn.onclick = () => {
            console.log("next");
        }

到目前为止,我们已经可以看到一个雏形了,但是这是有形无神的,现在我们再来准备我们的神魂精华,也就是获取日期的逻辑。

        function get42ArrayDate(year, month){
            // month 是正常月份
            let now = year && month? new Date(year, month -1 ) : new Date();
            let _year = now.getFullYear();
            let _month = now.getMonth(); // Date 的月份比正常的月份小 1

            let firstDayDateObj = new Date(_year, _month, 1); // 当月第一天日期

            let _day = firstDayDateObj.getDay() == 0 ? 7 : firstDayDateObj.getDay(); // 修正一下星期的日期,当为 0 的时候说明为星期天

            let needFillDayCount = _day - 1; // 我们需要补的日期, 当我们一号为星期天的时候,也就是 _date 为 7,则我们需要把星期一到星期六的天数给补完

            let prevMonthLastDayDateObj = new Date(_year, _month, 0); // 上一个月最后一天的日期
            let currentMonthLastDayDateObj = new Date(_year, _month + 1, 0); // 当前月最后一天的日期

            let arr = Array.from({length:42}, (v, k) => k - needFillDayCount + 1 ) ; // 需要填充的数组 0 - 41 = 42 位  再加 1 是为了让 K 从 1开始 
            console.log(arr);
            let ret_arrObj = arr.map((val, index) => {
                // 现在我们对 负数日期,和超出限定的日期进行修正处理
                // 我们留下这个负数和超出的是为了让其他地方也可以使用这些数据通过 Date 直接构造正确的日期
                // 首先修正负数, 并且把正确的放在一个对象里面
                let prevMonthLastDay = prevMonthLastDayDateObj.getDate(); // 上个月最后一天的日期
                let currentMonthLastDay = currentMonthLastDayDateObj.getDate(); // 当前月最后一天
                let ret_obj = {
                    index: val
                };
                if( val <= 0 ){
                   // 对负数日期进行修正
                   ret_obj.showDate = prevMonthLastDay + val; // 此时的 val 是一个负数,当 -0 的时候就是上月最后一天, - 1就是倒数第二天,以此类推。
                   ret_obj.showMonth = _month; // showMonth 顾名思义应该为我们所理解的月份,也就是从1开始的,而 _month 是从 0 开始的,所以我们应该 + 1, 而这里是上个月的,所以要 -1 所以刚好抵消
                }else if (val > currentMonthLastDay){
                    // 对超出日期进行修正
                    ret_obj.showDate = val - currentMonthLastDay ; // 此时的 val 是超出正常日期的,我们让这个数减去单月的最后一天的值,相当于我们此时重新开始从 1 开始计数。 32 - 31 为 1号。 33 -31 为 2号, 以此类推。
                    ret_obj.showMonth = _month + 2;
                }else{
                    // 正常的本月日期
                    ret_obj.showDate = val;
                    ret_obj.showMonth = _month + 1;
                }

                return ret_obj;
            });

            return {
                currentMonth: _month + 1,
                currentYear: _year,
                dateArray: ret_arrObj
            }

        }

        console.dir(get42ArrayDate());

解释都在注释里面,仔细看一下注释,然后自己运行一下,看看结果正确不正确。

接下来我们要把一些鼠标点击的事件,必须要等到已经渲染完HTML结构之后才能绑定,所以绑定的逻辑我们需要修改一下位置。

顺便我们把我们的渲染逻辑给写好,如下。

为了把当月的日期明显一点,我们增加一点样式

        .date-select table tr td.current-month-day{
            background: #d6d6d6;
        }
function render(date){
            let code = `
            <table>
                <caption>
                    <span id="prev-month"> < </span>
                    <small>${date.currentYear} - ${date.currentMonth}</small>
                    <span id="next-month"> > </span>
                </caption>
                <tbody>
                    <tr>
                        <th>一</th>
                        <th>二</th>
                        <th>三</th>
                        <th>四</th>
                        <th>五</th>
                        <th>六</th>
                        <th>日</th>
                    </tr>
                    <tr>
            `;

            date.dateArray.forEach((item, index) => {
                if(index % 7 == 0) {
                    code += '</tr><tr>'
                }
                code += `<td class="${date.currentMonth == item.showMonth? 'current-month-day': ''}" data-index='${item.index}'>${item.showDate}</td>`
            });

            dateSelect.innerHTML = code;

           const prevMonthBtn = document.querySelector('#prev-month'),
                 nextMonthBtn = document.querySelector('#next-month');

            prevMonthBtn.onclick = () => {
                let month = date.currentMonth - 1;
                let year = date.currentYear;
                let prevMonthDate = get42ArrayDate(year, month);
                render(prevMonthDate); // 重新渲染数据
            }

            nextMonthBtn.onclick = () => {
                let month = date.currentMonth + 1;
                let year = date.currentYear;
                let nextMonthDate = get42ArrayDate(year, month);
                render(nextMonthDate); // 重新渲染数据
            }

            dateSelect.addEventListener('click', (e) => {
                let targetDom = e.target;
                console.dir(targetDom)
                if(targetDom.tagName == 'SPAN' || targetDom.tagName != 'TD') return;
                let selectDay = new Date(date.currentYear, date.currentMonth - 1, targetDom.dataset.index); // currentMonth是正常月份所以要减一
                const inputValue = selectDay.getFullYear() + " - " + (selectDay.getMonth() + 1) + " - " + selectDay.getDate();
                dateInput.value = inputValue;
                dateSelect.style.display = "none"; // 隐藏选择框
            },false)

        }
        let date = get42ArrayDate();
        render(date);