(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define('calendar', ['jquery'], factory);
} else if (typeof exports === 'object') {
module.exports = factory(require('jquery'));
} else {
factory(root.jquery);
}
}(this, function($) {
// default config
var defaults = {
// 宽度
width: 280,
// 高度, 不包含头部,头部固定高度
height: 280,
zindex: 1,
// selector or element
// 设置触发显示的元素,为null时默认显示
trigger: null,
// 偏移位置,可设正负值
// trigger 设置时生效
offset: [0, 1],
// 自定义类,用于重写样式
customclass: '',
// 显示视图
// 可选:date, month
view: 'date',
// 默认日期为当前日期
date: new date(),
format: 'yyyy/mm/dd',
// 一周的第一天
// 0表示周日,依次类推
startweek: 0,
// 星期格式
weekarray: ['日', '一', '二', '三', '四', '五', '六'],
// 设置选择范围
// 格式:[开始日期, 结束日期]
// 开始日期为空,则无上限;结束日期为空,则无下限
// 如设置2015年11月23日以前不可选:[new date(), null] or ['2015/11/23']
selectedrang: null,
// 日期关联数据 [{ date: string, value: object }, ... ]
// 日期格式与 format 一致
// 如 [ {date: '2015/11/23', value: '面试'} ]
data: null,
// 展示关联数据
// 格式化参数:{m}视图,{d}日期,{v}value
// 设置 false 表示不显示
label: '{d}\n{v}',
// 切换字符
prev: '<',
next: '>',
// 切换视图
// 参数:view, y, m
viewchange: $.noop,
// view: 视图
// date: 不同视图返回不同的值
// value: 日期关联数据
onselected: function(view, date, value) {
// body...
},
// 参数同上
onmouseenter: $.noop,
onclose: $.noop
},
// static variable
action_namespace = 'data-calendar-',
display_vd = '[' + action_namespace + 'display-date]',
display_vm = '[' + action_namespace + 'display-month]',
arrow_date = '[' + action_namespace + 'arrow-date]',
arrow_month = '[' + action_namespace + 'arrow-month]',
item_day = action_namespace + 'day',
item_month = action_namespace + 'month',
disabled = 'disabled',
mark_data = 'markdata',
view_class = {
date: 'calendar-d',
month: 'calendar-m'
},
old_day_class = 'old',
new_day_class = 'new',
today_class = 'now',
select_class = 'selected',
mark_day_html = '',
date_dis_tpl = '{year}/{month}',
item_style = 'style="width:{w}px;height:{h}px;line-height:{h}px"',
week_item_tpl = '
{wk}',
day_item_tpl = '{value}',
month_item_tpl = '{m}月',
template = [
'',
'
',
'
',
'
',
'
',
'
{yyyy}',
'
',
'{prev}',
'{next}',
'
',
'
',
'
{month}
',
'
',
'
',
'
',
''
],
os = object.prototype.tostring;
// utils
function isdate(obj) {
return os.call(obj) === '[object date]';
}
function isstring(obj) {
return os.call(obj) === '[object string]';
}
function getclass(el) {
return el.getattribute('class') || el.getattribute('classname');
}
// extension methods
string.prototype.repeat = function(data) {
return this.replace(/\{\w+\}/g, function(str) {
var prop = str.replace(/\{|\}/g, '');
return data[prop] || '';
});
}
string.prototype.todate = function() {
var dt = new date(),
dot = this.replace(/\d/g, '').charat(0),
arr = this.split(dot);
dt.setfullyear(arr[0]);
dt.setmonth(arr[1] - 1);
dt.setdate(arr[2]);
return dt;
}
date.prototype.format = function(exp) {
var y = this.getfullyear(),
m = this.getmonth() + 1,
d = this.getdate();
return exp.replace('yyyy', y).replace('mm', m).replace('dd', d);
}
date.prototype.issame = function(y, m, d) {
if (isdate(y)) {
var dt = y;
y = dt.getfullyear();
m = dt.getmonth() + 1;
d = dt.getdate();
}
return this.getfullyear() === y && this.getmonth() + 1 === m && this.getdate() === d;
}
date.prototype.add = function(n) {
this.setdate(this.getdate() + n);
}
date.prototype.minus = function(n) {
this.setdate(this.getdate() - n);
}
date.prototype.cleartime = function(n) {
this.sethours(0);
this.setseconds(0);
this.setminutes(0);
this.setmilliseconds(0);
return this;
}
date.isleap = function(y) {
return (y % 100 !== 0 && y % 4 === 0) || (y % 400 === 0);
}
date.getdaysnum = function(y, m) {
var num = 31;
switch (m) {
case 2:
num = this.isleap(y) ? 29 : 28;
break;
case 4:
case 6:
case 9:
case 11:
num = 30;
break;
}
return num;
}
date.getsiblingsmonth = function(y, m, n) {
var d = new date(y, m - 1);
d.setmonth(m - 1 + n);
return {
y: d.getfullyear(),
m: d.getmonth() + 1
};
}
date.getprevmonth = function(y, m, n) {
return this.getsiblingsmonth(y, m, 0 - (n || 1));
}
date.getnextmonth = function(y, m, n) {
return this.getsiblingsmonth(y, m, n || 1);
}
date.tryparse = function(obj) {
if (!obj) {
return obj;
}
return isdate(obj) ? obj : obj.todate();
}
// calendar class
function calendar(element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn.calendar.defaults, options);
this.$element.addclass('calendar ' + this.options.customclass);
this.width = this.options.width;
this.height = this.options.height;
this.date = this.options.date;
this.selectedrang = this.options.selectedrang;
this.data = this.options.data;
this.init();
}
calendar.prototype = {
constructor: calendar,
getdayaction: function(day) {
var action = item_day;
if (this.selectedrang) {
var start = date.tryparse(this.selectedrang[0]),
end = date.tryparse(this.selectedrang[1]);
if ((start && day < start.cleartime()) || (end && day > end.cleartime())) {
action = disabled;
}
}
return action;
},
getdaydata: function(day) {
var ret, data = this.data;
if (data) {
for (var i = 0, len = data.length; i < len; i++) {
var item = data[i];
if (day.issame(item.date.todate())) {
ret = item.value;
}
}
}
return ret;
},
getdayitem: function(y, m, d, f) {
var dt = this.date,
idt = new date(y, m - 1, d),
data = {
w: this.width / 7,
h: this.height / 7,
value: d
},
markdata,
$item;
var selected = dt.issame(y, m, d) ? select_class : '';
if (f === 1) {
data['class'] = old_day_class;
} else if (f === 3) {
data['class'] = new_day_class;
} else {
data['class'] = '';
}
if (dt.issame(y, m, d)) {
data['class'] += ' ' + today_class;
}
data.action = this.getdayaction(idt);
markdata = this.getdaydata(idt);
$item = $(day_item_tpl.repeat(data));
if (markdata) {
$item.data(mark_data, markdata);
$item.html(d + mark_day_html);
}
return $item;
},
getdayshtml: function(y, m) {
var year, month, firstweek, daysnum, prevm, prevdiff,
dt = this.date,
$days = $('
');
if (isdate(y)) {
year = y.getfullyear();
month = y.getmonth() + 1;
} else {
year = number(y);
month = number(m);
}
firstweek = new date(year, month - 1, 1).getday() || 7;
prevdiff = firstweek - this.options.startweek;
daysnum = date.getdaysnum(year, month);
prevm = date.getprevmonth(year, month);
prevdaysnum = date.getdaysnum(year, prevm.m);
nextm = date.getnextmonth(year, month);
// month flag
var prev_flag = 1,
curr_flag = 2,
next_flag = 3,
count = 0;
for (var p = prevdaysnum - prevdiff + 1; p <= prevdaysnum; p++, count++) {
$days.append(this.getdayitem(prevm.y, prevm.m, p, prev_flag));
}
for (var c = 1; c <= daysnum; c++, count++) {
$days.append(this.getdayitem(year, month, c, curr_flag));
}
for (var n = 1, nl = 42 - count; n <= nl; n++) {
$days.append(this.getdayitem(nextm.y, nextm.m, n, next_flag));
}
return $('').width(this.options.width).append($days);
},
getweekhtml: function() {
var week = [],
weekarray = this.options.weekarray,
start = this.options.startweek,
len = weekarray.length,
w = this.width / 7,
h = this.height / 7;
for (var i = start; i < len; i++) {
week.push(week_item_tpl.repeat({
w: w,
h: h,
wk: weekarray[i]
}));
}
for (var j = 0; j < start; j++) {
week.push(week_item_tpl.repeat({
w: w,
h: h,
wk: weekarray[j]
}));
}
return week.join('');
},
getmonthhtml: function() {
var month = [],
w = this.width / 4,
h = this.height / 4,
i = 1;
for (; i < 13; i++) {
month.push(month_item_tpl.repeat({
w: w,
h: h,
m: i
}));
}
return month.join('');
},
setmonthaction: function(y) {
var m = this.date.getmonth() + 1;
this.$monthitems.children().removeclass(today_class);
if (y === this.date.getfullyear()) {
this.$monthitems.children().eq(m - 1).addclass(today_class);
}
},
fillstatic: function() {
var staticdata = {
prev: this.options.prev,
next: this.options.next,
week: this.getweekhtml(),
month: this.getmonthhtml()
};
this.$element.html(template.join('').repeat(staticdata));
},
updatedisdate: function(y, m) {
this.$disdate.html(date_dis_tpl.repeat({
year: y,
month: m
}));
},
updatedismonth: function(y) {
this.$dismonth.html(y);
},
filldateitems: function(y, m) {
var ma = [
date.getprevmonth(y, m), {
y: y,
m: m
},
date.getnextmonth(y, m)
];
this.$dateitems.html('');
for (var i = 0; i < 3; i++) {
var $item = this.getdayshtml(ma[i].y, ma[i].m);
this.$dateitems.append($item);
}
},
hide: function(view, date, data) {
this.$trigger.val(date.format(this.options.format));
this.options.onclose.call(this, view, date, data);
this.$element.hide();
},
trigger: function() {
this.$trigger = this.options.trigger instanceof $ ? this.options.trigger : $(this.options.trigger);
var _this = this,
$this = _this.$element,
post = _this.$trigger.offset(),
offs = _this.options.offset;
$this.addclass('calendar-modal').css({
left: (post.left + offs[0]) + 'px',
top: (post.top + _this.$trigger.outerheight() + offs[1]) + 'px',
zindex: _this.options.zindex
});
_this.$trigger.click(function() {
$this.show();
});
$(document).click(function(e) {
if (_this.$trigger[0] != e.target && !$.contains($this[0], e.target)) {
$this.hide();
}
});
},
render: function() {
this.$week = this.$element.find('.week');
this.$dateitems = this.$element.find('.date-items');
this.$monthitems = this.$element.find('.month-items');
this.$label = this.$element.find('.calendar-label');
this.$disdate = this.$element.find(display_vd);
this.$dismonth = this.$element.find(display_vm);
var y = this.date.getfullyear(),
m = this.date.getmonth() + 1;
this.updatedisdate(y, m);
this.updatemonthview(y);
this.filldateitems(y, m);
this.options.trigger && this.trigger();
},
setview: function(view) {
this.$element.removeclass(view_class.date + ' ' + view_class.month)
.addclass(view_class[view]);
this.view = view;
},
updatedateview: function(y, m, dirc, cb) {
m = m || this.date.getmonth() + 1;
var _this = this,
$dis = this.$dateitems,
exec = {
prev: function() {
var pm = date.getprevmonth(y, m),
ppm = date.getprevmonth(y, m, 2),
$previtem = _this.getdayshtml(ppm.y, ppm.m);
m = pm.m;
y = pm.y;
$dis.animate({
marginleft: 0
}, 300, 'swing', function() {
$dis.children(':last').remove();
$dis.prepend($previtem).css('margin-left', '-100%');
$.isfunction(cb) && cb.call(_this);
});
},
next: function() {
var nm = date.getnextmonth(y, m),
nnm = date.getnextmonth(y, m, 2),
$nextitem = _this.getdayshtml(nnm.y, nnm.m);
m = nm.m;
y = nm.y;
$dis.animate({
marginleft: '-200%'
}, 300, 'swing', function() {
$dis.children(':first').remove();
$dis.append($nextitem).css('margin-left', '-100%');
$.isfunction(cb) && cb.call(_this);
});
}
};
if (dirc) {
exec[dirc]();
} else {
this.filldateitems(y, m);
}
this.updatedisdate(y, m);
this.setview('date');
return {
y: y,
m: m
};
},
updatemonthview: function(y) {
this.updatedismonth(y);
this.setmonthaction(y);
this.setview('month');
},
getdisdatevalue: function() {
var arr = this.$disdate.html().split('/'),
y = number(arr[0]),
m = number(arr[1].match(/\d{1,2}/)[0]);
return [y, m];
},
selectedday: function(d, type) {
var arr = this.getdisdatevalue(),
y = arr[0],
m = arr[1],
toggleclass = function() {
this.$dateitems.children(':eq(1)')
.find('[' + item_day + ']:not(.' + new_day_class + ', .' + old_day_class + ')')
.removeclass(select_class)
.filter(function(index) {
return parseint(this.innerhtml) === d;
}).addclass(select_class);
};
if (type) {
var ret = this.updatedateview(y, m, {
'old': 'prev',
'new': 'next'
}[type], toggleclass);
y = ret.y;
m = ret.m;
this.options.viewchange('date', y, m);
} else {
toggleclass.call(this);
}
return new date(y, m - 1, d);
},
showlabel: function(event, view, date, data) {
var $lbl = this.$label;
$lbl.find('p').html(this.options.label.repeat({
m: view,
d: date.format(this.options.format),
v: data
}).replace(/\n/g, '
'));
var w = $lbl.outerwidth(),
h = $lbl.outerheight();
$lbl.css({
left: (event.pagex - w / 2) + 'px',
top: (event.pagey - h - 20) + 'px'
}).show();
},
haslabel: function() {
if (this.options.label) {
$('body').append(this.$label);
return true;
}
return false;
},
event: function() {
var _this = this,
vc = _this.options.viewchange;
// view change
_this.$element.on('click', display_vd, function() {
var arr = _this.getdisdatevalue();
_this.updatemonthview(arr[0], arr[1]);
vc('month', arr[0], arr[1]);
}).on('click', display_vm, function() {
var y = this.innerhtml;
_this.updatedateview(y);
vc('date', y);
});
// arrow
_this.$element.on('click', arrow_date, function() {
var arr = _this.getdisdatevalue(),
type = getclass(this),
y = arr[0],
m = arr[1];
var d = _this.updatedateview(y, m, type, function() {
vc('date', d.y, d.m);
});
}).on('click', arrow_month, function() {
var y = number(_this.$dismonth.html()),
type = getclass(this);
y = type === 'prev' ? y - 1 : y + 1;
_this.updatemonthview(y);
vc('month', y);
});
// selected
_this.$element.on('click', '[' + item_day + ']', function() {
var d = parseint(this.innerhtml),
cls = getclass(this),
type = /new|old/.test(cls) ? cls.match(/new|old/)[0] : '';
var day = _this.selectedday(d, type);
_this.options.onselected.call(this, 'date', day, $(this).data(mark_data));
_this.$trigger && _this.hide('date', day, $(this).data(mark_data));
}).on('click', '[' + item_month + ']', function() {
var y = number(_this.$dismonth.html()),
m = parseint(this.innerhtml);
_this.updatedateview(y, m);
vc('date', y, m);
_this.options.onselected.call(this, 'month', new date(y, m - 1));
});
// hover
_this.$element.on('mouseenter', '[' + item_day + ']', function(e) {
var arr = _this.getdisdatevalue(),
day = new date(arr[0], arr[1] - 1, parseint(this.innerhtml));
if (_this.haslabel && $(this).data(mark_data)) {
$('body').append(_this.$label);
_this.showlabel(e, 'date', day, $(this).data(mark_data));
}
_this.options.onmouseenter.call(this, 'date', day, $(this).data(mark_data));
}).on('mouseleave', '[' + item_day + ']', function() {
_this.$label.hide();
});
},
resize: function() {
var w = this.width,
h = this.height,
hdh = this.$element.find('.calendar-hd').outerheight();
this.$element.width(w).height(h + hdh)
.find('.calendar-inner, .view')
.css('width', w + 'px');
this.$element.find('.calendar-ct').width(w).height(h);
},
init: function() {
this.fillstatic();
this.resize();
this.render();
this.view = this.options.view;
this.setview(this.view);
this.event();
},
setdata: function(data) {
this.data = data;
if (this.view === 'date') {
var d = this.getdisdatevalue();
this.filldateitems(d[0], d[1]);
} else if (this.view === 'month') {
this.updatemonthview(this.$dismonth.html());
}
},
methods: function(name, args) {
if (os.call(this[name]) === '[object function]') {
return this[name].apply(this, args);
}
}
};
$.fn.calendar = function(options) {
var calendar = this.data('calendar'),
fn,
args = [].slice.call(arguments);
if (!calendar) {
return this.each(function() {
return $(this).data('calendar', new calendar(this, options));
});
}
if (isstring(options)) {
fn = options;
args.shift();
return calendar.methods(fn, args);
}
return this;
}
$.fn.calendar.defaults = defaults;
}));