UNPKG

@progress/kendo-angular-tooltip

Version:

Kendo UI Tooltip for Angular - A highly customizable and easily themeable tooltip from the creators developers trust for professional Angular components.

491 lines (490 loc) 18.8 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Directive, Input, TemplateRef, Optional, ElementRef, NgZone, Inject, isDevMode, Renderer2 } from '@angular/core'; import { take, filter } from 'rxjs/operators'; import { fromEvent, Subscription } from 'rxjs'; import { PopupService } from '@progress/kendo-angular-popup'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { TooltipSettings, TOOLTIP_SETTINGS } from './tooltip.settings'; import { TooltipContentComponent } from '../tooltip/tooltip.content.component'; import { align, closestBySelector, contains, containsItem, collision, hasParent } from '../utils'; import { isDocumentAvailable, Keys } from '@progress/kendo-angular-common'; import * as i0 from "@angular/core"; import * as i1 from "@progress/kendo-angular-popup"; import * as i2 from "./tooltip.settings"; /** * Represents the [Kendo UI Tooltip directive for Angular]({% slug overview_tooltip %}). * Displays additional information related to an element. * * @example * ```html * <div kendoTooltip> * <button kendoButton title="Save">Save</button> * </div> * ``` */ export class TooltipDirective { tooltipWrapper; ngZone; renderer; popupService; /** * Specifies a selector for elements within a container that display a tooltip * ([see example]({% slug anchorelements_tooltip %})). The possible values include any * DOM `selector`. * @default '[title]' */ filter = '[title]'; /** * Specifies the position of the Tooltip relative to the * anchor element ([see example]({% slug positioning_tooltip %})). * * @default 'top' */ position = 'top'; /** * Sets the template for the tooltip header title. [See example]({% slug anchorelements_tooltip %}). */ titleTemplate; /** * Specifies the mouse action that triggers the Tooltip to show. [See example]({% slug programmaticopening_tooltip %}). */ showOn; /** * Specifies the delay in milliseconds before the Tooltip is shown. * * @default 100 */ showAfter = 100; /** * Determines if the Tooltip displays a callout arrow. * * @default true */ callout = true; /** * Determines if the Tooltip displays a **Close** button. [See example]({% slug closable_tooltip %}). * * @default false */ closable = false; /** * Specifies the offset in pixels between the Tooltip and the anchor. * If the `callout` property is set to `true`, the offset is rendered from the callout arrow. * If the `callout` property is set to `false`, the offset is rendered from the content of the Tooltip. * * @default 6 */ offset = 6; /** * Sets the width of the Tooltip. [See example]({% slug anchorelements_tooltip %}). */ tooltipWidth; /** * Sets the height of the Tooltip. */ tooltipHeight; /** * Sets a CSS class for the Tooltip. */ tooltipClass; /** * @hidden * Specifies a CSS class that will be added to the kendo-tooltip element. */ tooltipContentClass; /** * Provides screen boundary detection when the Тooltip is shown. */ collision; /** * Sets the title of the **Close** button. */ closeTitle; /** * Sets a custom content template for the Tooltip. * The template is rendered inside the Tooltip content area. [See example]({% slug templates_tooltip %}). */ set tooltipTemplate(value) { this.template = value; } get tooltipTemplate() { return this.template; } popupRef; template; showTimeout; anchor = null; mouseOverSubscription; mouseOutSubscription; mouseClickSubscription; anchorTitleSubscription; popupPositionChangeSubscription; popupMouseOutSubscription; keyboardNavigationSubscription = new Subscription(); closeClickSubscription; validPositions = ['top', 'bottom', 'right', 'left']; validShowOptions = ['hover', 'click', 'none']; constructor(tooltipWrapper, ngZone, renderer, popupService, settings, legacySettings) { this.tooltipWrapper = tooltipWrapper; this.ngZone = ngZone; this.renderer = renderer; this.popupService = popupService; validatePackage(packageMetadata); Object.assign(this, settings, legacySettings); this.ngZone.runOutsideAngular(() => { const wrapper = this.tooltipWrapper.nativeElement; this.anchorTitleSubscription = fromEvent(wrapper, 'mouseover') .pipe(filter(() => this.filter !== '')) .subscribe((e) => { const filterElement = closestBySelector(e.target, this.filter); if (filterElement) { this.hideElementTitle({ nativeElement: filterElement }); } }); this.mouseOverSubscription = fromEvent(wrapper, 'mouseover') .pipe(filter(() => this.filter !== '')) .subscribe(e => this.onMouseOver(e)); this.mouseOutSubscription = fromEvent(wrapper, 'mouseout') .subscribe(e => this.onMouseOut(e)); }); } /** * Shows the Tooltip. * @param anchor - The element used as an anchor. The Tooltip opens relative to this element. */ show(anchor) { if (this.popupRef) { return; } if (anchor instanceof Element) { anchor = { nativeElement: anchor }; } this.anchor = anchor; if (this.showOn === 'hover') { if (this.popupRef) { return; } clearTimeout(this.showTimeout); this.showTimeout = setTimeout(() => this.showContent(this.anchor), this.showAfter); } else { this.hideElementTitle(this.anchor); this.showContent(this.anchor); } } /** * Hides the Tooltip. */ hide() { clearTimeout(this.showTimeout); const anchor = this.anchor && this.anchor.nativeElement; if (anchor && anchor.getAttribute('data-title')) { if (!anchor.getAttribute('title') && anchor.hasAttribute('title')) { anchor.setAttribute('title', anchor.getAttribute('data-title')); } anchor.setAttribute('data-title', ''); } if (this.popupMouseOutSubscription) { this.popupMouseOutSubscription.unsubscribe(); } if (this.closeClickSubscription) { this.closeClickSubscription.unsubscribe(); } this.closePopup(); } /** * Toggles the visibility of the Tooltip. * @param anchor - The element used as an anchor. * @param show - Optional. Boolean. Specifies if the Tooltip is rendered. */ toggle(anchor, show) { const previousAnchor = this.anchor && this.anchor.nativeElement; if (anchor instanceof Element) { anchor = { nativeElement: anchor }; } if (previousAnchor !== anchor.nativeElement) { this.hide(); } if (previousAnchor === anchor.nativeElement && this.showOn === 'click') { this.hide(); } if (typeof show === 'undefined') { show = !this.popupRef; } if (show) { this.show(anchor); } else { this.hide(); } } ngOnInit() { if (this.showOn === undefined) { this.showOn = 'hover'; } this.keyboardNavigationSubscription.add(this.renderer.listen(this.tooltipWrapper.nativeElement, 'keydown', event => this.onKeyDown(event))); this.verifyProperties(); } ngOnChanges(changes) { if (changes.showOn && isDocumentAvailable()) { this.subscribeClick(); } } ngAfterViewChecked() { if (!this.popupRef) { return; } if (this.anchor && !hasParent(this.anchor.nativeElement || this.anchor, this.tooltipWrapper.nativeElement)) { this.anchor = null; this.hide(); } } ngOnDestroy() { this.hide(); this.template = null; this.anchorTitleSubscription.unsubscribe(); this.mouseOverSubscription.unsubscribe(); this.mouseOutSubscription.unsubscribe(); this.keyboardNavigationSubscription.unsubscribe(); if (this.mouseClickSubscription) { this.mouseClickSubscription.unsubscribe(); } if (this.popupPositionChangeSubscription) { this.popupPositionChangeSubscription.unsubscribe(); } if (this.popupMouseOutSubscription) { this.popupMouseOutSubscription.unsubscribe(); } } showContent(anchorRef) { if (!anchorRef.nativeElement.getAttribute('data-title') && !this.template) { return; } this.ngZone.run(() => { this.openPopup(anchorRef); this.bindContent(this.popupRef.content, anchorRef); }); this.popupRef.popupAnchorViewportLeave .pipe(take(1)) .subscribe(() => this.hide()); } bindContent(contentComponent, anchorRef) { const content = contentComponent.instance; this.closeClickSubscription = content.close .subscribe(() => { this.hide(); }); if (!this.template) { content.templateString = this.anchor.nativeElement.getAttribute('data-title'); } else { content.templateRef = this.template; } if (this.titleTemplate) { content.titleTemplate = this.titleTemplate; } content.closeTitle = this.closeTitle; content.anchor = anchorRef; content.callout = this.callout; content.closable = this.closable; content.position = this.position; content.tooltipWidth = this.tooltipWidth; content.tooltipHeight = this.tooltipHeight; this.popupRef.content.changeDetectorRef.detectChanges(); } hideElementTitle(elementRef) { const element = elementRef.nativeElement; if (element.getAttribute('title')) { element.setAttribute('data-title', element.getAttribute('title')); element.setAttribute('title', ''); } } openPopup(anchorRef) { const alignSettings = align(this.position, this.offset); const anchorAlign = alignSettings.anchorAlign; const popupAlign = alignSettings.popupAlign; const popupMargin = alignSettings.popupMargin; this.popupRef = this.popupService.open({ anchor: anchorRef, anchorAlign, animate: false, content: TooltipContentComponent, collision: collision(this.collision, this.position), margin: popupMargin, popupAlign, popupClass: 'k-popup-transparent' }); if (this.tooltipClass) { this.renderer.addClass(this.popupRef.popupElement, this.tooltipClass); } if (this.tooltipContentClass) { this.renderer.addClass(this.popupRef.content.instance['content'].nativeElement, this.tooltipContentClass); } const popupInstance = this.popupRef.content.instance; if (anchorRef) { this.renderer.setAttribute(anchorRef.nativeElement, 'aria-labelledby', popupInstance.tooltipId); } if (popupInstance.callout) { this.popupPositionChangeSubscription = this.popupRef.popupPositionChange .subscribe(({ flip }) => { const isFlip = flip.horizontal === true || flip.vertical === true; popupInstance.updateCalloutPosition(this.position, isFlip); }); } if (this.showOn === 'hover') { this.ngZone.runOutsideAngular(() => { const popup = this.popupRef.popupElement; this.popupMouseOutSubscription = fromEvent(popup, 'mouseout') .subscribe((e) => this.onMouseOut(e)); }); } } closePopup() { if (this.popupRef) { if (this.anchor) { this.renderer.removeAttribute(this.anchor.nativeElement, 'aria-labelledby'); } this.popupRef.close(); this.popupRef = null; } if (this.popupPositionChangeSubscription) { this.popupPositionChangeSubscription.unsubscribe(); } } subscribeClick() { if (this.mouseClickSubscription) { this.mouseClickSubscription.unsubscribe(); } if (this.showOn === 'click') { this.mouseClickSubscription = fromEvent(document, 'click') .pipe(filter(() => this.filter !== '')) .subscribe(e => this.onMouseClick(e, this.tooltipWrapper.nativeElement)); } } onMouseClick(e, wrapper) { const target = e.target; const filterElement = closestBySelector(target, this.filter); const popup = this.popupRef && this.popupRef.popupElement; if (popup) { if (popup.contains(target)) { return; } if (this.closable) { return; } } if (wrapper.contains(target) && filterElement) { this.toggle(filterElement, true); } else if (popup) { this.hide(); } } onKeyDown(event) { const keyCode = event.keyCode; const target = event.target; if (this.popupRef) { const tooltipId = this.popupRef.content.location.nativeElement.getAttribute('id'); const anchorLabelledBy = target.getAttribute('aria-labelledby'); if (keyCode === Keys.Escape && this.canCloseTooltip(target, tooltipId, anchorLabelledBy)) { this.closePopup(); } } } canCloseTooltip(target, tooltipId, anchorLabelledBy) { const isIdEqualsLabel = tooltipId === anchorLabelledBy; const filterElement = closestBySelector(target, this.filter); const isTargetFocused = target === document.activeElement; const isTargetInsideWrapper = this.tooltipWrapper.nativeElement.contains(target); return isTargetInsideWrapper && filterElement && isTargetFocused && isIdEqualsLabel; } onMouseOver(e) { const filterElement = closestBySelector(e.target, this.filter); if (this.showOn !== 'hover') { return; } if (filterElement) { this.toggle(filterElement, true); } } onMouseOut(e) { if (this.showOn !== 'hover') { return; } if (this.closable) { return; } const popup = this.popupRef && this.popupRef.popupElement; const relatedTarget = e.relatedTarget; if (relatedTarget && this.anchor && contains(this.anchor.nativeElement, relatedTarget)) { return; } if (relatedTarget && contains(popup, relatedTarget)) { return; } this.hide(); } verifyProperties() { if (!isDevMode()) { return; } if (!containsItem(this.validPositions, this.position)) { throw new Error(`Invalid value provided for position property.The available options are 'top', 'bottom', 'left', or 'right'.`); } if (!containsItem(this.validShowOptions, this.showOn)) { throw new Error(`Invalid value provided for showOn property.The available options are 'hover' or 'none'.`); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TooltipDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.PopupService }, { token: i2.TooltipSettings, optional: true }, { token: TOOLTIP_SETTINGS, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: TooltipDirective, isStandalone: true, selector: "[kendoTooltip]", inputs: { filter: "filter", position: "position", titleTemplate: "titleTemplate", showOn: "showOn", showAfter: "showAfter", callout: "callout", closable: "closable", offset: "offset", tooltipWidth: "tooltipWidth", tooltipHeight: "tooltipHeight", tooltipClass: "tooltipClass", tooltipContentClass: "tooltipContentClass", collision: "collision", closeTitle: "closeTitle", tooltipTemplate: "tooltipTemplate" }, exportAs: ["kendoTooltip"], usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TooltipDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTooltip]', exportAs: 'kendoTooltip', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.PopupService }, { type: i2.TooltipSettings, decorators: [{ type: Optional }] }, { type: i2.TooltipSettings, decorators: [{ type: Optional }, { type: Inject, args: [TOOLTIP_SETTINGS] }] }]; }, propDecorators: { filter: [{ type: Input }], position: [{ type: Input }], titleTemplate: [{ type: Input }], showOn: [{ type: Input }], showAfter: [{ type: Input }], callout: [{ type: Input }], closable: [{ type: Input }], offset: [{ type: Input }], tooltipWidth: [{ type: Input }], tooltipHeight: [{ type: Input }], tooltipClass: [{ type: Input }], tooltipContentClass: [{ type: Input }], collision: [{ type: Input }], closeTitle: [{ type: Input }], tooltipTemplate: [{ type: Input }] } });