不干活,摸鱼,就是玩..
项目不着急的时候就想搞点新花样给自己网站上增点彩,于是乎便看上了 gitee 上的贡献日历,一开始还真没想着自己搞,嫌麻烦想找现成,但是 goo 一圈了之后又回到了原点,虽然有那几个类似,但都不是我想要设计 UI 效果,经常逛豆瓣看电影或者设置想看的书的时候,个人书影栏目会有一个那样的展示日历,效果符合预期,开整。
功能需求
- 需要有一个横向日历展示(...)。
- 日历需要展示
hover
当天的日期和发文次数。 - 倒叙排列的日历需要去掉前后多余的日期,且有一个开始的时间。
- 日历需要年份展示,且有横向滚动监听年份变化处理。
目前好像就只有四个比较突出的功能点,一些不重要的细节就不写在上面了,为了实现上面的功能,我就上面展开一些思考的问题。
- 从
css
的布局的角度去考虑怎么设计? - 如何去做日历的倒叙排列?
- 一个月中的开始时段和结束时段的前后日期如何去处理?
- 怎么去监听日历的横向滚动变化,从而设置年份的变化?
这是我开始写之前面对这几个问题,也折腾的几天,下面针对着这几个问题一个个击破。
从css的布局的角度去考虑怎么设计?
从 gitee 上f12看到的代码写法是 div
式的布局,但是看另外的 goo 到的是使用的 svg
格式的写法。两者有优缺点, div
布局简单粗暴,要多少个就写多少个,但缺点是很多繁杂的 dom
操作和遍历;而 svg
则是需要定位来布局,缺点是计算定位的距离等。 svg
我没尝试过,计算搞来搞去头大,索性就直接用 div
加 flex
梭哈得了。
容器布局基本长这样:
<div class="git">
<div class="git-box">
<div class="git-left"></div>
<div class="git-right">
<div class="git-wrap">
<div class="git-content">
<div class="month">
<div class="year">年份</div>
<div class="title">月份</div>
<div class="week">
<div class="day">1号</div>
</div>
<div class="week"></div>
</div>
</div>
<div class="git-year">2021</div>
</div>
</div>
</div>
<div class="git-color"></div>
</div>
布局很好理解,一个容器里面,包含左边的日期周几,右边的滚动的容器,里面包含需要滚动的月份,其中月份里面会有 title
和 year
年份设定,为了之后获取方便。设定好布局结构之后就开始写我们常用的插件模式啦。这里有个细节的布局是,为了不让滚动条在容器内影响美观,特意在多写了一个盒子,撑开父级的高度,让滚动条居于容器外部。
说到写插件这个地方,我多说一句,最近在面试的过程中也会问有没有自己写过插件,这时候有很多人都说没有写过,说写过一写组件的封装,这其实是考验面试着自己的学习能力和动手能力,简单的功能组件封装是很浅的一部分(复杂的当我没说...),至少也要自己动手做过一些。这里我就写一个简单的插件封装格式,不会的同志可以直接照葫芦画瓢,搞里头!
基本格式如下:
(function(window){
// params 标示传过来的参数
var Git = function(params) {
// 可以使用 extend 重新拷贝一份
this.extend(this.params, params)
}
Git.prototype = {
// 这里就是默认参数啦,如果new 的过程没有传入参数,就是用这里的参数即可
params: {
data: null
},
...
// 简单的拷贝
extend: function (a, b) {
for (var key in b) {
if (b.hasOwnProperty(key)) {
a[key] = b[key]
}
}
return a
}
}
window.Git = Git;
})(window)
// 这样就可以在外面直接, new Git()
现在好了,布局和插件格式都已经处理完成,开撸!
如何去做日历的倒叙排列?
在做排序之前,我想到一个问题。优先看到是今天的日期,滚动到后面看到的是开始的节点,如果是倒着排列,那么必然日历也要倒着排列,因为从 querySelectorAll('.day')
获取的日期的时候就是从前往后获取的而不是从后面开始获取,所以日历也需要倒着排列。
接下来要获取一下今天的日期到开始的日期的时间月份差,这样就可以知道要循环几次的月份,再循环的过程中,使用 insertAdjacentHTML
的方法传入参数是 afterbegin
,在文档的最前面添加MDN地址 。
获取循环得到的时间
// 获取开始日期的时间
var date = new Date(startDate)
// 设置当前月份的Date,跟随月份递增
date.setMonth(date.getMonth() + i);
var year = date.getFullYear();
var month = date.getMonth();
获取当前月份的是从哪个日期开始的
var setCurrentDate = new Date(year, month, 1);
var firstDay = setCurrentDate.getDay();
设置每个月的月份,这里使用的是索引 0,而不是 i 是因为倒叙的关系,只获取第一个 dom 元素
element.querySelectorAll('.title')[0].innerHTML = monthMap[month]
在获取一年之中的月份日期数,这里有一个需要判断的地方是,闰年2月为28的情况。
var months = [31, this.isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// 判断 平年闰年[四年一闰,百年不闰,四百年再闰]
function isLeapYear(year) {
return (year % 4 === 0) && (year % 100 !== 0 || year % 400 === 0);
}
获取一个月内有多少周 (一周7天,所以要除以7),这样就可以设置对应月份里面 week
var weeks = Math.ceil((firstDay + months[month]) / 7);
一个月中的开始时段和结束时段的前后日期如何去处理?
有了当月的开始时间,那也必然要获取到上个月的时间天数了,这样才可以把当月之前上个月的日期给补上。比如说,这个7月开始的时间1号对应的是第一周的周四,那么获取上个月的是时间只有30天,那就可以在周三的时候30循环--直到27号,这样的话最后的一周可以去掉。
var lastMonth = (month - 1 >= 0) ? (months[month - 1]) : 31;
// 设置每个月有多少周,最后减一是为了去掉最后一周,因为在第一周的时候加上了上个月的时间
// 这里有个注意的地方是,如果当月的最后一天如果是周日,就不需要再去减掉最后一周,可以直接显示出来
// 这里的len 是表示最后需要截止多少日
date.setMonth(date.getMonth() + 1);
var lastDay = new Date(date.setDate(0)).getDay();
var len = 0;
if (i !== diffMonth - 1 && lastDay !== 6) {
weeks = weeks - 1;
len = 0;
} else {
len = 7 - lastDay;
}
for (var j = 0; j < weeks; j++) {
var whtml = '<div class="week">' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'<div class="day less"></div>' +
'</div>'
element.querySelectorAll('.month')[0].insertAdjacentHTML('beforeEnd', whtml)
}
以上的过程都走完了,就会的到一个 month
的节点,再通过这个节点去找到下面所有的 day
节点,最后再通过反向操作逐个添加相应的属性和对比数据。具体看代码:
var days = element.querySelectorAll('.month')[0].querySelectorAll('.day');
// 保存临时变量使用
var _firstDay = firstDay;
var _month = month;
var _year = year;
// 每日的日期
var day = 0;
for (var m = days.length; m > len; m--) {
day++
var d = --_firstDay;
// 因为是倒叙,所以就减 1 来获取索引
var dayIndex = m - 1;
// 获取上个月的最后一周的时间段 天数
var lastDays = dayIndex - firstDay;
// 给当月的 1号 的前面加上 上个月的日期
if (m > days.length - firstDay) {
// 去掉第一个月的第一周里面的上一个月的末尾日期
if (i === 0) {
days[dayIndex].classList.add('none')
}
// 当时间到 12 月时需要重新设置一下年份和月份
if (month === 0) {
_month = 12
_year = year - 1
} else {
_month = month
_year = year
}
var num = this.parseDate(days[dayIndex], _year, _month, lastMonth - d)
days[dayIndex].setAttribute('data-time', _year + '-' + _month + '-' + (lastMonth - d))
days[dayIndex].setAttribute('data-tit', num + '次发文:' + _year + '-' + _month + '-' + (lastMonth - d))
}
if (!days[lastDays]) break;
var num = this.parseDate(days[lastDays], year, month + 1, day)
days[lastDays].setAttribute('data-time', year + '-' + (month + 1) + '-' + day)
days[lastDays].setAttribute('data-tit', num + '次发文:' + year + '-' + (month + 1) + '-' + day)
// 去掉最后一个月之中从当前日期往后的日期
if (i === diffMonth - 1) {
if (day > currentDate.getDate()) {
days[lastDays].classList.add('none')
}
}
}
这里的 parseDate
是比较函数,根据传值过来的 data
数据和日期比较,如果时间相等就添加次数加 1,最后返回次数,添加到自定义属性中,方便后续的获取。
在使用自定义属性时,为了使鼠标可以 hover
展示数据就使用了伪类处理,通过 attr()
函数来获取的节点上的相关属性,这样也方便操作不需要重新在 JS
中写提示相关的代码。有一个小细节,这里在赋值 setAttribute('data-tit', '我是一只小小鸟!')
属性去通过 attr()
获取时,我本想是加入换行的字符,但是不管如何去操作都不得成功。通过查阅资料和操作得知,只有直接写在节点上的属性加上 A 才可以实现换行,这里我用的另外一个方法,那就是在content 中获取两个属性,中间用 A 把这两个拼接起来,结果是完美的。
// 1
<div data-tit="哈哈哈\A啦啦啦"></div>
// 2
<div data-tit="哈哈哈" data-time="啦啦啦"></div>
<style>
div::before {
content: attr(data-tit)
}
// 这里不需要加上 + 号
div::after {
content: attr(data-tit)"\A"attr(data-time)
}
</style>
怎么去监听日历的横向滚动变化,从而设置年份的变化?
最后一步监听滚动的变化也好处理,写一个监听事件,然后获取这个节点滚动的 scrollLeft
属性。先把每一个年份出现的节点距离左边的距离拿到放进一个数组里面,在滚动的时候去监听滚动的具体来判断是哪个年份的中间,最后添加其给定的样式即可。具体代码:
var allMonths = document.querySelectorAll('.month');
var yearList = [];
var listWidth = [];
for (var q = 0; q < allMonths.length; q++) {
var item = allMonths[q]
if (item.querySelector('.year').getAttribute('data-year')) {
listWidth.push(item.offsetLeft);
yearList.push(item);
}
}
// 偏移量,最好 0 - 10 之间
var distance = 8;
this.addEvent(document.querySelector('.git-wrap'), 'scroll', function (e) {
var scrollX = e.target.scrollLeft;
for (var i = 0; i < listWidth.length; i++) {
var width1 = listWidth[i];
var width2 = listWidth[i + 1];
if (scrollX > width1 && scrollX < width2) {
if (width2 - scrollX < TITLE_WIDTH + distance) {
document.querySelector('.git-year').innerHTML = yearList[i + 1].querySelector('.year').getAttribute('data-year')
yearList[i + 1].querySelector('.year').classList.add('active')
} else {
document.querySelector('.git-year').innerHTML = yearList[i].querySelector('.year').getAttribute('data-year')
yearList[i + 1].querySelector('.year').classList.remove('active')
}
}
}
})
结束
完结撒花?
以上差不多都算是完成了本次的代码插件化,有很多也是边做边查,一开始不是很顺利,但是做的这个过程痛并快乐,一是奇怪的姿势又涨了一点,二是又可以把做的分享给大家。
纯JS手写,懒得用es6了,写的不好,轻一点...
代码实地查看,已上传 github ,觉得还行给个鼓励。