@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.
328 lines (327 loc) • 12.9 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Directive, ElementRef, Input, isDevMode, NgZone, Renderer2 } from "@angular/core";
import { PopupService } from "@progress/kendo-angular-popup";
import { closest, hasObservers, isDocumentAvailable, Keys } from "@progress/kendo-angular-common";
import { ERRORS } from '../constants';
import { PopoverHideEvent, PopoverShowEvent, PopoverShownEvent, PopoverHiddenEvent } from "../models/events";
import { align, containsItem } from "../utils";
import { PopoverComponent } from "./popover.component";
import { Subscription } from "rxjs";
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-popup";
const validShowOptions = ['hover', 'click', 'none', 'focus'];
/**
* @hidden
*/
export class PopoverDirectivesBase {
ngZone;
popupService;
renderer;
/**
* Specifies the popover instance that will be rendered.
* Accepts a [`PopoverComponent`]({% slug api_tooltip_popovercomponent %}) instance or
* a [`PopoverFn`]({% slug api_tooltip_popoverfn %}) callback which returns a [`PopoverComponent`]({% slug api_tooltip_popovercomponent %}) instance
* depending on the current anchor element.
*
* [See example](slug:templates_popover#toc-passing-data-to-templates)
*/
set popover(value) {
if (value instanceof PopoverComponent || typeof value === `function`) {
this._popover = value;
}
else {
if (isDevMode) {
throw new Error(ERRORS.popover);
}
}
}
get popover() {
return this._popover;
}
/**
* The event on which the Popover will be shown
*
* The supported values are:
* - `click` (default) —The Popover will be shown when its `anchor` element is clicked.
* - `hover`—The Popover will be shown when its `anchor` element is hovered.
* - `focus`—The Popover will be shown when its `anchor` element is focused.
* - `none`—The Popover will not be shown on user interaction. It could be rendered via the Popover API methods.
*/
set showOn(value) {
if (isDevMode && !containsItem(validShowOptions, value)) {
throw new Error(ERRORS.showOn);
}
this._showOn = value;
}
get showOn() {
return this._showOn;
}
/**
* @hidden
*/
anchor = null;
popupRef;
disposeHoverOverListener;
disposeHoverOutListener;
disposeClickListener;
disposePopupHoverOutListener;
disposePopupHoverInListener;
disposePopupFocusOutListener;
subs = new Subscription();
_popoverService;
_hideSub;
_focusInsideSub;
_popover;
_showOn = 'click';
_popupOpenSub;
_popupCloseSub;
_popupSubs;
constructor(ngZone, popupService, renderer) {
this.ngZone = ngZone;
this.popupService = popupService;
this.renderer = renderer;
}
ngAfterViewInit() {
if (!isDocumentAvailable()) {
return;
}
this.manageEvents();
}
ngOnDestroy() {
this.closePopup();
this.subs.unsubscribe();
this._popupSubs && this._popupSubs.unsubscribe();
if (this.disposeHoverOverListener) {
this.disposeHoverOverListener();
}
if (this.disposeHoverOutListener) {
this.disposeHoverOutListener();
}
if (this.disposeClickListener) {
this.disposeClickListener();
}
if (this._focusInsideSub) {
this._focusInsideSub.unsubscribe();
}
if (this._hideSub) {
this._hideSub.unsubscribe();
}
if (this._popupOpenSub) {
this._popupOpenSub.unsubscribe();
}
if (this._popupCloseSub) {
this._popupCloseSub.unsubscribe();
}
}
/**
* Hides the Popover ([See example]({% slug programmaticcontrol_popover %})).
*/
hide() {
this.closePopup();
}
/**
* @hidden
*/
closePopup() {
if (this.popupRef) {
if (this.anchor) {
this.renderer.removeAttribute(this.anchor, 'aria-describedby');
}
this.popupRef.close();
this.popupRef = null;
if (this.disposePopupHoverOutListener) {
this.disposePopupHoverOutListener();
}
if (this.disposePopupHoverInListener) {
this.disposePopupHoverInListener();
}
if (this.disposePopupFocusOutListener) {
this.disposePopupFocusOutListener();
}
this._popupSubs.unsubscribe();
}
}
/**
* @hidden
*/
openPopup(anchor) {
this.anchor = anchor instanceof ElementRef ? anchor.nativeElement : anchor;
const popoverComp = this.popover instanceof PopoverComponent ? this.popover : this.popover(this.anchor);
const alignSettings = align(popoverComp.position, popoverComp.offset);
const anchorAlign = alignSettings.anchorAlign;
const popupAlign = alignSettings.popupAlign;
const popupMargin = alignSettings.popupMargin;
const _animation = popoverComp.animation;
this.popupRef = this.popupService.open({
anchor: { nativeElement: this.anchor },
animate: _animation,
content: PopoverComponent,
popupAlign,
anchorAlign,
margin: popupMargin,
collision: { horizontal: 'fit', vertical: 'fit' }
});
const popupInstance = this.popupRef.content.instance;
this._popupSubs = new Subscription();
if (anchor) {
this._popupSubs.add(this.renderer.listen(this.anchor, 'keydown', event => this.onKeyDown(event)));
this.renderer.setAttribute(this.anchor, 'aria-describedby', popupInstance.popoverId);
}
this._popupSubs.add(popupInstance.closeOnKeyDown.subscribe(() => {
this.anchor.focus();
this.hide();
}));
this.applySettings(this.popupRef.content, popoverComp);
this.monitorPopup();
this.initializeCompletionEvents(popoverComp, this.anchor);
}
/**
* @hidden
*/
isPrevented(anchorElement, show) {
const popoverComp = this.popover instanceof PopoverComponent ? this.popover : this.popover(anchorElement);
let eventArgs;
// eslint-disable-next-line prefer-const
eventArgs = this.initializeEvents(popoverComp, eventArgs, show, anchorElement);
return eventArgs.isDefaultPrevented();
}
/**
* @hidden
*/
monitorPopup() {
if (this.showOn === 'hover') {
this.ngZone.runOutsideAngular(() => {
const popup = this.popupRef.popupElement;
this.disposePopupHoverInListener = this.renderer.listen(popup, 'mouseenter', _ => {
this.ngZone.run(_ => this._popoverService.emitPopoverState(true));
});
this.disposePopupHoverOutListener = this.renderer.listen(popup, 'mouseleave', _ => {
this.ngZone.run(_ => this._popoverService.emitPopoverState(false));
});
});
}
if (this.showOn === 'focus') {
this.ngZone.runOutsideAngular(() => {
const popup = this.popupRef.popupElement;
this.disposePopupFocusOutListener = this.renderer.listen(popup, 'focusout', (e) => {
const isInsidePopover = closest(e.relatedTarget, (node) => node.classList && node.classList.contains('k-popover'));
if (!isInsidePopover) {
this.ngZone.run(_ => this._popoverService.emitFocusInsidePopover(false));
}
});
});
}
}
applySettings(contentComponent, popover) {
const content = contentComponent.instance;
content.visible = true;
content.anchor = this.anchor;
content.position = popover.position;
content.offset = popover.offset;
content.width = popover.width;
content.height = popover.height;
content.title = popover.title;
content.body = popover.body;
content.callout = popover.callout;
content.animation = popover.animation;
content.contextData = popover.templateData(this.anchor);
content.titleTemplate = popover.titleTemplate;
content.bodyTemplate = popover.bodyTemplate;
content.actionsTemplate = popover.actionsTemplate;
this.popupRef.content.changeDetectorRef.detectChanges();
}
manageEvents() {
this.ngZone.runOutsideAngular(() => {
switch (this.showOn) {
case 'hover':
this.subscribeToShowEvents([{
name: 'mouseenter', handler: this.mouseenterHandler
}, {
name: 'mouseleave', handler: this.mouseleaveHandler
}]);
break;
case 'focus':
this.subscribeToShowEvents([{
name: 'focus', handler: this.focusHandler
}, {
name: 'blur', handler: this.blurHandler
}]);
break;
case 'click':
this.subscribeClick();
break;
default:
break;
}
});
}
/**
* @hidden
*/
initializeEvents(popoverComp, eventArgs, show, anchorElement) {
if (show) {
eventArgs = new PopoverShowEvent(anchorElement);
if (this.shouldEmitEvent(!!this.popupRef, 'show', popoverComp)) {
this.ngZone.run(() => popoverComp.show.emit(eventArgs));
}
}
else {
eventArgs = new PopoverHideEvent(anchorElement, this.popupRef);
if (this.shouldEmitEvent(!!this.popupRef, 'hide', popoverComp)) {
this.ngZone.run(() => popoverComp.hide.emit(eventArgs));
}
}
return eventArgs;
}
onKeyDown(event) {
const keyCode = event.keyCode;
if (keyCode === Keys.Escape) {
this.hide();
}
}
initializeCompletionEvents(popoverComp, _anchor) {
if (this.shouldEmitCompletionEvents('shown', popoverComp)) {
this.popupRef.popupOpen.subscribe(() => {
const eventArgs = new PopoverShownEvent(_anchor, this.popupRef);
popoverComp.shown.emit(eventArgs);
});
}
if (this.shouldEmitCompletionEvents('hidden', popoverComp)) {
this.popupRef.popupClose.subscribe(() => {
this.ngZone.run(_ => {
const eventArgs = new PopoverHiddenEvent(_anchor);
popoverComp.hidden.emit(eventArgs);
});
});
}
}
shouldEmitEvent(hasPopup, event, popoverComp) {
if ((event === 'show' && !hasPopup && hasObservers(popoverComp[event]))
|| (event === 'hide' && hasPopup && hasObservers(popoverComp[event]))) {
return true;
}
return false;
}
shouldEmitCompletionEvents(event, popoverComp) {
if ((hasObservers(popoverComp[event]) && !this._popupOpenSub)
|| (hasObservers(popoverComp[event]) && !this._popupCloseSub)) {
return true;
}
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverDirectivesBase, deps: [{ token: i0.NgZone }, { token: i1.PopupService }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverDirectivesBase, inputs: { popover: "popover", showOn: "showOn" }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverDirectivesBase, decorators: [{
type: Directive,
args: [{}]
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i1.PopupService }, { type: i0.Renderer2 }]; }, propDecorators: { popover: [{
type: Input
}], showOn: [{
type: Input
}] } });