Adds fuzzy search and enhances services UI interactions
Implements advanced service search using Fuse.js Improves category expand/collapse animations Adds interactive search functionality for service list Enhances user experience by enabling quick service filtering and smooth UI interactions
This commit is contained in:
parent
4cf30e1f27
commit
fad5e46dfb
4 changed files with 141 additions and 3 deletions
12
PlanTempus.Application/package-lock.json
generated
12
PlanTempus.Application/package-lock.json
generated
|
|
@ -4,6 +4,9 @@
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"fuse.js": "^7.1.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.27.2",
|
||||||
"purgecss": "^6.0.0"
|
"purgecss": "^6.0.0"
|
||||||
|
|
@ -654,6 +657,15 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fuse.js": {
|
||||||
|
"version": "7.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz",
|
||||||
|
"integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/glob": {
|
"node_modules/glob": {
|
||||||
"version": "10.5.0",
|
"version": "10.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,8 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze-css": "node analyze-css.js"
|
"analyze-css": "node analyze-css.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"fuse.js": "^7.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,16 @@ swp-search-input {
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
font-size: var(--font-size-base);
|
font-size: var(--font-size-base);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--color-text-tertiary);
|
color: var(--color-text-tertiary);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,133 @@
|
||||||
/**
|
/**
|
||||||
* Services Controller
|
* Services Controller
|
||||||
* Handles category collapse/expand animations
|
* Handles category collapse/expand animations and fuzzy search
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
|
interface ServiceItem {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
element: HTMLElement;
|
||||||
|
categoryRow: HTMLElement;
|
||||||
|
}
|
||||||
|
|
||||||
export class ServicesController {
|
export class ServicesController {
|
||||||
|
private fuse: Fuse<ServiceItem> | null = null;
|
||||||
|
private services: ServiceItem[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
private init(): void {
|
private init(): void {
|
||||||
this.setupCategoryToggle();
|
this.setupCategoryToggle();
|
||||||
|
this.setupSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupSearch(): void {
|
||||||
|
const searchInput = document.querySelector('swp-search-input input') as HTMLInputElement;
|
||||||
|
if (!searchInput) return;
|
||||||
|
|
||||||
|
// Build service index from DOM
|
||||||
|
this.buildServiceIndex();
|
||||||
|
|
||||||
|
// Initialize Fuse.js
|
||||||
|
this.fuse = new Fuse(this.services, {
|
||||||
|
keys: ['name'],
|
||||||
|
threshold: 0.3,
|
||||||
|
minMatchCharLength: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Listen for input
|
||||||
|
searchInput.addEventListener('input', (e) => {
|
||||||
|
const query = (e.target as HTMLInputElement).value.trim();
|
||||||
|
this.filterServices(query);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildServiceIndex(): void {
|
||||||
|
const serviceRows = document.querySelectorAll('swp-card.services-list swp-data-table-row');
|
||||||
|
|
||||||
|
serviceRows.forEach((row) => {
|
||||||
|
const element = row as HTMLElement;
|
||||||
|
const id = element.getAttribute('data-service-detail') || '';
|
||||||
|
const nameCell = element.querySelector('swp-data-table-cell:first-child');
|
||||||
|
const name = nameCell?.textContent?.trim() || '';
|
||||||
|
|
||||||
|
// Find the category row for this service
|
||||||
|
let categoryRow: HTMLElement | null = null;
|
||||||
|
let sibling = element.previousElementSibling;
|
||||||
|
while (sibling) {
|
||||||
|
if (sibling.tagName.toLowerCase() === 'swp-category-row') {
|
||||||
|
categoryRow = sibling as HTMLElement;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sibling = sibling.previousElementSibling;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (categoryRow) {
|
||||||
|
this.services.push({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
element,
|
||||||
|
categoryRow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private filterServices(query: string): void {
|
||||||
|
if (!query || query.length < 2) {
|
||||||
|
// Show all services and categories
|
||||||
|
this.showAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.fuse) return;
|
||||||
|
|
||||||
|
// Get matching services
|
||||||
|
const results = this.fuse.search(query);
|
||||||
|
const matchingIds = new Set(results.map(r => r.item.id));
|
||||||
|
|
||||||
|
// Track which categories have visible services
|
||||||
|
const visibleCategories = new Set<HTMLElement>();
|
||||||
|
|
||||||
|
// Show/hide services
|
||||||
|
this.services.forEach(service => {
|
||||||
|
if (matchingIds.has(service.id)) {
|
||||||
|
service.element.style.display = 'grid';
|
||||||
|
visibleCategories.add(service.categoryRow);
|
||||||
|
} else {
|
||||||
|
service.element.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide categories based on whether they have visible services
|
||||||
|
const allCategoryRows = document.querySelectorAll('swp-card.services-list swp-category-row');
|
||||||
|
allCategoryRows.forEach((row) => {
|
||||||
|
const categoryRow = row as HTMLElement;
|
||||||
|
if (visibleCategories.has(categoryRow)) {
|
||||||
|
categoryRow.style.display = 'grid';
|
||||||
|
// Ensure category is expanded when filtering
|
||||||
|
categoryRow.setAttribute('data-expanded', 'true');
|
||||||
|
} else {
|
||||||
|
categoryRow.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private showAll(): void {
|
||||||
|
// Show all services
|
||||||
|
this.services.forEach(service => {
|
||||||
|
service.element.style.display = 'grid';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show all categories
|
||||||
|
const allCategoryRows = document.querySelectorAll('swp-card.services-list swp-category-row');
|
||||||
|
allCategoryRows.forEach((row) => {
|
||||||
|
(row as HTMLElement).style.display = 'grid';
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupCategoryToggle(): void {
|
private setupCategoryToggle(): void {
|
||||||
|
|
@ -18,7 +136,6 @@ export class ServicesController {
|
||||||
if (!categoryRow) return;
|
if (!categoryRow) return;
|
||||||
|
|
||||||
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
|
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
|
||||||
const categoryId = categoryRow.getAttribute('data-category');
|
|
||||||
|
|
||||||
// Find all service rows belonging to this category
|
// Find all service rows belonging to this category
|
||||||
const serviceRows = this.getServiceRowsForCategory(categoryRow);
|
const serviceRows = this.getServiceRowsForCategory(categoryRow);
|
||||||
|
|
@ -77,7 +194,7 @@ export class ServicesController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private expandRows(rows: HTMLElement[]): void {
|
private expandRows(rows: HTMLElement[]): void {
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row) => {
|
||||||
// First make visible but with 0 height
|
// First make visible but with 0 height
|
||||||
row.style.display = 'grid';
|
row.style.display = 'grid';
|
||||||
row.style.height = '0';
|
row.style.height = '0';
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue