Problemas resueltos: - Contador de badges mostraba solo productos de página actual (20) en lugar del total - Productos cargados con lazy loading no se filtraban por tags seleccionados Cambios en realtime_search.js: - Eliminado recálculo dinámico de contadores en _filterProducts() - Los contadores permanecen estáticos (calculados por backend sobre dataset completo) - Mejorado logging para debug de tags seleccionados Cambios en infinite_scroll.js: - Después de cargar nueva página, actualiza lista de productos para realtime search - Aplica filtros activos automáticamente a productos recién cargados - Garantiza consistencia de estado de filtrado en toda la aplicación Documentación: - Añadido docs/TAG_FILTER_FIX.md con explicación completa del sistema - Incluye arquitectura, flujo de datos y casos de prueba
485 lines
20 KiB
JavaScript
485 lines
20 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
})();
|