CSS location:sticky etkinliği için

Özet

İşin sırrı şöyle: Bir sonraki uygulamanızda scroll etkinliklerine ihtiyacınız olmayabilir. Bir IntersectionObserver kullanarak, position:sticky öğeleri sabitlendiğinde veya bitişik durduğunda özel etkinlikleri nasıl tetikleyebileceğinizi gösteriyorum. Üstelik bunlar kaydırma işleyicileri kullanmadan. Bunu kanıtlayacak harika bir demo bile var:

Demoyu göster | Kaynak

Karşınızda sticky-change etkinliği

CSS sabit konumunu kullanmanın pratik sınırlamalarından biri, mülkün etkin olup olmadığını belirten bir platform sinyali sağlamamasıdır. Başka bir deyişle, bir öğenin ne zaman yapışkan hale geldiğini veya yapışkan kalmasının ne zaman durduğunu bilmek mümkün değildir.

Üst kapsayıcısının üst kısmından 10 piksellik bir <div class="sticky"> sabitlenen aşağıdaki örneği ele alalım:

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

Öğeler bu işarete çarptığında tarayıcının bunu bildirmesi güzel olmaz mıydı? Böyle düşünen tek kişi ben değilim. position:sticky için bir sinyal kullanarak birçok kullanım alanının kilidini açabilir:

  1. Sabitlenen banner'a gölge uygulayabilirsiniz.
  2. Bir kullanıcı içeriğinizi okurken ilerleme durumunu öğrenmek için analiz isabetlerini kaydedin.
  3. Kullanıcı sayfayı kaydırırken kayan TOC widget'ını geçerli bölüme güncelleyin.

Bu kullanım alanlarını göz önünde bulundurarak bir nihai hedef belirledik: Bir position:sticky öğesi sabitlendiğinde tetiklenen bir etkinlik oluşturmak. Bu etkinliği sticky-change etkinliği olarak adlandıralım:

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, düzeltildiğinde bir gölgenin üstbilgisini eklemek için bu etkinliği kullanır. Sayfanın üst kısmındaki yeni başlık da güncellenir.

Demoda efektler, kaydırma etkinlikleri olmadan uygulanır.

Kaydırma etkinlikleri olmayan kaydırma efektleri?

Sayfanın yapısı.
Sayfanın yapısı.

Yazının geri kalanında bu adlara bakabilmem için biraz terminolojiyi öğrenelim:

  1. Kayan kapsayıcı - "blog yayınları" listesini içeren içerik alanı (görünür görüntü alanı).
  2. Başlıklar - position:sticky içeren her bölümde mavi başlık.
  3. Yapışkan bölümler: Her içerik bölümü. Yapışkan başlıkların altında kayan metin.
  4. "Sabit mod" - öğeye position:sticky uygulanırken.

Hangi başlığın "yapışkan moda" girdiğini öğrenmek için kayan kapsayıcının kaydırma ofsetini belirlememiz gerekir. Bu, bize gösterilen başlığı hesaplamamızı sağlar. Ancak, scroll etkinlikleri olmadan bunu yapmak oldukça zordur :) Diğer sorun, position:sticky öğesinin düzeltildiğinde öğeyi düzenden kaldırmasıdır.

Bu nedenle, kaydırma etkinlikleri olmadan başlıklarda düzenle ilgili hesaplamalar yapamayız.

Kaydırma konumunu belirlemek için model DOM ekleniyor

headers ne zaman sabit moda girip çıkacağını belirlemek için scroll etkinlikleri yerine bir IntersectionObserver kullanacağız. Biri üstte ve diğeri altta olacak şekilde her bir yapışkan bölüme iki düğüm (merkezler) eklemek, kaydırma konumunu belirlemek için ara noktalar olarak kullanılır. Bu işaretçiler kapsayıcıya girip çıktıkça, görünürlükleri değişir ve Intersection Observer bir geri çağırma başlatır.

Sentinel öğeleri gösterilmiyor
Gizli koruyucu öğeler.

Yukarı ve aşağı kaydırma ile ilgili dört durumu kapsamak için iki koruyucuya ihtiyacımız var:

  1. Aşağı kaydırma: En üst koruyucusu, kapsayıcının üst kısmıyla kesiştiğinde başlık yapışkan hale gelir.
  2. Aşağı kaydırma: Başlık, bölümün alt kısmına ulaştığında ve alt koruyucu boyutu kapsayıcının üstünü geçerken yapışkan moddan çıkar.
  3. Yukarı kaydırma: Başlık, üst koruyucunun ekranı yukarıdan tekrar görünüme geçtiğinde yapışkan moddan çıkar.
  4. Yukarı kaydırma: Alttaki koruyucunun üst taraftan görünüme geçerek başlık yapışkan hale gelir.

