(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-day',
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 = [
'',
'
',
'
',
'
',
'
',
'{prev}',
/* '{next}',*/
'
',
'
',
'{yyyy}-年{mm}月',
'',
'
',
/* '{prev}',*/
'{next}',
'
',
'
',
'
',
'
',
'
',
'
',
'
',
'{prev}',
/* '{next}',*/
'
',
'
{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);
$item.addClass("markDay");
}
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;
}));