油猴插件实现简书网站左侧目录生成

1、安装 Tampermonkey

谷歌浏览器翻墙,从chrome扩展应用商店搜索Tampermonkey安装好。

点击油猴插件添加新脚本:

然后就可以在编辑器里写脚本为页面添加侧边目录。

2、脚步代码

思路:
查看简书文章页面的源码,可以发现所有的标题行都放在h1,h2,h3,h4,h5,h6 标签内,所以将页面设计成在文章左侧插入列表, 按标题层级进行缩进,点击进行跳转。
将下面的代码贴进编辑器即可。

// ==UserScript==
// @name         简书网站左侧目录生成
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  简书网站左侧目录生成,支持非h1标题,支持滚动
// ==/UserScript==

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

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 = []
    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)
        }
    }
}

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)}</${tag}>`
    return link
}

function IdEscape(text) {
    return text.replace(/[\s"']/g, '_') //注意这里不加 g 的话就会只匹配第一个匹配,所以会出错
}
function htmlEscape(text) {
    return text
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
}
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
}

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

function addAllStyle(highlightColor) {
    highlightColor = highlightColor || "#c7254e"
    var sheet = newStyleSheet()

    function newStyleSheet() {
        var style = document.createElement("style");
        // 对WebKit hack :(
        style.appendChild(document.createTextNode(""));
        // 将 <style> 元素加到页面中
        document.head.appendChild(style);
        return style.sheet;
    }
    var position = 0

    function addStyle(str) {
        sheet.insertRule(str,position++);
    }
    addStyle(`.sidebar{position:fixed;    z-index: 10;
        top: 61px;
        left: 0;
        bottom: 0;
        overflow-x: hidden;
        overflow-y: auto;
        padding: 40px 20px 60px 30px;
        max-width: 310px;
    }`)
    addStyle(`.menu-root { list-style:none; text-align:left }`)
    addStyle(`.menu-root .h1-link{
        display:inline-block;
        color:rgb(44, 62, 80);
        font-family:"source sans pro", "helvetica neue", Arial, sans-serif;
        font-size:17.55px;
        font-weight:600;
        height:22px;
        line-height:22.5px;
        list-style-type:none;
        margin-block-end:11px;
        margin-block-start:11px;
    }`)
    addStyle(`.menu-root .h2-link:hover {
        border-bottom: 2px solid ${highlightColor};
    }`)
    addStyle(`.menu-root .h2-link.current+.menu-sub{
        display:block;
    }`)
    addStyle(`.menu-root .h2-link{
        color:rgb(127,140,141);
        cursor:pointer;
        font-family:"source sans pro", "helvetica neue", Arial, sans-serif;
        font-size:15px;
        height:auto;
        line-height:22.5px;
        list-style-type:none;
        text-align:left;
        text-decoration-color:rgb(127, 140, 141);
        text-decoration-line:none;
        text-decoration-style:solid;
        margin-left:12.5px;
    }`)
    addStyle(`.menu-sub {
        padding-left:25px;
        list-style:none;
        display:none;
    }`)
    addStyle(`.menu-sub .h3-link{
        color:#333333;
        cursor:pointer;
        display:inline;
        font-family:"source sans pro", "helvetica neue", Arial, sans-serif;
        font-size:12.75px;
        height:auto;
        line-height:19.125px;
        list-style-type:none;
        text-align:left;
        text-decoration-color:rgb(52, 73, 94);
        text-decoration-line:none;
        text-decoration-style:solid;
    }`)
    addStyle(`@media only screen and (max-width : 1300px){
        .content-with-sidebar {
            margin-left:310px !important;
        }
    }`)
    addStyle(`.sidebar .active{
        color:${highlightColor};
        font-weight:700;
    }`)
}

function throttle(fn, interval = 300) {
    let canRun = true;
    return function () {
        if (!canRun) return;
        canRun = false;
        setTimeout(() => {
            fn.apply(this, arguments);
            canRun = true;
        }, interval);
    };
}

3、效果

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,794评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,050评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,587评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,861评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,901评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,898评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,832评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,617评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,077评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,349评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,483评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,199评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,824评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,442评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,632评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,474评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,393评论 2 352