Zdarzenie klasy CSS „position:sticky”

TL;DR

Oto tajemnica: w następnej aplikacji możesz nie potrzebować zdarzeń scroll. Na przykładzie zdarzenia IntersectionObserver pokażę, jak wywołać zdarzenie niestandardowe, gdy elementy position:sticky zostaną zablokowane lub przestaną być przyklejone. Wszystko bez wykorzystanie detektorów przewijania. W celach demonstracyjnych możesz to sprawdzić:

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. Innymi słowy, nie ma zdarzenia, które pozwoliłoby stwierdzić, kiedy element staje się przyklejony lub kiedy przestanie być klejnotowy.

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, 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. Dodaj cień do banera, gdy jest on przyklejony.
  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 widget spisu treści, aby wyświetlał aktualną sekcję.

Mając na uwadze te przypadki użycia, opracowaliśmy ostateczny cel: utworzenie zdarzenia, które 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;
});

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 w prezentacji bez zdarzeń przewijania.

Czy efekty przewijania bez zdarzeń przewijania?

strukturę strony;
Struktura strony.

Unikajmy terminologii, żeby ułatwić mi określenie tych nazw przez resztę posta:

  1. Scrolling container (przewijany kontener) – obszar treści (widoczny widok) zawierający listę „postów na blogu”.
  2. Nagłówki – niebieski tytuł w każdej sekcji, który zawiera position:sticky.
  3. Sekcje przyklejone – każda sekcja treści. Tekst, który przewija się pod przyklejone nagłówki.
  4. „Tryb klawiszy trwałych” – gdy elementowi jest przypisana wartość 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. 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.

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

Dodawanie fikcyjnego DOM-u 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ą. 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 elementów strażniczych
Ukryte elementy sentinela.

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 przylegający, gdy dociera do dolnej krawędzi sekcji, a jego dolny strażnik przekracza górną krawędź 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 obejrzeć screencasty z punktów 1–4 w kolejności, w jakiej się pojawiają:

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ę u góry i na dole każdej sekcji. .sticky_sentinel--top znajduje się u góry nagłówka, a .sticky_sentinel--bottom – 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;
}

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 strażnik przewinie widoczny obszar, wiemy, że nagłówek stał się stały lub przestał być przyklejony. 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 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 listę najlepszych strażników i dodaje ją do każdej sekcji. Obserwator oblicza przecięcie wag z kontener i określa, czy wnika on w widoczny obszar czy go opuszcza. Ten 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 za pomocą threshold: [0], więc wywołanie zwrotne jest wywoływane, gdy tylko strażnik stanie się widoczny.

Proces jest podobny w przypadku strażnika dolnego (.sticky_sentinel--bottom). Drugi obserwator jest tworzony, aby wywołać działanie, gdy stopki przejdą przez dół kontenera z przewijaniem. Funkcja observeFooters tworzy węzłów ustawodawczych i przyłą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.

Wersja demonstracyjna

Utworzyliśmy zdarzenie niestandardowe, gdy elementy z position:sticky stają się naprawiono i dodano 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 odpowiedź brzmi „tak i nie”. Semantyka interfejsu API IntersectionObserver sprawia, że trudno jest go używać 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.

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 „Style Mutation Observer” (Obserwator mutacji stylu). Obserwatorzy mutacji mogą być przydatne do obserwowania zmian w obliczanych stylach elementu. position: sticky.