引文

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

恰巧最近需要写个类似的小玩意,简单的做下笔记,为了避免有人只熟悉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 class="container">
        <div id="nav">
          <div class="nav-item">目录 1</div>
          <div class="nav-item">目录 2</div>
          <div class="nav-item">目录 3</div>
          <div class="nav-item">目录 4</div>
          <div class="nav-item">目录 5</div>
          <div class="nav-item">目录 6</div>
        </div>
        <div id="content">
          <div class="content-block">内容 1</div>
          <div class="content-block">内容 2</div>
          <div class="content-block">内容 3</div>
          <div class="content-block">内容 4</div>
          <div class="content-block">内容 5</div>
          <div class="content-block">内容 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);

    最后实际效果如下

Screen-Recording-2023-06-10-at-12.31.55.gif

现在能基本实现点击左侧的导航来使右侧内容滚动到指定区域,这样完全可行,但是如果需要平滑滚动的话,该怎么来实现?

scrollTo这个函数提供了滚动方式的选项设置,指定滚动方式为平滑滚动方式,就可以实现。

content.scrollTo({
  top: contentBlocks[index].offsetTop,
  behavior: 'smooth
});

来看下效果

Screen+Recording+2023-06-10+at+13.06.24 (2).gif

发现页面的滚动确实变得平滑了,但是在点击左侧的目录后会发生抖动的情况,那么为什么会发生这样的情况?

首先观察下现象,在点击目录5后,目录5会在短暂高亮后,然后目录1开始高亮直到目录5。能够改变高亮目录的出了我们点击的时候会让目录高亮,另外一个会使目录高亮的地方就是在滚动事件函数里会根据内容所在位置来让目录高亮。

// content.addEventListener("scroll", handleScroll);

那么我们把对滚动事件的监听给去掉后,我们可以看看测试结果。

Screen+Recording+2023-06-10+at+15.21.52.gif

那么现在问题确定了,就是在滚动过程中会影响目录导航的高亮,所以在刚开始滚动的时候会首先高亮目录1,那么怎么解决?

比较直接的想法就是我在点击目录后,内容区域在滚动到对应内容区域时这段时间不触发滚动事件,自然也不会反过来锚定目录了,但是scrollTo引起内容区域的滚动是平滑滚动,需要一段时间滚动才能结束,但怎么判断滚动已经结束了呢?

这里我给出自己的思路,就是判断内容区域的scrollTop是否还在变化,如果没有变化了,那么就认为滚动过程已经结束了。

let timerId = null;

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); // 滚动结束后,记得把滚动事件函数重新绑定到scroll事件上去
      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",
      });
    }
  });
});

看看效果

Screen+Recording+2023-06-10+at+16.31.20 (1).gif

总结

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

<!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 class="container">
      <div id="nav">
        <div class="nav-item">目录 1</div>
        <div class="nav-item">目录 2</div>
        <div class="nav-item">目录 3</div>
        <div class="nav-item">目录 4</div>
        <div class="nav-item">目录 5</div>
        <div class="nav-item">目录 6</div>
      </div>
      <div id="content">
        <div class="content-block">内容 1</div>
        <div class="content-block">内容 2</div>
        <div class="content-block">内容 3</div>
        <div class="content-block">内容 4</div>
        <div class="content-block">内容 5</div>
        <div class="content-block">内容 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>

Tqing
112 声望16 粉丝