Ö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:
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:
- Sabitlenen banner'a gölge uygulayabilirsiniz.
- Bir kullanıcı içeriğinizi okurken ilerleme durumunu öğrenmek için analiz isabetlerini kaydedin.
- 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.
Kaydırma etkinlikleri olmayan kaydırma efektleri?
Yazının geri kalanında bu adlara bakabilmem için biraz terminolojiyi öğrenelim:
- Kayan kapsayıcı - "blog yayınları" listesini içeren içerik alanı (görünür görüntü alanı).
- Başlıklar -
position:sticky
içeren her bölümde mavi başlık. - Yapışkan bölümler: Her içerik bölümü. Yapışkan başlıkların altında kayan metin.
- "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.
Yukarı ve aşağı kaydırma ile ilgili dört durumu kapsamak için iki koruyucuya ihtiyacımız var:
- 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.
- 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.
- Yukarı kaydırma: Başlık, üst koruyucunun ekranı yukarıdan tekrar görünüme geçtiğinde yapışkan moddan çıkar.
- 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:
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:
: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.
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
.