首發個人博客 - https://blog.cdswyda.com/post/2017121010css
日曆控件多的不勝枚舉,爲何咱們還要再造一個輪子呢?html
由於大多很多天曆控件都是用於選擇日期的,有種需求是要在日曆上展現各類各樣的內容,這樣的日曆控件較少,並且試用下來並不滿意。git
所以就再造一個輪子,如今帶你一塊兒基於使用以前完成的組件機制來開發一個日曆控件。github
需求ajax
簡單把需求整理以下:瀏覽器
首先咱們拿系統中自帶的日曆觀察一下,看看日曆的特徵究竟是怎麼樣的。app
一個月中有 28 到 31 天不等,可是爲了保證完整的結構,日曆中會有部分上一月和下一月的日期,總結下來,一個月中顯示的一定是整整6周的日期。dom
那麼只要獲得當月的開始日期就能夠繪製日曆了。post
如何計算當月日曆視圖中的開始日期呢? 前面已經分析了,爲了保證完整,它顯示了上一月的部分天數,那麼只用從當月的1號開始往前推算就能夠了。this
開始日期 = 當月1號的日期 - 當月1號的星期 結束日期 = 開始日期 + 42天
這個問題搞清楚了,感受實現這麼一個日曆就沒什麼大阻礙了,開始動工吧!
首先構建以下所示的基本結構
其中:
主體區域中用繪製整個日曆
在初始化好日曆結構後就能夠開始繪製日曆了。
首先完成開始和結束時間的計算
{ // 初始化當前月份的開始日期和結束日期 _initStartEnd: function () { // 當月1號 var currMonth = moment(this.currMonth, 'YYYY-MM'), // 當月1號是周幾 the ISO day of the week with 1 being Monday and 7 being Sunday. firstDay_weekday = currMonth.isoWeekday(), startDateOfMonth, endDateOfMonth; if (!this.dayStartFromSunday) { // 開始爲週一 則向前減小周幾的天數-1即爲 開始的日期 startDateOfMonth = currMonth.subtract(firstDay_weekday - 1, 'day'); } else { // 開始爲週日 則直接向前周幾的天數便可 startDateOfMonth = currMonth.subtract(firstDay_weekday, 'day'); } endDateOfMonth = startDateOfMonth.clone().add(41, 'day'); this.startDateOfMonth = startDateOfMonth; this.endDateOfMonth = endDateOfMonth; } }
因爲要處理不少日期,而JavaScript中關於日期處理時,不一樣瀏覽器下差別較大,所以直接使用 moment.js 來對日期進行統一處理。
因爲使用習慣不一樣,一週的開始究竟是週一仍是週日是不肯定的,所以直接做爲配置便可。
上面已經計算獲得了一個月的開始日期和結束日期,那麼只用遍歷進行繪製便可。
因爲咱們使用了表格實現,所以須要按行繪製。
實現以下:
{ // 日曆可變部分的渲染 _render: function () { this._initStartEnd(); var weeks = 6, days = 7, curDate = this.startDateOfMonth.clone(), tr; var start = this.startDateOfMonth.format('YYYY-MM-DD'), end = this.endDateOfMonth.format('YYYY-MM-DD'); // 清空 並開始新的渲染 this._clearDays(); this._renderTitle(); for (var i = 0; i < weeks; ++i) { tr = document.createElement('tr'); tr.className = 'ep-calendar-week'; this._daysBody.appendChild(tr); for (var j = 0; j < days; ++j) { // 渲染一天 並遞增 this._renderDay(curDate, tr); curDate.add(1, 'day'); } } }, // 天天的渲染 _renderDay: function (date, currTr) { var td = document.createElement('td'), tdInner = document.createElement('div'), text = document.createElement('span'), day = date.isoWeekday(), // 返回的月份是0-11 month = date.month() + 1; tdInner.appendChild(text); td.appendChild(tdInner); td.className = 'ep-calendar-date'; tdInner.className = 'ep-calendar-date-inner'; // 完整日期 td.setAttribute('data-date', date.format('YYYY-MM-DD')); // 對應的iso星期 td.setAttribute('data-isoweekday', day); // 週末標記text.className if (day === 6 || day === 7) { td.className += ' ep-calenday-weekend'; } // 非本月標記 // substr 在ie8下有問題 // if (month != parseInt(this.currMonth.substr(-2))) { if (month != parseInt(this.currMonth.substr(5), 10)) { td.className += ' ep-calendar-othermonth'; } // 今天標記 if (this.today == date.format('YYYY-MM-DD')) { td.className += ' ep-calendar-today'; } // 天天渲染時發生 還未插入頁面 var renderEvent = this.fire('cellRender', { // 當天的完整日期 date: date.format('YYYY-MM-DD'), // 當天的iso星期 isoWeekday: day, // 日曆dom el: this.el, // 當前單元格 tdEl: td, // 日期文本 dateText: date.date(), // 日期class dateCls: 'ep-calendar-date-text', // 須要注入的額外的html extraHtml: '', isHeader: false }); // 處理對dayText內容和樣式的更改 text.innerText = renderEvent.dateText; text.className = renderEvent.dateCls; // 添加新增內容 if (renderEvent.extraHtml) { jQuery(renderEvent.extraHtml).appendTo(tdInner); } currTr.appendChild(renderEvent.tdEl); // 天天渲染後發生 插入到頁面 this.fire('afterCellRender', { date: date.format('YYYY-MM-DD'), isoWeekday: day, el: this.el, tdEl: td, dateText: text.innerText, dateCls: text.className, extraHtml: renderEvent.extraHtml, isHeader: false }); } }
直接從開始日期日後依次畫出42天便可。
爲了靈活性,在繪製的不一樣時機觸發了不一樣的事件,在使用時可綁定相應的事件,在其中進行個性化操做。
也爲了使用了方便和靈活性,直接在繪製日期時,在相應的dom上加入了所對應的日期和星期屬性。
在此過程當中須要對日期是否週末、是否本月、是不是選中的、是不是今天等進行相應的標記處理。
除了上面所述以外此外還要繪製出年月選擇、標題等,這些實際就是給已經有的dom元素中更改內容而已,就再也不展開了。
上面已經基本繪製出了一個日曆,切換月份實際就更簡單了,只用根據新的月份從新計算開始日期,清空原來的內容,從新進行繪製便可。
{ // 設置月份 setMonth: function (ym) { var date = moment(ym, 'YYYY-MM'); if (date.isValid()) { var oldMonth = this.currMonth, aimMonth = date.format('YYYY-MM'); // 月份變更前 this.fire('beforeMonthChange', { el: this.el, oldMonth: oldMonth, newMonth: aimMonth }); this.currMonth = aimMonth; this.render(); // 月份變更後 this.fire('afterMonthChange', { el: this.el, oldMonth: oldMonth, newMonth: aimMonth }); } else { throw new Error(ym + '是一個不合法的日期'); } } }
要處理的事件較多,此處僅僅以日期的點擊做爲示意。
{ // 初始化事件 _initEvent: function () { var my = this; jQuery(this.el) // 日期單元格 .on('click', '.ep-calendar-date', function (e) { var date = this.getAttribute('data-date'), ev = my.fire('dayClick', { ev: e, date: date, day: this.getAttribute('data-isoweekday'), el: my.el, tdEl: this }); // 若是修改事件對象的cancel爲true後 則不進行後續的選中操做 if (!ev.cancel) { my.setSelected(date); } }) } }
因爲日期所對應的dom元素始終會添加和移除,直接把事件綁定在日期的dom元素上,則必須在每次新增後從新綁定事件,十分麻煩。
直接使用事件代理機制,將事件綁定在整個日曆的dom上便可,這樣事件只用在建立時初始化一次便可,簡單、高效、省內存。
咱們新增這個控件的主要目的就是要支持在日曆中繪製任意內容,怎麼使用呢?
var testCalendar = epctrl.init('Calendar', { el: '#date', // 資源加載過程當中的事件須要直接在這裏指定 events: { beforeSourceLoad: function (e) { // 資源加載前,在加入咱們的皮膚樣式文件 e.cssUrl.push('./test-skin.css'); } } }); // 日期部分渲染前 支持動態獲取數據 testCalendar.on('beforeDateRender', function (e) { var startDate = e.startDate, endDate = e.endDate; // 若是須要動態獲取數據 // 則將獲取數據的ajax加到事件對象的ajax屬性上便可 // 日期渲染的cellRender事件將在ajax成功獲取數據後執行 e.ajax = $.ajax({ url: 'getDateInfo.xxx', // 將當月視圖的開始和結束時間傳遞過去 data: { start: startDate, end: endDate } }); }); // 控制渲染過程 可插入任意內容或修改原來的內容 testCalendar.on('cellRender', function (e) { if (!e.isHeader) { // 如:週五週六則插入週末 不然插入工做日 e.extraHtml = '<div>' + (e.isoWeekday > 5 ? '週末': '工做日') + '</div>'; } });
以上就是關於一個月視圖日曆控件核心步驟了。
此日曆實現基於一個控件基類擴展而來,其必要功能僅爲一套事件機制,可參考實現一套自定義事件機制
上面只分析了關鍵步驟,和核心代碼,爲了方便使用和擴展性,實際代碼中還要處理不少問題。源碼和文檔以下,感興趣能夠閱讀:月視圖日曆