详细讲解js实现电梯导航
阅读原文时间:2023年08月31日阅读:5

场景

对于某一个页面内容繁多,
如果我们滚动的时间较长,为了增加用户体验。
我们需要实现点击某一个按钮,然后滚动到对应的区域。
滚动的时候,右侧对应的分类实现高亮
其实,这个功能就2个步骤:
1.点击元素高亮,滚动到对应区域
2.滚动的时候,右侧导航的分类高亮

点击当前元素高亮的实现

1.我们利用事件委托的原理:给被点击子元素的父元素绑定点击事件
2.然后移除 li 元素的激活类
3.给当前被点击的子元素添加上激活类

事件委托也称为事件代理:就是利用事件冒泡,把子元素的事件都绑定到父元素上。


<style>
  :root {
    --h:931px;
  }
  *{
    padding: 0;
    margin: 0;
  }
  .demo1{
    height:var(--h);
    background-color: antiquewhite;
  }
  .demo2{
    height:var(--h);
    background-color: aqua;
  }
  .demo3{
    height:var(--h);
    background-color: blue;
  }
  .demo4{
    height:var(--h);
    background-color:chartreuse;
  }
  .fix-post{
    position: fixed;
    right: 50px;
    bottom: 100px;
    width: 200px;
  }
  li{
    height: 40px;
    line-height: 40px;
    text-align: center;
    list-style: none;
    border-top: 1px solid #fff;
    border-left:  1px solid #fff;
    border-right:  1px solid #fff;
  }
  li:last-child{
    border-bottom: 1px solid #fff;
  }
  .liactive{
    background-color: cornsilk;
  }
</style>

<body>
  <div>
    <div class="demo1">1</div>
    <div class="demo2">2</div>
    <div class="demo3">3</div>
    <div class="demo4">4</div>
  </div>
  <ul class="fix-post" id="nav">
    <li class="liactive">1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
</body>
<script>
  let domNav= document.getElementById('nav')
  let liList=document.querySelectorAll("#nav li")
  console.log('liList', liList)
  // 给父级元素注册点击事件(利用事件委托)
  domNav.addEventListener('click',(event)=>{
    for(let i=0;i<liList.length;i++){
      // 移除所有元素的类名liactive
      liList[i].classList.remove('liactive')
    }
    console.log('event.target', event.target)
    // 给当前元素添加激活这个类名
    event.target.classList.add('liactive');
  })
</script>

点击右侧按钮,滚动到对应区域

1.给右侧的按钮添加上索引。
2.点击索引的时候 * 每个区域的高度
3.使用window.scrollTo({ })进行滚动


<script>
  let domNav= document.getElementById('nav')
  let liList=document.querySelectorAll("#nav li")
  console.log('liList', liList)
  // 给父级元素注册点击事件(利用事件委托)
  domNav.addEventListener('click',(event)=>{
    for(let i=0;i<liList.length;i++){
      // 移除所有元素的类名liactive
      liList[i].classList.remove('liactive')
      // 给右侧的按钮添加上索引
      liList[i]['index']=i
    }
    console.log('event.target', event.target.index)
    // 给当前元素添加激活这个类名
    event.target.classList.add('liactive'); 

    // 点击按钮的时候,滚动到相应的区域
    window.scrollTo({
      top:event.target.index * 931,
      behavior:"smooth" // 平滑的滚动
    })
  })
</script>

滑动到对应区域右侧按钮自动高亮

// 实现滚动的时候,右侧区域自动高亮
window.addEventListener('scroll',()=>{
  // 兼容
  let top = document.documentElement.scrollTop || document.body.scrollTop
  // 获取当前区域的下标
  let index = Math.floor(top/931)
  console.log('top,index',top,index)
  for(let i=0;i<liList.length;i++){
    // 移除所有元素的类名liactive
    liList[i].classList.remove('liactive')
  }
  // 给当前元素添加激活这个类名
  liList[index].classList.add('liactive');
},false)

发现2个问题

问题1:出现这个问题的原因是:距离顶部的高度仅仅超过第一层区域的一点,
这个时候页面显示绝大部分区域是第二层的,
但是右侧按钮显示的是当前是第1层。
怎么解决这个问题?我们给当前区域手动新增一个高度。
这个高度一般为 3/10

问题2:我们每次滚动的时候,都在移除元素激活类,然后新增。
这样不太好,没有必须要。
我们需要判断一下

优化代码[每次滚动的时候都在移除元素的激活类]

// 实现滚动的时候,右侧区域自动高亮
let index=0
window.addEventListener('scroll',()=>{
  console.log(222)
  // 兼容
  let top = (document.documentElement.scrollTop || document.body.scrollTop) + 279 //手动新增一个值
  // 如果索引不变,则不取新增或者移除类名
  if( Math.floor(top/931) != index){
    // 获取当前区域的下标
    let index = Math.floor(top/931)
    for(let i=0;i<liList.length;i++){
      // 移除所有元素的类名liactive
      liList[i].classList.remove('liactive')
    }
    // 给当前元素添加激活这个类名
    liList[index].classList.add('liactive');
  }
},false)

