Ö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:
sticky-change
etkinliğiyle tanışın
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;
}
Tarayıcı, öğelerin bu işarete ne zaman ulaştığını söylese ne güzel olurdu.
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 widget'ını mevcut 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ğe sticky-change
etkinliği adını verelim:
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. Ayrıca sayfanın üst kısmındaki yeni başlığı da günceller.
Kaydırma etkinliği olmadan kaydırma efektleri?
Yazının geri kalanında bu adlara referans verebilmek için bazı terimleri açıklayalım:
- Kaydırma kapsayıcısı: "Blog yayınları" listesini içeren içerik alanı (görünür görüntü alanı).
- Başlıklar: Her bölümde
position:sticky
içeren 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 yapışkan bölüme birer tane (veya gözetmen) iki düğüm eklemek, kaydırma konumunu belirlemek için yol noktası görevi görü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 koruyucusu 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özetleyicisi en üstten tekrar görüntüye girdiğinde yapışkan moddan çıkar.
- Yukarı kaydırma: Alt gözcü üstten tekrar görüntüye girdiğinde başlık yapışkan hale gelir.
1-4 arasındaki adımları sırayla gösteren bir ekran kaydı görmek faydalı olabilir:
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;
}
Kesişim gözlemcilerini ayarlama
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. Bizim örneğimizde, ebeveyn kapsayıcıyla kesişimleri gözlemliyoruz.
İşin sırrı IntersectionObserver
. Her gözetmen, kaydırma kapsayıcısında kesişim görünürlüğünü gözlemlemek için bir IntersectionObserver
alır. Bir gözetmen görünür görüntü alanına kaydırıldığında, bir üstbilginin sabitlendiğini veya yapışkanlığını kaybettiğini biliriz. 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 kaydırılabilir kapsayıcının üst kısmından geçtiğinde (her iki yönde de) 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 geri çağırma işlevi, gözetmen görünür hale gelir gelmez 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, gözetmen düğümlerini oluşturur ve her 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 sabitlenip sabitlenmeyeceğini 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. Bir DOM öğesindeki stil değişikliklerini gözlemleyebilmemiz gerekiyordu. 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" uzantısı, bir öğenin hesaplanmış stillerindeki değişiklikleri gözlemlemek için yararlı olabilir.
position: sticky
.