思路
- 目前标题id唯一. 由于需要数组存放标题高亮状态, 仍旧为标题进行编号, 使用编号读取数组值
- 文章目录标题项和正文标题项均使用li元素定位
- 出现在页面的所有标题高亮
框架
获取文章目录标题数和正文标题数, 判等
1const toc = document.querySelector('.toc-panel');
2if (!toc)
3{
4 console.log("toc is null", toc);
5 return;
6}
7
8const headings = Array.apply(null, document.querySelectorAll('h2[id], h3[id], h4[id]'))
9 .filter(function(value, index, arr) { return arr[index].querySelector('.anchor'); });
10const tocHeadings = toc.querySelectorAll('li');
11
12if (tocHeadings.length !== headings.length)
13{
14 console.log(headings);
15 console.log(tocHeadings);
16 return;
17}
为文章目录标题和正文标题添加关联编号, 创建标题高亮数组
1function addHeadingIdx(list)
2{
3 let i = 0;
4 list.forEach((item) => { item.setAttribute('headingIdx', i++); });
5}
6
7let HeadingFlag;
8
9addHeadingIdx(tocHeadings);
10addHeadingIdx(headings);
11
12HeadingFlag = new Array(headings.length).fill(false);
创建观察器, 当标题进入/离开窗口时触发处理
1const intersectionOptions = {threshold : 1.0};
2const headingObserver = new IntersectionObserver(headings => { unfold_headings(headings); }, intersectionOptions);
观察正文标题
1headings.forEach((heading) => { headingObserver.observe(heading); });
完整代码
1let HeadingFlag;
2
3function addHeadingIdx(list)
4{
5 let i = 0;
6 list.forEach((item) => { item.setAttribute('headingIdx', i++); });
7}
8
9document.addEventListener('DOMContentLoaded', () => {
10 const toc = document.querySelector('.toc-panel');
11 if (!toc)
12 {
13 console.log("toc is null", toc);
14 return;
15 }
16
17 const headings = Array.apply(null, document.querySelectorAll('h2[id], h3[id], h4[id]'))
18 .filter(function(value, index, arr) { return arr[index].querySelector('.anchor'); });
19 const tocHeadings = toc.querySelectorAll('li');
20
21 if (tocHeadings.length !== headings.length)
22 {
23 console.log(headings);
24 console.log(tocHeadings);
25 return;
26 }
27
28 addHeadingIdx(tocHeadings);
29 addHeadingIdx(headings);
30
31 HeadingFlag = new Array(headings.length).fill(false);
32
33 const intersectionOptions = {threshold : 1.0};
34 const headingObserver = new IntersectionObserver(headings => { unfold_headings(headings); }, intersectionOptions);
35
36 headings.forEach((heading) => { headingObserver.observe(heading); });
37});
文章目录结构
1<div>
2 <ul>
3 <li><a href="#"><span></span>heading 1</a></li>
4 <li>
5 <a href="#"><span></span>heading 2</a>
6 <button></button>
7 <div>
8 <ul>
9 <li><a href="#"><span></span>heading 2 - 1</a></li>
10 <li><a href="#"><span></span>heading 2 - 2</a>
11 </li>
12 </ul>
13 </div>
14 </li>
15 <li><a href="#><span></span>heading 3</a></li>
16 </ul>
17</div>
高亮和折叠标题逻辑
更新标题高亮状态, 记录最后一个移除高亮标题编号
1let last;
2// console.log(headings.length);
3headings.forEach(heading => {
4 // console.log('ratio', heading.target.getAttribute('id'), heading.intersectionRatio, heading.isIntersecting, HeadingCnt);
5 const idx = heading.target.getAttribute('headingIdx');
6 HeadingFlag[idx] = heading.isIntersecting;
7 if (!heading.isIntersecting)
8 {
9 last = parseInt(idx);
10 }
11});
计算当前高亮标题个数: 如果为0, 保留最后一个移除高亮标题的高亮状态
1function get_highlight_num()
2{
3 let cnt = 0;
4 for (let i = 0; i < HeadingFlag.length; ++i)
5 {
6 if (HeadingFlag[i])
7 {
8 ++cnt;
9 }
10 }
11 return cnt;
12}
13
14let cnt = get_highlight_num();
15if (cnt)
16{
17 last = -1;
18}
设置标题高亮
- 如果最后一个移除高亮标题编号不为-1, 保留其高亮状态
- 根据标题高亮状态设置标题
- 之前的高亮状态存在暂时不移除高亮的情况: 这一次遍历的例外情况已改变
1function refresh_highlight(last)
2{
3 for (let i = 0; i < HeadingFlag.length; ++i)
4 {
5 if (i === last)
6 {
7 continue;
8 }
9
10 if (HeadingFlag[i])
11 {
12 document.querySelector(`.toc-panel li[headingIdx="${i}"]`).childNodes[0].classList.add('active');
13 }
14 else
15 {
16 document.querySelector(`.toc-panel li[headingIdx="${i}"]`).childNodes[0].classList.remove('active');
17 }
18 }
19}
设置标题折叠/展开逻辑
- 遍历标题
1function refresh_fold(last)
2{
3 // console.log(last);
4 for (let i = 0; i < HeadingFlag.length; ++i)
5 {
6 const cur_toc_item = document.querySelector(`.toc-panel li[headingIdx="${i}"]`);
7
8 // 处理
9 }
10}
- 如果最后一个移除高亮标题编号不为-1, 保留其展开, 或双亲标题展开
- 若标题高亮, 展开; 否则折叠
可以展开/折叠的标题其子元素个数不为0
1// 展开/关闭子节点
2if (cur_toc_item.childElementCount !== 1)
3{
4 const toc_button = cur_toc_item.childNodes[1];
5 const toc_div = cur_toc_item.childNodes[2];
6
7 if (HeadingFlag[i] || i === last)
8 {
9 toc_button.setAttribute('aria-expanded', 'true');
10 toc_div.classList.add('show');
11 }
12 else
13 {
14 toc_button.setAttribute('aria-expanded', 'false');
15 toc_div.classList.remove('show');
16 }
17}
- 若标题高亮, 展开双亲标题
拥有子标题的节点也可以有双亲节点
1if (HeadingFlag[i] || i === last) // 展开双亲节点
2{
3 // if (i === last) console.log(1, i);
4 // console.log(cur_toc_item);
5 // console.log(cur_toc_item.parentElement.parentElement.previousElementSibling.tagName);
6 if (cur_toc_item && cur_toc_item.parentElement && cur_toc_item.parentElement.parentElement &&
7 cur_toc_item.parentElement.parentElement.previousElementSibling &&
8 cur_toc_item.parentElement.parentElement.previousElementSibling.tagName == "BUTTON")
9 {
10
11 // console.log(cur_toc_item.parentElement.parentElement);
12 // console.log(cur_toc_item.parentElement.parentElement.previousElementSibling);
13 const toc_div = cur_toc_item.parentElement.parentElement;
14 const toc_button = toc_div.previousElementSibling;
15
16 toc_div.classList.add('show');
17 toc_button.setAttribute('aria-expanded', 'true');
18
19 let sub_item = toc_div.parentElement;
20
21 while (sub_item && sub_item.parentElement && sub_item.parentElement.parentElement &&
22 sub_item.parentElement.parentElement.previousElementSibling &&
23 sub_item.parentElement.parentElement.previousElementSibling.tagName == "BUTTON")
24 {
25 // console.log(sub_item.parentElement.parentElement);
26 // console.log(sub_item.parentElement.parentElement.previousElementSibling.childNodes[2]);
27
28 const toc_div_sub = sub_item.parentElement.parentElement;
29 const toc_button_sub = toc_div_sub.previousElementSibling;
30
31 toc_div_sub.classList.add('show');
32 toc_button_sub.setAttribute('aria-expanded', 'true');
33
34 sub_item = toc_div_sub.parentElement;
35 }
36 }
37}
完整代码
1function refresh_highlight(last)
2{
3 for (let i = 0; i < HeadingFlag.length; ++i)
4 {
5 if (i === last)
6 {
7 continue;
8 }
9
10 if (HeadingFlag[i])
11 {
12 document.querySelector(`.toc-panel li[headingIdx="${i}"]`).childNodes[0].classList.add('active');
13 }
14 else
15 {
16 document.querySelector(`.toc-panel li[headingIdx="${i}"]`).childNodes[0].classList.remove('active');
17 }
18 }
19}
20
21function refresh_fold(last)
22{
23 // console.log(last);
24 for (let i = 0; i < HeadingFlag.length; ++i)
25 {
26 const cur_toc_item = document.querySelector(`.toc-panel li[headingIdx="${i}"]`);
27
28 // console.log(cur_toc_item);
29 // console.log(cur_toc_item.childElementCount);
30
31 // 展开/关闭子节点
32 if (cur_toc_item.childElementCount !== 1)
33 {
34 // console.log(cur_toc_item.childNodes[1]);
35
36 const toc_button = cur_toc_item.childNodes[1];
37 const toc_div = cur_toc_item.childNodes[2];
38
39 if (HeadingFlag[i] || i === last)
40 {
41 // if (i === last) console.log(2, i);
42 toc_button.setAttribute('aria-expanded', 'true');
43 toc_div.classList.add('show');
44 }
45 else
46 {
47 toc_button.setAttribute('aria-expanded', 'false');
48 toc_div.classList.remove('show');
49 }
50 }
51
52 if (HeadingFlag[i] || i === last) // 展开双亲节点
53 {
54 // if (i === last) console.log(1, i);
55 // console.log(cur_toc_item);
56 // console.log(cur_toc_item.parentElement.parentElement.previousElementSibling.tagName);
57 if (cur_toc_item && cur_toc_item.parentElement && cur_toc_item.parentElement.parentElement &&
58 cur_toc_item.parentElement.parentElement.previousElementSibling &&
59 cur_toc_item.parentElement.parentElement.previousElementSibling.tagName == "BUTTON")
60 {
61
62 // console.log(cur_toc_item.parentElement.parentElement);
63 // console.log(cur_toc_item.parentElement.parentElement.previousElementSibling);
64 const toc_div = cur_toc_item.parentElement.parentElement;
65 const toc_button = toc_div.previousElementSibling;
66
67 toc_div.classList.add('show');
68 toc_button.setAttribute('aria-expanded', 'true');
69
70 let sub_item = toc_div.parentElement;
71
72 while (sub_item && sub_item.parentElement && sub_item.parentElement.parentElement &&
73 sub_item.parentElement.parentElement.previousElementSibling &&
74 sub_item.parentElement.parentElement.previousElementSibling.tagName == "BUTTON")
75 {
76 // console.log(sub_item.parentElement.parentElement);
77 // console.log(sub_item.parentElement.parentElement.previousElementSibling.childNodes[2]);
78
79 const toc_div_sub = sub_item.parentElement.parentElement;
80 const toc_button_sub = toc_div_sub.previousElementSibling;
81
82 toc_div_sub.classList.add('show');
83 toc_button_sub.setAttribute('aria-expanded', 'true');
84
85 sub_item = toc_div_sub.parentElement;
86 }
87 }
88 }
89 }
90}
91
92function get_highlight_num()
93{
94 let cnt = 0;
95 for (let i = 0; i < HeadingFlag.length; ++i)
96 {
97 if (HeadingFlag[i])
98 {
99 ++cnt;
100 }
101 }
102 return cnt;
103}
104
105function unfold_headings(headings)
106{
107 let last;
108 // console.log(headings.length);
109 headings.forEach(heading => {
110 // console.log('ratio', heading.target.getAttribute('id'), heading.intersectionRatio, heading.isIntersecting, HeadingCnt);
111 const idx = heading.target.getAttribute('headingIdx');
112 HeadingFlag[idx] = heading.isIntersecting;
113 if (!heading.isIntersecting)
114 {
115 last = parseInt(idx);
116 }
117 });
118
119 let cnt = get_highlight_num();
120 if (cnt)
121 {
122 last = -1;
123 }
124 refresh_highlight(last);
125 refresh_fold(last);
126}