/** * Accordion Controller * * Generic accordion component with smooth expand/collapse animations. * Supports single-open behavior (only one item expanded at a time). */ export interface AccordionOptions { /** Only allow one item to be expanded at a time (default: true) */ singleOpen?: boolean; /** Animation duration for expand in ms (default: 250) */ expandDuration?: number; /** Animation duration for collapse in ms (default: 200) */ collapseDuration?: number; } export class Accordion { private container: HTMLElement; private singleOpen: boolean; private expandDuration: number; private collapseDuration: number; constructor(selector: string | HTMLElement, options: AccordionOptions = {}) { // Get container element if (typeof selector === 'string') { const el = document.querySelector(selector); if (!el) { console.warn(`Accordion: Element not found for selector "${selector}"`); return; } this.container = el; } else { this.container = selector; } // Set options with defaults this.singleOpen = options.singleOpen ?? true; this.expandDuration = options.expandDuration ?? 250; this.collapseDuration = options.collapseDuration ?? 200; this.setupEventListeners(); } /** * Setup click listeners on accordion headers */ private setupEventListeners(): void { const headers = this.container.querySelectorAll('swp-accordion-header'); headers.forEach(header => { header.addEventListener('click', (e) => { // Don't toggle if clicking on interactive elements const target = e.target as HTMLElement; if (target.closest('input, button, a, select')) return; const item = header.closest('swp-accordion-item'); if (item) { this.toggle(item); } }); }); } /** * Toggle an accordion item */ toggle(item: HTMLElement): void { const isExpanded = item.classList.contains('expanded'); if (isExpanded) { this.collapse(item); } else { // Close other items first if single-open mode if (this.singleOpen) { this.container.querySelectorAll('swp-accordion-item.expanded').forEach(otherItem => { if (otherItem !== item) { this.collapse(otherItem); } }); } this.expand(item); } } /** * Expand an accordion item with animation */ expand(item: HTMLElement): void { const content = item.querySelector('swp-accordion-content'); const toggle = item.querySelector('swp-accordion-toggle'); if (!content) return; // Add expanded class immediately for CSS to show content item.classList.add('expanded'); // Animate toggle icon rotation toggle?.animate([ { transform: 'rotate(0deg)' }, { transform: 'rotate(180deg)' } ], { duration: this.expandDuration, easing: 'ease-out', fill: 'forwards' }); // Animate content height const height = content.scrollHeight; content.animate([ { height: '0px', opacity: 0 }, { height: `${height}px`, opacity: 1 } ], { duration: this.expandDuration, easing: 'ease-out', fill: 'forwards' }); } /** * Collapse an accordion item with animation */ collapse(item: HTMLElement): void { const content = item.querySelector('swp-accordion-content'); const toggle = item.querySelector('swp-accordion-toggle'); if (!content) return; // Animate toggle icon rotation toggle?.animate([ { transform: 'rotate(180deg)' }, { transform: 'rotate(0deg)' } ], { duration: this.collapseDuration, easing: 'ease-out', fill: 'forwards' }); // Animate content height const height = content.scrollHeight; const animation = content.animate([ { height: `${height}px`, opacity: 1 }, { height: '0px', opacity: 0 } ], { duration: this.collapseDuration, easing: 'ease-out', fill: 'forwards' }); // Remove expanded class after animation completes animation.onfinish = () => { item.classList.remove('expanded'); }; } /** * Expand all items (only useful when singleOpen is false) */ expandAll(): void { this.container.querySelectorAll('swp-accordion-item:not(.expanded)').forEach(item => { this.expand(item); }); } /** * Collapse all items */ collapseAll(): void { this.container.querySelectorAll('swp-accordion-item.expanded').forEach(item => { this.collapse(item); }); } /** * Get all expanded items */ getExpanded(): HTMLElement[] { return Array.from(this.container.querySelectorAll('swp-accordion-item.expanded')); } } /** * Initialize all accordions on the page */ export function initAccordions(options: AccordionOptions = {}): Accordion[] { const accordions: Accordion[] = []; document.querySelectorAll('swp-accordion').forEach(container => { accordions.push(new Accordion(container, options)); }); return accordions; }