addons-cm/website_sale_aplicoop/static/src/js/infinite_scroll.js
snt 19eb1b91b5 [FIX] website_sale_aplicoop: Arreglar búsqueda y filtrado por tags
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
2026-02-18 18:51:26 +01:00

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