Özet
Size bir sır vereyim: Bir sonraki uygulamanızda scroll
etkinliklerine ihtiyacınız olmayabilir. IntersectionObserver
kullanarak, position:sticky
öğeleri sabitlendiğinde veya yapışmayı bıraktığında nasıl özel etkinlik tetikleyebileceğinizi göstereceğim. Üstelik kaydırma dinleyicileri kullanmadan. Bunu kanıtlamak için harika bir demo bile hazırladık:
Karşınızda sticky-change
etkinliği
CSS yapışkan konumunun pratik sınırlamalarından biri, mülkün ne zaman etkin olduğunu bilmek için bir platform sinyali sağlamamasıdır. Diğer bir deyişle, bir öğenin ne zaman yapışkan hale geldiğini veya ne zaman yapışkanlığını kaybettiğini bildiren bir etkinlik yoktur.
Aşağıdaki örneği inceleyin. Bu örnekte, <div class="sticky">
üst kapsayıcısının üst kısmından 10 piksel sabitlenmiştir:
.sticky {
position: sticky;
top: 10px;
}
Öğeler bu işarete çarptığında tarayıcının bunu bildirmesi güzel olmaz mıydı?
Bu konuda yalnızca ben değilim. position:sticky
sinyali, birçok kullanım alanında fayda sağlayabilir:
- Banner'a yapıştırırken gölge uygulayın.
- Kullanıcı içeriğinizi okurken ilerleme durumunu öğrenmek için Analytics isabetlerini kaydedin.
- Kullanıcı sayfayı kaydırırken, yüzen bir içindekiler listesi widget'ını geçerli bölüme güncelleyin.
Bu kullanım alanlarını göz önünde bulundurarak bir nihai hedef belirledik: position:sticky
öğesi düzeltildiğinde tetiklenen bir etkinlik oluşturma. 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, sabitlendiğinde başlıklara gölge eklemek için bu etkinliği kullanır. Sayfanın üst kısmındaki yeni başlık da güncellenir.
Kaydırma etkinliği olmadan kaydırma efektleri?
Yazının geri kalanında bu adlara bakabilmem için biraz terminolojiyi öğrenelim.
- Kaydırılabilir kapsayıcı: "Blog yayınları" listesini içeren içerik alanı (görünen görüntü alanı).
- Başlıklar -
position:sticky
içeren her bölümde mavi başlık. - Sabit bölümler: Her içerik bölümü. Sabit üstbilgilerin altında kaydırılan metin.
- "Sabit mod":
position:sticky
öğeye uygulandığında.
Hangi başlığın "yapışkan moda" gireceğini bilmek için kaydırma kapsayıcısının kaydırma ofsetini belirlememiz gerekir. Bu sayede, şu anda gösterilen başlığı hesaplayabiliriz. Ancak bunu scroll
etkinlikleri olmadan yapmak oldukça zordur :) Diğer sorun da position:sticky
'un sabitlendiğinde öğeyi düzenden kaldırmasıdır.
Bu nedenle, kaydırma etkinlikleri olmadan başlıklarda düzenlemeyle ilgili hesaplamaları yapma özelliğini kaybettik.
Kaydırma konumunu belirlemek için boş DOM ekleme
Başlıklar'ın ne zaman yapışkan moda girip çıktığını belirlemek için scroll
etkinlikleri yerine bir IntersectionObserver
kullanacağız. Her bir yapışkan bölüme, biri üstte ve diğeri altta olacak şekilde 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 çıkarken görünürlükleri değişir ve Intersection Observer bir geri çağırma işlemi tetikler.
Yukarı ve aşağı kaydırmayla ilgili dört durumu kapsayacak iki gözetmene ihtiyacımız var:
- Aşağı kaydırma: Üst gözetleyicisi kapsayıcının üst kısmını geçtiğinde header yapışkan hale gelir.
- Sayfayı aşağı kaydırmak: Başlık, bölümün alt kısmına ulaştığında ve alt gözetleyicisi kapsayıcının üst kısmını geçtiğinde yapışkan moddan çıkar.
- Yukarı kaydırma: Başlık, üst gözcüsünün üstten tekrar görüntüye gelmesi durumunda 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
Gözetmenler 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, hedef öğe ile doküman 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.
İşin sırrı 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 gözetleyici görüntü alanından çıktığında da.
Öncelikle, üstbilgi ve altbilgi gözetmenleri için gözlemciler oluşturdum:
/**
* 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 üst kısmından (her iki yönde) geçtiğinde tetiklenecek bir gözlemci ekledim.
observeHeaders
işlevi, en iyi gözcüleri oluşturur ve her bölüme ekler. Gözlemci, gözetmenin kapsayıcının üst kısmıyla kesişim noktasını hesaplar ve gözetmenin görüntü alanının içine girip girmediğine karar verir. Bu bilgiler, bölüm başlığının yapışkan olup olmadığı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.
Alt gözetleyici (.sticky_sentinel--bottom
) için süreç benzerdir. Altbilgiler kaydırma kapsayıcısının alt kısmından geçtiğinde tetiklenecek ikinci bir gözlemci oluşturulur. observeFooters
işlevi, koruyucu düğümleri oluşturur ve bunları her bir bölüme ekler. Gözlemci, gözetmenin kapsayıcı tabanı ile kesişim noktasını hesaplar ve gözetmenin içeri girip girmediğine karar verir. Bu bilgiler, bölüm başlığının sabit olup olmadığı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 geri çağırma işlevi, düğümün tamamı görünümde olduğunda tetiklenir.
Son olarak, sticky-change
özel etkinliğini tetiklemek ve gözetmenleri oluşturmak için kullandığım iki yardımcı programımı paylaşmak isterim:
/**
* @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.
Nihai demo
position:sticky
içeren öğeler sabitlendiğinde özel bir etkinlik oluşturduk ve scroll
etkinlikleri kullanmadan kaydırma efektleri ekledik.
Sonuç
IntersectionObserver
'un, yıllar içinde geliştirilen scroll
etkinlik tabanlı kullanıcı arayüzü kalıplarının bazılarını değiştirmek için yararlı bir araç olup olmadığını sık sık merak ettim. Yanıtın hem evet hem de hayır olduğu ortaya çıktı. IntersectionObserver
API'sinin semantikleri, her şey için kullanılmasını zorlaştırıyor. Ancak burada gösterdiğim gibi, bazı ilginç teknikler için kullanabilirsiniz.
Stil değişikliklerini tespit etmenin başka bir yolu var mı?
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 bir özellik bulunmuyor.
MutationObserver
ilk tercih olarak mantıklı bir seçenek olsa da çoğu durumda işe yaramaz. Örneğin, demoda sticky
sınıfı bir öğeye eklendiğinde geri çağırma alırız ancak öğenin hesaplanmış stili değiştiğinde geri çağırma almayız.
sticky
sınıfının sayfa yüklenirken zaten tanımlandığını unutmayın.
Gelecekte, Mutation Observer'lara eklenecek bir"Style Mutation Observer", bir öğenin hesaplanmış stillerindeki değişiklikleri gözlemlemek için yararlı olabilir.
position: sticky
.