import { AfterViewInit, Component, ElementRef, HostBinding, Input, OnDestroy, ViewChild, PLATFORM_ID, inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common'; import { createPopper, Instance as PopperInstance } from '@popperjs/core'; @Component({ selector: 'app-dropdown', template: `
`, standalone: true, }) export class DropdownComponent implements AfterViewInit, OnDestroy { @ViewChild('targetEl') targetEl!: ElementRef; @Input() triggerEl!: HTMLElement; @Input() placement: any = 'bottom'; @Input() triggerType: 'click' | 'hover' = 'click'; @Input() offsetSkidding: number = 0; @Input() offsetDistance: number = 10; @Input() delay: number = 300; @Input() ignoreClickOutsideClass: string | false = false; @HostBinding('class.hidden') isHidden: boolean = true; private platformId = inject(PLATFORM_ID); private isBrowser = isPlatformBrowser(this.platformId); private popperInstance: PopperInstance | null = null; isVisible: boolean = false; private clickOutsideListener: any; private hoverShowListener: any; private hoverHideListener: any; ngAfterViewInit() { if (!this.isBrowser) return; if (!this.triggerEl) { console.error('Trigger element is not provided to the dropdown component.'); return; } this.initializePopper(); this.setupEventListeners(); } ngOnDestroy() { this.destroyPopper(); this.removeEventListeners(); } private initializePopper() { this.popperInstance = createPopper(this.triggerEl, this.targetEl.nativeElement, { placement: this.placement, modifiers: [ { name: 'offset', options: { offset: [this.offsetSkidding, this.offsetDistance], }, }, ], }); } private setupEventListeners() { if (!this.isBrowser) return; if (this.triggerType === 'click') { this.triggerEl.addEventListener('click', () => this.toggle()); } else if (this.triggerType === 'hover') { this.hoverShowListener = () => this.show(); this.hoverHideListener = () => this.hide(); this.triggerEl.addEventListener('mouseenter', this.hoverShowListener); this.triggerEl.addEventListener('mouseleave', this.hoverHideListener); this.targetEl.nativeElement.addEventListener('mouseenter', this.hoverShowListener); this.targetEl.nativeElement.addEventListener('mouseleave', this.hoverHideListener); } this.clickOutsideListener = (event: MouseEvent) => this.handleClickOutside(event); document.addEventListener('click', this.clickOutsideListener); } private removeEventListeners() { if (!this.isBrowser) return; if (this.triggerType === 'click') { this.triggerEl.removeEventListener('click', () => this.toggle()); } else if (this.triggerType === 'hover') { this.triggerEl.removeEventListener('mouseenter', this.hoverShowListener); this.triggerEl.removeEventListener('mouseleave', this.hoverHideListener); this.targetEl.nativeElement.removeEventListener('mouseenter', this.hoverShowListener); this.targetEl.nativeElement.removeEventListener('mouseleave', this.hoverHideListener); } document.removeEventListener('click', this.clickOutsideListener); } toggle() { this.isVisible ? this.hide() : this.show(); } show() { this.isVisible = true; this.isHidden = false; this.targetEl.nativeElement.classList.remove('hidden'); this.popperInstance?.update(); } hide() { this.isVisible = false; this.isHidden = true; this.targetEl.nativeElement.classList.add('hidden'); } private handleClickOutside(event: MouseEvent) { if (!this.isVisible || !this.isBrowser) return; const clickedElement = event.target as HTMLElement; if (this.ignoreClickOutsideClass) { const ignoredElements = document.querySelectorAll(`.${this.ignoreClickOutsideClass}`); const arr = Array.from(ignoredElements); for (const el of arr) { if (el.contains(clickedElement)) return; } } if (!this.targetEl.nativeElement.contains(clickedElement) && !this.triggerEl.contains(clickedElement)) { this.hide(); } } private destroyPopper() { if (this.popperInstance) { this.popperInstance.destroy(); this.popperInstance = null; } } }