/** * KTUI - Free & Open-Source Tailwind UI Components by Keenthemes * Copyright 2025 by Keenthemes Inc */ import KTData from '../../helpers/data'; import KTDom from '../../helpers/dom'; import KTComponent from '../component'; import { KTTooltipInterface, KTTooltipConfigInterface } from './types'; import { createPopper, PositioningStrategy, Instance, Placement, } from '@popperjs/core'; import { KTTooltipTriggerType } from './types'; declare global { interface Window { KT_TOOLTIP_INITIALIZED: boolean; KTTooltip: typeof KTTooltip; } } export class KTTooltip extends KTComponent implements KTTooltipInterface { protected override _name: string = 'tooltip'; protected override _defaultConfig: KTTooltipConfigInterface = { target: '', hiddenClass: 'hidden', trigger: 'hover', placement: 'top', placementRtl: 'top', container: '', strategy: 'fixed', offset: '0, 5px', offsetRtl: '0, 5px', delayShow: 0, delayHide: 0, permanent: false, zindex: '100', }; protected override _config: KTTooltipConfigInterface = this._defaultConfig; protected _isOpen: boolean = false; protected _targetElement: HTMLElement; protected _popper!: Instance; protected _transitioning: boolean = false; protected _timeout!: ReturnType; constructor( element: HTMLElement, config: KTTooltipConfigInterface | null = null, ) { super(); if (KTData.has(element as HTMLElement, this._name)) return; this._init(element); this._buildConfig(config); this._targetElement = this._getTargetElement(); if (!this._targetElement) { return; } this._handlers(); } private _getTargetElement(): HTMLElement | null { return ( KTDom.getElement( this._element.getAttribute('data-kt-tooltip') as string, ) || this._element.querySelector('[data-kt-tooltip-content]') || KTDom.getElement(this._getOption('target') as string) ); } protected _handlers(): void { if (!this._element) return; if (this._getOption('trigger') === 'click') { this._element.addEventListener('click', () => this._toggle()); } if (this._getOption('trigger') === 'focus') { this._element.addEventListener('focus', () => this._toggle()); this._element.addEventListener('blur', () => this._hide()); } if (this._getOption('trigger') === 'hover') { this._element.addEventListener('mouseenter', () => this._show()); this._element.addEventListener('mouseleave', () => this._hide()); } } protected _show(): void { if (this._timeout) { clearTimeout(this._timeout); } if (this._isOpen) return; this._timeout = setTimeout( () => { const payload = { cancel: false }; this._fireEvent('show', payload); this._dispatchEvent('show', payload); if (payload.cancel === true) { return; } if (!this._targetElement) { return; } if (!this._element) return; this._createPopper(); this._handleContainer(); this._setZindex(); this._targetElement.classList.add('show'); this._targetElement.classList.remove( this._getOption('hiddenClass') as string, ); this._targetElement.style.opacity = '0'; KTDom.reflow(this._targetElement); this._targetElement.style.opacity = '1'; this._transitioning = true; this._isOpen = true; KTDom.transitionEnd(this._targetElement, () => { this._targetElement.style.opacity = ''; this._transitioning = false; this._fireEvent('shown'); this._dispatchEvent('shown'); }); }, this._getOption('delayShow') as number, ); } protected _hide(): void { if (this._timeout) { clearTimeout(this._timeout); } if (!this._isOpen) return; this._timeout = setTimeout( () => { const payload = { cancel: false }; this._fireEvent('hide', payload); this._dispatchEvent('hide', payload); if (payload.cancel === true) { return; } if (!this._targetElement) { return; } this._targetElement.style.opacity = '1'; KTDom.reflow(this._targetElement); this._targetElement.style.opacity = '0'; this._transitioning = true; this._isOpen = false; KTDom.transitionEnd(this._targetElement, () => { this._popper.destroy(); this._targetElement.classList.remove('show'); this._targetElement.classList.add( this._getOption('hiddenClass') as string, ); this._targetElement.style.opacity = ''; this._transitioning = false; this._fireEvent('hidden'); this._dispatchEvent('hidden'); }); }, this._getOption('delayHide') as number, ); } protected _toggle(): void { const payload = { cancel: false }; this._fireEvent('toggle', payload); this._dispatchEvent('toggle', payload); if (payload.cancel === true) { return; } if (this._isOpen) { this._hide(); } else { this._show(); } } protected _createPopper(): void { if (!this._element) return; const isRtl = KTDom.isRTL(); // Placement let placement = this._getOption('placement') as Placement; if (isRtl && this._getOption('placementRtl')) { placement = this._getOption('placementRtl') as Placement; } // Offset let offsetValue = this._getOption('offset'); if (isRtl && this._getOption('offsetRtl')) { offsetValue = this._getOption('offsetRtl') as Placement; } const offset = offsetValue ? offsetValue .toString() .split(',') .map((value) => parseInt(value.trim(), 10)) : [0, 0]; if (!this._targetElement) { return; } this._popper = createPopper(this._element, this._targetElement, { placement: placement, strategy: this._getOption('strategy') as PositioningStrategy, modifiers: [ { name: 'offset', options: { offset, }, }, ], }); } protected _handleContainer(): void { if (this._getOption('container')) { if (this._getOption('container') === 'body') { document.body.appendChild(this._targetElement); } else { document .querySelector(this._getOption('container') as string) ?.appendChild(this._targetElement); } } } protected _setZindex(): void { let zindex: number = parseInt(this._getOption('zindex') as string); if (parseInt(KTDom.getCssProp(this._element, 'z-index')) > zindex) { zindex = parseInt(KTDom.getCssProp(this._element, 'z-index')); } if (KTDom.getHighestZindex(this._element) > zindex) { zindex = KTDom.getHighestZindex(this._element) + 1; } this._targetElement.style.zIndex = String(zindex); } public show(): void { this._show(); } public hide(): void { this._hide(); } public toggle(): void { this._toggle(); } public getContentElement(): HTMLElement | null { return this._targetElement; } public isOpen(): boolean { return this._isOpen; } public getTriggerOption(): KTTooltipTriggerType { return this._getOption('trigger') as KTTooltipTriggerType; } public isPermanent(): boolean { return this._getOption('permanent') as boolean; } public static initHandlers(): void { document.addEventListener('click', (event: Event) => { document .querySelectorAll('[data-kt-tooltip-initialized]') .forEach((tooltipElement) => { const tooltip = KTTooltip.getInstance(tooltipElement as HTMLElement); if ( tooltip && tooltip.isOpen() && tooltip.getTriggerOption() !== 'hover' && !tooltip.isPermanent() ) { const contentElement = tooltip.getContentElement(); if ( contentElement && (contentElement === event.target || contentElement.contains(event.target as HTMLElement)) ) { return; } else { tooltip.hide(); } } }); }); } public static getInstance(element: HTMLElement): KTTooltip { if (!element) return null; if (KTData.has(element, 'tooltip')) { return KTData.get(element, 'tooltip') as KTTooltip; } if (element.getAttribute('data-kt-tooltip')) { return new KTTooltip(element); } return null; } public static getOrCreateInstance( element: HTMLElement, config?: KTTooltipConfigInterface, ): KTTooltip { return this.getInstance(element) || new KTTooltip(element, config); } public static createInstances(): void { document.querySelectorAll('[data-kt-tooltip]').forEach((element) => { new KTTooltip(element as HTMLElement); }); } public static init(): void { KTTooltip.createInstances(); if (window.KT_TOOLTIP_INITIALIZED !== true) { KTTooltip.initHandlers(); window.KT_TOOLTIP_INITIALIZED = true; } } } if (typeof window !== 'undefined') { window.KTTooltip = KTTooltip; }