Ekran video kaydını gerçekleşme sırasına göre 1'den 4'e kadar görüntülemek faydalıdır:

Kesişim Gözlemcileri, koruyucular kaydırma kapsayıcısına girdiğinde/kapadığında geri çağırmaları tetikler.

CSS

Nöbetçiler her bölümün üst ve alt kısmına yerleştirilir. .sticky_sentinel--top başlığın üst kısmında, .sticky_sentinel--bottom ise bölümün alt kısmında yer alır:

Alttaki koruyucu, eşiğine ulaşıyor.
Üst ve alttaki koruyucu öğelerin konumu.
: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;
}

Kavşak Gözlemcilerini Kurma

Kesişim Gözlemcileri, bir hedef öğe ile belge görüntü alanının veya üst kapsayıcının kesişimindeki değişiklikleri eşzamansız olarak gözlemler. Örneğimizde, bir üst kapsayıcıyla kesişimler gözlemliyoruz.

Sihirli sos IntersectionObserver. Her bir koruyucu, kaydırma kapsayıcısı içinde kesişim görünürlüğünü gözlemlemek için bir IntersectionObserver alır. Bir koruyucu, görünür görüntü alanına kaydırıldığında başlığın sabitlendiğini veya yapışkan olmadığını biliyoruz. Benzer şekilde, bir koruyucu görüntü alanından çıktığında da bunu yapabilirsiniz.

İlk olarak, üstbilgi ve altbilgi koruyucuları için gözlemcileri ayarladım:

/**
 * 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'));

Ardından, .sticky_sentinel--top öğeleri kayan kapsayıcının (her iki yönde) üst kısmından geçtiğinde etkinleşecek bir gözlemci ekledim. observeHeaders işlevi en iyi korsanları oluşturur ve bunları her bölüme ekler. Gözlemci, koruyucunun kapsayıcının üst kısmıyla kesişimini hesaplar ve görüntü alanına girip girmediğine karar verir. Bu bilgiler, bölüm başlığının yapılıp yapılmadığını belirler.

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

Gözlemci threshold: [0] ile yapılandırıldığından, koruyucu görünür hale gelir gelmez geri çağırması tetiklenir.

Bu süreç alt koruyucu (.sticky_sentinel--bottom) için de benzerdir. Altbilgiler kayan kapsayıcının altından geçtiğinde etkinleşmek üzere ikinci bir gözlemci oluşturulur. observeFooters işlevi, koruyucu düğümleri oluşturur ve bunları her bir bölüme ekler. Gözlemci, koruyucunun kapsayıcının alt kısmıyla kesişimini hesaplar ve buna girip çıkmayacağına karar verir. Bu bilgi, bölüm başlığının yapılıp kalmadığını belirler.

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

Gözlemci threshold: [1] ile yapılandırıldığından, düğümün tamamı görünür olduğunda geri çağırması tetiklenir.

Son olarak, sticky-change özel etkinliğini tetiklemek ve nöbetçileri oluşturmak için kullanabileceğim iki yardımcı program var:

/**
 * @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);
}

İşte bu kadar.

Son demo

position:sticky içeren öğeler düzeltildiğinde özel bir etkinlik oluşturduk ve scroll etkinliklerini kullanmadan kaydırma efektleri ekledik.

Demoyu göster | Kaynak

Sonuç

IntersectionObserver'ın yıllar içinde gelişen scroll etkinliğine dayalı kullanıcı arayüzü kalıplarından bazılarının yerini alması açısından yararlı bir araç olup olmayacağını sıklıkla merak ediyorum. Cevabın evet ve hayır olduğu anlaşılıyor. IntersectionObserver API'nin anlamı, her şey için kullanımı zorlaştırır. Ancak burada gösterdiğim gibi, bu yöntemi bazı ilginç teknikler için kullanabilirsiniz.

Stil değişikliklerini tespit etmenin başka bir yolu nedir?

Pek sayılmaz. İhtiyacımız olan şey, bir DOM öğesindeki stil değişikliklerini gözlemlemekti. Maalesef web platformu API'lerinde stil değişikliklerini izlemenize olanak tanıyan hiçbir şey yoktur.

MutationObserver, mantıklı bir ilk tercihtir ancak çoğu durumda işe yaramaz. Örneğin, demoda sticky sınıfı bir öğeye eklendiğinde bir geri çağırma alırız ancak öğenin hesaplanan stili değiştiğinde geri aranmazız. sticky sınıfının sayfa yükleme sırasında zaten bildirilmiş olduğunu unutmayın.

Gelecekte, Mutasyon Gözlemcileri için bir "Stil Değişimi Gözlemci" uzantısı, bir öğenin hesaplanan stillerindeki değişiklikleri gözlemlemek açısından faydalı olabilir. position: sticky.