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
641 lines
28 KiB
JavaScript
641 lines
28 KiB
JavaScript
/*
|
|
* Copyright 2025 Criptomart
|
|
* License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
*/
|
|
|
|
(function () {
|
|
"use strict";
|
|
|
|
window.realtimeSearch = {
|
|
searchInput: null,
|
|
categorySelect: null,
|
|
allProducts: [],
|
|
debounceTimer: null,
|
|
debounceDelay: 0,
|
|
categoryHierarchy: {}, // Maps parent category IDs to their children
|
|
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...");
|
|
|
|
// searchInput y categorySelect ya fueron asignados por tryInit()
|
|
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!");
|
|
return false;
|
|
}
|
|
|
|
if (!this.categorySelect) {
|
|
console.error("[realtimeSearch] ERROR: Category select not found!");
|
|
return false;
|
|
}
|
|
|
|
this._buildCategoryHierarchyFromDOM();
|
|
this._storeAllProducts();
|
|
console.log(
|
|
"[realtimeSearch] After _storeAllProducts(), calling _attachEventListeners()..."
|
|
);
|
|
this._attachEventListeners();
|
|
console.log("[realtimeSearch] ✓ Initialized successfully");
|
|
return true;
|
|
},
|
|
|
|
_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 optionStack = []; // Stack para mantener los padres en cada nivel
|
|
|
|
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];
|
|
if (!self.categoryHierarchy[parentId]) {
|
|
self.categoryHierarchy[parentId] = [];
|
|
}
|
|
if (!self.categoryHierarchy[parentId].includes(categoryId)) {
|
|
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) {
|
|
optionStack[depth] = categoryId;
|
|
} else {
|
|
optionStack.push(categoryId);
|
|
}
|
|
});
|
|
|
|
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");
|
|
|
|
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") || "";
|
|
|
|
// 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);
|
|
});
|
|
}
|
|
|
|
self.allProducts.push({
|
|
element: card,
|
|
name: name.toLowerCase(),
|
|
category: categoryId.toString(),
|
|
originalCategory: categoryId,
|
|
tags: tagIds, // Array of tag IDs for this product
|
|
});
|
|
});
|
|
|
|
console.log("[realtimeSearch] Total products stored: " + this.allProducts.length);
|
|
},
|
|
|
|
_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 = "";
|
|
|
|
// Prevent form submission completely
|
|
var form = self.searchInput.closest("form");
|
|
if (form) {
|
|
form.addEventListener("submit", function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
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") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
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) {
|
|
try {
|
|
console.log('[realtimeSearch] INPUT event - value: "' + e.target.value + '"');
|
|
self._filterProducts();
|
|
} catch (error) {
|
|
console.error("[realtimeSearch] Error in input listener:", error.message);
|
|
}
|
|
});
|
|
|
|
// Also keep 'keyup' for extra compatibility
|
|
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);
|
|
}
|
|
});
|
|
|
|
// Category select
|
|
self.categorySelect.addEventListener("change", function (e) {
|
|
try {
|
|
console.log(
|
|
'[realtimeSearch] CHANGE event - selected: "' + e.target.value + '"'
|
|
);
|
|
self._filterProducts();
|
|
} catch (error) {
|
|
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");
|
|
|
|
// 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
|
|
);
|
|
|
|
// 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");
|
|
|
|
// 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
|
|
);
|
|
} else {
|
|
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) {
|
|
e.preventDefault();
|
|
var tagId = parseInt(badge.getAttribute("data-tag-id"), 10);
|
|
var originalColor = self.originalTagColors[tagId];
|
|
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");
|
|
} else {
|
|
// Select
|
|
self.selectedTags.add(tagId);
|
|
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);
|
|
|
|
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)"
|
|
);
|
|
} 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)"
|
|
);
|
|
} 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)"
|
|
);
|
|
}
|
|
});
|
|
|
|
// 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);
|
|
|
|
var pollingCounter = 0;
|
|
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 || ""
|
|
: "";
|
|
|
|
// 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("═══════════════════════════════════════════");
|
|
}
|
|
|
|
// Log every 20 polls (reduce spam)
|
|
if (pollingCounter % 20 === 0) {
|
|
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 +
|
|
'")'
|
|
);
|
|
self.lastSearchValue = currentSearchValue;
|
|
self.lastCategoryValue = currentCategoryValue;
|
|
|
|
// 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);
|
|
}
|
|
}, 300); // Check every 300ms
|
|
|
|
console.log("[realtimeSearch] ✅ Polling interval started with ID:", pollInterval);
|
|
|
|
console.log("[realtimeSearch] Event listeners attached with polling fallback");
|
|
},
|
|
|
|
_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");
|
|
var count = countSpan ? parseInt(countSpan.textContent, 10) : 0;
|
|
|
|
self.availableTags[tagId] = {
|
|
id: tagId,
|
|
name: tagName,
|
|
count: count,
|
|
};
|
|
});
|
|
|
|
console.log(
|
|
"[realtimeSearch] Initialized " +
|
|
Object.keys(self.availableTags).length +
|
|
" available tags"
|
|
);
|
|
},
|
|
|
|
_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(",")
|
|
);
|
|
|
|
// 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 descendants = [];
|
|
if (self.categoryHierarchy[parentId]) {
|
|
self.categoryHierarchy[parentId].forEach(function (childId) {
|
|
descendants.push(childId);
|
|
allowedCategories[childId] = true;
|
|
// Recursivamente obtener descendientes del hijo
|
|
var grandDescendants = getAllDescendants(childId);
|
|
descendants = descendants.concat(grandDescendants);
|
|
});
|
|
}
|
|
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)
|
|
);
|
|
}
|
|
|
|
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) {
|
|
var nameMatches = !searchQuery || product.name.indexOf(searchQuery) !== -1;
|
|
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) {
|
|
return self.selectedTags.has(productTagId);
|
|
});
|
|
}
|
|
|
|
var shouldShow = nameMatches && categoryMatches && tagMatches;
|
|
|
|
if (shouldShow) {
|
|
product.element.classList.remove("hidden-product");
|
|
visibleCount++;
|
|
|
|
// Count this product's tags toward the dynamic counters
|
|
product.tags.forEach(function (tagId) {
|
|
if (tagCounts.hasOwnProperty(tagId)) {
|
|
tagCounts[tagId]++;
|
|
}
|
|
});
|
|
} else {
|
|
product.element.classList.add("hidden-product");
|
|
hiddenCount++;
|
|
}
|
|
});
|
|
|
|
// Update badge counts dynamically
|
|
for (var tagId in tagCounts) {
|
|
var badge = document.querySelector('[data-tag-id="' + tagId + '"]');
|
|
if (badge) {
|
|
var countSpan = badge.querySelector(".tag-count");
|
|
if (countSpan) {
|
|
countSpan.textContent = tagCounts[tagId];
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
},
|
|
};
|
|
|
|
// Initialize when DOM is ready
|
|
console.log("[realtimeSearch] Script loaded, DOM state: " + document.readyState);
|
|
|
|
function tryInit() {
|
|
try {
|
|
console.log("[realtimeSearch] Attempting initialization...");
|
|
|
|
// Query 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);
|
|
if (searchInput) {
|
|
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);
|
|
|
|
if (productCards.length > 0 && searchInput) {
|
|
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!");
|
|
} else {
|
|
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
|
|
);
|
|
}
|
|
setTimeout(tryInit, 500);
|
|
}
|
|
} catch (error) {
|
|
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");
|
|
tryInit();
|
|
});
|
|
} else {
|
|
console.log("[realtimeSearch] DOM already loaded, initializing with delay");
|
|
setTimeout(tryInit, 500);
|
|
}
|
|
})();
|