首发我的博客 - https://blog.cdswyda.com/post/2017121010
日历控件多的不胜枚举,为什么我们还要再造一个轮子呢?
因为大多数日历控件都是用于选择日期的,有种需求是要在日历上展示各种各样的内容,这样的日历控件较少,而且试用下来并不满意。
因此就再造一个轮子,现在带你一起基于使用之前完成的组件机制来开发一个日历控件。
需求
简单把需求整理如下:
- 月视图
- 支持在日历中每一天中插入任意的内容
- 相关点击事件
- 获取日历当前视图的开始和结束日期
- 获取设置选中的日期
实现分析
首先我们拿系统中自带的日历观察一下,看看日历的特征到底是怎么样的。

一个月中有 28 到 31 天不等,但是为了保证完整的结构,日历中会有部分上一月和下一月的日期,总结下来,一个月中显示的必定是整整6周的日期。
那么只要得到当月的开始日期就可以绘制日历了。
如何计算当月日历视图中的开始日期呢? 前面已经分析了,为了保证完整,它显示了上一月的部分天数,那么只用从当月的1号开始往前推算就可以了。
开始日期 = 当月1号的日期 - 当月1号的星期<br />结束日期 = 开始日期 + 42天<br />
这个问题搞清楚了,感觉实现这么一个日历就没什么大阻碍了,开始动工吧!
必要结构准备
首先构建如下所示的基本结构

