import { Directive, ElementRef, HostListener, Input, OnDestroy } from "@angular/core"; import { ConnectedPosition, Overlay, OverlayPositionBuilder, OverlayRef } from "@angular/cdk/overlay"; import { ComponentPortal } from "@angular/cdk/portal"; import { TooltipComponent } from "./tooltip.component"; import { Utils } from "@sinequa/core/base"; import { Observable, of, Subscription } from "rxjs"; import { delay } from "rxjs/operators"; @Directive({selector: "[sqTooltip]"}) export class TooltipDirective<T> implements OnDestroy { @Input("sqTooltip") text?: string | ((data?: T) => Observable<string|undefined>) = ""; @Input("sqTooltipData") data?: T; @Input() placement: "top" | "bottom" | "right" | "left" = "bottom"; @Input() delay = 300; private overlayRef: OverlayRef; private subscription?: Subscription; private clearTimeout?: any; constructor( private overlay: Overlay, private overlayPositionBuilder: OverlayPositionBuilder, private elementRef: ElementRef ) {} ngOnDestroy() { // do not forget to clear timeout function this.clearSubscription(); } @HostListener("mouseenter", ['$event']) show(event: MouseEvent) { event.preventDefault(); event.stopPropagation(); // The tooltip is already showing: just cancel the hiding if(this.clearTimeout) { clearTimeout(this.clearTimeout); return; } this.clearSubscription(); if(!this.text) return; let obs: Observable<string|undefined>; if(Utils.isFunction(this.text)) { obs = this.text(this.data); } else { obs = of(this.text) .pipe(delay(this.delay)) } this.subscription = obs.subscribe(text => { this.overlayRef?.detach(); if(!text?.trim().length) return; const positionStrategy = this.overlayPositionBuilder .flexibleConnectedTo(this.elementRef) .withPositions([this.position()]); const scrollStrategy = this.overlay.scrollStrategies.close(); this.overlayRef = this.overlay.create({positionStrategy, scrollStrategy}); const tooltipRef = this.overlayRef.attach(new ComponentPortal(TooltipComponent)); tooltipRef.instance.text = text; }); } @HostListener("mousedown", ['$event']) mouseClick(event: MouseEvent) { event.preventDefault() event.stopPropagation(); this.clearSubscription(); } @HostListener("mouseleave", ['$event']) hide(event: MouseEvent) { if(!this.clearTimeout) { this.clearTimeout = setTimeout(() => this.clearSubscription(), 10); } } position(): ConnectedPosition { switch (this.placement) { case "bottom": return { originX: "center", originY: "bottom", overlayX: "center", overlayY: "top", offsetY: 8 }; case "right": return { originX: "end", originY: "center", overlayX: "start", overlayY: "center", offsetX: 8 }; case "left": return { originX: "start", originY: "center", overlayX: "end", overlayY: "center", offsetX: -8 }; default: return { originX: "center", originY: "top", overlayX: "center", overlayY: "bottom", offsetY: -8 }; } } /** * Clear timeout function and detach overlayRef */ private clearSubscription() { this.subscription?.unsubscribe(); if(this.clearTimeout) { clearTimeout(this.clearTimeout); this.clearTimeout = undefined; } this.overlayRef?.detach(); } }