Zdarzenie klasy CSS „position:sticky”

TL;DR

Oto tajemnica: w przyszłej aplikacji nie musisz używać zdarzeń scroll. Na przykładzie zdarzenia IntersectionObserver pokażę, jak wywołać zdarzenie niestandardowe, gdy elementy position:sticky zostaną zablokowane lub przestaną być przyklejone. bez korzystania z nasłuchiwania scrollowania. W celach demonstracyjnych znajdziesz nawet świetny przykład:

Wyświetl demo | Źródło

Przedstawiamy wydarzenie sticky-change

Jednym z praktycznych ograniczeń używania przypiętej pozycji CSS jest to, że nie przekazuje ona platformie sygnału o tym, kiedy usługa jest aktywna. Innymi słowy, nie ma zdarzenia, które informuje, kiedy element staje się przyklejony lub przestaje być przyklejony.

Oto przykład, w którym <div class="sticky"> jest przesunięty o 10 pikseli od górnej krawędzi kontenera nadrzędnego:

.sticky {
  position: sticky;
  top: 10px;
}

Czy nie byłoby miło, gdyby przeglądarka informowała, kiedy elementy osiągną ten punkt? Wygląda na to, że nie jestem jedyną osobą, która tak uważa. Sygnał position:sticky może się przydać w kilku przypadkach użycia:

  1. Zastosuj cień do przyklejonego banera.
  2. Gdy użytkownik czyta Twoje treści, możesz rejestrować żądania Analytics, aby śledzić jego postępy.
  3. Gdy użytkownik przewija stronę, aktualizuj pływający widget spisu treści, aby wyświetlał aktualną sekcję.

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;
});

Demo używa tego zdarzenia do tworzenia cienia padającego na nagłówki, gdy są one zablokowane. Zaktualizuje też nowy tytuł u góry strony.

Efekty są stosowane bez zdarzeń przewijania.

Czy efekty przewijania bez zdarzeń przewijania?

struktura strony;
Struktura strony.

Użyjmy terminologii, by przywołać te nazwy w dalszej części posta:

  1. Kontener z przewijaniem – obszar treści (widoczny widoczny obszar) zawierający listę „postów na blogu”.
  2. Nagłówki – niebieski tytuł w każdej sekcji, który zawiera position:sticky.
  3. Stały widok sekcji – każda sekcja treści. Tekst, który przewija się pod przyklejonymi nagłówkami.
  4. „Tryb klawiszy trwałych” – gdy elementowi jest przypisana wartość position:sticky.

Aby wiedzieć, który element nagłówka przechodzi w „tryb przylegający”, musimy znaleźć sposób na określenie przesunięcia przewijania kontenera przewijania. Dzięki temu będziemy mogli obliczyć nagłówek, który jest obecnie wyświetlany. Jednak bez zdarzeń scroll jest to dość trudne do wykonania. Innym problemem jest to, że zdarzenie position:sticky usuwa element z układu, gdy staje się on stały.

Bez zdarzeń przewijania utraciliśmy możliwość wykonywania obliczeń dotyczących układu w nagłówkach.

Dodawanie zastępczego modelu DOM w celu określenia pozycji przewijania

Zamiast zdarzeń scroll użyjemy zdarzeń IntersectionObserver, aby określić, kiedy nagłówki przechodzą w tryb przypinania i z niego wychodzą. Dodanie 2 węzłów (czyli strażników) w każdej przylepającej sekcji, jednego u góry i jednego u dołu, będzie pełnić funkcję punktów pośrednich służących do określania pozycji przewijania. Gdy te znaczniki wchodzą i wychodzą z kontenera, ich widoczność się zmienia, a Intersection Observer wywołuje funkcję zwracającą wartość.

Bez widocznych elementów wartych
Ukryte elementy sentinela.

Potrzebujemy 2 strażników, aby objąć 4 przypadki przewijania w górę i w dół:

  1. Przewijanie w dół – nagłówek staje się przyklejony, gdy jego górna granica przekroczy górną krawędź kontenera.
  2. 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.
  3. Przewijanie w góręnagłówek wyłącza tryb klawiszy trwałych, gdy jego górny czujnik przewija się z powrotem do widoku z góry.
  4. Przewijanie w górę – nagłówek staje się przyklejony, gdy dolna granica okienka przesuwa się w górę.

Warto zobaczyć screencasty 1–4 w kolejności od wystąpienia:

Zdarzenia Intersection Observer wywołują wywołania zwrotne, gdy strażnicy wchodzą do kontenera przewijania lub go opuszczają.

Usługa porównywania cen

Strażnicy znajdują się 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:

Dolny strażnik osiąga swój próg.
Pozycja górnych i dolnych elementów strażnika.
: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;
}

Konfigurowanie IntersectionObserver

Intersection Observer asynchronicznie obserwuje zmiany na przecięciu elementu docelowego z widocznym obszarem dokumentu lub kontenera nadrzędnego. W naszym przypadku obserwujemy przecięcia z kontenerem nadrzędnym.

Magiczny algorytm to IntersectionObserver. Każdy strażnik ma IntersectionObserver, aby obserwować widoczność przecięcia w kontenerze przewijania. Gdy czujnik przewija się w widocznym obszarze, wiemy, że nagłówek został naprawiony lub przestał przyklejać. Podobnie, gdy strażnik opuszcza 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 ma się aktywować, gdy elementy .sticky_sentinel--top przelatują przez górną część przewijanego kontenera (w dowolnym kierunku). Funkcja observeHeaders tworzy najskuteczniejsze czujniki i dodaje je do każdej sekcji. Obserwator oblicza przecięcie strażnika z górą kontenera i decyduje, czy wkracza on do widoku, czy z niego wychodzi. Te informacje określają, czy nagłówek sekcji jest przyklejony, czy nie.

/**
 * 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 za pomocą threshold: [0], więc wywołanie zwrotne jest wywoływane, gdy tylko strażnik stanie 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 strażnicze i dołącza je do każdej sekcji. Obserwator oblicza punkt przecięcia strażnika z dna kontenera i decyduje, czy jest to wejście czy wyjście. Te informacje określają, 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 za pomocą threshold: [1], więc wywołanie zwrotne jest wywoływane, gdy cały węzeł jest widoczny.

Na koniec podaję 2 programy do wywoływania 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 position:sticky stają się stałe, i dodaliśmy efekty przewijania bez użycia zdarzeń scroll.

Wyświetl demonstrację | Źródło

Podsumowanie

Często zastanawiałem się, czy IntersectionObserver byłoby przydatnym narzędziem do zastąpienia niektórych scrollwzorów interfejsu opartych na zdarzeniach, które powstały 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 już pokazałem, możesz go używać do różnych ciekawych technik.

Inny sposób wykrywania zmian stylu?

Nie bardzo Potrzebowaliśmy sposobu na obserwowanie zmian stylu elementów DOM. Niestety interfejsy API platformy internetowej nie umożliwiają śledzenia zmian stylu.

Pierwszym logicznym wyborem byłby MutationObserver, ale w większości przypadków nie sprawdza się to. Na przykład w tym pokazie otrzymamy wywołanie zwrotne, gdy do elementu zostanie dodana klasa sticky, ale nie wtedy, gdy zmieni się obliczany styl elementu. Pamiętaj, że klasa sticky została już zadeklarowana podczas wczytywania strony.

W przyszłości rozszerzenie „Style Mutation Observer” dla obserwatorów mutacji może być przydatne do obserwowania zmian w obliczonych stylach elementu. position: sticky.