半熟前端

軟體工程師 / 台灣人 / 在前端的路上一邊探索其他領域的可能性

前端

透過 overflow-anchor 實作 pin to bottom 元件

前言

這篇文章是看完 Implementing a pin-to-bottom scrolling element with only CSS 後,並且介紹使用 JavaScript 的做法整理而成。

現在網頁越上越常出現每次加入新的內容時,就將 scroll 的位置調整到最底下。像是 Twitch 的留言區:

或是 Youtube 的留言區:

在中文上好像還沒有類似的名詞來敘述這樣的行為,先叫做 pin to bottom 好了。

[JavaScript] 如何實作?

function append(comment) {
  const comments = document.querySelector(".comments")
  comments.appendChild(comment)
  comments.scrollTop = comments.scrollHeight
}

首先將容器設定為固定高度,並且加入 overflow-y: auto。這樣一來內容超出容器高度的時候就會變成滾動的方式。

只要每次加上新的元素,我們都呼叫一次 comments.scrollTop = comments.scrollHeight,讓 scrollTop 永遠會等於 scrollHeight 的高度就可以了。

要考慮的問題

這個實作很簡單,不過會出現一個問題,例如當使用者想要滾動回去看看其他內容的時,如果有新的留言出現,還是會被強制滾動到最下面。

為了解決這件事情,我們可以先檢查容器當前的 scrollTop,如果跟 scrollHeight 有一定的差距的話,我們就不幫使用者滑動。

if (
  comments.offsetHeight < comments.scrollHeight &&
  comments.scrollTop + comments.offsetHeight + 150 > comments.scrollHeight
) {
  comments.scrollTop = comments.scrollHeight - comments.offsetHeight
}

這邊的 offsetHeight 代表容器的高度(css height),而 scrollHeight 代表的是容器滾動的總高度,scrollTop 則是目前的滾動位置。

  1. 如果還沒有超過 offsetHeight,就先不用定義往下滑動的行為
  2. 如果使用者滾動超過 150px 的話,就不主動幫使用者滑動。

另外如果不幫使用者做滑動,通常實作上會再加上一顆按鈕或是指示,讓使用者可以跳回最新的留言。

再來是如果留言列表很長,也有可能會有效能上的問題,處理方式有幾種:

  1. 只留下最新的前 500 則留言(數字大小看實際需求)
  2. 用像是 windowing 的方式來優化。(但我不確定是否能適用在高度不定的情況下)

overflow-anchor

今天要介紹的是透過 overflow-anchor 來達到類似效果。這個屬性是從 Implementing a pin-to-bottom scrolling element with only CSS 這篇文章看到的,文章中透過 overflow-anchor 的方式來達到上述的效果。

這個屬性被定義在 CSS Scroll Anchoring Module 當中,為了有效率地讓使用者消費內容,這個草案定義了 overflow-anchor 的方式,當發生 resize 或 scroll 行為,會自動偵測需要被錨點的內容。

詳細的演算法定義在這裡。

  1. 滾動內容 S 每次滑動時,會選擇屬性不為 none 的子元素中選擇一個錨點 node。
  2. 如果 N 在滾動內容 S 裡頭,並且他沒辦法被看見的話,不會執行操作(原文寫 excluded subtree 以及 fully clipped,簡單來講就是在滾動框裡面看不到),則任何事情都不做
  3. 如果 N 在滾動內容 S 裡頭是可見的,那就選擇 N 作為錨點。
  4. 如果 N 在滾動內容 S 裡頭是半可見的話,則會對子元素繼續進行以上演算法來找到錨點。

實際上草案裡頭還定義很多行為,有興趣的話可以去看一看,不過大概理解它能夠做什麼就好了。

首先我們在容器裡頭加入一個錨點:

<ul class="comments">
  <li class="anchor"></li>
</ul>

這邊的 anchor 要定義 CSS 屬性 overflow-anchor: auto 來讓瀏覽器把它視為錨點。

.comments {
  & > .comment {
    overflow-anchor: none; // 不要當作錨點
  }

  .anchor {
    overflow-anchor: auto; // 當作錨點
  }
}

然後將 comment 的部分改用 insertBefore 確保插入在 anchor 前面。

實測結果

左邊是用一般的方式來做,右邊則是用 overflow-anchor 的方式實作,不過我發現好像要滾動一下錨點的行為才會正確運作。

額外一提,裡頭的文字內容是我之前看完伊坂幸太郎的汽油生活後寫的文章,為了方便就隨便從裡頭抓幾段複製貼上。

目前的瀏覽器支援度是 firefox 66 以及 Chrome 56,還不算很普及,不過未來要實作類似的功能或許又有新的作法了,雖然缺點在於必須要額外加上一個錨點,但可以透過純 CSS 來做到還算是蠻有趣的方法。