@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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— 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— ElementRef|Element. Specifies the element that will be used as an anchor.
* @param show— 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
}] } });