之前项目运用到了这个时间控件,期间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是一致的。

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

bootstrap-datetimepicker.js学习的更多相关文章

  1. bootstrap插件学习-bootstrap.typehead.js

    先看bootstrap.typehead.js的结构 var Typeahead = function ( element, options ){} //构造器 Typeahead.prototype ...

  2. bootstrap插件学习-bootstrap.carousel.js

    先看bootstrap.carousel.js的结构 var Carousel = function (element, options){} //构造器 Carousel.prototype = { ...

  3. bootstrap插件学习-bootstrap.collapse.js

    先看bootstrap.collapse.js的结构 var Collapse = function ( element, options ){} // 构造器 Collapse.prototype ...

  4. bootstrap插件学习-bootstrap.alert.js

    我们先看bootstrap.alert.js的结构 var dismiss = '[data-dismiss="alert"]' //自定义属性 Alert = function ...

  5. bootstrap插件学习-bootstrap.button.js

    先看bootstrap.button.js的结构 var Button = function ( element, options ){} //构造器 Button.prototype = {} // ...

  6. bootstrap插件学习-bootstrap.popover.js

    先看bootstrap.popover.js的结构 var Popover = function ( element, options ){} //构造器 Popover.prototype = {} ...

  7. bootstrap插件学习-bootstrap.scrollspy.js

    先看bootstrap.dropdown.js的结构 function ScrollSpy(){} //构造函数 ScrollSpy.prototype = {} //构造器的原型 $.fn.scro ...

  8. bootstrap插件学习-bootstrap.dropdown.js

    bootstrap插件学习-bootstrap.dropdown.js 先看bootstrap.dropdown.js的结构 var toggle = '[data-toggle="drop ...

  9. bootstrap插件学习-bootstrap.modal.js

    bootstrap插件学习-bootstrap.modal.js 先从bootstrap.modal.js的结构看起. function($){ var Modal = function(){} // ...

  10. Angular JS 学习之Bootstrap

    1.要使用Bootstrap框架,必须在<head>中加入链接: <link rel="stylesheet" href="//maxcdn.boots ...

随机推荐

  1. Sql Servicer 复习笔记(1) 存储过程分布

    第一步:创建表 declare @countInt int declare @age int ) begin ),@countInt), @age,'中国北京') ; ; ) begin ; end ...

  2. JAVA 求和程序

  3. 【转】WIN32 控制台程序

    http://blog.csdn.net/houmin0036/article/details/7702236 win32控制台项目指在32位Windows命令提示符(即所谓的dos)环境下运行的应用 ...

  4. 读入一个c程序,并按字母表顺序分组打印变量名,每组前N个字符相同(TCPL 练习6-2)

    在建立结构tnode的过程中,我们没有预设门槛.这道题目就设置了门槛,必须根据前N个字符来进行分组,于是排除了长度小于N的变量,以便减轻负担. 因为要求对变量名分组打印,组别理所应当地应该按照至少是升 ...

  5. FreeRTOS任务栈

    configTOTAL_HEAP_SIZE  定义堆大小,FreeRTOS 内核,用户动态内存申请,任务栈,任务创建,信号量创建,消息队列创建 等都需要用这个空间.

  6. Eclipse无法启动错误之Ensure that the org.eclipse.core.runtime bundle is resolved and started (see config.ini)

    悲剧,在安装Android Build Tools时,提醒需要关闭Eclipse进行安装,于是我在Tools安装完成后重启了Eclipse.但是Eclipse却无法启动,在log中有如下提示: Una ...

  7. 字符串反混淆实战 Dotfuscator 4.9 字符串加密技术应对策略

    因为手头需要使用一个第三方类库,网络上又找不到它的可用的版本,于是只好自己动手.这个类库使用了Dotfuscator 加密,用.NET Reflector加载程序集, 看到的字符串是乱码,如下面的代码 ...

  8. 【Win10 UWP】QQ SDK(二):SDK的回调处理

    上一讲,我们介绍了QQ SDK的使用方法,请看<[Win10 UWP]QQ SDK(一):SDK基本使用方法> 一. 回调的基本形式 从前面的介绍中我们知道,我们的应用和QQ客户端之间需要 ...

  9. 开始VS 2012 中LightSwitch系列的第1部分:表中有什么?描述你的数据

    [原文发表地址]  Beginning LightSwitch in VS 2012 Part 1: What’s in a Table? Describing Your Data [原文发表时间]  ...

  10. Windows 下使用git 将代码托管到开源中国-(http://git.oschina.net/)

    一.准备工作 当然是准备在windows 下使用需要的环境,和工具. msysgit  下载地址:http://msysgit.github.io/ TortoiseGit 下载地址:https:// ...