ホームページ > 記事 > ウェブフロントエンド > Javascript を使用して 2 次元の週次ビューを開発する Calendar_JavaScript スキル
この記事では、
JavaScript を使用して 2 次元の週間ビュー カレンダーを開発する方法に関する関連情報を主に、サンプル コードを通じて詳しく紹介します。 JavaScript興味のある友達は、編集者をフォローして一緒に学びましょう。
はじめにこの記事では、JavaScript を使用した 2 次元の週表示カレンダーの開発について紹介します。つまり、以前は月表示カレンダーを実装しました。今日は 2 次元の週表示カレンダーを実装します。 。
重要な部分を以下に分析します。 構造の準備
違いは、主に 1 週間以内のスケジュールや会議の手配などに使用される、さまざまなカテゴリを表示するためのカレンダーに基づく分類軸があることです。
つまり、コンテンツを挿入するレイヤーは別個に配置され、グリッドの上にある時間と分類。
上記の分析に基づいて、まず次の基本構造を構築します:<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 }); } }グリッドとコンテンツ
上記で2次元ビューの2つの軸が準備されました。そして、グリッドとコンテンツレイヤーを描画できます。
グリッド
ここでは、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次元がサポートされるはずです行をまたぐ場合、列をまたぐ場合が 2 つあります。つまり、コンテンツ領域は要素のブロック全体である必要があります。しかし、実際の状況と組み合わせると、時間を超えた要件が一般的になります (1 つのものが一定期間にわたって継続的に使用される)。相互分類には実際的な意味はあまりなく、分類ごとに管理する必要があり、そうするとまた相互分類が複雑になります。また、複数のものを一定期間同時に使用する必要がある場合でも、直接実装できます(カテゴリ 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>
以上就是本篇文章的所有内容,希望会给大家带来帮助!!
相关推荐:
以上がJavascript を使用して 2 次元の週次ビューを開発する Calendar_JavaScript スキルの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。