TL;DR
Oto sekret: w swojej następnej aplikacji możesz nie potrzebować zdarzeń scroll
. Korzystając z IntersectionObserver
,
pokazuję, jak uruchomić zdarzenie niestandardowe, gdy elementy position:sticky
zostaną naprawione lub przestaną się przyklejać. Bez detektorów przewijania. Dostępnych jest nawet świetna wersja demonstracyjna, która to potwierdza:
Przedstawiamy wydarzenie sticky-change
Jednym z praktycznych ograniczeń stosowania stałej pozycji CSS jest to, że nie zapewnia ona sygnału platformy wskazującego, czy usługa jest aktywna. Innymi słowy, nie ma żadnego zdarzenia, na podstawie którego można by sprawdzić, kiedy element przestanie być przyklejony.
Oto przykład, który poprawia element <div class="sticky">
o 10 pikseli od góry kontenera nadrzędnego:
.sticky {
position: sticky;
top: 10px;
}
Czy nie byłoby miło, gdyby przeglądarka informowała o tym, że elementy dotykają tego znacznika?
Wygląda na to, że nie tylko ja tak uważam. Sygnał dla position:sticky
może wykorzystać wiele przypadków użycia:
- Zastosuj cień do przyklejonego banera.
- Gdy użytkownik czyta treści, rejestruj działania analityczne, aby śledzić postępy.
- Gdy użytkownik przewija stronę, aktualizuj pływający widżet TOC do bieżącej sekcji.
Mając na uwadze te przypadki użycia, opracowaliśmy cel końcowy: utworzenie zdarzenia wywoływanego po naprawieniu elementu position:sticky
. Nazwijmy to zdarzeniem 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;
});
Demonstracja używa tego zdarzenia do nadania nagłówka cieniu po naprawieniu. Zmieni się też nowy tytuł u góry strony.
Przewijanie bez zdarzeń przewijania?
Użyjmy terminologii, by przywołać te nazwy w dalszej części posta:
- Kontener z przewijaniem – obszar treści (widoczny widoczny obszar) zawierający listę „postów na blogu”.
- Nagłówki – niebieski tytuł w każdej sekcji z tagiem
position:sticky
. - Sekcje przyklejone – każda sekcja treści. Tekst przewijany pod przyklejonymi nagłówkami.
- „Tryb przyklejony” – gdy do elementu stosuje się
position:sticky
.
Aby dowiedzieć się, który nagłówek przechodzi w „tryb przyklejony”, potrzebujemy sposobu na określenie przesunięcia kontenera przewijania. Dzięki temu będziemy mogli obliczyć wyświetlany nagłówek. Robi się to jednak dość trudno bez zdarzeń scroll
:) Innym problemem jest to, że position:sticky
usuwa element z układu po naprawieniu.
Dlatego bez zdarzeń przewijania utraciliśmy możliwość wykonywania obliczeń związanych z układem nagłówków.
Dodawanie zastępczego modelu DOM w celu określenia pozycji przewijania
Zamiast zdarzeń scroll
użyjemy IntersectionObserver
do określania, kiedy headers przejdą w tryb przyklejenia lub z niego wyjdą. Dodanie 2 węzłów (czyli strażników) w każdej sekcji przyklejonej, 1 u góry i u dołu, będzie służyć jako punkty pośrednie do ustalania pozycji przewijania. Gdy te znaczniki dochodzą do kontenera i je opuszczają, ich widoczność się zmienia, a obserwacja interakcji wywołuje wywołanie zwrotne.
Potrzebujemy 2 strażników, by uwzględnić 4 przypadki przewijania w górę i w dół:
- Przewijanie w dół – nagłówek staje się przyklejony, gdy jego górny czujnik przecina górną część kontenera.
- Przewijanie w dół – nagłówek opuszcza tryb przyklejenia, gdy dochodzi do dołu sekcji, a jego dolny element strażniczy przekracza górną część kontenera.
- Przewijanie w górę – nagłówek wyłącza tryb klawiszy trwałych, gdy górny słupek przewija się z powrotem do widoku z góry.
- Przewijanie w górę – nagłówek staje się przyklejony, a jego dolny strażnik powraca do widoku z góry.
Warto zobaczyć screencasty 1–4 w kolejności od wystąpienia:
Usługa porównywania cen
Strażnicy znajdują się na górze i na dole każdej sekcji.
Element .sticky_sentinel--top
znajduje się na górze nagłówka, a .sticky_sentinel--bottom
– na dole sekcji:
: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;
}
Konfiguracja obserwatorów skrzyżowań
Obserwatorzy skrzyżowań asynchronicznie monitorują zmiany na przecięciu elementu docelowego z widocznym obszarem dokumentu lub kontenerem nadrzędnym. W naszym przypadku widzimy skrzyżowania z kontenerem nadrzędnym.
Magiczny algorytm to IntersectionObserver
. Każdy strażnik otrzymuje IntersectionObserver
, aby obserwować jego widoczność w kontenerze przewijania. Gdy czujnik przewija się w widocznym obszarze, wiemy, że nagłówek został naprawiony lub przestanie być przyklejony. Podobnie gdy strażnik opuści
widoczny obszar.
Najpierw skonfiguruję obserwatorów dla strażników nagłówka i stopki:
/**
* 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'));
Następnie dodałem obserwatora, który będzie się uruchamiać, gdy elementy .sticky_sentinel--top
przejdą przez górną część kontenera z przewijaniem (w dowolnym kierunku).
Funkcja observeHeaders
tworzy strażnicy i dodaje je do każdej sekcji. Obserwator oblicza przecięcie czujnika z górą kontenera i decyduje, czy wchodzi on w widoczny obszar, czy go opuszcza. Ta informacja określa, czy nagłówek sekcji jest przyklejony.
/**
* 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));
}
Obserwator jest skonfigurowany z użyciem funkcji threshold: [0]
, tak więc jego wywołanie zwrotne jest uruchamiane, gdy tylko strażnik staje się widoczny.
Proces jest podobny w przypadku dolnego strażnika (.sticky_sentinel--bottom
).
Drugi obserwator jest tworzony, gdy stopki przechodzą przez dolną część kontenera z przewijaniem. Funkcja observeFooters
tworzy węzły ważne i dołącza je do każdej sekcji. Obserwator oblicza przecięcie czujnika z dnem kontenera i decyduje, czy wsiaduje do niego, czy go opuszcza. Ta informacja określa, czy nagłówek sekcji jest przyklejony.
/**
* 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));
}
Obserwator jest skonfigurowany z użyciem funkcji threshold: [1]
, tak więc jego wywołanie zwrotne jest uruchamiane, gdy cały węzeł znajduje się w widoku.
Mam jeszcze 2 narzędzia do uruchamiania zdarzenia niestandardowego sticky-change
i generowania strażników:
/**
* @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);
}
Znakomicie.
Ostatnia wersja demonstracyjna
Utworzyliśmy zdarzenie niestandardowe, gdy elementy z elementem position:sticky
zostaną naprawione i dodamy efekty przewijania bez użycia zdarzeń scroll
.
Podsumowanie
Często zastanawiam się, czy narzędzie IntersectionObserver
mogłoby zastąpić niektóre wzorce interfejsu użytkownika oparte na zdarzeniach scroll
, które rozwinęły się na przestrzeni lat. Okazuje się, że tak i nie. Semantyka interfejsu API IntersectionObserver
sprawia, że jest on trudny w obsłudze do wszystkiego. Ale jak pokazałam, można go używać z kilkoma interesującymi technikami.
Inny sposób wykrywania zmian stylu?
Raczej nie. Potrzebowaliśmy sposobu na obserwowanie zmian stylu elementów DOM. W interfejsach API platformy internetowej nie ma niestety żadnych funkcji, za pomocą których można obserwować zmiany stylu.
MutationObserver
to logiczny wybór, który w większości przypadków się nie sprawdza. Na przykład w wersji demonstracyjnej nasze wywołanie zwrotne pojawi się, gdy do elementu zostanie dodana klasa sticky
, a nie po zmianie obliczonego stylu elementu.
Pamiętaj, że klasa sticky
została już zadeklarowana podczas wczytywania strony.
W przyszłości do obserwowania zmian w obliczonych stylach elementu może pomóc rozszerzenie „Style Mutation Observer” (Obserwatorzy mutacji).
position: sticky
.