詳細講解js實現電梯導航

2023-08-31 15:00:39

場景

對於某一個頁面內容繁多,
如果我們捲動的時間較長,為了增加使用者體驗。
我們需要實現點選某一個按鈕,然後捲動到對應的區域。
捲動的時候,右側對應的分類實現高亮
其實,這個功能就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>