TL;DR
祕訣:下一個應用程式中不一定會用到 scroll
事件。使用
IntersectionObserver
,
我會示範在 position:sticky
元素修正完畢或停止勾選時觸發自訂事件。除了
捲動事件監聽器此外,還有精彩的示範可以證明:
隆重推出 sticky-change
活動
使用 CSS 固定式位置的其中一項實際限制,就是 未提供可得知資源啟用時間的平台信號。 換句話說,沒有可知道元素何時會變得固定 即可停止黏著
以下範例將修正<div class="sticky">
位於其父項容器的頂端:
.sticky {
position: sticky;
top: 10px;
}
如果瀏覽器在元素點擊該標記時發出通知,那該有多好?
我很明顯不是唯一的威脅
符合的道理。「position:sticky
」的信號可用來解鎖多種用途:
- 在黏貼橫幅上套用投射陰影。
- 當使用者閱讀您的內容時,記錄 Analytics 匹配即可得知 進度。
- 當使用者捲動網頁時,將浮動的 TOC 小工具更新為目前的 專區。
考量這些使用案例,我們制定了最終目標:建立能夠
修正 position:sticky
元素時會觸發。稱為
sticky-change
個事件:
document.addEventListener('sticky-change', e => {
const header = e.detail.target; // header became sticky or stopped sticking.
const sticking = e.detail.stuck; // true when header is sticky.
header.classList.toggle('shadow', sticking); // add drop shadow when sticking.
document.querySelector('.who-is-sticking').textContent = header.textContent;
});
示範會使用 這個事件會在解決陰影後形成標頭。該元件也會更新
沒有捲動事件的捲動效果?
![網頁結構。](https://developer.chrome.google.cn/static/docs/css-ui/sticky-headers/image/structure-the-page-de19d5c111bd1.png?authuser=19&hl=zh-tw)
我們來解一下這些名稱 請繼續保持:
- 捲動容器 - 包含 「網誌文章」清單。
- 標題 - 含有
position:sticky
的每個區段中的藍色標題。 - 固定式版面 - 每個內容版面。捲動至 固定式標頭。
- "固定模式":將
position:sticky
套用至元素時。
如要得知哪個標頭進入「固定模式」,我們需要一種判斷方式
捲動容器的捲動偏移。這樣我們就能
來計算目前顯示的標頭。但多虧了
若是沒有 scroll
事件,則難以採取這些做法 :) 另一個問題是
當修正後,position:sticky
會將元素從版面配置中移除。
因此,如果沒有捲動事件,我們將無法執行與版面配置相關的功能 計算標頭。
新增 dumby DOM 以決定捲動位置
我們要使用 IntersectionObserver
,而不是 scroll
事件
判斷標題進入及結束固定模式的時機。新增兩個節點
(又稱「固定式雪花」) 在每個固定式區塊,一個在頂端和一個
會成為判斷捲動位置的路徑。由於這些
標記進入並離開容器後,它們的可見性會變更,
Intersection Observer 會觸發回呼。
![未顯示 Sentinel 元素](https://developer.chrome.google.cn/static/docs/css-ui/sticky-headers/image/without-sentinel-elements-f9529d3483c91.gif?authuser=19&hl=zh-tw)
我們需要兩智慧型尖,涵蓋四個上下捲動的情況:
- 向下捲動 - 標題在頂端組合交叉線交叉時變成固定式 容器的頂端
- 向下捲動 - 標題接近底部 區段及其底部密封件會跨越容器的頂部。
- 向上捲動 - 標題會在頂端貼紙捲動時退出固定模式 所有內容。
- 向上捲動 - 標題因底部標記交叉線交叉而變為固定不動 從頂部快速深入探索
按照發生順序查看 1 到 4 部的螢幕側錄內容,有助我們達成目標:
CSS
這些硬殼位於每個部分的頂端和底部。
.sticky_sentinel--top
位於標頭頂端
本節底部還有 .sticky_sentinel--bottom
區:
![已達門檻。](https://developer.chrome.google.cn/static/docs/css-ui/sticky-headers/image/bottom-sentinel-reaching-208a13f07bd7c.png?authuser=19&hl=zh-tw)
:root {
--default-padding: 16px;
--header-height: 80px;
}
.sticky {
position: sticky;
top: 10px; /* adjust sentinel height/positioning based on this position. */
height: var(--header-height);
padding: 0 var(--default-padding);
}
.sticky_sentinel {
position: absolute;
left: 0;
right: 0; /* needs dimensions */
visibility: hidden;
}
.sticky_sentinel--top {
/* Adjust the height and top values based on your on your sticky top position.
e.g. make the height bigger and adjust the top so observeHeaders()'s
IntersectionObserver fires as soon as the bottom of the sentinel crosses the
top of the intersection container. */
height: 40px;
top: -24px;
}
.sticky_sentinel--bottom {
/* Height should match the top of the header when it's at the bottom of the
intersection container. */
height: calc(var(--header-height) + var(--default-padding));
bottom: 0;
}
設定交叉觀察器
交集觀測器以非同步方式觀察 也就是目標元素以及文件可視區域或父項容器在這個範例中 我們要觀察到與父項容器交集
魔法醬是 IntersectionObserver
每個密封件都會獲得
IntersectionObserver
,可觀察在
捲動容器。當中央針捲動到可見的可視區域時,我們就知道
標頭變得固定或停止。同樣地,傳送板離開
檢視區域
首先,針對標頭與頁尾設定觀察器:
/**
* Notifies when elements w/ the `sticky` class begin to stick or stop sticking.
* Note: the elements should be children of `container`.
* @param {!Element} container
*/
function observeStickyHeaderChanges(container) {
observeHeaders(container);
observeFooters(container);
}
observeStickyHeaderChanges(document.querySelector('#scroll-container'));
然後,我新增了一個觀察器,可在 .sticky_sentinel--top
元素通過時觸發
實作捲動容器頂端 (任一方向)。
observeHeaders
函式會建立頂層已傳送幾筆高度,並新增至
每個部分觀察器會使用
判斷該容器是否進入或離開可視區域。沒錯
資訊會決定區段標題是否固定。
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--top` become visible/invisible at the top of the container.
* @param {!Element} container
*/
function observeHeaders(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
// Started sticking.
if (targetInfo.bottom < rootBoundsInfo.top) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.bottom >= rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [0], root: container});
// Add the top sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--top');
sentinels.forEach(el => observer.observe(el));
}
觀察器設為使用 threshold: [0]
,因此其回呼會盡快觸發
才能看到 Sentinel 視窗
這套程序與底部壓桿 (.sticky_sentinel--bottom
) 的流程類似。
建立第二個觀察器,當頁尾於底部時啟動
捲動容器的其餘部分。observeFooters
函式會建立
並附加至每個區段。觀察器會計算
然後判斷它是不是
進入或離開。這項資訊會決定區段的標頭
會不會特別來電
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--bottom` become visible/invisible at the bottom of the
* container.
* @param {!Element} container
*/
function observeFooters(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
const ratio = record.intersectionRatio;
// Started sticking.
if (targetInfo.bottom > rootBoundsInfo.top && ratio === 1) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.top < rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [1], root: container});
// Add the bottom sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--bottom');
sentinels.forEach(el => observer.observe(el));
}
觀察器是透過 threshold: [1]
設定,因此會在以下情況下觸發回呼
請查看檢視區塊
最後,有兩個公用程式可觸發 sticky-change
自訂事件
並產生這些情報:
/**
* @param {!Element} container
* @param {string} className
*/
function addSentinels(container, className) {
return Array.from(container.querySelectorAll('.sticky')).map(el => {
const sentinel = document.createElement('div');
sentinel.classList.add('sticky_sentinel', className);
return el.parentElement.appendChild(sentinel);
});
}
/**
* Dispatches the `sticky-event` custom event on the target element.
* @param {boolean} stuck True if `target` is sticky.
* @param {!Element} target Element to fire the event on.
*/
function fireEvent(stuck, target) {
const e = new CustomEvent('sticky-change', {detail: {stuck, target}});
document.dispatchEvent(e);
}
這樣就大功告成了!
最終示範
已建立自訂事件時,系統會建立含有 position:sticky
的元素
修正並新增捲動效果,但不使用 scroll
事件。
結論
我經常想知道 IntersectionObserver
是否會
這項實用工具,可取代部分以 scroll
事件為基礎的 UI 模式。
多年來的發展結果是,答案是「是」或「否」。語意
IntersectionObserver
API 的其餘部分讓所有工作都難以使用,但
如這裡所示,你可以在這裡運用一些有趣的技巧
還有一種方式能偵測樣式變更嗎?
算不上是。我們需要的一種方式是觀察 DOM 元素的樣式變化。 遺憾的是,網路平台 API 並無允許您 手錶樣式變化。
MutationObserver
是邏輯的第一選擇,但不適用
大多數情況例如,在示範中,我們會在 sticky
回應後收到回呼
類別就會新增至元素,但如果元素的計算樣式變更,則不會新增類別。
提醒您,sticky
類別已在網頁載入時宣告。
日後,
「樣式變動觀察器」
適用於 Mutation Observer 的擴充功能,可用於觀察
元素的計算樣式
position: sticky
。