前几天在 GitHub 上发现一个用 JavaScript 生成文章目录的项目 Tocbot,于是顺手加到博客上了。

What's TOC?

A table of contents, usually headed simply "Contents" and abbreviated informally as TOC, is a list, usually found on a page before the start of a written work, of its chapter or section titles or brief descriptions with their commencing page numbers.A table of contents, usually headed simply "Contents" and abbreviated informally as TOC, is a list, usually found on a page before the start of a written work, of its chapter or section titles or brief descriptions with their commencing page numbers.

form Wikipedia

Use TOC in HTML

一份标准的 HTML 文档,应该有严格的格式,比如通过 <h1><h2><h3> 这样的标签作为大标题、小标题;用 <p> 来标记段落。(很惭愧早期的文章我自己也没有太在意标题部分,包括主题上的标题也是用得比较随性,据说这样对搜索引擎极不友好)

当然用 HTML 标签直接写文章的人应该没有几个吧,通常我们使用 Markdown 写文章,然后通过一个 parser 转换为 HTML,而 Markdown 中的 # h1 balabala## h2 balabala 等正对应了 HTML 中的 <h1><h2>等。所以只要你的文章是用 Markdwon 写的,生成一个目录是很容易的。

下面举几个例子,都可以在 WordPress 的原生 Markdown parser 上实现(个人并不喜欢 Jetpack 插件并且也未测试过其是否支持):

The Site {#header}
=======
在标题中指定 id

## The Site 1 {.main}

在标题中指定 class

### The Site 2 {style=color:red;}

在标题中指定 style

以上生成的标签是这样的:

<h1 id="header">The Site</h1>
<h2 class="main">The Site 1</h2>
<h3 style="color:red;">The Site 2</h3>

现在要说的 Tocbot 就是生成这些段落标记目录的。如果单纯想要一个目录其实不是难事,用 jQuery 写起来很简单的,基本思路就是遍历正文中的所有 <h> 标签,并按 <h1><h2><h3> 的顺序建立套嵌逻辑,最后根据 element 的 id 建立链接。当然 Tocbot 的功能比这个高级得多,具体可以看本页上的效果(未适配手机,请用电脑查看,并保证屏幕宽度在 1200px 以上),值得一提的是 Tocbot 并没有使用 jQuery,完全靠原生 JS 实现。
toc.gif

Make It Work

那么现在开始使用 TOC,首先为了使目录中的链接可以跳到文章的指定位置,我们通过 <a> 标签的 href 属性跳转到唯一 id 标记的元素,如 <a href="#header"> 将跳转到 <h1 id="header">The Site</h1> 的位置。不过注意通过 id 跳转不支持中文 id,今可使用数字、英文、英文符号。

Unique ID

那么工作的第一步就是为每一个标题生成唯一的 id,你可以像上面那样在 Markdown 中指定 id,当然这很麻烦,那就让 JS 来帮忙吧。以下使用 jQuery 为每一个标题添加 id:

var id = 1;
$(".entry-content").children("h1,h2,h3,h4,h5").each(function () {
    // entry-content 为正文容器的 class,根据自己的情况修改
    //var hyphenated = $(this).text().replace(/\s/g, '-');
    // 如果你希望使用中文 id 的话就用上面这行,注意非ANSI编码文字会导致无法跳转
    var hyphenated = "mashiro-" + id;
    $(this).attr('id', hyphenated);
    id++;
});

Initial Tocbot

这时每一个标题将加上唯一的序号作为 id,下面是引入 Tocbot,并在页面中添加一个目录容器:

<link href="https://cdn.bootcss.com/tocbot/4.1.1/tocbot.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/tocbot/4.1.1/tocbot.min.js"></script>
<div class="toc"></div>

初始化 Tocbot:

tocbot.init({
    // Where to render the table of contents.
    tocSelector: '.toc', // 放置目录的容器
    // Where to grab the headings to build the table of contents.
    contentSelector: '.entry-content', // 正文内容所在
    // Which headings to grab inside of the contentSelector element.
    headingSelector: 'h1, h2, h3, h4, h5', // 需要索引的标题级别
    positionFixedSelector: ".toc", //目录位置固定
    scrollEndCallback: function (e) { //回调函数
        window.scrollTo(window.scrollX, window.scrollY - 80);
        //修正滚动后页面的位置,80 是自己顶部栏的高度
    },
});

还有很多可选的属性

Customize

这时目录已经生成了,之后是随页面滚动及悬浮效果的实现。玄学 CSS 我实在写不出来,所以就用 jQuery 写了一个滚动及悬浮效果(你大概也注意到滚动的时候有一些延时,我尝试过用正弦函数修正,但延时反因运算量的增大变得更加严重,我的 JS 性能显然是比不上 CSS 的2333):

$(document).ready(function() {
    if ($("div").hasClass("toc")) { // 检测是否存在目录容器
        var $elm = $('.toc'); // 选择容器
        var iniTop = 500; // 初始高度
        var finTop = 100; // 悬浮高度
        var hasScrolled = $('.site-header').offset().top; //获取当前页面距离顶部高度
        //以上是根据我的header确定的,根据自己的顶部栏情况修改
        if (hasScrolled > iniTop) { // 初始定位(主要针对着陆位置不在页面顶部的情况)
            $elm.css({
                'top': finTop
            });
        }
        $(window).scroll(function() { // 追踪鼠标滚动事件
            var p = $(window).scrollTop(); // 获取已滚动高度
            if (p > iniTop - finTop) { // 悬浮定位
                $elm.css({
                    'top': finTop
                });
            } else { //滚动定位
                $elm.css({
                    'top': iniTop - p
                });
            }
        });
    }
});

以上代码控制了纵向位置,水平位置则通过下面 CSS 确定:

.toc {
    width: 200px;
    height: auto;
    z-index: 98;
    background-color: rgba(255,255,255,0);
    transform: translateX(0);
    right: calc((100% - 950px - 250px) / 2);
    position: fixed !important;
    top:480px;
    position: absolute;
    padding-top: 10px;
    padding-bottom: 10px;
}

下面把上面 JS 整合到一个函数里:

function mashiroToc(mashiro) {
    // 滚动及悬浮
    $(document).ready(function() {
        if ($("div").hasClass("toc")) {
            var $elm = $('.toc');
            var iniTop = 500; 
            var finTop = 100; 
            var hasScrolled = $('.site-header').offset().top;
            if (hasScrolled > iniTop) {
                $elm.css({
                    'top': finTop
                });
            }
            $(window).scroll(function() {
                var p = $(window).scrollTop();
                if (p > iniTop - finTop) {
                    $elm.css({
                        'top': finTop
                    });
                } else {
                    $elm.css({
                        'top': iniTop - p
                    });
                }
            });
        }
    });
    // 初始化
    if (mashiro) {
        var id = 1;
        $(".entry-content").children("h1,h2,h3,h4,h5").each(function() {
            //var hyphenated = $(this).text().replace(/\s/g, '-');
            var hyphenated = "mashiro-" + id;
            $(this).attr('id', hyphenated);
            id++;
        });
        // 初始化 tocbot.js
        tocbot.init({
            tocSelector: '.toc',
            contentSelector: '.entry-content',
            headingSelector: 'h1, h2, h3, h4, h5',
            positionFixedSelector: ".toc",
            scrollEndCallback: function (e) {
                window.scrollTo(window.scrollX, window.scrollY - 80);
            },
        });
    }
}
mashiroToc(true);

Examples

下面是更多层级目录的展示

This is a h3

This is a h4

This is a h5

Q.E.D.