menu search

文章目录 / Table of Contents

Aug 30, 2017

文章目录这功能我以前从来没用过。Hugo自带了一个.TableOfContents变量(详情),可以自动输出文章树。但有个缺点是,Hugo是从h1开始遍历,并使用ul来做嵌套。我文章使用的是h2-h6,导致输出的文章树会有多余的嵌套

不过有人在GitHub Issue里面提出了个解决方案:基于正则匹配,并用Go Template来实现同样的功能。我对Go Template并不怎么熟悉,改起来也麻烦,所以决定用JavaScript来生成。

文章目录这个东西其实说白了就是遍历所有的heading标签,并输出链接以供点击。这个实现起来很简单,例如:

document.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach((heading, i) => {
    if (!heading.id) {
        return;
    };
    let toc = $('#toc ul')[0];

    let li = document.createElement('li');
    li.className = 'toc-' + heading.tagName.toLowerCase();
    li.id = 'toc-' + i;

    let a = document.createElement('a');
    a.textContent = heading.textContent;
    a.dataset.id = heading.id;
    a.setAttribute('href', window.location.origin + window.location.pathname + '#' + heading.id);

    li.appendChild(a);
    toc.appendChild(li);
});

最后的效果差不多是这样的:

<aside id="toc">
    <h2>Table of contents</h2>
    <ul>
        <li class="toc-h2 active" id="toc-0">
            <a data-id="关于博客" href="https://blog.jimmycai.com/about/#关于博客">关于博客</a>
        </li>
        <li class="toc-h2" id="toc-1">
            <a data-id="事记" href="https://blog.jimmycai.com/about/#事记">事记</a>
        </li>
        <li class="toc-h2" id="toc-2">
            <a data-id="其他" href="https://blog.jimmycai.com/about/#其他">其他</a>
        </li>
        <li class="toc-h2" id="toc-3">
            <a data-id="联系方式" href="https://blog.jimmycai.com/about/#联系方式">联系方式</a>
        </li>
        <li class="toc-h2" id="toc-4">
            <a data-id="pgp-key" href="https://blog.jimmycai.com/about/#pgp-key">PGP Key</a>
        </li>
        <li class="toc-h2" id="toc-5">
            <a data-id="版权" href="https://blog.jimmycai.com/about/#版权">版权</a>
        </li>
    </ul>
</aside>

没有搞成ul+li多个嵌套的结构。不过我觉得应该也不会很难:在循环外放个变量记录上个标签,并与当前循环的标签对比<h{N}> N的大小,如果变大就关闭ul标签,如果小了就开启。


监听滚动

我还有还有一个需求是在下拉时高亮当前的标题,也就是需要监听滚动事件。市面上已经有很多这类的插件了,像是Bootstrap自带的Scrollspy

如开头所说,这篇文章主要记录想法,做的应该是没有那些插件好,凑合能用 🌚

我的想法是先遍历文章所有的h标签,将元素与其offset top存入数组。接着监听滚动事件,并使用filter方法过滤出当前viewport内的标题:

var headings = [];

/// Start loop
	headings.push({
		el: li, /// #TOC > ul > li
		offsetTop: heading.offsetTop
	});
// End loop

记录了两个属性:文章树里的li,和原标题的offsetTop。这么做等下就可以不用再计算一遍了。但如果发生变化需要重新遍历一遍。

window.addEventListener('scroll', (e) => {
    let scrollTop = window.scrollY,
        bottom = document.getElementById('post-content').offsetTop + document.getElementById('post-content').getBoundingClientRect().height;
  
    let currents = headings.filter((heading, i, arr) => {
        let next = arr[i + 1],
            nextScrollTop;

        if (!next) {
            nextScrollTop = bottom;
        } else {
            nextScrollTop = next.offsetTop - (window.innerHeight / 5);
        };

        let scrollTop = window.scrollY + (window.innerHeight / 2);
        return heading.offsetTop <= scrollTop && nextScrollTop > window.scrollY;
    });

    let current = currents[0],
        past = document.getElementById('toc').querySelectorAll('.active')[0];
    if (past) {
        past.classList.remove('active');
    };

    if (current) {
        current.el.classList.add('active');

    } else if (scrollTop > bottom) {
        document.body.classList.remove('toc--show');
    };
});

写的有点乱 😅

如果满足以下条件就代表目前标题指向的Section还是处于可见状态:

  1. 标题的offsetTop超过了window.scrollY + (window.innerHeight / 2)
  2. 下一个标题的offsetTopwindow.scrollY大,也就还没滚动到那个位置

遍历的时候会保留顺序,我直接拿筛选后的第一个标题来添加类。

这个实现方法可能在性能上会有些问题,毕竟每次滚动都会触发filter,并遍历数组中的对象。(虽然说如果标题数量很少的话应该不会感觉到卡顿。)

我有个想法是把上一个和下一个标题的offsetTop保存在变量中,每次触发滚动事件先检查看看当前的滚动距离是否出于两者之间。如果不是再进行过滤。

不清楚Bootstrap里的插件是如何实现此功能的,有空得去啃啃代码。


Photo by Brandon Green on Unsplash

Comments

edit x send markdown image
paragraph comment heart