168 lines
5.6 KiB
TypeScript
168 lines
5.6 KiB
TypeScript
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`);
|
|
}
|
|
}
|
|
}
|