這篇文章主要為大家介紹了關於利用Javascript如何開發一個二維週視圖日曆的相關資料,文中透過範例程式碼介紹的非常詳細,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。
前言
本文介紹了Javascript開發二維週視圖行事曆的相關內容,也就是先前實作了一個月視圖行事曆,我們今天來實作一個二維週視圖的日曆。
以下進行分析其中的關鍵部分。
結構準備
不同之處在於其在日曆的基礎上還有一個分類軸,用來展示不同的類別目,主要用於一週內的日程安排、會議安排等。
二維則和之前單獨的有所不同,二維日曆再切換日期時不用全部重新渲染,分類是不用變的,僅改變顯示的日期即可。
而且由於是二維的,插入的內容必定是同時屬於一個分類和一個時間段的,內容肯定是可以跨越時間(即日期軸)的,因此不能直接將插入的內容像開始的日曆一樣直接放置在日曆的格子中。而要進行單獨的處理。
另外,只要分類不變,日期和分類構成的網格是不用重繪的。
考慮到以上情況,插入內容的和網格是需要分開來的,我將現成的日曆弄成一下3D效果示意:
即插入內容的層是單獨放置在時間和分類構成的網格上方的。
基於上述分析,先建構如下基本結構:
<p class="ep-weekcalendar border"> <!-- 头部 --> <p class="ep-weekcalendar-header"> <p class="ep-weekcalendar-header-left"></p> <p 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> </p> <p class="ep-weekcalendar-header-right"></p> </p> <!-- 主体 --> <p class="ep-weekcalendar-body"> <!-- 分类区域 --> <p class="ep-weekcalendar-category-area"> <p class="ep-weekcalendar-category-header"> <span class="ep-weekcalendar-category-title">车辆</span> </p> <ul class="ep-weekcalendar-category-list"> </ul> </p> <!-- 内容区域 --> <p class="ep-weekcalendar-time-area"> <!-- 每周日期渲染区域。切换日期时重新绘制内容 --> <p class="ep-weekcalendar-weeks"></p> <p class="ep-weekcalendar-main"> <!-- 分类和内容构建的网格区域,仅在分类改变时进行调整 --> <p class="ep-weekcalendar-grid"> </p> <!-- 可插入任意内容的区域,日期切换时清空,根据使用需求插入内容 --> <p class="ep-weekcalendar-content"></p> </p> </p> </p> <!-- 底部 --> <p class="ep-weekcalendar-body"></p> </p>
結構如上,實作程式碼就不用展示了。
繪製實作
初始好了必要的結構,我們接著進行行事曆的繪製工作。
分類繪製
首先要處理的是分類,週視圖中,一週的天數是固定的,確定好分類才能繪製出主體部分的網格。
對於分類,暫時考慮如下必要資料格式:
{ 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('p'), 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('p'), 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('p'), 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('p'), 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 ,会导致先触发日历上的日期点击或日期范围点击。
使用
此日历实现基于一个控件基类扩展而来,其必要功能仅为一套事件机制,可参考实现一套自定义事件机制
实测一下效果吧:
<p id="week-calendar" style="width:100%;height:80vh"></p> <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>
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
以上是使用Javascript如何開發二維週視圖日曆的詳細內容。更多資訊請關注PHP中文網其他相關文章!