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.

505 lines (504 loc) 19.3 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 %}). * Used to display additional information that is related to an element. * * @example * ```ts-no-run * <div kendoTooltip> * <a title="Tooltip title" href="foo">foo</a> * </div> * ``` */ export class TooltipDirective { tooltipWrapper; ngZone; renderer; popupService; /** * Specifies a selector for elements within a container which will display a tooltip * ([see example]({% slug anchorelements_tooltip %})). The possible values include any * DOM `selector`. The default value is `[title]`. */ filter = '[title]'; /** * Specifies the position of the Tooltip that is relative to the * anchor element ([see example]({% slug positioning_tooltip %})). * * The possible values are: * * `top` (default) * * `bottom` * * `left` * * `right` */ position = 'top'; /** * Renders the passed template as a header title of the Tooltip * ([see example]({% slug anchorelements_tooltip %})). */ titleTemplate; /** * Specifies when the Тooltip will be rendered * ([see example]({% slug programmaticopening_tooltip %})). * * The possible values are: * * `hover` (default) * * `click` * * `none` */ showOn; /** * Specifies the delay in milliseconds before the Tooltip is shown. * * `100` (default) milliseconds. */ showAfter = 100; /** * Specifies if the Тooltip will display a callout arrow. * * The possible values are: * * `true` (default) * * `false` */ callout = true; /** * Specifies if the Тooltip will display a **Close** button * ([see example]({% slug closable_tooltip %})). * * The possible values are: * * `true` * * `false` */ closable = false; /** * Specifies the offset in pixels between the Tooltip and the anchor. Defaults to `6` pixels. * 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. */ offset = 6; /** * Specifies the width of the Тooltip ([see example]({% slug anchorelements_tooltip %})). */ tooltipWidth; /** * Specifies the height of the Тooltip. */ tooltipHeight; /** * Specifies a CSS class that will be added to 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; /** * Specifies the title of the close button. */ closeTitle; /** * Sets the content of the Tooltip as a template reference * ([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&mdash; ElementRef|Element. * Specifies the element that will be used as an anchor. The Tooltip opens relative to that 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(); } /** * Toggle visibility of the Tooltip. * * @param anchor&mdash; ElementRef|Element. Specifies the element that will be used as an anchor. * @param show&mdash; Optional. Boolean. Specifies if the Tooltip will be 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 }] } });