最近在使用 Jonathan Peterson 所写的 bootstrap-datetimepicker 这一插件,然而由于网页设计的一些问题,无法在静态元素上正常使用该插件,蹦出自己尝试修改源代码的想法。说干就干,还竟然成功了,现在再完整地研究一下该插件的源代码!
bootstrap-datetimepicker 源代码(version:4.17.45)一共2725行,总的来说,分为两个部分
第一个函数用来检查是否使用了AMD规范,以及所需要的 jQuery 和 moment.js 是否已经加载,最后调用factory。这个函数比较简单,代码如下:
function (factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
// AMD is used - Register as an anonymous module.
define(['jquery', 'moment'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('jquery'), require('moment'));
} else {
// Neither AMD nor CommonJS used. Use global variables.
if (typeof jQuery === 'undefined') {
throw 'bootstrap-datetimepicker requires jQuery to be loaded first';
}
if (typeof moment === 'undefined') {
throw 'bootstrap-datetimepicker requires Moment.js to be loaded first';
}
factory(jQuery, moment);
}
}
重点来了,让我们仔细观察第二个函数的内容,简化为三个部分
function ($, moment) {
'use strict';
if (!moment) {
throw new Error('bootstrap-datetimepicker requires Moment.js to be loaded first');
}
// Part one
var dateTimePicker = function (element, options) {...};
// Part two
$.fn.datetimepicker = function (options) {...};
// Part three
$.fn.datetimepicker.defaults = {...};
return $.fn.datetimepicker;
}
整体上,这个函数首先声明一个名为 dateTimePicker 的 JavaScript 对象,将 datetimepicker 这个方法拓展为 $.fn
的方法,接着给 datetimepicker 一个名为 defaults 的属性赋值,最后返回 $.fn.datetimepicker
接下来,我们对每一部分的代码进行解析
首先,让我们来看一下第一部分,这一部分的主要工作是返回一个对象,接受的参数是element以及options,部分代码如下:
var dateTimePicker = function (element, options) {
var picker = {},
.
.
.
return picker;
}
那么这两个参数到底接受的是什么内容呢?在第二部分的代码中我们会找到答案,现在就一起先睹为快:
$.fn.datetimepicker = function (options) {
options = options || {};
var args = Array.prototype.slice.call(arguments, 1),
isInstance = true,
thisMethods = ['destroy', 'hide', 'show', 'toggle'],
returnValue;
if (typeof options === 'object') {
return this.each(function () {
var $this = $(this),
_options;
if (!$this.data('DateTimePicker')) {
// create a private copy of the defaults object
_options = $.extend(true, {}, $.fn.datetimepicker.defaults, options);
$this.data('DateTimePicker', dateTimePicker($this, _options));
}
}); // 剩余部分省略
关键在于 dateTimePicker($this, _options)
,那么这个 $this
又是何方圣神呢?继续向上溯源,发现var $this = $(this)
,也就是说,这个$this
代表的就是调用 datetimepicker 的HTML元素所对应的 jQuery 对象,而这个 options 则是一个 JavaScript 对象,这个对象是由用户在使用插件时添加的一些选项所组成的。
明白这个函数的最终功能之后,我们看具体的代码。
首先定义了许多局部变量、函数以及 picker 的各种方法,这些方法比较简单,这里就不再赘述。
先来看具体的执行过程,主要代码如下:
// initializing element and component attributes
if (element.is('input')) {
input = element;
} else {
input = element.find(options.datepickerInput);
if (input.length === 0) {
input = element.find('input');
} else if (!input.is('input')) {
throw new Error('CSS class "' + options.datepickerInput + '" cannot be applied to non input element');
}
}
if (element.hasClass('input-group')) {
// in case there is more then one 'input-group-addon' Issue #48
if (element.find('.datepickerbutton').length === 0) {
component = element.find('.input-group-addon');
} else {
component = element.find('.datepickerbutton');
}
}
if (!options.inline && !input.is('input')) {
throw new Error('Could not initialize DateTimePicker without an input element');
}
// Set defaults for date here now instead of in var declaration
date = getMoment();
viewDate = date.clone();
$.extend(true, options, dataToOptions());
picker.options(options);
initFormatting();
attachDatePickerElementEvents();
if (input.prop('disabled')) {
picker.disable();
}
if (input.is('input') && input.val().trim().length !== 0) {
setValue(parseInputDate(input.val().trim()));
}
else if (options.defaultDate && input.attr('placeholder') === undefined) {
setValue(options.defaultDate);
}
if (options.inline) {
show();
}
一开始的三个 if
主要完成的是一些初始化,随后获得当前的时间,并且用户输入的option
更新dateTimePicker
中的值,这里用到了 dataToOptions()
函数,我们来仔细看一看这个函数。
dataToOptions = function () {
var eData,
dataOptions = {};
if (element.is('input') || options.inline) {
eData = element.data();
} else {
eData = element.find('input').data();
}
if (eData.dateOptions && eData.dateOptions instanceof Object) {
dataOptions = $.extend(true, dataOptions, eData.dateOptions);
}
$.each(options, function (key) {
var attributeName = 'date' + key.charAt(0).toUpperCase() + key.slice(1);
if (eData[attributeName] !== undefined) {
dataOptions[key] = eData[attributeName];
}
});
return dataOptions;
}
遍历用户输入的所有 option ,将其中有效的数据读取出来,紧接着调用 picker.options()
,让我们来看一下这个函数
picker.options = function (newOptions) {
if (arguments . length === 0) {
return $.extend(true, {}, options);
}
if (!(newOptions instanceof Object)) {
throw new TypeError('options() options parameter should be an object');
}
$.extend(true, options, newOptions);
$.each(options, function (key, value) {
if (picker[key] !== undefined) {
picker[key](value);
} else {
throw new TypeError('option ' + key + ' is not recognized!');
}
});
return picker;
};
这个函数主要功能是利用用户输入的option
来更新picker
中的数据:如果该选项是合法的,则更新数据;如果该选项是不合法的,则抛出异常,输出错误信息。这里用到了之前略过的那些函数,用户可以修改的option
都对应了一个形如picker.x
的函数,这个函数完成了对数据的检验以及数据更新,其中option
可以是一个字符串(例如: format),也可以是一个类对象(例如:widgetPositioning)。
现在,让我们来看一看如何在这个插件上进行功能拓展。可以看到,判断用户输入的option
是否合法的逻辑很简单,如果可以找到一个形如picker.x
的函数,那么就认为这个option
是合法的。这也就是说,如果想新增加一个option
选项new_option
(此处的new_option
只是选项名,可以自行修改),只需编写picker.new_option = function(new_option){}
函数即可,这个函数需要实现的功能是接收到用户的new_option
设置后进行检验和处理数据,并用处理后的数据更新原有数据。是不是很兴奋,原来功能拓展这么容易!但是,这仅仅是数据处理,具体的功能实现需要另写函数。
让我们接着来看代码,剩下的代码就比较简单:
initFormatting();
attachDatePickerElementEvents();
if (input.prop('disabled')) {
picker.disable();
}
if (input.is('input') && input.val().trim().length !== 0) {
setValue(parseInputDate(input.val().trim()));
}
else if (options.defaultDate && input.attr('placeholder') === undefined) {
setValue(options.defaultDate);
}
if (options.inline) {
show();
}
return picker;
初始化了时间格式,设置datetimepicker
了基本行为,最后做一些判断。有兴趣的读者可以自己研读,这里贴出initFormatting()
和attachDatePickerElementEvents()
的源代码
attachDatePickerElementEvents = function () {
input.on({
'change': change,
'blur': options.debug ? '' : hide,
'keydown': keydown,
'keyup': keyup,
'focus': options.allowInputToggle ? show : ''
});
if (element.is('input')) {
input.on({
'focus': show
});
} else if (component) {
component.on('click', toggle);
component.on('mousedown', false);
}
}
initFormatting = function () {
var format = options.format || 'L LT';
actualFormat = format.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (formatInput) {
var newinput = date.localeData().longDateFormat(formatInput) || formatInput;
return newinput.replace(/(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g, function (formatInput2) { //temp fix for #740
return date.localeData().longDateFormat(formatInput2) || formatInput2;
});
});
parseFormats = options.extraFormats ? options.extraFormats.slice() : [];
if (parseFormats.indexOf(format) < 0 && parseFormats.indexOf(actualFormat) < 0) {
parseFormats.push(actualFormat);
}
use24Hours = (actualFormat.toLowerCase().indexOf('a') < 1 && actualFormat.replace(/\[.*?\]/g, '').indexOf('h') < 1);
if (isEnabled('y')) {
minViewModeNumber = 2;
}
if (isEnabled('M')) {
minViewModeNumber = 1;
}
if (isEnabled('d')) {
minViewModeNumber = 0;
}
currentViewMode = Math.max(minViewModeNumber, currentViewMode);
if (!unset) {
setValue(date);
}
}
让我们进入第二部分的代码:
$.fn.datetimepicker = function (options) {
options = options || {};
var args = Array.prototype.slice.call(arguments, 1),
isInstance = true,
thisMethods = ['destroy', 'hide', 'show', 'toggle'],
returnValue;
if (typeof options === 'object') {
return this.each(function () {
var $this = $(this),
_options;
if (!$this.data('DateTimePicker')) {
// create a private copy of the defaults object
_options = $.extend(true, {}, $.fn.datetimepicker.defaults, options);
$this.data('DateTimePicker', dateTimePicker($this, _options));
}
});
} else if (typeof options === 'string') {
this.each(function () {
var $this = $(this),
instance = $this.data('DateTimePicker');
if (!instance) {
throw new Error('bootstrap-datetimepicker("' + options + '") method was called on an element that is not using DateTimePicker');
}
returnValue = instance[options].apply(instance, args);
isInstance = returnValue === instance;
});
if (isInstance || $.inArray(options, thisMethods) > -1) {
return this;
}
return returnValue;
}
throw new TypeError('Invalid arguments for DateTimePicker: ' + options);
}
首先,对options
进行初始化,并获得该函数对的所有参数。
如果options
是对象,那么利用这个对象,更新datetimepicker.default
中对应数据的值;如果options
是字符串,则分两种情况,当options
是'destroy', 'hide', 'show', 'toggle'
中的一个时,做相应的处理后,返回这个对象;如果不是,则判断options
是否合法,如果合法则返回该option
所对应数值,否则会报错。
最后一部分就是datatimepicker
的default值,主要是赋初值,作为用户没有任何修改时的初始状态,如果有需要可以自己改动其中的初始化。另外,如果有需要,自己添加的功能的初始值也应该在这里一并赋值。下面是代码:
$.fn.datetimepicker.defaults = {
timeZone: '',
format: false,
dayViewHeaderFormat: 'MMMM YYYY',
extraFormats: false,
stepping: 1,
minDate: false,
maxDate: false,
useCurrent: true,
collapse: true,
locale: moment.locale(),
defaultDate: false,
disabledDates: false,
enabledDates: false,
icons: {
time: 'glyphicon glyphicon-time',
date: 'glyphicon glyphicon-calendar',
up: 'glyphicon glyphicon-chevron-up',
down: 'glyphicon glyphicon-chevron-down',
previous: 'glyphicon glyphicon-chevron-left',
next: 'glyphicon glyphicon-chevron-right',
today: 'glyphicon glyphicon-screenshot',
clear: 'glyphicon glyphicon-trash',
close: 'glyphicon glyphicon-remove'
},
tooltips: {
today: 'Go to today',
clear: 'Clear selection',
close: 'Close the picker',
selectMonth: 'Select Month',
prevMonth: 'Previous Month',
nextMonth: 'Next Month',
selectYear: 'Select Year',
prevYear: 'Previous Year',
nextYear: 'Next Year',
selectDecade: 'Select Decade',
prevDecade: 'Previous Decade',
nextDecade: 'Next Decade',
prevCentury: 'Previous Century',
nextCentury: 'Next Century',
pickHour: 'Pick Hour',
incrementHour: 'Increment Hour',
decrementHour: 'Decrement Hour',
pickMinute: 'Pick Minute',
incrementMinute: 'Increment Minute',
decrementMinute: 'Decrement Minute',
pickSecond: 'Pick Second',
incrementSecond: 'Increment Second',
decrementSecond: 'Decrement Second',
togglePeriod: 'Toggle Period',
selectTime: 'Select Time'
},
useStrict: false,
sideBySide: false,
daysOfWeekDisabled: false,
calendarWeeks: false,
viewMode: 'days',
toolbarPlacement: 'default',
showTodayButton: false,
showClear: false,
showClose: false,
widgetPositioning: {
horizontal: 'auto',
vertical: 'auto'
},
widgetParent: null,
ignoreReadonly: false,
keepOpen: false,
focusOnShow: true,
inline: false,
keepInvalid: false,
datepickerInput: '.datepickerinput',
keyBinds: {
up: function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().subtract(7, 'd'));
} else {
this.date(d.clone().add(this.stepping(), 'm'));
}
},
down: function(widget) {
if (!widget) {
this.show();
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().add(7, 'd'));
} else {
this.date(d.clone().subtract(this.stepping(), 'm'));
}
},
'control up': function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().subtract(1, 'y'));
} else {
this.date(d.clone().add(1, 'h'));
}
},
'control down': function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().add(1, 'y'));
} else {
this.date(d.clone().subtract(1, 'h'));
}
},
left: function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().subtract(1, 'd'));
}
},
right: function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().add(1, 'd'));
}
},
pageUp: function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().subtract(1, 'M'));
}
},
pageDown: function(widget) {
if (!widget) {
return;
}
var d = this.date() || this.getMoment();
if (widget.find('.datepicker').is(':visible')) {
this.date(d.clone().add(1, 'M'));
}
},
enter: function() {
this.hide();
},
escape: function() {
this.hide();
},
'control space': function(widget) {
if (!widget) {
return;
}
if (widget.find('.timepicker').is(':visible')) {
widget.find('.btn[data-action="togglePeriod"]').click();
}
},
t: function() {
this.date(this.getMoment());
},
'delete': function() {
this.clear();
}
},
debug: false,
allowInputToggle: false,
disabledTimeIntervals: false,
disabledHours: false,
enabledHours: false,
viewDate: false
};
至此,源码部分告一段落(PS:第一次看完2000多行的项目,感觉给自己补充了很多知识,看源码真的很有效果!)
最后,让我们回到令人兴奋的功能拓展部分。之前解决了数据更新的问题,现在,我们来看看实现了功能之后怎么办。源代码中有一个名为show()
的函数,这个函数的作用是显示datetimepicker
,那么,实现相应功能的函数在这里调用即可。如果加入的功能和是对现在已有功能的拓展,直接修改原功能对应的函数即可。
附录解释:
i. $.fn
是 jQuery 中特别的用法,相当于$.prototype
。每个对象都可以调用prototype(fn)下的方法,通过 $.fn = function(){}
可以为每一个jQuery对象添加一个方法。
ii. 闭包:允许将函数看作对象,然后能像在对象中的操作搬在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。
var abc=function(y){
var x=y;// 这个是局部变量
return function(){
alert(x++);// 就是这里调用了闭包特性中的一级函数局部变量的x,并对它进行操作
alert(y--);// 引用的参数变量也是自由变量
}}(5);// 初始化
abc();// "5" "5"
abc();// "6" "4"
abc();// "7" "3"
alert(x);// 报错!“x”未定义!
iii. call apply
call
的第一个功能是用于函数的调用
var test = 'Test'
function doSomething(){
alert(test);
}
// 以下两句语句可以实现相同的效果:弹出对话框,内容是 Test
doSomething();
doSomething.call();
这仅仅是一个基础功能,更大的用处在于利用call
可以改变函数中this
所指示的对象,示例代码如下
var test= 'Test';
function doSomething(){
alert(this.test);
}
var obj = {
test: 'Obj'
}
// 通过`.call(obj)`,this所指示的对象变为了obj
doSomething.call(obj); // 弹出对话框,内容是 Obj
apply()
和call()
类似的,只是apply()
要求第二个参数必须是一个数组。这个数组会作为参数传递给目标函数。
iv. arguments
JavaScript中每一个函数都是一个对象,每个函数的对象中都有着保存参数列表的arguments,它有一个代表长度的属性,并且可以像数组那样用下标来访问。然而,arguments实际上并不是数组,也就意味着没有split、push、pop等方法。
JavaScript允许函数接受比他声明时数量更多的参数,这些参数都存储在arguments中,下面的例子综合了apply和arguments
var o = {x : 15};
function f(message1, message2) {
alert(message1 + ( this.x * this.x) + message2);
}
function g(object, func) {
// arguments[0] = object
// arguments[1] = func
var args = [];
for(var i = 2; i < arguments.length; i++) {
args.push(arguments[i]);
}
func.apply(object, args);
}
g(o, f, 'The value of x squared = ', '. Wow!');
// 输出:The value of x squared = 225. Wow!
v.|| &&
运算符
JavaScript的逻辑运算中,-0 0 "" null false undefined NaN
都会判为false
;另外,大多数语言都支持“短路”:对于&&
,终止于第一个为false的表达式;对于||
,终止于第一个为true的表达式。
JavaScript的逻辑运算表达式的返回结果不一定是简单的true
或者false
,准确来说,返回的是真值或假值,上述的-0 0 "" null false undefined NaN
是假值,而除此之外的都是真值(包括对象)。对于&&
,返回第一个假值或最后一个真值;对于||
,返回第一个真值或最后一个假值。
正是因此,在源代码中有这样的赋值过程:
// 如果 options 是一个对象,则将 options 赋值给 options ,否则新创建一个对象。
options = options || {};