TL;DR
Bí mật: Bạn có thể không cần các sự kiện scroll
trong ứng dụng tiếp theo của mình. Bằng cách sử dụng IntersectionObserver
, tôi sẽ hướng dẫn bạn cách kích hoạt một sự kiện tuỳ chỉnh khi các phần tử position:sticky
được cố định hoặc khi chúng không còn hoạt động nữa. Tất cả đều không cần đến trình nghe cuộn. Thậm chí còn có bản minh hoạ tuyệt vời để chứng minh điều đó:
Giới thiệu sự kiện sticky-change
Một trong những hạn chế thực tế của việc sử dụng vị trí cố định CSS là vị trí này không cung cấp tín hiệu nền tảng để biết khi nào thuộc tính đang hoạt động. Nói cách khác, không có sự kiện nào để biết khi nào một phần tử trở thành cố định hoặc khi nào phần tử đó ngừng cố định.
Lấy ví dụ sau đây để khắc phục một <div class="sticky">
10px từ đầu vùng chứa mẹ:
.sticky {
position: sticky;
top: 10px;
}
Sẽ không tốt nếu trình duyệt thông báo khi các phần tử đạt đến điểm đó phải không?
Có vẻ như tôi không phải là người duy nhất nghĩ như vậy. Một tín hiệu cho position:sticky
có thể mở khoá một số trường hợp sử dụng:
- Áp dụng hiệu ứng bóng đổ cho biểu ngữ khi biểu ngữ cố định.
- Khi người dùng đọc qua nội dung của bạn, hãy ghi lại các lượt truy cập số liệu phân tích để biết tiến trình của họ.
- Khi người dùng cuộn trang, hãy cập nhật tiện ích Lựa chọn tốt nhất nổi cho phần hiện tại.
Với những trường hợp sử dụng này, chúng tôi đã tạo ra một mục tiêu cuối cùng: tạo một sự kiện sẽ kích hoạt khi phần tử position:sticky
được khắc phục. Hãy gọi sự kiện này là sự kiện 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;
});
Bản minh hoạ sử dụng sự kiện này để tiêu đề bóng đổ khi chúng được khắc phục. Tiêu đề mới cũng cập nhật ở đầu trang.
Hiệu ứng cuộn mà không có sự kiện cuộn?
Hãy lấy một vài thuật ngữ để tôi có thể nhắc đến những cái tên này trong suốt phần còn lại của bài đăng này:
- Vùng chứa cuộn – vùng nội dung (khung nhìn hiển thị) chứa danh sách "bài đăng trên blog".
- Tiêu đề - tiêu đề màu xanh dương trong mỗi phần có
position:sticky
. - Phần cố định – từng phần nội dung. Văn bản cuộn dưới các tiêu đề cố định.
- "Chế độ cố định" – khi
position:sticky
đang áp dụng cho phần tử.
Để biết tiêu đề nào chuyển sang "chế độ cố định", chúng ta cần một số cách xác định độ lệch cuộn của vùng chứa cuộn. Điều đó sẽ giúp chúng tôi có cách tính tiêu đề đang hiển thị. Tuy nhiên, việc này khá phức tạp khi không có sự kiện scroll
:) Vấn đề còn lại là position:sticky
sẽ xoá phần tử khỏi bố cục khi nó được khắc phục.
Vì vậy, nếu không có sự kiện cuộn, chúng tôi sẽ mất khả năng thực hiện các phép tính liên quan đến bố cục trên các tiêu đề.
Thêm DOM dumby để xác định vị trí cuộn
Thay vì các sự kiện scroll
, chúng ta sẽ sử dụng IntersectionObserver
để xác định thời điểm headers vào và thoát khỏi chế độ cố định. Việc thêm hai nút (còn gọi là người gửi) trong mỗi phần cố định, một nút ở trên cùng và một nút ở dưới cùng, sẽ đóng vai trò là điểm tham chiếu để xác định vị trí cuộn. Khi các điểm đánh dấu này vào và rời khỏi vùng chứa, chế độ hiển thị của chúng sẽ thay đổi và Trình quan sát giao lộ sẽ kích hoạt một lệnh gọi lại.
Chúng ta cần hai người canh gác để xử lý bốn trường hợp cuộn lên và xuống:
- Cuộn xuống – tiêu đề trở nên cố định khi quan sát trên cùng của nó đi qua phần trên cùng của vùng chứa.
- Cuộn xuống – tiêu đề rời khỏi chế độ cố định khi đạt đến cuối phần và giám sát dưới cùng đi qua phần trên cùng của vùng chứa.
- Cuộn lên – tiêu đề rời khỏi chế độ cố định khi giám sát trên cùng cuộn lại vào khung hiển thị từ trên cùng.
- Cuộn lên – tiêu đề trở nên cố định khi quan sát ở dưới cùng di chuyển lên lại vào khung hiển thị từ trên cùng.
Bạn nên xem bản ghi màn hình từ 1 đến 4 theo thứ tự xuất hiện:
Dịch vụ so sánh giá (CSS)
Các vệ sĩ được đặt ở trên cùng và dưới cùng của mỗi phần.
.sticky_sentinel--top
nằm ở đầu tiêu đề trong khi .sticky_sentinel--bottom
nằm ở cuối phần:
: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;
}
Thiết lập Quan sát giao lộ
Trình quan sát giao điểm quan sát không đồng bộ các thay đổi trong giao điểm của một phần tử mục tiêu và khung nhìn tài liệu hoặc một vùng chứa mẹ. Trong trường hợp này, chúng ta đang quan sát các điểm giao cắt với một vùng chứa mẹ.
Loại sốt thần kỳ là IntersectionObserver
. Mỗi giám sát sẽ nhận được một IntersectionObserver
để quan sát chế độ hiển thị giao lộ của nó trong vùng chứa cuộn. Khi một người giám sát cuộn vào khung nhìn đang hiển thị, chúng ta biết rằng một tiêu đề sẽ bị sửa hoặc không còn cố định nữa. Tương tự như vậy, khi một người gửi thoát khỏi khung nhìn.
Trước tiên, tôi thiết lập trình quan sát cho các quan sát ở đầu trang và chân trang:
/**
* 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'));
Sau đó, tôi thêm một trình quan sát để kích hoạt khi các phần tử .sticky_sentinel--top
đi qua phần trên cùng của vùng chứa cuộn (theo một trong hai hướng).
Hàm observeHeaders
tạo các quan điểm hàng đầu và thêm các chỉ số đó vào từng phần. Đối tượng tiếp nhận dữ liệu tính toán giao điểm của quan điểm với phần trên của vùng chứa và quyết định nó sẽ vào hay ra khỏi khung nhìn. Thông tin đó xác định xem tiêu đề mục có được giữ lại hay không.
/**
* 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));
}
Đối tượng tiếp nhận dữ liệu được định cấu hình bằng threshold: [0]
để lệnh gọi lại của nó kích hoạt ngay khi người gửi xuất hiện.
Quá trình này tương tự như quan sát dưới cùng (.sticky_sentinel--bottom
). Trình quan sát thứ hai được tạo để kích hoạt khi chân trang đi qua phần dưới cùng của vùng chứa cuộn. Hàm observeFooters
tạo các nút Sentinel và đính kèm các nút đó vào từng phần. Đối tượng tiếp nhận dữ liệu tính toán giao điểm của quan điểm với phần đáy của vùng chứa và quyết định xem nó đi vào hay ra khỏi. Thông tin đó xác định liệu tiêu đề mục có cố định hay không.
/**
* 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));
}
Trình quan sát được định cấu hình bằng threshold: [1]
để lệnh gọi lại của nó kích hoạt khi toàn bộ nút nằm trong khung hiển thị.
Cuối cùng, có hai tiện ích để kích hoạt sự kiện tuỳ chỉnh sticky-change
và tạo nhân viên giám sát:
/**
* @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);
}
Vậy là xong!
Bản minh hoạ cuối cùng
Chúng tôi đã tạo một sự kiện tuỳ chỉnh khi các phần tử có position:sticky
trở thành
cố định và thêm hiệu ứng cuộn mà không cần sử dụng sự kiện scroll
.
Kết luận
Tôi thường thắc mắc liệu IntersectionObserver
có phải là một công cụ hữu ích để thay thế một số mẫu giao diện người dùng dựa trên sự kiện scroll
đã phát triển trong nhiều năm qua hay không. Hoá ra câu trả lời là có và không. Ngữ nghĩa của API IntersectionObserver
khiến việc sử dụng API cho mọi thứ trở nên khó khăn. Nhưng như tôi trình bày ở đây, bạn có thể sử dụng công cụ này cho một số kỹ thuật thú vị.
Một cách khác để phát hiện các thay đổi về kiểu?
Thực ra là không. Điều chúng tôi cần là một cách để quan sát những thay đổi về kiểu trên phần tử DOM. Rất tiếc là không có API nền tảng web nào cho phép bạn xem những thay đổi về kiểu.
MutationObserver
là lựa chọn đầu tiên về mặt logic nhưng không hiệu quả trong hầu hết các trường hợp. Ví dụ: trong bản minh hoạ, chúng ta sẽ nhận được lệnh gọi lại khi lớp sticky
được thêm vào một phần tử, chứ không phải khi kiểu đã tính của phần tử thay đổi.
Hãy nhớ rằng lớp sticky
đã được khai báo khi tải trang.
Trong tương lai, tiện ích "Trình quan sát đột biến kiểu" cho Trình quan sát đột biến có thể hữu ích khi quan sát các thay đổi đối với kiểu đã tính toán của phần tử.
position: sticky
.