112 lines
3 KiB
TypeScript
112 lines
3 KiB
TypeScript
|
|
/**
|
||
|
|
* Services Controller
|
||
|
|
* Handles category collapse/expand animations
|
||
|
|
*/
|
||
|
|
|
||
|
|
export class ServicesController {
|
||
|
|
constructor() {
|
||
|
|
this.init();
|
||
|
|
}
|
||
|
|
|
||
|
|
private init(): void {
|
||
|
|
this.setupCategoryToggle();
|
||
|
|
}
|
||
|
|
|
||
|
|
private setupCategoryToggle(): void {
|
||
|
|
document.addEventListener('click', (e) => {
|
||
|
|
const categoryRow = (e.target as HTMLElement).closest('swp-category-row');
|
||
|
|
if (!categoryRow) return;
|
||
|
|
|
||
|
|
const isExpanded = categoryRow.getAttribute('data-expanded') !== 'false';
|
||
|
|
const categoryId = categoryRow.getAttribute('data-category');
|
||
|
|
|
||
|
|
// Find all service rows belonging to this category
|
||
|
|
const serviceRows = this.getServiceRowsForCategory(categoryRow);
|
||
|
|
|
||
|
|
if (isExpanded) {
|
||
|
|
// Collapse - set attribute immediately so chevron animates with rows
|
||
|
|
categoryRow.setAttribute('data-expanded', 'false');
|
||
|
|
this.collapseRows(serviceRows);
|
||
|
|
} else {
|
||
|
|
// Expand - set attribute immediately so chevron animates with rows
|
||
|
|
categoryRow.setAttribute('data-expanded', 'true');
|
||
|
|
this.expandRows(serviceRows);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private getServiceRowsForCategory(categoryRow: Element): HTMLElement[] {
|
||
|
|
const rows: HTMLElement[] = [];
|
||
|
|
let sibling = categoryRow.nextElementSibling;
|
||
|
|
|
||
|
|
while (sibling && sibling.tagName.toLowerCase() === 'swp-data-table-row') {
|
||
|
|
rows.push(sibling as HTMLElement);
|
||
|
|
sibling = sibling.nextElementSibling;
|
||
|
|
}
|
||
|
|
|
||
|
|
return rows;
|
||
|
|
}
|
||
|
|
|
||
|
|
private collapseRows(rows: HTMLElement[]): void {
|
||
|
|
if (rows.length === 0) return;
|
||
|
|
|
||
|
|
// Animate each row
|
||
|
|
rows.forEach((row) => {
|
||
|
|
const height = row.offsetHeight;
|
||
|
|
row.style.height = `${height}px`;
|
||
|
|
row.style.overflow = 'hidden';
|
||
|
|
|
||
|
|
// Force reflow
|
||
|
|
row.offsetHeight;
|
||
|
|
|
||
|
|
row.style.transition = 'height 0.2s ease, opacity 0.2s ease';
|
||
|
|
row.style.height = '0';
|
||
|
|
row.style.opacity = '0';
|
||
|
|
});
|
||
|
|
|
||
|
|
// After animation completes
|
||
|
|
setTimeout(() => {
|
||
|
|
rows.forEach(row => {
|
||
|
|
row.style.display = 'none';
|
||
|
|
row.style.height = '';
|
||
|
|
row.style.opacity = '';
|
||
|
|
row.style.overflow = '';
|
||
|
|
row.style.transition = '';
|
||
|
|
});
|
||
|
|
}, 200);
|
||
|
|
}
|
||
|
|
|
||
|
|
private expandRows(rows: HTMLElement[]): void {
|
||
|
|
rows.forEach((row, index) => {
|
||
|
|
// First make visible but with 0 height
|
||
|
|
row.style.display = 'grid';
|
||
|
|
row.style.height = '0';
|
||
|
|
row.style.opacity = '0';
|
||
|
|
row.style.overflow = 'hidden';
|
||
|
|
|
||
|
|
// Measure natural height
|
||
|
|
row.style.height = 'auto';
|
||
|
|
const naturalHeight = row.offsetHeight;
|
||
|
|
row.style.height = '0';
|
||
|
|
|
||
|
|
// Force reflow
|
||
|
|
row.offsetHeight;
|
||
|
|
|
||
|
|
// Animate to natural height
|
||
|
|
row.style.transition = 'height 0.2s ease, opacity 0.2s ease';
|
||
|
|
row.style.height = `${naturalHeight}px`;
|
||
|
|
row.style.opacity = '1';
|
||
|
|
});
|
||
|
|
|
||
|
|
// Cleanup after animation
|
||
|
|
setTimeout(() => {
|
||
|
|
rows.forEach(row => {
|
||
|
|
row.style.height = '';
|
||
|
|
row.style.opacity = '';
|
||
|
|
row.style.overflow = '';
|
||
|
|
row.style.transition = '';
|
||
|
|
});
|
||
|
|
}, 200);
|
||
|
|
}
|
||
|
|
}
|