前言
在日常的学习和工作中,经常会浏览的这样一种网页,它的结构为左侧是侧边栏,右侧是内容区域,当点击左侧的侧边栏上的目录时,右侧的内容区域会自动滚动到该目录所对应的内容区域;当滚动内容区域时,侧边栏上对应的目录也会高亮。
恰巧最近需要写个类似的小玩意,简单的做下笔记,为了避免有人只熟悉 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>