src/directives/sq-tooltip/sq-tooltip.directive.ts
Angular directive for creating and managing tooltips.
This directive allows you to easily add tooltips to elements in your Angular application. Tooltips can display additional information when hovering or clicking on an element.
See Official Tooltip Documentation
Example :<!-- Basic usage -->
<div [tooltip]="'Tooltip message'" placement="center top"></div>
OnInit
OnDestroy
| Selector | [tooltip] |
| Standalone | true |
Properties |
|
Methods |
Inputs |
HostListeners |
constructor()
|
|
Initializes the SqTooltipDirective. Binds the hide function to the current instance, assigns the document object, and initializes the window reference. |
| delay | |
Type : number
|
|
Default value : 0
|
|
|
The delay in milliseconds before showing the tooltip. |
|
| placement | |
Type : string
|
|
Default value : 'center top'
|
|
|
The placement of the tooltip relative to the host element. Possible values: 'left top', 'left center', 'left bottom', 'center top', 'center center', 'center bottom', 'right top', 'right center', 'right bottom'. |
|
| theme | |
Type : "light" | "dark"
|
|
Default value : 'dark'
|
|
|
The theme of the tooltip. Possible values: 'light' or 'dark'. |
|
| tooltip | |
Type : string | null | TemplateRef<any>
|
|
Default value : ''
|
|
|
The content of the tooltip. Can be a string message or an Angular TemplateRef. |
|
| trigger | |
Type : "hover" | "click"
|
|
Default value : 'hover'
|
|
|
The trigger for displaying the tooltip. Possible values: 'hover' or 'click'. |
|
| click |
|
Event listener for the 'click' event to toggle the tooltip on click. |
| mouseenter |
|
Event listener for the 'mouseenter' event to show the tooltip on hover. |
| mouseleave |
|
Event listener for the 'mouseleave' event to hide the tooltip on hover. |
| create |
create()
|
|
Creates the tooltip element and appends it to the DOM.
Returns :
void
|
| hide |
hide()
|
|
Hides the tooltip with a delay to allow for animations, and performs cleanup.
Returns :
void
|
| isTouch |
isTouch()
|
|
Checks if the device has touch support.
Returns :
boolean
|
| ngOnDestroy |
ngOnDestroy()
|
|
Cleans up the directive when it is destroyed, ensuring the tooltip is hidden.
Returns :
void
|
| ngOnInit |
ngOnInit()
|
|
Initializes the directive and subscribes to router events to hide the tooltip on navigation.
Returns :
void
|
| onClick |
onClick()
|
Decorators :
@HostListener('click')
|
|
Event listener for the 'click' event to toggle the tooltip on click.
Returns :
void
|
| onMouseEnter |
onMouseEnter()
|
Decorators :
@HostListener('mouseenter')
|
|
Event listener for the 'mouseenter' event to show the tooltip on hover.
Returns :
void
|
| onMouseLeave |
onMouseLeave()
|
Decorators :
@HostListener('mouseleave')
|
|
Event listener for the 'mouseleave' event to hide the tooltip on hover.
Returns :
void
|
| setPosition |
setPosition()
|
|
Sets the position of the tooltip relative to the host element.
Returns :
void
|
| Async show |
show()
|
|
Shows the tooltip and sets its position.
Returns :
any
|
| document |
Type : Document
|
|
Reference to the Document object for interacting with the DOM. |
| Private documentImported |
Default value : inject(DOCUMENT)
|
|
The injected Document object for DOM manipulation. |
| Private el |
Default value : inject(ElementRef)
|
|
The ElementRef of the host element. |
| Public getWindow |
Default value : inject(GetWindow)
|
|
The GetWindow service for accessing the window object. |
| offset |
Type : number
|
Default value : 10
|
|
The offset between the tooltip and the host element. |
| open |
Default value : false
|
|
Indicates whether the tooltip is open or closed. Used for internal control. |
| Private renderer |
Default value : inject(Renderer2)
|
|
The Renderer2 for DOM manipulation. |
| Private router |
Default value : inject(Router)
|
|
The Angular Router service. |
| tooltipElement |
Type : HTMLElement | null
|
Default value : null
|
|
Reference to the generated tooltip element. |
| Private viewContainerRef |
Default value : inject(ViewContainerRef)
|
|
The ViewContainerRef for the tooltip. |
| window |
Type : | null
|
Default value : null
|
|
Reference to the window object. |
import { DOCUMENT } from '@angular/common';
import {
Directive,
ElementRef,
HostListener,
inject,
Input,
OnDestroy,
OnInit,
Renderer2,
TemplateRef,
ViewContainerRef,
} from '@angular/core';
import { NavigationEnd, Router } from '@angular/router';
import { sleep } from '../../helpers/sleep.helper';
import { GetWindow } from '../../helpers/window.helper';
/**
* Angular directive for creating and managing tooltips.
*
* This directive allows you to easily add tooltips to elements in your Angular application.
* Tooltips can display additional information when hovering or clicking on an element.
*
* @see {@link https://css.squidit.com.br/components/tooltip|Official Tooltip Documentation}
*
* @example
* <!-- Basic usage -->
* <div [tooltip]="'Tooltip message'" placement="center top"></div>
*/
@Directive({
selector: '[tooltip]',
standalone: true,
})
export class SqTooltipDirective implements OnInit, OnDestroy {
/**
* The content of the tooltip. Can be a string message or an Angular TemplateRef.
*/
@Input('tooltip') content?: string | null | TemplateRef<any> = '';
/**
* The placement of the tooltip relative to the host element.
* Possible values: 'left top', 'left center', 'left bottom', 'center top',
* 'center center', 'center bottom', 'right top', 'right center', 'right bottom'.
*/
@Input() placement = 'center top';
/**
* The delay in milliseconds before showing the tooltip.
*/
@Input() delay = 0;
/**
* The theme of the tooltip. Possible values: 'light' or 'dark'.
*/
@Input() theme: 'light' | 'dark' = 'dark';
/**
* The trigger for displaying the tooltip. Possible values: 'hover' or 'click'.
*/
@Input() trigger: 'hover' | 'click' = 'hover';
/**
* Reference to the generated tooltip element.
*/
tooltipElement: HTMLElement | null = null;
/**
* The offset between the tooltip and the host element.
*/
offset = 10;
/**
* Reference to the window object.
*/
window: (Window & typeof globalThis) | null = null;
/**
* Indicates whether the tooltip is open or closed. Used for internal control.
*/
open = false;
/**
* Reference to the Document object for interacting with the DOM.
*/
document: Document;
/**
* The ElementRef of the host element.
*/
private el = inject(ElementRef);
/**
* The Renderer2 for DOM manipulation.
*/
private renderer = inject(Renderer2);
/**
* The Angular Router service.
*/
private router = inject(Router);
/**
* The ViewContainerRef for the tooltip.
*/
private viewContainerRef = inject(ViewContainerRef);
/**
* The injected Document object for DOM manipulation.
*/
private documentImported = inject(DOCUMENT);
/**
* The GetWindow service for accessing the window object.
*/
public getWindow = inject(GetWindow);
/**
* Initializes the SqTooltipDirective.
* Binds the hide function to the current instance, assigns the document object, and initializes the window reference.
*/
constructor() {
// Bind the hide function to the current instance.
this.hide = this.hide.bind(this);
// Assign the document object for DOM manipulation.
this.document = this.documentImported || document;
// Initialize window reference
this.window = this.getWindow.window();
}
/**
* Event listener for the 'mouseenter' event to show the tooltip on hover.
*/
@HostListener('mouseenter') onMouseEnter() {
if (!this.isTouch() && !this.tooltipElement && this.trigger === 'hover') {
this.show();
}
}
/**
* Event listener for the 'mouseleave' event to hide the tooltip on hover.
*/
@HostListener('mouseleave') onMouseLeave() {
if (this.tooltipElement && this.trigger === 'hover' && !this.isTouch()) {
this.hide();
}
}
/**
* Event listener for the 'click' event to toggle the tooltip on click.
*/
@HostListener('click') onClick() {
if (this.tooltipElement && (this.trigger === 'click' || this.isTouch())) {
this.hide();
}
if (!this.tooltipElement && (this.trigger === 'click' || this.isTouch())) {
this.show();
}
}
/**
* Initializes the directive and subscribes to router events to hide the tooltip on navigation.
*/
ngOnInit() {
this.router.events.subscribe((val: unknown) => {
if (val instanceof NavigationEnd) {
this.hide();
}
});
}
/**
* Cleans up the directive when it is destroyed, ensuring the tooltip is hidden.
*/
ngOnDestroy() {
this.hide();
}
/**
* Checks if the device has touch support.
*
* @returns {boolean} - True if the device supports touch events; otherwise, false.
*/
isTouch(): boolean {
const window = this.getWindow.window();
if (window) {
const maxTouchPoints = navigator.maxTouchPoints & 0xff;
return 'ontouchstart' in window || maxTouchPoints > 0;
}
return false;
}
/**
* Shows the tooltip and sets its position.
*/
async show() {
this.create();
this.setPosition();
this.document?.addEventListener('click', this.hide, true);
if (this.tooltipElement) {
this.renderer.addClass(this.tooltipElement, 'tooltip-show');
await sleep(500); // Wait for animations.
this.open = true;
}
}
/**
* Hides the tooltip with a delay to allow for animations, and performs cleanup.
*/
hide() {
if (
(this.tooltipElement && (this.isTouch() || this.trigger === 'click') && this.open) ||
(!this.isTouch() && this.trigger === 'hover')
) {
try {
this.renderer.removeClass(this.tooltipElement, 'tooltip-show');
} catch (e) {
// Ignore error
}
this.getWindow?.window()?.setTimeout(() => {
try {
this.renderer.removeChild(this.document.body, this.tooltipElement);
} catch (e) {
// Ignore error
}
this.open = false;
this.document.removeEventListener('click', this.hide, true);
this.tooltipElement = null;
}, this.delay);
}
}
/**
* Creates the tooltip element and appends it to the DOM.
*/
create() {
if (this.content) {
const arrow: HTMLElement = this.renderer.createElement('div');
this.renderer.addClass(arrow, 'tooltip-arrow');
if (this.isTouch()) {
this.renderer.addClass(arrow, 'tooltip-not-arrow');
}
this.tooltipElement = this.renderer.createElement('div');
if (this.tooltipElement) {
if (this.content instanceof TemplateRef) {
for (const node of this.viewContainerRef.createEmbeddedView(this.content).rootNodes) {
this.renderer.appendChild(this.tooltipElement, node);
}
}
if (typeof this.content === 'string') {
this.tooltipElement.innerHTML = this.content;
}
}
this.renderer.addClass(this.tooltipElement, 'tooltip');
this.renderer.addClass(this.tooltipElement, 'tooltip-generated');
this.renderer.addClass(this.tooltipElement, `tooltip-${this.theme}`);
this.renderer.addClass(this.tooltipElement, `tooltip-${this.placement.replace(' ', '-')}`);
this.renderer.addClass(this.tooltipElement, 'text-left');
this.renderer.setStyle(this.tooltipElement, '-webkit-transition', `opacity ${this.delay}ms`);
this.renderer.setStyle(this.tooltipElement, '-moz-transition', `opacity ${this.delay}ms`);
this.renderer.setStyle(this.tooltipElement, '-o-transition', `opacity ${this.delay}ms`);
this.renderer.setStyle(this.tooltipElement, 'transition', `opacity ${this.delay}ms`);
this.renderer.appendChild(this.tooltipElement, arrow);
this.renderer.appendChild(this.document.body, this.tooltipElement);
}
}
/**
* Sets the position of the tooltip relative to the host element.
*/
setPosition() {
if (this.tooltipElement) {
const parentCoords = this.el.nativeElement.getBoundingClientRect();
const tooltipCoords = this.tooltipElement.getBoundingClientRect();
const posHorizontal = this.placement.split(' ')[0] || 'center';
const posVertical = this.placement.split(' ')[1] || 'bottom';
const distance = 7;
let top;
let left;
switch (posHorizontal) {
case 'left':
left = parseInt(parentCoords.left) - distance - tooltipCoords.width;
if (parseInt(parentCoords.left) - tooltipCoords.width < 0) {
left = distance;
}
break;
case 'right':
left = parentCoords.right + distance;
if (parseInt(parentCoords.right) + tooltipCoords.width > document.documentElement.clientWidth) {
left = document.documentElement.clientWidth - tooltipCoords.width - distance;
}
break;
default:
case 'center':
left = parseInt(parentCoords.left) - tooltipCoords.width / 2 + parentCoords.width / 2;
}
switch (posVertical) {
case 'center':
top = (parseInt(parentCoords.top) + parseInt(parentCoords.bottom)) / 2 - this.tooltipElement.offsetHeight / 2;
break;
case 'bottom':
top = parseInt(parentCoords.bottom) + distance;
break;
default:
case 'top':
top = parseInt(parentCoords.top) - this.tooltipElement.offsetHeight - distance;
}
this.renderer.setStyle(this.tooltipElement, 'left', `${left < 0 ? parseInt(parentCoords.left) : left}px`);
const scrollY = this.getWindow?.window()?.scrollY ?? 0;
this.renderer.setStyle(
this.tooltipElement,
'top',
`${(top < 0 ? parseInt(parentCoords.bottom) + distance : top) + scrollY}px`
);
}
}
}