预览图

不干活,摸鱼,就是玩..

项目不着急的时候就想搞点新花样给自己网站上增点彩,于是乎便看上了 gitee 上的贡献日历,一开始还真没想着自己搞,嫌麻烦想找现成,但是 goo 一圈了之后又回到了原点,虽然有那几个类似,但都不是我想要设计 UI 效果,经常逛豆瓣看电影或者设置想看的书的时候,个人书影栏目会有一个那样的展示日历,效果符合预期,开整。

功能需求

  1. 需要有一个横向日历展示(...)。
  2. 日历需要展示 hover 当天的日期和发文次数。
  3. 倒叙排列的日历需要去掉前后多余的日期,且有一个开始的时间。
  4. 日历需要年份展示,且有横向滚动监听年份变化处理。

目前好像就只有四个比较突出的功能点,一些不重要的细节就不写在上面了,为了实现上面的功能,我就上面展开一些思考的问题。

  • css 的布局的角度去考虑怎么设计?
  • 如何去做日历的倒叙排列?
  • 一个月中的开始时段和结束时段的前后日期如何去处理?
  • 怎么去监听日历的横向滚动变化,从而设置年份的变化?

这是我开始写之前面对这几个问题,也折腾的几天,下面针对着这几个问题一个个击破。

从css的布局的角度去考虑怎么设计?

gitee 上f12看到的代码写法是 div 式的布局,但是看另外的 goo 到的是使用的 svg 格式的写法。两者有优缺点, div 布局简单粗暴,要多少个就写多少个,但缺点是很多繁杂的 dom 操作和遍历;而 svg 则是需要定位来布局,缺点是计算定位的距离等。 svg 我没尝试过,计算搞来搞去头大,索性就直接用 divflex 梭哈得了。
容器布局基本长这样:

<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>

布局很好理解,一个容器里面,包含左边的日期周几,右边的滚动的容器,里面包含需要滚动的月份,其中月份里面会有 titleyear 年份设定,为了之后获取方便。设定好布局结构之后就开始写我们常用的插件模式啦。这里有个细节的布局是,为了不让滚动条在容器内影响美观,特意在多写了一个盒子,撑开父级的高度,让滚动条居于容器外部。

说到写插件这个地方,我多说一句,最近在面试的过程中也会问有没有自己写过插件,这时候有很多人都说没有写过,说写过一写组件的封装,这其实是考验面试着自己的学习能力和动手能力,简单的功能组件封装是很浅的一部分(复杂的当我没说...),至少也要自己动手做过一些。这里我就写一个简单的插件封装格式,不会的同志可以直接照葫芦画瓢,搞里头!

基本格式如下:

(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 ,觉得还行给个鼓励。