/** * Infinite Scroll Handler for Eskaera Shop * * Automatically loads more products as user scrolls down the page. * Falls back to manual "Load More" button if disabled or on error. */ console.log("[INFINITE_SCROLL] Script loaded!"); // DEBUG: Add MutationObserver to detect WHO is clearing the products grid (function () { var setupGridObserver = function () { var grid = document.getElementById("products-grid"); if (!grid) { console.log("[MUTATION_DEBUG] products-grid not found yet, will retry..."); setTimeout(setupGridObserver, 100); return; } console.log("[MUTATION_DEBUG] 🔍 Setting up MutationObserver on products-grid"); console.log("[MUTATION_DEBUG] Initial child count:", grid.children.length); console.log("[MUTATION_DEBUG] Grid innerHTML length:", grid.innerHTML.length); // Watch the grid itself for child changes var gridObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === "childList") { if (mutation.removedNodes.length > 0) { console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS REMOVED FROM GRID!"); console.log( "[MUTATION_DEBUG] Removed nodes count:", mutation.removedNodes.length ); console.log("[MUTATION_DEBUG] Stack trace:"); console.trace(); } if (mutation.addedNodes.length > 0) { console.log("[MUTATION_DEBUG] Products added:", mutation.addedNodes.length); } } }); }); gridObserver.observe(grid, { childList: true, subtree: false }); // ALSO watch the parent for the grid element itself being replaced/removed var parent = grid.parentElement; if (parent) { console.log( "[MUTATION_DEBUG] 🔍 Also watching parent element:", parent.tagName, parent.className ); var parentObserver = new MutationObserver(function (mutations) { mutations.forEach(function (mutation) { if (mutation.type === "childList") { mutation.removedNodes.forEach(function (node) { if ( node.id === "products-grid" || (node.querySelector && node.querySelector("#products-grid")) ) { console.log( "[MUTATION_DEBUG] ⚠️⚠️⚠️ PRODUCTS-GRID ELEMENT ITSELF WAS REMOVED!" ); console.log("[MUTATION_DEBUG] Stack trace:"); console.trace(); } }); } }); }); parentObserver.observe(parent, { childList: true, subtree: true }); } // Poll to detect innerHTML being cleared (as backup) var lastChildCount = grid.children.length; setInterval(function () { var currentGrid = document.getElementById("products-grid"); if (!currentGrid) { console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID ELEMENT NO LONGER EXISTS!"); console.trace(); return; } var currentChildCount = currentGrid.children.length; if (currentChildCount !== lastChildCount) { console.log( "[MUTATION_DEBUG] 📊 Child count changed: " + lastChildCount + " → " + currentChildCount ); if (currentChildCount === 0 && lastChildCount > 0) { console.log("[MUTATION_DEBUG] ⚠️⚠️⚠️ GRID WAS EMPTIED!"); console.trace(); } lastChildCount = currentChildCount; } }, 100); console.log("[MUTATION_DEBUG] ✅ Observers attached (grid + parent + polling)"); }; // Start observing as soon as possible if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", setupGridObserver); } else { setupGridObserver(); } })(); (function () { "use strict"; // Also run immediately if DOM is already loaded var initInfiniteScroll = function () { console.log("[INFINITE_SCROLL] Initializing infinite scroll..."); var infiniteScroll = { orderId: null, searchQuery: "", category: "0", perPage: 20, currentPage: 1, isLoading: false, hasMore: true, config: {}, init: function () { console.log("[INFINITE_SCROLL] 🔧 init() called"); // Get configuration from page data var configEl = document.getElementById("eskaera-config"); console.log("[INFINITE_SCROLL] eskaera-config element:", configEl); if (!configEl) { console.error( "[INFINITE_SCROLL] ❌ No eskaera-config found, lazy loading disabled" ); return; } this.orderId = configEl.getAttribute("data-order-id"); this.searchQuery = configEl.getAttribute("data-search") || ""; this.category = configEl.getAttribute("data-category") || "0"; this.perPage = parseInt(configEl.getAttribute("data-per-page")) || 20; this.currentPage = parseInt(configEl.getAttribute("data-current-page")) || 1; console.log("[INFINITE_SCROLL] Config loaded:", { orderId: this.orderId, searchQuery: this.searchQuery, category: this.category, perPage: this.perPage, currentPage: this.currentPage, }); // Check if there are more products to load from data attribute var hasNextAttr = configEl.getAttribute("data-has-next"); this.hasMore = hasNextAttr === "true" || hasNextAttr === "True"; console.log( "[INFINITE_SCROLL] hasMore=" + this.hasMore + " (data-has-next=" + hasNextAttr + ")" ); if (!this.hasMore) { console.log( "[INFINITE_SCROLL] ⚠️ No more pages available, but keeping initialized for filter handling (has_next=" + hasNextAttr + ")" ); // Don't return - we need to stay initialized so realtime_search can call resetWithFilters() } console.log("[INFINITE_SCROLL] Initialized with:", { orderId: this.orderId, searchQuery: this.searchQuery, category: this.category, perPage: this.perPage, currentPage: this.currentPage, }); // Only attach scroll listener if there are more pages to load if (this.hasMore) { this.attachScrollListener(); this.attachFallbackButtonListener(); } else { console.log("[INFINITE_SCROLL] Skipping scroll listener (no more pages)"); } }, attachScrollListener: function () { var self = this; var scrollThreshold = 300; // Load when within 300px of the bottom of the grid window.addEventListener("scroll", function () { if (self.isLoading || !self.hasMore) { return; } var grid = document.getElementById("products-grid"); if (!grid) { return; } // Calculate distance from bottom of grid to bottom of viewport var gridRect = grid.getBoundingClientRect(); var gridBottom = gridRect.bottom; var viewportBottom = window.innerHeight; var distanceFromBottom = gridBottom - viewportBottom; // Load more if we're within threshold pixels of the grid bottom if (distanceFromBottom <= scrollThreshold && distanceFromBottom > 0) { console.log( "[INFINITE_SCROLL] Near grid bottom (distance: " + Math.round(distanceFromBottom) + "px), loading next page" ); self.loadNextPage(); } }); console.log( "[INFINITE_SCROLL] Scroll listener attached (threshold: " + scrollThreshold + "px from grid bottom)" ); }, attachFallbackButtonListener: function () { var self = this; var btn = document.getElementById("load-more-btn"); if (!btn) { console.log("[INFINITE_SCROLL] No fallback button found"); return; } btn.addEventListener("click", function (e) { e.preventDefault(); if (!self.isLoading && self.hasMore) { console.log("[INFINITE_SCROLL] Manual button click, loading next page"); self.loadNextPage(); } }); console.log("[INFINITE_SCROLL] Fallback button listener attached"); }, resetWithFilters: function (searchQuery, categoryId) { /** * Reset infinite scroll to page 1 with new filters and reload products. * Called by realtime_search when filters change. * * WARNING: This clears the grid! Only call when filters actually change. */ console.log( "[INFINITE_SCROLL] ⚠️⚠️⚠️ resetWithFilters CALLED - search=" + searchQuery + " category=" + categoryId ); console.trace("[INFINITE_SCROLL] ⚠️⚠️⚠️ WHO CALLED resetWithFilters? Call stack:"); // Normalize values: empty string to "", null to "0" for category var newSearchQuery = (searchQuery || "").trim(); var newCategory = (categoryId || "").trim() || "0"; // CHECK IF VALUES ACTUALLY CHANGED before clearing grid! if (newSearchQuery === this.searchQuery && newCategory === this.category) { console.log( "[INFINITE_SCROLL] ✅ NO CHANGE - Skipping reset (values are identical)" ); return; // Don't clear grid if nothing changed! } console.log( "[INFINITE_SCROLL] 🔥 VALUES CHANGED - Old: search=" + this.searchQuery + " category=" + this.category + " → New: search=" + newSearchQuery + " category=" + newCategory ); this.searchQuery = newSearchQuery; this.category = newCategory; this.currentPage = 0; // Set to 0 so loadNextPage() increments to 1 this.isLoading = false; this.hasMore = true; console.log( "[INFINITE_SCROLL] After normalization: search=" + this.searchQuery + " category=" + this.category ); // Update the config element data attributes for consistency var configEl = document.getElementById("eskaera-config"); if (configEl) { configEl.setAttribute("data-search", this.searchQuery); configEl.setAttribute("data-category", this.category); configEl.setAttribute("data-current-page", "1"); configEl.setAttribute("data-has-next", "true"); console.log("[INFINITE_SCROLL] Updated eskaera-config attributes"); } // Clear the grid and reload from page 1 var grid = document.getElementById("products-grid"); if (grid) { console.log("[INFINITE_SCROLL] 🗑️ CLEARING GRID NOW!"); grid.innerHTML = ""; console.log("[INFINITE_SCROLL] Grid cleared"); } // Load first page with new filters console.log("[INFINITE_SCROLL] Calling loadNextPage()..."); this.loadNextPage(); }, loadNextPage: function () { console.log( "[INFINITE_SCROLL] 🚀 loadNextPage() CALLED - currentPage=" + this.currentPage + " isLoading=" + this.isLoading + " hasMore=" + this.hasMore ); if (this.isLoading || !this.hasMore) { console.log("[INFINITE_SCROLL] ❌ ABORTING - already loading or no more pages"); return; } var self = this; this.isLoading = true; // Only increment if we're not loading first page (currentPage will be 0 after reset) if (this.currentPage === 0) { console.log( "[INFINITE_SCROLL] ✅ Incrementing from 0 to 1 (first page after reset)" ); this.currentPage = 1; } else { console.log( "[INFINITE_SCROLL] ✅ Incrementing page " + this.currentPage + " → " + (this.currentPage + 1) ); this.currentPage += 1; } console.log( "[INFINITE_SCROLL] 📡 About to fetch page", this.currentPage, "for order", this.orderId ); // Show spinner var spinner = document.getElementById("loading-spinner"); if (spinner) { spinner.classList.remove("d-none"); } var data = { page: this.currentPage, search: this.searchQuery, category: this.category, }; fetch("/eskaera/" + this.orderId + "/load-products-ajax", { method: "POST", headers: { "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", }, body: JSON.stringify(data), }) .then(function (response) { if (!response.ok) { throw new Error("Network response was not ok: " + response.status); } return response.json(); }) .then(function (result) { if (result.error) { console.error("[INFINITE_SCROLL] Server error:", result.error); self.isLoading = false; self.currentPage -= 1; return; } console.log("[INFINITE_SCROLL] Page loaded successfully", result); // Insert HTML into grid var grid = document.getElementById("products-grid"); if (grid && result.html) { grid.insertAdjacentHTML("beforeend", result.html); console.log("[INFINITE_SCROLL] Products inserted into grid"); } // Update has_more flag self.hasMore = result.has_next || false; if (!self.hasMore) { console.log("[INFINITE_SCROLL] No more products available"); } // Hide spinner if (spinner) { spinner.classList.add("d-none"); } self.isLoading = false; // Re-attach event listeners for newly added products if ( window.aplicoopShop && typeof window.aplicoopShop._attachEventListeners === "function" ) { window.aplicoopShop._attachEventListeners(); console.log("[INFINITE_SCROLL] Event listeners re-attached"); } // Update realtime search to include newly loaded products if ( window.realtimeSearch && typeof window.realtimeSearch._storeAllProducts === "function" ) { window.realtimeSearch._storeAllProducts(); console.log( "[INFINITE_SCROLL] Products list updated for realtime search" ); // Apply current filters to newly loaded products if (typeof window.realtimeSearch._filterProducts === "function") { window.realtimeSearch._filterProducts(); console.log("[INFINITE_SCROLL] Filters applied to new products"); } } }) .catch(function (error) { console.error("[INFINITE_SCROLL] Fetch error:", error); self.isLoading = false; self.currentPage -= 1; // Hide spinner on error if (spinner) { spinner.classList.add("d-none"); } // Show fallback button var btn = document.getElementById("load-more-btn"); if (btn) { btn.classList.remove("d-none"); btn.style.display = ""; } }); }, }; // Initialize infinite scroll infiniteScroll.init(); // Export to global scope for debugging window.infiniteScroll = infiniteScroll; }; // Run on DOMContentLoaded if DOM not yet ready if (document.readyState === "loading") { console.log("[INFINITE_SCROLL] DOM not ready, waiting for DOMContentLoaded..."); document.addEventListener("DOMContentLoaded", initInfiniteScroll); } else { // DOM is already loaded console.log("[INFINITE_SCROLL] DOM already loaded, initializing immediately..."); initInfiniteScroll(); } })();