Filters on advanced search
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
{
|
||||
"filterGroups": [
|
||||
{
|
||||
"id": "type",
|
||||
"label": "Type",
|
||||
"icon": "📋",
|
||||
"order": 1,
|
||||
"options": [
|
||||
{
|
||||
"id": "storm_door",
|
||||
"label": "Storm Door",
|
||||
"value": "Storm Door",
|
||||
"aliases": ["storm-door", "stormdoor"],
|
||||
"keywords": ["storm", "door"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "subType.door",
|
||||
"matchMethod": "exact",
|
||||
"normalizedValue": "stormdoor"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "storm_window",
|
||||
"label": "Storm Window",
|
||||
"value": "Storm Window",
|
||||
"aliases": ["storm-window", "stormwindow"],
|
||||
"keywords": ["storm", "window"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "subType.window",
|
||||
"matchMethod": "exact",
|
||||
"normalizedValue": "stormwindow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "primary_window",
|
||||
"label": "Primary Window",
|
||||
"value": "Primary Window",
|
||||
"aliases": ["primary-window", "primarywindow"],
|
||||
"keywords": ["primary", "window"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "subType.window",
|
||||
"matchMethod": "exact",
|
||||
"normalizedValue": "primarywindow"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logicWithinGroup": "OR",
|
||||
"autoCheckBehavior": {
|
||||
"enabled": true,
|
||||
"requireAllKeywords": true,
|
||||
"requireSequential": true,
|
||||
"minSpecificity": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "color",
|
||||
"label": "Color",
|
||||
"icon": "🎨",
|
||||
"order": 2,
|
||||
"options": [
|
||||
{
|
||||
"id": "white",
|
||||
"label": "White",
|
||||
"value": "White",
|
||||
"aliases": ["wht"],
|
||||
"keywords": ["white"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "bronze",
|
||||
"label": "Bronze",
|
||||
"value": "Bronze",
|
||||
"aliases": ["brz", "brown"],
|
||||
"keywords": ["bronze"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "bronze"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "black",
|
||||
"label": "Black",
|
||||
"value": "Black",
|
||||
"aliases": ["blk"],
|
||||
"keywords": ["black"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "black"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sandstone",
|
||||
"label": "Sandstone",
|
||||
"value": "Sandstone",
|
||||
"aliases": ["sand", "sndst"],
|
||||
"keywords": ["sandstone"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "sandstone"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "tan",
|
||||
"label": "Tan",
|
||||
"value": "Tan",
|
||||
"aliases": [],
|
||||
"keywords": ["tan"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "tan"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mill",
|
||||
"label": "Mill",
|
||||
"value": "Mill",
|
||||
"aliases": ["mill finish"],
|
||||
"keywords": ["mill"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "colors",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "mill"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logicWithinGroup": "OR",
|
||||
"autoCheckBehavior": {
|
||||
"enabled": true,
|
||||
"requireAllKeywords": true,
|
||||
"requireSequential": false,
|
||||
"minSpecificity": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "material",
|
||||
"label": "Material",
|
||||
"icon": "🔧",
|
||||
"order": 3,
|
||||
"options": [
|
||||
{
|
||||
"id": "aluminum",
|
||||
"label": "Aluminum",
|
||||
"value": "Aluminum",
|
||||
"aliases": ["alum", "aluminium", "al"],
|
||||
"keywords": ["aluminum"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "materials",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "aluminum"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "vinyl",
|
||||
"label": "Vinyl",
|
||||
"value": "Vinyl",
|
||||
"aliases": ["vnyl"],
|
||||
"keywords": ["vinyl"],
|
||||
"requiredKeywords": "all",
|
||||
"associations": {
|
||||
"productField": "materials",
|
||||
"matchMethod": "array_includes",
|
||||
"normalizedValue": "vinyl"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logicWithinGroup": "OR",
|
||||
"autoCheckBehavior": {
|
||||
"enabled": true,
|
||||
"requireAllKeywords": true,
|
||||
"requireSequential": false,
|
||||
"minSpecificity": 1
|
||||
}
|
||||
}
|
||||
],
|
||||
"baseTypes": [
|
||||
{
|
||||
"id": "door",
|
||||
"label": "Door",
|
||||
"keywords": ["door"],
|
||||
"impliesOptions": ["storm_door"],
|
||||
"productField": "baseType",
|
||||
"normalizedValue": "door"
|
||||
},
|
||||
{
|
||||
"id": "window",
|
||||
"label": "Window",
|
||||
"keywords": ["window"],
|
||||
"impliesOptions": ["storm_window", "primary_window"],
|
||||
"productField": "baseType",
|
||||
"normalizedValue": "window"
|
||||
},
|
||||
{
|
||||
"id": "patio_door",
|
||||
"label": "Patio Door",
|
||||
"keywords": ["patio", "door"],
|
||||
"requiredKeywords": "all",
|
||||
"impliesOptions": [],
|
||||
"productField": "baseType",
|
||||
"normalizedValue": "patiodoor"
|
||||
}
|
||||
],
|
||||
"searchBehavior": {
|
||||
"defaultLogic": "AND",
|
||||
"filterLogicBetweenGroups": "AND",
|
||||
"filterLogicWithinGroup": "OR",
|
||||
"processOrder": ["type", "color", "material"],
|
||||
"removeMatchedFromSearch": true,
|
||||
"requireSequentialForMultiWord": true,
|
||||
"ignoreWords": ["and", "or", "the", "a", "an"]
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,101 @@
|
||||
margin-bottom: 20px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Filter Sections */
|
||||
.filters-container {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.filter-section {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.filter-toggle {
|
||||
width: 100%;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: #495057;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.filter-toggle:hover {
|
||||
background-color: #e9ecef;
|
||||
border-color: #ced4da;
|
||||
}
|
||||
|
||||
.filter-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.filter-title {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.filter-values {
|
||||
color: #6a4c93;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-arrow {
|
||||
font-size: 12px;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-content {
|
||||
margin-top: 5px;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.filter-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-checkbox:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.filter-checkbox input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.filter-checkbox span {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -103,6 +198,14 @@
|
||||
style="width: 100%; padding: 15px 20px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;"
|
||||
onkeyup="handleSearch(event)"
|
||||
>
|
||||
<!-- Hidden field stores the cleaned search text after filter keywords are removed -->
|
||||
<input type="hidden" id="remainingSearchText" value="">
|
||||
|
||||
<!-- Filter Sections (dynamically generated from filter_config.json) -->
|
||||
<div id="filtersContainer" class="filters-container" style="margin-top: 20px;">
|
||||
<!-- Filters will be generated here by JavaScript -->
|
||||
</div>
|
||||
|
||||
<button
|
||||
onclick="performSearch()"
|
||||
style="margin-top: 15px; padding: 15px 30px; font-size: 16px; background-color: #6a4c93; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600;"
|
||||
@@ -127,8 +230,239 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Product data
|
||||
// Global data
|
||||
let productData = [];
|
||||
let filterConfig = null;
|
||||
let searchFieldDirty = false; // Track if user has manually edited search after initial load
|
||||
|
||||
// Load filter configuration
|
||||
async function loadFilterConfig() {
|
||||
try {
|
||||
const response = await fetch('/quiz/data/filter_config.json');
|
||||
filterConfig = await response.json();
|
||||
console.log('Loaded filter config:', filterConfig);
|
||||
return filterConfig;
|
||||
} catch (error) {
|
||||
console.error('Error loading filter config:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Generate filter UI from configuration
|
||||
function generateFiltersUI() {
|
||||
if (!filterConfig) return;
|
||||
|
||||
const container = document.getElementById('filtersContainer');
|
||||
let html = '';
|
||||
|
||||
// Sort filter groups by order
|
||||
const sortedGroups = [...filterConfig.filterGroups].sort((a, b) => a.order - b.order);
|
||||
|
||||
sortedGroups.forEach(group => {
|
||||
const filterId = `${group.id}Filter`;
|
||||
const valuesId = `${group.id}FilterValues`;
|
||||
|
||||
html += `
|
||||
<div class="filter-section">
|
||||
<button class="filter-toggle" onclick="toggleFilter('${filterId}')" type="button">
|
||||
<span class="filter-icon">${group.icon}</span>
|
||||
<span class="filter-label">
|
||||
<span class="filter-title">${group.label}:</span>
|
||||
<span class="filter-values" id="${valuesId}">None selected</span>
|
||||
</span>
|
||||
<span class="filter-arrow" id="${filterId}-arrow">▼</span>
|
||||
</button>
|
||||
<div class="filter-content" id="${filterId}" style="display: none;">
|
||||
<div class="filter-options">
|
||||
`;
|
||||
|
||||
group.options.forEach(option => {
|
||||
html += `
|
||||
<label class="filter-checkbox">
|
||||
<input type="checkbox"
|
||||
name="${group.id}"
|
||||
value="${option.value}"
|
||||
data-option-id="${option.id}">
|
||||
<span>${option.label}</span>
|
||||
</label>
|
||||
`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Toggle filter visibility
|
||||
function toggleFilter(filterId) {
|
||||
const filterContent = document.getElementById(filterId);
|
||||
const arrow = document.getElementById(filterId + '-arrow');
|
||||
|
||||
if (filterContent.style.display === 'none') {
|
||||
filterContent.style.display = 'block';
|
||||
arrow.textContent = '▲';
|
||||
} else {
|
||||
filterContent.style.display = 'none';
|
||||
arrow.textContent = '▼';
|
||||
}
|
||||
}
|
||||
|
||||
// Get all checked filter values organized by group
|
||||
function getCheckedFilters() {
|
||||
const filters = {};
|
||||
|
||||
filterConfig.filterGroups.forEach(group => {
|
||||
filters[group.id] = [];
|
||||
document.querySelectorAll(`input[name="${group.id}"]:checked`).forEach(checkbox => {
|
||||
const optionId = checkbox.dataset.optionId;
|
||||
const option = group.options.find(opt => opt.id === optionId);
|
||||
if (option) {
|
||||
filters[group.id].push(option);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return filters;
|
||||
}
|
||||
|
||||
// Update filter display to show selected values
|
||||
function updateFilterDisplays() {
|
||||
if (!filterConfig) return;
|
||||
|
||||
filterConfig.filterGroups.forEach(group => {
|
||||
const valuesId = `${group.id}FilterValues`;
|
||||
const display = document.getElementById(valuesId);
|
||||
if (!display) return;
|
||||
|
||||
const checked = document.querySelectorAll(`input[name="${group.id}"]:checked`);
|
||||
|
||||
if (checked.length > 0) {
|
||||
const labels = Array.from(checked).map(cb => cb.value);
|
||||
display.textContent = labels.join(', ');
|
||||
display.style.color = '#6a4c93';
|
||||
} else {
|
||||
display.textContent = 'None selected';
|
||||
display.style.color = '#999';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listeners to all checkboxes
|
||||
function initializeFilterListeners() {
|
||||
document.querySelectorAll('.filter-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.addEventListener('change', updateFilterDisplays);
|
||||
});
|
||||
}
|
||||
|
||||
// Check if keywords appear sequentially in search string
|
||||
function checkSequentialMatch(searchString, keywords) {
|
||||
if (keywords.length === 0) return false;
|
||||
if (keywords.length === 1) return searchString.includes(keywords[0]);
|
||||
|
||||
let lastIndex = -1;
|
||||
for (const keyword of keywords) {
|
||||
const index = searchString.indexOf(keyword, lastIndex + 1);
|
||||
if (index === -1 || index <= lastIndex) {
|
||||
return false;
|
||||
}
|
||||
lastIndex = index;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove only the specific keywords from search string, not text between them
|
||||
function removeKeywordsFromSearch(searchString, keywords) {
|
||||
if (keywords.length === 0) return searchString;
|
||||
|
||||
let result = searchString;
|
||||
|
||||
// Remove each keyword individually
|
||||
keywords.forEach(keyword => {
|
||||
// Use word boundary to remove only complete words
|
||||
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
|
||||
result = result.replace(regex, '').trim();
|
||||
});
|
||||
|
||||
// Clean up multiple spaces
|
||||
result = result.replace(/\s+/g, ' ').trim();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Auto-check filters based on search query using sequential matching
|
||||
function autoCheckFiltersFromQuery(searchQuery) {
|
||||
if (!searchQuery || !filterConfig) return;
|
||||
|
||||
let remainingSearch = searchQuery.toLowerCase().trim();
|
||||
|
||||
// Remove ignored words
|
||||
const ignoreWords = filterConfig.searchBehavior.ignoreWords || [];
|
||||
ignoreWords.forEach(word => {
|
||||
const regex = new RegExp(`\\b${word}\\b`, 'gi');
|
||||
remainingSearch = remainingSearch.replace(regex, ' ').replace(/\s+/g, ' ').trim();
|
||||
});
|
||||
|
||||
console.log('Processing search:', remainingSearch);
|
||||
|
||||
// Process filter groups in configured order
|
||||
const processOrder = filterConfig.searchBehavior.processOrder || [];
|
||||
|
||||
processOrder.forEach(groupId => {
|
||||
const group = filterConfig.filterGroups.find(g => g.id === groupId);
|
||||
if (!group || !group.autoCheckBehavior.enabled) return;
|
||||
|
||||
group.options.forEach(option => {
|
||||
// Normalize keywords
|
||||
const normalizedKeywords = option.keywords.map(kw => kw.toLowerCase());
|
||||
|
||||
// Check if minimum specificity is met
|
||||
const minSpec = group.autoCheckBehavior.minSpecificity || 1;
|
||||
if (normalizedKeywords.length < minSpec) return;
|
||||
|
||||
// Check if all keywords are present
|
||||
const allPresent = normalizedKeywords.every(kw =>
|
||||
remainingSearch.includes(kw)
|
||||
);
|
||||
|
||||
if (!allPresent) return;
|
||||
|
||||
// If sequential is required, check that
|
||||
if (group.autoCheckBehavior.requireSequential && normalizedKeywords.length > 1) {
|
||||
if (!checkSequentialMatch(remainingSearch, normalizedKeywords)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the checkbox
|
||||
const checkbox = document.querySelector(
|
||||
`input[name="${group.id}"][data-option-id="${option.id}"]`
|
||||
);
|
||||
if (checkbox) {
|
||||
checkbox.checked = true;
|
||||
console.log(`Auto-checked: ${option.label}`);
|
||||
|
||||
// Remove keywords from search if configured
|
||||
if (filterConfig.searchBehavior.removeMatchedFromSearch) {
|
||||
remainingSearch = removeKeywordsFromSearch(remainingSearch, normalizedKeywords);
|
||||
console.log('Remaining search:', remainingSearch);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update displays after auto-checking
|
||||
updateFilterDisplays();
|
||||
|
||||
console.log('Final remaining search:', remainingSearch);
|
||||
|
||||
// Store the cleaned search text in hidden field
|
||||
document.getElementById('remainingSearchText').value = remainingSearch;
|
||||
}
|
||||
|
||||
// Load product data on page load
|
||||
async function loadProductData() {
|
||||
@@ -174,25 +508,69 @@
|
||||
return normalizeText(value).includes(term);
|
||||
}
|
||||
|
||||
// Search products with partial matching
|
||||
function searchProducts(searchQuery) {
|
||||
if (!searchQuery || searchQuery.trim() === '') {
|
||||
return [];
|
||||
// Match product against filter option
|
||||
function matchesFilterOption(product, option) {
|
||||
const association = option.associations;
|
||||
const field = association.productField;
|
||||
const normalizedValue = association.normalizedValue;
|
||||
const matchMethod = association.matchMethod;
|
||||
|
||||
// Get the product field value
|
||||
let productValue;
|
||||
if (field.includes('.')) {
|
||||
const parts = field.split('.');
|
||||
productValue = product[parts[0]]?.[parts[1]];
|
||||
} else {
|
||||
productValue = product[field];
|
||||
}
|
||||
|
||||
// Split search query into individual terms
|
||||
if (!productValue) return false;
|
||||
|
||||
// Apply match method
|
||||
switch (matchMethod) {
|
||||
case 'exact':
|
||||
return normalizeText(productValue) === normalizedValue;
|
||||
case 'array_includes':
|
||||
if (Array.isArray(productValue)) {
|
||||
return productValue.some(v => normalizeText(v) === normalizedValue);
|
||||
}
|
||||
return normalizeText(productValue) === normalizedValue;
|
||||
case 'contains':
|
||||
return normalizeText(productValue).includes(normalizedValue);
|
||||
default:
|
||||
return normalizeText(productValue).includes(normalizedValue);
|
||||
}
|
||||
}
|
||||
|
||||
// Search products with new filter-based logic
|
||||
function searchProducts(searchQuery, checkedFilters) {
|
||||
if (!filterConfig) return [];
|
||||
|
||||
// Filter products
|
||||
const results = productData.filter(product => {
|
||||
// Step 1: Check filters (AND between groups, OR within group)
|
||||
for (const groupId in checkedFilters) {
|
||||
const selectedOptions = checkedFilters[groupId];
|
||||
|
||||
if (selectedOptions.length > 0) {
|
||||
// Check if product matches ANY of the selected options (OR logic)
|
||||
const matchesGroup = selectedOptions.some(option =>
|
||||
matchesFilterOption(product, option)
|
||||
);
|
||||
|
||||
if (!matchesGroup) return false; // AND logic between groups
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Check search terms (AND logic)
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
const searchTerms = searchQuery.toLowerCase()
|
||||
.split(/\s+/)
|
||||
.filter(term => term.length > 0)
|
||||
.map(term => normalizeText(term));
|
||||
|
||||
console.log('Search terms:', searchTerms);
|
||||
|
||||
// Filter products that match ALL search terms
|
||||
const results = productData.filter(product => {
|
||||
// Each product must match ALL search terms
|
||||
return searchTerms.every(term => {
|
||||
// Check if the term matches any of these fields
|
||||
// Product must match ALL search terms
|
||||
const matchesSearch = searchTerms.every(term => {
|
||||
return (
|
||||
matchesTerm(product.productCode, term) ||
|
||||
matchesTerm(product.id, term) ||
|
||||
@@ -205,6 +583,11 @@
|
||||
matchesTerm(product.location, term)
|
||||
);
|
||||
});
|
||||
|
||||
if (!matchesSearch) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return results;
|
||||
@@ -278,22 +661,79 @@
|
||||
resultsDiv.innerHTML = html;
|
||||
}
|
||||
|
||||
// Perform search
|
||||
// Perform search with new filter-based logic
|
||||
function performSearch() {
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
const searchQuery = searchInput.value.trim();
|
||||
let searchQuery = searchInput.value.trim();
|
||||
let remainingSearch;
|
||||
|
||||
if (searchQuery === '') {
|
||||
// If search field has been manually modified, re-process everything
|
||||
if (searchFieldDirty) {
|
||||
console.log('=== Search field modified, re-processing ===');
|
||||
|
||||
// Clear all filters
|
||||
document.querySelectorAll('.filter-option input[type="checkbox"]').forEach(cb => {
|
||||
cb.checked = false;
|
||||
});
|
||||
|
||||
// Re-run auto-check from the new search text
|
||||
if (searchQuery) {
|
||||
autoCheckFiltersFromQuery(searchQuery);
|
||||
}
|
||||
|
||||
// Use the cleaned search from hidden field
|
||||
remainingSearch = document.getElementById('remainingSearchText').value.trim();
|
||||
|
||||
// Reset the dirty flag
|
||||
searchFieldDirty = false;
|
||||
} else {
|
||||
// Use the pre-calculated remaining search from hidden field
|
||||
remainingSearch = document.getElementById('remainingSearchText').value.trim().toLowerCase();
|
||||
console.log('=== Using stored remaining search ===');
|
||||
}
|
||||
|
||||
// Get checked filters
|
||||
const checkedFilters = getCheckedFilters();
|
||||
|
||||
// Check if we have any search criteria
|
||||
const hasRemainingSearch = remainingSearch && remainingSearch.trim().length > 0;
|
||||
const hasFilters = Object.values(checkedFilters).some(arr => arr.length > 0);
|
||||
|
||||
if (!hasRemainingSearch && !hasFilters) {
|
||||
document.getElementById('searchResults').innerHTML = `
|
||||
<div class="no-results">
|
||||
<p>Please enter a search term</p>
|
||||
<p>Please enter a search term or select filters</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
const results = searchProducts(searchQuery);
|
||||
displayResults(results, searchQuery);
|
||||
console.log('Remaining search terms:', remainingSearch);
|
||||
console.log('Checked filters:', checkedFilters);
|
||||
|
||||
// Perform search
|
||||
const results = searchProducts(remainingSearch, checkedFilters);
|
||||
|
||||
// Build display query string
|
||||
const filterParts = [];
|
||||
filterConfig.filterGroups.forEach(group => {
|
||||
const checkedOptions = checkedFilters[group.id] || [];
|
||||
if (checkedOptions.length > 0) {
|
||||
const labels = checkedOptions.map(opt => opt.label).join(', ');
|
||||
filterParts.push(`${group.label}: ${labels}`);
|
||||
}
|
||||
});
|
||||
|
||||
let displayQuery = remainingSearch || '';
|
||||
if (filterParts.length > 0) {
|
||||
if (displayQuery) {
|
||||
displayQuery += ' | ' + filterParts.join(' | ');
|
||||
} else {
|
||||
displayQuery = filterParts.join(' | ');
|
||||
}
|
||||
}
|
||||
|
||||
displayResults(results, displayQuery || 'all filters');
|
||||
}
|
||||
|
||||
// Handle Enter key in search input
|
||||
@@ -307,6 +747,15 @@
|
||||
function clearSearch() {
|
||||
document.getElementById('searchInput').value = '';
|
||||
document.getElementById('searchResults').innerHTML = '';
|
||||
|
||||
// Uncheck all filters
|
||||
document.querySelectorAll('.filter-checkbox input[type="checkbox"]').forEach(checkbox => {
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
// Update filter displays
|
||||
updateFilterDisplays();
|
||||
|
||||
document.getElementById('searchInput').focus();
|
||||
}
|
||||
|
||||
@@ -363,6 +812,19 @@
|
||||
async function init() {
|
||||
await loadUserInfo();
|
||||
await loadProductData();
|
||||
await loadFilterConfig();
|
||||
|
||||
// Generate filter UI from config
|
||||
if (filterConfig) {
|
||||
generateFiltersUI();
|
||||
initializeFilterListeners();
|
||||
}
|
||||
|
||||
// Mark search field as dirty when user manually types
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
searchFieldDirty = true;
|
||||
console.log('Search field marked as dirty');
|
||||
});
|
||||
|
||||
// Check if there's a search query in URL
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
@@ -373,6 +835,9 @@
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
searchInput.value = queryParam;
|
||||
|
||||
// Auto-check filters that match the search query
|
||||
autoCheckFiltersFromQuery(queryParam);
|
||||
|
||||
// Automatically perform the search
|
||||
performSearch();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user