lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,374 lines (1,219 loc) • 75.5 kB
JavaScript
/**
* @DateTime.js
* @author zhangxinxu
* @version
* Created: 15-07-03
*/
(function (global, factory) {
if (typeof define === 'function' && (define.amd || define.cmd)) {
define(factory);
} else {
global.DateTime = factory();
}
}(this, function (require) {
if (typeof require == 'function') {
require('common/ui/Follow');
} else if (!$().follow) {
if (window.console) {
window.console.error('need Follow.js');
}
return {};
}
var prefixDate = 'ui-date-';
var prefixRange = 'ui-range-';
var prefixDay = 'ui-day-';
var prefixYear = 'ui-year-';
var prefixMonth = 'ui-month-';
var prefixHour = 'ui-hour-';
var prefixMinute = 'ui-minute-';
var SELECTED = 'selected';
var ACTIVE = 'active';
var regDate = /-|\//g;
// 拓展方法之字符串变时间对象
String.prototype.toDate = function() {
var year, month, day;
var arrDate = this.split(regDate);
year = arrDate[0] * 1;
month = arrDate[1] || 1;
day = arrDate[2] || 1;
// 年份需要是数值字符串
if (!year) {
return new Date();
}
return new Date(year, month - 1, day);
};
// 日期对象变成年月日数组
Date.prototype.toArray = function() {
var year = this.getFullYear();
var month = this.getMonth() + 1;
var date = this.getDate();
if (month < 10) {
month = '0' + month;
}
if (date < 10) {
date = '0' + date;
}
return [year, month, date];
};
/**
* 日期,时间选择器
* 基于HTML5时间类输入框
* @example new DateTime(trigger, options);
* @trigger 触发的元素,可以是文本框也可以是文本框容器(父级)
* @options 可选参数
*/
var DateTime = function(trigger, options) {
if (!trigger || !trigger.length) return this;
// 默认参数
var defaults = {
// 文本框初始值,优先从文本框获取。
value: '',
// 浮层面板类型,可以是'date'(2015-01-01), 'year'(2015), 'month'(2015-01), 'time'(12:00), 'date-range'(2015-01-01 至 2015-06-01)
type: 'auto',
// 时间范围最小值,优先从文本框获取。'auto'表示无最小限制。
min: 'auto',
// 时间范围最大值,优先从文本框获取。'auto'表示无最大限制。
max: 'auto',
// 文本框值修改时候触发的文本框事件
trigger: ['change'],
onShow: $.noop,
onHide: $.noop
};
// 参数合并
var params = $.extend({}, defaults, options || {});
// 找到文本输入框
var input = null;
if (trigger.get(0).type) {
input = trigger;
trigger = input.parent();
} else {
input = trigger.find('input');
}
// 如果没有时间类输入框,则byebye
if (input.length == 0) {
return this;
}
// readonly
input.prop('readonly', true);
// hover, active style
input.parent().hover(function() {
$(this).addClass('hover');
}, function() {
$(this).removeClass('hover');
});
// 时间选择的类型
var type = params.type;
if (type == 'auto') {
type = input.attr('type') || 'date';
}
// 插入下拉箭头
var id = input.attr('id');
if (!id) {
// 如果没有id, 创建随机id
// 下拉箭头需要
id = type + (Math.random() + '').replace('0.', '');
input.attr('id', id);
}
// 下拉元素
$('<label for="' + id + '"></label>').addClass(prefixDate + 'arrow')
// 插入到文本框的后面
.insertAfter(input);
// 初始值
var initValue = input.val();
if (initValue == '' && params.value) {
input.val(params.value);
initValue = params.value;
}
// 暴露的一些数据 提前暴露,因为将获取 min 和 max 封装成一个函数,函数需要使用 input 与 min
this.el = {};
this.el.trigger = trigger;
this.el.input = input;
this.type = type;
// 最大小值的处理在各个种类的面板中执行
// 以便支持动态时间修改
// 例如,选择A时间,然后设置B时间的min值为A时间
this.max = params.max;
this.min = params.min;
// 回调方法
this.callback = {
show: params.onShow,
hide: params.onHide,
trigger: params.trigger
};
// 初始值转换成时间值
switch (type) {
case 'date': case 'year': case 'month': {
// 日期
var initDate = initValue.toDate();
var arrDate = initDate.toArray();
if (initValue == '') {
// 赋值今日
// 当没有设置初始值,但是设置了 min 和 max 的时候,会出现看上去无法选择的状态. 所以当设置了 min 和 max 的话,要对 min 和 max 进行判断,看今天是否符合 max 和 min的范围里,如果不符合就将设置为 min 的值
var minMax = this.minMax('date');
var min = minMax.min;
var max = minMax.max;
if (
arrDate.join('') < min.toArray().join('') ||
arrDate.join('') > max.toArray().join('')
) {
arrDate = min.toArray();
}
if (type == 'date') {
input.val(arrDate.join('-'));
} else if (type == 'year') {
input.val(arrDate[0]);
} else if (type == 'month') {
input.val(arrDate.slice(0, 2).join('-'));
}
}
// eg. [2015,07,20]
this[SELECTED] = arrDate;
break;
}
case 'time': case 'hour': case 'minute': {
var arrTime = initValue.split(':');
// 时间
var hour = arrTime[0];
var minute = arrTime[1];
// 这里的处理情况如下
// 1. 空值
// 2. 小时数不是数值
// 3. 小时数不在0~24之间
// 以上均认为时间是00:00
if (initValue == '' || !(hour < 24 && hour > 0)) {
hour = '00';
minute = '00';
} else {
// 分钟无论如何都要是正常值
if (!(minute > 0 && minute < 60) || type == 'hour') {
minute = '00';
} else if (minute.length == 1) {
minute = '0' + minute;
}
// 补0是必要的
if (hour.length == 1) {
hour = '0' + hour;
}
}
input.val([hour, minute].join(':'));
this[SELECTED] = [hour, minute];
break;
}
case 'date-range': case 'month-range': {
// 跟 date 的情况一样, 当没有初始值,但是存在 min 或者 max 的设置的时候,并且今天或者本月不在这个范围里面,会出现无法选择的假象,对这情况处理
var minMax = this.minMax('date')
var min = minMax.min
var max = minMax.max
if (initValue.toDate() < min || initValue.toDate() > max) {
initValue = min.toArray().join('-')
}
// 日期范围
var beginDate = min;
var endDate = min;
// 前后时间字符串
var arrRange = initValue.split(' ');
// 有如下一些情况:
// 1. 空,则选择范围就是今日
// 2. 只有一个时间,则选择范围指这个时间到今天这个范围
// 3. 其他就是正常的
if (initValue != '' && arrRange.length == 1) {
var someDate = arrRange[0].toDate();
if (someDate.getTime() > beginDate.getTime()) {
endDate = someDate;
} else {
beginDate = someDate;
}
} else {
beginDate = arrRange[0].toDate();
endDate = arrRange[arrRange.length - 1].toDate();
}
// 赋值
var arrBegin = beginDate.toArray();
var arrEnd = endDate.toArray();
if (type == 'date-range') {
input.val(arrBegin.join('-') + ' 至 ' + arrEnd.join('-'));
} else {
input.val(arrBegin.slice(0, 2).join('-') + ' 至 ' + arrEnd.slice(0, 2).join('-'));
}
// 存储
this[SELECTED] = [arrBegin, arrEnd];
break;
}
}
var self = this;
var container = $('<div></div>').addClass(prefixDate + 'container').delegate('a', 'click', function() {
// 各个分支中可能会用到的变量
var year = 0;
var month = 0;
var date = 0;
var hour = 0;
// 日期范围
var arrRange = [];
// 根据选中状态决定新的状态
var range;
// 按钮类型
var typeButton;
// 各种事件
switch (container.attr('data-type')) {
case 'date': {
// 日期选择主体
// 1. 前后月份选择
if (/prev|next/.test(this.className)) {
month = $(this).attr('data-month');
self[SELECTED][1] = month * 1;
// 日期和月份要匹配,例如,不能出现4月31日
var monthDay = self._monthDay(self[SELECTED]);
var day = self[SELECTED][2];
var dayOverflow = container.data('dayOverflow');
var dayMax = (function() {
if (month - 1 < 0) {
return monthDay[11];
} else if (month > monthDay.length) {
return monthDay[0];
}
return monthDay[month - 1];
})();
// 例如,我们超出日期是31日,如果月份可以满足31日,使用31日
if (dayOverflow) {
self[SELECTED][2] = Math.min(dayOverflow, dayMax);
} else if (self[SELECTED][2] > dayMax) {
self[SELECTED][2] = dayMax;
// 这里是对体验的升级,
// 虽然下月份变成了30号,但是再回来时候,原来的31号变成了30号
// 不完美,我们需要处理下
// 通过一个变量记住,点击item项的时候,移除
// 且只在第一次的时候记住
// 因为28,29,30,31都可能出现,每次记忆会混乱
container.data('dayOverflow', day);
}
self[SELECTED] = self[SELECTED].join('-').toDate().toArray();
// 刷新
self.date();
// 如果在时间范围内
if (container.find('.' + SELECTED).get(0).href) {
self.val();
}
} else if (/item/.test(this.className)) {
// 选择某日期啦
date = this.innerHTML;
if (/\D/.test(date)) {
// 今天
self[SELECTED] = new Date().toArray();
} else {
if (date < 10) date = '0' + date;
// 修改全局
self[SELECTED][2] = date;
}
// 赋值
self.val();
// 隐藏
self.hide();
container.removeData('dayOverflow');
} else if ($(this).attr('data-type') == 'month') {
// 切换到月份选择
self.month();
}
break;
}
case 'date-range': {
// 区域选择
// 1. 前后月份选择
if (/prev|next/.test(this.className)) {
month = $(this).attr('data-month') * 1;
arrRange = self.el.container.data('date') || self[SELECTED][0];
// 跟其他面板不同,这里只刷新,点击确定再赋值
self.el.container.data('date', new Date(arrRange[0], month - 1, 1).toArray());
// 之前arrRange[2]存在跨多月风险,尤其31号的日子
// 刷新
self['date-range']();
} else if (/item/.test(this.className)) {
// 选择某日期
// 获得选中的年月日
year = $(this).attr('data-year');
month = $(this).attr('data-month');
date = this.innerHTML;
if (month < 10) month = '0' + month;
if (date < 10) date = '0' + date;
// 根据选中状态决定新的状态
range = self[SELECTED];
if (range[0].join() == range[1].join()) {
// 如果之前前后日期一样,说明只选中了一个日期
// 根据前后顺序改变其中一个日期
if (year + month + date > range[0].join('')) {
// 新时间靠后
range[1] = [year, month, date];
} else {
range[0] = [year, month, date];
}
} else {
// 如果前后时间不一样,说明现在有范围
// 则取消范围,变成单选
range = [[year, month, date], [year, month, date]];
}
self[SELECTED] = range;
self['date-range']();
} else if (/button/.test(this.className)) {
typeButton = $(this).attr('data-type');
if (typeButton == 'ensure') {
// 点击确定按钮
// 赋值
self.val();
// 修改存储值
self._rangeSelected = self[SELECTED];
// 关闭浮层
self.hide();
} else if (typeButton == 'cancel') {
// 重置选中值
if (self._rangeSelected) {
self[SELECTED] = self._rangeSelected;
}
// 关闭浮层
self.hide();
}
}
break;
}
case 'month-range': {
// 区域选择
// 1. 前后年份选择
if (/prev|next/.test(this.className)) {
year = $(this).attr('data-year') * 1;
arrRange = self.el.container.data('date') || self[SELECTED][0];
// 跟其他面板不同,这里只刷新,点击确定再赋值
self.el.container.data('date', new Date(year, arrRange[1], 1).toArray());
// 刷新
self['month-range']();
} else if (/item/.test(this.className)) {
// 选择某日期
// 获得选中的年月日
year = $(this).attr('data-year');
month = $(this).attr('data-value');
date = '01';
// 根据选中状态决定新的状态
range = self[SELECTED];
if (range[0].join() == range[1].join()) {
// 如果之前前后日期一样,说明只选中了一个日期
// 根据前后顺序改变其中一个日期
if (year + month + date > range[0].join('')) {
// 新时间靠后
range[1] = [year, month, date];
} else {
range[0] = [year, month, date];
}
} else {
// 如果前后时间不一样,说明现在有范围
// 则取消范围,变成单选
range = [[year, month, date], [year, month, date]];
}
self[SELECTED] = range;
self['month-range']();
} else if (/button/.test(this.className)) {
typeButton = $(this).attr('data-type');
if (typeButton == 'ensure') {
// 点击确定按钮
// 赋值
self.val();
// 修改存储值
self._rangeSelected = self[SELECTED];
// 关闭浮层
self.hide();
} else if (typeButton == 'cancel') {
// 重置选中值
if (self._rangeSelected) {
self[SELECTED] = self._rangeSelected;
}
// 关闭浮层
self.hide();
}
}
break;
}
case 'month': {
// 选择月份,可能从年份,也可能从日期过来
// 1. 前后年份
if (/prev|next/.test(this.className)) {
year = $(this).attr('data-year');
// 修改当前选中的年份数
self[SELECTED][0] = year * 1;
// 刷新
self.month();
// 文本框赋值
// 如果在区域内状态
if (container.find('.' + SELECTED).get(0).href) {
self.val();
}
} else if (/item/.test(this.className)) {
// value实际上是月份两位数值
var value = $(this).attr('data-value');
if (value) {
self[SELECTED][1] = value;
} else {
// 今月,只改变年月为今年和今月
var arrToday = new Date().toArray();
self[SELECTED][0] = arrToday[0];
self[SELECTED][1] = arrToday[1];
}
// 在这里赋值前要先判断新选择的月份以及旧的日期是否在可选范围内, 否则会出现选取到超越范围的日期. 要将向外补露的数据提前, 因为 minMax 函数
var minMax = self.minMax('date');
var min = minMax.min;
var max = minMax.max;
var nowSelect = self[SELECTED].join('');
// 当前值符合区域内才进行赋值操作
if (
min.toArray().join('') <= nowSelect &&
max.toArray().join('') >= nowSelect
) {
// 赋值
self.val();
}
// 根据是否是月份输入框,决定是面板切换,还是关闭
if (self.type == 'month') {
// 隐藏
self.hide();
} else {
self.date();
}
} else if ($(this).attr('data-type') == 'year') {
// 切换到年份选择
self.year();
}
break;
}
case 'year': {
// 选择年份,可能从月份过来,也可能直接打开
// 1. 前后12年翻页
if (/prev|next/.test(this.className)) {
year = $(this).attr('data-year');
// 修改当前选中的年份数
self[SELECTED][0] = year * 1;
// 刷新
self.year();
// 文本框赋值
// 如果在区域内状态
if (container.find('.' + SELECTED).get(0).href) {
self.val();
}
} else if (/item/.test(this.className)) {
if (this.innerHTML == '今年') {
self[SELECTED][0] = new Date().getFullYear();
} else {
self[SELECTED][0] = this.innerHTML * 1;
}
// 在这里赋值前要先判断新选择的年份以及旧的月份是否在可选范围内, 要将向外补露的数据提前,因为 minMax 方法
// 最大月份和最小月份
var minMax = self.minMax('date')
var min = minMax.min
var max = minMax.max
var nowSelect = self[SELECTED].join('')
// 当前值符合区域内才进行赋值操作
if (
min.toArray().join('') <= nowSelect &&
max.toArray().join('') >= nowSelect
) {
// 赋值
self.val()
}
// 如果是年份选择输入框
if (self.type == 'year') {
// 隐藏
self.hide();
} else {
// 回到月份面板
self.month();
}
}
break;
}
case 'minute': {
// 选择分钟,可以是minute类型,或者time类型, 但不包括hour类型
// 1. 前后翻页
if (/prev|next/.test(this.className)) {
hour = $(this).attr('data-hour');
if (hour.length == 1) {
hour = '0' + hour;
}
// 修改当前选中的小时数
self[SELECTED][0] = hour;
// 刷新
self.minute();
// 文本框赋值
// 如果在区域内状态
if (container.find('.' + SELECTED).attr('href')) {
self.val();
}
} else if (/item/.test(this.className)) {
// 确定选择时间
self[SELECTED] = this.innerHTML.split(':');
self.val();
self.hide();
} else if ($(this).attr('data-type') == 'hour') {
// 切换到小时选择
self.hour();
}
break;
}
case 'hour': {
if (/item/.test(this.className)) {
// 修改选中的小时
self[SELECTED][0] = this.innerHTML.split(':')[0];
// 在这里赋值前要先判断新选择的小时以及旧的分钟是否在可选范围内, 要将向外补露的数据提前
// 最大月份和最小月份
var minMax = self.minMax('minute');
var min = minMax.min;
var max = minMax.max;
var nowSelect = self[SELECTED].join('');
// 当前值符合区域内才进行赋值操作
if (min <= nowSelect && max >= nowSelect) {
// 赋值
self.val();
}
// 如果是从分钟模式切换过来,则切换回去,否则,直接隐藏
if (self.type == 'hour') {
self.hide();
} else {
self.minute();
}
}
}
}
});
this.el.container = container;
trigger.click($.proxy(function(event) {
if (!this.display) {
this.show();
} else {
this.hide();
}
event.preventDefault();
}, this));
// keyboard
var randId = ('d_' + Math.random()).replace('0.', '');
container.attr('id', randId).addClass('ESC');
input.attr('data-target', randId);
// 记录input的id
container.attr('data-id', id);
input.on({
keydown: function (event) {
if (event.keyCode == 13) {
this.click();
event.preventDefault();
}
},
focus: function () {
if (window.isKeyEvent) {
trigger.addClass('ui-outline');
}
},
blur: function () {
trigger.removeClass('ui-outline');
}
});
// 时间范围选择点击页面空白区域不会隐藏
$(document).mouseup($.proxy(function(event) {
// 点击页面空白区域,隐藏
var target = event && event.target;
var container = this.el.container[0];
if (target &&
trigger[0] != target && trigger[0].contains(target) == false &&
container != target && container.contains(target) == false
) {
this.hide();
}
}, this));
// IE9+ 前后分页图标使用内联SVG, 以便支持retina屏幕
this.svg = '';
if (window.addEventListener) {
this.svg = '<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="0 0 200 200"><path d="M85.876,100.5l49.537-50.526c4.089-4.215,4.089-11.049,0-15.262 c-4.089-4.218-10.719-4.218-14.808,0L63.586,92.868c-4.089,4.215-4.089,11.049,0,15.264l57.018,58.156 c4.089,4.215,10.719,4.215,14.808,0c4.089-4.215,4.089-11.049,0-15.262L85.876,100.5z"/></svg>';
}
return this;
};
/**
* 输入框的值根据日期类型进行格式化
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.format = function() {
// 根据当前value值修改实例对象缓存的选中值
// 特殊情况一般不处理
var type = this.type;
var initValue = this.el.input.val();
if (initValue == '') {
return this;
}
switch (type) {
case 'date': case 'year': case 'month': {
// 日期
var initDate = initValue.toDate();
var arrDate = initDate.toArray();
// eg. [2015,07,20]
this[SELECTED] = arrDate;
break;
}
case 'time': case 'hour': case 'minute': {
var arrTime = initValue.split(':');
// 时间
var hour = arrTime[0];
var minute = arrTime[1];
// 补0
if (arrTime.length == 2) {
// 分钟无论如何都要是正常值
if (!(minute > 0 && minute < 60) || type == 'hour') {
minute = '00';
} else if (minute.length == 1) {
minute = '0' + minute;
}
// 补0是必要的
if (hour.length == 1) {
hour = '0' + hour;
}
this.el.input.val([hour, minute].join(':'));
this[SELECTED] = [hour, minute];
}
break;
}
case 'date-range': case 'month-range': {
// 日期范围
var beginDate = new Date();
var endDate = new Date();
// 前后时间字符串
// 确定了数组长度是3 没有必要再去求数组长度减 1
var arrRange = initValue.split(' ');
if (arrRange.length == 3) {
beginDate = arrRange[0].toDate();
endDate = arrRange[2].toDate();
// 存储
this[SELECTED] = [beginDate.toArray(), endDate.toArray()];
}
break;
}
}
return this;
};
/**
* 赋值
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.val = function() {
var input = this.el.input;
var selected = this[SELECTED];
var value = input.val();
switch (this.type) {
case 'date': {
input.val(selected.join('-'));
break;
}
case 'month': {
input.val(selected.slice(0, 2).join('-'));
break;
}
case 'year': {
input.val(selected[0]);
break;
}
case 'date-range': {
input.val(selected[0].join('-') + ' 至 ' + selected[1].join('-'));
break;
}
case 'month-range': {
input.val(selected[0].slice(0, 2).join('-') + ' 至 ' + selected[1].slice(0, 2).join('-'));
break;
}
case 'time': case 'minute': {
input.val(selected.join(':'));
break;
}
case 'hour': {
input.val(selected[0] + ':00');
break;
}
}
if (input.val() != value) {
if ($.isArray(this.callback.trigger)) {
$.each(this.callback.trigger, function(i, eventType) {
input.trigger(eventType);
});
} else {
input.trigger(this.callback.trigger);
}
}
return this;
};
/**
* 返回日历HTML字符串等数据的私有方法
// 因date和range浮层主结构类似,因此这里公用下
* @param {Array} date 格式化为数组的日期
* @return {Object} 返回包含需要的数据的对象,生成的HTML字符内容以及最大最小月份等
*/
DateTime.prototype._calendar = function(date) {
var html = '';
// 根据当前日期数据返回
// eg. [2015,'02', 23]
var arrDate = date;
// 类型
var type = this.type;
// 整合到 minMax 函数里面
var minMax = this.minMax('date')
var min = minMax.min
var max = minMax.max
var chinese = ['日', '一', '二', '三', '四', '五', '六'];
var monthDay = this._monthDay(arrDate);
// 目前日期对象
var currentDate = arrDate.join('-').toDate();
// 3 星期几七大罪
html = html + '<div class="' + prefixDay + 'x">' + (function() {
var htmlDay = '';
$.each(chinese, function(index, value) {
htmlDay = htmlDay + '<span class="' + prefixDay + 'item">' + value + '</span>';
});
return htmlDay;
})() + '</div>';
// 4. 日期详细
// 4.1 首先算出今月1号是星期几
var newDate = arrDate.join('-').toDate();
var dayFirst = 0;
// 设置为1号
newDate.setDate(1);
if (newDate.getDate() == 2) {
newDate.setDate(0);
}
// 每月的第一天是星期几
dayFirst = newDate.getDay();
// 上个月是几月份
var lastMonth = newDate.getMonth() - 1;
if (lastMonth < 0) {
lastMonth = 11;
}
var htmlData = 'data-year="' + arrDate[0] + '" data-month="' + (newDate.getMonth() + 1) + '"';
var htmlYearMonthData = 'data-date=';
var htmlFullData = '';
html = html + '<div class="' + prefixDate + 'body">' + (function() {
var htmlDate = '';
var cl = '';
for (var tr = 0; tr < 6; tr++) {
htmlDate = htmlDate + '<div class="' + prefixDate + 'tr">';
// 日期遍历
for (var td = 0; td < 7; td++) {
// 类名
cl = prefixDate + 'item col' + td;
// 今天
var yearNow = arrDate[0];
var monthNow = newDate.getMonth() + 1;
var dayNow;
var dateNow;
// 由于range选择和date选择UI上有比较大大差异
// 为了可读性以及后期维护
// 这里就不耦合在一起,而是分开处理
if (type == 'date') {
// 第一行上个月一些日期补全
if (tr == 0 && td < dayFirst) {
// 当前日子
dayNow = monthDay[lastMonth] - dayFirst + td + 1;
// 当前日期
dateNow = new Date(yearNow, lastMonth, dayNow);
// 完整data-date属性及其值
htmlFullData = htmlYearMonthData + dateNow.toArray().join('-');
// HTML拼接
htmlDate = htmlDate + '<span class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</span>';
} else {
// 当前日子
dayNow = tr * 7 + td - dayFirst + 1;
// 如果没有超过这个月末
if (dayNow <= monthDay[newDate.getMonth()]) {
// 这个日子对应的时间对象
dateNow = new Date(yearNow, newDate.getMonth(), dayNow);
// 完整data-date属性及其值
htmlFullData = htmlYearMonthData + dateNow.toArray().join('-');
// 如果日子匹配
if (currentDate.getDate() == dayNow) {
cl = cl + ' ' + SELECTED;
}
// 如果在日期范围内
// 直接使用时间对象 Date 类作比较
if (dateNow >= min && dateNow <= max) {
htmlDate = htmlDate + '<a href="javascript:;" ' + htmlData + ' class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</a>';
} else {
htmlDate = htmlDate + '<span class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</span>';
}
} else {
dayNow = dayNow - monthDay[newDate.getMonth()];
// 更新htmlFullData
htmlFullData = htmlYearMonthData + new Date(yearNow, monthNow, dayNow).toArray().join('-');
// 日期字符拼接
htmlDate = htmlDate + '<span class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</span>';
}
}
} else if (type == 'date-range') {
// 非当前月部分使用空格补全
if (tr == 0 && td < dayFirst) {
htmlDate = htmlDate + '<span class="' + cl + '"> </span>';
} else {
dayNow = tr * 7 + td - dayFirst + 1;
// 如果没有超过这个月末
if (dayNow <= monthDay[newDate.getMonth()]) {
// 这个日子对应的时间对象
dateNow = new Date(yearNow, newDate.getMonth(), dayNow);
// 完整data-date属性及其值
htmlFullData = htmlYearMonthData + dateNow.toArray().join('-');
// range选择的匹配规则如下:
// 1. 获得已经选中到时间范围
// 2. 起始时间和结束时间是方框表示
// 3. 之间的时间是选中表示
var dateBegin = this[SELECTED][0].join('-').toDate();
var dateEnd = this[SELECTED][1].join('-').toDate();
// 各个时间的时间戳
var timeNow = dateNow.getTime();
var timeBegin = dateBegin.getTime();
var timeEnd = dateEnd.getTime();
if (timeNow >= timeBegin && timeNow <= timeEnd) {
// 在时间范围之内
cl = cl + ' ' + SELECTED;
// 跟开始时间一样
if (timeNow == timeBegin) {
cl = cl + ' ' + prefixDate + 'begin';
}
// 跟结束时间一样
if (timeNow == timeEnd) {
cl = cl + ' ' + prefixDate + 'end';
}
// 今月的第一天还是最后一天
if (dayNow == 1) {
cl = cl + ' ' + prefixDate + 'first';
} else if (dayNow == monthDay[newDate.getMonth()]) {
cl = cl + ' ' + prefixDate + 'last';
}
}
// 如果在日期范围内
// 直接使用时间对象 Date 类作比较
if (dateNow >= min && dateNow <= max) {
htmlDate = htmlDate + '<a href="javascript:;" ' + htmlData + ' class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</a>';
} else {
htmlDate = htmlDate + '<span class="' + cl + '" ' + htmlFullData + '>' + dayNow + '</span>';
}
} else {
htmlDate = htmlDate + '<span class="' + cl + '"> </span>';
}
}
}
}
htmlDate += '</div>';
}
return htmlDate;
}).call(this) + '</div>';
return {
monthDay: monthDay,
html: html,
min: min,
max: max
};
};
/**
* 当前日期下这一年每月最大的日期数目
* @param {Array} date 格式化为数组的日期
* @return {Array} 返回这一年每月最大的日期数目
*/
DateTime.prototype._monthDay = function(date) {
var arrDate = date;
if ($.isArray(date) == false) {
arrDate = date.toArray();
}
var monthDay = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// 如果是闰年
if (((arrDate[0] % 4 == 0) && (arrDate[0] % 100 != 0)) || (arrDate[0] % 400 == 0)) {
monthDay[1] = 29;
}
return monthDay;
},
/**
* 上个月同日期
* @param {Array} date 格式化为数组的日期
* @return {Object} 返回Date类型对象
*/
DateTime.prototype._datePrevMonth = function(date) {
// add on 2016-03-31
// 原先的加减月份的跨月计算是有些问题的
// 因为例如31号的下一月,系统会自动变成下下个月的1号
var arrDate = date;
if ($.isArray(date) == false) {
// 日期数组化
arrDate = date.toArray();
}
var month = arrDate[1] * 1;
var monthDay = this._monthDay(arrDate);
if (month == 1) {
// 上个月是前一年的12月
// 12月有31号,无压力
return [arrDate[0] - 1, 12, arrDate[2]].join('-').toDate();
}
// 其他月份不跨年
// 如果上一个月的最后1天小于本月的最后1天
// 嘿嘿嘿嘿
// 年不变,月减一,日期下个月
if (monthDay[month - 2] < arrDate[2]) {
return [arrDate[0], month - 1, monthDay[month - 2]].join('-').toDate();
}
// 年不变,月减一,日期不变
return [arrDate[0], month - 1, arrDate[2]].join('-').toDate();
},
/**
* 下个月同日期
* @param {Array} date 格式化为数组的日期
* @return {Object} 返回Date类型对象
*/
DateTime.prototype._dateNextMonth = function(date) {
var arrDate = date;
if ($.isArray(date) == false) {
// 日期数组化
arrDate = date.toArray();
}
var month = arrDate[1] * 1;
var monthDay = this._monthDay(arrDate);
if (month == 12) {
// 下个月跨年了
// 1月份也有31天,无压力
return [arrDate[0] + 1, 1, arrDate[2]].join('-').toDate();
}
// 其他月份不跨年
// 如果下个月最后1天小于本月最后1天,例如,3月31日
if (monthDay[month] < arrDate[2]) {
return [arrDate[0], month + 1, monthDay[month]].join('-').toDate();
}
// 其他时候正常
return [arrDate[0], month + 1, arrDate[2]].join('-').toDate();
},
/**
* 选择日期
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.date = function() {
var arrDate = this[SELECTED];
// 目前日期对象
// var currentDate = arrDate.join('-').toDate();
// 上一个月
var prevMonth = arrDate[1] - 1;
// 下一个月
var nextMonth = arrDate[1] * 1 + 1;
var obj = this._calendar(arrDate);
// 选择日期的完整HTML代码
// 1. 日期专属类名容器
var html = '<div class="' + prefixDate + 'x">';
// 2. 头部月份切换
html = html + '<div class="' + prefixDate + 'head">';
// 根据前后月份是否在范围之外,决定使用的标签类型
// span标签则是禁用态,本组件全部都是如此
// 2.1 上个月
// datePrevMonth指上个月日期
var datePrevMonth = this._datePrevMonth(arrDate);
// prevMonth指上个月
var prevMonthGet = datePrevMonth.getMonth();
var prevYearGet = datePrevMonth.getFullYear();
//if (datePrevMonth >= obj.min && datePrevMonth <= obj.max) {
// add on 2015-12-01
// 原来的判断逻辑有问题
// 只要上个月份的最大日期比最小限制大就可以了
if (new Date(prevYearGet, prevMonthGet, obj.monthDay[prevMonthGet]) >= obj.min && datePrevMonth <= obj.max) {
html = html + '<a href="javascript:" class="' + prefixDate + 'prev" data-month="' + prevMonth + '">' + this.svg + '</a>';
} else {
html = html + '<span class="' + prefixDate + 'prev">' + this.svg + '</span>';
}
// 2.2 下个月
var dateNextMonth = this._dateNextMonth(arrDate);
var nextMonthGet = dateNextMonth.getMonth();
var nextYearGet = dateNextMonth.getFullYear();
//if (dateNextMonth >= obj.min && dateNextMonth <= obj.max) {
// add on 2015-12-01
// 经反馈,如果当前日期是30号,但是最大日期是下月1号,则下个月不能进入
// 这是有问题的
// 原因是此时的dateNextMonth > obj.max
// 因此判断应该从月初
if (dateNextMonth >= obj.min && new Date(nextYearGet, nextMonthGet, 1) <= obj.max) {
html = html + '<a href="javascript:" class="' + prefixDate + 'next" data-month="' + nextMonth + '">' + this.svg + '</a>';
} else {
html = html + '<span class="' + prefixDate + 'next">' + this.svg + '</span>';
}
// 头部月份公用结束
html = html + '<a href="javascript:" class="' + prefixDate + 'switch" data-type="month">' + arrDate.slice(0, 2).join('-') + '</a>\
</div>';
// 3. 主体内容来自_calendar()方法
html += obj.html;
// 今天
// 首先,今天要在时间范围内
if (new Date() >= obj.min && new Date() <= obj.max) {
html = html + '<a href="javascript:" class="' + prefixDate + 'item ' + prefixDate + 'now">今天</a>';
} else {
html = html + '<span class="' + prefixDate + 'item ' + prefixDate + 'now">今天</span>';
}
// 容器闭合标签
html += '</div>';
this.el.container.attr('data-type', 'date').html(html);
return this;
};
/**
* 选择日期范围
* @return {Object} 返回当前实例对象
*/
DateTime.prototype['date-range'] = function() {
var arrDates = this[SELECTED];
// 当前起始日期
// 默认(第一次)打开使用选中日期
// 如果有,使用面板存储月份
// 因为range选择不是即时更新文本框
var arrDate = this.el.container.data('date') || arrDates[0];
this.el.container.data('date', arrDate);
// 前一个月
var prevMonth = arrDate[1] - 1;
// 后一个月
var nextMonth = arrDate[1] * 1 + 1;
// 根据起始时间创建新的时间对象
// var dateBegin = arrDate.join('-').toDate();
// 含时间范围和对应月份日历HTML的对象
var obj = this._calendar(arrDate);
// 选择时间范围完整HTML代码
// 1. 同样的,range容器
var html = '<div class="' + prefixRange + 'x">';
// 2. 头部
html = html + '<div class="' + prefixDate + 'head">\
<div class="' + prefixDate + 'half">';
// 2.1 上一个月箭头
// 这里也不能使用全部情况 arrDate[2] 因为 arrDate[2] 默认是 1, 当 min 存在,最前的月份会无法选择 例如 prevMonth 是 10 , obj.min 为 10-14, 那么 datePrevMonth 为 10-01 永远是小于 obj.min 的
var day = arrDate[2]
var minArr = obj.min.toArray()
// 如果上个月匹配的是 min 的月份 那么将 day 改为 min 的日期
if (minArr[0] + '' + minArr[1] == arrDate[0] + '' + prevMonth) {
day = minArr[2]
}
var datePrevMonth = new Date(arrDate[0], prevMonth - 1, day);
if (datePrevMonth >= obj.min && datePrevMonth <= obj.max) {
html = html + '<a href="javascript:" class="' + prefixDate + 'prev" data-month="' + prevMonth + '" aria-label="上一个月,当前' + arrDate[1] + '月">' + this.svg + '</a>';
} else {
html = html + '<span class="' + prefixDate + 'prev">' + this.svg + '</span>';
}
// 今月月份显示
html = html + '<span class="' + prefixDate + 'switch">' + new Date(arrDate[0], prevMonth, arrDate[2]).toArray().slice(0, 2).join('-') + '</span>\
</div>\
<div class="' + prefixDate + 'half">';
// 2.2 下下个月
var dateNextMonth = new Date(arrDate[0], arrDate[1], 1);
var dateAfterMonth = new Date(arrDate[0], nextMonth, arrDate[2]);
// 例如是3月31日
// 此时 arrDate[1] '03'
// nextMonth 是 4
// 此时原来的实现是由bug的
// 因为下个月没有4月31日,于是,就会变成5月
// 因此arrDate[2]应使用1代替
// 下个月的当前日期在合理范围之内,则使用该月
if (dateAfterMonth >= obj.min && dateAfterMonth <= obj.max) {
html = html + '<a href="javascript:" class="' + prefixDate + 'next" data-month="' + nextMonth + '" aria-label="下一个月,当前' + nextMonth + '月">' + this.svg + '</a>';
} else {
html = html + '<span class="' + prefixDate + 'next">' + this.svg + '</span>';
}
// 下月月份显示
html = html + '<span class="' + prefixDate + 'switch">' + dateNextMonth.toArray().slice(0, 2).join('-') + '</span>\
</div>';
// 头部闭合
html += '</div>';
// 3. 两个主体列表
// 这里要嵌套一层range特有的body
html = html + '<div class="' + prefixRange + 'body ' + prefixRange + 'date-body">' +
// 根据_calendar()方法创建两个月份日历
'<div class="' + prefixDate + 'half">' + obj.html + '</div>' +
'<div class="' + prefixDate + 'half">' + this._calendar(dateNextMonth.toArray()).html + '</div>' +
// 主体标签闭合
'</div>';
// 4. 确定与取消按钮
html = html + '<div class="' + prefixRange + 'footer">' +
'<a href="javascript:;" class="ui-button ui-button-primary" data-type="ensure">确定</a>' +
'<a href="javascript:;" class="ui-button" data-type="cancel">取消</a>' +
'</div>';
// 容器闭合标签
html += '</div>';
this.el.container.attr('data-type', 'date-range').html(html);
return this;
};
/**
* 月份组装
* @param {Array} arrDate 数组化的日期表示值
* @return {Object} 返回的是后续方法需要的数据的纯对象,包括组装的HTML字符数据,月份最大和最小值。
*/
DateTime.prototype._month = function(arrDate) {
// 替换成集成方法
var minMax = this.minMax('month')
var min = minMax.min
var max = minMax.max
var chinese = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
// 年份
var year = arrDate[0] * 1;
// 类型
var type = this.type;
// 添加 container
var container = this.el.container;
var html = '<div class="' + prefixMonth + 'body">' + (function() {
var htmlDate = '';
var cl = '';
var month = '';
for (var i = 1; i <= 12; i += 1) {
// 合法格式字符串
if (i < 10) {
month = '0' + i;
} else {
month = i + '';
}
// 基本类名
cl = prefixDate + 'item';
// 如果 type 为 date 的时候在选择 month 时不会出现这个 selected 效果, 改为 container的 data-type 判断
if (container.attr('data-type') == 'month') {
if (i == arrDate[1]) {
// 选中态的类名
cl = cl + ' ' + SELECTED;
}
} else if (type == 'month-range') {
// range选择的匹配规则如下:
// 1. 获得已经选中到时间范围
// 2. 起始时间和结束时间是选中表示
// 3. 之间的时间也是选中表示
var strBegin = this[SELECTED][0].slice(0, 2).join('');
var strEnd = this[SELECTED][1].slice(0, 2).join('');
var strNow = year + month;
if (strNow >= strBegin && strNow <= strEnd) {
cl = cl + ' ' + SELECTED;
}
}
// 是否在范围以内
if (year + month >= min && year + month <= max) {
htmlDate = htmlDate + '<a href="javascript:" class="' + cl + '" data-year="' + year + '" data-value="' + month + '">' + chinese[i - 1] + '月</a>';
} else {
htmlDate = htmlDate + '<span class="' + cl + '" data-value="' + month + '">' + chinese[i - 1] + '月</span>';
}
}
return htmlDate;
}).call(this) +
'</div>';
return {
html: html,
min: min,
max: max
};
};
/**
* 选择月份
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.month = function() {
var arrDate = this[SELECTED];
// 将设置 container 的 属性 data-type 提前,目的是为了在 type = date 的时候,从日期选择跳转到月份选择的时候看到月份被选择的样式
var container = this.el.container
container.attr('data-type', 'month')
var obj = this._month(arrDate);
var min = obj.min;
var max = obj.max;
// 选择月份的完整HTML代码
// 1. month专属类名容器
var html = '<div class="' + prefixMonth + 'x">';
// 2. 月份切换的头部
var year = arrDate[0] * 1;
// 为什么呢?因为年份的范围是当前年份前面6个,后面5个
// 例如,假设今年是2015年,则头部年份范围是2009-2020
html = html + '<div class="' + prefixDate + 'head">';
// 2.1 是否还有上一年
if (year - 1 >= Math.floor(min / 100) && year - 1 <= Math.floor(max / 100)) {
html = html + '<a href="javascript:" class="' + prefixDate + 'prev" data-year="' + (year - 1) + '">' + this.svg + '</a>';
} else {
html = html + '<span class="' + prefixDate + 'prev">' + this.svg + '</spa