Zdarzenie klasy CSS „position:sticky”

TL;DR

Oto tajemnica: w następnej aplikacji możesz nie potrzebować zdarzeń scroll. Za pomocą IntersectionObserver, Pokazuję, jak można wywołać zdarzenie niestandardowe, gdy elementy position:sticky zostaną naprawione lub przestaną się przyklejać. Wszystko bez wykorzystanie detektorów przewijania. Dostępnych jest nawet świetna wersja demonstracyjna, która to potwierdza:

.
Zobacz prezentację | Źródło

Przedstawiamy wydarzenie sticky-change

Jednym z praktycznych ograniczeń stosowania stałej pozycji CSS jest to, że nie dostarcza sygnału platformy, by sprawdzić, czy usługa jest aktywna. Inaczej mówiąc, nie ma zdarzenia, na podstawie którego można by sprawdzić, kiedy element stanie się przyklejony lub kiedy przestaje być już klejony.

W poniższym przykładzie poprawiamy <div class="sticky"> 10 pikseli z parametru górnej części 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 jestem jedyną osobą a w konsekwencji Sygnał dla aplikacji position:sticky może odblokować wiele przypadków użycia:

  1. Zastosuj cień do przyklejonego banera.
  2. Gdy użytkownik czyta treści, rejestruj działania analityczne, aby wiedzieć, postęp.
  3. Gdy użytkownik przewija stronę, aktualizuj pływający widżet TOC do bieżącego .

Mając to na uwadze, opracowaliśmy cel końcowy: zdarzenie uruchamia się, gdy element position:sticky zostanie naprawiony. Nazwijmy go sticky-change zdarzenie:

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

W wersji demonstracyjnej używamy: to zdarzenie, aby nagłówki były wyświetlane jako cień, gdy zostaną naprawione. Aktualizuje też nowy tytuł u góry strony.

.
W wersji demonstracyjnej efekty są stosowane bez zdarzeń przewijania.

Przewijanie bez zdarzeń przewijania?

Struktura strony.
Struktura strony.

Żeby nie zapomnieć o tych nazwach, przez resztę posta:

  1. Kontener przewijany – obszar treści (widoczny widoczny obszar) zawierający listę „postów na blogu”.
  2. Nagłówki – niebieski tytuł w każdej sekcji z tagiem position:sticky.
  3. Sekcje przyklejone – każda sekcja treści. Tekst, który przewija się pod przyklejone nagłówki.
  4. „Tryb przyklejony” – gdy do elementu stosuje się position:sticky.

Aby dowiedzieć się, który nagłówek przechodzi w „tryb przyklejony”, musimy znaleźć sposób przesunięcie przewijania kontenera przewijania. Dzięki temu moglibyśmy aby obliczyć wyświetlany nagłówek. Robi się to jednak dość trudne do wykonania bez zdarzeń scroll :) Innym problemem jest to, position:sticky usuwa element z układu, gdy staje się stały.

Dlatego bez zdarzeń przewijania utraciliśmy możliwość wykonywania działań związanych z układem obliczeniach w nagłówkach.

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

Zamiast zdarzeń scroll użyjemy IntersectionObserver do określać, kiedy nagłówki wchodzą w tryb klawiszy trwałych i kiedy z niego wychodzą. Dodawanie 2 węzłów (inaczej strażnicy) w każdej przyklejonej sekcji, jednej na górze i drugiej będzie służyć jako punkty pośrednie do określania pozycji przewijania. Ponieważ te do wewnątrz i na zewnątrz kontenera, ich widoczność zmienia się Obserwator segmentów uruchamia wywołanie zwrotne.

Bez widocznych elementów wartych
Ukryte elementy czujnika.

Potrzebujemy 2 strażników, by uwzględnić 4 przypadki przewijania w górę i w dół:

  1. Przewijanie w dółnagłówek przykleja się, gdy jego górna część krzyży się krzyżyk górnej części kontenera.
  2. Przewijanie w dółnagłówek opuszcza tryb przyklejony, gdy dochodzi do dołu przekrój i jego dolny strażnik krzyżują się z górną krawędzią kontenera.
  3. Przewijanie w góręnagłówek wyłącza tryb klawiszy trwałych, gdy przewijany jest górny czujnik. z powrotem do góry.
  4. Przewijanie w góręnagłówek staje się przyklejony, a jego dolna część przesuwa się z powrotem. tak aby było dobrze widoczne z góry.

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

