import { CommonModule } from '@angular/common'; import { AfterViewInit, Component, ElementRef, Input, OnChanges, OnDestroy, Renderer2, SimpleChanges } from '@angular/core'; @Component({ selector: 'app-tooltip', standalone: true, imports: [CommonModule], templateUrl: './tooltip.component.html', }) export class TooltipComponent implements AfterViewInit, OnDestroy, OnChanges { @Input() id: string; @Input() text: string; @Input() isVisible: boolean = false; @Input() position: 'top' | 'right' | 'bottom' | 'left' = 'top'; private tooltipElement: HTMLElement | null = null; private arrowElement: HTMLElement | null = null; private resizeObserver: ResizeObserver | null = null; private parentElement: HTMLElement | null = null; constructor(private elementRef: ElementRef, private renderer: Renderer2) {} ngAfterViewInit() { this.tooltipElement = document.getElementById(this.id); this.parentElement = this.elementRef.nativeElement.parentElement; if (this.tooltipElement && this.parentElement) { // Create arrow element this.arrowElement = this.renderer.createElement('div'); this.renderer.addClass(this.arrowElement, 'tooltip-arrow'); this.renderer.appendChild(this.tooltipElement, this.arrowElement); // Setup resize observer this.setupResizeObserver(); // Initial positioning this.updatePosition(); // Initial visibility this.updateTooltipVisibility(); } } ngOnChanges(changes: SimpleChanges) { if (changes['isVisible'] && this.tooltipElement) { this.updateTooltipVisibility(); } if ((changes['position'] || changes['isVisible']) && this.isVisible) { setTimeout(() => this.updatePosition(), 0); } } ngOnDestroy() { if (this.resizeObserver) { this.resizeObserver.disconnect(); } } private setupResizeObserver() { if (window.ResizeObserver && this.parentElement) { this.resizeObserver = new ResizeObserver(() => { if (this.isVisible) { this.updatePosition(); } }); this.resizeObserver.observe(this.parentElement); if (this.tooltipElement) { this.resizeObserver.observe(this.tooltipElement); } } } private updateTooltipVisibility() { if (!this.tooltipElement) return; if (this.isVisible) { this.renderer.removeClass(this.tooltipElement, 'invisible'); this.renderer.removeClass(this.tooltipElement, 'opacity-0'); this.renderer.addClass(this.tooltipElement, 'visible'); this.renderer.addClass(this.tooltipElement, 'opacity-100'); this.updatePosition(); } else { this.renderer.removeClass(this.tooltipElement, 'visible'); this.renderer.removeClass(this.tooltipElement, 'opacity-100'); this.renderer.addClass(this.tooltipElement, 'invisible'); this.renderer.addClass(this.tooltipElement, 'opacity-0'); } } private updatePosition() { if (!this.tooltipElement || !this.parentElement || !this.arrowElement) return; const parentRect = this.parentElement.getBoundingClientRect(); const tooltipRect = this.tooltipElement.getBoundingClientRect(); // Reset any previous positioning this.renderer.removeStyle(this.tooltipElement, 'top'); this.renderer.removeStyle(this.tooltipElement, 'right'); this.renderer.removeStyle(this.tooltipElement, 'bottom'); this.renderer.removeStyle(this.tooltipElement, 'left'); // Reset arrow classes this.renderer.removeClass(this.arrowElement, 'arrow-top'); this.renderer.removeClass(this.arrowElement, 'arrow-right'); this.renderer.removeClass(this.arrowElement, 'arrow-bottom'); this.renderer.removeClass(this.arrowElement, 'arrow-left'); let top: number = 0; let left: number = 0; switch (this.position) { case 'top': top = -tooltipRect.height - 8; left = (parentRect.width - tooltipRect.width) / 2; this.renderer.addClass(this.arrowElement, 'arrow-bottom'); break; case 'right': top = (parentRect.height - tooltipRect.height) / 2; left = parentRect.width + 8; this.renderer.addClass(this.arrowElement, 'arrow-left'); break; case 'bottom': top = parentRect.height + 8; left = (parentRect.width - tooltipRect.width) / 2; this.renderer.addClass(this.arrowElement, 'arrow-top'); break; case 'left': top = (parentRect.height - tooltipRect.height) / 2; left = -tooltipRect.width - 8; this.renderer.addClass(this.arrowElement, 'arrow-right'); break; } // Apply positioning this.renderer.setStyle(this.tooltipElement, 'top', `${top}px`); this.renderer.setStyle(this.tooltipElement, 'left', `${left}px`); // Make sure the tooltip stays within viewport this.adjustToViewport(); } private adjustToViewport() { if (!this.tooltipElement) return; const tooltipRect = this.tooltipElement.getBoundingClientRect(); const viewportWidth = window.innerWidth; const viewportHeight = window.innerHeight; // Adjust horizontal position if (tooltipRect.left < 0) { this.renderer.setStyle(this.tooltipElement, 'left', '0px'); } else if (tooltipRect.right > viewportWidth) { const newLeft = Math.max(0, viewportWidth - tooltipRect.width); this.renderer.setStyle(this.tooltipElement, 'left', `${newLeft}px`); } // Adjust vertical position if (tooltipRect.top < 0) { this.renderer.setStyle(this.tooltipElement, 'top', '0px'); } else if (tooltipRect.bottom > viewportHeight) { const newTop = Math.max(0, viewportHeight - tooltipRect.height); this.renderer.setStyle(this.tooltipElement, 'top', `${newTop}px`); } } }