IntersectionObserver + scrollIntoView 實現電梯導航

xingba-coder發表於2024-08-06

電梯導航也被稱為錨點導航,當點選錨點元素時,頁面內相應標記的元素滾動到視口。而且頁面內元素滾動時相應錨點也會高亮。電梯導航一般把錨點放在左右兩側,類似電梯一樣。常見的電梯導航效果如下,比如一些官方文件中:

image

image

之前可能會用 getBoundingClientRect() 判斷元素是否在視口中來實現類似效果,但現在有更方便的方法了,那就是 IntersectionObserver + scrollIntoView,輕鬆實現電梯導航。

scrollIntoView() 介紹

scrollIntoView() 方法會滾動元素的父容器,使元素出現在可視區域。預設是立即滾動,沒有動畫效果。

如果要新增動畫效果,可以這麼做:

scrollIntoView({
  behavior: 'smooth'  // instant 為立即滾動
})

它還有兩個可選引數 blockinline

block 表示元素出現在視口時垂直方向與父容器的對齊方式,inline 表示元素出現在視口時水平方向與父容器的對齊方式。

他們同樣都有四個值可選 startcenterendnearest。預設為 start;

scrollIntoView({
  behavior: 'smooth',
  block:'center',
  inline:'center',
})

對於 block

  • start 將元素的頂部和滾動容器的頂部對齊。

  • center 將元素的中心和滾動容器的中心垂直對齊。

  • end 將元素的底部和滾動容器的底部對齊。

對於 inline

  • start 將元素的左側和滾動容器的左側對齊。

  • center 將元素的中心和滾動容器的中心水平對齊。

  • end 將元素的右側和容器的右側對齊。

nearest 不論是垂直方向還是水平方向,只要出現在視口任務就完成了。可以理解為以最小移動量讓元素出現在視口,(慵懶移動)。如果元素已經完全出現在視口中,則不會發生變化。

透過下面動圖來感受這個變化,下面滾動容器中有四行五列,包含了從字母 AT。點選 出現在視口 的按鈕會取三個下拉框的值作為引數來呼叫 scrollIntoView() 方法。

image

再來看看設定為 nearest 後的滾動情況

image

當字母 G 在視口內時,呼叫方法滾動容器不會發生變化。當 G 不完全在視口內,則會滾動到完全出現在視口內為止。

在這裡可以檢視這個完整例子 scrollIntoView 可選項引數實踐(codepen)

而且 scrollIntoView 相容性也很好

image

IntersectionObserver 介紹

Intersection Observer API(交叉觀察器 API) 提供了一種非同步檢測目標元素與祖先元素或頂級文件的視口相交情況變化的方法。也就是能判斷元素是否在視口中,並且能監聽元素在視口中出現的可見部分的比例,從而可以執行我們自定義的邏輯。

由於是非同步,也就不會阻塞主執行緒,效能自然比之前的頻繁執行 getBoundingClientRect() 判斷元素是否在視口中要好。

建立一個 IntersectionObserver

let options = {
  root: document.querySelector(selector),
  rootMargin: "0",
  threshold: 1.0,
};

let observer = new IntersectionObserver(callback, options);

let target = document.querySelector(selector);
observer.observe(target); //監聽目標元素

透過呼叫 IntersectionObserver 建構函式可以建立一個交叉觀察器,建構函式接收兩個引數,一個回撥函式和一個可選項。上面例子中,當元素完全出現(100%)在視口中時會呼叫回撥函式。

可選項

  • root 用作視口的元素,必須是目標的祖先。預設為瀏覽器視口。

  • rootMargin 根周圍的邊距,也就是可以限制根元素檢測視口的大小。值的方向大小和平常用的 margin 一樣,例如 "10px 20px 30px 40px"(上、右、下、左)。只不過正數是增大根元素檢測範圍,負數是減小檢測範圍。

比如設定一個可以滾動的 div 容器為根元素,寬高都為1000px。 此時設定 rootMargin:0 表示根元素檢測視口大小就是當前根元素可視區域大小,也就是 1000px * 1000px。設定 rootMargin:25% 0 25% 0 表示上下邊距為 25%,那麼檢測視口大小就是 1000px * 500px。

  • threshold 一個數字或一個數字陣列,表示目標出現在視口中達到多少百分比時,觀察器的回撥就應該執行。如果只想在能見度超過 50% 時檢測,可以使用 0.5 的值。如果希望每次能見度超過 25% 時都執行回撥,則需要指定陣列 [0, 0.25, 0.5, 0.75, 1]。預設值為 0(這意味著只要有一個畫素可見,回撥就會執行)。值為 1.0 意味著在每個畫素都可見之前,閾值不會被認為已透過。

回撥函式

當目標元素匹配了可選項中的配置後,會觸發我們定義的回撥函式

let options = {
  root: document.querySelector(selector),
  rootMargin: "0",
  threshold: 1.0,
};

let observer = new IntersectionObserver(function (entries) {
      entries.forEach(entry => {
        
      })
    }, options);

