zoukankan      html  css  js  c++  java
  • bootstrap-datetimepicker.js学习

    之前项目运用到了这个时间控件,期间bug还是一些。抽个时间,简单地看一下。

    先看一下datetimepicker.js的结构

    var DateTimePicker = function(element, options){}//构造器
    var dateToDate = function(dt){}
    DateTimePicker.prototype ={}//构造器的原型
    $.fn.datetimepicker = function ( option, val ){}//jQuery原型对象上的方法
    $.fn.datetimepicker.defaults ={}//默认配置参数
    $.fn.datetimepicker.Constructor = DateTimePicker;
    //以下是一些默认信息和配置内容
    var dpgId = 0;
    var dates = $.fn.datetimepicker.dates = {}
    var dateFormatComponents = {}
    function escapeRegExp(str){}
    ....//自定义方法
    var DPGlobal ={}
    DPGlobal.template ='' //日期控件页面
    var TPGlobal = {}
    TPGlobal.getTemplate = function(is12Hours, showSeconds) {}//时分秒控件页面模版

    来看一下HTML的例子

    <!DOCTYPE HTML>
    <html>
    <head>
        <link href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.2.2/css/bootstrap-combined.min.css" rel="stylesheet">
        <link rel="stylesheet" type="text/css" media="screen"
              href="datepicker.css">
    </head>
    <body>
    <div id="datetimepicker" class="input-append date">
        <input type="text"/>
          <span class="add-on">
            <i data-time-icon="icon-time" data-date-icon="icon-calendar"></i>
          </span>
    </div>
    <script type="text/javascript"
            src="http://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.3/jquery.min.js">
    </script>
    <script type="text/javascript"
            src="bootstrap.js">
    </script>
    <script type="text/javascript"
            src="bootstrap-dateTimePicker.js">
    </script>
    <script type="text/javascript">
        $('#datetimepicker').datetimepicker({
            format: 'MM/dd/yyyy hh:mm',
            language: 'en',
            pickDate: true,
            pickTime: true,
            hourStep: 1,
            minuteStep: 15,
            secondStep: 30,
            inputMask: true
        });
    </script>
    </body>
    <html>

    可以看到页面上调用了datetimepicker方法,这个插件在页面使用时,需要手动初始化。下面简单地列出插件运作流程。

     在我们看init方法之前,先看一下传入default信息

    我们再来看一下init方法

    init: function(element, options) {
                var icon;
                if (!(options.pickTime || options.pickDate))
                    throw new Error('Must choose at least one picker');
                this.options = options;
                this.$element = $(element);
                this.language = options.language in dates ? options.language : 'en';
                this.pickDate = options.pickDate;//true
                this.pickTime = options.pickTime;//true
                this.isInput = this.$element.is('input');//判断是否为input控件
                this.component = false;
                if (this.$element.find('.input-append') || this.$element.find('.input-prepend'))
                    this.component = this.$element.find('.add-on');//获得触发时间控件的按钮
                this.format = options.format;//控件显示日期的格式
                if (!this.format) {
                    if (this.isInput) this.format = this.$element.data('format');
                    else this.format = this.$element.find('input').data('format');//寻找input控件data属性定义的时间格式
                    if (!this.format) this.format = 'MM/dd/yyyy';//如果都没有定义,采用系统默认格式MM/dd/yyyy
                }
                this._compileFormat();//根据日期显示格式,封装正则表达式,拼接正则表达式
                if (this.component) {
                    icon = this.component.find('i');//找到控件上的时间小标签(图标)
                }
                if (this.pickTime) {
                    if (icon && icon.length) this.timeIcon = icon.data('time-icon');
                    if (!this.timeIcon) this.timeIcon = 'icon-time';//如果页面上没有写data-time-icon,
                    icon.addClass(this.timeIcon);//这里系统默认帮你填上,类名为icon-time
                }
                if (this.pickDate) {
                    if (icon && icon.length) this.dateIcon = icon.data('date-icon');
                    if (!this.dateIcon) this.dateIcon = 'icon-calendar';//如果页面上没有写data-date-icon属性。
                    icon.removeClass(this.timeIcon);//系统将统一添加icon-calendar类,删除icon-time类
                    icon.addClass(this.dateIcon);//类名为icon-calendar
                }
                //拼接完控件页面插入body中,返回拼接的jQuery的dom对象
                this.widget = $(getTemplate(this.timeIcon, options.pickDate, options.pickTime, options.pick12HourFormat, options.pickSeconds, options.collapse)).appendTo('body');
                this.minViewMode = options.minViewMode||this.$element.data('date-minviewmode')||0;
                if (typeof this.minViewMode === 'string') {
                    switch (this.minViewMode) {
                        case 'months':
                            this.minViewMode = 1;
                            break;
                        case 'years':
                            this.minViewMode = 2;
                            break;
                        default:
                            this.minViewMode = 0;
                            break;
                    }
                }
                this.viewMode = options.viewMode||this.$element.data('date-viewmode')||0;
                if (typeof this.viewMode === 'string') {
                    switch (this.viewMode) {
                        case 'months':
                            this.viewMode = 1;
                            break;
                        case 'years':
                            this.viewMode = 2;
                            break;
                        default:
                            this.viewMode = 0;
                            break;
                    }
                }
                this.startViewMode = this.viewMode;
                this.weekStart = options.weekStart||this.$element.data('date-weekstart')||0;
                this.weekEnd = this.weekStart === 0 ? 6 : this.weekStart - 1;
                this.setStartDate(options.startDate || this.$element.data('date-startdate'));//设置StartDate
                this.setEndDate(options.endDate || this.$element.data('date-enddate'));// 设置endDate
                this.fillDow();//生成星期标题
                this.fillMonths();//生成月份标题
                this.fillHours();//生成小时面板
                this.fillMinutes();//生成分钟面板
                this.fillSeconds();//生成秒钟面板
                this.update();//填写面板
                this.showMode();//显示默认面板
                this._attachDatePickerEvents();//绑定触发事件
            }

    再看一下_compileFormat()方法

    _compileFormat: function () {
                var match, component, components = [], mask = [],
                    str = this.format, propertiesByIndex = {}, i = 0, pos = 0;
                while (match = formatComponent.exec(str)) {
                    component = match[0];
                    if (component in dateFormatComponents) {
                        i++;
                        propertiesByIndex[i] = dateFormatComponents[component].property;//取property属性
                        components.push('\s*' + dateFormatComponents[component].getPattern( //根据component取到getPattern中返回字符串正则,加进行拼接
                            this) + '\s*');
                        mask.push({ //重新装箱
                            pattern: new RegExp(dateFormatComponents[component].getPattern(
                                this)),
                            property: dateFormatComponents[component].property,
                            start: pos,//日期格式长度从第0位开始
                            end: pos += component.length//结束位置,是该结构的长度
                        });
                    }
                    else {
                        components.push(escapeRegExp(component));
                        mask.push({
                            pattern: new RegExp(escapeRegExp(component)),
                            character: component,
                            start: pos,
                            end: ++pos//特殊字符,一般都是一位
                        });
                    }
                    str = str.slice(component.length);//删掉已经匹配处理过的字符串,然后继续循环,这个匹配完这个字符串
                }
                this._mask = mask;//将封装过的信息传给实例
                this._maskPos = 0;
                this._formatPattern = new RegExp(
                    '^\s*' + components.join('') + '\s*$');//最后将加工过的正则再拼成一个大的正则,传给实例
                this._propertiesByIndex = propertiesByIndex;
            }

    以上的内容还是比较简单,我们需要配合默认参数来看。

    dateFormatComponents:

    最后拼接成一个大的正则表达式。正则比较长,但比较简单

    再看一下getTemplate方法

    //获取时间控件页面(这里将时间控件页面分成两块,1是日期页面,2是时分秒页面)
        function getTemplate(timeIcon, pickDate, pickTime, is12Hours, showSeconds, collapse) {
            //这里是可以选择的是否使用date或time,通过配置pickDate和pickTime来控制
            if (pickDate && pickTime) {
                return (//拼接时间控件的html
                    '<div class="bootstrap-datetimepicker-widget dropdown-menu">' +
                        '<ul>' +
                        '<li' + (collapse ? ' class="collapse in"' : '') + '>' +
                        '<div class="datepicker">' +
                        DPGlobal.template + //DPGlobal.template是一个日期页面
                        '</div>' +
                        '</li>' +
                        '<li class="picker-switch accordion-toggle"><a><i class="' + timeIcon + '"></i></a></li>' +
                        '<li' + (collapse ? ' class="collapse"' : '') + '>' +
                        '<div class="timepicker">' +
                        TPGlobal.getTemplate(is12Hours, showSeconds) +
                        '</div>' +
                        '</li>' +
                        '</ul>' +
                        '</div>'
                    );
            } else if (pickTime) {
                return (
                    '<div class="bootstrap-datetimepicker-widget dropdown-menu">' +
                        '<div class="timepicker">' +
                        TPGlobal.getTemplate(is12Hours, showSeconds) +
                        '</div>' +
                        '</div>'
                    );
            } else {
                return (
                    '<div class="bootstrap-datetimepicker-widget dropdown-menu">' +
                        '<div class="datepicker">' +
                        DPGlobal.template +
                        '</div>' +
                        '</div>'
                    );
            }
        }

    以上的代码是生成插件模版,以上分为两种控件页面模版(日期控件页面和时分秒控件页面模版)

    //日期控件页面
        DPGlobal.template =
            '<div class="datepicker-days">' +
                '<table class="table-condensed">' +
                DPGlobal.headTemplate +
                '<tbody></tbody>' +
                '</table>' +
                '</div>' +
                '<div class="datepicker-months">' +
                '<table class="table-condensed">' +
                DPGlobal.headTemplate +
                DPGlobal.contTemplate+
                '</table>'+
                '</div>'+
                '<div class="datepicker-years">'+
                '<table class="table-condensed">'+
                DPGlobal.headTemplate+
                DPGlobal.contTemplate+
                '</table>'+
                '</div>';

    上图就是日期控件,注意,这里只是生成标题,类似Su,Mo,Tu,We,Th,Fr,Sa。具体的下面的日期需要靠别的方法往里面填写,init方法里还有几个方法是创建方法

     fillDow方法

    //生成星期标题
            fillDow: function() {
                var dowCnt = this.weekStart;
                var html = $('<tr>');
                while (dowCnt < this.weekStart + 7) {
                    html.append('<th class="dow">' + dates[this.language].daysMin[(dowCnt++) % 7] + '</th>');
                }//生成
                this.widget.find('.datepicker-days thead').append(html);//找到thead插入
            }

    注意这里while的循环的,可以看到循环7次。以上的代码,可以生成如下的插件内容:

    fillMonths方法

    //生成月份标题
            fillMonths: function() {
                var html = '';
                var i = 0;
                while (i < 12) {
                    html += '<span class="month">' + dates[this.language].monthsShort[i++] + '</span>';
                }
                this.widget.find('.datepicker-months td').append(html);
            }

    生成的方式跟星期标题一致,循环了12次,再看一下生成的插件内容:

    fillHours方法

    //生成小时选择标题
            fillHours: function() {
                var table = this.widget.find(
                    '.timepicker .timepicker-hours table');
                table.parent().hide();//将小时选择面板隐藏
                var html = '';
                if (this.options.pick12HourFormat) {
                    var current = 1;
                    for (var i = 0; i < 3; i += 1) {
                        html += '<tr>';
                        for (var j = 0; j < 4; j += 1) {
                            var c = current.toString();
                            html += '<td class="hour">' + padLeft(c, 2, '0') + '</td>';
                            current++;
                        }
                        html += '</tr>'
                    }
                } else {
                    var current = 0;
                    for (var i = 0; i < 6; i += 1) {//循环24次,完成小时面板上小时显示
                        html += '<tr>';
                        for (var j = 0; j < 4; j += 1) {
                            var c = current.toString();//转成字符串
                            html += '<td class="hour">' + padLeft(c, 2, '0') + '</td>';
                            current++;//js是弱类型语言
                        }
                        html += '</tr>'
                    }
                }
                table.html(html);
            }

    这里有一个padLeft方法,我们来看一下:

    function padLeft(s, l, c) {//如何长度是1为,就采用0与数字组合,如果数字长度为两位,则直接返回这个数字
            if (l < s.length) return s;
            else return Array(l - s.length + 1).join(c || ' ') + s;
        }

    看一下这个fillHour方法,两个for循环一共运行了24次,大家可以想到。一天是24个小时,将类似1,2的小时点数通过padLeft方法转成01,02..。然后往table中插入,给出以上代码生成的插件内容

    fillMinutes方法

    //生成分钟选择标题
            fillMinutes: function() {
                var table = this.widget.find(
                    '.timepicker .timepicker-minutes table');
                table.parent().hide();
                var html = '';
                var current = 0;
                for (var i = 0; i < 5; i++) {//循环20次,每次添加3,完成60的遍历
                    html += '<tr>';
                    for (var j = 0; j < 4; j += 1) {
                        var c = current.toString();
                        html += '<td class="minute">' + padLeft(c, 2, '0') + '</td>';
                        current += 3;
                    }
                    html += '</tr>';
                }
                table.html(html);
            }

    基本和fillHour方法一致。遍历20次,每次添加3,完成60的遍历,正好对应1个小时是60分钟。生成的插件内容。

    fillSecond方法

    //生成秒钟选择标题
            fillSeconds: function() {
                var table = this.widget.find(
                    '.timepicker .timepicker-seconds table');
                table.parent().hide();
                var html = '';
                var current = 0;//给分钟类似
                for (var i = 0; i < 5; i++) {
                    html += '<tr>';
                    for (var j = 0; j < 4; j += 1) {
                        var c = current.toString();
                        html += '<td class="second">' + padLeft(c, 2, '0') + '</td>';
                        current += 3;
                    }
                    html += '</tr>';
                }
                table.html(html);
            }

    生成的插件内容为:

    以上的内容跟分钟内容是一致的。但它们是两个页面。

    再来看一下update方法

    //填写面板
            update: function(newDate){
                var dateStr = newDate;
                if (!dateStr) {
                    if (this.isInput) {//是否是input控件
                        dateStr = this.$element.val();
                    } else {
                        dateStr = this.$element.find('input').val();//取到input里的内容信息
                    }
                    if (dateStr) {
                        this._date = this.parseDate(dateStr);
                    }
                    if (!this._date) {
                        var tmp = new Date()
                        this._date = UTCDate(tmp.getFullYear(),
                            tmp.getMonth(),
                            tmp.getDate(),
                            tmp.getHours(),
                            tmp.getMinutes(),
                            tmp.getSeconds(),
                            tmp.getMilliseconds())
                    }
                }
                this.viewDate = UTCDate(this._date.getUTCFullYear(), this._date.getUTCMonth(), 1, 0, 0, 0, 0);
                this.fillDate();//填写月份面板和年份面板
                this.fillTime();//填写小时面板
            }

    这里我们可以看到this._date是input(插件中)里的内容,如果我们第一次使用插件,肯定没有任何时间信息,那这个时候this._date则等于当前时间(new Date()),这里我们再看一下UTCDate()方法

    function UTCDate() {
            return new Date(Date.UTC.apply(Date, arguments));
        }

    Date.UTC方法是可根据世界时返回 1970 年 1 月 1 日 到指定日期的毫秒数。在将这个毫秒数传入Date中,再转成UTC格式的时间,这个方法的作用是通过传入年,月,日,小时,分钟,秒钟,最后转成UTC格式的日期。

    fillDate方法,这个方法我们分成两个部分来看。

    先看第一部分:

    //生成月份面板和年份面板
            fillDate: function() {
                var year = this.viewDate.getUTCFullYear();//获取当前日期的年份
                var month = this.viewDate.getUTCMonth();//获取当前日期所在的月份
                var currentDate = UTCDate(
                    this._date.getUTCFullYear(),
                    this._date.getUTCMonth(),
                    this._date.getUTCDate(),
                    0, 0, 0, 0
                );//获取当前日期
    
                var startYear  = typeof this.startDate === 'object' ? this.startDate.getUTCFullYear() : -Infinity;
                var startMonth = typeof this.startDate === 'object' ? this.startDate.getUTCMonth() : -1;
                var endYear  = typeof this.endDate === 'object' ? this.endDate.getUTCFullYear() : Infinity;
                var endMonth = typeof this.endDate === 'object' ? this.endDate.getUTCMonth() : 12;
    
                this.widget.find('.datepicker-days').find('.disabled').removeClass('disabled');
                this.widget.find('.datepicker-months').find('.disabled').removeClass('disabled');
                this.widget.find('.datepicker-years').find('.disabled').removeClass('disabled');
    
    
                this.widget.find('.datepicker-days th:eq(1)').text(
                    dates[this.language].months[month] + ' ' + year);//根据input控件里所填写的信息生成日期标题
    
    
                var prevMonth = UTCDate(year, month-1, 28, 0, 0, 0, 0);//获取上一个月的内容
                var day = DPGlobal.getDaysInMonth(
                    prevMonth.getUTCFullYear(), prevMonth.getUTCMonth());
                prevMonth.setUTCDate(day);//获得的前一个月的天数,将这个天数赋给这个prevMonth
                prevMonth.setUTCDate(day - (prevMonth.getUTCDay() - this.weekStart + 7) % 7);
                if ((year == startYear && month <= startMonth) || year < startYear) {
                    this.widget.find('.datepicker-days th:eq(0)').addClass('disabled');
                }
                if ((year == endYear && month >= endMonth) || year > endYear) {
                    this.widget.find('.datepicker-days th:eq(2)').addClass('disabled');
                }
    
    
                var nextMonth = new Date(prevMonth.valueOf());
                nextMonth.setUTCDate(nextMonth.getUTCDate() + 42);//设置下一个月
                nextMonth = nextMonth.valueOf();
                var html = [];
                var row;
                var clsName;
                while (prevMonth.valueOf() < nextMonth) {
                    if (prevMonth.getUTCDay() === this.weekStart) {
                        row = $('<tr>');
                        html.push(row);
                    }
                    clsName = '';
                    if (prevMonth.getUTCFullYear() < year ||
                        (prevMonth.getUTCFullYear() == year &&
                            prevMonth.getUTCMonth() < month)) {//如果不是当前月的日期时,是上一个月则需要加上old类,灰化效果
                        clsName += ' old';
                    } else if (prevMonth.getUTCFullYear() > year ||
                        (prevMonth.getUTCFullYear() == year &&
                            prevMonth.getUTCMonth() > month)) {//如果是下一个月的则需要加上new类,也是灰化效果
                        clsName += ' new';
                    }
                    if (prevMonth.valueOf() === currentDate.valueOf()) {//将当前被选中的日期加上active类,高亮效果
                        clsName += ' active';
                    }
                    if ((prevMonth.valueOf() + 86400000) <= this.startDate) {
                        clsName += ' disabled';
                    }
                    if (prevMonth.valueOf() > this.endDate) {
                        clsName += ' disabled';
                    }
                    row.append('<td class="day' + clsName + '">' + prevMonth.getUTCDate() + '</td>');//这里循环生成日期内容
                    prevMonth.setUTCDate(prevMonth.getUTCDate() + 1);//依次加1
                }
                this.widget.find('.datepicker-days tbody').empty().append(html);//先清空后再添加上信息

    this.widget.find('.datepicker-days th:eq(1)').text(dates[this.language].months[month] + ' ' + year);这里生成的插件内容

    其中生成October,是通过dates[this.language].month寻找对应的英语内容,下面是语言包内容

    var dates = $.fn.datetimepicker.dates = { //语言包,可以自己定义
            en: {
                days: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday",
                    "Friday", "Saturday", "Sunday"],
                daysShort: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
                daysMin: ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"],
                months: ["January", "February", "March", "April", "May", "June",
                    "July", "August", "September", "October", "November", "December"],
                monthsShort: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul",
                    "Aug", "Sep", "Oct", "Nov", "Dec"]
            }
        }

    while循环中使用html去保存生成的dom内容,使用prevMonth.getUTCDate()+1进行迭代。依次生成日期内容,插件这里提供了一个非常不错的思路:通过UTCDate生成标准的UTC日期,然后再通过DPGlobal.getDaysInMonth()获取这个月的天数,然后再去遍历天数显示出这个月的日期,至于页面上效果,通过比较当前月,设置出较之当前月的前一个月和后一个月灰化效果。那我们再看一下DPGlobal.getDaysInMonth()方法

    isLeapYear: function (year) {//判断是否是闰年
                return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
            },
            getDaysInMonth: function (year, month) {//获取某月的天数
                //简单地将十二个月的天数写在一个数组里,通过month标签获取该月数的天数
                return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
            }

    先比较是否是闰年,将一年的月份放入一个数组中,通过我们的month下标去获取月数的天数。

    ok,以下我们来看一下生成效果:

    至于高亮部分,是通过以下代码实现

    if (prevMonth.valueOf() === currentDate.valueOf()) {//将当前被选中的日期加上active类,高亮效果
                        clsName += ' active';
                    }
    row.append('<td class="day' + clsName + '">' + prevMonth.getUTCDate() + '</td>')

    再来看另一部分

    html = '';//清空html
                year = parseInt(year/10, 10) * 10;
                var yearCont = this.widget.find('.datepicker-years').find(
                    'th:eq(1)').text(year + '-' + (year + 9)).end().find('td');
                this.widget.find('.datepicker-years').find('th').removeClass('disabled');
                if (startYear > year) {
                    this.widget.find('.datepicker-years').find('th:eq(0)').addClass('disabled');
                }
                if (endYear < year+9) {
                    this.widget.find('.datepicker-years').find('th:eq(2)').addClass('disabled');
                }
                year -= 1;
                for (var i = -1; i < 11; i++) {
                    html += '<span class="year' + (i === -1 || i === 10 ? ' old' : '') + (currentYear === year ? ' active' : '') + ((year < startYear || year > endYear) ? ' disabled' : '') + '">' + year + '</span>';
                    year += 1;
                }
                //每一个年份面板上,将第一个和最后一个灰化,这里对于disabled不可用的情况,我们需要自己设定startYear和endYear的值才可以
                yearCont.html(html);//填入内容

     这里使用html保存dom内容,最后通过yearCont.html(html)插入html文档中,看一下生成内容

    高亮效果是通过如下代码实现:

    html += '<span class="year' + (i === -1 || i === 10 ? ' old' : '') + (currentYear === year ? ' active' : '') + ((year < startYear || year > endYear) ? ' disabled' : '') + '">' + year + '</span>';

    继续看fillTime方法

    //生成小时面板
            fillTime: function() {
                if (!this._date)
                    return;
                var timeComponents = this.widget.find('.timepicker span[data-time-component]');
                var table = timeComponents.closest('table');
                var is12HourFormat = this.options.pick12HourFormat;
                var hour = this._date.getUTCHours();//获取input控件中的小时数值
                var period = 'AM';
                if (is12HourFormat) { //判断AM和PM
                    if (hour >= 12) period = 'PM';
                    if (hour === 0) hour = 12;
                    else if (hour != 12) hour = hour % 12;
                    this.widget.find(
                        '.timepicker [data-action=togglePeriod]').text(period);
                }
    
                hour = padLeft(hour.toString(), 2, '0');
                var minute = padLeft(this._date.getUTCMinutes().toString(), 2, '0');
                var second = padLeft(this._date.getUTCSeconds().toString(), 2, '0');
                //填入相应的小时,分钟和秒钟
                timeComponents.filter('[data-time-component=hours]').text(hour);
                timeComponents.filter('[data-time-component=minutes]').text(minute);
                timeComponents.filter('[data-time-component=seconds]').text(second);
            }

    这个this._date我们之前认识过,它可以是input控件里的内容,如过控件中没有信息,那它则默认是当前系统日期。通过padLeft将1,2等字符串转成01,02等,最后将hour,minute和second写如dom中,看一下生成内容

    下面我们再来看init中的倒数第二个方法showMode方法

    showMode: function(dir) {
                if (dir) {
                    this.viewMode = Math.max(this.minViewMode, Math.min(
                        2, this.viewMode + dir));
                }
                this.widget.find('.datepicker > div').hide().filter(//这里依旧是先将year面板,month面板和days面板都隐藏了
                    '.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();//默认显示day面板
            }

    下面是最后一个方法_attachDatePickerEvents方法

    //绑定事件
            _attachDatePickerEvents: function() {
                var self = this;
                // this handles date picker clicks
                this.widget.on('click', '.datepicker *', $.proxy(this.click, this));//为datepicker下面所有的标签,绑定了this.click
                // this handles time picker clicks
                this.widget.on('click', '[data-action]', $.proxy(this.doAction, this));//这里存在bug,这里data-action属性存在小时面板也存在于小时(分钟,秒钟)详细面板
                //这里绑定了this.doAction
                this.widget.on('mousedown', $.proxy(this.stopEvent, this));//mousedown事件
                if (this.pickDate && this.pickTime) {
                    this.widget.on('click.togglePicker', '.accordion-toggle', function(e) {
                        e.stopPropagation();
                        var $this = $(this);
                        var $parent = $this.closest('ul');
                        var expanded = $parent.find('.collapse.in'); //这里主要是控制显示切换
                        var closed = $parent.find('.collapse:not(.in)');
    
    
                        if (expanded && expanded.length) {
                            var collapseData = expanded.data('collapse');
                            if (collapseData && collapseData.transitioning) return;
                            expanded.collapse('hide');//切换显示
                            closed.collapse('show');
                            $this.find('i').toggleClass(self.timeIcon + ' ' + self.dateIcon);
                            self.$element.find('.add-on i').toggleClass(self.timeIcon + ' ' + self.dateIcon);//修改
                        }
                    });
                }
                if (this.isInput) {
                    this.$element.on({
                        'focus': $.proxy(this.show, this),
                        'change': $.proxy(this.change, this)
                    });
                    if (this.options.maskInput) {
                        this.$element.on({
                            'keydown': $.proxy(this.keydown, this),
                            'keypress': $.proxy(this.keypress, this)
                        });
                    }
                } else {
                    this.$element.on({
                        'change': $.proxy(this.change, this)//为控件绑定change事件,调用了这个this.change方法
                    }, 'input');
                    if (this.options.maskInput) {
                        this.$element.on({
                            'keydown': $.proxy(this.keydown, this),
                            'keypress': $.proxy(this.keypress, this)
                        }, 'input');
                    }
                    if (this.component){
                        this.component.on('click', $.proxy(this.show, this));//为add-on标签绑定事件,触发this.show
                    } else {
                        this.$element.on('click', $.proxy(this.show, this));
                    }
                }
            }

    这里出现了插件第一个比较大的bug

    this.widget.on('click', '[data-action]', $.proxy(this.doAction, this));

    点击空白处,导致出现这个问题:

    这个bug主要是由于事件绑定而导致的我在注释中也说明了。如果想修改这个bug,方法也非常多,可以添加新class,在重新绑定新class事件。也有其他方法,大家酌情自己修改吧。

    关于this.widget.on('click.togglePicker', '.accordion-toggle', function(e) {})这句的作用,其实就是小时面板跟日期面板的切换,如下图:

    仔细的话,大家可以发现,只要我们点击了a标签,其外的li就会套上in类,让其显示,其他的则删去in类,让其隐藏,这一做法跟bootstrap的其他插件差不多。

    ok,到此绑定完事件,我们基本结束了datetimepicker的初始化工作了,下面就是简单地看一下触发事件了

    我们先列举attachDatePickerEvents方法中所出现过的触发事件方法

    1.this.click

    2.this.doAction

    3.this.stopEvent

    4.this.show

    5.this.change

    6.this.keydown

    7.this.keypress

    触发事件

    1.click

    在我们看click源码之前,我们有必要先看一下,这个插件为哪些控件绑定了click事件。

    this.widget.on('click', '.datepicker *', $.proxy(this.click, this));可以看出为类datepicker 以下的所有标签绑定click事件,那么这个拥有类datepicker的面板主要有哪些呢?

    日期面板:                                             月份面板:                                 年份面板:

              

     从简单开始,我们可以尝试点击每个面板上的标题,左右按钮,具体的哪个日期(月份,年份)

    1.1 面板标题

    如果我们点击了任意面板的标题,就会进入这个插件设置好的层级菜单(面板),如上图,我们将日期,月份,年份分别列出,实际上在datetimepicker这个插件内部,将这三个面板分别定义成三个层级,日期面板为0级菜单(面板),月份面板为1级菜单(面板),年份面板为2级菜单(面板),也就是说当我们点击日期面板上的October 2010这个标题时,就会进入1级菜单(月份面板),依次类推,如果我们在年份面板上选择了2010,那插件会带我们从2级菜单回到1级菜单选择月份。这就是这个插件的升降级的处理流程。具体看一下代码:

    case 'switch':
                                        this.showMode(1);
                                        break;

    如果点击的是面板标题,我们会进入showMode这个方法,并传入1这个参数

    //在日期面板,月份面板和年份面板间切换时,会调用这个方法,其传入的参数,作为降级处理
            showMode: function(dir) {
                if (dir) {
                    this.viewMode = Math.max(this.minViewMode, Math.min(
                        2, this.viewMode + dir));//这段代码完成降级处理,何为降级处理,即你选择完年份之后,会自动进入月份面板。选择完月份之后,会自动进入日期面板,层级递减
                }
                this.widget.find('.datepicker > div').hide().filter(//这里依旧是先将year面板,month面板和days面板都隐藏了
                    '.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();//默认显示day面板
                //根据降级处理过的viewMode数值在DPGlobal.model查找到对应的需要跳转的面板名称,跳转到这个面板
            }

    这里简单说一下代码如何控制升降级的,我们首先传入参数1,默认this.viewMode为0,当我们点击了日期的标题时,this.viewMode+1为1进入1级菜单,我们再点击月份面板的标题时,传入的参数为1,则这时的this.viewMode为2,进入2级菜单,如果此时我们再点击年份面板的标题时,传入参数依旧是1,但是注意了这里this.viewMode还是2,如果this.viewMode超过2了,就会选择2.

    Math.min(2, this.viewMode + dir)

    试想一下,如果this.viewMode无限减一怎么办,也没有关系,如果this.viewMode为0级时,也就是说你已经跳到日期面板时,将无法往下跳级了。

    Math.max(this.minViewMode, Math.min(2, this.viewMode + dir))

    代码中,最小取0,避免了无限往下跳级的情况,这里出现这个插件第二个比较蛋疼的bug,就是插件没有为0级菜单绑定关闭插件的方法,导致用户需要点击网页空白处才能完成。

    1.2  左右按钮

    这个比较好理解,看一下代码:

    case 'prev'://左右切换月份,年份,或者一起切换
    case 'next':
    var vd = this.viewDate;//获取当前时间(input里的或者是系统时间)
    var navFnc = DPGlobal.modes[this.viewMode].navFnc;//根据层级,选择需要切换是月份还是年份
    var step = DPGlobal.modes[this.viewMode].navStep;
    if (target[0].className === 'prev') step = step * -1;
    vd['set' + navFnc](vd['get' + navFnc]() + step);//进行月份或着年份的递减或递增
    this.fillDate();//显示在插件面板上
    this.set();//显示在input中
    break;

    因为3个面板都存在左右切换的按钮,所以我们在使用的时候需要告诉插件,是哪一个面板,插件如何办到了?很简单,插件通过层级this.viewMode来在DPGlobal中查找相应的面板名称。经过fillDate处理在页面显示,再经过set在input框中显示

    //将选中信息填入input控件中
            set: function() {
                var formatted = '';
                if (!this._unset) formatted = this.formatDate(this._date);
                if (!this.isInput) {
                    if (this.component){
                        var input = this.$element.find('input');
                        input.val(formatted);//将选择出来的信息写入input框中
                        this._resetMaskPos(input);
                    }
                    this.$element.data('date', formatted);//保存选中信息
                } else {
                    this.$element.val(formatted);
                    this._resetMaskPos(this.$element);
                }
            }

    1.3  具体那个日期(月份,年份)

    1.3.1 日期

    case 'td'://点击日期
                                if (target.is('.day')) {
                                    var day = parseInt(target.text(), 10) || 1;//转成整型=
                                    var month = this.viewDate.getUTCMonth();//当前月份-1(以下可以为系统默认当前时间)
                                    var year = this.viewDate.getUTCFullYear();//当前年数
                                    if (target.is('.old')) {//如果你选择旧的日期
                                        if (month === 0) {//如果当前日期为1月,那我们点击上一个月的日期时,月份需要变为11(即12月份),年数则需要减一
                                            month = 11;
                                            year -= 1;
                                        } else {
                                            month -= 1;//如果当前日期不为1月,那我们点击一个月的日期时,月份只需要减一
                                        }
                                    } else if (target.is('.new')) {//如果你选择新的日期
                                        if (month == 11) {//如果当前日期为12月时,那当我们点击下一个月的日期时,月份需要变为0(即1月份),年数则需要加一
                                            month = 0;
                                            year += 1;
                                        } else {
                                            month += 1;//如果当前日期不为12月时,那当我们点击一下月的日期时,月份则只需要加一即可
                                        }
                                    }
                                    this._date = UTCDate(
                                        year, month, day,
                                        this._date.getUTCHours(),
                                        this._date.getUTCMinutes(),
                                        this._date.getUTCSeconds(),
                                        this._date.getUTCMilliseconds()
                                    );
                                    this.viewDate = UTCDate(
                                        year, month, Math.min(28, day) , 0, 0, 0, 0);
                                    this.fillDate();
                                    this.set();
                                    this.notifyChange();
                                }

    逻辑在注释上写的很清楚了,不是很难。最后经过fillDate渲染,set显示在input框中。

    1.3.2 月份和年份

    case 'span':
                                if (target.is('.month')) {//这里控制的是月份面板
                                    var month = target.parent().find('span').index(target);
                                    this.viewDate.setUTCMonth(month);//将你选择的月份赋给viewDate,等会处理完显示在input控件上
                                } else {//这里是年份面板
                                    var year = parseInt(target.text(), 10) || 0;
                                    this.viewDate.setUTCFullYear(year);//将你选择的年份赋给viewDate,等会处理完显示在input控件上
                                }
                                if (this.viewMode !== 0) {//考虑几级菜单,这个插件认为日期面板属于0级菜单,月份属于1级,年份属于2级
                                    this._date = UTCDate(
                                        this.viewDate.getUTCFullYear(),
                                        this.viewDate.getUTCMonth(),
                                        this.viewDate.getUTCDate(),
                                        this._date.getUTCHours(),
                                        this._date.getUTCMinutes(),
                                        this._date.getUTCSeconds(),
                                        this._date.getUTCMilliseconds()
                                    );
                                    this.notifyChange();
                                }
                                this.showMode(-1);//降级操作
                                this.fillDate();//降级完,将该面板数据呈现出来
                                this.set();
                                break;

    这里出现了降级处理。

    2. doAction

    先看一下,插件中哪些部分绑定了doAction方法。this.widget.on('click', '[data-action]', $.proxy(this.doAction, this));这里我们可以知道是拥有data-action属性的标签拥有可以触发这个doAction方法,具体到插件的显示部分,我们来看一下

    小时面板                                                小时子菜单                                         分钟子菜单                                      秒钟子菜单

                           

    之前说过这里存在bug,都在子菜单中存在,点击空白处会出现Nan,对于修改,我们最后统一修改

    //关于小时面板的层级跳转控制
            doAction: function(e) {
                e.stopPropagation();
                e.preventDefault();
                if (!this._date) this._date = UTCDate(1970, 0, 0, 0, 0, 0, 0);
                var action = $(e.currentTarget).data('action');
                var rv = this.actions[action].apply(this, arguments);
                this.set();
                this.fillTime();//通过这个fillTime显示出来
                this.notifyChange();
                return rv;
            }

    如果我们仔细看一下小时面板和各个子菜单。我们可以看到它们的data-action后面的内容都不相同,这就是插件去判断到底是谁点击了,响应谁的一个标记。看一下action,这里我给出结构

    action:{
      //小时加1(当前时间)
      incrementHours: function(e) {}  
      //分钟加1(当前时间)
      incrementMinutes: function(e) {}
      //秒钟加1(当前时间)
      incrementSeconds: function(e) {}
      //小时减1(当前时间)
      decrementHours: function(e) {}
      //分钟减1(当前时间)
      decrementMinutes: function(e){}
      //秒钟减1(当前时间)
      decrementSeconds: function(e){}
      togglePeriod: function(e) {}
      //将所有从小时,分钟,秒钟的子菜单跳转到小时面板
     showPicker: function() {}
      //显示关于小时的子菜单(面板)
     showHours: function() {}
      //显示关于分钟的子菜单(面板)
      showMinutes: function() {}
      //显示关于秒针的子菜单(面板)
      showSeconds: function() {}
      //小时子菜单中获取用户选择的小时信息
     selectHour: function(e) {}
      //分钟子菜单中获取用户选择的分钟信息
      selectMinute: function(e) {}
      //秒钟子菜单中获取用户选择的秒钟信息
      selectSecond: function(e) {}
    }

    action中每一个属性名对应了data-action=后面的值,这样可以调用相应的方法,上面的分为show,select,增减三大类。增减总要是到时分秒进行增减,最后还是要通过fillTime显示出来。show之类的方法主要是跳转,因为小时面板中存在3个子菜单,如果我们在某个子菜单中选择了一个值,那就需要跳转到小时面板上,这里没有之前通过层级控制,而是简单的show和hide实现。其中showHours,showMinutes,showSeconds是跳转到相应的子菜单的,而showPicker方法是从任何子菜单跳回到小时面板。最后是select类的方法,主要是获取各个子菜单上的选择的信息。调用showPicker方法,返回小时面板。

    3. stopEvent

    //阻止冒泡和默认行为
            stopEvent: function(e) {
                e.stopPropagation();
                e.preventDefault();
            }

    。。很刚很生猛的方法。

    4. show

    //显示
            show: function(e) {
                this.widget.show();//整个插件显示
                this.height = this.component ? this.component.outerHeight() : this.$element.outerHeight();
                this.place();
                this.$element.trigger({
                    type: 'show',
                    date: this._date
                });
                this._attachDatePickerGlobalEvents();
                if (e) {
                    e.stopPropagation();
                    e.preventDefault();
                }
            }

    首先这个show方法,先将整个插件显示出来。这个有一个place方法和_attachDatePickerGlobalEvents方法

    这里的place方法,主要是控制控件的显示位置,_attachDatePickerGlobalEvents则主要是绑定hide方法和resize事件

    //在show方法绑定的事件
            _attachDatePickerGlobalEvents: function() {
                $(window).on(
                    'resize.datetimepicker' + this.id, $.proxy(this.place, this));
                if (!this.isInput) {
                    $(document).on(
                        'mousedown.datetimepicker' + this.id, $.proxy(this.hide, this));//将关闭事件绑定到了文档中
                }
            }

    这里可以看到,我们只有点击网页空白处,才能完成关闭插件的效果。这里刚才我们提到的第二个bug了。既然说到了show方法,就提一下hide方法

    hide: function() {
                // Ignore event if in the middle of a picker transition
                var collapse = this.widget.find('.collapse')
                for (var i = 0; i < collapse.length; i++) {
                    var collapseData = collapse.eq(i).data('collapse');
                    if (collapseData && collapseData.transitioning)
                        return;
                }
                this.widget.hide();//隐藏掉整个控件
                this.viewMode = this.startViewMode;//层级归零
                this.showMode();//下次点击进入时,应该是零级面板
                this.set();
                this.$element.trigger({
                    type: 'hide',
                    date: this._date
                });
                this._detachDatePickerGlobalEvents();//删除datetimepicker下的mousedown绑定事件
            }

    基本都是擦屁股的事情,看一下_detachDatePickerGlobalEvents

    _detachDatePickerGlobalEvents: function () {
                $(window).off('resize.datetimepicker' + this.id);
                if (!this.isInput) {
                    $(document).off('mousedown.datetimepicker' + this.id);
                }
            }

    5. change

    这个方法主要是为了防止用户手动自定义修改input框中的内容,其中这个插件如此多此一举的行为,给了不好的用户体验,导致我直接在input中写入任意信息,鼠标点击空白处时,input框中自动转成当前日期,算是一个bug吧。建议整个input框不可以自定义填写。

    //控制用户自定义修改input内容
            change: function(e) {
                var input = $(e.target);
                var val = input.val();
                if (this._formatPattern.test(val)) {//满足一个之前定义好的标准的时间格式
                    this.update();
                    this.setValue(this._date.getTime());
                    this.notifyChange();
                    this.set();
                } else if (val && val.trim()) {//不满足时,用户修改了input中信息时,将修改为系统当前时间,个人觉得这个功能不好,这个input应该是不可自定义填写的
                    this.setValue(this._date.getTime());
                    if (this._date) this.set();//显示在input中
                    else input.val('');
                } else {
                    if (this._date) {
                        this.setValue(null);
                        // unset the date when the input is
                        // erased
                        this.notifyChange();
                        this._unset = true;
                    }
                }
                this._resetMaskPos(input);
            }

    这里有个setValue方法

    //如果input框中被修改了,如果不满足时间格式,将默认修改为系统时间
            setValue: function(newDate) {
                if (!newDate) {
                    this._unset = true;
                } else {
                    this._unset = false;
                }
                if (typeof newDate === 'string') {//如果是字符串类型的使用parseDate转
                    this._date = this.parseDate(newDate);
                } else if(newDate) {
                    this._date = new Date(newDate);//否则使用Date转
                }
                this.set();
                this.viewDate = UTCDate(this._date.getUTCFullYear(), this._date.getUTCMonth(), 1, 0, 0, 0, 0);
                this.fillDate();
                this.fillTime();
            }

    通过正则判断,发现input框中的信息不符合时间格式,那插件强行修改为当前日期,setValue方法的主要功能主要是更新时间。

    6. keydown

    7. keypress这里暂时不讨论

    至此整个插件算是勉强看完,下面留下了一些这个插件的bug,我们来总结一下:

    1.时分秒子菜单存在点击出现Nan的bug

    2.插件没有为0级菜单绑定关闭插件的方法,导致用户需要点击网页空白处才能完成。

    3.change事件监听较为繁琐,可以直接输入任何字符显示系统日期,建议去除。

    修改bootstrap-datetimepicker.js

    以下的修改是本人的一点想法,读者如果有更好的想法可以分享一下,我这里就抛砖引玉了,另外这个插件如果还有别的bug,也希望能够提出来,大家一起解决。

    对于第一个bug,我们知道是因为data-action属性放置的地方不对,不应该放置在div上,而是放置在div内的table上。所以我们只需要修改时分秒子菜单的模版即可

    TPGlobal.getTemplate = function(is12Hours, showSeconds) {
    ......
    '</tr>' +
                    '</table>' +
                    '</div>' +
                    '<div class="timepicker-hours" data-action="selectHour">' +
                    '<table class="table-condensed">' +
                    '</table>'+
                    '</div>'+
                    '<div class="timepicker-minutes" data-action="selectMinute">' +
                    '<table class="table-condensed">' +
                    '</table>'+
                    '</div>'+
                    (showSeconds ?
                        '<div class="timepicker-seconds" data-action="selectSecond">' +
                            '<table class="table-condensed">' +
                            '</table>'+
                            '</div>': '')
    }
    修改为
    '</tr>' +
                    '</table>' +
                    '</div>' +
                    '<div class="timepicker-hours">' +
                    '<table class="table-condensed" data-action="selectHour">' +
                    '</table>'+
                    '</div>'+
                    '<div class="timepicker-minutes">' +
                    '<table class="table-condensed" data-action="selectMinute">' +
                    '</table>'+
                    '</div>'+
                    (showSeconds ?
                        '<div class="timepicker-seconds">' +
                            '<table class="table-condensed" data-action="selectSecond">' +
                            '</table>'+
                            '</div>': '')
                );

    对于第二个bug,对于点击0级菜单不能关闭插件,我们找到day部分绑定了click触发事件,我们只要在click方法最后加上一个原型上的hide方法,就可以帮插件关闭。看一下:

    click: function(e) {
    ....
    case 'td'://点击日期
         if (target.is('.day')) {
    ....
    this.viewDate = UTCDate(
         year, month, Math.min(28, day) , 0, 0, 0, 0);
         this.fillDate();
         this.set();
         this.notifyChange();
         this.hide();//这里加上hide方法
        }
         break;
    }

    最后一个bug,这个我们可以直接在input上修改,将其改为只读就行

    <input type="text" disabled="disabled"/>

    ok,几个比较明显的bug改完,这个插件依旧还有一点东西需要我们再看一下。

    使用时间插件,会有一种情况就是,需要控制用户输入的日期,比如不让用户选择超过当今日期的,不得小于2012年10月1日的,前面的日期必须大于后面的日期等等,解决方法有很多,可以直接由插件控制,也可以在input框触发事件,脱离时间控件控制。bootstrap-datetimepicker.js提供了内部控制。我们只需要做的,仅仅在初始化时传入的参数中多一个startDate或者是endDate即可。这里插件还有一个不足之处,就是这传入的开始时间和结束时间需要格式化,js时间的格式话比较麻烦,插件本身拥有这个格式化的方法,但是没有公共出来,你可以自己写一个格式化方法,也可以将插件内的格式化方法公共出来。源码之后添加如下代码:

    window.UTCDate = UTCDate;

    看一下例子:

    //UTCDate(year, month, date, hours, minutes, seconds, milliseconds)
        var date1 = new Date();
        var date = UTCDate(2013,9,10);
        $('#datetimepicker').datetimepicker({
            format: 'MM/dd/yyyy hh:mm',
            language: 'en',
            pickDate: true,
            pickTime: true,
            hourStep: 1,
            minuteStep: 15,
            secondStep: 30,
            inputMask: true,
            startDate: date
        });

    插件内部提供了UTCDate方法来格式化时间,如例子所写的插件必须选择大于2013年10月9日的,注意月份会加1。endDate的道理和startDate是一致的。

     以上是本人的一点读码分析,不足之处还请指正。不胜感谢。

  • 相关阅读:
    隔行扫描 和 逐行扫描
    CSS3--关于z-index不生效问题
    vue与其他框架对比
    跨域(转)
    vue 事件修饰符(阻止默认行为和事件冒泡)
    vue 3.0新特性
    bash leetcode
    数据库
    css排版
    盒模型
  • 原文地址:https://www.cnblogs.com/wumadi/p/3363784.html
Copyright © 2011-2022 走看看