import { ComponentRef, Directive, ElementRef, HostListener, inject, Input, OnDestroy, TemplateRef, ViewContainerRef } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { TooltipContentComponent } from '../tooltip-content.component'; @Directive({ selector: '[kitTooltip]', exportAs: 'kitTooltip' }) export class TooltipDirective implements OnDestroy { @Input() kitTooltip = ''; @Input() tooltipDescription = ''; @Input() tooltipTemplate?: TemplateRef; @Input() tooltipPosition: 'top' | 'bottom' | 'left' | 'right' = 'top'; @Input() tooltipOffset = 8; @Input() tooltipDelay = 100; private tooltipRef?: ComponentRef; private showTimeoutId?: number; private hideTimeoutId?: number; private viewContainerRef = inject(ViewContainerRef); private document = inject(DOCUMENT); constructor(private elementRef: ElementRef) {} @HostListener('mouseenter') onMouseEnter(): void { this.clearTimeouts(); this.showTimeoutId = window.setTimeout(() => { this.show(); }, this.tooltipDelay); } @HostListener('mouseleave') onMouseLeave(): void { this.clearTimeouts(); if (this.tooltipRef) { this.hideTimeoutId = window.setTimeout(() => { this.hide(); }, 100); } } private show(): void { if (this.tooltipRef) { return; } // Create tooltip component this.tooltipRef = this.viewContainerRef.createComponent(TooltipContentComponent); // Set inputs this.tooltipRef.instance.title = this.kitTooltip; this.tooltipRef.instance.description = this.tooltipDescription; this.tooltipRef.instance.templateRef = this.tooltipTemplate; this.tooltipRef.instance.positionType = this.tooltipPosition; this.tooltipRef.instance.offset = this.tooltipOffset; // Append to body to avoid clipping issues this.document.body.appendChild(this.tooltipRef.location.nativeElement); // Calculate position and show const rect = this.elementRef.nativeElement.getBoundingClientRect(); let x = rect.left + rect.width / 2; let y = 0; switch (this.tooltipPosition) { case 'top': y = rect.top; break; case 'bottom': y = rect.bottom; break; case 'left': case 'right': y = rect.top + rect.height / 2; break; } if (this.tooltipPosition === 'left') { x = rect.left; } else if (this.tooltipPosition === 'right') { x = rect.right; } // Give the browser a chance to render before calculating position setTimeout(() => { this.tooltipRef?.instance.show(x, y, this.tooltipPosition); }); } private hide(): void { if (this.tooltipRef) { this.tooltipRef.instance.hide(); setTimeout(() => { this.tooltipRef?.destroy(); this.tooltipRef = undefined; }, 150); } } private clearTimeouts(): void { if (this.showTimeoutId) { clearTimeout(this.showTimeoutId); this.showTimeoutId = undefined; } if (this.hideTimeoutId) { clearTimeout(this.hideTimeoutId); this.hideTimeoutId = undefined; } } ngOnDestroy(): void { this.clearTimeouts(); if (this.tooltipRef) { this.tooltipRef.destroy(); } } }