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.

328 lines (327 loc) 12.9 kB
/**----------------------------------------------------------------------------------------- * 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 }] } });