[FIX] website_sale_aplicoop: Complete infinite scroll and search filter integration

Major fixes:
- Fix JSON body parsing in load_products_ajax with type='http' route
  * Parse JSON from request.httprequest.get_data() instead of post params
  * Correctly read page, search, category from JSON request body

- Fix search and category filter combination
  * Use intersection (&) instead of replacement to preserve both filters
  * Now respects search AND category simultaneously

- Integrate realtime_search.js with infinite_scroll.js
  * Add resetWithFilters() method to reset scroll to page 1 with new filters
  * When search/category changes, reload products from server
  * Clear grid and load fresh results

- Fix pagination reset logic
  * Set currentPage = 0 in resetWithFilters() so loadNextPage() increments to 1
  * Prevents loading empty page 2 when resetting filters

Results:
 Infinite scroll loads all pages correctly (1, 2, 3...)
 Search filters work across all products (not just loaded)
 Category filters work correctly
 Search AND category filters work together
 Page resets to 1 when filters change
This commit is contained in:
snt 2026-02-17 01:26:20 +01:00
parent 5eb039ffe0
commit 40ce973bd6
4 changed files with 463 additions and 239 deletions

View file

@ -3,8 +3,8 @@
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
*/
(function() {
'use strict';
(function () {
"use strict";
window.realtimeSearch = {
searchInput: null,
@ -16,57 +16,59 @@
selectedTags: new Set(), // Set of selected tag IDs (for OR logic filtering)
availableTags: {}, // Maps tag ID to {id, name, count}
init: function() {
console.log('[realtimeSearch] Initializing...');
init: function () {
console.log("[realtimeSearch] Initializing...");
// searchInput y categorySelect ya fueron asignados por tryInit()
console.log('[realtimeSearch] Search input:', this.searchInput);
console.log('[realtimeSearch] Category select:', this.categorySelect);
console.log("[realtimeSearch] Search input:", this.searchInput);
console.log("[realtimeSearch] Category select:", this.categorySelect);
if (!this.searchInput) {
console.error('[realtimeSearch] ERROR: Search input not found!');
console.error("[realtimeSearch] ERROR: Search input not found!");
return false;
}
if (!this.categorySelect) {
console.error('[realtimeSearch] ERROR: Category select not found!');
console.error("[realtimeSearch] ERROR: Category select not found!");
return false;
}
this._buildCategoryHierarchyFromDOM();
this._storeAllProducts();
console.log('[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()...');
console.log(
"[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()..."
);
this._attachEventListeners();
console.log('[realtimeSearch] ✓ Initialized successfully');
console.log("[realtimeSearch] ✓ Initialized successfully");
return true;
},
_buildCategoryHierarchyFromDOM: function() {
_buildCategoryHierarchyFromDOM: function () {
/**
* Construye un mapa de jerarquía de categorías desde las opciones del select.
* Ahora todas las opciones son planas pero con indentación visual ( arrows).
*
*
* La profundidad se determina contando el número de arrows ().
* Estructura: categoryHierarchy[parentCategoryId] = [childCategoryId1, childCategoryId2, ...]
*/
var self = this;
var allOptions = this.categorySelect.querySelectorAll('option[value]');
var allOptions = this.categorySelect.querySelectorAll("option[value]");
var optionStack = []; // Stack para mantener los padres en cada nivel
allOptions.forEach(function(option) {
var categoryId = option.getAttribute('value');
allOptions.forEach(function (option) {
var categoryId = option.getAttribute("value");
var text = option.textContent;
// Contar arrows para determinar profundidad
var arrowCount = (text.match(/↳/g) || []).length;
var depth = arrowCount; // Depth: 0 for roots, 1 for children, 2 for grandchildren, etc.
// Ajustar el stack al nivel actual
// Si la profundidad es menor o igual, sacamos elementos del stack
while (optionStack.length > depth) {
optionStack.pop();
}
// Si hay un padre en el stack (profundidad > 0), agregar como hijo
if (depth > 0 && optionStack.length > 0) {
var parentId = optionStack[optionStack.length - 1];
@ -77,7 +79,7 @@
self.categoryHierarchy[parentId].push(categoryId);
}
}
// Agregar este ID al stack como posible padre para los siguientes
// Adjust position in stack based on depth
if (optionStack.length > depth) {
@ -86,286 +88,407 @@
optionStack.push(categoryId);
}
});
console.log('[realtimeSearch] Complete category hierarchy built:', self.categoryHierarchy);
console.log(
"[realtimeSearch] Complete category hierarchy built:",
self.categoryHierarchy
);
},
_storeAllProducts: function() {
var productCards = document.querySelectorAll('.product-card');
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
_storeAllProducts: function () {
var productCards = document.querySelectorAll(".product-card");
console.log("[realtimeSearch] Found " + productCards.length + " product cards");
var self = this;
this.allProducts = [];
productCards.forEach(function(card, index) {
var name = card.getAttribute('data-product-name') || '';
var categoryId = card.getAttribute('data-category-id') || '';
var tagIdsStr = card.getAttribute('data-product-tags') || '';
productCards.forEach(function (card, index) {
var name = card.getAttribute("data-product-name") || "";
var categoryId = card.getAttribute("data-category-id") || "";
var tagIdsStr = card.getAttribute("data-product-tags") || "";
// Parse tag IDs from comma-separated string
var tagIds = [];
if (tagIdsStr) {
tagIds = tagIdsStr.split(',').map(function(id) {
return parseInt(id.trim(), 10);
}).filter(function(id) {
return !isNaN(id);
});
tagIds = tagIdsStr
.split(",")
.map(function (id) {
return parseInt(id.trim(), 10);
})
.filter(function (id) {
return !isNaN(id);
});
}
self.allProducts.push({
element: card,
name: name.toLowerCase(),
category: categoryId.toString(),
originalCategory: categoryId,
tags: tagIds // Array of tag IDs for this product
tags: tagIds, // Array of tag IDs for this product
});
});
console.log('[realtimeSearch] Total products stored: ' + this.allProducts.length);
console.log("[realtimeSearch] Total products stored: " + this.allProducts.length);
},
_attachEventListeners: function() {
_attachEventListeners: function () {
var self = this;
// Initialize available tags from DOM
self._initializeAvailableTags();
// Store original colors for each tag badge
self.originalTagColors = {}; // Maps tag ID to original color
// Store last values at instance level so polling can access them
self.lastSearchValue = '';
self.lastCategoryValue = '';
self.lastSearchValue = "";
self.lastCategoryValue = "";
// Prevent form submission completely
var form = self.searchInput.closest('form');
var form = self.searchInput.closest("form");
if (form) {
form.addEventListener('submit', function(e) {
form.addEventListener("submit", function (e) {
e.preventDefault();
e.stopPropagation();
console.log('[realtimeSearch] Form submission prevented and stopped');
console.log("[realtimeSearch] Form submission prevented and stopped");
return false;
});
}
// Prevent Enter key from submitting
self.searchInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
self.searchInput.addEventListener("keypress", function (e) {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
console.log('[realtimeSearch] Enter key prevented on search input');
console.log("[realtimeSearch] Enter key prevented on search input");
return false;
}
});
// Search input: listen to 'input' for real-time filtering
self.searchInput.addEventListener('input', function(e) {
self.searchInput.addEventListener("input", function (e) {
try {
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in input listener:', error.message);
console.error("[realtimeSearch] Error in input listener:", error.message);
}
});
// Also keep 'keyup' for extra compatibility
self.searchInput.addEventListener('keyup', function(e) {
self.searchInput.addEventListener("keyup", function (e) {
try {
console.log('[realtimeSearch] KEYUP event - value: "' + e.target.value + '"');
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in keyup listener:', error.message);
console.error("[realtimeSearch] Error in keyup listener:", error.message);
}
});
// Category select
self.categorySelect.addEventListener('change', function(e) {
self.categorySelect.addEventListener("change", function (e) {
try {
console.log('[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"');
console.log(
'[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"'
);
self._filterProducts();
} catch (error) {
console.error('[realtimeSearch] Error in category change listener:', error.message);
console.error(
"[realtimeSearch] Error in category change listener:",
error.message
);
}
});
// Tag filter badges: click to toggle selection (independent state)
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
console.log('[realtimeSearch] Found ' + tagBadges.length + ' tag filter badges');
console.log("[realtimeSearch] Found " + tagBadges.length + " tag filter badges");
// Get theme colors from CSS variables
var rootStyles = getComputedStyle(document.documentElement);
var primaryColor = rootStyles.getPropertyValue('--bs-primary').trim() ||
rootStyles.getPropertyValue('--primary').trim() ||
'#0d6efd';
var secondaryColor = rootStyles.getPropertyValue('--bs-secondary').trim() ||
rootStyles.getPropertyValue('--secondary').trim() ||
'#6c757d';
console.log('[realtimeSearch] Theme colors - Primary:', primaryColor, 'Secondary:', secondaryColor);
var primaryColor =
rootStyles.getPropertyValue("--bs-primary").trim() ||
rootStyles.getPropertyValue("--primary").trim() ||
"#0d6efd";
var secondaryColor =
rootStyles.getPropertyValue("--bs-secondary").trim() ||
rootStyles.getPropertyValue("--secondary").trim() ||
"#6c757d";
console.log(
"[realtimeSearch] Theme colors - Primary:",
primaryColor,
"Secondary:",
secondaryColor
);
// Store original colors for each badge BEFORE adding event listeners
tagBadges.forEach(function(badge) {
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagColor = badge.getAttribute('data-tag-color');
tagBadges.forEach(function (badge) {
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var tagColor = badge.getAttribute("data-tag-color");
// Store the original color (either from data-tag-color or use secondary for tags without color)
if (tagColor) {
self.originalTagColors[tagId] = tagColor;
console.log('[realtimeSearch] Stored original color for tag ' + tagId + ': ' + tagColor);
console.log(
"[realtimeSearch] Stored original color for tag " + tagId + ": " + tagColor
);
} else {
self.originalTagColors[tagId] = 'var(--bs-secondary, ' + secondaryColor + ')';
console.log('[realtimeSearch] Tag ' + tagId + ' has no color, using secondary');
self.originalTagColors[tagId] = "var(--bs-secondary, " + secondaryColor + ")";
console.log("[realtimeSearch] Tag " + tagId + " has no color, using secondary");
}
});
tagBadges.forEach(function(badge) {
badge.addEventListener('click', function(e) {
tagBadges.forEach(function (badge) {
badge.addEventListener("click", function (e) {
e.preventDefault();
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var originalColor = self.originalTagColors[tagId];
console.log('[realtimeSearch] Tag badge clicked: ' + tagId + ' (original color: ' + originalColor + ')');
console.log(
"[realtimeSearch] Tag badge clicked: " +
tagId +
" (original color: " +
originalColor +
")"
);
// Toggle tag selection
if (self.selectedTags.has(tagId)) {
// Deselect
self.selectedTags.delete(tagId);
console.log('[realtimeSearch] Tag ' + tagId + ' deselected');
console.log("[realtimeSearch] Tag " + tagId + " deselected");
} else {
// Select
self.selectedTags.add(tagId);
console.log('[realtimeSearch] Tag ' + tagId + ' selected');
console.log("[realtimeSearch] Tag " + tagId + " selected");
}
// Update colors for ALL badges based on selection state
tagBadges.forEach(function(badge) {
var id = parseInt(badge.getAttribute('data-tag-id'), 10);
tagBadges.forEach(function (badge) {
var id = parseInt(badge.getAttribute("data-tag-id"), 10);
if (self.selectedTags.size === 0) {
// No tags selected: restore all to original colors
var originalColor = self.originalTagColors[id];
badge.style.setProperty('background-color', originalColor, 'important');
badge.style.setProperty('border-color', originalColor, 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' reset to original color (no selection)');
badge.style.setProperty("background-color", originalColor, "important");
badge.style.setProperty("border-color", originalColor, "important");
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " +
id +
" reset to original color (no selection)"
);
} else if (self.selectedTags.has(id)) {
// Selected: primary color
badge.style.setProperty('background-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
badge.style.setProperty('border-color', 'var(--bs-primary, ' + primaryColor + ')', 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' set to primary (selected)');
badge.style.setProperty(
"background-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-primary, " + primaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to primary (selected)"
);
} else {
// Not selected but others are: secondary color
badge.style.setProperty('background-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
badge.style.setProperty('border-color', 'var(--bs-secondary, ' + secondaryColor + ')', 'important');
badge.style.setProperty('color', '#ffffff', 'important');
console.log('[realtimeSearch] Badge ' + id + ' set to secondary (not selected)');
badge.style.setProperty(
"background-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty(
"border-color",
"var(--bs-secondary, " + secondaryColor + ")",
"important"
);
badge.style.setProperty("color", "#ffffff", "important");
console.log(
"[realtimeSearch] Badge " + id + " set to secondary (not selected)"
);
}
});
// Filter products (independent of search/category state)
self._filterProducts();
});
});
// POLLING FALLBACK: Since Odoo components may intercept events,
// use polling to detect value changes
console.log('[realtimeSearch] 🚀 POLLING INITIALIZATION STARTING');
console.log('[realtimeSearch] Search input element:', self.searchInput);
console.log('[realtimeSearch] Category select element:', self.categorySelect);
console.log("[realtimeSearch] 🚀 POLLING INITIALIZATION STARTING");
console.log("[realtimeSearch] Search input element:", self.searchInput);
console.log("[realtimeSearch] Category select element:", self.categorySelect);
var pollingCounter = 0;
var pollInterval = setInterval(function() {
var pollInterval = setInterval(function () {
try {
pollingCounter++;
// Try multiple ways to get the search value
var currentSearchValue = self.searchInput.value || '';
var currentSearchAttr = self.searchInput.getAttribute('value') || '';
var currentSearchDataValue = self.searchInput.getAttribute('data-value') || '';
var currentSearchInnerText = self.searchInput.innerText || '';
var currentCategoryValue = self.categorySelect ? (self.categorySelect.value || '') : '';
var currentSearchValue = self.searchInput.value || "";
var currentSearchAttr = self.searchInput.getAttribute("value") || "";
var currentSearchDataValue = self.searchInput.getAttribute("data-value") || "";
var currentSearchInnerText = self.searchInput.innerText || "";
var currentCategoryValue = self.categorySelect
? self.categorySelect.value || ""
: "";
// FIRST POLL: Detailed debug
if (pollingCounter === 1) {
console.log('═══════════════════════════════════════════');
console.log('[realtimeSearch] 🔍 FIRST POLLING DEBUG (POLL #1)');
console.log('═══════════════════════════════════════════');
console.log('Search input .value:', JSON.stringify(currentSearchValue));
console.log('Search input getAttribute("value"):', JSON.stringify(currentSearchAttr));
console.log('Search input getAttribute("data-value"):', JSON.stringify(currentSearchDataValue));
console.log('Search input innerText:', JSON.stringify(currentSearchInnerText));
console.log('Category select .value:', JSON.stringify(currentCategoryValue));
console.log('Last stored values - search:"' + self.lastSearchValue + '" category:"' + self.lastCategoryValue + '"');
console.log('═══════════════════════════════════════════');
console.log("═══════════════════════════════════════════");
console.log("[realtimeSearch] 🔍 FIRST POLLING DEBUG (POLL #1)");
console.log("═══════════════════════════════════════════");
console.log("Search input .value:", JSON.stringify(currentSearchValue));
console.log(
'Search input getAttribute("value"):',
JSON.stringify(currentSearchAttr)
);
console.log(
'Search input getAttribute("data-value"):',
JSON.stringify(currentSearchDataValue)
);
console.log(
"Search input innerText:",
JSON.stringify(currentSearchInnerText)
);
console.log(
"Category select .value:",
JSON.stringify(currentCategoryValue)
);
console.log(
'Last stored values - search:"' +
self.lastSearchValue +
'" category:"' +
self.lastCategoryValue +
'"'
);
console.log("═══════════════════════════════════════════");
}
// Log every 20 polls (reduce spam)
if (pollingCounter % 20 === 0) {
console.log('[realtimeSearch] POLLING #' + pollingCounter + ': search="' + currentSearchValue + '" category="' + currentCategoryValue + '"');
console.log(
"[realtimeSearch] POLLING #" +
pollingCounter +
': search="' +
currentSearchValue +
'" category="' +
currentCategoryValue +
'"'
);
}
// Check for ANY change in either field
if (currentSearchValue !== self.lastSearchValue || currentCategoryValue !== self.lastCategoryValue) {
console.log('[realtimeSearch] ⚡ CHANGE DETECTED: search="' + currentSearchValue + '" (was:"' + self.lastSearchValue + '") | category="' + currentCategoryValue + '" (was:"' + self.lastCategoryValue + '")');
if (
currentSearchValue !== self.lastSearchValue ||
currentCategoryValue !== self.lastCategoryValue
) {
console.log(
'[realtimeSearch] ⚡ CHANGE DETECTED: search="' +
currentSearchValue +
'" (was:"' +
self.lastSearchValue +
'") | category="' +
currentCategoryValue +
'" (was:"' +
self.lastCategoryValue +
'")'
);
self.lastSearchValue = currentSearchValue;
self.lastCategoryValue = currentCategoryValue;
self._filterProducts();
// Reset infinite scroll with new filters (will reload from server)
if (
window.infiniteScroll &&
typeof window.infiniteScroll.resetWithFilters === "function"
) {
console.log(
"[realtimeSearch] Calling infiniteScroll.resetWithFilters()"
);
window.infiniteScroll.resetWithFilters(
currentSearchValue,
currentCategoryValue
);
} else {
// Fallback: filter locally (but this only filters loaded products)
console.log(
"[realtimeSearch] infiniteScroll not available, filtering locally only"
);
self._filterProducts();
}
}
} catch (error) {
console.error('[realtimeSearch] ❌ Error in polling:', error.message);
console.error("[realtimeSearch] ❌ Error in polling:", error.message);
}
}, 300); // Check every 300ms
console.log('[realtimeSearch] ✅ Polling interval started with ID:', pollInterval);
console.log('[realtimeSearch] Event listeners attached with polling fallback');
}, 300); // Check every 300ms
console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval);
console.log("[realtimeSearch] Event listeners attached with polling fallback");
},
_initializeAvailableTags: function() {
_initializeAvailableTags: function () {
/**
* Initialize availableTags map from the DOM tag filter badges.
* Format: availableTags[tagId] = {id, name, count}
*/
var self = this;
var tagBadges = document.querySelectorAll('[data-toggle="tag-filter"]');
tagBadges.forEach(function(badge) {
var tagId = parseInt(badge.getAttribute('data-tag-id'), 10);
var tagName = badge.getAttribute('data-tag-name') || '';
var countSpan = badge.querySelector('.tag-count');
tagBadges.forEach(function (badge) {
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
var tagName = badge.getAttribute("data-tag-name") || "";
var countSpan = badge.querySelector(".tag-count");
var count = countSpan ? parseInt(countSpan.textContent, 10) : 0;
self.availableTags[tagId] = {
id: tagId,
name: tagName,
count: count
count: count,
};
});
console.log('[realtimeSearch] Initialized ' + Object.keys(self.availableTags).length + ' available tags');
console.log(
"[realtimeSearch] Initialized " +
Object.keys(self.availableTags).length +
" available tags"
);
},
_filterProducts: function() {
_filterProducts: function () {
var self = this;
try {
var searchQuery = (self.searchInput.value || '').toLowerCase().trim();
var selectedCategoryId = (self.categorySelect.value || '').toString();
console.log('[realtimeSearch] Filtering: search=' + searchQuery + ' category=' + selectedCategoryId + ' tags=' + Array.from(self.selectedTags).join(','));
var searchQuery = (self.searchInput.value || "").toLowerCase().trim();
var selectedCategoryId = (self.categorySelect.value || "").toString();
console.log(
"[realtimeSearch] Filtering: search=" +
searchQuery +
" category=" +
selectedCategoryId +
" tags=" +
Array.from(self.selectedTags).join(",")
);
// Build a set of allowed category IDs (selected category + ALL descendants recursively)
var allowedCategories = {};
if (selectedCategoryId) {
allowedCategories[selectedCategoryId] = true;
// Recursive function to get all descendants
var getAllDescendants = function(parentId) {
var getAllDescendants = function (parentId) {
var descendants = [];
if (self.categoryHierarchy[parentId]) {
self.categoryHierarchy[parentId].forEach(function(childId) {
self.categoryHierarchy[parentId].forEach(function (childId) {
descendants.push(childId);
allowedCategories[childId] = true;
// Recursivamente obtener descendientes del hijo
@ -375,47 +498,57 @@
}
return descendants;
};
var allDescendants = getAllDescendants(selectedCategoryId);
console.log('[realtimeSearch] Selected category ' + selectedCategoryId + ' has ' + allDescendants.length + ' total descendants');
console.log('[realtimeSearch] Allowed categories:', Object.keys(allowedCategories));
console.log(
"[realtimeSearch] Selected category " +
selectedCategoryId +
" has " +
allDescendants.length +
" total descendants"
);
console.log(
"[realtimeSearch] Allowed categories:",
Object.keys(allowedCategories)
);
}
var visibleCount = 0;
var hiddenCount = 0;
// Track tag counts for dynamic badge updates
var tagCounts = {};
for (var tagId in self.availableTags) {
tagCounts[tagId] = 0;
}
self.allProducts.forEach(function(product) {
self.allProducts.forEach(function (product) {
var nameMatches = !searchQuery || product.name.indexOf(searchQuery) !== -1;
var categoryMatches = !selectedCategoryId || allowedCategories[product.category];
var categoryMatches =
!selectedCategoryId || allowedCategories[product.category];
// Tag filtering: if tags are selected, product must have AT LEAST ONE selected tag (OR logic)
var tagMatches = true;
if (self.selectedTags.size > 0) {
tagMatches = product.tags.some(function(productTagId) {
tagMatches = product.tags.some(function (productTagId) {
return self.selectedTags.has(productTagId);
});
}
var shouldShow = nameMatches && categoryMatches && tagMatches;
if (shouldShow) {
product.element.classList.remove('hidden-product');
product.element.classList.remove("hidden-product");
visibleCount++;
// Count this product's tags toward the dynamic counters
product.tags.forEach(function(tagId) {
product.tags.forEach(function (tagId) {
if (tagCounts.hasOwnProperty(tagId)) {
tagCounts[tagId]++;
}
});
} else {
product.element.classList.add('hidden-product');
product.element.classList.add("hidden-product");
hiddenCount++;
}
});
@ -424,71 +557,85 @@
for (var tagId in tagCounts) {
var badge = document.querySelector('[data-tag-id="' + tagId + '"]');
if (badge) {
var countSpan = badge.querySelector('.tag-count');
var countSpan = badge.querySelector(".tag-count");
if (countSpan) {
countSpan.textContent = tagCounts[tagId];
}
}
}
console.log('[realtimeSearch] Filter result: visible=' + visibleCount + ' hidden=' + hiddenCount);
console.log(
"[realtimeSearch] Filter result: visible=" +
visibleCount +
" hidden=" +
hiddenCount
);
} catch (error) {
console.error('[realtimeSearch] ERROR in _filterProducts():', error.message);
console.error('[realtimeSearch] Stack:', error.stack);
console.error("[realtimeSearch] ERROR in _filterProducts():", error.message);
console.error("[realtimeSearch] Stack:", error.stack);
}
}
},
};
// Initialize when DOM is ready
console.log('[realtimeSearch] Script loaded, DOM state: ' + document.readyState);
console.log("[realtimeSearch] Script loaded, DOM state: " + document.readyState);
function tryInit() {
try {
console.log('[realtimeSearch] Attempting initialization...');
console.log("[realtimeSearch] Attempting initialization...");
// Query product cards
var productCards = document.querySelectorAll('.product-card');
console.log('[realtimeSearch] Found ' + productCards.length + ' product cards');
var productCards = document.querySelectorAll(".product-card");
console.log("[realtimeSearch] Found " + productCards.length + " product cards");
// Use the NEW pure HTML input with ID (not transformed by Odoo)
var searchInput = document.getElementById('realtime-search-input');
console.log('[realtimeSearch] Search input found:', !!searchInput);
var searchInput = document.getElementById("realtime-search-input");
console.log("[realtimeSearch] Search input found:", !!searchInput);
if (searchInput) {
console.log('[realtimeSearch] Search input class:', searchInput.className);
console.log('[realtimeSearch] Search input type:', searchInput.type);
console.log("[realtimeSearch] Search input class:", searchInput.className);
console.log("[realtimeSearch] Search input type:", searchInput.type);
}
// Category select with ID (not transformed by Odoo)
var categorySelect = document.getElementById('realtime-category-select');
console.log('[realtimeSearch] Category select found:', !!categorySelect);
var categorySelect = document.getElementById("realtime-category-select");
console.log("[realtimeSearch] Category select found:", !!categorySelect);
if (productCards.length > 0 && searchInput) {
console.log('[realtimeSearch] ✓ All elements found! Initializing...');
console.log("[realtimeSearch] ✓ All elements found! Initializing...");
// Assign elements to window.realtimeSearch BEFORE calling init()
window.realtimeSearch.searchInput = searchInput;
window.realtimeSearch.categorySelect = categorySelect;
window.realtimeSearch.init();
console.log('[realtimeSearch] ✓ Initialization complete!');
console.log("[realtimeSearch] ✓ Initialization complete!");
} else {
console.log('[realtimeSearch] Waiting for elements... (products:' + productCards.length + ', search:' + !!searchInput + ')');
console.log(
"[realtimeSearch] Waiting for elements... (products:" +
productCards.length +
", search:" +
!!searchInput +
")"
);
if (productCards.length === 0) {
console.log('[realtimeSearch] No product cards found. Current HTML body length:', document.body.innerHTML.length);
console.log(
"[realtimeSearch] No product cards found. Current HTML body length:",
document.body.innerHTML.length
);
}
setTimeout(tryInit, 500);
}
} catch (error) {
console.error('[realtimeSearch] ERROR in tryInit():', error.message);
console.error("[realtimeSearch] ERROR in tryInit():", error.message);
}
}
if (document.readyState === 'loading') {
console.log('[realtimeSearch] Adding DOMContentLoaded listener');
document.addEventListener('DOMContentLoaded', function() {
console.log('[realtimeSearch] DOMContentLoaded fired');
if (document.readyState === "loading") {
console.log("[realtimeSearch] Adding DOMContentLoaded listener");
document.addEventListener("DOMContentLoaded", function () {
console.log("[realtimeSearch] DOMContentLoaded fired");
tryInit();
});
} else {
console.log('[realtimeSearch] DOM already loaded, initializing with delay');
console.log("[realtimeSearch] DOM already loaded, initializing with delay");
setTimeout(tryInit, 500);
}
})();