🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

HEB Infinite Scroll

Convert HEB search results pagination to infinite scroll

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

You will need to install an extension such as Tampermonkey to install this script.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

// ==UserScript==
// @name         HEB Infinite Scroll
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Convert HEB search results pagination to infinite scroll
// @author       You
// @match        https://www.heb.com/search*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    let isLoading = false;
    let hasMorePages = true;
    let currentPage = 1;
    let lastLoadedUrl = null;

    // Find the product grid container
    function findProductGrid() {
        // Try to find the container with class BasicGrid_basicGrid__dZgBP
        const grid = document.querySelector('.BasicGrid_basicGrid__dZgBP');
        if (grid) return grid;

        // Fallback: find container with product links
        const firstProduct = document.querySelector('a[href*="/product-detail/"]');
        if (firstProduct) {
            let container = firstProduct.parentElement;
            // Traverse up to find the grid container
            while (container && container !== document.body) {
                if (container.classList && container.classList.contains('BasicGrid_basicGrid__dZgBP')) {
                    return container;
                }
                // Check if this container has multiple product links
                if (container.querySelectorAll('a[href*="/product-detail/"]').length > 1) {
                    return container;
                }
                container = container.parentElement;
            }
        }
        return null;
    }

    // Find pagination navigation
    function findPagination() {
        return document.querySelector('nav[aria-label*="Pagination"], nav[aria-label*="pagination"]') ||
               document.querySelector('nav:has(a[href*="page="])');
    }

    // Get next page URL
    function getNextPageUrl(doc = document) {
        const pagination = doc.querySelector('nav[aria-label*="Pagination"], nav[aria-label*="pagination"]') ||
                           (doc === document ? findPagination() : null);
        if (!pagination) return null;

        // Find "Next" link
        const nextLink = pagination.querySelector('a[aria-label*="next"], a[aria-label*="Next"]') ||
                         Array.from(pagination.querySelectorAll('a')).find(a => 
                             a.textContent.trim().toLowerCase() === 'next'
                         );

        if (nextLink) {
            // Check if it's disabled or points to current page
            if (nextLink.hasAttribute('disabled') || 
                nextLink.classList.contains('disabled') ||
                !nextLink.href ||
                nextLink.href === window.location.href) {
                return null;
            }
            return nextLink.href;
        }

        // Fallback: find next page number link
        const currentPageLink = pagination.querySelector('a[aria-current="page"]') ||
                               Array.from(pagination.querySelectorAll('a')).find(a => 
                                   a.getAttribute('aria-current') === 'page' || 
                                   a.classList.contains('active')
                               );

        if (currentPageLink) {
            const currentPageNum = parseInt(currentPageLink.textContent.trim()) || 1;
            const nextPageNum = currentPageNum + 1;
            const nextPageLink = Array.from(pagination.querySelectorAll('a')).find(a => {
                const text = a.textContent.trim();
                return text === nextPageNum.toString() || a.href.includes(`page=${nextPageNum}`);
            });
            if (nextPageLink) return nextPageLink.href;
        }

        // Only construct URL if we're sure there are more pages
        // Don't auto-construct on last page
        return null;
    }

    // Check if there are more pages (can check a specific document/HTML)
    function checkHasMorePages(doc = document) {
        const pagination = doc.querySelector('nav[aria-label*="Pagination"], nav[aria-label*="pagination"]') ||
                           (doc === document ? findPagination() : null);
        
        if (!pagination) return false;

        // Check if "Next" link exists and is not disabled
        const nextLink = pagination.querySelector('a[aria-label*="next"], a[aria-label*="Next"]') ||
                         Array.from(pagination.querySelectorAll('a')).find(a => 
                             a.textContent.trim().toLowerCase() === 'next'
                         );

        if (nextLink) {
            // Check if it's disabled or has no href
            if (nextLink.hasAttribute('disabled') || 
                nextLink.classList.contains('disabled') ||
                !nextLink.href ||
                nextLink.href === window.location.href) {
                return false;
            }
            return true;
        }

        // Check if there are more page number links
        const pageLinks = Array.from(pagination.querySelectorAll('a[href*="page="]'));
        if (pageLinks.length > 0) {
            const pageNumbers = pageLinks.map(link => {
                const match = link.href.match(/page=(\d+)/);
                return match ? parseInt(match[1]) : 0;
            }).filter(num => num > 0);
            
            if (pageNumbers.length === 0) return false;
            
            const maxPage = Math.max(...pageNumbers);
            const url = new URL(window.location.href);
            const currentPageNum = parseInt(url.searchParams.get('page')) || 1;
            return currentPageNum < maxPage;
        }

        return false;
    }

    // Extract product items from HTML
    function extractProducts(html) {
        const parser = new DOMParser();
        const doc = parser.parseFromString(html, 'text/html');
        const grid = doc.querySelector('.BasicGrid_basicGrid__dZgBP');

        if (!grid) return [];

        // Find all product wrapper elements (direct children of grid)
        // These are the elements that contain product cards
        const products = Array.from(grid.children).filter(child => {
            return child.querySelector('a[href*="/product-detail/"]');
        });

        return products;
    }

    // Load next page
    async function loadNextPage() {
        if (isLoading || !hasMorePages) return;

        const nextPageUrl = getNextPageUrl();
        if (!nextPageUrl) {
            hasMorePages = false;
            return;
        }

        // Prevent loading the same URL twice
        if (nextPageUrl === lastLoadedUrl) {
            hasMorePages = false;
            return;
        }

        // Normalize URLs for comparison (remove hash, trailing slashes, etc.)
        const normalizeUrl = (url) => {
            try {
                const u = new URL(url, window.location.origin);
                u.hash = '';
                // Normalize search params
                const params = new URLSearchParams(u.search);
                u.search = params.toString();
                return u.toString().replace(/\/$/, '');
            } catch {
                return url;
            }
        };

        const normalizedNextUrl = normalizeUrl(nextPageUrl);
        const normalizedLastUrl = lastLoadedUrl ? normalizeUrl(lastLoadedUrl) : null;

        if (normalizedNextUrl === normalizedLastUrl) {
            hasMorePages = false;
            return;
        }

        isLoading = true;
        const grid = findProductGrid();
        if (!grid) {
            isLoading = false;
            return;
        }

        // Show loading indicator
        const loadingIndicator = document.createElement('div');
        loadingIndicator.textContent = 'Loading more products...';
        loadingIndicator.style.cssText = `
            text-align: center;
            padding: 20px;
            color: #666;
            font-size: 14px;
        `;
        grid.appendChild(loadingIndicator);

        try {
            const response = await fetch(nextPageUrl);
            const html = await response.text();
            
            // Parse the HTML to check pagination in the fetched page
            const parser = new DOMParser();
            const fetchedDoc = parser.parseFromString(html, 'text/html');
            
            const products = extractProducts(html);

            if (products.length === 0) {
                hasMorePages = false;
                loadingIndicator.textContent = 'No more products';
                setTimeout(() => loadingIndicator.remove(), 2000);
                return;
            }

            // Remove loading indicator
            loadingIndicator.remove();

            // Append products to grid
            // Import nodes from the parsed document into the main document
            products.forEach(product => {
                const importedProduct = document.importNode(product, true);
                grid.appendChild(importedProduct);
            });

            // Update tracking
            lastLoadedUrl = nextPageUrl;
            const url = new URL(nextPageUrl);
            const pageNum = parseInt(url.searchParams.get('page')) || currentPage + 1;
            currentPage = pageNum;

            // Update the pagination in the DOM with the fetched page's pagination
            const fetchedPagination = fetchedDoc.querySelector('nav[aria-label*="Pagination"], nav[aria-label*="pagination"]');
            if (fetchedPagination) {
                const currentPagination = findPagination();
                if (currentPagination) {
                    // Replace the pagination with the new one
                    const importedPagination = document.importNode(fetchedPagination, true);
                    currentPagination.replaceWith(importedPagination);
                }
            }

            // Check if there are more pages based on the fetched page's pagination
            // Also check if we can get a next page URL from the fetched page
            const nextPageUrlFromFetched = getNextPageUrl(fetchedDoc);
            hasMorePages = checkHasMorePages(fetchedDoc) && nextPageUrlFromFetched !== null;

            // Additional check: if the next page URL from fetched doc is the same as what we just loaded, we're stuck
            if (nextPageUrlFromFetched) {
                const normalizedFetchedNext = normalizeUrl(nextPageUrlFromFetched);
                const normalizedJustLoaded = normalizeUrl(nextPageUrl);
                if (normalizedFetchedNext === normalizedJustLoaded) {
                    hasMorePages = false;
                }
            }

            // Hide pagination if no more pages
            if (!hasMorePages) {
                const pagination = findPagination();
                if (pagination) {
                    pagination.style.display = 'none';
                }
                const endMessage = document.createElement('div');
                endMessage.textContent = 'No more products';
                endMessage.style.cssText = `
                    text-align: center;
                    padding: 20px;
                    margin-top: 20px;
                    color: #666;
                    font-size: 14px;
                `;
                grid.appendChild(endMessage);
            }

        } catch (error) {
            console.error('Error loading next page:', error);
            loadingIndicator.textContent = 'Error loading products';
            setTimeout(() => loadingIndicator.remove(), 2000);
            hasMorePages = false;
        } finally {
            isLoading = false;
        }
    }

    // Check if user is near bottom of page
    function checkScrollPosition() {
        const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
        const windowHeight = window.innerHeight;
        const documentHeight = document.documentElement.scrollHeight;

        // Trigger when within 500px of bottom
        if (documentHeight - (scrollTop + windowHeight) < 500) {
            loadNextPage();
        }
    }

    // Initialize
    function init() {
        // Reset state on new page load
        isLoading = false;
        lastLoadedUrl = null;
        
        const grid = findProductGrid();
        if (!grid) {
            console.log('HEB Infinite Scroll: Product grid not found');
            return;
        }

        // Check initial pagination state
        hasMorePages = checkHasMorePages();

        // Hide pagination initially (we'll show it again if needed)
        const pagination = findPagination();
        if (pagination && hasMorePages) {
            pagination.style.opacity = '0.3';
        }

        // Set up scroll listener
        let scrollTimeout;
        window.addEventListener('scroll', () => {
            clearTimeout(scrollTimeout);
            scrollTimeout = setTimeout(checkScrollPosition, 100);
        }, { passive: true });

        // Also check on initial load if page is already scrolled
        setTimeout(checkScrollPosition, 1000);
    }

    // Wait for page to load
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            setTimeout(init, 1000);
        });
    } else {
        setTimeout(init, 1000);
    }

    // Re-initialize on navigation (for SPAs)
    let lastUrl = location.href;
    new MutationObserver(() => {
        const url = location.href;
        if (url !== lastUrl) {
            lastUrl = url;
            setTimeout(init, 1000);
        }
    }).observe(document, { subtree: true, childList: true });

})();