scroll 事件不滚动也会触发

我们每次刷新页面的时候,滚动事件都会被触发。
因为:刷新的时候可视区域距离顶部有距离。所以滚动事件会被触发;【现象】
这样就会导致初始化(可视区域距离顶部有距离)刷新页面的时候。
右侧的指示灯会切换2次(第一次html上写激活类,第二次是由于有距离触发了滚动事件)。
这样不太好。我们需要优化一下:
删除html上的激活类。如果距离为0

处理右侧的指示灯在距离顶部有距离的时候,快速切换了2次

let index
let topValue = document.documentElement.scrollTop || document.body.scrollTop
// 距离为0.显示第一个指示灯
if(topValue==0){
  liList[0].classList.add('liactive');
}

scroll 事件特别说明

在 iOS UIWebViews 中,
滚动进行时不会触发 scroll 事件;
只有当滚动结束后事件才会被触发。
参见 Bootstrap issue #16202。Safari 和 WKWebViews 则没有这个问题。
ps:刷新的时候可视区域距离顶部有距离。滚动事件也会被触发;不一定滚动才会触发

每个区域固定高度实现导航【全部代码】

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    :root {
      --h:931px;
    }
    *{
      padding: 0;
      margin: 0;
    }
    .demo1{
      height:var(--h);
      background-color: antiquewhite;
    }
    .demo2{
      height:var(--h);
      background-color: aqua;
    }
    .demo3{
      height:var(--h);
      background-color: blue;
    }
    .demo4{
      height:var(--h);
      background-color:chartreuse;
    }
    .fix-post{
      position: fixed;
      right: 50px;
      bottom: 100px;
      width: 200px;
    }
    li{
      height: 40px;
      line-height: 40px;
      text-align: center;
      list-style: none;
      border-top: 1px solid #fff;
      border-left:  1px solid #fff;
      border-right:  1px solid #fff;
    }
    li:last-child{
      border-bottom: 1px solid #fff;
    }
    .liactive{
      background-color: cornsilk;
    }
  </style>
</head>
<body>
  <div>
    <div class="demo1">1111</div>
    <div class="demo2">2</div>
    <div class="demo3">3</div>
    <div class="demo4">4</div>
  </div>
  <ul class="fix-post" id="nav">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
  </ul>
</body>
<script>
  let domNav= document.getElementById('nav')
  let liList=document.querySelectorAll("#nav li")
  console.log('liList', liList)
  // 给父级元素注册点击事件(利用事件委托)
  domNav.addEventListener('click',(event)=>{
    for(let i=0;i<liList.length;i++){
      // 移除所有元素的类名liactive
      liList[i].classList.remove('liactive')
      // 给右侧的按钮添加上索引
      liList[i]['index']=i
    }
    console.log('event.target', event.target.index)
    // 给当前元素添加激活这个类名
    event.target.classList.add('liactive'); 

    // 点击按钮的时候,滚动到相应的区域
    window.scrollTo({
      top:event.target.index * 931,
      behavior:"smooth" // 平滑的滚动
    })
  })

  let index
  let topValue = document.documentElement.scrollTop || document.body.scrollTop
  // 离为0.显示第一个指示灯
  if(topValue==0){
    liList[0].classList.add('liactive');
  }
  // 实现滚动的时候,右侧区域自动高亮
  window.addEventListener('scroll',()=>{
    console.log('scroll-触发')
    // 兼容
    let top = (document.documentElement.scrollTop || document.body.scrollTop) + 279 //手动新增一个值
    // 如果索引不变,则不取新增或者移除类名
    if( Math.floor(top/931) != index){
      // 获取当前区域的下标
      index = Math.floor(top/931)
      for(let i=0;i<liList.length;i++){
        // 移除所有元素的类名liactive
        liList[i].classList.remove('liactive')
      }
      // 给当前元素添加激活这个类名
      liList[index].classList.add('liactive');
    }
  },false)
</script>
</html>

每个区域高度不一致怎么滚动到对应的区域

虽然我们的滚动可以正确显示右侧的高亮。
点击右侧区域也可以显示到对应的区域。
但是我们每个区域的高度是一致的。
在有些情况,每个区域的高度不一致,
怎么滚动到对应的区域,这个问题怎么处理呢?
我们可以判断当前区域在哪个区间。

<body>
  <div id="cont">
    <div class="demo1">1111</div>
    <div class="demo2">2</div>
    <div class="demo3">3</div>
    <div class="demo4">4</div>
  </div>
  <ul class="fix-post" id="nav">
    <li id="demo1">1</li>
    <li id="demo2">2</li>
    <li id="demo3">3</li>
    <li id="demo4">4</li>
  </ul>
</body>


