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;
|
margin-bottom: 20px;
|
||||||
color: #666;
|
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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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;"
|
style="width: 100%; padding: 15px 20px; font-size: 16px; border: 2px solid #ddd; border-radius: 8px; box-sizing: border-box;"
|
||||||
onkeyup="handleSearch(event)"
|
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
|
<button
|
||||||
onclick="performSearch()"
|
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;"
|
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>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Product data
|
// Global data
|
||||||
let productData = [];
|
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
|
// Load product data on page load
|
||||||
async function loadProductData() {
|
async function loadProductData() {
|
||||||
@@ -174,37 +508,86 @@
|
|||||||
return normalizeText(value).includes(term);
|
return normalizeText(value).includes(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search products with partial matching
|
// Match product against filter option
|
||||||
function searchProducts(searchQuery) {
|
function matchesFilterOption(product, option) {
|
||||||
if (!searchQuery || searchQuery.trim() === '') {
|
const association = option.associations;
|
||||||
return [];
|
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;
|
||||||
const searchTerms = searchQuery.toLowerCase()
|
|
||||||
.split(/\s+/)
|
|
||||||
.filter(term => term.length > 0)
|
|
||||||
.map(term => normalizeText(term));
|
|
||||||
|
|
||||||
console.log('Search terms:', searchTerms);
|
// 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 that match ALL search terms
|
// Filter products
|
||||||
const results = productData.filter(product => {
|
const results = productData.filter(product => {
|
||||||
// Each product must match ALL search terms
|
// Step 1: Check filters (AND between groups, OR within group)
|
||||||
return searchTerms.every(term => {
|
for (const groupId in checkedFilters) {
|
||||||
// Check if the term matches any of these fields
|
const selectedOptions = checkedFilters[groupId];
|
||||||
return (
|
|
||||||
matchesTerm(product.productCode, term) ||
|
if (selectedOptions.length > 0) {
|
||||||
matchesTerm(product.id, term) ||
|
// Check if product matches ANY of the selected options (OR logic)
|
||||||
matchesTerm(product.description, term) ||
|
const matchesGroup = selectedOptions.some(option =>
|
||||||
matchesTerm(product.category, term) ||
|
matchesFilterOption(product, option)
|
||||||
matchesTerm(product.baseType, term) ||
|
);
|
||||||
matchesTerm(product.subType, term) ||
|
|
||||||
matchesTerm(product.materials, term) ||
|
if (!matchesGroup) return false; // AND logic between groups
|
||||||
matchesTerm(product.colors, term) ||
|
}
|
||||||
matchesTerm(product.location, term)
|
}
|
||||||
);
|
|
||||||
});
|
// 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));
|
||||||
|
|
||||||
|
// Product must match ALL search terms
|
||||||
|
const matchesSearch = searchTerms.every(term => {
|
||||||
|
return (
|
||||||
|
matchesTerm(product.productCode, term) ||
|
||||||
|
matchesTerm(product.id, term) ||
|
||||||
|
matchesTerm(product.description, term) ||
|
||||||
|
matchesTerm(product.category, term) ||
|
||||||
|
matchesTerm(product.baseType, term) ||
|
||||||
|
matchesTerm(product.subType, term) ||
|
||||||
|
matchesTerm(product.materials, term) ||
|
||||||
|
matchesTerm(product.colors, term) ||
|
||||||
|
matchesTerm(product.location, term)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!matchesSearch) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
@@ -278,22 +661,79 @@
|
|||||||
resultsDiv.innerHTML = html;
|
resultsDiv.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform search
|
// Perform search with new filter-based logic
|
||||||
function performSearch() {
|
function performSearch() {
|
||||||
const searchInput = document.getElementById('searchInput');
|
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 = `
|
document.getElementById('searchResults').innerHTML = `
|
||||||
<div class="no-results">
|
<div class="no-results">
|
||||||
<p>Please enter a search term</p>
|
<p>Please enter a search term or select filters</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const results = searchProducts(searchQuery);
|
console.log('Remaining search terms:', remainingSearch);
|
||||||
displayResults(results, searchQuery);
|
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
|
// Handle Enter key in search input
|
||||||
@@ -307,6 +747,15 @@
|
|||||||
function clearSearch() {
|
function clearSearch() {
|
||||||
document.getElementById('searchInput').value = '';
|
document.getElementById('searchInput').value = '';
|
||||||
document.getElementById('searchResults').innerHTML = '';
|
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();
|
document.getElementById('searchInput').focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +812,19 @@
|
|||||||
async function init() {
|
async function init() {
|
||||||
await loadUserInfo();
|
await loadUserInfo();
|
||||||
await loadProductData();
|
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
|
// Check if there's a search query in URL
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -373,6 +835,9 @@
|
|||||||
const searchInput = document.getElementById('searchInput');
|
const searchInput = document.getElementById('searchInput');
|
||||||
searchInput.value = queryParam;
|
searchInput.value = queryParam;
|
||||||
|
|
||||||
|
// Auto-check filters that match the search query
|
||||||
|
autoCheckFiltersFromQuery(queryParam);
|
||||||
|
|
||||||
// Automatically perform the search
|
// Automatically perform the search
|
||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user