前言
在日常的学习和工作中,经常会浏览的这样一种网页,它的结构为左侧是侧边栏,右侧是内容区域,当点击左侧的侧边栏上的目录时,右侧的内容区域会自动滚动到该目录所对应的内容区域;当滚动内容区域时,侧边栏上对应的目录也会高亮。
恰巧最近需要写个类似的小玩意,简单的做下笔记,为了避免有人只熟悉 Vue 或 React 框架中的一个框架,还是使用原生 JS 来进行实现。
思路
- 点击侧边栏上的目录时,通过获取点击的目录的类名、或 id、或 index,用这些信息作为标记,然后在内容区域查找对应的内容。
- 滚动内容区域时,根据内容区域的内容的 dom 节点获取标记,根据标记来查找目录。
实现
页面初始化
首先把 html 和 css 写成左边为目录,右边为内容的页面结构,为测试提供 ui 界面。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>目录与内容相互锚定</title>
<style>
.container {
display: flex;
flex-direction: row;
}
#nav {
width: 150px;
height: 400px;
background-color: #eee;
}
#nav .nav-item {
cursor: pointer;
}
#nav .nav-item.active {
font-weight: bold;
background-color: #f60;
}
#content {
flex: 1;
margin-left: 10px;
position: relative;
width: 300px;
height: 400px;
overflow-y: scroll;
}
.content-block {
margin-top: 25px;
height: 200px;
background-color: #eee;
}
.content-block:first-child {
margin-top: 0;
}
</style>
</head>
<body>
<div >
<div id="nav">
<div >目录 1</div>
<div >目录 2</div>
<div >目录 3</div>
<div >目录 4</div>
<div >目录 5</div>
<div >目录 6</div>
</div>
<div id="content">
<div >内容 1</div>
<div >内容 2</div>
<div >内容 3</div>
<div >内容 4</div>
<div >内容 5</div>
<div >内容 6</div>
</div>
</div>
</body>
</html>
通过点击实现内容的滚动
const nav = document.querySelector("#nav");
const navItems = document.querySelectorAll(".nav-item");
navItems[0].classList.add("active");
nav.addEventListener('click', e => {
navItems.forEach((item, index) => {
navItems[index].classList.remove("active");
if (e.target === item) {
navItems[index].classList.add("active");
content.scrollTo({
top: contentBlocks[index].offsetTop,
});
}
});
})
通过滚动内容实现导航的高亮
const content = document.querySelector("#content");
const contentBlocks = document.querySelectorAll(".content-block");
let currentBlockIndex = 0;
const handleScroll = function () {
for (let i = 0; i < contentBlocks.length; i++) {
const block = contentBlocks[i];
if (
block.offsetTop <= content.scrollTop && block.offsetTop + block.offsetHeight > content.scrollTop
) {
currentBlockIndex = i;
break;
}
}
for (let i = 0; i < navItems.length; i++) {
const item = navItems[i];
item.classList.remove("active");
}
navItems[currentBlockIndex].classList.add("active");
};
content.addEventListener("scroll", handleScroll);
效果如下:

总结
目前功能已经实现,下面把完整的代码贴出来:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>目录与内容相互锚定</title>
<style>
.container {
display: flex;
flex-direction: row;
}
#nav {
width: 150px;
height: 400px;
background-color: #eee;
}
#nav .nav-item {
cursor: pointer;
}
#nav .nav-item.active {
font-weight: bold;
background-color: #f60;
}
#content {
flex: 1;
margin-left: 10px;
position: relative;
width: 300px;
height: 400px;
overflow-y: scroll;
}
.content-block {
margin-top: 25px;
height: 200px;
background-color: #eee;
}
.content-block:first-child {
margin-top: 0;
}
</style>
</head>
<body>
<div >
<div id="nav">
<div >目录 1</div>
<div >目录 2</div>
<div >目录 3</div>
<div >目录 4</div>
<div >目录 5</div>
<div >目录 6</div>
</div>
<div id="content">
<div >内容 1</div>
<div >内容 2</div>
<div >内容 3</div>
<div >内容 4</div>
<div >内容 5</div>
<div >内容 6</div>
</div>
</div>
<script>
const content = document.querySelector("#content");
const contentBlocks = document.querySelectorAll(".content-block");
const navItems = document.querySelectorAll(".nav-item");
const nav = document.querySelector("#nav");
let timerId = null;
let currentBlockIndex = 0;
navItems[currentBlockIndex].classList.add("active");
const handleScroll = function () {
for (let i = 0; i < contentBlocks.length; i++) {
const block = contentBlocks[i];
if (
block.offsetTop <= content.scrollTop &&
block.offsetTop + block.offsetHeight > content.scrollTop
) {
currentBlockIndex = i;
break;
}
}
for (let i = 0; i < navItems.length; i++) {
const item = navItems[i];
item.classList.remove("active");
}
navItems[currentBlockIndex].classList.add("active");
};
nav.addEventListener("click", (e) => {
if (timerId) {
window.clearInterval(timerId);
}
content.removeEventListener("scroll", handleScroll);
let lastScrollPosition = content.scrollTop;
timerId = window.setInterval(() => {
const currentScrollPosition = content.scrollTop;
console.log(currentScrollPosition, lastScrollPosition);
if (lastScrollPosition === currentScrollPosition) {
content.addEventListener("scroll", handleScroll);
window.clearInterval(timerId);
}
lastScrollPosition = currentScrollPosition;
}, 150);
navItems.forEach((item, index) => {
navItems[index].classList.remove("active");
if (e.target === item) {
navItems[index].classList.add("active");
content.scrollTo({
top: contentBlocks[index].offsetTop,
behavior: "smooth",
});
}
});
});
content.addEventListener("scroll", handleScroll);
</script>
</body>
</html>