bootstrap-datetimepicker源代码分析
bootstrap-datetimepicker日期插件应用非常流行,如果对其源代码一无所知,在遇到意外问题时可能就有麻烦。
本人在应用datetimepicker时曾经遇到百思不得其解的问题,经历几天的源代码分析和测试,才最终得知原因和解决办法,闹半天,这么高大上的插件也是有
bug有坑的,这也体现了前端的复杂性,后台java再复杂也没有这种事件复杂性。
本文就是分享一下本人对源代码分析的结果。
1)首先,写一下datetimepicker在页面引入使用的常规简单方法:
<link rel="stylesheet" href="../css/bootstrap.min.css">
<link rel="stylesheet" href="../css/bootstrap-datetimepicker.min.css">
<script src="jquery.js" ></script>
<script src="bootstrap-datetimepicker.js" ></script>
</head>
<body>
<input type="text" id="datetimepicker" class="form-control" readonly />
<script>
$( "#datetimepicker" ).datetimepicker({
format: 'yyyy-mm-dd',//显示格式
startView:2, // 选day(年月日)
minView:2,
language: 'zh-CN',
autoclose: 1,//选择后自动关闭
clearBtn:true//清除按钮
});
view子页面0-4别对应分时日月年,比如view=2对应day(日)。
format参数决定在input显示哪些部分,比如'yyyy-mm-dd'则不显示时分。
以选取年月日为例,如果maxView=2,则点击年不会选年,所以不能写maxView:2。
datetimepicker弹窗层自己写了css,与bootstrap.css无关。
2)datetimepicker的入口函数代码
$("input").datetimepicker(options)入口函数代码:
$.fn.datetimepicker = function (option) {
...
this.each(function () {
var $this = $(this),
data = $this.data('datetimepicker'),
if (!data) {
$this.data('datetimepicker', (data = new Datetimepicker(this, $.extend({}, $.fn.datetimepicker.defaults, options))));
}
...
};
它是遍历$("input")的每一个input元素,new Datetimepicker() 一个实例,保存在input元素的data属性中。
var Datetimepicker = function (element, options) {
此即为new实例的构造函数,相当于java的主类,这里先略过。
如果页面有多个input,则多实例,每个input执行一次new Datetimepicker(element,options)实例,创建一个DOM插入页面:
<div class="datetimepicker datetimepicker-dropdown-bottom-right dropdown-menu"
页面中这些容器没有区别,它们对应js的不同对象(jquery对象)保存在不同的实例中。
每个实例对应页面中的一个input和<div>,<div>通过内部对象索引对应js对象实例,多实例相互之间无关系。
$()无论是新建html元素插入页面还是从页面选取html元素,都是与页面元素对应,对这个对象进行操作就是对页面元素进行操作,其内部原理就是html/DOM技术,app处理界面其实与html是类似的。
多实例举例:
var obj1 = $('<div>hello</div');
var obj2 = $('<div>hello</div');
$().append(obj1)
$().append(obj2)
看页面是没法区分这两个<div>的,但在js它们对应不同的jquery对象,实际上就是对应不同的DOM对象,obj1对应第一个<div>,obj2对应第二个<div>,
js代码可以对任意一个<div>进行操作,比如obj1.remove()就从页面把第一个<div>删除了,obj1把自身删除了。
3)datetimepicker的主类
下面来看主类:
var Datetimepicker = function (element, options) { // element是input元素,options是传入的参数{}
this.xxx = xxx; // 接受传入的参数,设置一些属性保存在实例中,不再一一细说,自己看
...
其中:
this.picker = $(template)
.appendTo(this.isInline ? this.element : this.container) // 'body'
.on({
click: $.proxy(this.click, this),
mousedown: $.proxy(this.mousedown, this)
});
这是构造弹窗层template/table并插入页面之后绑定点击事件,构造template比较复杂,分年月日时分好几块。
一般来说弹窗层插入body,在body页面中定位,但也可以插入div容器中,它有个判断。
input绑定点击事件的代码:
Datetimepicker.prototype = {
constructor: Datetimepicker,
_events: [],
_attachEvents: function () {
this._detachEvents();
if (this.isInput) { // single input
this._events = [
[this.element, {
focus: $.proxy(this.show, this),
keyup: $.proxy(this.update, this),
keydown: $.proxy(this.keydown, this)
}]
];
}else...
for (var i = 0, el, ev; i < this._events.length; i++) {
el = this._events[i][0];
ev = this._events[i][1];
el.on(ev);
}
一般事件绑定的函数直接写函数,函数中可以引用全局对象,如果函数涉及所在的对象,可以写成:
$.proxy(this.show, this),
有点类似bind的意思,变换/指定函数所在的对象。
注意它是绑定input的focus事件,这是有bug有坑的,而且复杂深奥不好debug找原因,踩过hover和blur坑的人可能能猜到会出什么坑,
我这里只提示一下,与浮动块(弹窗层)有关,就不细说了。
下面是document绑定"点击"事件:
$(document).on('mousedown', function (e) {
// Clicked outside the datetimepicker, hide it
if ($(e.target).closest('.datetimepicker').length === 0 ) {
that.hide();
}
});
当点击页面时,也就是当点击的区域不是弹窗层时,关闭日期选取弹窗层。
看了上述鼠标事件绑定之后,我们知道,当点击input时,如果有focus事件则触发执行this,show()方法显示弹窗,很简单。
this.show()方法就是简单设置display=block显示弹窗层,并且执行e.stopPropagation()阻止鼠标事件冒泡到document,否则鼠标事件冒泡到document
会执行事件处理函数关闭弹窗。
如果鼠标在input反复点击,由于focus事件要鼠标离开input再点击input才有,所以没有重复的focus事件,就没有反应。由于input不做为输入,而是做为
一个按钮,反复点击应该有toggle效果,所以这样用户体验是不好的,严格说是不能通过测试的,可以自己修改代码解决这个问题,具体怎么改,只有知道
这个问题,相信都有这个能力,比如可以在document点击事件处理函数加判断当判断鼠标事件target是input时,就toggle弹窗层显示,而不是仅仅简单
判断如果点击的区域不是弹窗层就关闭弹窗层,这是这个插件的败笔所在。
大家还记得input是绑定focus,document是绑定mousedown,考虑到弹窗层的向上小三角与input略有重叠,这是有bug有坑的,
会出现奇怪的现象,体现了前端事件的复杂性,这个插件的作者可能没有意识到这个问题(大意了)。
一些细节分析:
选取天会更新this.date会setValue()更新input的值,如果执行hide(),也会更新input的值。
每次点击都要更新this.date日期值,因为可以点击选取上个月末尾几天或者下个月头几天,计算年月日还是挺复杂的,
再根据date重新构造年月日时分列表。
Datetimepicker是核心类,fill是核心数据处理代码,每次点击因为日期数据发生变化因此都要执行更新代码,更新弹窗页面内容,点击更新处理非常复杂,
属于复杂数据和页面处理。
fill: function(){
构造弹窗层里面的html内容,每个月的day列表,table形式显示,类似日历显示,数据算法处理比较复杂。
while循环产生每一天的html代码,最后拼接插入tbody。
for循环产生小时/分列表。
用Date.getUTCxxx和setUTCxxx方法,类似java Calendar。
place()
弹窗插入页面body,在页面定位top = input top + input高度。
datepicker年月日数据算法
isLeapYear: function (year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
},
getDaysInMonth: function (year, month) {
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
},
bootstrap-datetimepicker.xxx.js是客户化语言包,中文语言包是zh-CN:
$.fn.datetimepicker.dates['zh-CN'] = {
days: ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"],
daysShort: ["周日", "周一", "周二", "周三", "周四", "周五", "周六", "周日"],
daysMin: ["日", "一", "二", "三", "四", "五", "六", "日"],
months: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
monthsShort: ["一月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "十一月", "十二月"],
today: "今天",
suffix: [],
meridiem: ["上午", "下午"]
};
另外,比如这种代码:
this.element.trigger({
type: 'changeDay',
date: this.viewDate
});
这是自定义事件,并没有绑定这个自定义事件,所以没有用。
datetimepicker弹窗层上边框上箭头实现方法:
.datetimepicker-dropdown-bottom-right:before {
top: -7px;
left: 6px;
}
[class*=" datetimepicker-dropdown"]:before {
content: '';
display: inline-block;
border-left: 7px solid transparent; // 向上三角,背景色是灰色
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0,0,0,0.2);
position: absolute;
}
*, *:before, *:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
}
.datetimepicker-dropdown-bottom-right:after {
top: -6px;
left: 7px;
}
[class*=" datetimepicker-dropdown"]:after {
content: '';
display: inline-block;
border-left: 6px solid transparent; // 向上三角,背景色是白色,与向上灰色三角重叠覆盖住再往下偏移一点
border-right: 6px solid transparent;
border-bottom: 6px solid #fff;
position: absolute;
}
它是before/after两个重叠的三角(上箭头),一个灰色,一个白色,白色箭头块略向下错开一点,效果就显示出灰色的上箭头,由于是浮动块,会遮挡父元素的边框线。
本文到此结束,本人水平有限,文中不妥之处欢迎大家指正。