最近研究下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())); }); });