이 글에서는 Javascript를 사용하여 2차원 주간 보기 캘린더를 개발하는 방법에 대한 관련 정보를 주로 소개합니다. 이 글에서는 누구나 JavaScript를 배울 수 있는 확실한 참고 학습 가치를 제공합니다. JavaScript관심 있는 친구들은 편집자를 따라가서 함께 배워보세요.
머리말
이 기사에서는 이전에 Javascript를 사용한 2차원 주 보기 달력 개발에 대해 소개합니다. 즉, 오늘은 2차원 주 보기 달력을 구현해 보겠습니다. .
핵심 부분은 아래에서 분석됩니다.
구조 준비
다양한 카테고리를 표시하는 달력 기반 분류 축도 있다는 점이 차이점이며 주로 일주일 내 일정, 회의 준비 등에 사용됩니다.
2차원 달력은 이전 싱글 달력과 다릅니다. 날짜 전환 시 2차원 달력은 다시 렌더링할 필요가 없으며 표시되는 날짜만 변경됩니다.
그리고 2차원적이기 때문에 삽입된 콘텐츠는 카테고리와 기간에 동시에 속해야 합니다. 콘텐츠는 확실히 시간(즉, 날짜 축)에 걸쳐 있을 수 있으므로 삽입된 콘텐츠를 직접 삽입할 수 없습니다. 시작 달력처럼 달력 그리드에 배치하세요. 단, 별도로 처리해야 합니다.
또한 카테고리가 변경되지 않은 한 날짜와 카테고리로 구성된 그리드를 다시 그릴 필요가 없습니다.
위의 상황을 고려하여, 삽입된 컨텐츠와 그리드를 분리해야 합니다. 기성 달력을 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 }); } }
Grid and content
위에서 2차원 뷰의 두 축을 준비한 후 그리드와 콘텐츠 레이어를 그릴 수 있습니다.
Grid
여기서 분류는 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; } }
Content
이론적으로 2차원은 다음을 지원해야 합니다. 교차 행, 열을 가로지르는 두 가지 경우, 즉 콘텐츠 영역은 요소의 전체 블록이어야 합니다. 그러나 실제 상황과 결합하면 교차 시간 요구 사항이 일반적입니다(한 가지가 일정 기간 동안 지속적으로 사용됨). 교차분류는 실질적인 의미가 별로 없으며, 분류별로 별도로 관리해야 하고, 그렇게 되면 교차분류가 다시 복잡해집니다. 그리고 일정 기간 동안 여러 가지를 동시에 사용해야 하는 경우에도 직접 구현할 수 있습니다(카테고리 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 = {}; } }
행 교차, 열 교차 상황을 구현해야 하는 경우 콘텐츠를 전체 요소로 직접 그릴 수 있습니다. 단, 이벤트를 클릭하고 콘텐츠 구성 요소를 삽입할 때 해당하는 계산을 수행해야 합니다. 카테고리와 날짜 및 시간을 동시에 사용할 수 있습니다.
구현상의 어려움
콘텐츠 구성 요소 삽입
이 2차원 주간 보기 달력을 구현하는 주요 목적은 임의의 콘텐츠 삽입을 지원하는 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>
以上就是本篇文章的所有内容,希望会给大家带来帮助!!
相关推荐:
위 내용은 자바스크립트를 활용한 2차원 주간 보기 Calendar_javascript 기술 개발의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!