TL;DR
Oto sekret: możesz nie potrzebować zdarzeń scroll
w następnej aplikacji. Pokażę, jak za pomocą IntersectionObserver
uruchomić zdarzenie niestandardowe, gdy elementy position:sticky
zostaną naprawione lub przestaną się przyklejać. A to wszystko bez
detektorów przewijania. Możesz też skorzystać z imponującej prezentacji:
Przedstawiamy wydarzenie sticky-change
Jednym z praktycznych ograniczeń korzystania z przyklejonej pozycji CSS jest to, że nie dostarcza ona sygnału platformy wskazującej, kiedy usługa jest aktywna. Inaczej mówiąc, nie ma żadnego zdarzenia, w którym można określić, kiedy element staje się przyklejony lub gdy przestaje się przyklejać.
Przeanalizujmy ten przykład, który pokazuje, jak element <div class="sticky">
umieszcza się w odległości 10 pikseli od górnej krawędzi kontenera nadrzędnego:
.sticky {
position: sticky;
top: 10px;
}
Czy nie byłoby dobrze, gdyby przeglądarka informowała użytkowników o tym, że dany element trafia w ten znak?
Wygląda na to, że nie tylko ja tak uważam. Sygnał w przypadku position:sticky
może pomóc w wielu przypadkach użycia:
- Dodaj cień do przyklejonego banera.
- Gdy użytkownik czyta Twoje treści, rejestruj działania Analytics, aby śledzić ich postępy.
- Gdy użytkownik przewija stronę, zaktualizuj 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, które jest wywoływane, gdy element position:sticky
zostanie naprawiony. Nazwijmy to zdarzenie 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;
});
Wersja demonstracyjna używa tego zdarzenia do nagłówka cienia, gdy zostanie naprawiony. Spowoduje to też zaktualizowanie nowego tytułu u góry strony.
Efekty przewijania bez zdarzeń przewijania?
Pozbądźmy się terminologii, aby móc odwoływać się do tych nazw w pozostałej części tego posta:
- Kontener przewijany – obszar treści (widoczny widoczny obszar) zawierający listę „postów na blogu”.
- Nagłówki – niebieski tytuł w każdej sekcji z atrybutem
position:sticky
. - Sekcje przyklejone – każda sekcja treści. Tekst, który przewija się pod przyklejonymi nagłówkami.
- "Tryb przyklejony" – gdy do elementu stosuje się parametr
position:sticky
.
Aby dowiedzieć się, który nagłówek przechodzi w „tryb przyklejony”, potrzebujemy sposobu określenia przesunięcia przewijania kontenera przewijania. Umożliwi nam to obliczenie aktualnie wyświetlanego nagłówka. Bez zdarzeń scroll
jest to jednak dość skomplikowane. :) Inny problem polega na tym, że position:sticky
usuwa element z układu, gdy zostaje on naprawiony.
Z tego powodu bez zdarzeń przewijania stracisz możliwość wykonywania obliczeń związanych z układem nagłówków.
Dodawanie demonstracyjnego modelu DOM w celu określenia pozycji przewijania
Zamiast zdarzeń scroll
będziemy używać tagu IntersectionObserver
do określania, kiedy headers mają włączać i wyłączać tryb klawiszy trwałych. Dodanie 2 węzłów w każdej sekcji przyklejonej (jednego u góry i jednego u dołu) będzie stanowić punkty pośrednie przy określaniu pozycji przewijania. Gdy te znaczniki trafiają do kontenera i go opuszczają, ich widoczność zmienia się, a obserwacja intersekcji uruchamia wywołanie zwrotne.
Potrzebujemy dwóch strażników, żeby objąć 4 przypadki przewijania w górę i w dół:
- Przewinięcie w dół – nagłówek staje się przyklejony, gdy górny wskaźnik przecina górną część kontenera.
- Przewijanie w dół – nagłówek opuszcza tryb klawiszy trwałych, gdy dotrze do dolnej części sekcji, a jej dolny wskaźnik przecina górną część kontenera.
- Przewijanie w górę – nagłówek opuszcza tryb klawiszy trwałych, gdy jego górny wskaźnik przewija się z góry na widok.
- Przewijanie w górę – nagłówek staje się przyklejony, gdy dolny słupek przesuwa się z powrotem na widok z góry.
Przydatne jest wyświetlenie screencasta od 1 do 4 w kolejności:
Usługa porównywania cen
Ostrzeżenia są umieszczone na górze i na dole każdej sekcji.
.sticky_sentinel--top
znajduje się u góry 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 obserwacji skrzyżowań
Obserwatorzy połączeń asynchronicznie rejestrują zmiany na przecięciu elementu docelowego i widocznego obszaru dokumentu lub kontenera nadrzędnego. W naszym przypadku obserwujemy skrzyżowania z kontenerem nadrzędnym.
Magiczny sos to IntersectionObserver
. Każdy wskaźnik otrzymuje IntersectionObserver
, który umożliwia obserwowanie widoczności skrzyżowania w kontenerze przewijania. Gdy czujnik przewija się w widocznym obszarze, wiemy, że nagłówek staje się stały lub przestaje się przyklejać. I analogicznie, gdy sygnalizator
zamknie widoczny obszar.
Najpierw skonfiguruję obserwatorów alertó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'));
Potem dodałem obserwatora, który uruchamia się, gdy elementy .sticky_sentinel--top
przechodzą przez górną część przewijanego kontenera (w dowolnym kierunku).
Funkcja observeHeaders
tworzy główne wskaźniki i dodaje je do każdej sekcji. Obserwator oblicza przecięcie wskaźnika z górą kontenera i podejmuje decyzję o wejściu do widocznego obszaru czy o zamknięciu go. Na podstawie tych informacji można określić, 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));
}
Pole obserwatora jest skonfigurowane w zasadzie threshold: [0]
, więc jego wywołanie zwrotne uruchamia się, gdy tylko komunikator stanie się widoczny.
Proces jest podobny w przypadku dolnego wskaźnika (.sticky_sentinel--bottom
). Drugi obserwator jest uruchamiany, gdy stopki przechodzą przez dolną część przewijanego kontenera. Funkcja observeFooters
tworzy węzły ważne i łączy je do każdej sekcji. Obserwator oblicza przecięcie wskaźnika z dołem kontenera i decyduje, czy trafia do niego, czy z niego wychodzi. Od tej informacji zależy,
czy nagłówek sekcji się przykleja.
/**
* 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 w zasadzie threshold: [1]
, więc jego wywołanie zwrotne uruchamia się, gdy cały węzeł znajduje się w polu widzenia.
Używam też 2 narzędzi do uruchamiania zdarzenia niestandardowego sticky-change
i generowania komunikatów alarmowych:
/**
* @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.
Ostateczna wersja demonstracyjna
Utworzyliśmy zdarzenie niestandardowe, w którym elementy z atrybutem position:sticky
stają się stałe i dodaliśmy efekty przewijania bez użycia zdarzeń scroll
.
Podsumowanie
Często zastanawiam się, czy IntersectionObserver
nie byłoby pomocnym narzędziem w zastąpieniu niektórych wzorców interfejsu opartych na zdarzeniach scroll
, które rozwijały się od lat. Okazuje się, że odpowiedź brzmi „tak” i „nie”. Semantyka interfejsu API IntersectionObserver
utrudnia używanie go we wszystkich zastosowaniach. Ale, jak już tu zobaczyłem,
można go wykorzystać z kilkoma interesującymi technikami.
Jak inaczej można wykryć zmiany stylu?
Raczej nie. Potrzebowaliśmy sposobu na obserwowanie zmian stylu w elemencie DOM. Niestety w interfejsach API platformy internetowej nie ma nic, co pozwalałoby obserwować zmiany stylu.
MutationObserver
to pierwszy wybór logiczny, ale w większości przypadków się nie sprawdza. Na przykład w wersji demonstracyjnej otrzymamy wywołanie zwrotne po dodaniu klasy sticky
do elementu, ale nie po zmianie jego obliczonego stylu.
Pamiętaj, że klasa sticky
została już zadeklarowana podczas wczytywania strony.
W przyszłości rozszerzenie „Style Mutation Observer” (Obserwacja mutacji) może być przydatne do obserwowania zmian w obliczonych stylach elementu.
position: sticky
.