前言:最近想练习一下JS的API,经过再三思考,自认为用原生JS写UI组件是一个好方法,理由有:
a. 熟悉大量原生API,像什么字符串,数组,DOM操作是肯定跑不了的
b. 可以锻炼逻辑思维能力。插件的需要基本一样,要实现什么功能不用自己太多考虑可以把主要精力放在功能实现上面,即代码层面的分析和解决问题能力
c. 可以加强自己的信心。写组件就是对自己前端能力的综合锻炼,涉及的知识面会很广。
昨天和今天花了点时间写了个日历组件,下面就对日历组件做一个小结吧。
一: 日历组件需求(要实现的功能)
a. 界面仿window任务栏日历(页面构建相关的东西)
b. 某年某月的日期能正常显示(某月有多少天,某一天是星期几)
c. 上一月,下一月切换功能
d. 假日提示功能,能自定义节假日
最近效果如下图:
二:问题拆分及解决过程
问题a: 界面构建问题
解决办法:通过分析windows任务栏日历,发现其界面就是一个 7*6 的固定的格子块。像这样较多数据展示当然用table了。然后thead里面的th来展示星期,caption来展示当前年月。考虑到每天有选中,hover等效果所以在td里面多使用了a标签(根据经验,是不推荐直接使用td裸标签,因为td的display属性是table-cell,其样式控制不够好)
最终的html结构(因为考虑到文章篇幅,html结构使用了Emmet[前身是zenCoding]伪代码)
<table>
<caption>2014年1月6日</caption>
<thead>
tr>th*7
</thead>
<tbody>
tr*6>td*7>a[href=javascript]
</tbody>
</table>
问题b: 参数设计
解决办法:因为以前写过一些基于jQuery的插件,所以参数上也使用了jQuery插件常用的默认加自定义的方式.因为没有使用JS库,所以在Canlendar上面挂了一些工具方法.
function Canlendar(opts) {
var $ = Canlendar;
var defaults = {
dateInput: $.query('#j-canlendar-date-input'),// 单击显示日历的trigger
container:$.query('#j-canlendar-container'), //日历的container
btnPrevMonth:$.query('#j-canlendar-prev-month'), //上一月按钮
btnNextMonth:$.query('#j-canlendar-next-month'), //下一月按钮
dateFormatStr:'{year}年{month}月{date}日',//格式化输出的日期
customFestival: [{
date:'3,04',//那一天,格式严格按此
name:'自定义假日名称' //节日名称
}] //自定义节假日,如某人生日等,对象数组
};
this.opts = $.extend(defaults, opts, true); //默认参数会被自定义参数覆盖,这种设计基本在每个jQuery插件中都可以看到
this.init();
}
Canlendar.query = function(selector) {
return /^\#/.test(selector) ? document.getElementById(selector.substr(1)) : document.querySelectorAll(selector);
};
Canlendar.extend = function(target, source, dep) {
var key;
if(dep) {
for(key in source) {
if(source.hasOwnProperty(key)) {
target[key] = source[key];
}
}
} else {
for(key in source) {
if(source.hasOwnProperty(key) && target[key] != null) {
target[key] = source[key];
}
}
}
return target;
};
Canlendar.removeClass = function(element, className) {
if(element.className.indexOf(className) < 0) return;
element.className = element.className.replace(/(\w+)/gm, function(match) {
return match === className ? '' : match;
});
};
Canlendar.addClass = function(element, className) {
if(element.className.indexOf(className) > 0) return;
element.className = element.className === '' ? className : element.className + ' ' + className;
};
问题c:如果正确显示每一个月的天数,即某月有多少天,某一天是星期几.
解决办法:通过分析发现:
1. 1个月最多有31天,而我们有7*6=42个格子(最多能显示42天),所以只需要找到一个合适的开始位置"把把每月天数(从1连续的超过31的整数)放入格式即可.
2. 上面所提到的"合适的开始位置"即把当前月的1号与星期给对应上
3. 做一些恰当优化,因为研究windows日历会发现,如果某月1号是星期天,那么"合适的开始位置"其实是第二行的第一个位置,而不是第一行的第一个位置
通过上面分析,我们发现多出了两个要解决的问题:
1. 某年某月有多少天;
2. 某月的1号是星期几
问题解决代码如下:
//获取某月1号是星期几。实现方法是构建一个日期对象,通过调用 getDay()方法对到当前日期是星期几
function getStartPos() {
return new Date(dateStr.replace(/(\d+)$/, '1')).getDay();
}
//获取某月有多少天。2月的润年是29天,平年28天,还有就是1,3,5,7,8,10,12月是31天
function getMonthDaysNum() {
var year = c.year;
//return new Date(c.year+"/"+ (c.month+1) +"/0").getDate();
//js中的月份从0开始,现实中从1开始
if(c.month === 1) {
return (year%4==0&&year%100!=0)||(year%400==0) ? 29 : 28;
}else if(~'0 2 4 6 7 9 11'.indexOf(c.month+'')) {
return 31;
} else {
return 30;
}
}
问题d: 如果实现自定义节日
解决办法: 对Canlendar类(型)增一个域:festival,这个域的产生会取默认与自定义节日的一个并集,是一个对象数组.在每渲染日历后,会根据此数组对td里面的a标签加一个date-festival的自定义属性,然后使用a::after伪元素来显示此属性
//节假日对象数组
this.festival = [
{date:'4,01', name:'愚人节'},
{date:'5,01', name:'劳动节'},
{date:'6,01', name:'儿童节'},
{date:'8,01', name:'建军节'},
{date:'10,01', name:'国庆节'}
].concat(this.opts.customFestival);
/*使用::after显示元素的某个属性值,并通过定位来显示在当前Hover a元素的旁边*/
.canlendar td a::after{
z-index: 10;
display: none;
position: absolute;
white-space: nowrap;
bottom:-15px;
left:0;
background-color: #000;
color: #f00;
content: attr(date-festival);
font-size: 12px;
line-height: 16px;
height: 16px;
}
.canlendar td:hover a::after {
display: block;
}
问题e:其它
1. 每个Canlendar对象有一个_canlendar属性,包含了四个属性.始终根据此对象去计算并渲染日历
{
date: c.getDate(), //日期
day: c.getDay(), //星期几
year: c.getFullYear(), //年份
month: c.getMonth() //月分
}
2. 此组件的核心方法是render方法,在所有的与DOM相关的操作都在这里面.此方法会在组件初始化里自动调用一次.后面当上一月或下一月被点击要切换月数时也会调用此方法
3. 在放多API设计上采用了jQuery的思想,把setter,getter合到一个API上面,只有一个参数是getter,两个参数是setter.如前面提供的month()方法
三:后记
当然由于这只是个练习,所以也只是实现了一个简单的日历.像常见的双日历(双日历之间的通信)以及其它一些Bug还需要后续不断完善.不过到此还是有个日历的样子.源文件下载地址:日历组件练习,由于没有考虑浏览器兼容性,所以请在高版本浏览器里面查看效果.推荐使用chrome,firefox。