# TOC (title of content) - 将 HTML 中的标题生成目录
# 1. 介绍
根据 HTML 中的所有标题生成文章目录:
- 点击文章目录中的链接,页面可以滚动到对应标题的位置
- 页面滚动时,文章目录也需要同步:
- 页面视口最上面的标题对应的链接 要 高亮显示
- 链接过多时,要让高亮链接滚动到容器的中间
# 2. 点击目录,页面滚动
页面:
<div class="page">
<h1>Vue 组件化编程</h1>
<h2 id="id_1-0">1. 模块与组件、模块化与组件化</h2>
<h3 id="id_1-1">1.1. 模块</h3>
<h3 id="id_1-2">1.2. 组件</h3>
<h3 id="id_1-3">1.3. 模块化</h3>
<h3 id="id_1-4">1.4. 组件化</h3>
<h2 id="id_2-0">2. 非单文件组件</h2>
<h3 id="id_2-1">2.1. 基本使用</h3>
<h3 id="id_2-2">2.2. 注意点</h3>
<h3 id="id_2-3">2.3. 组件的嵌套</h3>
<h3 id="id_2-4">2.4. VueComponent</h3>
<h3 id="id_2-5">2.5. VueComponent 与 Vue 的关系</h3>
<h2 id="id_3-0">3. 单文件组件</h2>
</div>
抽取对象:
const headerNodeList = document.querySelectorAll('.page > h2, .page > h3');
let headers = Array.from(headerNodeList).map((element) => {
const { id, textContent: title, nodeName } = element;
return {
id,
title,
level: Number(nodeName.substring(1))
}
});
// --
headers = [
{ id: 'id_1-0', title: '1. 模块与组件、模块化与组件化', level: 2 },
{ id: 'id_1-1', title: '1.1. 模块', level: 3 },
{ id: 'id_1-2', title: '1.2. 组件', level: 3 },
{ id: 'id_1-3', title: '1.3. 模块化', level: 3 },
{ id: 'id_1-4', title: '1.4. 组件化', level: 3 },
{ id: 'id_2-0', title: '2. 非单文件组件', level: 2 },
{ id: 'id_2-1', title: '2.1. 基本使用', level: 3 },
{ id: 'id_2-2', title: '2.2. 注意点', level: 3 },
{ id: 'id_2-3', title: '2.3. 组件的嵌套', level: 3 },
{ id: 'id_2-4', title: '2.4. VueComponent', level: 3 },
{ id: 'id_2-5', title: '2.5. VueComponent 与 Vue 的关系', level: 3 },
{ id: 'id_3-0', title: '3. 单文件组件', level: 2 },
]
目录:
<!-- 根据 headers 构造目录 -->
<div class="toc">
<a href="#id_1-0" class="toc-item level2 active">1. 模块与组件、模块化与组件化</a>
<a href="#id_1-1" class="toc-item level3">1.1. 模块</a>
<a href="#id_1-2" class="toc-item level3">1.2. 组件</a>
<a href="#id_1-3" class="toc-item level3">1.3. 模块化</a>
<a href="#id_1-4" class="toc-item level3">1.4. 组件化</a>
<a href="#id_2-0" class="toc-item level2">2. 非单文件组件</a>
<a href="#id_2-1" class="toc-item level3">2.1. 基本使用</a>
<a href="#id_2-2" class="toc-item level3">2.2. 注意点</a>
<a href="#id_2-3" class="toc-item level3">2.3. 组件的嵌套</a>
<a href="#id_2-4" class="toc-item level3">2.4. VueComponent</a>
<a href="#id_2-5" class="toc-item level3">2.5. VueComponent 与 Vue 的关系</a>
<a href="#id_3-0" class="toc-item level2">3. 单文件组件</a>
</div>
<style>
.level3 { margin-left: 2em; }
.level4 { margin-left: 4em; }
.level5 { margin-left: 6em; }
</style>
<script>
// 点击 link 后,
// 当前 link 元素,添加 active 样式类,
// 其他 link 元素,删除 active 样式类
document.querySelector('.toc').addEventListener('click', (event) => {
const { target } = event;
if (!target.classList.contains('toc-item')) {
return;
}
const tocLinks = target.parentElement.children();
for (let i = 0, len = tocLinks.length; i < len; i++) {
tocLinks[i].classList.remove('active');
}
target.classList.add('active');
});
</script>
# 3. 页面滚动,目录同步
document.addEventListener('scroll', () => {
// 找到视口顶部的 header 元素(<h2>/<h3>/<hn>)
const firstHeader = getFirstHeaderInViewport();
// 找到对应的目录链接
const tocLink = getTocLinkByHeaderId(firstHeader.id);
// 让目录中对应的链接高亮
highlightTocLink(tocLink);
// 目录中的高亮链接滚动到容器的中间
scrollToCenterOfParent(tocLink);
});
function getFirstHeaderInViewport() {
const headerElements = document.querySelectorAll('.page > h2, .page > h3');
let topHeaderElement = null;
for (let i = 0, len = headerElements.length; i < len; i++) {
const headerElement = headerElements[i];
const topInViewport = headerElement.getBoundingClientRect().top;
if (topInViewport > 0 && topInViewport < 40) {
topHeaderElement = headerElement;
break;
}
}
return topHeaderElement;
}
function getTocLinkByHeaderId(id) {
return document.querySelector(`.toc > [href="#${id}"]`);
}
function highlightTocLink(tocLink) {
const tocLinks = tocLink.parentElement.children();
for (let i = 0, len = tocLinks.length; i < len; i++) {
tocLinks[i].classList.remove('active');
}
tocLink.classList.add('active');
}
function scrollToCenterOfParent(target) {
const parent = target.parentElement;
// 元素的高度
const parentHeight = parent.clientHeight;
// 滚动条在 y 轴移动的距离
const parentScrollTop = parent.scrollTop;
// 元素 与 父元素顶部 的距离
const targetOffsetTop = target.offsetTop;
// tocItem 垂直居中需要 toc 滚动的高度
const parentScrollY = targetOffsetTop - parentHeight / 2;
parent.scrollTo(0, parentScrollY);
}
上一篇: 下一篇:
本章目录