您好,登錄后才能下訂單哦!
前言
本文給大家介紹了Javascript開發二維周視圖日歷的相關內容,即之前實現了一個月視圖日歷,我們今天來實現一個二維周視圖的日歷。
以下進行分析其中的關鍵部分。
結構準備
不同之處在于其在日歷的基礎上還有一個分類軸,用于展示不同的類目,主要用于一周內的日程安排、會議安排等。
二維則和之前單獨的有所不同,二維日歷再切換日期時不用全部重新渲染,分類是不用變的,僅僅改變顯示的日期即可。
而且由于是二維的,插入的內容必定是同時屬于一個分類和一個時間段的,內容肯定是可以跨越時間(即日期軸)的,因此不能直接將插入的內容像開始的日歷一樣直接放置在日歷的格子中。而要進行單獨的處理。
另外,只要分類不變,日期和分類構成的網格是不用重繪的。
考慮到以上情況,插入內容的和網格是需要分開來的,我將現成的日歷弄成一下3D效果示意:
即插入內容的層是單獨放置在時間和分類構成的網格上方的。
基于以上分析,先構建如下基本結構:
<div class="ep-weekcalendar border"> <!-- 頭部 --> <div class="ep-weekcalendar-header"> <div class="ep-weekcalendar-header-left"></div> <div class="ep-weekcalendar-header-center"> <span class="ep-weekcalendar-header-btn ep-weekcalendar-header-btn-prev"></span> <span class="ep-weekcalendar-title">2017年12月04日 - 10日</span> <span class="ep-weekcalendar-header-btn ep-weekcalendar-header-btn-next"></span> </div> <div class="ep-weekcalendar-header-right"></div> </div> <!-- 主體 --> <div class="ep-weekcalendar-body"> <!-- 分類區域 --> <div class="ep-weekcalendar-category-area"> <div class="ep-weekcalendar-category-header"> <span class="ep-weekcalendar-category-title">車輛</span> </div> <ul class="ep-weekcalendar-category-list"> </ul> </div> <!-- 內容區域 --> <div class="ep-weekcalendar-time-area"> <!-- 每周日期渲染區域。切換日期時重新繪制內容 --> <div class="ep-weekcalendar-weeks"></div> <div class="ep-weekcalendar-main"> <!-- 分類和內容構建的網格區域,僅在分類改變時進行調整 --> <div class="ep-weekcalendar-grid"> </div> <!-- 可插入任意內容的區域,日期切換時清空,根據使用需求插入內容 --> <div class="ep-weekcalendar-content"></div> </div> </div> </div> <!-- 底部 --> <div class="ep-weekcalendar-body"></div> </div>
結構如上,實現代碼就不用展示了。
繪制實現
初始好了必要的結構,我們接著進行日歷的繪制工作。
分類繪制
首先要處理的是分類,周視圖中,一周的天數是固定的,確定好分類才能繪制出主體部分的網格。
對于分類,暫時考慮如下必要數據格式:
{ id: 'cate-1', // 分類ID name: '法拉利', // 分類名稱 content: '蘇E00000' // 分類的具體描述 }
實現如下:
{ // 設置分類數據 setCategory: function (data) { if (!(data instanceof Array)) { this.throwError('分類數據必須是一個數組'); return; } this._categoryData = data; // 繪制分類 this._renderCatagories(); // 繪制其他需要改變的部分 this._renderChanged(); }, // 左側分類渲染 _renderCatagories: function () { this._categoryListEl.innerHTML = ''; var i = 0, data = this._categoryData, node = document.createElement('li'), cataEl; node.className = 'ep-weekcalendar-category'; // 用行作為下標記錄當前分類id集合 this._categoryIndexs = []; // id為鍵記錄索引 this._categoryReocrds = {}; while (i < data.length) { this._categoryIndexs.push(data[i].id); this._categoryReocrds[data[i].id] = i; cataEl = node.cloneNode(true); this._rendercategory(data[i], cataEl); i++; } }, _rendercategory: function (cate, cateEl) { cateEl.setAttribute('data-cateid', cate.id); var titleEl = document.createElement('span'), contentEl = document.createElement('span'); titleEl.className = 'title'; contentEl.className = 'content'; titleEl.innerHTML = cate.name; contentEl.innerHTML = cate.content; cateEl.appendChild(titleEl); cateEl.appendChild(contentEl); this.fire('categoryRender', { categoryEl: cateEl, titleEl: titleEl, contentEl: contentEl }); this._categoryListEl.appendChild(cateEl); this.fire('agterCategoryRender', { categoryEl: cateEl, titleEl: titleEl, contentEl: contentEl }); } }
上面通過設置分類數據 setCategory 作為入口,調用繪制分類方法,其中還調用了 _renderChanged 此方法用于重新繪制日歷的可變部分,如標題、日期和其中的內容,會在之后進行介紹。
日期繪制
上面已經準備好了分類軸,還需要繪制出日期軸,對于周視圖而言,一周的實現就非常簡單了,根據一周的開始日期,依次渲染7天即可。 注意在繪制過程中提供日期的必要信息給相應事件,一遍使用者能夠在事件中進行個性化處理。
{ // 渲染日歷的星期 _renderWeeks: function () { this._weeksEl.innerHTML = ''; var i = 0, currDate = this._startDate.clone(), node = document.createElement('div'), week; node.className = 'ep-weekcalendar-week'; // 單元格列作為下標記錄日期 this._dateRecords = []; while (i++ < 7) { // 更新記錄日期 this._dateRecords.push(currDate.clone()); week = node.cloneNode(true); this._renderWeek(currDate, week); currDate.add(1, 'day'); } // 切換日期 需要重繪內容區域 this._rednerContent(); }, _renderWeek: function (date, node) { var dateText = date.format('YYYY-MM-DD'), day = date.isoWeekday(); if (day > 5) { node.className += ' weekend'; } if (date.isSame(this.today, 'day')) { node.className += ' today'; } node.setAttribute('data-date', dateText); node.setAttribute('date-isoweekday', day); var ev = this.fire('dateRender', { // 當前完整日期 date: dateText, // iso星期 isoWeekday: day, // 顯示的文本 dateText: '周' + this._WEEKSNAME[day - 1] + ' ' + date.format('MM-DD'), // classname dateCls: node.className, // 日歷el el: this.el, // 當前el dateEl: node }); // 處理事件的修改 node.innerHTML = ev.dateText; node.className = ev.dateCls; this._weeksEl.appendChild(node); this.fire('afterDateRender', { // 當前完整日期 date: dateText, // iso星期 isoWeekday: day, // 顯示的文本 dateText: node.innerHTML, // classname dateCls: node.className, // 日歷el el: this.el, // 當前el dateEl: node }); } }
網格和內容
上面已經準備好了二維視圖中的兩個軸,接著進行網格和內容層的繪制即可。
網格
此處以分類為Y方向(行),日期為X方向(列)來進行繪制:
{ // 右側網格 _renderGrid: function () { this._gridEl.innerHTML = ''; var rowNode = document.createElement('div'), itemNode = document.createElement('span'), rowsNum = this._categoryData.length, i = 0, j = 0, row, item; rowNode.className = 'ep-weekcalendar-grid-row'; itemNode.className = 'ep-weekcalendar-grid-item'; while (i < rowsNum) { row = rowNode.cloneNode(); row.setAttribute('data-i', i); j = 0; while (j < 7) { item = itemNode.cloneNode(); // 周末標識 if (this.dayStartFromSunday) { if (j === 0 || j === 6) { item.className += ' weekend'; } } else { if (j > 4) { item.className += ' weekend'; } } item.setAttribute('data-i', i); item.setAttribute('data-j', j); row.appendChild(item); j++; } this._gridEl.appendChild(row); i++; } rowNode = itemNode = row = item = null; } }
內容
理論上來說,二維要支持跨行、跨列兩種情況,即內容區域應該為一整塊元素。但是結合到實際情況,跨時間的需求普遍存在(一個東西在一段時間內被連續使用)。跨分類并沒有多大的實際意義,本來就要分開以分類來管理,再跨分類,又變得復雜了。而且即使一定要實現一段時間內同時在使用多個東西,也是可以直接實現的(分類A在XX時間段內被使用,B在XX時間段內被使用,只是此時XX正好相同而已)。
因此此處僅處理跨時間情況,可將內容按行即分類進行繪制,這樣在插入內容部件時,可以簡化很多計算。
{ // 右側內容 _rednerContent: function () { this._contentEl.innerHTML = ''; var i = 0, node = document.createElement('div'), row; node.className = 'ep-weekcalendar-content-row'; while (i < this._categoryData.length) { row = node.cloneNode(); row.setAttribute('data-i', i); this._contentEl.appendChild(row); ++i; } row = node = null; }, // 日期切換時清空內容 _clearContent: function () { var rows = this._contentEl.childNodes, i = 0; while (i < rows.length) { rows[i].innerHTML && (rows[i].innerHTML = ''); ++i; } // 部件數據清空 this._widgetData = {}; } }
如果一定要實現跨行跨列的情況,直接將內容繪制成一整塊元素即可,但是在點擊事件和插入內容部件時,需要同時計算對應的分類和日期時間。
難點實現
內容部件插入
我們實現這個二維周視圖日歷的主要目的就是要支持插入任意的內容,上面已經準備好了插入內容的dom元素,這里要做的就是將數據繪制成dom放置在合適的位置。
考慮必要的內容部件數據結構如下:
{ id: '數據標識', categoryId: '所屬分類標識', title: '名稱', content: '內容', start: '開始日期時間' end: '結束日期時間' bgColor: '展示的背景色' }
由于上面在內容區域是直接按照分類作為繪制的,因此拿到數據后,對應的分類就已經存在了。重點要根據指定的開始和結束時間計算出開始和結束位置。
考慮如下:
因此關于位置計算可以用如下代碼處理:
{ // 日期時間分隔符 默認為空 對應格式為 '2017-11-11 20:00' // 對于'2017-11-11T20:00' 這樣的格式務必指定正確的日期和時間之間的分隔符T _dateTimeSplit:' ', // 一周分鐘數 _WEEKMINUTES: 7 * 24 * 60, // 一周秒數 _WEEKSECONDS: 7 * 24 * 3600, // 一天的分鐘數秒數 _DAYMINUTES: 24 * 60, _DAYSCONDS: 24 * 3600, // 計算位置的精度 取值second 或 minute posUnit: 'second', // 計算指定日期的分鐘或秒數 _getNumByUnits: function (dateStr) { var temp = dateStr.split(this._dateTimeSplit), date = temp[0]; // 處理左側溢出 if (this._startDate.isAfter(date, 'day')) { // 指定日期在開始日期之前 return 0; } // 右側溢出直接算作第7天即可 var times = (temp[1] || '').split(':'), days = (function (startDate) { var currDate = startDate.clone(), i = 0, d = moment(date, 'YYYY-MM-DD'); while (i < 7) { if (currDate.isSame(d, 'day')) { return i; } else { currDate.add(1, 'day'); ++i; } } console && console.error && console.error('計算天數時出錯!'); return i; }(this._startDate)), hours = parseInt(times[0], 10) || 0, minutes = parseInt(times[1], 10) || 0, seconds = parseInt(times[2], 10) || 0, // 對應分鐘數 result = days * this._DAYMINUTES + hours * 60 + minutes; return this.posUnit == 'minute' ? result : (result * 60 + seconds); }, // 計算日期時間的百分比位置 _getPos: function (dateStr) { var p = this._getNumByUnits(dateStr) / (this.posUnit == 'minute' ? this._WEEKMINUTES : this._WEEKSECONDS); return p > 1 ? 1 : p; } }
上面就拿到了一個數據所對應的開始位置和結束位置。基本上是已經完成了,但是還需要再處理一個情況:相同分類下的時間沖突問題。
考慮以如下方式進行:
實現如下:
{ /** * 檢查是否發生重疊 * * @param {Object} data 當前要加入的數據 * @returns false 或 和當前部件重疊的元素數組 */ _checkOccupied: function (data) { if (!this._widgetData[data.categoryId]) { return false; } var i = 0, cate = this._widgetData[data.categoryId], len = cate.length, result = false, occupied = []; for (; i < len; ++i) { // 判斷時間是否存在重疊 if (data.start < cate[i].end && data.end > cate[i].start) { occupied.push(cate[i]); result = true; } } return result ? occupied : false; } }
完成以上兩步就可以往我們的內容區域中插入了
{ // 緩存widget數據 _cacheWidgetData: function (data) { if (!this._widgetData[data.categoryId]) { this._widgetData[data.categoryId] = []; } // 記錄當前的 this._widgetData[data.categoryId].push(data); }, // 新增一個小部件 addWidget: function (data) { var row = this._contentEl.childNodes[this._categoryReocrds[data.categoryId]]; if (!row) { this.throwError('對應分類不存在,添加失敗'); return false; } // 先查找是否含有 var $aim = jQuery('.ep-weekcalendar-content-widget[data-id="' + data.id + '"]', row); if ($aim.length) { // 已經存在則不添加 return $aim[0]; } // 創建部件 var widget = document.createElement('div'), title = document.createElement('span'), content = document.createElement('p'), startPos = this._getPos(data.start), endPos = this._getPos(data.end), _data = { categoryId: data.categoryId, id: data.id, start: startPos, end: endPos, el: widget, data: data }; widget.className = 'ep-weekcalendar-content-widget'; title.className = 'ep-weekcalendar-content-widget-title'; content.className = 'ep-weekcalendar-content-widget-content'; widget.appendChild(title); widget.appendChild(content); // 通過絕對定位,指定其left和right來拉開寬度的方式來處理響應式 // 可以通過樣式設置一個最小寬度,來避免時間段過小時其中文本無法顯示的問題 widget.style.left = startPos * 100 + '%'; widget.style.right = (1 - endPos) * 100 + '%'; data.bgColor && (widget.style.backgroundColor = data.bgColor); data.id && widget.setAttribute('data-id', data.id); widget.setAttribute('data-start', data.start); widget.setAttribute('data-end', data.end); title.innerHTML = data.title; data.content && (content.innerHTML = data.content); widget.title = data.title; // 檢查是否發生重疊 var isoccupied = this._checkOccupied(_data); if (isoccupied) { // 觸發重疊事件 var occupiedEv = this.fire('widgetoccupied', { occupiedWidgets: (function () { var arr = []; for (var i = 0, l = isoccupied.length; i < l; ++i) { arr.push(isoccupied[i].el); } return arr; })(), currWidget: widget, widgetData: data }); // 取消后續執行 if (occupiedEv.cancel) { return false; } } // 緩存數據 this._cacheWidgetData(_data); var addEv = this.fire('widgetAdd', { widgetId: data.id, categoryId: data.categoryId, start: data.start, end: data.end, startPos: startPos, endPos: endPos, widgetEl: widget }); if (addEv.cancel) { return false; } row.appendChild(widget); this.fire('afterWidgetAdd', { widgetId: data.id, categoryId: data.categoryId, start: data.start, end: data.end, startPos: startPos, endPos: endPos, widgetEl: widget }); return widget; }, }
點擊事件和范圍選擇
此控件不僅用于結果展示,還要可用于點擊進行添加,需要處理其點擊事件,但是由于要展示內容,內容是覆蓋在分類和日期構成的網格之上的,用戶的點擊是點擊不到網格元素的,必須要根據點擊的位置進行計算來獲取所點擊的日期和所在分類。
同時,由于展示的部件都是時間范圍的,因此點擊返回某天和某個分類是不夠的,還需要能夠支持鼠標按下拖動再松開,來直接選的一段時間。
考慮到以上需求,點擊事件不能直接使用 click 來實現,考慮使用 mousedown 和 mouseup 來處理點擊事件,同時需要在 mousemove 中實時給出用戶響應。
{ _initEvent: function () { var me = this; // 點擊的行索引 var row, // 開始列索引 columnStart, // 結束列索引 columnEnd, // 是否在按下、移動、松開的click中 isDurringClick = false, // 是否移動過 用于處理按下沒有移動直接松開的過程 isMoveing = false, $columns, // 網格左側寬度 gridLeft, // 每列的寬度 columnWidth jQuery(this.el) // 按下鼠標 記錄分類和開始列 .on('mousedown.weekcalendar', '.ep-weekcalendar-content-row', function (e) { isDurringClick = true; gridLeft = jQuery(me._gridEl).offset().left; columnWidth = jQuery(me._gridEl).width() / 7; jQuery(me._gridEl).find('.ep-weekcalendar-grid-item').removeClass(me._selectedCls); row = this.getAttribute('data-i'); $columns = jQuery(me._gridEl).find('.ep-weekcalendar-grid-row').eq(row).children(); columnStart = (e.pageX - gridLeft) / columnWidth >> 0; }); // 移動和松開 松開鼠標 記錄結束列 觸發點擊事件 // 不能直接綁定在日期容器上 否則鼠標移出日歷后,松開鼠標,實際點擊已經結束,但是日歷上處理不到。 jQuery('body') // 點擊移動過程中 實時響應選中狀態 .on('mousemove.weekcalendar', function (e) { if (!isDurringClick) { return; } isMoveing = true; // 當前列索引 var currColumn; // mousemoveTimer = setTimeout(function () { currColumn = (e.pageX - gridLeft) / columnWidth >> 0; // 修正溢出 currColumn = currColumn > 6 ? 6 : currColumn; currColumn = currColumn < 0 ? 0 : currColumn; $columns.removeClass(me._selectedCls); // 起止依次選中 var start = Math.min(columnStart, currColumn), end = Math.max(columnStart, currColumn); do { $columns.eq(start).addClass(me._selectedCls); } while (++start <= end); }) // 鼠標松開 .on('mouseup.weekcalendar', function (e) { if (!isDurringClick) { return; } var startIndex = -1, endIndex = -1; columnEnd = (e.pageX - gridLeft) / columnWidth >> 0; columnEnd = columnEnd > 6 ? 6 : columnEnd; // 沒有移動過時 if (!isMoveing) { startIndex = endIndex = columnEnd; // 直接down up 沒有move的過程則只會有一個選中的,直接以結束的作為處理即可 $columns.eq(columnEnd).addClass(me._selectedCls) .siblings().removeClass(me._selectedCls); } else { startIndex = Math.min(columnStart, columnEnd); endIndex = Math.max(columnStart, columnEnd); } // 觸發點擊事件 me.fire('cellClick', { // 分類id categoryId: me._categoryIndexs[row], // 時間1 startDate: me._dateRecords[startIndex].format('YYYY-MM-DD'), // 日期2 endDate: me._dateRecords[endIndex].format('YYYY-MM-DD'), // 行索引 rowIndex: row, // 列范圍 columnIndexs: (function (i, j) { var arr = []; while (i <= j) { arr.push(i++); } return arr; }(startIndex, endIndex)) }); row = columnStart = columnEnd = isMoveing = isDurringClick = false; }); } }
此過程要注意的問題是:mousedown 必須綁定在日歷上,而 mouseup 和 mousemove 則不能綁定在日歷上,具體原因已經寫在上面代碼注釋中了。
另外需要注意,由于范圍點擊選擇使用了 mousedown 和 mouseup 來模擬,那么日歷內容區域中插入的數據部件的點擊事件也要用 mousedown 和 mouseup 來模擬,因為 mouseup 觸發比 click 早,如果使用 click ,會導致先觸發日歷上的日期點擊或日期范圍點擊。
使用
此日歷實現基于一個控件基類擴展而來,其必要功能僅為一套事件機制,可參考實現一套自定義事件機制
實測一下效果吧:
<div id="week-calendar" ></div> <script> var calendar = epctrl.init('WeekCalendar', { el: '#week-calendar', categoryTitle: '車輛', category: [{ id: 'cate-1', name: '法拉利', content: '蘇E00000' }, { id: 'cate-2', name: 'Lamborghini', content: '蘇E00001' }, { id: 'cate-3', name: '捷豹', content: '蘇E00002' }, { id: 'cate-4', name: '賓利', content: '蘇E00003' }, { id: 'cate-5', name: 'SSC', content: '蘇E00004' }], events: { // 日期變化時觸發 dateChanged: function (e) { var data = { start: e.startDate, end: e.endDate, }; // 獲取數據并逐個添加到日歷上 getData(data).done(function (data) { $.each(data, function (i, item) { calendar.addWidget(item); }); }); }, // 部件重疊時觸發 widgetOccupied: function (e) { // 沖突時禁止繼續添加 console.error(e.widgetData.categoryId + '分類下id為' + e.widgetData.id + '的部件和現有部件有重疊,取消添加'); e.cancel = true; } } }); calendar.on('dateClick', function (e) { alert(JSON.stringify({ '開始時間': e.startDate, '結束時間': e.endDate, '分類id': e.categoryId, '行索引': e.rowIndex, '列索引范圍': e.columnIndexs }, 0, 4)); }); </script>
源碼下載:
github
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。