资源引入


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

标题被挡住

对于这个问题,有两种方法解决:

  1. 使用scrollEndCallback配置项手动编写跳转后偏移的代码

  2. 使用scrollSmoothOffset这一配置参数

之前我采用的是第一种方法,但是感觉效果并不好,原因是跳转之后你的滚动条会再次滚一次来达到偏移。在tocbot>=4.4.4版本之后,项目的作者为其加上了scrollSmoothOffset
这个参数的作用就是在跳转同时进行一定的偏移,也就是说直接跳转到偏移之后的位置,而不是跳转到标题之后再进行偏移(滚动条只滚动一次),体验个人感觉比第一种方法好。
这一参数对应了scroll-smooth的offset参数,但是tocbot项目的README中似乎并未加上这一参数的说明。
注意要将smoothScroll这一项配置为true

滚动监听

偏移已经完成了,但是又碰上了一个新的问题:

在滚动到对应的标题的时候,目录会监听当前的位置对目录中的标题进行高亮/突出显示。但是如果两个标题靠太近,或者你对跳转时做了偏移处理,此时目录突出显示的标题也许并不会与文章中的标题对应上


例如下图,本应该高亮显示主体部分解析的标题(已做了偏移),但是却突出显示了error 部分这一标题。
2019-03-10 00-10-35 的屏幕截图.png


此时,headingsOffset就派上了用场。
2019-03-10 00-16-54 的屏幕截图.png


但是这个值应该如何设置呢?在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

});

}