首发个人博客 - https://blog.cdswyda.com/post/2017121010css
日历控件多的不胜枚举,为何咱们还要再造一个轮子呢?html
由于大多很多天历控件都是用于选择日期的,有种需求是要在日历上展现各类各样的内容,这样的日历控件较少,并且试用下来并不满意。git
所以就再造一个轮子,如今带你一块儿基于使用以前完成的组件机制来开发一个日历控件。github
需求ajax
简单把需求整理以下:浏览器
首先咱们拿系统中自带的日历观察一下,看看日历的特征究竟是怎么样的。app
一个月中有 28 到 31 天不等,可是为了保证完整的结构,日历中会有部分上一月和下一月的日期,总结下来,一个月中显示的一定是整整6周的日期。dom
那么只要获得当月的开始日期就能够绘制日历了。post
如何计算当月日历视图中的开始日期呢? 前面已经分析了,为了保证完整,它显示了上一月的部分天数,那么只用从当月的1号开始往前推算就能够了。this
开始日期 = 当月1号的日期 - 当月1号的星期 结束日期 = 开始日期 + 42天
这个问题搞清楚了,感受实现这么一个日历就没什么大阻碍了,开始动工吧!
首先构建以下所示的基本结构
其中:
主体区域中用绘制整个日历
在初始化好日历结构后就能够开始绘制日历了。
首先完成开始和结束时间的计算
{ // 初始化当前月份的开始日期和结束日期 _initStartEnd: function () { // 当月1号 var currMonth = moment(this.currMonth, 'YYYY-MM'), // 当月1号是周几 the ISO day of the week with 1 being Monday and 7 being Sunday. firstDay_weekday = currMonth.isoWeekday(), startDateOfMonth, endDateOfMonth; if (!this.dayStartFromSunday) { // 开始为周一 则向前减小周几的天数-1即为 开始的日期 startDateOfMonth = currMonth.subtract(firstDay_weekday - 1, 'day'); } else { // 开始为周日 则直接向前周几的天数便可 startDateOfMonth = currMonth.subtract(firstDay_weekday, 'day'); } endDateOfMonth = startDateOfMonth.clone().add(41, 'day'); this.startDateOfMonth = startDateOfMonth; this.endDateOfMonth = endDateOfMonth; } }
因为要处理不少日期,而JavaScript中关于日期处理时,不一样浏览器下差别较大,所以直接使用 moment.js 来对日期进行统一处理。
因为使用习惯不一样,一周的开始究竟是周一仍是周日是不肯定的,所以直接做为配置便可。
上面已经计算获得了一个月的开始日期和结束日期,那么只用遍历进行绘制便可。
因为咱们使用了表格实现,所以须要按行绘制。
实现以下:
{ // 日历可变部分的渲染 _render: function () { this._initStartEnd(); var weeks = 6, days = 7, curDate = this.startDateOfMonth.clone(), tr; var start = this.startDateOfMonth.format('YYYY-MM-DD'), end = this.endDateOfMonth.format('YYYY-MM-DD'); // 清空 并开始新的渲染 this._clearDays(); this._renderTitle(); for (var i = 0; i < weeks; ++i) { tr = document.createElement('tr'); tr.className = 'ep-calendar-week'; this._daysBody.appendChild(tr); for (var j = 0; j < days; ++j) { // 渲染一天 并递增 this._renderDay(curDate, tr); curDate.add(1, 'day'); } } }, // 天天的渲染 _renderDay: function (date, currTr) { var td = document.createElement('td'), tdInner = document.createElement('div'), text = document.createElement('span'), day = date.isoWeekday(), // 返回的月份是0-11 month = date.month() + 1; tdInner.appendChild(text); td.appendChild(tdInner); td.className = 'ep-calendar-date'; tdInner.className = 'ep-calendar-date-inner'; // 完整日期 td.setAttribute('data-date', date.format('YYYY-MM-DD')); // 对应的iso星期 td.setAttribute('data-isoweekday', day); // 周末标记text.className if (day === 6 || day === 7) { td.className += ' ep-calenday-weekend'; } // 非本月标记 // substr 在ie8下有问题 // if (month != parseInt(this.currMonth.substr(-2))) { if (month != parseInt(this.currMonth.substr(5), 10)) { td.className += ' ep-calendar-othermonth'; } // 今天标记 if (this.today == date.format('YYYY-MM-DD')) { td.className += ' ep-calendar-today'; } // 天天渲染时发生 还未插入页面 var renderEvent = this.fire('cellRender', { // 当天的完整日期 date: date.format('YYYY-MM-DD'), // 当天的iso星期 isoWeekday: day, // 日历dom el: this.el, // 当前单元格 tdEl: td, // 日期文本 dateText: date.date(), // 日期class dateCls: 'ep-calendar-date-text', // 须要注入的额外的html extraHtml: '', isHeader: false }); // 处理对dayText内容和样式的更改 text.innerText = renderEvent.dateText; text.className = renderEvent.dateCls; // 添加新增内容 if (renderEvent.extraHtml) { jQuery(renderEvent.extraHtml).appendTo(tdInner); } currTr.appendChild(renderEvent.tdEl); // 天天渲染后发生 插入到页面 this.fire('afterCellRender', { date: date.format('YYYY-MM-DD'), isoWeekday: day, el: this.el, tdEl: td, dateText: text.innerText, dateCls: text.className, extraHtml: renderEvent.extraHtml, isHeader: false }); } }
直接从开始日期日后依次画出42天便可。
为了灵活性,在绘制的不一样时机触发了不一样的事件,在使用时可绑定相应的事件,在其中进行个性化操做。
也为了使用了方便和灵活性,直接在绘制日期时,在相应的dom上加入了所对应的日期和星期属性。
在此过程当中须要对日期是否周末、是否本月、是不是选中的、是不是今天等进行相应的标记处理。
除了上面所述以外此外还要绘制出年月选择、标题等,这些实际就是给已经有的dom元素中更改内容而已,就再也不展开了。
上面已经基本绘制出了一个日历,切换月份实际就更简单了,只用根据新的月份从新计算开始日期,清空原来的内容,从新进行绘制便可。
{ // 设置月份 setMonth: function (ym) { var date = moment(ym, 'YYYY-MM'); if (date.isValid()) { var oldMonth = this.currMonth, aimMonth = date.format('YYYY-MM'); // 月份变更前 this.fire('beforeMonthChange', { el: this.el, oldMonth: oldMonth, newMonth: aimMonth }); this.currMonth = aimMonth; this.render(); // 月份变更后 this.fire('afterMonthChange', { el: this.el, oldMonth: oldMonth, newMonth: aimMonth }); } else { throw new Error(ym + '是一个不合法的日期'); } } }
要处理的事件较多,此处仅仅以日期的点击做为示意。
{ // 初始化事件 _initEvent: function () { var my = this; jQuery(this.el) // 日期单元格 .on('click', '.ep-calendar-date', function (e) { var date = this.getAttribute('data-date'), ev = my.fire('dayClick', { ev: e, date: date, day: this.getAttribute('data-isoweekday'), el: my.el, tdEl: this }); // 若是修改事件对象的cancel为true后 则不进行后续的选中操做 if (!ev.cancel) { my.setSelected(date); } }) } }
因为日期所对应的dom元素始终会添加和移除,直接把事件绑定在日期的dom元素上,则必须在每次新增后从新绑定事件,十分麻烦。
直接使用事件代理机制,将事件绑定在整个日历的dom上便可,这样事件只用在建立时初始化一次便可,简单、高效、省内存。
咱们新增这个控件的主要目的就是要支持在日历中绘制任意内容,怎么使用呢?
var testCalendar = epctrl.init('Calendar', { el: '#date', // 资源加载过程当中的事件须要直接在这里指定 events: { beforeSourceLoad: function (e) { // 资源加载前,在加入咱们的皮肤样式文件 e.cssUrl.push('./test-skin.css'); } } }); // 日期部分渲染前 支持动态获取数据 testCalendar.on('beforeDateRender', function (e) { var startDate = e.startDate, endDate = e.endDate; // 若是须要动态获取数据 // 则将获取数据的ajax加到事件对象的ajax属性上便可 // 日期渲染的cellRender事件将在ajax成功获取数据后执行 e.ajax = $.ajax({ url: 'getDateInfo.xxx', // 将当月视图的开始和结束时间传递过去 data: { start: startDate, end: endDate } }); }); // 控制渲染过程 可插入任意内容或修改原来的内容 testCalendar.on('cellRender', function (e) { if (!e.isHeader) { // 如:周五周六则插入周末 不然插入工做日 e.extraHtml = '<div>' + (e.isoWeekday > 5 ? '周末': '工做日') + '</div>'; } });
以上就是关于一个月视图日历控件核心步骤了。
此日历实现基于一个控件基类扩展而来,其必要功能仅为一套事件机制,可参考实现一套自定义事件机制
上面只分析了关键步骤,和核心代码,为了方便使用和扩展性,实际代码中还要处理不少问题。源码和文档以下,感兴趣能够阅读:月视图日历