TL;DR
Berikut rahasianya: Anda mungkin tidak memerlukan peristiwa scroll
di aplikasi berikutnya. Dengan menggunakan
IntersectionObserver
,
saya akan menunjukkan cara memicu peristiwa kustom saat elemen position:sticky
menjadi tetap atau saat berhenti menempel. Semua tanpa
penggunaan pemroses scroll. Bahkan ada demo keren untuk membuktikannya:
Memperkenalkan acara sticky-change
Salah satu batasan praktis penggunaan posisi melekat CSS adalah tidak memberikan sinyal platform untuk mengetahui kapan properti aktif. Dengan kata lain, tidak ada peristiwa yang perlu diketahui kapan suatu elemen menjadi melekat atau berhenti melekat.
Ambil contoh berikut, yang memperbaiki <div class="sticky">
10 piksel dari
bagian atas penampung induknya:
.sticky {
position: sticky;
top: 10px;
}
Bukankah lebih menyenangkan jika browser diberi tahu ketika elemen mencapai tanda itu?
Rupanya saya bukan satu-satunya
yang berpikir demikian. Sinyal untuk position:sticky
dapat membuka sejumlah kasus penggunaan:
- Terapkan drop shadow ke banner selagi menempel.
- Saat pengguna membaca konten Anda, catat hit analitik untuk mengetahui progresif.
- Saat pengguna men-scroll halaman, perbarui widget TOC mengambang ke bagian saat ini.
Dengan mempertimbangkan kasus penggunaan ini, kami telah membuat tujuan akhir: membuat peristiwa yang
diaktifkan saat elemen position:sticky
menjadi tetap. Sebut saja
Peristiwa 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;
});
Demo menggunakan peristiwa ini untuk memberi header bayangan jatuh saat diperbaiki. Hal ini juga memperbarui judul baru di bagian atas halaman.
Efek scroll tanpa peristiwa scroll?
Mari kita bahas beberapa terminologi agar saya dapat merujuk ke nama-nama ini di seluruh postingan:
- Penampung scroll - area konten (area tampilan yang terlihat) yang berisi daftar "postingan blog".
- Header - judul biru di setiap bagian yang memiliki
position:sticky
. - Bagian melekat - setiap bagian konten. Teks yang bergulir di bawah header melekat.
- "Mode melekat" - saat
position:sticky
diterapkan ke elemen.
Untuk mengetahui header mana yang memasuki "mode lekat", kita memerlukan beberapa cara untuk menentukan
offset scroll container scroll. Itu akan memberi
kita cara untuk
untuk menghitung header yang saat ini ditampilkan. Namun, kita akan sangat
sulit dilakukan tanpa peristiwa scroll
:) Masalah lainnya adalah
position:sticky
menghapus elemen dari tata letak saat menjadi tetap.
Jadi, tanpa peristiwa scroll, kita kehilangan kemampuan untuk melakukan penghitungan terkait tata letak pada header.
Menambahkan DOM dummy untuk menentukan posisi scroll
Sebagai ganti peristiwa scroll
, kita akan menggunakan IntersectionObserver
untuk
menentukan kapan headers masuk dan keluar dari mode lekat. Menambahkan dua node
(disebut juga penjaga) di setiap bagian yang melekat, satu di atas dan satu di bagian atas.
di bagian bawah, akan bertindak sebagai
titik jalan untuk mencari tahu posisi {i>scroll<i}. Saat penanda ini
memasuki dan keluar dari penampung, visibilitasnya akan berubah dan
Intersection Observer akan memicu callback.
Kita memerlukan dua sentinel untuk mencakup empat kasus scroll ke atas dan ke bawah:
- Men-scroll ke bawah - header menjadi melekat saat sentinel atasnya bersilangan bagian atas container.
- Men-scroll ke bawah - header keluar dari mode lekat saat mencapai bagian bawah bagian dan sentinel bawahnya bersilangan dengan bagian atas kontainer.
- Men-scroll ke atas - header keluar dari mode melekat saat sentinel atasnya di-scroll kembali ke tampilan dari atas.
- Men-scroll ke atas - header menjadi lengket saat sentinel bawahnya bersilangan ke belakang terlihat dari atas.
Sebaiknya lihat screencast 1-4 dalam urutan terjadinya:
CSS
Penjaga ditempatkan di bagian atas dan bawah setiap bagian.
.sticky_sentinel--top
berada di bagian atas header, sedangkan
.sticky_sentinel--bottom
berada di bagian bawah bagian:
: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;
}
Menyiapkan Pengamat Persimpangan
Pengamat Persimpangan secara asinkron mengamati perubahan pada perpotongan elemen target dan area pandang dokumen atau penampung induk. Dalam kasus kita, kita mengamati persimpangan dengan kontainer induk.
Saus ajaibnya adalah IntersectionObserver
. Setiap penjaga mendapatkan
IntersectionObserver
untuk mengamati visibilitas persimpangannya dalam
penampung scroll. Saat sentinel men-scroll ke area pandang yang terlihat, kita tahu bahwa header menjadi tetap atau berhenti melekat. Demikian pula, saat sentinel keluar dari area pandang.
Pertama, saya menyiapkan observer untuk sentinel header dan footer:
/**
* 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'));
Kemudian, saya menambahkan observer untuk diaktifkan saat elemen .sticky_sentinel--top
melewati
bagian atas penampung scroll (ke arah mana pun).
Fungsi observeHeaders
membuat sentinel teratas dan menambahkannya ke
setiap bagian. Observer menghitung persimpangan sentinel dengan
bagian atas penampung dan memutuskan apakah sentinel tersebut memasuki atau keluar dari area pandang. Bahwa
informasi menentukan apakah
{i>header<i} bagian melekat atau tidak.
/**
* 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));
}
Pengamat dikonfigurasi dengan threshold: [0]
sehingga callback-nya diaktifkan segera
setelah sentinel terlihat.
Prosesnya serupa untuk sentinel bawah (.sticky_sentinel--bottom
).
Pengamat kedua dibuat untuk dipicu ketika {i>footer<i} melewati bagian bawah
dari container scroll. Fungsi observeFooters
membuat
sentinel dan melampirkannya ke setiap bagian. Pengamat menghitung
perpotongan antara sentinel dengan bagian bawah
kontainer dan memutuskan apakah
masuk atau keluar. Informasi tersebut menentukan apakah header bagian
tetap atau tidak.
/**
* 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));
}
Observer dikonfigurasi dengan threshold: [1]
sehingga callback-nya diaktifkan saat
seluruh node dalam tampilan.
Terakhir, ada dua utilitas saya untuk mengaktifkan peristiwa kustom sticky-change
dan menghasilkan sentinel:
/**
* @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);
}
Selesai.
Demo akhir
Kami membuat peristiwa kustom saat elemen dengan position:sticky
menjadi
memperbaiki dan menambahkan efek scroll tanpa menggunakan peristiwa scroll
.
Kesimpulan
Saya sering bertanya-tanya apakah IntersectionObserver
akan
menjadi alat yang berguna untuk menggantikan beberapa pola UI berbasis peristiwa scroll
yang
telah dikembangkan selama bertahun-tahun. Ternyata jawabannya ya dan tidak. Semantik
dari IntersectionObserver
API membuatnya sulit digunakan untuk semuanya. Tapi ketika
yang telah saya tunjukkan di sini, Anda dapat menggunakannya
untuk beberapa teknik menarik.
Cara lain untuk mendeteksi perubahan gaya?
Tidak juga. Yang kita butuhkan adalah cara untuk mengamati perubahan gaya pada elemen DOM. Sayangnya, tidak ada satu pun di API platform web yang memungkinkan Anda untuk perubahan gaya smartwatch.
MutationObserver
akan menjadi pilihan pertama yang logis, tetapi tidak berfungsi untuk
sebagian besar kasus. Misalnya, dalam demo, kita akan menerima callback saat sticky
ditambahkan ke elemen, tetapi tidak ditambahkan saat gaya terkomputasi elemen berubah.
Ingat bahwa class sticky
sudah dideklarasikan saat halaman dimuat.
Di masa mendatang, ekstensi "Style Mutation Observer" ke Mutation Observer mungkin berguna untuk mengamati perubahan pada gaya yang dihitung elemen.
position: sticky
.