日历插件的详解
本插件是在jQuery 1.6.1基础上进行开发的。
var _count = 0;
var _expando = "CCalendar-" + (+new Date()) + "-",
var CCalendar = function(element,options){
this.initialize.apply(this,arguments); //在new出来的CCalendar对象的执行上下文中执行initialize方法,并传入参数element,options。
_count++; //这里的arguments是[element,options],通过apply方法后,会把数组中的每一项传参给initialize,而不是传数组给它。
}
//默认配置
CCalendar.defcfg = {
id: null,
showCount: 1,
direction: "Right",
startDate: new Date(),
maxDate: false,
minDate: false,
lang: 'zh-cn',
onSelect: null,
onSelectBack: null,
onHide: null,
};
CCalendar.prototype = {
constructor: CCalendar,
initialize: function (element, options) {
options = options || {}; //如果没有传入对象,就使用空对象
this.element = $(element); //把元素转化成jQuery对象
this.format = GLOBLE.parseFormat(options.format || this.element.data('date-format') || 'yyyy-mm-dd');
//GLOBLE.parseFormat解析日期的格式,优先级:开发人员传入的格式->元素date-format属性的格式->默认格式'yyyy-mm-dd'
this.isInput = this.element.is("input");
// 合并默认配置
var dftCfg = CCalendar.defcfg;
for (var i in dftCfg) {
if (options[i] === undefined) { //如果是传入的参数,就不使用默认值
options[i] = dftCfg[i];
}
}
this.direction = options.direction; //默认提示控件在Right
options.id = options.id || _expando + _count; //id为CCalendar-当前日期的毫秒数-当前的_count
this.picker = $("<div/>", {"id": options.id, "class": "ui-date"}).appendTo('body').bind({
click: $.proxy(this.click, this) //$.proxy返回一个新函数,并且这个新函数始终保持了特定的上下文(这里就是this,也就是new出来的CCalendar)
}); //对这个div,也就是this.picker绑定click事件
//$.proxy( me.test, me, you, they ),test(you,they,event)。第一个参数为function(点击事件触发的函数),第二个参数为context(函数执行的上下文),第三个参数,以及后面的参数,都会传入到function中去。function中总会有一个event参数。event.type -> click。
this.create(options);
//如果是输入框
if (this.isInput) {
this.element.bind({
focus: $.proxy(this.show, this), //对传入的input元素绑定focus和keyup事件
});
}
else { //如果元素不是input,就对元素绑定click事件
this.element.bind('click', $.proxy(this.show, this));
}
},
//创建日历
create: function (options) {
this.setOptions(options);
this.fill(this.date);
},
setOptions: function (options) {
this.options = options;
this.options.startDate && (this.options.startDate = GLOBLE.parseDate(this.options.startDate, this.format));
//是否传入了日历中显示的选中日期,如果没有就是当前时间的日期,如果有,传入的显示日期必须可以转化成相应的日期格式。默认为:'yyyy-mm-dd'
this.lang =GLOBLE.lang[this.options.lang]; //默认为'zh-cn'
(this.options.maxDate && (this.options.maxDate = GLOBLE.parseDate(this.options.maxDate, this.format)));
(this.options.minDate && (this.options.minDate = GLOBLE.parseDate(this.options.minDate, this.format)));
//日历控件里面可以选择的日期最大值和最小值,传入的日期必须能够转换成相应的日期格式,默认为:'yyyy-mm-dd'
this.date = { //日历上选中的日期,如果没传入startDate参数,那么就是当前时间的日期
year: this.options.startDate.getFullYear(),
month: this.options.startDate.getMonth(),
days: this.options.startDate.getDate()
};
},
//构建主体
fill: function (date) { //日历上默认被选择的日期
var dp = this,
options = dp.options;
var disabled,
isShowToday = true,
year = (date.year || this.date.year), //其实这里的date和this.date是同一个
month = (date.month || this.date.month),
_today = new Date(GLOBLE.today.year, GLOBLE.today.month, GLOBLE.today.date);
//比如在中国大陆、、香港、新加坡、马来西亚、菲律宾等地区的本地时间比UTC快8小时,记作UTC+8,意思就是比UTC时间快8小时。
this.date.year = year,
this.date.month = month;
var main = this.dom ? this.dom.main : $("<div/>", {"class": "main"}).appendTo(this.picker);
//再新建一个div,添加到id为options.id的div中。我们这里把外层的div叫做div1,main的称作div2。
main.empty(); //移除元素的所有内容,包括所有文本和子节点。remove,连元素都移除
for (var i = 0; i < options.showCount; i++) { //默认情况下为1
var dayCount = GLOBLE.getDaysInMonth(year, month),
//得到当前被选择日期的当前月的天数。(假设今天是2014,3月5号,那么year=2014,month=2)
prev = GLOBLE.getPNDate(year, (month + 1), -1), //得到上一个月的日期,{year:2014,month:1,day:28}
next = GLOBLE.getPNDate(year, (month + 1), 1), //得到下一个月的日期,{year:2014,month:3,day:30}
startPad = new Date(year, month, 1).getDay(),
//得到当前被选择日期的当前月的第一天是星期几,返回值是 0(周日) 到 6(周六) 之间的一个整数。{2014.3.1号是星期六}
pday,
list = $("<div/>", { "class": "ui-date-list ui-month" + i + (options.showCount > 1 && year == GLOBLE.today.year &&
month == (GLOBLE.today.month + 1) ? 'ui-month-cur' : "") }); //新建一个div,我们称为div3
var html = ['<div class="ui-date-head ui-date-top">'];
(i == 0) && html.push('<a data-action="YEAR_LEFT" class="ui-year-left" href="javascript:void(0)" title="'
+ dp.lang.previousYear + '">«</a><a data-action="MONTH_LEFT" class="ui-month-left" href="javascript:void(0)" title="'
+ dp.lang.previousMonth + '">‹</a>');
//添加上一年,上一个月的链接,只有i==0时才添加,因为一个日历上只需要一个。
html.push('<a data-action="SELECT_YEAR" class="ui-year" href="javascript:void(0)" title="' +
dp.lang.selectYear + '">' + year + '</a>年<a data-action="SELECT_MONTH" class="ui-month" data-year="' + year + '"
href="javascript:void(0)" data-dom="month" title="' + dp.lang.selectMonth + '" >' +
(((month + 1) < 10 ? '0' : '') + (month + 1)) + '</a>月'); //如果月份少于10,就添加0,变成09,08这种。{03}
(i + 1 == options.showCount) && html.push('<a data-action="MONTH_RIGHT" class="ui-month-right" href="javascript:void(0)" title="' + dp.lang.nextMonth + '">›</a><a data-action="YEAR_RIGHT" class="ui-year-right" href="javascript:void(0)" title="' + dp.lang.nextYear + '">»</a>');
//添加下一月,下一年的链接,也只添加一次。
html.push('</div><div class="ui-date-content clearfix"><div class="ui-date-week">');
for (var j = 0; j < 7; j++){
html.push('<span class="ui-week ' + j + '">' + dp.lang.week[j] + '</span>'); //添加星期一到星期天
}
html.push('</div><div class="ui-date-days">');
var nday = startPad = startPad == 0 ? 6 : startPad - 1; //假设今天星期六,那么返回6,startPad 和nday等于5.
for (; startPad >= 0; startPad--) { //循环6次
pday = (prev.day - startPad) //第一次循环时,prev.day=28,startPad =5,pday=23
_innerDay(prev, pday, true); //此方法就是把前一个月的23号显示在日历插件上,再循环就是24,25,一直到28号。它的显示样式是隐式的
}
for (var j = 1; j <= dayCount; j++) { //31天,2014年3月
disabled = false;
var day = new Date(year, month, j).getDay(); //针对每天,得到是星期几
html.push('<div class="d-' + day);
if (options.maxDate && options.maxDate <= new Date(year, month, j) || //默认maxDate 为null
options.minDate && options.minDate >= new Date(year,month, j)) {
disabled = true;
html.push(' ui-days-disabled');
}
if (GLOBLE.today.year == year && GLOBLE.today.month == month && GLOBLE.today.date == j) {
html.push(' ui-days-current'); //如果是当天的日期,比如3月5号,也就是j=5时,就让它的class变成current。
if (disabled) isShowToday = false;
}
html.push('"><a data-action="DAY" href="javascript:void(0)" data-year="' + year + '" data-month="' + (month + 1 ) +
'" data-day="' + j + '" data-disabled="' + disabled + '" data-old="' + (new Date(year, month, j) < _today ? 1 : 0 ) +
'" >' + j + '</a></div>');
}
for (var j = 1; j < 42 - dayCount - nday; j++) { //42-31-5 = 6,循环5次,因为日历上总共要显示42天
_innerDay(next, j); //把下个月的几天显示在日历上,隐式显示。
}
html.push('</div></div>');
list.html(html.join("")); //把所有的元素添加到list(div3)
main.append(list); //再把div3添加到div2中,div2在div1中,div1在body中。
if (++month == 12) { //month=2,这时month=3
year++;
month = 0;
}
}
if (GLOBLE.ie) { //如果是ie
this.picker.width(200 * options.showCount); //把div1宽度设置为(200*1=200)
if (GLOBLE.ie == 6) { //如果是ie6
var background = $('<div />', {"class": "ui-date-mark"}).css( //新建一个div称为div4,把宽度和高度设置为div1的宽度和高度
{
width: this.picker.innerWidth(),
height: this.picker.innerHeight()
}
).html('<iframe frameborder="0" src="about:blank" scrolling="no"></iframe>'); //并在div4中装入一个iframe
this.picker.append(background); //把div4添加到div1中。这时的html结构就是
//<div1><div2>日历控件的元素</div2><div4><iframe></iframe></div4></div1>,iframe做垫片,IE6的bug
}
}
this.dom = {
//在this上新添加dom属性,等于{},对象里面有一个main属性就是div2,通过对象的属性来引用(div2中有日历控件需要的html标签)。当重新调用fill时,只需要取到div2,并且empty,就可以重新往div2添加元素了(生成新的日历控件内容)
main: main
};
function _innerDay(obj, pday, flag) {
html.push('<div class="d-' + pday);
if (options.maxDate && options.maxDate <= new Date(obj.year, obj.month, pday) ||
options.minDate && options.minDate >= new Date(obj.year, obj.month, pday)) {
disabled = true;
html.push(' ui-days-disabled');
}
html.push(' ui-days-other">');
html.push('<a data-action="DAY" href="javascript:void(0)" data-year="' + obj.year + '" data-disabled="' + disabled +
'" data-month="' + (obj.month + 1 ) + '" data-day="' + pday + '">' + pday + '</a>');
html.push('</div>');
}
},
//显示
show: function (e) {
this.offset(); //设置日历控件显示的位置
this.picker.show();
$(window).bind('resize', $.proxy(this.offset, this)); //窗口改变,就会重新计算日历控件的位置。
if (e) {
e.stopPropagation(); //已经被 jQuery 做过标准化处理
e.preventDefault();
}
var that = this;
$(document).one('mousedown', function (ev) { //当用户点击页面的其他地方时(不是日历控件)就会隐藏日历控件。one方法表明只执行一次回调方法
if ($(ev.target).closest('.ui-date').length == 0) {
//closest方法,从当前元素开始,沿 DOM 树向上遍历,直到找到已应用选择器的一个匹配为止。 返回包含零个或一个元素的 jQuery 对象。由于只有this.packer有.ui-date属性,因此只有点击了this.packer才会有元素,等于0就代表点击的不是页面上的日历控件,因此隐藏
that.hide();
}
});
},
offset: function () {
//jQuery中的.height()、.innerHeight()和.outerHeight(),height就是content的高度,innerHeight是高度+padding,outerHeight是高度+padding+border+(如果传入true,则加上margin,如果传入false或者不传,就不加)
this.height = this.element.outerHeight();
this.width = this.element.outerWidth();
var offset = this.element.offset();//返回元素的偏移坐标。该方法返回的对象包含两个整型属性:top 和 left,以像素计。此方法只对可见元素有效。
var top = offset.top,
left = offset.left,
ww = $(window).width(),
pw = this.picker.width();
var css = { top: top + this.height };
//元素偏移页面的高度+元素的高度(不包括margin),这时日历控件就可以显示在元素的下面了(刚好在元素的border下)。
switch (this.direction) { //默认为Right
case "Left":
css.left = left >= pw ? (left - pw ) + this.width : left
break;
case "Right": //日历控件的left等于元素的left,左对齐显示。如果超过了窗口的宽度,就让日历控件跟元素右对齐
css.left = ww <= (pw + left) ? left - pw + this.width : left
break;
}
this.picker.css(css);
},
//隐藏
hide: function () {
this.picker.hide(); //日历控件隐藏
typeof this.options.onHide == "function" && this.options.onHide.call(this);
},
//监听点击事件,点击日历时,触发
click: function (e) {
e.stopPropagation();
e.preventDefault();
var dp = this,
options = dp.options,
target = $(e.target).closest('a')[0];
//看用户点击的是哪个链接元素,针对不同的链接元素进行不同处理,因此在创建日历控件的时候,只要是a链接的都会响应click事件
if (!target) return false; //如果点击的是日历控件上的其他位置,没有a链接的,将不做任何处理
if (target.getAttribute('data-disabled') == "true") return false;
//如果是禁止的选项,比如,定义了最大日期和最小日期,那其他范围的日期,点击也不做任何处理
var action = target.getAttribute('data-action'); //除了有a链接,还必须保证a链接上有data-action属性,此插件就是通过data-action来调用不同的回调方法。
if (!action) return false;
switch (action) {
case "SELECT_YEAR": //点击日历控件上的年份时,会弹出一个近10年的选择框,用户可以针对这个选择框点击上一页(上一个10年)和下一页|(下一个十年)
{
dp.createYearModel(dp.date.year, target);
}
break;
case "PREV-10-YEAR": //如果点击年份选择框中的上十年或者下十年,就重新生成新的十年选择框
case "NEXT-10-YEAR":
{
dp.createYearModel(parseInt(target.getAttribute("data-year")), null);
}
break;
case "SELECT_MONTH": //点击月份,就会弹出一个12个选项(从1到12)的月份选择框
{
dp.createMonthModel(dp.date.month, target); //实现方案跟年份的差不多,没年份的复杂,因为月份没有上十月和下十月,只有12个月
}
break;
case "YEAR": //当点击年份选择框中的年份时,就触发
{
dp.fill({
year: parseInt(target.getAttribute("data-year")) //重新生成新的日期时间
});
}
break;
case "MONTH":
{
dp.fill({
month: parseInt(target.getAttribute("data-month") - 1) //重新生成新的日期时间
});
}
break;
case "DAY":
{
this.select(target); //选择日时,就会选择这个日期
}
break;
case "YEAR_LEFT": //日历控件还有上一年的按钮
{
dp.date.year -= options.showCount; //年份减少一年,showCount默认为1
dp.fill(dp.date); //重新生成日期
}
break;
case "YEAR_RIGHT": //以此类推
{
dp.date.year += options.showCount;
dp.fill(dp.date);
}
break;
case "MONTH_LEFT": //以此类推
{
dp.date.month -= options.showCount;
if (dp.date.month < 0) {
dp.date.month = dp.date.month + 12;
dp.date.year--;
}
dp.fill(dp.date);
}
break;
case "MONTH_RIGHT": //以此类推
{
dp.date.month += options.showCount;
if (dp.date.month > 11) {
dp.date.month = dp.date.month - 12;
dp.date.year++;
}
dp.fill(dp.date);
}
break;
default :
{
}
}
},
select: function (target) {
var dp = this,
options = dp.options;
var yyyy = target.getAttribute('data-year'), //得到当前的年份
yy = yyyy.substr(2),
m = parseInt(target.getAttribute('data-month')), //得到当前的月份
mm = m < 10 ? '0' + m : m, //格式化为两位的月份
d = parseInt(target.getAttribute('data-day')), //得到当前的天
dd = d < 10 ? '0' + d : d, //格式化天为两位数
date = new Date(yyyy, m - 1, d),
time = date.getTime(),
back = GLOBLE.formatDate(date, dp.format), //把当前选择的日期格式化
backData = { //传给回调方法的json对象,使之可以使用各种格式的日期数据
yyyy: yyyy,
yy: yy,
mm: mm,
m: m,
dd: dd,
d: d,
back: back,
date: date,
time: time
};
//选择日期前
if (this.options.onSelect && !this.options.onSelect.call(dp, backData)) //如果传入onSelect调用后,传入选择的日期,返回false,则不做任何处理
return null; //这里可以让开发人员传入对象时,写上onSelect方法,然后在方法里面,判断选择的日期是否可选
if (!dp.isInput) {
dp.element.find('input').prop('value', back); //如果元素不是input,就去找input,然后显示出来
dp.element.data('date', back);
}
else {
dp.element.prop('value', back); //把日期显示在input框中
}
//选择日期后
if (!this.options.onSelectBack || ( this.options.onSelectBack && this.options.onSelectBack.call(dp, backData) === true)) {
//如果传入onSelectBack方法,那么选择日期,并显示在input后,如果 onSelectBack方法根据选择的日期返回true,就隐藏,如果返回false,就不隐藏日历,让用户继续选择。
dp.hide(); //隐藏日历控件
}
},
//创建月份面板
createMonthModel: function (month, target) {
var self = this,
list = $(".ui-month-list", self.dom.main);
if (list.length == 0) {
list = $("<div/>", {"class": "ui-month-list"});
self.dom.main.append(list);
self.dom["month"] = list;
self.dom.main.bind("mousedown", function (ev) {
if ($(ev.target).closest('.ui-month-list').length == 0) {
list.hide();
}
return false;
});
}
if (target) {
var offset = $(target).position();
list.css({
top: offset.top + 26,
left: offset.left - 23 / 2
}).show();
}
var items = [];
for (var i = 1; i <= 12; i++) {
items.push({
value: i,
label: i,
role: 'MONTH'
});
}
var current = {
value: month,
label: month
};
var html = [];
for (var j = 0, l = items.length; j < l; j++) {
var m = items[j]
html.push('<a href="javascript:void(0)" data-action="' + m.role + '" class="' + (m.value < GLOBLE.today.month + 1 ? " ui-date-old" : "") + (m.value == GLOBLE.today.month + 1 ? " ui-date-current" : "") + '" data-month="' + m.value + '">' + m.label + '</a>');
}
list.html(html.join(""));
},
//创建年份面板
createYearModel: function (year, target) { //传入的是当前的年份,也就是2014,
var self = this,
years = $(".ui-year-list", self.dom.main);
if (years.length == 0) {
years = $("<div/>", {"class": "ui-year-list"}); //如果没有显示年份的选择框,就新建一个div,表示year
self.dom.main.append(years);
self.dom["year"] = years;
self.dom.main.bind("mousedown", function (ev) {
if ($(ev.target).closest('.ui-year-list').length == 0) {
years.hide();
}
return false;
});
}
if (target) { //把这个年份的选择框,定位到日历年份的位置
var offset = $(target).position();
years.css({
top: offset.top + 17,
left: offset.left - 37 / 2
}).show();
}
var items = [ //item数组的第一项是上十年的按钮
{
value: year - 10,
label: '«',
role: 'PREV-10-YEAR'
}
];
for (var i = year - 6; i < year + 4; i++) { //前6年,当前年份,后3年
items.push({
value: i,
label: i,
role: 'YEAR'
});
}
items[7] = {value: year, label: year, role: 'YEAR', current: true};
items.push({ ////item数组的最后一项是下十年的按钮
value: year + 10,
label: '»',
role: 'NEXT-10-YEAR'
});
var current = { //当前年份,也就是2014
value: year,
label: year
};
var html = [];
for (var j = 0, l = items.length; j < l; j++) { //针对items数组,创建这个年份的选择框
var y = items[j]
html.push('<a href="javascript:void(0)" data-action="' + y.role + '" class="' + (y.value < GLOBLE.today.year ? " ui-date-old" : "") + (y.value == GLOBLE.today.year ? " ui-date-current" : "") + '" data-year="' + y.value + '">' + y.label + '</a>');
}
years.html(html.join(""));
},
}
var GLOBLE = {
ie: document.all && navigator.userAgent.match(/\s{1}\d{1}/),
//语言包
lang: {
"zh-cn": {
week: ['日', '一', '二', '三', '四', '五', '六'],
previousMonth: '上一月',
nextMonth: '下一月',
previousYear: '上一年',
nextYear: '下一年',
selectYear: '选择年',
selectMonth: '选择月',
more: '更多',
today: '今'
},
"en":{
..... //英文,国际化
}
},
//今天
today: {
year: new Date().getFullYear(),
month: new Date().getMonth(),
date: new Date().getDate()
},
//是否是闰年
isLeapYear: function (year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
},
//获取月份的天数
getDaysInMonth: function (year, month) {
return [31, (GLOBLE.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
},
//获取上下月
getPNDate: function (year, month, path) {
if ((month += (path || 0)) < 1) {
month = 12;
year--;
}
else if (month > 12) {
month = 1;
year++;
}
var date = new Date(year, month, 0);
return {
year: date.getFullYear(),
month: date.getMonth(),
day: date.getDate()
};
},
//转换为格式对象
parseFormat: function (format) {
var separator = format.match(/[.\/\-\s].*?/), parts = format.split(/\W+/);
if (!separator || !parts || parts.length === 0) {
throw new Error("Invalid date format.");
}
return {
separator: separator,
parts: parts
};
},
//按照格式对象,格式化字符日期
parseDate: function (date, format) {
var parts = date.split(format.separator), date = new Date(), val;
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
if (parts.length === format.parts.length) {
var year = date.getFullYear(), day = date.getDate(), month = date.getMonth();
for (var i = 0, cnt = format.parts.length; i < cnt; i++) {
val = parseInt(parts[i], 10) || 1;
switch (format.parts[i]) {
case 'dd':
case 'd':
day = val;
date.setDate(val);
break;
case 'mm':
case 'm':
month = val - 1;
date.setMonth(val - 1);
break;
case 'yy':
year = 2000 + val;
date.setFullYear(2000 + val);
break;
case 'yyyy':
year = val;
date.setFullYear(val);
break;
}
}
date = new Date(year, month, day, 0, 0, 0);
}
return date;
},
//把日期格式化日期字符串(format的格式)
formatDate: function (date, format) {
var val = {
d: date.getDate(),
m: date.getMonth() + 1,
yy: date.getFullYear().toString().substring(2),
yyyy: date.getFullYear()
};
val.dd = (val.d < 10 ? '0' : '') + val.d;
val.mm = (val.m < 10 ? '0' : '') + val.m;
var date = [];
for (var i = 0, cnt = format.parts.length; i < cnt; i++) {
date.push(val[format.parts[i]]);
}
return date.join(format.separator);
}
};
假设现在你有一个input元素需要绑定日历控件,那么只需要var date = new CCalendar(input元素,{})。这时将调用create方法创建日历,但是不显示,当input元素获得焦点时,就会触发show方法,显示出来。你可以在第二个参数中,传入json对象,比如:maxDate(可选择的最大日期),minDate(可选择的最小日期),lang(语言版本,中文,英文),onSelect(选择日期前,触发的方法),onHide(隐藏日期后,触发的方法),onSelectBack(选择日期后,触发的方法)等属性值。
如果你需要弄成jQuery插件,或者弄到sea.js模块加载中去,请看:http://www.cnblogs.com/chaojidan/p/4213942.html
加油!
posted on 2014-12-09 09:24 chaojidan 阅读(3228) 评论(0) 编辑 收藏 举报