自定义的Calendar类实现
这是在之前的封装的DateTime类的基础上,实现一个扩展日历控件。具体预览图如下,我这里的日历控件是用于一个将要实现的用户日程安排的功能模块,所以还有实现一个简单的Schedule类,将新的日程信息推送到日历底部的日程队列,这里的主题是说如何实现一个日历控件,下面的文章因篇幅的问题,留到有空时再写另外一文,Schedule的实现。
日历控件预览图日历控件基本的功能:
- 显示当前日期
- 遍历当前日期
- 日程提示
首先,在这里我们先用html模版定义一个日历控件的大体轮廓,由于我服务器是基于flask和jinja2模版引擎,因此设计好的日历控件模版可用于流行的python全栈框架,例如Flask和Pyramid.
<div class="calendar">
<div>
<div class="switch">
<button class="prev-month"><i class="fa fa-chevron-circle-left arrow"></i></button>
<div class="date"><span class="year"></span>年<span class="month"></span>月</div>
<button class="next-month"><i class="fa fa-chevron-circle-right arrow"></i></button>
</div>
<table id="ynCalendar" class=".table-sm">
<thead>
<th>日</th>
<th>一</th>
<th>二</th>
<th>三</th>
<th>四</th>
<th>五</th>
<th>六</th>
</thead>
<tbody>
{% for week in range(6) %}
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="calendar-footer">
{% include '_parts/schelist.html' %}
</div>
</div>
</div>
构造方法的初始化
其中构造函数用于初始化的变量,render方法和注册事件handler,如下图所示。
屏幕快照 2019-07-24 下午9.44.26.png
constructor的具体实现,一个Calendar实例会持有一个的DateTime的实例,用于完成各项参数的初始化。整个Calendar类核心的方法是render方法用于渲染日历到页面。
class Calender extends EventManager{
constructor(selector) {
super(true);
this.table = document.querySelector(selector);
this.days = this.table.querySelectorAll('tbody tr td');
this.title = document.querySelector('div.date');
this.yearSpan = this.title.querySelector('span.year');
this.monthSpan = this.title.querySelector('span.month');
this.curDate = new DateTime();
this.yearSpan.innerHTML = this.curDate.year;
this.monthSpan.innerHTML = this.curDate.month;
this.month = this.curDate.month;
this.weekname = DateTime.WEEK_SHORT_CN_NAME;
this.today = this.curDate.date;
this.prevButton = document.querySelector('button.prev-month');
this.nextButton = document.querySelector('button.next-month');
this.render(this.curDate.year, this.month);
this.is_today();
this.init_event_handlers();
}
}
render方法的实现
所有的日历控件的实现是大同小异的,首先日历多数是以42格的呈现的,这也是我上面提到的jinja2模版的具体实现。render方法需要分解成三个子问题。
- 显示当前日期的天数(最大31天)
- 显示上个月末参数的天数。
- 显示下个月初刻预览的天数。
显示当前日期的天数(最大31天)
首先我们来解析第一个子问题,要在日历中显示整个月份的,当然要找到当前日期月初第一天对应的星期名称,和月末最后一天的的星期名称,例如:
- 图中7月1日对应的星期一,我们可以用DateTime类的what_day静态方法获取当前月初对应的星期名,这里用局部变量startday表示,
let startDay = DateTime.what_day(year, month, 1);
然后通过startDay可以获取月初位于Calendar类的持有的days数组中所在的位置。在render方法中用一个局部变量s来表示。
let s = this.weekname.indexOf(startDay);
- ok,同理我们可以用相同的逻辑获取 7月31日对应的星期三在days数组中的月末位置,这里用局部变量e表示。
let e = DateTime.last_day(year, month);
知道月初和月末的索引位置之后,接下来就是填充table的单元格。局部变量i还要用于关联下面要计算“下个月初的位置”,所以独立于for循坏外。
let i = 1;
for (; i <= e; s++, i++) {
this.days[s].innerHTML = i;
this.days[s].className = 'this-month';
}
接下来,接解决第二个中,就是要显示上个月末残余的日期。
左上角上个月末参数的天数
由于已经知道月初s的索引,和月末e对应的位置,那么就是如下所示。局部变量r也是关联到计算“下个月初的位置”,所以也是放到for循环外。
let p = (new DateTime(year, month, 0)).date;
let r = 0;
for (let j = s - 1; j >= 0; j--, p--) {
this.days[j].innerHTML = p;
this.days[j].className = 'n-prev-month';
r += 1;
}
显示下个月初刻预览的天数
这里就不废话了,直接看代码
i = i + r - 1;
for (let n = 1; i < this.days.length; i++, n++) {
this.days[i].innerHTML = n;
this.days[i].className = 'n-next-month'
}
最后就是完整的render方法实现
render(year, month) {
//获取月初所在位置
let startDay = DateTime.what_day(year, month, 1);
let s = this.weekname.indexOf(startDay);
//显示上个月末残留的日期
let e = DateTime.last_day(year, month);
let p = (new DateTime(year, month, 0)).date;
let r = 0;
for (let j = s - 1; j >= 0; j--, p--) {
this.days[j].innerHTML = p;
this.days[j].className = 'n-prev-month';
r += 1;
}
//显示当前日期
let i = 1;
for (; i <= e; s++, i++) {
this.days[s].innerHTML = i;
this.days[s].className = 'this-month';
}
//显示下个月初可预览的天数
i = i + r - 1;
for (let n = 1; i < this.days.length; i++, n++) {
this.days[i].innerHTML = n;
this.days[i].className = 'n-next-month'
}
}
接下来,就是要实现日历的上翻和下翻的功能。这个功能属于用户行为触发的事件处理器并实例化一个表示上一个月或下一个月的DateTime的临时实例。然后将对应的year,month参数传递给render方法重新对table重新填充一遍而已。但填充之前,注意需要对table中原有的数据清理调,因此,在每次事件触发的第一个要调用的就是clean方法。
/**
* 清空日历
*/
clean() {
for (let i = 0; i < this.days.length; i++) {
this.days[i].innerHTML = '';
this.days[i].className = '';
}
}
这个是Calendar类的event handler代码,没什么需要过多解析。
/**
* 显示上一个月
* @param event
*/
prev_month(event) {
this.clean();
const prevDate = this.curDate.prev_month();
this.yearSpan.innerHTML = prevDate.year;
this.monthSpan.innerHTML = prevDate.month;
this.render(prevDate.year, prevDate.month);
event.stopPropagation();
event.preventDefault();
}
/**
* 显示下一个月
* @param event
*/
next_month(event) {
this.clean();
const nextDate = this.curDate.next_month();
this.yearSpan.innerHTML = nextDate.year;
this.monthSpan.innerHTML = nextDate.month;
this.render(nextDate.year, nextDate.month);
event.stopPropagation();
}
is_today() {
for (let i = 0; i < this.days.length; i++) {
if (this.curDate.year == this.yearSpan.textContent
&& this.curDate.month == this.monthSpan.textContent) {
if (this.curDate.date == this.days[i].textContent) {
this.days[i].className = 'today';
break;
}
}
}
}
init_event_handlers() {
this.listen(this.prevButton, 'click', this.prev_month);
this.listen(this.nextButton, 'click', this.next_month);
}
值得提一下的是,这里的事件处理用到我之前写的EventManager类,可以看这里https://www.jianshu.com/p/9a8c5cf4db84。
写在最后,可能有人看完又会说重造轮子,可能有些读者没搞清我最近写文的用意,就以本文为例,最近项目经常用到一些自定义的插件,当然很多jq库和vue库的插件都可以用,但那些都是基于他人代码写出来,当遇到不能满足自己的需求,或等到api变更了,你非得还要基于这些库二次实现一次累不累?我有这时间倒不如用简洁的原生es语法自己写符合自己需求的插件。反正用原生的js语法写出来的插件,写放到啊猫啊狗框架都可以用....当然日积月累,久而久之就形成自己一套常用的js库。