Cet article vous présente principalement les informations pertinentes sur la façon d'utiliser Javascript pour développer un calendrier hebdomadaire bidimensionnel. L'article le présente de manière très détaillée à travers un exemple de code. Il a une certaine valeur d'apprentissage de référence pour les études ou le travail de chacun. C'est nécessaire les amis, veuillez suivre l'éditeur pour apprendre ensemble.
Avant-propos
Cet article vous présente le contenu pertinent du développement Javascript d'un calendrier hebdomadaire bidimensionnel, c'est-à-dire une vue mensuelle Le calendrier a été précédemment implémenté. Aujourd'hui, nous allons implémenter un calendrier hebdomadaire en deux dimensions.
Les éléments clés sont analysés ci-dessous.
Préparation de la structure
La différence est qu'il dispose également d'un axe de classement basé sur le calendrier pour afficher différentes catégories. Principalement utilisé pour la planification, arrangements de réunion, etc. dans une semaine.
Le calendrier bidimensionnel est différent du précédent. Lors du changement de date, le calendrier bidimensionnel n'a pas besoin d'être restitué. La classification n'a pas besoin d'être modifiée, seulement la date affichée. est changé.
Et comme il est bidimensionnel, le contenu inséré doit appartenir à la fois à une catégorie et à une période temporelle. Le contenu peut certainement s'étendre sur le temps (c'est-à-dire l'axe des dates), donc le contenu inséré. ne peut pas être placé directement comme le début. Le calendrier est placé directement dans la grille du calendrier. Mais à traiter séparément.
De plus, tant que la catégorie reste inchangée, la grille composée des dates et des catégories n'a pas besoin d'être redessinée.
Compte tenu de la situation ci-dessus, l'insertion du contenu et la grille doivent être séparées. J'ai transformé le calendrier prêt à l'emploi en un effet 3D :
. Autrement dit, la couche d'insertion de contenu est placée séparément au-dessus de la grille composée de temps et de catégorie.
Sur la base de l'analyse ci-dessus, construisez d'abord la structure de base suivante :
<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>
La structure est comme ci-dessus et le code d'implémentation n'a pas besoin d'être affiché.
Mise en œuvre du dessin
Après avoir initialisé la structure nécessaire, nous procédons au dessin du calendrier.
Dessin de catégorie
La première chose à traiter est le classement Dans la vue hebdomadaire, le nombre de jours dans une semaine est fixé uniquement lorsque le classement est déterminé. peut-on dessiner la partie principale de la grille.
Pour la classification, envisagez temporairement le format de données nécessaire suivant :
{ id: 'cate-1', // 分类ID name: '法拉利', // 分类名称 content: '苏E00000' // 分类的具体描述 }
La mise en œuvre est la suivante :
{ // 设置分类数据 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 }); } }
Ce qui précède consiste à définir l'ensemble de données de classificationCatégorie comme point d'entrée et appelez la méthode de classification des dessins , qui appelle également _renderChanged. Cette méthode est utilisée pour redessiner les parties variables du calendrier, telles que le titre, la date et le contenu, qui seront introduits plus tard.
Dessin des dates
L'axe de classement a été préparé ci-dessus, et l'axe des dates doit être dessiné Pour la vue hebdomadaire, la mise en œuvre. de la semaine C'est très simple, il suffit de restituer 7 jours en séquence en fonction de la date de début de la semaine. Faites attention à fournir les informations nécessaires sur la date de l'événement correspondant pendant le processus de dessin, afin que l'utilisateur puisse personnaliser l'événement.
{ // 渲染日历的星期 _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 }); } }
Grille et contenu
Les deux axes de la vue bidimensionnelle ont été préparés ci-dessus, puis la grille et le contenu Dessinez simplement la couche de contenu.
Grille
Ici le classement est dans le sens Y (ligne) et la date est dans le sens X (colonne) :
{ // 右侧网格 _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; } }
Contenu
Théoriquement, deux dimensions devraient prendre en charge à la fois les situations entre lignes et entre colonnes, c'est-à-dire que la zone de contenu doit être un bloc entier d'éléments. Mais lorsqu’elles sont combinées à des situations réelles, les exigences transversales sont courantes (une chose est utilisée en continu sur une période donnée). La catégorisation croisée n'a pas beaucoup de signification pratique. Elle doit être gérée séparément par catégorie, et la catégorisation croisée devient alors à nouveau compliquée. Et même s'il est nécessaire d'utiliser plusieurs choses en même temps pendant une période de temps, cela peut être mis en œuvre directement (la catégorie A est utilisée dans la période XX, et B est utilisée dans la période XX, mais la catégorie XX est exactement la même chose en ce moment).
Nous ne traitons donc ici que des situations transversales dans le temps, et le contenu peut être dessiné par lignes ou catégories, ce qui peut simplifier de nombreux calculs lors de l'insertion de composants de contenu.
{ // 右侧内容 _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 = {}; } }
Si vous devez réaliser des situations entre lignes et colonnes, vous pouvez directement dessiner le contenu dans un bloc entier d'éléments. Cependant, lorsque vous cliquez sur des événements et insérez des composants de contenu, vous devez calculer le. la catégorie correspondante ainsi que la date et l'heure en même temps.
Difficultés de mise en œuvre
Insertion des composants de contenu
Nous mettons en œuvre cette semaine en deux dimensions afficher le calendrier L'objectif principal est de prendre en charge l'insertion de contenu arbitraire. L'élément DOM pour l'insertion de contenu a été préparé ci-dessus. Ce qui doit être fait ici est de dessiner les données dans le DOM et de les placer dans la position appropriée.
Considérez la structure de données des composants de contenu nécessaire comme suit :
{ id: '数据标识', categoryId: '所属分类标识', title: '名称', content: '内容', start: '开始日期时间' end: '结束日期时间' bgColor: '展示的背景色' }
Étant donné que la zone de contenu est dessinée directement en fonction de la classification, après avoir obtenu les données, la classification correspondante existe déjà . Le point clé est de calculer les positions de début et de fin en fonction des heures de début et de fin spécifiées.
Considérez ce qui suit :
Considérez la réactivité, et le calcul de la position est calculé en pourcentage
一周的总时间是固定的,开始日期时间和这周开始日期时间的差额占总时间的百分比即开始位置的百分比
结束日期时间和开始时间的差额占总时间的百分比即为结束时间距离最左侧的百分比
注意处理开始和结束时间溢出本周的情况
因此关于位置计算可以用如下代码处理:
{ // 日期时间分隔符 默认为空 对应格式为 '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>
上面是我整理给大家的,希望今后会对大家有帮助。
相关文章:
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!