简书文章左侧目录(带滚动)

前言

简书有一不太方便的一点就是没有左侧目录。所以自己定制给简书的博客自动生成侧边目录。

参考了简书博客 和hexo-theme-vue
但上述2个效果脚本都比较简单,我额外添加了滚动效果, 以及对非 h1 起头的标题的识别. 另外完善了注释

先看效果:

简书文章左侧目录(带滚动)_第1张图片

生成目录方法

1. 安装 Tampermonkey

从chrome网上应用商店搜到安装就好

2. 点击添加新脚本:

简书文章左侧目录(带滚动)_第2张图片

3. 在编辑器里写脚本为页面添加侧边目录。

简书文章左侧目录(带滚动)_第3张图片

4.保存

代码

// ==UserScript==
// @name         简书网站左侧目录生成
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  简书网站左侧目录生成,支持非h1标题,支持滚动
// @author       https://github.com/lxx2013
// @match        http://www.jianshu.com/p/*
// @match        https://www.jianshu.com/p/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';
    initSidebar('.sidebar', '.post');
})();

/**
* 简书网站左侧目录生成插件
* 代码参考了 https://github.com/vuejs/vuejs.org/blob/master/themes/vue/source/js/common.js
* @param {string} sidebarQuery - 目录 Element 的 query 字符串 
* @param {string} contentQuery - 正文 Element 的 query 字符串
*/
function initSidebar(sidebarQuery, contentQuery) {
    addAllStyle()
    var body = document.body
    var sidebar = document.querySelector(sidebarQuery)
    // 在 body 标签内部添加 div.sidebar 侧边栏,用于显示文档目录
    if (!sidebar) {
        sidebar = document.createElement('div')
        body.insertBefore(sidebar, body.firstChild)
    }
    sidebar.classList.add('sidebar')
    var content = document.querySelector(contentQuery)
    if (!content) {
        throw ('Error: content not find!')
        return
    }
    content.classList.add('content-with-sidebar');
    var ul = document.createElement('ul')
    ul.classList.add('menu-root')
    sidebar.appendChild(ul)

    var allHeaders = []
    // 遍历文章中的所有 h1或 h2(取决于最大的 h 是多大) , 编辑为li.h3插入 ul
    //因为标题一定是 h1 所以优先处理,然后再看文章正文部分是以 h1作为一级标题还是 h2或 h3作为一级标题
    //采用的方法是优先遍历正文, 然后再插入标题这个h1
    var i = 1
    var headers = [].slice.call(content.querySelectorAll('h' + i++), 1)
    while (!headers.length && i <= 6) {
        headers = Array.from(content.querySelectorAll('h' + i++))
    }
    [].unshift.call(headers, content.querySelector('h1'))
    if (headers.length) {
        [].forEach.call(headers, function (h) {
            var h1 = makeLink(h, 'a', 'h1-link')
            ul.appendChild(h1)
            allHeaders.push(h)
            //寻找h1的子标题
            var h2s = collectHs(h)
            if (h2s.length) {
                [].forEach.call(h2s, function (h2) {
                    allHeaders.push(h2)
                    var h3s = collectHs(h2)
                    h2 = makeLink(h2, 'a', 'h2-link')
                    ul.appendChild(h2)
                    //再寻找 h2 的子标题 h3
                    if (h3s.length) {
                        var subUl = document.createElement('ul')
                        subUl.classList.add('menu-sub')
                        h2.appendChild(subUl)
                            ;[].forEach.call(h3s, function (h3) {
                                allHeaders.push(h3)
                                h3 = makeLink(h3, 'a', 'h3-link')
                                subUl.appendChild(h3)
                            })
                    }
                })
            }
        })
    }
    //增加 click 点击处理,使用 scrollIntoView,增加控制滚动的 flag
    var scrollFlag = 0
    var scrollFlagTimer
    sidebar.addEventListener('click', function (e) {
        e.preventDefault()
        if (e.target.href) {
            scrollFlag = 1
            clearTimeout(scrollFlagTimer)
            scrollFlagTimer = setTimeout(() => scrollFlag = 0, 1500)
            setActive(e.target, sidebar)
            var target = document.getElementById(e.target.getAttribute('href').slice(1))
            target.scrollIntoView({ behavior: 'smooth', block: "center" })
        }
    })
    //监听窗口的滚动和缩放事件
    window.addEventListener('scroll', updateSidebar)
    window.addEventListener('resize', throttle(updateSidebar))
    function updateSidebar() {
        if (scrollFlag)
            return
        var doc = document.documentElement
        var top = doc && doc.scrollTop || document.body.scrollTop
        if (!allHeaders.length) return
        var last
        for (var i = 0; i < allHeaders.length; i++) {
            var link = allHeaders[i]
            if (link.offsetTop > (top + document.body.clientHeight / 2 - 73)) {
                if (!last) { last = link }
                break
            } else {
                last = link
            }
        }
        if (last) {
            setActive(last.id, sidebar)
        }
    }
}