<script>
  let contDivList= document.querySelectorAll('#cont div')
  let liList=document.querySelectorAll("#nav li")
  liList.forEach(link =>{
    // 给每个元素注册点击事件
    link.addEventListener('click',(event)=>{
      // 获取被点击元素的类名
      let currentClickElement= event.target.getAttribute('id')
      // 获取对应的区域元素dom
      let currentTargetTop= document.querySelector('.' + currentClickElement)
      // 获取当前这个点击元素的距离顶部的距离
      let eleTop=  currentTargetTop.offsetTop
       // 点击按钮的时候,滚动到相应的区域
      window.scrollTo({
        top:eleTop,
        behavior:"smooth" // 平滑的滚动
      })
    })
  })
  // 实现滚动的时候,右侧区域自动高亮
  window.addEventListener('scroll',()=>{
    let top = window.scrollTop || document.documentElement.scrollTop || document.body.scrollTop;
    console.log('top', top)
    contDivList.forEach(element => {
      // 获取每个元素距离顶部的距离
      const offsetTop = element.offsetTop;
      // 获取每个元素的高度
      const offsetHeight = element.offsetHeight;
      // 判断当前内容区块是否在可视范围内
      if (top >= offsetTop && top < offsetTop + offsetHeight) {
        liList.forEach(function (link) {
          if (link.getAttribute('id') === element.getAttribute('class')) {
            link.classList.add('liactive');
          } else {
            link.classList.remove('liactive');
          }
        });
      }
    });
  },false)
</script>

咋们这种判断方式有没有问题?

有的。
const offsetTop = element.offsetTop;
console.log('offsetTop', offsetTop)
// 获取每个元素的高度
const offsetHeight = element.offsetHeight;
// 判断当前内容区块是否在可视范围内
if (top >= offsetTop && top < offsetTop + offsetHeight) {

}
这个判断是不准确的。容易出问题。
比如说:某一个区域的高度大于屏幕的可用区域并且下一个区域小于上一个区域的高度。
就可能出现问题。
下一楼层无法正确高亮(在滚动的时候)区域与高亮区域不匹配

全部代码:每个区域高度不确定导航

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    :root {
      --h:931px;
    }
    *{
      padding: 0;
      margin: 0;
    }
    .demo1{
      height: 800px;
      background-color: antiquewhite;
    }
    .demo2{
      height: 450px;
      background-color: aqua;
    }
    .demo3{
      height: 1200px;
      background-color: blue;
    }
    .demo4{
      height: 660px;
      background-color:chartreuse;
    }
    .demo5{
      height: 1000px;
      background-color:rgb(33, 58, 7);
    }
    .fix-post{
      position: fixed;
      right: 50px;
      bottom: 100px;
      width: 200px;
    }
    li{
      height: 40px;
      line-height: 40px;
      text-align: center;
      list-style: none;
      border-top: 1px solid #fff;
      border-left:  1px solid #fff;
      border-right:  1px solid #fff;
    }
    li:last-child{
      border-bottom: 1px solid #fff;
    }
    .liactive{
      background-color: cornsilk;
    }
  </style>
</head>
<body>
  <div id="cont">
    <div class="demo1">1111</div>
    <div class="demo2">2</div>
    <div class="demo3">3</div>
    <div class="demo4">4</div>
    <div class="demo5">5</div>
  </div>
  <ul class="fix-post" id="nav">
    <li id="demo1">1</li>
    <li id="demo2">2</li>
    <li id="demo3">3</li>
    <li id="demo4">4</li>
    <li id="demo5">5</li>
  </ul>
</body>
<script>
  let contDivList= document.querySelectorAll('#cont div')
  let liList=document.querySelectorAll("#nav li")
  liList.forEach(link =>{
    // 给每个元素注册点击事件
    link.addEventListener('click',(event)=>{
      // 获取被点击元素的类名
      let currentClickElement= event.target.getAttribute('id')
      // 获取对应的区域元素dom
      let currentTargetTop= document.querySelector('.' + currentClickElement)
      // 获取当前这个点击元素的距离顶部的距离
      let eleTop=  currentTargetTop.offsetTop
       // 点击按钮的时候,滚动到相应的区域
      window.scrollTo({
        top:eleTop,
        behavior:"smooth" // 平滑的滚动
      })
    })
  })

  // 实现滚动的时候,右侧区域自动高亮
  window.addEventListener('scroll',()=>{
    let top = window.scrollTop || document.documentElement.scrollTop || document.body.scrollTop;
    console.log('top', top)
    contDivList.forEach(element => {
      // 获取每个元素距离顶部的距离
      const offsetTop = element.offsetTop;
      console.log('offsetTop', offsetTop)
      // 获取每个元素的高度
      const offsetHeight = element.offsetHeight;
      // 判断当前内容区块是否在可视范围内
      if (top >= offsetTop && top < offsetTop + offsetHeight) {
        liList.forEach(function (link) {
          if (link.getAttribute('id') === element.getAttribute('class')) {
            link.classList.add('liactive');
          } else {
            link.classList.remove('liactive');
          }
        });
      }
    });
  },false)
</script>
</html>