entries 表示被監聽目標元素組成的陣列,陣列裡面每個 entry 都有下列一些值

  • entry.boundingClientRect 返回目標元素的邊界資訊,值和 getBoundingClientRect() 形式一樣。

  • entry.intersectionRatio 目標元素和根元素交叉的比例,也就是出現在檢測區域的比例。

  • entry.intersectionRect 返回根和目標元素的相交區域的邊界資訊,值和 getBoundingClientRect() 形式一樣。

  • entry.isIntersecting 返回true或者fasle,表示是否出現在根元素檢測區域內

  • entry.rootBounds 返回根元素的邊界資訊,值和 getBoundingClientRect() 形式一樣。

  • entry.target 返回出現在根元素檢測區域內的目標元素

  • entry.time 返回從交叉觀察器被建立到目標元素出現在檢測區域內的時間戳

比如,要檢測目標元素有75%出現在檢測區域中就可以這樣做:

entries.forEach(entry => {
    if(entry.isIntersecting && entry.intersectionRatio>0.75){
         
    }
})

監聽目標元素

建立一個觀察器後,對一個或多個目標元素進行觀察。

let target = document.querySelector(selector);
observer.observe(target);

document.querySelectorAll('div').forEach(el => {
    observer.observe(el)
})

IntersectionObserver 的相容性也很好:

image

掌握了 IntersectionObserver + scrollIntoView 的用法,實現電梯導航就簡單了。

簡單寫一個電梯導航的 htmlcss

<div class="a" style="background:aqua;">第一章</div>
<div class="b" style="background: blueviolet;">第二章</div>
<div class="c" style="background: chartreuse;">第三章</div>
<div class="d" style="background: darkgoldenrod;">第四章</div>
<div class="e" style="background: firebrick;">第五章</div>
<div class="f" style="background: gold;">第六章</div>
<div class="g" style="background: hotpink;">第七章</div>
<ul class="rightBox">
    <li class="aLi">第一章</li>
    <li class="bLi">第二章</li>
    <li class="cLi">第三章</li>
    <li class="dLi">第四章</li>
    <li class="eLi">第五章</li>
    <li class="fLi">第六章</li>
    <li class="gLi">第七章</li>
</ul>
html,
body {
  width: 100%;
  height: 100%;
  background-color: #fff;
}

ul,li{list-style: none;}

body {
  padding: 20px 0;
}

div{
  width: 60%;
  height: 70%;
  border-radius: 10px;
  margin-left: auto;
  margin-right: auto;
  opacity: 0.4;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 30px;
  font-weight: bold;
  color: #000;
}

div+div {
  margin-top: 20px;
}

.rightBox {
  position: fixed;
  right: 20px;
  top: 50%;
  color: teal;
  transform: translatey(-50%);
}

li {
  cursor: pointer;
  box-sizing: border-box;
  border: 1px solid #fff;
  border-radius: 4px;
  padding: 8px 12px;
}

li:hover {
  background: #f5d2c4;
}

.active {
  background: #f5d2c4;
}

預覽如下:

image


第一步:點選右邊的導航選單,利用 scrollIntoView 方法使內容區域對應的元素出現在可視區域中。

    let rightBox = document.querySelector('.rightBox')
    rightBox.addEventListener('click', function (e) {
      let target = e.target || e.srcElement;
      if (target && !target.classList.contains('rightBox')) {
        document.querySelector('.' + target.className.replace('Li', '')).scrollIntoView({
          behavior: 'smooth',
          block: 'center'
        })
      }
    }, false)

image


第二步:頁面容器滾動時,當目標元素出現在檢測區域內則聯動改變對應導航的樣式。

這裡 threshold 被設定為 1,也就是當目標元素完全顯示在可視區域時執行回撥,改變導航選單的樣式。

let observer = new IntersectionObserver(function (entries) {
  entries.forEach(entry => {
    let target = document.querySelector('.' + entry.target.className + 'Li')
    if (entry.isIntersecting) { // 出現在檢測區域內
      document.querySelectorAll('li').forEach(el => {
        if(el.classList.contains('active')){
          el.classList.remove('active')
        }
      })
      if (!target.classList.contains('active')) {
        target.classList.add('active')
      }
    }
  })
}, {
  threshold: 1
})

document.querySelectorAll('div').forEach(el => {
  observer.observe(el)
})

效果如下:

image


基本要求達到了,不過在滾動過程中,還有些問題。比如連續兩個元素來回切換時,第二個元素比第一個元素在檢測區域顯示的比例更高,雖然沒達到 100%,這時候導航選單顯示還是第一個元素的。見下圖:

image


所以這裡可以控制的更細,兩個元素之間誰顯示的比例更高時就高亮誰的導航選單。

let observer = new IntersectionObserver(function (entries) {
    entries.forEach(entry => {
        if (entry.isIntersecting && entry.intersectionRatio > 0.65) {
            
        }
    })
}, {
    threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8]
})

這裡設定了 threshold: [0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8],當目標元素出現在檢測區域的比例達到 20%,30%,40%,50%,60%,70%,80% 的時候會執行回撥函式,在回撥函式里,目標元素可見並且在檢測區域顯示的比例達到 65% 時高亮導航選單。這樣效果就好些了:

image

在這裡可以檢視這個完整例子 IntersectionObserver + scrollIntoView 實現電梯導航

當然,具體還是看實際元素塊大小和業務需求來定。

如有幫助,幫忙點點贊,感謝~

相關文章