/**
>为正文的标题创建一个对应的锚,返回的节点格式为`
  • some text
  • ` @param {HTMLElement} h - 需要在目录中为其创建链接的一个标题,它的`NodeType`可能为`H1 | H2 | H3` @param {string} tag - 返回的 li 中的节点类型, 默认为 a @param {string} className - 返回的 tag 的 class ,默认为空 @returns {HTMLElement} 返回的节点格式为`
  • some text
  • ` */ function makeLink(h, tag, className) { tag = tag || 'a' className = className || '' var link = document.createElement('li') var text = [].slice.call(h.childNodes).map(function (node) { if (node.nodeType === Node.TEXT_NODE) { return node.nodeValue } else if (['CODE', 'SPAN', 'A'].indexOf(node.tagName) !== -1) { return node.textContent } else { return '' } }).join('').replace(/\(.*\)$/, '') if (!h.id) h.id = IdEscape(text) link.innerHTML = `<${tag} class="${className}" href="#${h.id}">${htmlEscape(text)}` return link } /** *对 id 进行格式化.把空白字符和引号转义为下划线 *>注意:id值使用字符时,除了 ASCII字母和数字、“—”、“-"、"."之外,可能会引起兼容性问题,因为在HTML4中是不允许包含这些字符的,这个限制在HTML5中更加严格,为了兼容性id值必须由字母开头,同时不允许其中有空格。参考https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/id *>但是本程序中使用了 document.getElementById 的要求稍放宽了一些,"#3.1_createComponent"这样的 id能成功执行 @param {string} text - HTML特殊字符 @returns {string} 转义后的字符串,例如`# 1'2"3标题`被转义为`#_1_2_3标题` */ function IdEscape(text) { return text.replace(/[\s"']/g, '_') //注意这里不加 g 的话就会只匹配第一个匹配,所以会出错 } /** >HTML 特殊字符[ &, ", ', <, > ]转义 @param {string} text - HTML特殊字符 @returns {string} 转义后的字符,例如`<`被转义为`<` */ function htmlEscape(text) { return text .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>') } /** *为一个 `h(x)`标题节点收集跟在它屁股后面的 `h(x+1)`标题节点, >若屁股后面没有`h(x+1)`节点,则收集`h(x+2)`节点甚至`h(x+3)`,毕竟不知道文章作者喜欢用哪种大小做标题 >收集过程中若遇到 `h(x)或h(x-1)`节点的话要立即返回 @param {HTMLElement} h - HTML 标题节点 `H1~H6` @returns {HTMLElement[]} 一个由 h(x+1)或 h(x+2)等后代目录节点组成的数组 */ function collectHs(h) { var childIndexes = [] var thisTag = h.tagName var count = 1 do { var childTag = h.tagName[0] + (parseInt(h.tagName[1]) + count++) var next = h.nextElementSibling while (next) { if (next.tagName[0] == 'H' && next.tagName[1] <= thisTag[1]) { break } else if (next.tagName === childTag) { childIndexes.push(next) } next = next.nextElementSibling } } while (childTag < 'H6' && childIndexes.length == 0) return childIndexes } /** *设置目录的激活状态,按既定规则添加 active 和 current 类 *>无论对h2还是 h3进行操作,首先都要移除所有的 active 和 current 类, 然后对 h2添加 active 和 current, 或对 h3添加 active 对其父目录添加 current @param {String|HTMLElement} id - HTML标题节点或 querySelector 字符串 @param {HTMLElement} sidebar - 边栏的 HTML 节点 */ function setActive(id, sidebar) { //1.无论对h2还是 h3进行操作,首先都要移除所有的 active 和 current 类, var previousActives = sidebar.querySelectorAll(`.active`) ;[].forEach.call(previousActives, function (h) { h.classList.remove('active') }) previousActives = sidebar.querySelectorAll(`.current`) ;[].forEach.call(previousActives, function (h) { h.classList.remove('current') }) //获取要操作的目录节点 var currentActive = typeof id === 'string' ? sidebar.querySelector('a[href="#' + id + '"]') : id if (currentActive.classList.contains('h2-link') != -1) { //2. 若为 h2,则添加 active 和 current currentActive.classList.add('active', 'current') } if ([].indexOf.call(currentActive.classList, 'h3-link') != -1) { //3. 若为 h3,则添加 active 且对其父目录添加 current currentActive.classList.add('active') var parent = currentActive while (parent && parent.tagName != 'UL') { parent = parent.parentNode } parent.parentNode.querySelector('.h2-link').classList.add('current', 'active') } //左侧目录太长时的效果 currentActive.scrollIntoView({ behavior: 'smooth' }) } /** >增加 sidebar 需要的全部样式 @param {string} highlightColor - 高亮颜色, 默认为'#c7254e' */ function addAllStyle(highlightColor) { highlightColor = highlightColor || "#c7254e" var sheet = newStyleSheet() /** >创建一个新的``标签插入``中 @return {Object} style.sheet,`它具有方法insertRule` */ function newStyleSheet() { var style = document.createElement("style"); // 对WebKit hack :( style.appendChild(document.createTextNode("")); // 将