最近研究下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 879083421@qq.com */ 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())); }); });