给你的Hugo博客添加一个日历!
我又挖了个大坑…日历尚未成型,同志仍需努力💪 基本功能已实现!
之前我使用的是 jsCalendar 这个插件,但是它功能比较复杂,加载时间长;并且我还想在日历上添加其它的功能,1500行的代码看不下去,还不如自定义了!
基本日历结构
呜呜呜,我高看了自己,写 js 还是很吃力的,因为之前也没有基础。单纯的功能实现还好,但怎么让代码能运行起来呢?为了省事儿,我直接一股脑儿地把所有代码放到 window.onload = function () {}
里面,然后在 html 页面引入 js 的路径就可以让代码执行了!(随便看了看 github 里面用 js 实现的代码,貌似有一个固定的模版,好像还有什么构造器之类的,不过我现在的目标是能用就好)
代码不难,但是要搞清楚 Date() 对象各种变量的有效范围,比如说月份是 0~11 之间,星期是 0~6 之间,其中星期六=6,星期日=0。要事先想清楚渲染的效果,在写 html 结构的时候要加入对应的 class。
拆解一下日历的结构,可以总结出来重点关注两个日子:上个月的最后一天 以及 这个月的最后一天。如果上个月的最后一天是星期六,那么无需考虑上个月;同理,如果这个月的最后一天是星期六,无需考虑下个月。根据上个月是星期几,我们能推算出当前日历的第一天是几号,由此渲染完第一行。之后的几行比较简单,日子加加即可,直到遇到这个月的最后一天。此时,日子从 1 开始计算,直到该行满 7 个元素为止。
方法外部的变量
var today = new Date();
var thisYear = today.getFullYear();
var thisMonth = today.getMonth() + 1; // Note that `getMonth()` returns 0-11
var thisDate = today.getDate();
var calendar = document.getElementsByClassName('my-calendar')[0];
var tableHtml = '';
日历初始化
// the first to run, the year and month equal to this year and this month
// function: initialize the `tableHtml`
function initCalendar() {
tableHtml += '<table><thead class="calendar-head"><tr class="calendar-title"><th class="calendar-title-left">\<</th> <th colspan="5" class="calendar-title-name"><span class="cur-year">' + thisYear + '</span>年<span class="cur-month">' + thisMonth + '</span>月</th><th class="calendar-title-right">\></th></tr><tr class="calendar-week-days"><th>日</th><th>一</th><th>二</th><th>三</th><th>四</th><th>五</th><th>六</th></tr></thead>';
tableHtml += generateCalendarBody(thisYear, thisMonth);
tableHtml += '</table>';
calendar.innerHTML = tableHtml;
}
日历更新
function updateCalendar(year, month) {
// update the head
document.getElementsByClassName('cur-year')[0].innerHTML = year;
document.getElementsByClassName('cur-month')[0].innerHTML = month;
// update the body
document.getElementsByClassName('calendar-content')[0].innerHTML = generateCalendarBody(year, month);
}
日历body更新
function generateCalendarBody(year, month) {
// Note that `Date()` uses month 0-11
var lastDayOfPreviousMonth = new Date(year, month - 1, 0);
var lastDayOfCurrentMonth = new Date(year, month, 0);
var lastDayOfPreviousMonthDay = lastDayOfPreviousMonth.getDay();
var lastDayOfPreviousMonthDate = lastDayOfPreviousMonth.getDate();
var lastDayOfCurrentMonthDate = lastDayOfCurrentMonth.getDate();
var lastDayOfCurrentMonthDay = lastDayOfCurrentMonth.getDay();
// day=6 Saturday means
// the first day of this month is Sunday -> the first date in calendar is 1
var firstDayOfCalendarDate = (lastDayOfPreviousMonthDay != 6) ? lastDayOfPreviousMonthDate - lastDayOfPreviousMonthDay : 1;
var tbodyHtml = '<tbody class="calendar-content">';
var displayDate = 0; // date to display in the calendar
var isThisMonth = (year == thisYear && month == thisMonth); // check if this month
// render the first line in the calendar body
for (var i = 0; i < 7; i++) {
var tmpDate = firstDayOfCalendarDate + i;
if (firstDayOfCalendarDate == 1) {
displayDate = tmpDate;
// today class will have a different style
if (isThisMonth && displayDate == thisDate) {
tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
} else {
tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
}
} else if (tmpDate > lastDayOfPreviousMonthDate) {
displayDate = tmpDate - lastDayOfPreviousMonthDate;
// today class will have a different style
if (isThisMonth && displayDate == thisDate) {
tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
} else {
tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
}
} else {
displayDate = tmpDate;
tbodyHtml += '<td class="last-month">' + displayDate + '</td>';
}
}
tbodyHtml += '</tr><tr>'; // the end of a line & the begin of another line
displayDate++;
var daysInALine = 0;
while (displayDate <= lastDayOfCurrentMonthDate) {
// today class will have a different style
if (isThisMonth && displayDate == thisDate) {
tbodyHtml += '<td class="cur-month today" onclick="getCalendarDate(this);">' + displayDate + '</td>';
} else {
tbodyHtml += '<td class="cur-month" onclick="getCalendarDate(this);">' + displayDate + '</td>';
}
daysInALine++;
displayDate++;
if (daysInALine == 7) {
tbodyHtml += '</tr>'; // the end of a line
if (displayDate != lastDayOfCurrentMonthDate)
tbodyHtml += '<tr>'; // the begin of another line, only for current display date doesn't equal to the last day of current month
daysInALine = 0
}
}
displayDate = 1; // the first day of next month
while (daysInALine < 7 && lastDayOfCurrentMonthDay != 6) {
tbodyHtml += '<td class="next-month">' + displayDate + '</td>';
displayDate++;
daysInALine++;
}
tbodyHtml += '</tr></tbody>';
return tbodyHtml
}
前后月跳转
这里我遇到了一个困难:点击事件只能响应一次。查阅资料发现是修改了 html 内容导致,因为一开始重新加载日历时,我把整个 html 都更新了,导致动态绑定的事件失效了。于是我退而求其次,保留日历的 head 部分,只更新 body 部分。
自定义变量
var prev = document.getElementsByClassName('calendar-title-left')[0];
var next = document.getElementsByClassName('calendar-title-right')[0];
var year = document.getElementsByClassName('cur-year')[0].innerHTML; // display year
var month = document.getElementsByClassName('cur-month')[0].innerHTML; // display month
响应点击事件
prev.onclick = function () {
console.log("prev");
if (month == 1) {
year--;
month = 12;
} else {
month--;
}
updateCalendar(year, month);
};
next.onclick = function () {
console.log("next");
if (month == 12) {
year++;
month = 1;
} else {
month++;
}
updateCalendar(year, month);
};
响应日期点击事件
又碰到了一个问题:onclick 的函数没被定义,我明明写了呀?一脸疑惑。
根据这位作者的解释,我把原函数的这种形式
function dosave(){
alert("会报错!!");
}
改成了这种
dosave = function (){
alert("成功啦!");
}
然后就好了!作者说
dosave = function(){} 的写法会把 dosave 函数作为全局作用域函数
而 onclick 函数要求是全剧函数,恰好符合要求。
题外话:作为一个木有前端基础的同学,我的主要目的是为了实现想要的功能,解决自己的问题,所以都并没有查阅相关专业资料,若有问题,请多多和我讨论哈~
下面是相关代码:
getCalendarDate = function (object) {
var year = document.getElementsByClassName('cur-year')[0].innerHTML; //string
var month = Number(document.getElementsByClassName('cur-month')[0].innerHTML);//int
var date = Number(object.innerHTML);
if (month < 10) {
month = '0' + month;//string
}
if (date < 10) {
date = '0' + date;//string
}
console.log(year + month + date);
}
目前能做到提取当前点击的日期,但还没有实现跳转当日所有文章的功能。
绑定博文日期
Ummmm,现在还没想好一个好的解决方案。有以下问题需要思考 🤔
1. Hexo 貌似是事先生成一个 json 文件,绑定到日期上??还需要仔细研究研究
2. Hugo 内置根据日期排列的模版,但只能用于归档的感觉?一次性的全排列,不知道可不可以分开
3. URL 绑定日期,输入相关 url 即可访问当日所写全部博文
4. 日历区分有博文的日子和没有博文的日子: style + 如何判断当日博客数为0
5. 当日所写全部博文页面渲染
一直没想到怎么做,最近也有点忙,这个边角料的任务就放在一旁。今天在网上看到这篇博文,顿时就有了启发,下面讲一讲我怎么做的吧!
博客 config 配置
[permalinks]
post = "/:title/"
archd = "/:year/:month/:day/"
archm = "/:year/:month/"
post
这个本来是设置成 /:year/:month/:day/:title/
的,后来才发现这会影响文章间 markdown 跳转,于是我改回了只包含文章标题的模式。
archd
和 archm
分别用于按日期和月份归档。
创建对应文件夹和文件
要想能通过月份和日期访问,单单设定一个 permalinks 是不够的,我们需要把所有写文章的日子都记下来,因为存在文件,所以才能访问。文件夹结构大概如此:
├── archd
│ ├── 2019-10-28.md
│ ├── 2019-10-29.md
│ ├── 2019-10-30.md
├── archm
│ ├── 2019-10.md
│ ├── 2019-11.md
├── post
│ ├── 博文1.md
│ ├── 博文2.md
每个文件的内容格式如下:
{"date": "2019-11-01 00:00:00"}
其中,日期根据文件名来命名。
日月归档分类渲染
这块比较难,因为对 Hugo 不太熟,很多地方一试再试。贴一下我最后的代码:
layouts/archd/single.html
{{ define "content"}}
{{ $archYear := .Date.Format "2006" }}
{{ $archMonth := .Date.Format "January" }}
{{ $archDay := .Date.Format "21" }}
<h3 class="archive-title">
{{ slicestr .Date 0 10 }} 也是一个勤奋的小蜜蜂!
</h3>
{{ range where .Site.Pages "Section" "post" }}
{{ if and (eq (.Date.Format "2006") $archYear) ( and (eq (.Date.Format "January") $archMonth) (eq (.Date.Format "21") $archDay)) }}
<article class="post">
<header>
<h1 class="post-title">
<a href="{{ .Permalink }}" title="{{ .Title }}">{{ .Title }}</a>
</h1>
</header>
<date class="post-meta meta-date">
{{ .Date.Year }}年{{ printf "%d" .Date.Month }}月{{ .Date.Day }}日
</date>
{{ with .Params.Categories }}
<div class="post-meta">
<span>|</span>
{{ range . }}
<span class="meta-category"><a href='{{ "/categories/" | absLangURL }}{{ . | urlize }}'>{{ . }}</a></span>
{{ end }}
</div>
{{ end }}
<div class="post-meta">
<span>|</span>
<span>{{ .WordCount }} 字</span>
</div>
<div class="post-meta">
<span>|</span>
<span>需要 {{ .ReadingTime }} 分钟</span>
</div>
<div class="post-content">
{{ .Summary }}
</div>
<p class="readmore"><a href="{{ .Permalink }}">阅读全文</a></p>
</article>
{{ end }}
{{ end }}
{{ end }}
知道了日归档怎么写,月归档就很简单啦!这里就不贴代码了。
generate-archives.py
完成以上步骤,我们就可以通过 https://hanmei.netlify.app/2020/05/21/
这样的网址访问 2020年5月21日 撰写的博文啦~但是,如果 2020-05-21.md
如果没有在文件夹中创建,那这个网址就无法访问了。为了避免每次很麻烦的添加代码,我写了一个 Python 脚本,可以自动生成归档需要的目录以及文件。
日历绑定归档地址
这一步我需要知道那些网址是可访问,也就是说当天有撰写博文。如果只往这方面考虑就走近了死胡同,想通过判断这个网址是否404来渲染日历,但这个我不会实现。后来我换了一种思路,和前面的步骤一样,先把有文章的日子记下来,日历的 js 文件通过这个日期列表来渲染,这个问题就完美的解决啦!
具体实现上,通过 generate-archives.py
向某个 js 中间文件中写入能访问的日期列表。只要把这个 js 文件和 calendar 的 js 文件都引用在 sidebar 的 html 中,就可以在 calendar.js
中直接调用 js 文件里面的日期列表变量(这个地方我研究了好久,不知道怎么在 js 中引用另一个 js 文件)。
PS: 发现之前有个小问题,就是只能在主页显示日历,这是因为路径没处理好,在引用 js 文件时得用上 src='{{ "js/myCalendar.js" | relURL }}'
。
在 calendar.js
得到这个日期列表后,事情就变得简单了。修改修改条件语句判断和 click
事件,最后修修 css 样式一切就大功告成了。
顺便学了个如何利用 js 进行页面跳转:
window.location.href = url;
来看看最终效果
点击 14 号,可以看到能跳转到 14 号撰写的所有文章的链接。
氮素有个小问题,2020年5月21日的界面是这样的:
我推断,问题应该出在文件夹里有一个对自己的链接 .
,但我也不太确定是不是这个所导致的。至于怎么解决,我暂且搁置一边,毕竟对整体功能没多大影响。
还有一点不是很满意的是,日历能点击的部分我想有个小手手,而不是小箭头,在内部使用 pointer
没有效果,之后有闲情在处理吧。