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

前言

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

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

先看效果:

image

生成目录方法

1. 安装 Tampermonkey

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

2. 点击添加新脚本:

image

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

image

4.保存

图片.png

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// ==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)
}
}
}

/**
>为正文的标题创建一个对应的锚,返回的节点格式为`<li><tag class="className"> some text </tag><li>`
@param {HTMLElement} h - 需要在目录中为其创建链接的一个标题,它的`NodeType`可能为`H1 | H2 | H3`
@param {string} tag - 返回的 li 中的节点类型, 默认为 a
@param {string} className - 返回的 tag 的 class ,默认为空
@returns {HTMLElement} 返回的节点格式为`<li><a> some text </a><li>`
*/
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
}

/**
*对 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} 转义后的字符,例如`<`被转义为`&lt`
*/
function htmlEscape(text) {
return text
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
/**
*为一个 `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()
/**
>创建一个新的`<style></style>`标签插入`<head>`中
@return {Object} style.sheet,`它具有方法insertRule`
*/
function newStyleSheet() {
var style = document.createElement("style");
// 对WebKit hack :(
style.appendChild(document.createTextNode(""));
// 将 <style> 元素加到页面中
document.head.appendChild(style);
return style.sheet;
}
var position = 0
/**
>添加一条 css 规则
@param {string} str - css样式,也可以是@media
*/
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;
}`)
}
/**
>函数节流
>参考https://juejin.im/entry/58c0379e44d9040068dc952f
@param {Fuction} fn - 要执行的函数
*/
function throttle(fn, interval = 300) {
let canRun = true;
return function () {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, interval);
};
}

代码来源

新版下载
本文原创,商业转载请联系作者获得授权,非商业转载请注明出处。

评论

发送评论 编辑评论


                        

待分类分类热门文章

标签热门文章排行

☛免责声明 ☛本站使用教程
Theme Argon With Ry-Plus By 清欢
我的第24569位朋友,历经142822次回眸才与你相遇
内容失效/资源代找/交流学习
内容失效/资源代找/交流学习