其中:
- 头部左右为个性化区域,用于实际使用时放置任意内容。中间用于显示当前月份和切换按钮
- 主体区域中用绘制整个日历
- thead 中绘制周一至周日 或周日至周一的星期,这段内容是不会随月份切换而改变的,可以直接准备好
- tbody 中用于绘制可变的日期,准备好容器留空即可。
- 脚部区域用于实际使用时放置任意各项化内容
- menu区域用于切换日期时弹出的面板
绘制日历
在初始化好日历结构后就可以开始绘制日历了。
计算一个月中的开始日期和结束日期
首先完成开始和结束时间的计算
{<br /> // 初始化当前月份的开始日期和结束日期<br /> _initStartEnd: function () {<br /> // 当月1号<br /> var currMonth = moment(this.currMonth, 'YYYY-MM'),<br /> // 当月1号是周几 the ISO day of the week with 1 being Monday and 7 being Sunday.<br /> firstDay_weekday = currMonth.isoWeekday(),<br /> startDateOfMonth,<br /> endDateOfMonth;<br /> if (!this.dayStartFromSunday) {<br /> // 开始为周一 则向前减少周几的天数-1即为 开始的日期<br /> startDateOfMonth = currMonth.subtract(firstDay_weekday - 1, 'day');<br /> } else {<br /> // 开始为周日 则直接向前周几的天数即可<br /> startDateOfMonth = currMonth.subtract(firstDay_weekday, 'day');<br /> }<br /><br /> endDateOfMonth = startDateOfMonth.clone().add(41, 'day');<br /><br /> this.startDateOfMonth = startDateOfMonth;<br /> this.endDateOfMonth = endDateOfMonth;<br /> }<br />}<br />
由于要处理很多日期,而JavaScript中关于日期处理时,不同浏览器下差异较大,因此直接使用 moment.js 来对日期进行统一处理。
由于使用习惯不同,一周的开始到底是周一还是周日是不确定的,因此直接作为配置即可。
绘制一月中的日期
上面已经计算得到了一个月的开始日期和结束日期,那么只用遍历进行绘制即可。
由于我们使用了表格实现,因此需要按行绘制。
实现如下:
{<br /> // 日历可变部分的渲染<br /> _render: function () {<br /> this._initStartEnd();<br /><br /> var weeks = 6,<br /> days = 7,<br /> curDate = this.startDateOfMonth.clone(),<br /> tr;<br /><br /> var start = this.startDateOfMonth.format('YYYY-MM-DD'),<br /> end = this.endDateOfMonth.format('YYYY-MM-DD');<br /><br /> // 清空 并开始新的渲染<br /> this._clearDays();<br /> this._renderTitle();<br /><br /> for (var i = 0; i < weeks; ++i) {<br /> tr = document.createElement('tr');<br /> tr.className = 'ep-calendar-week';<br /> this._daysBody.appendChild(tr);<br /><br /> for (var j = 0; j < days; ++j) {<br /> // 渲染一天 并递增<br /> this._renderDay(curDate, tr);<br /> curDate.add(1, 'day');<br /> }<br /> }<br /> },<br /> // 每天的渲染<br /> _renderDay: function (date, currTr) {<br /> var td = document.createElement('td'),<br /> tdInner = document.createElement('div'),<br /> text = document.createElement('span'),<br /> day = date.isoWeekday(),<br /> // 返回的月份是0-11<br /> month = date.month() + 1;<br /><br /> tdInner.appendChild(text);<br /> td.appendChild(tdInner);<br /><br /> td.className = 'ep-calendar-date';<br /> tdInner.className = 'ep-calendar-date-inner';<br /> // 完整日期<br /> td.setAttribute('data-date', date.format('YYYY-MM-DD'));<br /> // 对应的iso星期<br /> td.setAttribute('data-isoweekday', day);<br /><br /> // 周末标记text.className<br /> if (day === 6 || day === 7) {<br /> td.className += ' ep-calenday-weekend';<br /> }<br /> // 非本月标记<br /> // substr 在ie8下有问题<br /> // if (month != parseInt(this.currMonth.substr(-2))) {<br /> if (month != parseInt(this.currMonth.substr(5), 10)) {<br /> td.className += ' ep-calendar-othermonth';<br /> }<br /> // 今天标记<br /> if (this.today == date.format('YYYY-MM-DD')) {<br /> td.className += ' ep-calendar-today';<br /> }<br /><br /> // 每天渲染时发生 还未插入页面<br /> var renderEvent = this.fire('cellRender', {<br /> // 当天的完整日期<br /> date: date.format('YYYY-MM-DD'),<br /> // 当天的iso星期<br /> isoWeekday: day,<br /> // 日历dom<br /> el: this.el,<br /> // 当前单元格<br /> tdEl: td,<br /> // 日期文本<br /> dateText: date.date(),<br /> // 日期class<br /> dateCls: 'ep-calendar-date-text',<br /> // 需要注入的额外的html<br /> extraHtml: '',<br /><br /> isHeader: false<br /> });<br /><br /> // 处理对dayText内容和样式的更改<br /> text.innerText = renderEvent.dateText;<br /> text.className = renderEvent.dateCls;<br /><br /> // 添加新增内容<br /> if (renderEvent.extraHtml) {<br /> jQuery(renderEvent.extraHtml).appendTo(tdInner);<br /> }<br /><br /> currTr.appendChild(renderEvent.tdEl);<br /><br /> // 每天渲染后发生 插入到页面<br /> this.fire('afterCellRender', {<br /> date: date.format('YYYY-MM-DD'),<br /> isoWeekday: day,<br /> el: this.el,<br /> tdEl: td,<br /> dateText: text.innerText,<br /> dateCls: text.className,<br /> extraHtml: renderEvent.extraHtml,<br /> isHeader: false<br /> });<br /> }<br />}<br />
直接从开始日期往后依次画出42天即可。
为了灵活性,在绘制的不同时机触发了不同的事件,在使用时可绑定相应的事件,在其中进行个性化操作。
也为了使用了方便和灵活性,直接在绘制日期时,在相应的dom上加入了所对应的日期和星期属性。
在此过程中需要对日期是否周末、是否本月、是否是选中的、是否是今天等进行相应的标记处理。
绘制其他内容
除了上面所述之外此外还要绘制出年月选择、标题等,这些实际就是给已经有的dom元素中更改内容而已,就不再展开了。
切换月份的实现
上面已经基本绘制出了一个日历,切换月份实际就更简单了,只用根据新的月份重新计算开始日期,清空原来的内容,重新进行绘制即可。
{<br /> // 设置月份<br /> setMonth: function (ym) {<br /> var date = moment(ym, 'YYYY-MM');<br /><br /> if (date.isValid()) {<br /> var oldMonth = this.currMonth,<br /> aimMonth = date.format('YYYY-MM');<br /><br /> // 月份变动前<br /> this.fire('beforeMonthChange', {<br /> el: this.el,<br /> oldMonth: oldMonth,<br /> newMonth: aimMonth<br /> });<br /><br /> this.currMonth = aimMonth;<br /> this.render();<br /><br /> // 月份变动后<br /> this.fire('afterMonthChange', {<br /> el: this.el,<br /> oldMonth: oldMonth,<br /> newMonth: aimMonth<br /> });<br /><br /> } else {<br /> throw new Error(ym + '是一个不合法的日期');<br /> }<br /> }<br />}<br />
事件的处理
要处理的事件较多,此处仅仅以日期的点击作为示意。
{<br /> // 初始化事件<br /> _initEvent: function () {<br /> var my = this;<br /> jQuery(this.el)<br /> // 日期单元格<br /> .on('click', '.ep-calendar-date', function (e) {<br /> var date = this.getAttribute('data-date'),<br /> ev = my.fire('dayClick', {<br /> ev: e,<br /> date: date,<br /> day: this.getAttribute('data-isoweekday'),<br /> el: my.el,<br /> tdEl: this<br /> });<br /><br /> // 如果修改事件对象的cancel为true后 则不进行后续的选中操作<br /> if (!ev.cancel) {<br /> my.setSelected(date);<br /> }<br /> })<br /> }<br />}<br />
由于日期所对应的dom元素始终会添加和移除,直接把事件绑定在日期的dom元素上,则必须在每次新增后重新绑定事件,十分麻烦。
直接使用事件代理机制,将事件绑定在整个日历的dom上即可,这样事件只用在创建时初始化一次即可,简单、高效、省内存。
使用
我们新增这个控件的主要目的就是要支持在日历中绘制任意内容,怎么使用呢?
var testCalendar = epctrl.init('Calendar', {<br /> el: '#date',<br /> // 资源加载过程中的事件需要直接在这里指定<br /> events: {<br /> beforeSourceLoad: function (e) {<br /> // 资源加载前,在加入我们的皮肤样式文件<br /> e.cssUrl.push('./test-skin.css');<br /> }<br /> }<br />});<br />// 日期部分渲染前 支持动态获取数据<br />testCalendar.on('beforeDateRender', function (e) {<br /> var startDate = e.startDate,<br /> endDate = e.endDate;<br /> // 如果需要动态获取数据<br /> // 则将获取数据的ajax加到事件对象的ajax属性上即可<br /> // 日期渲染的cellRender事件将在ajax成功获取数据后执行<br /> e.ajax = $.ajax({<br /> url: 'getDateInfo.xxx',<br /> // 将当月视图的开始和结束时间传递过去<br /> data: {<br /> start: startDate,<br /> end: endDate<br /> }<br /> });<br />});<br />// 控制渲染过程 可插入任意内容或修改原来的内容<br />testCalendar.on('cellRender', function (e) {<br /> if (!e.isHeader) {<br /> // 如:周五周六则插入周末 否则插入工作日<br /> e.extraHtml = '<div>' + (e.isoWeekday > 5 ? '周末': '工作日') + '</div>';<br /> }<br />});<br />
总结
以上就是关于一个月视图日历控件核心步骤了。
此日历实现基于一个控件基类扩展而来,其必要功能仅为一套事件机制,可参考 实现一套自定义事件机制
上面只分析了关键步骤,和核心代码,为了方便使用和扩展性,实际代码中还要处理很多问题。源码和文档如下,感兴趣可以阅读: 月视图日历
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。