文章目录标题高亮
2023年6月12日 2024年2月13日
说明
- 设置高亮样式
css
- 使用Intersection Observer实现
JavaScript
- 文章目录设置
config/_default/markup.toml
- startLevel 2 endLevel 3 - 另一个实现思路为Scrollspy
- 移植到Doks主题
便签
- | |
---|---|
文章目录滚动高亮 | 1. 为文章目录设置类 |
2. 高亮样式 | |
3. 提供一种实现 | |
高亮样式 | 当前标题底部加横线 |
滚动高亮 | 设置元素的父元素 |
滚动高亮 | option和intersectionObserver的callback |
IntersectionObserver API | 1. 添加观察元素: observe |
2. intersectionRatio | |
3. target | |
querySelectorAll用法 | 1. 获取有指定属性的元素: “a[target]” |
2. 获取指定父元素的元素: “div > p” | |
3. 获取多个分类元素: “h2[id], h3[id]” | |
获取元素的方式 | 1. id: getElementById, 上下文要求是documnet |
2. name: getElementsByName, 一组 | |
将NodeList转换为数组 | apply |
从数组中移除元素 | filter |
设置数组初始值 | fill |
文章目录
- Doks主题通过设置startLevel和endLevel限制ToC显示的标题。endLevel上限为4,startLevel一般从2开始
- 高亮的是ToC中的链接
高亮样式
- 设置颜色
- 加粗
- 过渡
- 暗色主题
assets/scss/common/_global.scss
1.my-toc a.active { 2 color: $primary; 3 font-weight: 800; 4 transition: all .25s ease-in-out 5}
assets/scss/common/_dark.scss
1[data-dark-mode] .my-toc a.active { 2 // color: $link-color-dark; 3 color: $zdoc-highlight-dark; 4}
为ToC设置类
layouts/partials/sidebar/docs-toc.html:23
1<nav class="my-toc"> 2 {{ .TableOfContents }} 3</nav>
进入判断
1document.addEventListener('DOMContentLoaded', () => { 2 const toc = document.querySelector('.my-toc'); 3 if (!toc) return; 4 5 // 后续处理 6});
文章目录标题
获取有类my-toc的元素的链接
1const tocHeadings = toc.querySelectorAll('a');
正文标题
只关注显示在文章目录的标题, 筛选时, 要求标题的下级链接有类anchor
1const headings = Array.apply(null, document.querySelectorAll('h2[id], h3[id]')).filter(function (value, index, arr) { 2 return arr[index].querySelector('.anchor'); 3});
将文章目录标题和正文标题关联
当二者标题数一致时, 认为一一对应, 设置标号; 否则就此结束
1function addHeadingIdx(list) { 2 let i = 0; 3 list.forEach((item) => { 4 item.setAttribute('headingIdx', i++); 5 }); 6} 7 8if (tocHeadings.length !== headings.length) return; 9 10addHeadingIdx(tocHeadings); 11addHeadingIdx(headings);
搭建交叉观察框架
- 触发条件:当标题的intersectionRatio变为threshold,或者不再为threshold时,对标题进行处理
1const intersectionOptions = { 2 threshold: 1.0 3}
- 创建观察器
1const headingObserver = new IntersectionObserver(headings => { 2 headings.forEach(heading => { 3 // 处理标题 4 }) 5}, intersectionOptions);
- 将正文标题添加到观察列表
1headings.forEach((heading) => { 2 headingObserver.observe(heading); 3});
为标题添加/移除高亮
- 标题的intersectionRatio满足阈值,isIntersecting为true,intersectionRatio不满足阈值,为false
- 根据isIntersecting更新标题高亮状态
- 新增高亮标题, 或者有多个标题高亮时移除高亮: 根据高亮状态刷新, 更新高亮标题个数
添加全局数组,保存标题高亮状态,初始值为false
1let HeadingFlag; 2 3HeadingFlag = new Array(headings.length).fill(false);
添加变量,保存高亮标题个数, 初始值为0
1let HeadingCnt; 2 3HeadingCnt = 0;
进入观察器回调函数: 更新标题高亮状态; 新增高亮, 或者有多个标题高亮时移除高亮, 根据高亮状态设置标题,更新高亮个数
1function refreshHighlight() { 2 HeadingCnt = 0; 3 for (let i = 0; i < HeadingFlag.length; ++i) { 4 if (HeadingFlag[i]) { 5 ++HeadingCnt; 6 document.querySelector(`.my-toc a[headingIdx="${i}"]`).classList.add('active'); 7 } 8 else 9 { 10 document.querySelector(`.my-toc a[headingIdx="${i}"]`).classList.remove('active'); 11 } 12 } 13} 14 15const idx = heading.target.getAttribute('headingIdx'); 16if ((HeadingFlag[idx] = heading.isIntersecting) || (HeadingCnt !== 1)) { 17 refreshHighlight(); 18}
完整JavaScript代码
assets/js/toc.js
1let HeadingFlag; 2let HeadingCnt; 3 4function addHeadingIdx(list) { 5 let i = 0; 6 list.forEach((item) => { 7 item.setAttribute('headingIdx', i++); 8 }); 9} 10 11function refreshHighlight() { 12 HeadingCnt = 0; 13 for (let i = 0; i < HeadingFlag.length; ++i) { 14 if (HeadingFlag[i]) { 15 ++HeadingCnt; 16 document.querySelector(`.my-toc a[headingIdx="${i}"]`).classList.add('active'); 17 } 18 else 19 { 20 document.querySelector(`.my-toc a[headingIdx="${i}"]`).classList.remove('active'); 21 } 22 } 23} 24 25document.addEventListener('DOMContentLoaded', () => { 26 const toc = document.querySelector('.my-toc'); 27 if (!toc) return; 28 29 const tocHeadings = toc.querySelectorAll('a'); 30 const headings = Array.apply(null, document.querySelectorAll('h2[id], h3[id]')).filter(function (value, index, arr) { 31 return arr[index].querySelector('.anchor'); 32 }); 33 34 if (tocHeadings.length !== headings.length) return; 35 36 addHeadingIdx(tocHeadings); 37 addHeadingIdx(headings); 38 39 HeadingFlag = new Array(headings.length).fill(false); 40 HeadingCnt = 0; 41 42 const intersectionOptions = { 43 threshold: 1.0 44 }; 45 46 const headingObserver = new IntersectionObserver(headings => { 47 headings.forEach(heading => { 48 // console.log('ratio', heading.target.getAttribute('id'), heading.intersectionRatio, heading.isIntersecting, HeadingCnt); 49 const idx = heading.target.getAttribute('headingIdx'); 50 if ((HeadingFlag[idx] = heading.isIntersecting) || (HeadingCnt !== 1)) { 51 refreshHighlight(); 52 } 53 }); 54 }, intersectionOptions); 55 56 headings.forEach((heading) => { 57 headingObserver.observe(heading); 58 }); 59});
Doks主题执行JavaScript脚本
参照assets/highlight.js的执行
layouts/partials/footer/script-footer.html
10
1{{ $toc := resources.Get "js/toc.js" -}} 2{{ $toc := $toc | js.Build -}}
86
1<script src="{{ $toc.RelPermalink }}" defer></script>
103
1{{ $toc := $toc | minify | fingerprint "sha512" -}}
113
1<script src="{{ $toc.RelPermalink }}" integrity="{{ $toc.Data.Integrity }}" crossorigin="anonymous" defer></script>