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.

1,342 lines (1,322 loc) 97.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 * as i0 from '@angular/core'; import { forwardRef, Directive, Input, Optional, isDevMode, EventEmitter, Component, HostBinding, Output, ViewChild, ContentChild, ElementRef, Injectable, InjectionToken, Inject, NgModule } from '@angular/core'; import * as i1 from '@progress/kendo-angular-l10n'; import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n'; import { PreventableEvent, focusableSelector, Keys, isDocumentAvailable, closest, hasObservers, ResizeBatchService } from '@progress/kendo-angular-common'; import * as i1$1 from '@progress/kendo-angular-popup'; import { PopupService } from '@progress/kendo-angular-popup'; import { take, auditTime, filter } from 'rxjs/operators'; import { Subscription, BehaviorSubject, Subject, combineLatest, fromEvent } from 'rxjs'; import { validatePackage } from '@progress/kendo-licensing'; import { NgIf, NgStyle, NgClass, NgTemplateOutlet } from '@angular/common'; import { xIcon } from '@progress/kendo-svg-icons'; import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons'; /** * @hidden */ class LocalizedMessagesDirective extends ComponentMessages { service; /** * The title of the close button. */ closeTitle; constructor(service) { super(); this.service = service; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LocalizedMessagesDirective, deps: [{ token: i1.LocalizationService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: LocalizedMessagesDirective, isStandalone: true, selector: "[kendoTooltipLocalizedMessages]", inputs: { closeTitle: "closeTitle" }, providers: [ { provide: ComponentMessages, useExisting: forwardRef(() => LocalizedMessagesDirective) } ], usesInheritance: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: LocalizedMessagesDirective, decorators: [{ type: Directive, args: [{ providers: [ { provide: ComponentMessages, useExisting: forwardRef(() => LocalizedMessagesDirective) } ], selector: `[kendoTooltipLocalizedMessages]`, standalone: true }] }], ctorParameters: function () { return [{ type: i1.LocalizationService }]; }, propDecorators: { closeTitle: [{ type: Input }] } }); /** * @hidden */ const ERRORS = { popover: `Invalid value provided for the 'popover' property. The accepted data types are 'PopoverComponent' or 'PopoverFn'.`, templateData: `templateData must be a function, but received`, showOn: `Invalid value provided for the 'showOn' property. The available options are 'click', 'hover', 'focus' or 'none'.` }; /** * Arguments for the `show` event. The `show` event fires when a popover is about * to be opened. If you cancel the event, the opening is prevented. */ class PopoverShowEvent extends PreventableEvent { /** * The host element related to the popover. */ anchor; /** * @hidden * Constructs the event arguments for the `show` event. * @param anchor - The host element related to the popover. */ constructor(anchor) { super(); this.anchor = anchor; } } /** * Arguments for the `hide` event. The `hide` event fires when a popover is about * to be closed. If you cancel the event, the popover stays open. */ class PopoverHideEvent extends PreventableEvent { /** * The host element related to the popover. */ anchor; /** * The popover element. */ popover; /** * @hidden * Constructs the event arguments for the `hide` event. * @param anchor - The host element related to the popover. * @param popover - The popover element. */ constructor(anchor, popover) { super(); this.anchor = anchor; this.popover = popover; } } /** * Arguments for the `shown` event. The `shown` event fires after the popover has opened and its opening animation has finished. */ class PopoverShownEvent { /** * The host element related to the popover. */ anchor; /** * The popover element. */ popover; /** * @hidden * Constructs the event arguments for the `shown` event. * @param anchor - The host element related to the popover. * @param popover - The popover element. */ constructor(anchor, popover) { this.anchor = anchor; this.popover = popover; } } /** * Arguments for the `hidden` event. The `hidden` event fires after the popover has closed and its closing animation has finished. */ class PopoverHiddenEvent { /** * The host element related to the popover. */ anchor; /** * @hidden * Constructs the event arguments for the `hidden` event. * @param anchor - The host element related to the popover. */ constructor(anchor) { this.anchor = anchor; } } /** * @hidden */ let idx = 0; /** * @hidden */ let popoverTitleIdx = 0; /** * @hidden */ let popoverBodyIdx = 0; /** * @hidden */ const getId = (prefix, idSource) => { switch (idSource) { case 'popoverTitle': return `${prefix}-${++popoverTitleIdx}`; case 'popoverBody': return `${prefix}-${++popoverBodyIdx}`; default: return `${prefix}-${++idx}`; } }; /** * @hidden */ function align(position, offset) { let anchorAlign = {}; let popupAlign = {}; let popupMargin = {}; switch (position) { case 'top': anchorAlign = { horizontal: 'center', vertical: 'top' }; popupAlign = { horizontal: 'center', vertical: 'bottom' }; popupMargin = { horizontal: 0, vertical: offset }; break; case 'bottom': anchorAlign = { horizontal: 'center', vertical: 'bottom' }; popupAlign = { horizontal: 'center', vertical: 'top' }; popupMargin = { horizontal: 0, vertical: offset }; break; case 'right': anchorAlign = { horizontal: 'right', vertical: 'center' }; popupAlign = { horizontal: 'left', vertical: 'center' }; popupMargin = { horizontal: offset, vertical: 0 }; break; case 'left': anchorAlign = { horizontal: 'left', vertical: 'center' }; popupAlign = { horizontal: 'right', vertical: 'center' }; popupMargin = { horizontal: offset, vertical: 0 }; break; default: break; } return { anchorAlign, popupAlign, popupMargin }; } /** * @hidden */ function collision(inputcollision, position) { if (inputcollision) { return inputcollision; } if (position === 'top' || position === 'bottom') { return { horizontal: 'fit', vertical: 'flip' }; } return { horizontal: 'flip', vertical: 'fit' }; } function isDocumentNode(container) { return container.nodeType === 9; } /** * @hidden */ function closestBySelector(element, selector) { if (element.closest) { return element.closest(selector); } const matches = Element.prototype.matches ? (el, sel) => el.matches(sel) : (el, sel) => el.msMatchesSelector(sel); let node = element; while (node && !isDocumentNode(node)) { if (matches(node, selector)) { return node; } node = node.parentNode; } } /** * @hidden */ function contains(container, child) { if (!container) { return false; } if (isDocumentNode(container)) { return false; } if (container.contains) { return container.contains(child); } if (container.compareDocumentPosition) { return !!(container.compareDocumentPosition(child) & Node.DOCUMENT_POSITION_CONTAINED_BY); } } /** * @hidden */ const hasParent = (node, parent) => { while (node && node !== parent) { node = node.parentNode; } return node; }; /** * @hidden */ function getCenterOffset(item, dir, size) { const rect = item.getBoundingClientRect(); return rect[dir] + (rect[size] / 2); } /** * @hidden */ function containsItem(collection, item) { return collection.indexOf(item) !== -1; } /** * @hidden */ function getAllFocusableChildren(parent) { return parent.querySelectorAll(focusableSelector); } /** * @hidden */ function getFirstAndLastFocusable(parent) { const all = getAllFocusableChildren(parent); const firstFocusable = all.length > 0 ? all[0] : parent; const lastFocusable = all.length > 0 ? all[all.length - 1] : parent; return [firstFocusable, lastFocusable]; } /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-tooltip', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1743579699, version: '18.4.0', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/' }; /** * Represents a template that defines the content of the Popover title. * * To define the template, nest an `<ng-template>` tag * with the `kendoPopoverTitleTemplate` directive inside the `<kendo-popover>` tag. */ class PopoverTitleTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverTitleTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverTitleTemplateDirective, isStandalone: true, selector: "[kendoPopoverTitleTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverTitleTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoPopoverTitleTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Represents a template that defines the content of the Popover body. * * To define the template, nest an `<ng-template>` tag * with the `kendoPopoverBodyTemplate` directive inside the `<kendo-popover>` tag. */ class PopoverBodyTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverBodyTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverBodyTemplateDirective, isStandalone: true, selector: "[kendoPopoverBodyTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverBodyTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoPopoverBodyTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /** * Represents a template that defines the content of the Popover actions. * * To define the template, nest an `<ng-template>` tag * with the `kendoPopoverActionsTemplate` directive inside the `<kendo-popover>` tag. */ class PopoverActionsTemplateDirective { templateRef; constructor(templateRef) { this.templateRef = templateRef; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverActionsTemplateDirective, deps: [{ token: i0.TemplateRef, optional: true }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: PopoverActionsTemplateDirective, isStandalone: true, selector: "[kendoPopoverActionsTemplate]", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverActionsTemplateDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoPopoverActionsTemplate]', standalone: true }] }], ctorParameters: function () { return [{ type: i0.TemplateRef, decorators: [{ type: Optional }] }]; } }); /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Represents the [Kendo UI Popover component for Angular]({% slug overview_popover %}). * Used to display additional information that is related to a target element. * * @example * ```ts-no-run * <kendo-popover> * <ng-template kendoPopoverTitleTemplate>Foo Title</ng-template> * <ng-template kendoPopoverBodyTemplate>Foo Body</ng-template> * <ng-template kendoPopoverActionsTemplate>Foo Actions</ng-template> * </kendo-popover> * ``` */ class PopoverComponent { localization; renderer; element; zone; /** * @hidden */ anchor; /** * Specifies the position of the Popover in relation to its anchor element. [See example]({% slug positioning_popover %}) * * The possible options are: * `top` * `bottom` * `right` (Default) * `left` */ position = 'right'; /** * Specifies the distance from the Popover to its anchor element in pixels. * * @default `6` */ set offset(value) { this._offset = value; } get offset() { const calloutBuffer = 14; return this.callout ? calloutBuffer + this._offset : this._offset; } /** * Determines the width of the popover. Numeric values are treated as pixels. * @default 'auto' */ set width(value) { this._width = typeof value === 'number' ? `${value}px` : value; } get width() { return this._width; } /** * Determines the height of the popover. Numeric values are treated as pixels. * @default 'auto' */ set height(value) { this._height = typeof value === 'number' ? `${value}px` : value; } get height() { return this._height; } /** * @hidden */ direction; /** * Specifies the main header text of the Popover. * * If a `titleTemplate` is provided it would take precedence over the title. */ title; /** * @hidden * Specifies the secondary header text of the Popover. * * If a `titleTemplate` is provided it would take precedence over the subtitle. */ subtitle; /** * Represents the text that will be rendered in the Popover body section. * * If a `bodyTemplate` is provided it would take precedence over this text. */ body; /** * Determines whether a callout will be rendered along the Popover. [See example]({% slug callout_popover %}) * * @default true */ callout = true; /** * Enables and configures the Popover animation. [See example]({% slug animations_popover %}) * * The possible options are: * * * `boolean`&mdash;Enables the default animation * * `PopoverAnimation`&mdash;A configuration object which allows setting the `direction`, `duration` and `type` of the animation. * * @default false */ animation = false; /** * Defines a callback function which returns custom data passed to the Popover templates. * It exposes the current anchor element as an argument. [See example](slug:templates_popover#toc-passing-data-to-templates) */ set templateData(fn) { if (isDevMode && typeof fn !== 'function') { throw new Error(`${ERRORS.templateData} ${JSON.stringify(fn)}.`); } this._templateData = fn; } get templateData() { return this._templateData; } /** * @hidden * Determines the visibility of the Popover. */ visible = false; /** * @hidden */ get isHidden() { return !this.visible; } /** * @hidden */ get hasAttributeHidden() { return !this.visible; } /** * Fires before the Popover is about to be shown ([see example]({% slug events_popover %})). * The event is preventable. If canceled, the Popover will not be displayed. [See example]({% slug events_popover %}) */ show = new EventEmitter(); /** * Fires after the Popover has been shown and the animation has ended. [See example]({% slug events_popover %}) */ shown = new EventEmitter(); /** * Fires when the Popover is about to be hidden ([see example]({% slug events_popover %})). * The event is preventable. If canceled, the Popover will remain visible. */ hide = new EventEmitter(); /** * Fires after the Popover has been hidden and the animation has ended. [See example]({% slug events_popover %}) */ hidden = new EventEmitter(); /** * @hidden */ closeOnKeyDown = new EventEmitter(); /** * @hidden */ popoverWrapper; /** * @hidden */ titleTemplateWrapper; /** * @hidden */ bodyTemplateWrapper; /** * @hidden */ titleTemplate; /** * @hidden */ bodyTemplate; /** * @hidden */ actionsTemplate; /** * @hidden */ contextData; /** * @hidden */ _width = 'auto'; /** * @hidden */ _height = 'auto'; /** * @hidden */ popoverId = ''; _offset = 6; subs = new Subscription(); constructor(localization, renderer, element, zone) { this.localization = localization; this.renderer = renderer; this.element = element; this.zone = zone; validatePackage(packageMetadata); } ngOnInit() { this.popoverId = getId('k-popover'); this.subs.add(this.localization.changes.subscribe(({ rtl }) => { this.direction = rtl ? 'rtl' : 'ltr'; })); this.subs.add(this.renderer.listen(this.element.nativeElement, 'keydown', event => this.onKeyDown(event))); } ngAfterViewInit() { this.zone.onStable.pipe(take(1)).subscribe(() => { if (this.visible) { const wrapper = this.popoverWrapper.nativeElement; const focusablePopoverChildren = getAllFocusableChildren(wrapper); if (focusablePopoverChildren.length > 0) { focusablePopoverChildren[0].focus(); } this.setAriaAttributes(wrapper, focusablePopoverChildren); } }); } ngOnDestroy() { this.subs.unsubscribe(); } /** * @hidden */ getCalloutPosition() { switch (this.position) { case 'top': return { 'k-callout-s': true }; case 'bottom': return { 'k-callout-n': true }; case 'left': return { 'k-callout-e': true }; case 'right': return { 'k-callout-w': true }; default: return { 'k-callout-s': true }; } } /** * @hidden */ onKeyDown(event) { const keyCode = event.keyCode; const target = event.target; if (keyCode === Keys.Tab) { this.keepFocusWithinComponent(target, event); } if (keyCode === Keys.Escape) { this.closeOnKeyDown.emit(); } } _templateData = () => null; keepFocusWithinComponent(target, event) { const wrapper = this.popoverWrapper.nativeElement; const [firstFocusable, lastFocusable] = getFirstAndLastFocusable(wrapper); const tabAfterLastFocusable = !event.shiftKey && target === lastFocusable; const shiftTabAfterFirstFocusable = event.shiftKey && target === firstFocusable; if (tabAfterLastFocusable) { event.preventDefault(); firstFocusable.focus(); } if (shiftTabAfterFirstFocusable) { event.preventDefault(); lastFocusable.focus(); } } setAriaAttributes(wrapper, focusablePopoverChildren) { if (this.titleTemplate) { const titleRef = this.titleTemplateWrapper.nativeElement; const focusableHeaderChildren = getAllFocusableChildren(titleRef).length > 0; if (focusableHeaderChildren) { const headerId = getId('k-popover-header', 'popoverTitle'); this.renderer.setAttribute(titleRef, 'id', headerId); this.renderer.setAttribute(wrapper, 'aria-labelledby', headerId); } } if (this.bodyTemplate) { const bodyRef = this.bodyTemplateWrapper.nativeElement; const focusableBodyChildren = getAllFocusableChildren(bodyRef).length > 0; if (focusableBodyChildren) { const bodyId = getId('k-popover-body', 'popoverBody'); this.renderer.setAttribute(bodyRef, 'id', bodyId); this.renderer.setAttribute(wrapper, 'aria-describedby', bodyId); } } this.renderer.setAttribute(wrapper, 'id', this.popoverId); this.renderer.setAttribute(wrapper, 'role', focusablePopoverChildren.length > 0 ? 'dialog' : 'tooltip'); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverComponent, deps: [{ token: i1.LocalizationService }, { token: i0.Renderer2 }, { token: i0.ElementRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: PopoverComponent, isStandalone: true, selector: "kendo-popover", inputs: { position: "position", offset: "offset", width: "width", height: "height", title: "title", subtitle: "subtitle", body: "body", callout: "callout", animation: "animation", templateData: "templateData" }, outputs: { show: "show", shown: "shown", hide: "hide", hidden: "hidden", closeOnKeyDown: "closeOnKeyDown" }, host: { properties: { "attr.dir": "this.direction", "class.k-hidden": "this.isHidden", "attr.aria-hidden": "this.hasAttributeHidden", "style.width": "this._width", "style.height": "this._height" } }, providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.popover' } ], queries: [{ propertyName: "titleTemplate", first: true, predicate: PopoverTitleTemplateDirective, descendants: true }, { propertyName: "bodyTemplate", first: true, predicate: PopoverBodyTemplateDirective, descendants: true }, { propertyName: "actionsTemplate", first: true, predicate: PopoverActionsTemplateDirective, descendants: true }], viewQueries: [{ propertyName: "popoverWrapper", first: true, predicate: ["popoverWrapper"], descendants: true }, { propertyName: "titleTemplateWrapper", first: true, predicate: ["titleTemplateWrapper"], descendants: true }, { propertyName: "bodyTemplateWrapper", first: true, predicate: ["bodyTemplateWrapper"], descendants: true }], ngImport: i0, template: ` <div #popoverWrapper *ngIf="visible" class="k-popover k-popup" [ngStyle]="{'width': width, 'height': height}"> <div class="k-popover-callout" [ngClass]="getCalloutPosition()" *ngIf="callout"></div> <div class="k-popover-inner" *ngIf="callout; else noCallout"> <ng-container *ngTemplateOutlet="noCallout"></ng-container> </div> <ng-template #noCallout> <div #titleTemplateWrapper *ngIf="titleTemplate || title" class="k-popover-header"> <ng-template *ngIf="titleTemplate" [ngTemplateOutlet]="titleTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> <ng-container *ngIf="title && !titleTemplate"> {{ title }} </ng-container> </div> <div #bodyTemplateWrapper *ngIf="bodyTemplate || body" class="k-popover-body"> <ng-template *ngIf="bodyTemplate" [ngTemplateOutlet]="bodyTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> <ng-container *ngIf="body && !bodyTemplate"> {{ body }} </ng-container> </div> <div *ngIf="actionsTemplate" class="k-popover-actions k-actions k-actions-stretched k-actions-horizontal"> <ng-template *ngIf="actionsTemplate" [ngTemplateOutlet]="actionsTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> </div> </ng-template> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverComponent, decorators: [{ type: Component, args: [{ selector: 'kendo-popover', providers: [ LocalizationService, { provide: L10N_PREFIX, useValue: 'kendo.popover' } ], template: ` <div #popoverWrapper *ngIf="visible" class="k-popover k-popup" [ngStyle]="{'width': width, 'height': height}"> <div class="k-popover-callout" [ngClass]="getCalloutPosition()" *ngIf="callout"></div> <div class="k-popover-inner" *ngIf="callout; else noCallout"> <ng-container *ngTemplateOutlet="noCallout"></ng-container> </div> <ng-template #noCallout> <div #titleTemplateWrapper *ngIf="titleTemplate || title" class="k-popover-header"> <ng-template *ngIf="titleTemplate" [ngTemplateOutlet]="titleTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> <ng-container *ngIf="title && !titleTemplate"> {{ title }} </ng-container> </div> <div #bodyTemplateWrapper *ngIf="bodyTemplate || body" class="k-popover-body"> <ng-template *ngIf="bodyTemplate" [ngTemplateOutlet]="bodyTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> <ng-container *ngIf="body && !bodyTemplate"> {{ body }} </ng-container> </div> <div *ngIf="actionsTemplate" class="k-popover-actions k-actions k-actions-stretched k-actions-horizontal"> <ng-template *ngIf="actionsTemplate" [ngTemplateOutlet]="actionsTemplate?.templateRef" [ngTemplateOutletContext]="{ $implicit: anchor, data: contextData }"> </ng-template> </div> </ng-template> </div> `, standalone: true, imports: [NgIf, NgStyle, NgClass, NgTemplateOutlet] }] }], ctorParameters: function () { return [{ type: i1.LocalizationService }, { type: i0.Renderer2 }, { type: i0.ElementRef }, { type: i0.NgZone }]; }, propDecorators: { position: [{ type: Input }], offset: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], direction: [{ type: HostBinding, args: ['attr.dir'] }], title: [{ type: Input }], subtitle: [{ type: Input }], body: [{ type: Input }], callout: [{ type: Input }], animation: [{ type: Input }], templateData: [{ type: Input }], isHidden: [{ type: HostBinding, args: ['class.k-hidden'] }], hasAttributeHidden: [{ type: HostBinding, args: ['attr.aria-hidden'] }], show: [{ type: Output }], shown: [{ type: Output }], hide: [{ type: Output }], hidden: [{ type: Output }], closeOnKeyDown: [{ type: Output }], popoverWrapper: [{ type: ViewChild, args: ['popoverWrapper'] }], titleTemplateWrapper: [{ type: ViewChild, args: ['titleTemplateWrapper'] }], bodyTemplateWrapper: [{ type: ViewChild, args: ['bodyTemplateWrapper'] }], titleTemplate: [{ type: ContentChild, args: [PopoverTitleTemplateDirective, { static: false }] }], bodyTemplate: [{ type: ContentChild, args: [PopoverBodyTemplateDirective, { static: false }] }], actionsTemplate: [{ type: ContentChild, args: [PopoverActionsTemplateDirective, { static: false }] }], _width: [{ type: HostBinding, args: ['style.width'] }], _height: [{ type: HostBinding, args: ['style.height'] }] } }); /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ const validShowOptions = ['hover', 'click', 'none', 'focus']; /** * @hidden */ 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) &mdash;The Popover will be shown when its `anchor` element is clicked. * - `hover`&mdash;The Popover will be shown when its `anchor` element is hovered. * - `focus`&mdash;The Popover will be shown when its `anchor` element is focused. * - `none`&mdash;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$1.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$1.PopupService }, { type: i0.Renderer2 }]; }, propDecorators: { popover: [{ type: Input }], showOn: [{ type: Input }] } }); /** * @hidden */ class PopoverService { ngZone; _pointerOverPopup = new BehaviorSubject(null); _pointerOverAnchor = new BehaviorSubject(null); _focusInsidePopover = new BehaviorSubject(null); _hidePopover = new Subject(); _isOrigin; originAnchor; currentAnchor; subs = new Subscription(); constructor(ngZone) { this.ngZone = ngZone; this.monitor(); } ngOnDestroy() { this.subs.unsubscribe(); } get isPopoverHovered() { return this._pointerOverPopup.asObservable(); } emitPopoverState(isHovered) { this.ngZone.run(_ => this._pointerOverPopup.next(isHovered)); } get isAnchorHovered() { return this._pointerOverAnchor.asObservable(); } emitAnchorState(isHovered, anchor) { this._isOrigin = this.originAnchor === anchor; this.currentAnchor = anchor; if (isHovered) { this.originAnchor = anchor; } this.ngZone.run(_ => this._pointerOverAnchor.next(isHovered)); } get isFocusInsidePopover() { return this._focusInsidePopover.asObservable(); } emitFocusInsidePopover(isFocused) { this.ngZone.run(_ => this._focusInsidePopover.next(isFocused)); this._focusInsidePopover.next(null); } get hidePopover() { return this._hidePopover.asObservable(); } monitor() { this.subs.add(combineLatest(this.isPopoverHovered, this.isAnchorHovered).pipe( // `auditTime` is used because the `mouseleave` event is emitted before `mouseenter` // i.e. there is a millisecond in which the pointer leaves the first target (e.g. anchor) and hasn't reached the second one (e.g. popup) // resulting in both observables emitting `false` auditTime(20)).subscribe(val => { const [isPopoverHovered, isAnchorHovered] = val; this._hidePopover.next([isPopoverHovered, isAnchorHovered, this._isOrigin, this.currentAnchor]); })); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopoverService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.NgZone }]; } }); /* eslint-disable @typescript-eslint/no-explicit-any */ /** * Represents the [`kendoPopoverAnchor`](slug:configuration_popover#toc-popover-anchor) directive. * It is used to target an element, which should display a popover on interaction. * * @example * ```ts-no-run * <button kendoPopoverAnchor [popover]="myPopover">Show Popover</button> * ``` */ class PopoverAnchorDirective extends PopoverDirectivesBase { hostEl; ngZone; popupService; renderer; popoverService; constructor(hostEl, ngZone, popupService, renderer, popoverService) { super(ngZone, popupService, renderer); this.hostEl = hostEl; this.ngZone = ngZone; this.popupService = popupService; this.renderer = renderer; this.popoverService = popoverService; this._popoverService = this.popoverService; } ngOnChanges(changes) { if (changes['showOn'] && !changes['showOn'].isFirstChange()) { this.subs.unsubscribe(); if (this.disposeClickListener) { this.disposeClickListener(); } this.subs = new Subscription(); this.manageEvents(); } } /** * Shows the Popover. [See example]({% slug programmaticcontrol_popover %}) */ show() { if (this.popupRef) { return; } this.ngZone.run(() => { this.openPopup(this.hostEl); }); this.popupRef.popupAnchorViewportLeave .pipe(take(1)) .subscribe(() => this.hide()); } /** * Toggles the visibility of the Popover. [See example]({% slug programmaticcontrol_popover %}) */ toggle() { if (this.popupRef) { this.hide(); } else { this.show(); } } subscribeToShowEvents(arr) { const hostEl = this.hostEl.nativeElement; this.subs.add(this.renderer.listen(hostEl, arr[0].name, () => { this.popoverService.emitAnchorState(true, hostEl); arr[0].handler(); })); this.subs.add(this.renderer.listen(hostEl, arr[1].name, (e) => { this.popoverService.emitAnchorState(false, null); arr[1].handler({ domEvent: e }); })); } subscribeClick() { if (this.disposeClickListener) { this.disposeClickListener(); } this.disposeClickListener = this.renderer.listen(document, 'click', (e) => { this.onClick(e); }); } mouseenterHandler = () => { this.controlVisibility(this.hostEl.nativeElement, true); }; mouseleaveHandler = () => { if (this.isPrevented(this.hostEl.nativeElement, false)) { return; } if (!this._hideSub) { this._hideSub = this.popoverService.hidePopover.subscribe((val) => { const [isPopoverHovered, isAnchorHovered] = val; if (!isPopoverHovered && !isAnchorHovered) { this.hide(); } }); } }; focusHandler = () => { this.controlVisibility(this.hostEl.nativeElement, true); }; blurHandler = (args) => { const event = args.domEvent; if (this.isPrevented(this.hostEl.nativeElement, false)) { return; } // from anchor to popup focus check const isFocusInside = !!closest(event.relatedTarget, (node) => node.classList && node.classList.contains('k-popover')); if (!isFocusInside) { this.hide(); } if (!this._focusInsideSub) { // inside popup focus check this._focusInsideSub = this.popoverService.isFocusInsidePopover.pipe(filter(v => v !== null)).subscribe((val) => { if (!val) { this.hide(); } }); } }; /** * @hidden */ onClick(event) { const isInsidePopup = !!closest(event.target, (node) => node.classList && node.classList.contains('k-popup')); const isAnchor = !!closest(event.target, (node) => node === this.hostEl.nativeElement); if (isInsidePopup || (this.popupRef && isAnchor)) { return; } if (isAnchor) { // on open