Obserwatorzy skrzyżowań uruchamiają wywołania zwrotne, gdy strażnicy i zamknij kontener przewijania.

Usługa porównywania cen

Strażnicy znajdują się na górze i na dole każdej sekcji. Komponent .sticky_sentinel--top znajduje się na górze nagłówka, Element .sticky_sentinel--bottom znajduje się na dole sekcji:

Dolny poziom wartości zbliża się do progu.
Położenie górnego i dolnego elementu wartowniczego.
: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 obserwują zmiany na przecięciu elementu docelowego i widocznego obszaru dokumentu lub kontenera nadrzędnego. W naszym przypadku widzimy skrzyżowania z kontenerem nadrzędnym.

Magiczny algorytm to IntersectionObserver. Każdy strażnik otrzymuje IntersectionObserver, aby zaobserwować jego widoczność przecięcia w obrębie kontener przewijania. Gdy czujnik przewija się w widoczny obszar, wiemy, naprawiono lub przestał być przyklejony. Podobnie, gdy strażnik opuści w widocznym obszarze.

Najpierw skonfigurowałem 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 zostaną spełnione w górnej części kontenera z przewijaniem (w dowolnym kierunku). Funkcja observeHeaders tworzy górne strażnicy i dodaje je do każdej sekcji. Obserwator oblicza przecięcie wag z kontener i określa, czy wnika on w widoczny obszar czy go opuszcza. To informacje o tym, 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 z użyciem funkcji threshold: [0], więc jego wywołanie zwrotne uruchamia się natychmiast w miarę jak strażnik staje się widoczny.

Proces jest podobny w przypadku dolnego strażnika (.sticky_sentinel--bottom). Tworzony jest drugi obserwator, który uruchamia się, gdy stopki przechodzą przez dolną część w kontenerze przewijania. Funkcja observeFooters tworzy węzłów ustawodawczych i przyłącza je do każdej sekcji. Obserwator oblicza przecina się z dnem kontenera i decyduje, czy wejścia i wyjścia. Ta informacja określa, czy nagłówek sekcji to przyklejanie, czy nie.

/**
 * 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ł jest w widoku.

Do uruchamiania zdarzenia niestandardowego sticky-change chcę wykorzystać dwa narzędzia: i generowanie 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ę naprawiono i dodano efekty przewijania bez użycia zdarzeń scroll.

.
Zobacz prezentację | Źródło

Podsumowanie

Często zastanawiam się, czy IntersectionObserver może zastąpić niektóre wzorce interfejsu użytkownika scroll oparte na zdarzeniach, rozwijały się na przestrzeni lat. Odpowiedź brzmi: tak i nie. Semantyka interfejsu API IntersectionObserver utrudniają korzystanie z nich do wszystkiego. Ale, jak Pokazaliśmy tutaj, że można go wykorzystać z kilkoma interesującymi technikami.

Inny sposób wykrywania zmian stylu?

Nie bardzo Potrzebowaliśmy sposobu na obserwowanie zmian stylu elementów DOM. Niestety w ramach interfejsów API platformy internetowej nie ma nic, co pozwoliłoby Ci styl zegarka się zmienia.

MutationObserver to logiczny wybór, ale nie sprawdza się w przypadku: w większości przypadków. W wersji demonstracyjnej na przykład użytkownik sticky oddzwoni telefon, gdy jest dodawana do elementu, ale nie wtedy, gdy zmieni się obliczony styl elementu. Pamiętaj, że klasa sticky została już zadeklarowana podczas wczytywania strony.

W przyszłości „Style Mutation Observer” (Obserwator mutacji stylu). Obserwatorzy mutacji mogą być przydatne do obserwowania zmian w obliczanych stylach elementu. position: sticky.