التمرير اللا نهائي

تم تصميم تطبيق التمرير اللا نهائي هذا لضمان عدم حدوث أي متغيرات في التنسيق مطلقًا، بغض النظر عن المدة التي يستغرقها الخادم للاستجابة للمحتوى الجديد.

تتمثل إحدى المشاكل الأكثر شيوعًا في العديد من عمليات تنفيذ التمرير اللانهائي في أنه يتم دفع تذييل الصفحة (أو عنصر تجربة مستخدم مشابه) إلى أسفل الصفحة كلما تمت إضافة عناصر جديدة. مع تطبيق التمرير اللا نهائي هذا، لا يحدث هذا أبدًا.

النهج العالي المستوى

كلما أمكن ذلك، يتم إدراج عناصر جديدة في الصفحة قبل وصول المستخدم إليها. ولأنّ هذا الإدراج يحدث خارج الشاشة (ولا يكون مرئيًا للمستخدم)، لا يواجه المستخدم أي متغيّرات في التصميم.

في حال تعذّر إدراج محتوى جديد في الوقت المناسب، سيتم عرض الزر "عرض المزيد" بدلاً من ذلك. ومع ذلك، لا يتم تمكين الزر إلا عندما تكون العناصر الجديدة جاهزة للعرض، وهذا يضمن أن المستخدم لا ينقر على الزر فقط لمعرفة عدم حدوث أي شيء. وبالتالي، بغض النظر عن مدى بطء استجابة الخادم مع المحتوى الجديد (أو مدى سرعة تمرير المستخدم)، لن تكون هناك أي تغييرات غير متوقعة في التصميم.

التنفيذ

تُعد Intersection Observer API طريقة فعّالة لمراقبة موضع عناصر الصفحة ومستوى رؤيتها. يتم تنفيذ هذا التصميم باستخدام اثنين من مراقبي التفاعل المنفصلين:

  • يلاحظ listObserver موضع #infinite-scroll-button الذي يظهر في نهاية قائمة التمرير اللانهائي. عندما يقترب الزر من إطار العرض، تتم إضافة المحتوى غير المُدرج إلى نموذج العناصر في المستند (DOM).
  • يلاحظ sentinelObserver موضع العنصر #sentinel. وعندما يصبح الحارس مرئيًا، يتم طلب المزيد من المحتوى من الخادم. ويعد ضبط موضع الحارس وسيلة للتحكم في مدى مسبقًا طلب المحتوى الجديد من الخادم.

هذه ليست الطريقة الوحيدة لمعالجة متغيّرات التصميم الناشئة عن استخدام التمرير اللا نهائي. تشمل الطرق الأخرى للتعامل مع هذه المشكلة التبديل إلى التقسيم على صفحات، واستخدام المحاكاة الافتراضية للقوائم، وضبط تخطيطات الصفحات.

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();