资源引入
<script src="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.5.0/tocbot.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.5.0/tocbot.css">
注意到在这个教程里,tocbot.min.js
的版本必须>=4.4.4
。下文会有解释
构建目录
标题锚点
需要注意的是,要使文章目录点击标题跳转生效,在文章中的h1,h2,h3,h4,h5,h6
的标题必须要有id
这一属性,也就是要提供一个锚点。例如:
<h1 id="TOC-build-toc">构建目录</h1>
<h2 id="TOC-test">测试</h2>
<!-- ... -->
但是既然是在typecho这个平台上写文章,那自然大部分人会优先使用Markdown语法进行书写。
# 标题1
## 标题2
### 标题3
然而对于typecho默认的Markdown解析器HyperDown,我在其Parser.php
中并未找到给标题添加id
这一属性的代码。
也就是说,给标题增加id
属性这一工作了得自己来完成。
我在此采用的是前端的方法,后端当然也可以。
一些问题的考虑:
标题id中最好不要有符号或其他特殊字符
给重复的id增加一个索引号以区别
出于这些考虑,就有了如下增加id
的代码
var headerEl = 'h1,h2,h3,h4', //headers
content = '.post-content',//文章容器
idArr = {}; //标题数组以确定是否增加索引id
//add #id
$(content).children(headerEl).each(function () {
//去除空格以及多余标点
var headerId = $(this).text().replace(/[\s|\~|`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\||\|\[|\]|\{|\}|\;|\:|\"|\'|\,|\<|\.|\>|\/|\?|\:|\,|\。]/g, '');
headerId = headerId.toLowerCase();
if (idArr[headerId]) {
//id已经存在
$(this).attr('id', headerId + '-' + idArr[headerId]);
idArr[headerId]++;
}
else {
//id未存在
idArr[headerId] = 1;
$(this).attr('id', headerId);
}
});
这段理解起来比较容易,在这里就不解释了。区别重复id的方法当然不止这一种,我只是选择了比较快捷简便的一种方法。
初始化
官方给出的比较简单的初始化方法:
tocbot.init({
// 构建目录的容器
tocSelector: '.js-toc',
// 文章容器
contentSelector: '.js-toc-content',
// 需要解析的标题
headingSelector: 'h1, h2, h3',
});
而根据我的需求,我的初始化方法如下:
var headerEl = 'h1,h2,h3,h4', //headers
content = '.post-content',//文章容器
idArr = {}; //标题数组以确定是否增加索引id
tocbot.init({
tocSelector: '.toc',
contentSelector: content,
headingSelector: headerEl,
//positionFixedSelector: '.toc',
//positionFixedClass: 'is-position-fixed',
//fixedSidebarOffset: 'auto',
scrollSmooth: true,
scrollSmoothOffset: -80,
headingsOffset: -500
});
关于positionFixedSelector
/positionFixedClass
/fixedSidebarOffset
/scrollSmooth
/scrollSmoothOffset
/headingsOffset
这几个参数我会在下文进行解析。
PJAX回调
这里通过jquery-pjax
进行举例:
$(document).on('pjax:send',function(){
//destroy()方法
if ($('.toc').length) tocbot.destroy();
})
$(document).on('pjax:complete',function(){
//再调用一次toc.init()方法
//例如
toc.init(tocOptions);
})
到此,基本目录已经构建完成了(如果不嫌比较糙的话。。。)。
进阶
目录滚动跟随
与此相关的参数有positionFixedSelector
/positionFixedClass
/fixedSidebarOffset
positionFixedSelector - Element to add the positionFixedClass to.
positionFixedClass - Fixed position class to add to make sidebar fixed after scrolling down past the fixedSidebarOffset.
fixedSidebarOffset - fixedSidebarOffset can be any number but by default is set to auto which sets the fixedSidebarOffset to the sidebar element's offsetTop from the top of the document on init.
要实现滚动跟随的话,你可以如下配置,例如:
positionFixedSelector: '.toc',
positionFixedClass: 'is-position-fixed',
fixedSidebarOffset: 'auto',
第三项一般设置为auto
即可,在目录不可见时,tocbot会为.toc
的元素加上is-position-fixed
这一class
。
.is-position-fixed {
position:fixed !important;
top:0
}
当然is-position-fixed
的css你可以自行配置,tocbot默认的如上。
也就是说,当目录不可见的时候,目录会变为fixed
,跟着屏幕进行滚动。
但是我采用的是sticky
方法,所以我上面三项配置都注释掉了。
其实并不推荐使用sticky
,按照MDN的说法
“这是一个实验性的 API,请尽量不要在生产环境中使用它。”
然而我比较喜欢偷懒....
跳转偏移
对于跳转到某一标题,有一个比较常见的问题:跳转到对应标题后,浮动的导航栏(或者是别的元素)可能会挡住你的标题(#21)
对于这个问题,有两种方法解决:
使用
scrollEndCallback
配置项手动编写跳转后偏移的代码使用
scrollSmoothOffset
这一配置参数
之前我采用的是第一种方法,但是感觉效果并不好,原因是跳转之后你的滚动条会再次滚一次来达到偏移。在tocbot>=4.4.4
版本之后,项目的作者为其加上了scrollSmoothOffset
。
这个参数的作用就是在跳转同时进行一定的偏移,也就是说直接跳转到偏移之后的位置,而不是跳转到标题之后再进行偏移(滚动条只滚动一次),体验个人感觉比第一种方法好。
这一参数对应了scroll-smooth的offset
参数,但是tocbot项目的README中似乎并未加上这一参数的说明。
注意要将smoothScroll
这一项配置为true
滚动监听
偏移已经完成了,但是又碰上了一个新的问题:
在滚动到对应的标题的时候,目录会监听当前的位置对目录中的标题进行高亮/突出显示。但是如果两个标题靠太近,或者你对跳转时做了偏移处理,此时目录突出显示的标题也许并不会与文章中的标题对应上。
例如下图,本应该高亮显示主体部分解析的标题(已做了偏移),但是却突出显示了error 部分
这一标题。
但是这个值应该如何设置呢?在tocbot项目的build-html.js
中的165-174行左右可以看到:
if (heading.offsetTop > top + options.headingsOffset + 10) {
// Don't allow negative index value.
var index = (i === 0) ? i : i - 1
topHeader = headings[index]
return true
} else if (i === headings.length - 1) {
// This allows scrolling for the last heading on the page.
topHeader = headings[headings.length - 1]
return true
}
也就是说,满足heading.offsetTop > top + options.headingsOffset + 10
这一条件,高亮的标题才会被更新。那么headingOffset
在这种需求下应该设为负值。而这个值具体设置为多少,可以自己尝试。
代码参考
if ($('.toc').length > 0) {
var headerEl = 'h1,h2,h3,h4', //headers
content = '.post-content',//文章容器
idArr = {}; //标题数组以确定是否增加索引id
//add #id
$(content).children(headerEl).each(function () {
//去除空格以及多余标点
var headerId = $(this).text().replace(/[\s|\~|`|\!|\@|\#|\$|\%|\^|\&|\*|\(|\)|\_|\+|\=|\||\|\[|\]|\{|\}|\;|\:|\"|\'|\,|\<|\.|\>|\/|\?|\:|\,|\。]/g, '');
headerId = headerId.toLowerCase();
if (idArr[headerId]) {
//id已经存在
$(this).attr('id', headerId + '-' + idArr[headerId]);
idArr[headerId]++;
}
else {
//id未存在
idArr[headerId] = 1;
$(this).attr('id', headerId);
}
});
tocbot.init({
// Where to render the table of contents.
tocSelector: '.toc',
// Where to grab the headings to build the table of contents.
contentSelector: content,
// Which headings to grab inside of the contentSelector element.
headingSelector: headerEl,
//positionFixedSelector: '.toc',
//positionFixedClass: 'is-position-fixed',
scrollSmooth: true,
scrollSmoothOffset: -80,
headingsOffset: -500
});
}