js生成目录并实现目录节点跟随滚动高亮
# 场景
当网页有文章,文章中有很多标题。我们有时会需要生成目录大纲,以便他人查阅。生成目录到是不难,但是怎么实现目录跟随着页面滚动而改变目录的高亮标题?
# 实现思虑
# 获取所有标题,并生成大纲
// 获取所有标题
let tocList = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
let HList = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
let str = `<div class="dir"><ul id="toc">`;
// 生成目录(你也可以更具自己需要生成一个对象也可以,我这里生成 html字符串)
Array.from(tocList,v => {
const H = HList.indexOf(v.nodeName) + 1 || 1; // 标题等级 1,2,3,4,5,6
str += `<li class="li li-${H}">
<a href="javascript:void(0);" id="${v.id}" >${v.textContent}</a>
</li>\n`;
})
str += `</ul>`;
str += `<div class="sider"><span class="siderbar"><span></div></div>`;
// 添加 html 字符串到 页面
let toc = document.querySelector(".toc");
toc.insertAdjacentHTML("beforeend", str);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
生成节点大概如下:(如果是vue或react可以生成对象,在页面渲染成大概这样子就行)
# 给每个目录节点绑定点击事件
为什么不用瞄点定位?a 标签 #id 确实可以定位到该标题,但这个我个人觉得有点不好用,如果顶部有导航栏,瞄点定位的标题会被顶部导航栏遮挡住,为了不被遮挡,可以给每个标题都添加一个 padding-top 或 margin-top 隔开顶部导航栏,但我不想那么做。
Array.from(tocList,v => {
const btn = document.querySelector(`#toc #${v.id}`);
const ele = document.querySelector(`.container #${v.id}`);
if (!btn || !ele) return;
btn.addEventListener("click", () => {
window.scrollTo({ top: ele.offsetTop - 80, behavior: "smooth" });
})
})
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 监听滚动时目录高亮节点
这里不适用 scolll 来监听滚动,我这里用 observe 来监听
const visibleChnage = (obs) => {
var sider = document.querySelector(".siderbar");
var toc = document.querySelectorAll("#toc .li a");
item.forEach(observe => {
// 找到对应的节点
const id = observe.target.getAttribute('id'), anchor = document.querySelector(`#toc .li #${id}`);
if (!anchor) return false;
// 如果节点出现在可视视窗
if (observe.isIntersecting) {
// 排他(这个为了清除所有a标签中的类名 li-active)
removeClass();
// 目录 a 标签的类名为 li-active 时高亮
anchor.classList.add("li-active");
const index = Array.from(toc, v => v.getAttribute('id')).indexOf(id);
// 左边高亮目录条
sider.style.transform = `translateY(${index * 30}px)`;
}
});
}
const observer = new IntersectionObserver(visibleChnage);
// 监听滚动
Array.from(tocList).map(item => observer.observe(item));
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
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
排他: removeClass()
// 移除所有的 li-active 排他
const removeClass = () => {
const list = document.querySelectorAll("#toc .li a");
Array.from(list,v => v.classList.remove("li-active"));
}
1
2
3
4
5
2
3
4
5
# 完整代码
# CSS
.container {
width: 50%;
}
.toc {
position: fixed;
right: 2%;
top: 80px;
min-width: 300px;
}
.dir {
position: relative;
padding-bottom: 8px;
box-sizing: border-box;
}
#toc {
margin: 0;
padding-left: 15px;
box-sizing: border-box;
}
.sider {
width: 2px;
height: 100%;
background: #eee;
position: absolute;
left: 0;
top: 0;
border-radius: 10px;
margin: auto;
bottom: 0;
}
.siderbar {
display: flex;
width: 100%;
height: 20px;
line-height: 2;
background: #f36;
transition: all 0.1s;
}
li {
height: 30px;
box-sizing: border-box;
list-style: none;
}
a {
color: #333;
text-decoration: none;
font-size: 14px;
}
.li-1 {
padding-left: 0px;
}
.li-2 {
padding-left: 8px;
}
.li-3 {
padding-left: 16px;
}
.li-4 {
padding-left: 24px;
}
.li-5 {
padding-left: 32px;
}
.li-6 {
padding-left: 40px;
}
.li-active {
color: #f36;
}
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
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
# HTML
<div class="container"></div>
<div class="toc"></div>
1
2
2
# JS
console.log("%c 自动生成目录 ", "color: #fff; background: #000;padding: 5px;");
window.addEventListener("load", () => tocList())
const tocList = () => {
const toc = document.querySelector(".toc");
const elements = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
if (!elements.length) return;
const HList = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'];
// 生成目录
let str = `<div class="dir">\n<ul id="toc">`;
Array.from(elements, v => {
const H = HList.indexOf(v.nodeName) + 1 || 1;
str += `<li class="li li-${H}"><a href="javascript:void(0);" id="${v.id}" >${v.textContent}</a></li>\n`;
})
str += `</ul>\n`;
str += `<div class="sider"><span class="siderbar"><span></div>\n</div>`;
// 添加目录到页面
toc.insertAdjacentHTML("beforeend", str);
// 给目录添加点击事件
Array.from(elements, v => {
const btn = document.querySelector(`#toc #${v.id}`);
const ele = document.querySelector(`.container #${v.id}`);
if (!btn || !ele) return;
btn.addEventListener("click", () => {
window.scrollTo({ top: ele.offsetTop - 80, behavior: "smooth" });
})
})
// 监听滚动
const observer = new IntersectionObserver(visibleChnage,{rootMargin: "80px 0px 0px 0px"});
Array.from(elements, item => observer.observe(item));
}
// 移除所有的 li-active 排他
const removeClass = () => {
const list = document.querySelectorAll("#toc .li a");
Array.from(list, v => v.classList.remove("li-active"));
}
// 监听滚动
const visibleChnage = (item) => {
var sider = document.querySelector(".siderbar");
var toc = document.querySelectorAll("#toc .li a");
item.forEach(observe => {
const id = observe.target.getAttribute('id'), anchor = document.querySelector(`#toc .li #${id}`);
if (!anchor) return false;
if (observe.isIntersecting) {
removeClass();
const index = Array.from(toc, v => v.getAttribute('id')).indexOf(id);
anchor.classList.add("li-active");
sider.style.transform = `translateY(${index * 30 + 10}px)`;
return;
}
});
}
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
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
上次更新: 2024/02/20, 17:31:36
- 01
- Node与GLIBC_2.27不兼容解决方案08-19
- 02
- Git清空本地文件跟踪缓存08-13