Sonsuz kaydırma

Bu sonsuz kaydırma uygulaması, sunucunun yeni içerikle yanıt vermesi için geçen süreden bağımsız olarak, hiçbir zaman düzen kayması olmamasını sağlayacak şekilde tasarlanmıştır.

Birçok sonsuz kaydırma uygulamasında bulunan en yaygın sorunlardan biri, yeni öğeler eklendiğinde sayfa altbilgisinin (veya benzer bir kullanıcı deneyimi öğesinin) sayfanın daha aşağısına itilmesidir. Bu sonsuz kaydırma uygulamasıyla bu durum hiçbir zaman gerçekleşmez.

Üst düzey yaklaşım

Mümkün olduğunda, yeni öğeler kullanıcı bunlara ulaşmadan sayfaya eklenir. Bu ekleme işlemi ekran dışında gerçekleştiği (ve kullanıcı tarafından görülemediği) için kullanıcı herhangi bir düzen kayması yaşamaz.

Yeni içeriğin zamanında eklenememesi durumunda, bunun yerine bir "Daha Fazla Göster" düğmesi görüntülenir. Bununla birlikte, düğme yalnızca yeni öğeler gösterilmeye hazır olduğunda etkinleştirilir. Böylece kullanıcı, düğmeyi yalnızca hiçbir şey olmadığını görmek için tıklamaz. Bu nedenle, sunucunun yeni içeriğe ne kadar yavaş yanıt verdiğinden (veya kullanıcının sayfayı ne kadar hızlı kaydırdığına) bakılmaksızın hiçbir zaman beklenmeyen düzen kaymaları olmaz.

Uygulama

Intersection Observer API, sayfa öğelerinin konumunu ve görünürlüğünü izlemenin etkili bir yoludur. Bu tasarım iki ayrı kesişim gözlemleyicisi kullanılarak uygulanır:

  • listObserver, sonsuz kaydırma listesinin sonunda yer alan #infinite-scroll-button öğesinin konumunu gözlemler. Düğme görüntü alanına yaklaştığında, yerleştirilmemiş içerik DOM'ye eklenir.
  • sentinelObserver, #sentinel öğesinin konumunu gözlemler. Koruyucu görünür hale geldiğinde sunucudan daha fazla içerik istenir. Koruyucunun konumunu ayarlamak, yeni içeriğin sunucudan ne kadar önceden isteneceğini kontrol etmenin bir yoludur.

Sonsuz kaydırma kullanımından kaynaklanan düzen kaymalarını ele almanın tek yolu bu değildir. Bu sorunu çözmek için izlenecek diğer yöntemler arasında sayfalara geçiş yapmak, liste sanallaştırmayı kullanmak ve sayfa düzenlerini ayarlamak yer alır.

HTML

<div id="infinite-scroll-container">
    <div id="sentinel"></div>
    <div class="item">A</div>
    <div class="item">B</div>
    <div class="item">C</div>
    <div class="item">D</div>
    <div class="item">E</div>
    <button id="infinite-scroll-button" disabled>
        <span class="disabled-text">Loading more items...</span>
        <span class="active-text">Show more</span>
    </button>
</div>

CSS


        :root {
    --active-button-primary: #0080ff;
    --active-button-font:#ffffff;
    --disabled-button-primary: #f5f5f5;
    --disabled-button-secondary: #c4c4c4;
    --disabled-button-font: #000000;
}
#infinite-scroll-container {
    position: relative;
}
#sentinel {
    position: absolute;
    bottom: 150vh;
}
#infinite-scroll-button {
    cursor: pointer;
    border: none;
    padding: 1em;
    width: 100%;
    font-size: 1em;
}
#infinite-scroll-button:enabled {
    color: var(--active-button-font);
    background-color: var(--active-button-primary)
}
#infinite-scroll-button:disabled {
    color: var(--disabled-button-font);
    background-color: var(--disabled-button-primary);
    cursor: not-allowed;
    animation: 3s ease-in-out infinite loadingAnimation;
}
#infinite-scroll-button:enabled .disabled-text {
    display: none;
}
#infinite-scroll-button:disabled .active-text {
    display: none;
}
@keyframes loadingAnimation {
    0% {
        background-color: var(--disabled-button-primary);
    }
    50% {
        background-color: var(--disabled-button-secondary);
    }
    100% {
        background-color: var(--disabled-button-primary);
    }
}
        

JS


        function infiniteScroll() {
    let responseBuffer = [];
    let hasMore;
    let requestPending = false;
    const loadingButtonEl = document.querySelector('#infinite-scroll-button');
    const containerEl = document.querySelector('#infinite-scroll-container');
    const sentinelEl = document.querySelector("#sentinel");
    const insertNewItems = () => {
        while (responseBuffer.length > 0) {
            const data = responseBuffer.shift();
            const el = document.createElement("div");
            el.textContent = data;
            el.classList.add("item");
            el.classList.add("new");
            containerEl.insertBefore(el, loadingButtonEl);
            console.log(`inserted: ${data}`);
        }
        sentinelObserver.observe(sentinelEl);
        if (hasMore === false) {
            loadingButtonEl.style = "display: none";
            sentinelObserver.unobserve(sentinelEl);
            listObserver.unobserve(loadingButtonEl);
        }
        loadingButtonEl.disabled = true
    }
    loadingButtonEl.addEventListener("click", insertNewItems);
    const requestHandler = () => {
        if (requestPending) return;
        console.log("making request");
        requestPending = true;
        fakeServer.fakeRequest().then((response) => {
            console.log("server response", response);
            requestPending = false;
            responseBuffer = responseBuffer.concat(response.items);
            hasMore = response.hasMore;
            loadingButtonEl.disabled = false;;
        });
    }
    const sentinelObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0) {
                observer.unobserve(sentinelEl);
                requestHandler();
            }
        });
    });
    const listObserver = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            if (entry.intersectionRatio > 0 && entry.intersectionRatio < 1) {
                insertNewItems();
            }
        });
    }, {
        rootMargin: "0px 0px 200px 0px"
    });
    sentinelObserver.observe(sentinelEl);
    listObserver.observe(loadingButtonEl);
}
infiniteScroll();