本文作者:领创集团前端工程师 唐冬
前言
在网页、APP中,日历可以说是非常常见的一个东西了,例如macbook自带的日历:
那么作为一个常年与图形界面打交道的前端er是否思考过它是如何实现的呢?这就要用到js的内置日期对象Date了,不断的操作Date对象来输出任意年月日到页面中。本文将使用原生js来实现一个常见的日历,理解并可以利用到复杂的场景中。
功能分析:在当前面板需要展示年月日信息,且今日的格子需要高亮,右上角有上个月、下个月还有今日按钮,点击分别切换到上个月、下个月数据,点击今日按钮快速跳回到今日面板。点击日期格子,则该格子会被选中,如果点击的日期格子不属于当前月份,则直接跳入它所属于的月份。日期格子的排列顺序为,开头为上个月的(灰色格子),中间为当前月的(高亮格子),尾部格子为下个月的格子(灰色格子)。
<div class="calendar-wrap">
<!-- 顶部按钮区域 -->
<section class="head">
<span id="year"></span><span id="month"></span>
<div class="btn-wrap">
<span id="btn-previous">Previous Month</span>
<span id="btn-today">Today</span>
<span id="btn-next">Next Month</span>
</div>
</section>
<section class="content">
<!-- 星期 -->
<section class="week">
<div>Sun</div>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
</section>
<!-- 日期 -->
<section class="date-number">
<ul id="number-wrap"></ul>
</section>
</section>
</div>
<ul id="number-wrap"></ul>
里面的内容即为日期小格子。现在先获取我们需要用到的页面元素,并且先将需要的月份名称装起来。const yearElement = document.getElementById('year'); const monthElement = document.getElementById('month');
const ulElement = document.getElementById('number-wrap');
const previousBtn = document.getElementById('btn-previous');
const todayBtn = document.getElementById('btn-today');
const nextBtn = document.getElementById('btn-next');
const monthArray = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
记录下当前的年月日,以及面板的年月,还有被选中的年月日。 // 记录当前年月日
const nowDate = new Date();
const nowYear = nowDate.getFullYear();
const nowMonth = nowDate.getMonth();
const nowDay = nowDate.getDate();
// 记录面板年月
let panelYear = nowYear;
let panelMonth = nowMonth;
// 记录被选中年月日
let selectedYear = null;
let selectedMonth = null;
let selectedDay = null;
const renderBoxFn = (initDate = null) => {
const currentDate = initDate || nowDate;
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
currentDate.setDate(1);
// 需要渲染多少个上个月的格子
const currentFirstWeek = currentDate.getDay();
currentDate.setDate(0);
// 上个月的年份与月份
const previousYear = currentDate.getFullYear();
const previousMonth = currentDate.getMonth();
// 上个月多少天
const previousDateNumber = currentDate.getDate();
// 设置成下个月
currentDate.setDate(63);
// 下个月的年份与月份
const nextYear = currentDate.getFullYear();
const nextMonth = currentDate.getMonth();
// 设置成当前月
currentDate.setDate(0);
// 当前月份多少天
const currentDateNumber = currentDate.getDate();
// 当前月份最后一天星期几
const currentDateWeek = currentDate.getDay();
// 需要渲染下个月的格子是多少个
const nextDeltaNumber = 6 - currentDateWeek;
}
const renderBoxFn = (initDate = null) => {
// ....省略前面代码
// 生成一个文档碎片,挂载li元素
const fragment = document.createDocumentFragment();
// 所有li元素都是append追加到ul元素下,所以每次挂载前都清空以前的元素
ulElement.innerHTML = null;
// 生成并挂载上个月的li
for (let i = currentFirstWeek; i > 0; i --) {
let previousLi = document.createElement('li');
previousLi.innerText = previousDateNumber - i + 1;
previousLi.classList.add('previous-box');
// 将年月日信息保存在li的store当中,点击的时候方便读取,store是自己随意定义的名称
previousLi.store = {
year: previousYear,
month: previousMonth,
date: previousDateNumber - i + 1,
};
fragment.appendChild(previousLi);
}
// 挂载元素
ulElement.appendChild(fragment);
}
const renderBoxFn = (initDate = null) => {
// ....省略前面代码
// 生成并挂载当前月的li
for (let i = 1; i <= currentDateNumber; i ++) {
let currentLi = document.createElement('li');
currentLi.innerText = i;
currentLi.store = {
year: currentYear,
month: currentMonth,
date: i,
};
// 渲染到了今年今月今日就加个is-today的class类,做个标记
if (nowYear === currentYear && nowMonth === currentMonth && nowDay === i) {
currentLi.classList.add('is-today');
}
// 渲染到了被选中的年月日则加个is-selected的class类,标记一下
if (selectedYear === currentYear && selectedMonth === currentMonth && selectedDay === i) {
currentLi.classList.add('is-selected');
}
fragment.appendChild(currentLi);
}
}
const renderBoxFn = (initDate = null) => {
// ....省略前面代码
// 生成并挂载下个月的li
for (let i = 0; i < nextDeltaNumber; i ++) {
let nextLi = document.createElement('li');
nextLi.innerText = i + 1;
nextLi.classList.add('next-box');
nextLi.store = {
year: nextYear,
month: nextMonth,
date: i + 1,
};
fragment.appendChild(nextLi);
}
}
补齐了最后一行日期格子,完整日历出来了:
// 点击li,事件代理到ul上
ulElement.onclick = (e) => {
const { year, month, date } = e.target.store;
// 如果点击的li是已经被选中的,且是当前面板的年月,则不必再render面板日期
if (year === selectedYear
&& month === selectedMonth
&& date === selectedDay
&& panelYear === year
&& panelMonth === month
){
return null;
}
selectedYear = year;
selectedMonth = month;
selectedDay = date;
if (panelYear === year && panelMonth === month) {
coverCurrentLiFn(year, month, date);
return null;
}
renderBoxFn(new Date(year, month, date));
panelYear = year;
panelMonth = month;
yearElement.innerHTML = year;
monthElement.innerHTML = monthArray[month];
}
// 覆盖当前的li
const coverCurrentLiFn = (yearClick, monthClick, dateClick) => {
const queryAllLi = document.querySelectorAll('#number-wrap li');
Array.from(queryAllLi).forEach((li) => {
const { year, month, date } = li.store;
if (yearClick === year && monthClick === month && dateClick === date) {
li.classList.add('is-selected');
} else {
li.classList.remove('is-selected');
}
})
}
// 上个月previousBtn.onclick = () => { const preDate = new Date(panelYear, panelMonth, 0); yearElement.innerHTML = panelYear = preDate.getFullYear(); panelMonth = preDate.getMonth(); monthElement.innerHTML = monthArray[panelMonth]; renderBoxFn(preDate);}// 下个月nextBtn.onclick = () => { const nextDate = new Date(panelYear, panelMonth, 32); yearElement.innerHTML = panelYear = nextDate.getFullYear(); panelMonth = nextDate.getMonth(); monthElement.innerHTML = monthArray[panelMonth]; renderBoxFn(nextDate);}
当页面打开的时候就需要展示一下面板年月日,默认就是当前日期的年月数据,所以我们当页面加载后就初始化渲染一下;当点击今日按钮的时候,也相当于重新渲染了一下面板,所以给今日按钮绑定初次渲染函数;
// 重置或首次渲染 const initRenderFn = () => {
panelYear = nowYear;
panelMonth = nowMonth;
yearElement.innerHTML = nowYear;
monthElement.innerHTML = monthArray[nowMonth];
renderBoxFn();
}
// 页面加载完成后调用
initRenderFn();
// 点击今天
todayBtn.onclick = initRenderFn;
总结
依靠操作js内置的Date对象来规避平年、闰年,某月有多少天,上个月与下个月有多少天,某一日期所处星期几等问题;
对于Date对象来说,new出来的实例,已经可以获得默认或者指定年月日的信息,设置天(setDate)如果为负数,则得到了上个月的日期对象,同理设置个比较大(大于当前月的天数),则得到了下个月的日期对象;再次操作这些对象,又可以得到上上个月或者下下个月的日期对象,如此往复变成万年历;
一个小格子代表一天,也代表一个li元素,在生成这个li元素的时候,给这个元素添加key-value,可以很好的区分他们,后面点击的时候可以取到值,这是挂载信息的一种方式,有比较多的应用场景。
获取当前年、月、日;
把当前日期设置成1号,获取1号所在的星期几,即从第几个格子开始写入当月日期;
然后减去1天,得到上个月最后一天日期,即上个月总天数;
把日期对象设置成下个月1号,然后减去1天,得到这个月最后一天日期,即为这个月总天数;
先渲染上个月末尾的几个格子,然后再渲染这个月的所有日期格子,最后渲染下个月的开头几个格子,它们组成了完整的面板展示日期;
拓展方向
关于领创集团