lu2
Version:
Simple and flexible UI component library based on native HTML and JavaScript
1,326 lines (1,167 loc) • 84.5 kB
JavaScript
/**
* @DateTime.js
* @author zhangxinxu
* @version
* @created: 15-07-03
* @editd: 19-11-12
*/
/* global module */
(function (global, factory) {
if (typeof exports === 'object' && typeof module !== 'undefined') {
global.Follow = require('./Follow');
module.exports = factory();
} else if (typeof define === 'function' && (define.amd || define.cmd)) {
define(factory);
} else {
global.DateTime = factory();
}
// eslint-disable-next-line
}((typeof global !== 'undefined') ? global
// eslint-disable-next-line
: ((typeof window !== 'undefined') ? window
: ((typeof self !== 'undefined') ? self : this)), function (require) {
var Follow = (this || self).Follow;
if (typeof require == 'function' && !Follow) {
Follow = require('common/ui/Follow');
} else if (!Follow) {
window.console.error('need Follow.js');
return {};
}
// 样式类名统一处理
var CL = {
toString: function () {
return 'ui-datetime';
}
};
['date', 'range', 'day', 'year', 'month', 'hour', 'minute'].forEach(function (key) {
CL[key] = function () {
return ['ui', key].concat([].slice.call(arguments)).join('-');
};
});
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 (element, options) {
var eleTrigger = element;
if (typeof element == 'string') {
eleTrigger = document.querySelector(element);
}
if (!eleTrigger || eleTrigger.hasAttribute('is-visible')) {
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',
onShow: function () {},
onHide: function () {}
};
// 参数合并
var objParams = Object.assign({}, defaults, options || {});
// 容器元素
var eleContainer = null;
// 找到文本输入框
var eleInput = null;
if (eleTrigger.getAttribute('type')) {
eleInput = eleTrigger;
eleTrigger = eleInput.parentElement;
} else {
eleInput = eleTrigger.querySelector('input');
}
// 如果没有时间类输入框,则认为不是点击展开模式
// 而是直接显示在容器中
if (!eleInput) {
eleInput = document.createElement('input');
if (objParams.min != 'auto') {
eleInput.setAttribute('min', objParams.min);
}
if (objParams.max != 'auto') {
eleInput.setAttribute('max', objParams.max);
}
if (objParams.type != 'auto') {
eleInput.setAttribute('type', objParams.type);
}
eleContainer = eleTrigger;
}
// 避免重复初始化
if (eleInput.data && eleInput.data.dateTime) {
return eleInput.data.dateTime;
}
// readonly
eleInput.setAttribute('readonly', 'readonly');
// 时间选择的类型
var strType = objParams.type;
if (strType == 'auto') {
strType = eleInput.getAttribute('type') || 'date';
}
// 插入下拉箭头
var strId = eleInput.id;
if (!strId) {
// 如果没有id, 创建随机id
// 下拉箭头需要
strId = ('lulu_' + Math.random()).replace('0.', '');
eleInput.id = strId;
}
// 下拉元素
var eleLabel = document.createElement('label');
eleLabel.classList.add(CL.date('arrow'));
// 插入到文本框的后面
if (getComputedStyle(eleTrigger).position != 'static') {
eleInput.insertAdjacentElement('afterend', eleLabel);
} else {
eleTrigger = eleInput;
}
// 初始值
var strInitValue = eleInput.value;
if (strInitValue == '' && objParams.value) {
strInitValue = eleInput.value = objParams.value;
}
// 初始值转换成时间值
switch (strType) {
case 'date': case 'year': case 'month': {
// 日期
var objInitDate = strInitValue.toDate();
var arrDate = objInitDate.toArray();
// 一开始的赋值
if (strInitValue == '') {
// 赋值今日
if (strType == 'date') {
eleInput.value = arrDate.join('-');
} else if (strType == 'year') {
eleInput.value = arrDate[0];
} else if (strType == 'month') {
eleInput.value = arrDate.slice(0, 2).join('-');
}
}
// eg. [2015,07,20]
this[SELECTED] = arrDate;
break;
}
case 'time': case 'hour': case 'minute': {
var arrTime = strInitValue.split(':');
// 时间
var strHour = arrTime[0];
var strMinute = arrTime[1];
// 这里的处理情况如下
// 1. 空值
// 2. 小时数不是数值
// 3. 小时数不在0~24之间
// 以上均认为时间是00:00
if (strInitValue == '' || !(strHour < 24 && strHour > 0)) {
strHour = '00';
strMinute = '00';
} else {
// 分钟无论如何都要是正常值
if (!(strMinute > 0 && strMinute < 60) || strType == 'hour') {
strMinute = '00';
} else if (strMinute.length == 1) {
strMinute = '0' + strMinute;
}
// 补0是必要的
if (strHour.length == 1) {
strHour = '0' + strHour;
}
}
eleInput.value = [strHour, strMinute].join(':');
this[SELECTED] = [strHour, strMinute];
break;
}
case 'date-range': case 'month-range': {
// 日期范围
var objBeginDate = new Date();
var objEndDate = new Date();
// 前后时间字符串
var arrRange = strInitValue.split(' ');
// 有如下一些情况:
// 1. 空,则选择范围就是今日
// 2. 只有一个时间,则选择范围只这个时间到今天这个范围
// 3. 其他就是正常的
if (strInitValue != '' && arrRange.length == 1) {
var someDate = arrRange[0].toDate();
if (someDate.getTime() > objBeginDate.getTime()) {
objEndDate = someDate;
} else {
objBeginDate = someDate;
}
} else {
objBeginDate = arrRange[0].toDate();
objEndDate = arrRange[arrRange.length - 1].toDate();
}
// 赋值
var arrBegin = objBeginDate.toArray();
var arrEnd = objEndDate.toArray();
if (strType == 'date-range') {
eleInput.value = arrBegin.join('-') + ' 至 ' + arrEnd.join('-');
} else {
eleInput.value = arrBegin.slice(0, 2).join('-') + ' 至 ' + arrEnd.slice(0, 2).join('-');
}
// 存储
this[SELECTED] = [arrBegin, arrEnd];
break;
}
}
// 容器元素的创建
if (!eleContainer) {
eleContainer = document.createElement('div');
eleContainer.classList.add(CL.date('container'));
// keyboard键盘无障碍访问需要
var strRandId = ('lulu_' + Math.random()).replace('0.', '');
eleContainer.setAttribute('id', strRandId);
eleContainer.classList.add('ESC');
eleInput.setAttribute('data-target', strRandId);
// 记录input的id
eleContainer.setAttribute('data-id', strId);
}
// 暴露的一些数据
this.element = {
container: eleContainer,
trigger: eleTrigger,
input: eleInput
};
this.params = {
type: strType,
// 最大小值的处理在各个种类的面板中执行
// 以便支持动态时间修改
// 例如,选择A时间,然后设置B时间的min值为A时间
// 这里先占个位
max: objParams.max,
min: objParams.min
};
// 回调方法
this.callback = {
show: objParams.onShow,
hide: objParams.onHide
};
// IE9+ 前后分页图标使用内联SVG, 以便支持retina屏幕
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>';
this.events();
if (!eleInput.data) {
eleInput.data = {};
}
eleInput.data.dateTime = this;
// 如果是静态模式,则实例对象暴露在容器元素上
if (eleContainer == eleTrigger) {
eleContainer.data = {
dateTime: this
};
// 同时显示
this.show();
}
return this;
};
/**
* 事件
* @return {[type]} [description]
*/
DateTime.prototype.events = function () {
var objElement = this.element;
// 具体元素们
var eleContainer = objElement.container;
var eleTrigger = objElement.trigger;
var eleInput = objElement.input;
// 点击容器的事件处理
eleContainer.addEventListener('click', function (event) {
// IE可能是文件节点
if (event.target.nodeType != 1 || !event.target.closest) {
return;
}
var eleClicked = event.target.closest('a, button');
if (!eleClicked) {
return;
}
// 各个分支中可能会用到的变量
var numYear = 0;
var numMonth = 0;
// var numDate = 0;
var numHour = 0;
var numDay = 0;
// 日期范围
var arrRange = [];
// 根据选中状态决定新的状态
var dataRange;
// 按钮类型
var strTypeButton = '';
// 各种事件
switch (eleContainer.getAttribute('data-type')) {
case 'date': {
// 日期选择主体
// 1. 前后月份选择
if (/prev|next/.test(eleClicked.className)) {
numMonth = eleClicked.getAttribute('data-month');
// 设置选择月份
// 这样可以获得目标月份对应的日期数据
// 就是下面this.getMonthDay做的事情
this[SELECTED][1] = numMonth * 1;
// 日期和月份要匹配,例如,不能出现4月31日
// arrMonthDay是一个数组,当前年份12个月每月最大的天数
var arrMonthDay = this.getMonthDay(this[SELECTED]);
// 切分月份
// 日期正常是不会变化的
// 但是类似31号这样的日子不是每个月都有的
// 因此,需要边界判断
// 下面这段逻辑就是做这个事情的
// 1. 当前月份最大多少天
var numDayMax = (function () {
if (numMonth - 1 < 0) {
return arrMonthDay[11];
} else if (numMonth > arrMonthDay.length) {
return arrMonthDay[0];
}
return arrMonthDay[numMonth - 1];
})();
// 2. 之前选择的天数
numDay = this[SELECTED][2];
// 之前记住的日期
var numDayOverflow = eleContainer.dataDayOverflow;
// 例如,我们超出日期是31日,如果月份可以满足31日,使用31日
if (numDayOverflow) {
this[SELECTED][2] = Math.min(numDayOverflow, numDayMax);
} else if (this[SELECTED][2] > numDayMax) {
this[SELECTED][2] = numDayMax;
// 这里是对体验的升级,
// 虽然下月份变成了30号,但是再回来时候,原来的31号变成了30号
// 不完美,我们需要处理下
// 通过一个变量记住,点击item项的时候,移除
// 且只在第一次的时候记住
// 因为28,29,30,31都可能出现,每次记忆会混乱
eleContainer.dataDayOverflow = numDay;
}
// 更新选择的月日数据
this[SELECTED] = this[SELECTED].join('-').toDate().toArray();
// 刷新
this.date();
// 如果在时间范围内
if (eleContainer.querySelector('.' + SELECTED + '[href]')) {
this.value();
}
} else if (/item/.test(eleClicked.className)) {
// 选择某日期啦
numDay = eleClicked.innerHTML;
// 含有非数字,认为是今天
if (/\D/.test(numDay)) {
// 今天
this[SELECTED] = new Date().toArray();
} else {
if (numDay < 10) {
numDay = '0' + numDay;
}
// 修改全局
this[SELECTED][2] = numDay;
}
// 赋值
this.value();
// 隐藏
this.hide();
delete eleContainer.dataDayOverflow;
} else if (eleClicked.getAttribute('data-type') == 'month') {
// 切换到年份选择
this.month();
}
break;
}
case 'date-range': {
// 区域选择
// 1. 前后月份选择
if (/prev|next/.test(eleClicked.className)) {
numMonth = eleClicked.getAttribute('data-month') * 1;
arrRange = eleContainer.dataDate || this[SELECTED][0];
// 跟其他面板不同,这里只刷新,点击确定再赋值
eleContainer.dataDate = new Date(arrRange[0], numMonth - 1, 1).toArray();
// 之前arrRange[2]存在跨多月风险,尤其31号的日子
// 刷新
this['date-range']();
} else if (/item/.test(eleClicked.className)) {
// 选择某日期
// 获得选中的年月日
numYear = eleClicked.getAttribute('data-year');
numMonth = eleClicked.getAttribute('data-month');
numDay = eleClicked.innerHTML;
// 位数不足补全
if (numMonth < 10) {
numMonth = '0' + numMonth;
}
if (numDay < 10) {
numDay = '0' + numDay;
}
// 根据选中状态决定新的状态
dataRange = this[SELECTED];
if (dataRange[0].join() == dataRange[1].join()) {
// 如果之前前后日期一样,说明只选中了一个日期
// 根据前后顺序改变其中一个日期
if (numYear + numMonth + numDay > dataRange[0].join('')) {
// 新时间靠后
dataRange[1] = [numYear, numMonth, numDay];
} else {
dataRange[0] = [numYear, numMonth, numDay];
}
} else {
// 如果前后时间不一样,说明现在有范围
// 则取消范围,变成单选
dataRange = [[numYear, numMonth, numDay], [numYear, numMonth, numDay]];
}
this[SELECTED] = dataRange;
this['date-range']();
} else if (/button/.test(eleClicked.className)) {
strTypeButton = eleClicked.getAttribute('data-type');
if (strTypeButton == 'primary') {
// 点击确定按钮
// 赋值
this.value();
// 修改存储值
this.dataRangeSelected = this[SELECTED];
// 关闭浮层
this.hide();
} else if (strTypeButton == 'normal') {
// 重置选中值
if (this.dataRangeSelected) {
this[SELECTED] = this.dataRangeSelected;
}
// 关闭浮层
this.hide();
}
}
break;
}
case 'month-range': {
// 区域选择
// 1. 前后年份选择
if (/prev|next/.test(eleClicked.className)) {
numYear = eleClicked.getAttribute('data-year') * 1;
arrRange = eleContainer.dataDate || this[SELECTED][0];
// 跟其他面板不同,这里只刷新,点击确定再赋值
eleContainer.dataDate = new Date(numYear, arrRange[1], 1).toArray();
// 刷新
this['month-range']();
} else if (/item/.test(eleClicked.className)) {
// 选择某日期
// 获得选中的年月日
numYear = eleClicked.getAttribute('data-year');
numMonth = eleClicked.getAttribute('data-value');
numDay = '01';
// 根据选中状态决定新的状态
dataRange = this[SELECTED];
if (dataRange[0].join() == dataRange[1].join()) {
// 如果之前前后日期一样,说明只选中了一个日期
// 根据前后顺序改变其中一个日期
if (numYear + numMonth + numDay > dataRange[0].join('')) {
// 新时间靠后
dataRange[1] = [numYear, numMonth, numDay];
} else {
dataRange[0] = [numYear, numMonth, numDay];
}
} else {
// 如果前后时间不一样,说明现在有范围
// 则取消范围,变成单选
dataRange = [[numYear, numMonth, numDay], [numYear, numMonth, numDay]];
}
this[SELECTED] = dataRange;
this['month-range']();
} else if (/button/.test(eleClicked.className)) {
strTypeButton = eleClicked.getAttribute('data-type');
if (strTypeButton == 'primary') {
// 点击确定按钮
// 赋值
this.value();
// 修改存储值
this.dataRangeSelected = this[SELECTED];
// 关闭浮层
this.hide();
} else if (strTypeButton == 'normal') {
// 重置选中值
if (this.dataRangeSelected) {
this[SELECTED] = this.dataRangeSelected;
}
// 关闭浮层
this.hide();
}
}
break;
}
case 'month': {
// 选择月份,可能从年份,也可能从日期过来
// 1. 前后年份
if (/prev|next/.test(eleClicked.className)) {
numYear = eleClicked.getAttribute('data-year');
// 修改当前选中的年份数
this[SELECTED][0] = numYear * 1;
// 刷新
this.month();
// 文本框赋值
// 如果在区域内状态
if (eleContainer.querySelector('.' + SELECTED + '[href]')) {
this.value();
}
} else if (/item/.test(eleClicked.className)) {
// value实际上是月份两位数值
var value = eleClicked.getAttribute('data-value');
if (value) {
this[SELECTED][1] = value;
} else {
// 今月,只改变年月为今年和今月
var arrToday = new Date().toArray();
this[SELECTED][0] = arrToday[0];
this[SELECTED][1] = arrToday[1];
}
// 日是否匹配月的合法性判断
var day = this[SELECTED][2];
var arrMonthDay = this.getMonthDay(this[SELECTED]);
// 如果日超出当月限制,使用本月最后一天作为日期
if (day > arrMonthDay[this[SELECTED][1] - 1]) {
this[SELECTED][2] = arrMonthDay[this[SELECTED][1] - 1];
}
// 赋值
this.value();
// 根据是否是月份输入框,决定是面板切换,还是关闭
if (this.params.type == 'month') {
// 隐藏
this.hide();
} else {
this.date();
}
} else if (eleClicked.getAttribute('data-type') == 'year') {
// 切换到年份选择
this.year();
}
break;
}
case 'year': {
// 选择年份,可能从月份过来,也可能直接打开
// 1. 前后12年翻页
if (/prev|next/.test(eleClicked.className)) {
numYear = eleClicked.getAttribute('data-year');
// 修改当前选中的年份数
this[SELECTED][0] = numYear * 1;
// 刷新
this.year();
// 文本框赋值
// 如果在区域内状态
if (eleContainer.querySelector('.' + SELECTED + '[href]')) {
this.value();
}
} else if (/item/.test(eleClicked.className)) {
if (eleClicked.innerHTML == '今年') {
this[SELECTED][0] = new Date().getFullYear();
} else {
this[SELECTED][0] = eleClicked.innerHTML * 1;
}
// 赋值
this.value();
// 如果是年份选择输入框
if (this.params.type == 'year') {
// 隐藏
this.hide();
} else {
// 回到月份面板
this.month();
}
}
break;
}
case 'minute': {
// 选择分钟,可以是minute类型,或者time类型, 但不包括hour类型
// 1. 前后翻页
if (/prev|next/.test(eleClicked.className)) {
numHour = eleClicked.getAttribute('data-hour');
if (numHour.length == 1) {
numHour = '0' + numHour;
}
// 修改当前选中的小时数
this[SELECTED][0] = numHour;
// 刷新
this.minute();
// 文本框赋值
// 如果在区域内状态
if (eleContainer.querySelector('.' + SELECTED + '[href]')) {
this.value();
}
} else if (/item/.test(eleClicked.className)) {
// 确定选择时间
this[SELECTED] = eleClicked.innerHTML.split(':');
this.value();
this.hide();
} else if (eleClicked.getAttribute('data-type') == 'hour') {
// 切换到小时选择
this.hour();
}
break;
}
case 'hour': {
if (/item/.test(eleClicked.className)) {
// 修改选中的小时
this[SELECTED][0] = eleClicked.innerHTML.split(':')[0];
// 赋值
this.value();
// 如果是从分钟模式切换过来,则切换回去,否则,直接隐藏
if (this.params.type == 'hour') {
this.hide();
} else {
this.minute();
}
}
break;
}
}
}.bind(this));
// 显隐控制
eleTrigger.addEventListener('click', function (event) {
event.preventDefault();
// 显隐控制
if (!this.display) {
this.show();
} else {
this.hide();
}
}.bind(this));
// 输入框元素行为
eleInput.addEventListener('keydown', function (event) {
if (event.keyCode == 13) {
event.preventDefault();
this.click();
}
});
// 时间范围选择点击页面空白区域不会隐藏
document.addEventListener('mouseup', function (event) {
// 点击页面空白区域,隐藏
var eleTarget = event.target;
if (eleTarget && eleTrigger.contains(eleTarget) == false && eleContainer.contains(eleTarget) == false) {
this.hide();
}
}.bind(this));
};
/**
* 输入框的值根据日期类型进行格式化
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.format = function () {
// 根据当前value值修改实例对象缓存的选中值
// 特殊情况一般不处理
var strType = this.params.type;
var eleInput = this.element.input;
// 此时输入框初始值
var strInitValue = eleInput && eleInput.value.trim();
if (!strInitValue) {
return this;
}
switch (strType) {
case 'date': case 'year': case 'month': {
// 日期
var objInitDate = strInitValue.toDate();
var arrDate = objInitDate.toArray();
// eg. [2015,07,20]
this[SELECTED] = arrDate;
break;
}
case 'time': case 'hour': case 'minute': {
var arrTime = strInitValue.split(':');
// 时间
var numHour = arrTime[0];
var numMinute = arrTime[1];
// 补0
if (arrTime.length == 2) {
// 分钟无论如何都要是正常值
if (!(numMinute > 0 && numMinute < 60) || strType == 'hour') {
numMinute = '00';
} else if (numMinute.length == 1) {
numMinute = '0' + numMinute;
}
// 补0是必要的
if (numHour.length == 1) {
numHour = '0' + numHour;
}
eleInput.value = [numHour, numMinute].join(':');
this[SELECTED] = [numHour, numMinute];
}
break;
}
case 'date-range': case 'month-range': {
// 日期范围
var objBeginDate = new Date();
var objEndDate = new Date();
// 前后时间字符串
var arrRange = strInitValue.split(' ');
if (arrRange.length == 3) {
objBeginDate = arrRange[0].toDate();
objEndDate = arrRange[arrRange.length - 1].toDate();
// 存储
this[SELECTED] = [objBeginDate.toArray(), objEndDate.toArray()];
}
break;
}
}
return this;
};
/**
* 赋值
* @return {Object} 返回当前输入框的值
*/
DateTime.prototype.value = function () {
var eleInput = this.element.input;
var arrSelected = this[SELECTED];
var strValue = eleInput.value;
switch (this.params.type) {
case 'date': {
eleInput.value = arrSelected.join('-');
break;
}
case 'month': {
eleInput.value = arrSelected.slice(0, 2).join('-');
break;
}
case 'year': {
eleInput.value = arrSelected[0];
break;
}
case 'date-range': {
eleInput.value = arrSelected[0].join('-') + ' 至 ' + arrSelected[1].join('-');
break;
}
case 'month-range': {
eleInput.value = arrSelected[0].slice(0, 2).join('-') + ' 至 ' + arrSelected[1].slice(0, 2).join('-');
break;
}
case 'time': case 'minute': {
eleInput.value = arrSelected.join(':');
break;
}
case 'hour': {
eleInput.value = arrSelected[0] + ':00';
break;
}
}
if (eleInput.value != strValue) {
eleInput.dispatchEvent(new CustomEvent('change', {
'bubbles': true
}));
}
return eleInput.value;
};
/**
* 返回日历HTML字符串等数据的私有方法
// 因date和range浮层主结构类似,因此这里公用下
* @param {Array} arrDate 格式化为数组的日期
* @return {Object} 返回包含需要的数据的对象,生成的HTML字符内容以及最大最小月份等
*/
Object.defineProperty(DateTime.prototype, 'getCalendarData', {
value: function (arrDate) {
var strHtml = '';
// 根据当前日期数据返回
// eg. [2015,'02', 23]
// 文本框元素比较常用,使用局部变量,一是节约文件大小,二是性能更好!
var eleInput = this.element.input;
// 类型
var strType = this.params.type;
// 最大日期和最小日期
var numMin = eleInput.getAttribute('min') || this.params.min;
var numMax = eleInput.getAttribute('max') || this.params.max;
// 这里使用日期对象(时间戳)做比对
var arrMapMinMax = [numMin, numMax].map(function (minMax, index) {
if (typeof minMax == 'string' && /^\d{8}$/.test(minMax.replace(regDate, '')) == true) {
// 这里对字符串进行比较简单的合法性判别
// 自己人使用不会乱来的
minMax = minMax.toDate();
} else if (typeof minMax != 'object' || !minMax.getTime) {
minMax = index ? new Date(9999, 0, 1) : new Date(0, 0, 1);
}
return minMax;
});
numMin = arrMapMinMax[0];
numMax = arrMapMinMax[1];
var arrChinese = ['日', '一', '二', '三', '四', '五', '六'];
var arrMonthDay = this.getMonthDay(arrDate);
// 目前日期对象
var currentDate = arrDate.join('-').toDate();
// 3 星期几七大罪
strHtml = strHtml + '<div class="' + CL.day('x') + '">' + (function () {
var strHtmlDay = '';
arrChinese.forEach(function (strChineseDay, indexDay) {
strHtmlDay = strHtmlDay + '<span class="' + CL.day('item') + ' col' + indexDay + '">' + strChineseDay + '</span>';
});
return strHtmlDay;
})() + '</div>';
// 4. 日期详细
// 4.1 首先算出今月1号是星期几
var objNewDate = arrDate.join('-').toDate();
var numDayFirst = 0;
// 设置为1号
objNewDate.setDate(1);
if (objNewDate.getDate() == 2) {
objNewDate.setDate(0);
}
// 每月的第一天是星期几
numDayFirst = objNewDate.getDay();
// 上个月是几月份
var numLastMonth = objNewDate.getMonth() - 1;
if (numLastMonth < 0) {
numLastMonth = 11;
}
var strHtmlData = 'data-year="' + arrDate[0] + '" data-month="' + (objNewDate.getMonth() + 1) + '"';
var strHtmlYearMonthData = 'data-date=';
var strHtmlFullData = '';
strHtml = strHtml + '<div class="' + CL.date('body') + '">' + (function () {
var strHtmlDate = '';
var strClass = '';
// 列表生成
for (var tr = 0; tr < 6; tr++) {
strHtmlDate = strHtmlDate + '<div class="' + CL.date('tr') + '">';
// 日期遍历
for (var td = 0; td < 7; td++) {
// 类名
strClass = CL.date('item') + ' col' + td;
// 今天
var numYearNow = arrDate[0];
var numMonthNow = objNewDate.getMonth() + 1;
var numDayNow;
var objDateNow;
// 由于range选择和date选择UI上有比较大大差异
// 为了可读性以及后期维护
// 这里就不耦合在一起,而是分开处理
if (strType == 'date') {
// 第一行上个月一些日期补全
if (tr == 0 && td < numDayFirst) {
// 当前日子
numDayNow = arrMonthDay[numLastMonth] - numDayFirst + td + 1;
// 当前日期
objDateNow = new Date(numYearNow, numLastMonth, numDayNow);
// 完整data-date属性及其值
strHtmlFullData = strHtmlYearMonthData + objDateNow.toArray().join('-');
// HTML拼接
strHtmlDate = strHtmlDate + '<span class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</span>';
} else {
// 当前日子
numDayNow = tr * 7 + td - numDayFirst + 1;
// 如果没有超过这个月末
if (numDayNow <= arrMonthDay[objNewDate.getMonth()]) {
// 这个日子对应的时间对象
objDateNow = new Date(numYearNow, objNewDate.getMonth(), numDayNow);
// 完整data-date属性及其值
strHtmlFullData = strHtmlYearMonthData + objDateNow.toArray().join('-');
// 如果日子匹配
if (currentDate.getDate() == numDayNow) {
strClass = strClass + ' ' + SELECTED;
}
// 如果在日期范围内
// 直接使用时间对象 Date 类作比较
if (objDateNow >= numMin && objDateNow <= numMax) {
strHtmlDate = strHtmlDate + '<a href="javascript:;" ' + strHtmlData + ' class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</a>';
} else {
strHtmlDate = strHtmlDate + '<span class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</span>';
}
} else {
numDayNow = numDayNow - arrMonthDay[objNewDate.getMonth()];
// 更新strHtmlFullData
strHtmlFullData = strHtmlYearMonthData + new Date(numYearNow, numMonthNow, numDayNow).toArray().join('-');
// 日期字符拼接
strHtmlDate = strHtmlDate + '<span class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</span>';
}
}
} else if (strType == 'date-range') {
// 非当前月部分使用空格补全
if (tr == 0 && td < numDayFirst) {
strHtmlDate = strHtmlDate + '<span class="' + strClass + '"></span>';
} else {
numDayNow = tr * 7 + td - numDayFirst + 1;
// 如果没有超过这个月末
if (numDayNow <= arrMonthDay[objNewDate.getMonth()]) {
// 这个日子对应的时间对象
objDateNow = new Date(numYearNow, objNewDate.getMonth(), numDayNow);
// 完整data-date属性及其值
strHtmlFullData = strHtmlYearMonthData + objDateNow.toArray().join('-');
// range选择的匹配规则如下:
// 1. 获得已经选中到时间范围
// 2. 起始时间和结束时间是方框表示
// 3. 之间的时间是选中表示
var dateBegin = this[SELECTED][0].join('-').toDate();
var dateEnd = this[SELECTED][1].join('-').toDate();
// 各个时间的时间戳
var timeNow = objDateNow.getTime();
var timeBegin = dateBegin.getTime();
var timeEnd = dateEnd.getTime();
if (timeNow >= timeBegin && timeNow <= timeEnd) {
// 在时间范围之内
strClass = strClass + ' ' + SELECTED;
// 跟开始时间一样
if (timeNow == timeBegin) {
strClass = strClass + ' ' + CL.date('begin');
}
// 跟结束时间一样
if (timeNow == timeEnd) {
strClass = strClass + ' ' + CL.date('end');
}
// 今月的第一天还是最后一天
if (numDayNow == 1) {
strClass = strClass + ' ' + CL.date('first');
} else if (numDayNow == arrMonthDay[objNewDate.getMonth()]) {
strClass = strClass + ' ' + CL.date('last');
}
}
// 如果在日期范围内
// 直接使用时间对象 Date 类作比较
if (objDateNow >= numMin && objDateNow <= numMax) {
strHtmlDate = strHtmlDate + '<a href="javascript:;" ' + strHtmlData + ' class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</a>';
} else {
strHtmlDate = strHtmlDate + '<span class="' + strClass + '" ' + strHtmlFullData + '>' + numDayNow + '</span>';
}
} else {
strHtmlDate = strHtmlDate + '<span class="' + strClass + '"></span>';
}
}
}
}
strHtmlDate += '</div>';
}
return strHtmlDate;
}).call(this) + '</div>';
return {
monthDay: arrMonthDay,
html: strHtml,
min: numMin,
max: numMax
};
}
});
/**
* 月份组装
* @param {Array} arrDate 数组化的日期表示值
* @return {Object} 返回的是后续方法需要的数据的纯对象,包括组装的HTML字符数据,月份最大和最小值。
*/
Object.defineProperty(DateTime.prototype, 'getMonthData', {
value: function (arrDate) {
// 文本框元素
var eleInput = this.element.input;
// 最大月份和最小月份
var numMin = eleInput.getAttribute('min') || this.params.min;
var numMax = eleInput.getAttribute('max') || this.params.max;
// 这里使用年月组合做比对
var arrMapMinMax = [numMin, numMax].map(function (minMax, index) {
if (typeof minMax == 'object' && minMax.getTime) {
minMax = minMax.toArray().slice(0, 2).join('');
} else if (typeof minMax == 'string' && /\D/.test(minMax.replace(regDate, '')) == false) {
// 这里对字符串进行比较简单的合法性判别
// 自己人使用不会乱来的
minMax = minMax.replace(regDate, '').slice(0, 6);
} else {
minMax = index ? '999912' : '000000';
}
return minMax;
});
numMin = arrMapMinMax[0];
numMax = arrMapMinMax[1];
var arrChinese = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二'];
// 年份
var numYear = arrDate[0] * 1;
// 类型
var strType = this.params.type;
var strHtml = '<div class="' + CL.month('body') + '">' + (function () {
var strHtmlDate = '';
var strClass = '';
var strMonth = '';
for (var i = 1; i <= 12; i += 1) {
// 合法格式字符串
if (i < 10) {
strMonth = '0' + i;
} else {
strMonth = i + '';
}
// 基本类名
strClass = CL.date('item');
if (strType == 'month') {
if (i == arrDate[1]) {
// 选中态的类名
strClass = strClass + ' ' + SELECTED;
}
} else if (strType == '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 = numYear + strMonth;
if (strNow >= strBegin && strNow <= strEnd) {
strClass = strClass + ' ' + SELECTED;
}
}
// 是否在范围以内
if (numYear + strMonth >= numMin && numYear + strMonth <= numMax) {
strHtmlDate = strHtmlDate + '<a href="javascript:" class="' + strClass + '" data-year="' + numYear + '" data-value="' + strMonth + '">' + arrChinese[i - 1] + '月</a>';
} else {
strHtmlDate = strHtmlDate + '<span class="' + strClass + '" data-value="' + strMonth + '">' + arrChinese[i - 1] + '月</span>';
}
}
return strHtmlDate;
}).call(this) +
'</div>';
return {
html: strHtml,
min: numMin,
max: numMax
};
}
});
/**
* 当前日期下这一年每月最大的日期数目
* @param {Array} date 格式化为数组的日期
* @return {Array} 返回这一年每月最大的日期数目
*/
Object.defineProperty(DateTime.prototype, 'getMonthDay', {
value: function (date) {
var arrDate = date;
// 如果不是数组
if (typeof date != 'object' && !date.map) {
arrDate = date.toArray();
}
var arrMonthDay = [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)) {
arrMonthDay[1] = 29;
}
return arrMonthDay;
}
});
/**
* 上个月同日期
* @param {Array} date 格式化为数组的日期
* @return {Object} 返回Date类型对象
*/
Object.defineProperty(DateTime.prototype, 'getDatePrevMonth', {
value: function (date) {
// add on 2016-03-31
// 原先的加减月份的跨月计算是有些问题的
// 因为例如31号的下一月,系统会自动变成下下个月的1号
var arrDate = date;
// 如果不是数组
if (typeof date != 'object' && !date.map) {
// 日期数组化
arrDate = date.toArray();
}
var numMonth = arrDate[1] * 1;
var arrMonthDay = this.getMonthDay(arrDate);
if (numMonth == 1) {
// 上个月是前一年的12月
// 12月有31号,无压力
return [arrDate[0] - 1, 12, arrDate[2]].join('-').toDate();
}
// 其他月份不跨年
// 如果上一个月的最后1天小于本月的最后1天
// 嘿嘿嘿嘿
// 年不变,月减一,日期下个月
if (arrMonthDay[numMonth - 2] < arrDate[2]) {
return [arrDate[0], numMonth - 1, arrMonthDay[numMonth - 2]].join('-').toDate();
}
// 年不变,月减一,日期不变
return [arrDate[0], numMonth - 1, arrDate[2]].join('-').toDate();
}
});
/**
* 下个月同日期
* @param {Array} date 格式化为数组的日期
* @return {Object} 返回Date类型对象
*/
Object.defineProperty(DateTime.prototype, 'getDateNextMonth', {
value: function (date) {
var arrDate = date;
// 如果不是数组
if (typeof date != 'object' && !date.map) {
// 日期数组化
arrDate = date.toArray();
}
var numMonth = arrDate[1] * 1;
var arrMonthDay = this.getMonthDay(arrDate);
if (numMonth == 12) {
// 下个月跨年了
// 1月份也有31天,无压力
return [arrDate[0] + 1, 1, arrDate[2]].join('-').toDate();
}
// 其他月份不跨年
// 如果下个月最后1天小于本月最后1天,例如,3月31日
if (arrMonthDay[numMonth] < arrDate[2]) {
return [arrDate[0], numMonth + 1, arrMonthDay[numMonth]].join('-').toDate();
}
// 其他时候正常
return [arrDate[0], numMonth + 1, arrDate[2]].join('-').toDate();
}
});
/**
* 选择日期
* @return {Object} 返回当前实例对象
*/
DateTime.prototype.date = function () {
var eleContainer = this.element.container;
var arrDate = this[SELECTED];
// 上一个月
var numPrevMonth = arrDate[1] - 1;
// 下一个月
var numNextMonth = arrDate[1] * 1 + 1;
var objCalender = this.getCalendarData(arrDate);
// 选择日期的完整HTML代码
// 1. 日期专属类名容器
var strHtml = '<div class="' + CL.date('x') + '">';
// 2. 头部月份切换
strHtml = strHtml + '<div class="' + CL.date('head') + '">';
// 根据前后月份是否在范围之外,决定使用的标签类型
// span标签则是禁用态,本组件全部都是如此
// 2.1 上个月
// datePrevMonth指上个月日期
var datePrevMonth = this.getDatePrevMonth(arrDate);
// numPrevMonth指上个月
var numPrevMonthGet = datePrevMonth.getMonth();
var numPrevYearGet = datePrevMonth.getFullYear();
// add on 2015-12-01
// 原来的判断逻辑有问题
// 只要上个月份的最大日期比最小限制大就可以了
if (new Date(numPrevYearGet, numPrevMonthGet, objCalender.monthDay[numPrevMonthGet]) >= objCalender.min && datePrevMonth <= objCalender.max) {
strHtml = strHtml + '<a href="javascript:" class="' + CL.date('prev') + '" data-month="' + numPrevMonth + '" role="button" aria-label="上一月">' + this.svg + '</a>';
} else {
strHtml = strHtml + '<span class="' + CL.date('prev') + '" aria-label="上一月">' + this.svg + '</span>';
}
// 2.2 下个月
var objDateNextMonth = this.getDateNextMonth(arrDate);
var numNextMonthGet = objDateNextMonth.getMonth();
var numNextYearGet = objDateNextMonth.getFullYear();
//if (objDateNextMonth >= objCalender.min && objDateNextMonth <= objCalender.max) {
// add on 2015-12-01
// 经反馈,如果当前日期是30号,但是最大日期是下月1号,则下个月不能进入
// 这是有问题的
// 原因是此时的objDateNextMonth > objCalender.max
// 因此判断应该从月初
if (objDateNextMonth >= objCalender.min && new Date(numNextYearGet, numNextMonthGet, 1) <= objCalender.max) {
strHtml = strHtml + '<a href="javascript:" class="' + CL.date('next') + '" data-month="' + numNextMonth + '" role="button" aria-label="下一月">' + this.s