历史搜索

JS如何实现页面导航与内容相互锚定?

游客2024-09-17 12:03:01
目录文章目录
  1. 前言
  2. 思路
  3. 实现
  4. 总结

前言

在日常的学习和工作中,经常会浏览的这样一种网页,它的结构为左侧是侧边栏,右侧是内容区域,当点击左侧的侧边栏上的目录时,右侧的内容区域会自动滚动到该目录所对应的内容区域;当滚动内容区域时,侧边栏上对应的目录也会高亮。

恰巧最近需要写个类似的小玩意,简单的做下笔记,为了避免有人只熟悉 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);

效果如下:

JS如何实现页面导航与内容相互锚定? 1

总结

目前功能已经实现,下面把完整的代码贴出来:

<!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>
标签:JavaScript