最近研究下JS日期级联效果 感觉还不错,然后看了下kissy也正好有这么一个组件,也看了下源码,写的还不错,通过google最早是在2011年 淘宝的虎牙(花名)用原审JS写了一个(貌似据说是从YUI那边重构下的) 具体的可以看他的 博客园 , 感觉kissy组件源码 思路也是和YUI类似 所以我今天的基本思路也和他们的一样 只是通过自己分析下及用自己的方式包装下。
基本原理
1.传参中有 '年份下拉框dom节点', '月份下拉框dom节点', '天数下拉框dom节点', "开始日期","结束日期","默认日期"配置项
1.如果开始传参日期为空 那么默认是从"1900-01-01"开始
2.如果"结束日期为空" 那么默认结束日期为当前的时间。
3. 如果默认日期为空 那么默认日期默认为当前的时间。
2. 月份对应的天数可以直接写死 如:_dayInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] ; 分别为1月份到12月份的各个月份的默认天数,当然还有2月份闰年29天的情况 待会在代码中会有判断的。
3. 分别渲染出年份区间,月份区间,及相应的天数。(如果有默认的日期的话 且默认日期大于或者等于开始日期 且小于或者等于结束日期的话) 那么页面加载的时候 显示默认日期。
4. 绑定change事件 当切换到不同年份的时候 月份和天数也要分别渲染出来。
基本配置项如下:
对外提供的方法
1. getDate() 返回当前时间,格式为yyyy-mm-dd
2. getYear() 返回当前的年份
3. getMonth() 返回当前的月份
4. getDay() 返回当前月份中的天数.
JSFiddle demo链接如下:
下面代码分析如下:
1. 初始化调用init方法:分别获取开始时间 结束时间 默认时间的 "年,月,天"。如下代码:
// 开始时间可选 如果为空的话 那么默认开始时间是1900-01-01 if(_config.dateStart != '') { this.startDate = { y: new Date(_config.dateStart).getFullYear(), m: new Date(_config.dateStart).getMonth() + 1, d: new Date(_config.dateStart).getDate() }; }else { var dateStart = '1900/01/01'; this.startDate = { y: new Date(dateStart).getFullYear(), m: new Date(dateStart).getMonth() + 1, d: new Date(dateStart).getDate() }; } // dateEnd 默认为空 如果没有传入的话 就取当前的时间 if(_config.dateEnd == '') { this.endDate = { y: new Date().getFullYear(), m: new Date().getMonth() + 1, d: new Date().getDate() }; }else { this.endDate = { y: new Date(_config.dateEnd).getFullYear(), m: new Date(_config.dateEnd).getMonth() + 1, d: new Date(_config.dateEnd).getDate() }; } // 默认时间可选 如果默认时间为空的话 那么就取当前的时间 if(_config.dateDefault != '') { this.defaultDate = { y: new Date(_config.dateDefault).getFullYear(), m: new Date(_config.dateDefault).getMonth() + 1, d: new Date(_config.dateDefault).getDate() }; }else { this.defaultDate = { y: new Date().getFullYear(), m: new Date().getMonth() + 1, d: new Date().getDate() }; } // 判断时间是否合理 if((Date.parse(self._changeFormat(_config.dateStart)) > Date.parse(self._changeFormat(_config.dateEnd))) || (Date.parse(self._changeFormat(_config.dateDefault)) > Date.parse(self._changeFormat(_config.dateEnd)))){ return; }
2. 渲染下拉框的年份:调用 y = self._renderYear();这个方法。
1. 获取年份的区间范围,获取方法就是:获取开始时间的年份 和 结束时的年份 如下代码:
/* * 获取年份的范围 最小-最大 * @method _getYearRange * @return {min,max} */ _getYearRange: function(){ var self = this, _config = self.config; return { min: self.startDate.y, max: self.endDate.y } },
2. 接着渲染年份,从最近的年份开始渲染,如果有默认的年份 且 满足条件的话 那么默认的年份显示出来。如下代码:
/* * 渲染年份下拉框 * @method _renderYear * private */ _renderYear: function(){ var self = this, _config = self.config, _cache = self.cache; var nodeyear = $(_config.nodeYear)[0], y = self.defaultDate.y, range, option; if(nodeyear) { range = self._getYearRange(); for(var i = range.max; i >= range.min; i--) { option = new Option(i,i); // 如果有默认年份的话 if(i == y) { option.selected = true; } // 兼容所有浏览器 插入到最后 nodeyear.add(option,undefined); } } $(nodeyear).attr('year',y); return y; },
3. 接着渲染月份 调用这个方法 y参数就是刚刚返回的年份 m = self._renderMonth(y);
1. 同理 渲染月份也要获取月份的范围 默认都是从1月份到12月份 但是也有列外。比如如下2个判断。
/* * 获取月份的范围 * @method _getMonthRange * @param {y} Number */ _getMonthRange: function(y){ var self = this, _config = self.config; var startDate = self.startDate, endDate = self.endDate, min = 1, max = 12; /* * 如果默认年份等于开始年份的话 那么月份最小取得是开始的月份 * 因为如果开始是1900-05-01 如果默认的是 1900-03-02 那么最小月份肯定取得是5 * 因为默认时间不可能小于开始时间 */ if(y == startDate.y) { // 开始年份 min = startDate.m; } /* * 同理 如果默认年份等于2014-04-01 那么取得是当前的年份(endDate未传的情况下) * 那么最大的肯定取得是当前年份的 月份 不可能取的是4 因为只渲染出当前月份出来 * 后面的月份没有渲染出来 */ if(y == endDate.y) { max = endDate.m; } return { min: min, max: max } },
2. 知道月份的范围后 然后根据上面的年份渲染相应的月份:代码如下:
/* * 根据年份 渲染所有的月份 * @method _renderMonth * @param {y} 年份 */ _renderMonth: function(y){ var self = this, _config = self.config; var nodeMonth = $(_config.nodeMonth)[0], m = $(nodeMonth).attr('month') || self.defaultDate.m, range, option, t = false; if(nodeMonth) { range = self._getMonthRange(y); nodeMonth.innerHTML = ''; for(var i = range.min; i <= range.max; i++) { option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的月份的话 if(i == m) { option.selected = true; m = i; t = true; } // 兼容所有浏览器 插入到最后 nodeMonth.add(option,undefined); } if(!t) { m = range.min; } } $(nodeMonth).attr('month',m); return m; },
上面的代码 用了这句判断 m = $(nodeMonth).attr('month') || self.defaultDate.m, 默认情况下 也就是说页面一加载的时候 可以获取默认的月份,但是当我触发change事件后 我取的月份 是从m = $(nodeMonth).attr('month') 这个里面取得。上面代码 nodeMonth.innerHTML = ''; 也是为了change时候 请清空掉 然后重新生成的。
4. 渲染天数 通过这个方法: self._renderDay(y,m);
1. 渲染天数 同理也要获得相应的天数。调用_getDayRange方法。此方法中有判断是闰年的情况的。如下代码:
/* * 获得天数的范围 * @method _getDayRange * @param {y,m} {number,number} */ _getDayRange: function(y,m){ var self = this, _config = self.config, _cache = self.cache; var startDate = self.startDate, endDate = self.endDate, min = 1, max; if(m) { if(m == 2) { max = self._isLeapYear(y) ? 29 : 28; }else { max = _cache._dayInMonth[m-1]; } // 如果年月份都等于开始日期的话 那么min也等于开始日 if(y == startDate.y && m == startDate.m) { min = startDate.d; } // 如果年月份都等于结束日期的话 那么max也等于结束日 if(y == endDate.y && m == endDate.m) { max = endDate.d; } } return { min: min, max: max } },
2.接着渲染天数的方法如下:
_renderDay: function(y,m) { var self = this, _config = self.config; var nodeDay = $(_config.nodeDay)[0], d = $(nodeDay).attr('day') || self.defaultDate.d, range, option, t = false; if(nodeDay) { range = self._getDayRange(y,m); nodeDay.innerHTML = ''; for(var i = range.min; i <= range.max; i++) { option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的天数的话 if(i == d) { option.selected = true; d = i; t = true; } // 兼容所有浏览器 插入到最后 nodeDay.add(option,undefined); } if(!t) { d = range.min; } } $(nodeDay).attr('day',d); return d; },
5 最后用绑定change事件 调用_bindEnv方法。如:
/* * 绑定所有事件 * @method _bindEnv * private */ _bindEnv:function(){ var self = this, _config = self.config, _cache = self.cache; //年份改变 $(_config.nodeYear).change(function(e){ var y = e.target.value, m = self._renderMonth(y); self._renderDay(y,m); $(_config.nodeYear).attr('year',y); }); //月份改变 $(_config.nodeMonth).change(function(e){ var m = e.target.value, y = $(_config.nodeYear).attr('year'); self._renderDay(y,m); $(_config.nodeMonth).attr('month',m); }); //日期改变 $(_config.nodeDay).change(function(e){ var d = e.target.value; $(_config.nodeDay).attr('day',d); }); },
HTML代码如下:
<label>出生日期: </label> <select id="year"> </select>年 <select id="month"> </select>月 <select id="day"> </select>日 <ul> <li><em>getDate</em> : <button id="testDate">日期</button><input id="textDate"/></li> <li><em>getYear</em> : <button id="testYear">年</button><input id="textYear"/></li> <li><em>getMonth</em> : <button id="testMonth">月</button><input id="textMonth"/></li> <li><em>getDay</em> : <button id="testDay">日</button><input id="textDay"/></li> </ul>
JS代码如下:
/** * JS日期级联组件 * @constructor DateCascade * @param {object} 可配置的对象 * @time 2014-1-13 * @author [email protected] */ function DateCascade(options) { this.config = { nodeYear : '#year', // 年份下拉框dom nodeMonth : '#month', // 月份下拉框dom nodeDay : '#day', // 日期下拉框dom dateStart : '', // 开始日期 dateEnd : '', // 结束日期(可选 默认为空就为当前时间) dateDefault : '' // 默认日期 }; this.cache = { _dayInMonth: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // 月份对应的天数 }; this.init(options); } DateCascade.prototype = { constructor: DateCascade, init: function(options) { this.config = $.extend(this.config,options || {}); var self = this, _config = self.config, _cache = self.cache; var y, m; /* 开始时间 和 截至时间 默认时间*/ // 开始时间可选 如果为空的话 那么默认开始时间是1900-01-01 if(_config.dateStart != '') { this.startDate = { y: new Date(_config.dateStart).getFullYear(), m: new Date(_config.dateStart).getMonth() + 1, d: new Date(_config.dateStart).getDate() }; }else { var dateStart = '1900/01/01'; this.startDate = { y: new Date(dateStart).getFullYear(), m: new Date(dateStart).getMonth() + 1, d: new Date(dateStart).getDate() }; } // dateEnd 默认为空 如果没有传入的话 就取当前的时间 if(_config.dateEnd == '') { this.endDate = { y: new Date().getFullYear(), m: new Date().getMonth() + 1, d: new Date().getDate() }; }else { this.endDate = { y: new Date(_config.dateEnd).getFullYear(), m: new Date(_config.dateEnd).getMonth() + 1, d: new Date(_config.dateEnd).getDate() }; } // 默认时间可选 如果默认时间为空的话 那么就取当前的时间 if(_config.dateDefault != '') { this.defaultDate = { y: new Date(_config.dateDefault).getFullYear(), m: new Date(_config.dateDefault).getMonth() + 1, d: new Date(_config.dateDefault).getDate() }; }else { this.defaultDate = { y: new Date().getFullYear(), m: new Date().getMonth() + 1, d: new Date().getDate() }; } // 判断时间是否合理 if((Date.parse(self._changeFormat(_config.dateStart)) > Date.parse(self._changeFormat(_config.dateEnd))) || (Date.parse(self._changeFormat(_config.dateDefault)) > Date.parse(self._changeFormat(_config.dateEnd)))){ return; } // 渲染年份 y = self._renderYear(); // 渲染月份 m = self._renderMonth(y); // 渲染天 self._renderDay(y,m); // 所有绑定事件 self._bindEnv(); }, /* * 渲染年份下拉框 * @method _renderYear * private */ _renderYear: function(){ var self = this, _config = self.config, _cache = self.cache; var nodeyear = $(_config.nodeYear)[0], y = self.defaultDate.y, range, option; if(nodeyear) { range = self._getYearRange(); for(var i = range.max; i >= range.min; i--) { option = new Option(i,i); // 如果有默认年份的话 if(i == y) { option.selected = true; } // 兼容所有浏览器 插入到最后 nodeyear.add(option,undefined); } } $(nodeyear).attr('year',y); return y; }, /* * 根据年份 渲染所有的月份 * @method _renderMonth * @param {y} 年份 */ _renderMonth: function(y){ var self = this, _config = self.config; var nodeMonth = $(_config.nodeMonth)[0], m = $(nodeMonth).attr('month') || self.defaultDate.m, range, option, t = false; if(nodeMonth) { range = self._getMonthRange(y); nodeMonth.innerHTML = ''; for(var i = range.min; i <= range.max; i++) { option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的月份的话 if(i == m) { option.selected = true; m = i; t = true; } // 兼容所有浏览器 插入到最后 nodeMonth.add(option,undefined); } if(!t) { m = range.min; } } $(nodeMonth).attr('month',m); return m; }, _renderDay: function(y,m) { var self = this, _config = self.config; var nodeDay = $(_config.nodeDay)[0], d = $(nodeDay).attr('day') || self.defaultDate.d, range, option, t = false; if(nodeDay) { range = self._getDayRange(y,m); nodeDay.innerHTML = ''; for(var i = range.min; i <= range.max; i++) { option = new Option(self.bitExpand(i),self.bitExpand(i)); // 如果有默认的天数的话 if(i == d) { option.selected = true; d = i; t = true; } // 兼容所有浏览器 插入到最后 nodeDay.add(option,undefined); } if(!t) { d = range.min; } } $(nodeDay).attr('day',d); return d; }, /* * 绑定所有事件 * @method _bindEnv * private */ _bindEnv:function(){ var self = this, _config = self.config, _cache = self.cache; //年份改变 $(_config.nodeYear).change(function(e){ var y = e.target.value, m = self._renderMonth(y); self._renderDay(y,m); $(_config.nodeYear).attr('year',y); }); //月份改变 $(_config.nodeMonth).change(function(e){ var m = e.target.value, y = $(_config.nodeYear).attr('year'); self._renderDay(y,m); $(_config.nodeMonth).attr('month',m); }); //日期改变 $(_config.nodeDay).change(function(e){ var d = e.target.value; $(_config.nodeDay).attr('day',d); }); }, /* * 获取年份的范围 最小-最大 * @method _getYearRange * @return {min,max} */ _getYearRange: function(){ var self = this, _config = self.config; return { min: self.startDate.y, max: self.endDate.y } }, /* * 获取月份的范围 * @method _getMonthRange * @param {y} Number */ _getMonthRange: function(y){ var self = this, _config = self.config; var startDate = self.startDate, endDate = self.endDate, min = 1, max = 12; /* * 如果默认年份等于开始年份的话 那么月份最小取得是开始的月份 * 因为如果开始是1900-05-01 如果默认的是 1900-03-02 那么最小月份肯定取得是5 * 因为默认时间不可能小于开始时间 */ if(y == startDate.y) { // 开始年份 min = startDate.m; } /* * 同理 如果默认年份等于2014-04-01 那么取得是当前的年份(endDate未传的情况下) * 那么最大的肯定取得是当前年份的 月份 不可能取的是4 因为只渲染出当前月份出来 * 后面的月份没有渲染出来 */ if(y == endDate.y) { max = endDate.m; } return { min: min, max: max } }, /* * 获得天数的范围 * @method _getDayRange * @param {y,m} {number,number} */ _getDayRange: function(y,m){ var self = this, _config = self.config, _cache = self.cache; var startDate = self.startDate, endDate = self.endDate, min = 1, max; if(m) { if(m == 2) { max = self._isLeapYear(y) ? 29 : 28; }else { max = _cache._dayInMonth[m-1]; } // 如果年月份都等于开始日期的话 那么min也等于开始日 if(y == startDate.y && m == startDate.m) { min = startDate.d; } // 如果年月份都等于结束日期的话 那么max也等于结束日 if(y == endDate.y && m == endDate.m) { max = endDate.d; } } return { min: min, max: max } }, /* * 判断是否是闰年 */ _isLeapYear: function(y){ return (y % 4 === 0 && y % 100 !== 0) || (y % 400 === 0); }, /** * 是否是Date格式 * @method _isDate * @param {Date} d * @private * @return {Boolean} */ _isDate: function(d){ return Object.prototype.toString.call(d) === '[object Date]' && d.toString() !== 'Invalid Date' && !isNaN(d); }, /* * 小于10的数字加零 * @method bitExpand */ bitExpand: function(num) { var num = num * 1; if(/\d/.test(num)) { if(num < 10) { return '0' + num; }else { return num; } } }, /* * 判断开始日期 默认日期 结束日期的格式 */ _changeFormat: function(date) { return date.replace(/'-'/g,'/'); }, /* * 获取日期 */ getDate: function(){ var self = this, _config = self.config; var year = $(_config.nodeYear).attr('year'), month = $(_config.nodeMonth).attr('month'), day = $(_config.nodeDay).attr('day'); return (year + '-' + self.bitExpand(month) + '-' + self.bitExpand(day)); }, /* * 获取年份 */ getYear: function(){ var self = this, _config = self.config; var year = $(_config.nodeYear).attr('year'); return year; }, /* * 获取月份 */ getMonth: function(){ var self = this, _config = self.config; var month = $(_config.nodeMonth).attr('month'); return month; }, /* * 获取天数 */ getDay: function(){ var self = this, _config = self.config; var day = $(_config.nodeDay).attr('day'); return day; } }
初始化方式如下:
// 初始化 $(function(){ var date = new DateCascade({}); $('#testDate').click(function(e){ $('#textDate').val(date.getDate()); }); $('#testYear').click(function(e){ $('#textYear').val(date.bitExpand(date.getYear())); }); $('#testMonth').click(function(e){ $('#textMonth').val(date.bitExpand(date.getMonth())); }); $('#testDay').click(function(e){ $('#textDay').val(date.bitExpand(date.getDay())); }); });