UNPKG

@progress/kendo-angular-popup

Version:

Kendo UI Angular Popup component - an easily customized popup from the most trusted provider of professional Angular components.

364 lines (363 loc) 16.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 { Component, ElementRef, EventEmitter, Input, Output, NgZone, Renderer2, ViewChild } from '@angular/core'; import { AlignService } from './services/align.service'; import { DOMService } from './services/dom.service'; import { PositionService } from './services/position.service'; import { ResizeService } from './services/resize.service'; import { ScrollableService } from './services/scrollable.service'; import { AnimationService } from './services/animation.service'; import { isDifferentOffset } from './util'; import { hasObservers, isDocumentAvailable, ResizeSensorComponent } from '@progress/kendo-angular-common'; import { from } from 'rxjs'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from './package-metadata'; import { NgClass, NgTemplateOutlet, NgIf } from '@angular/common'; import * as i0 from "@angular/core"; import * as i1 from "./services/align.service"; import * as i2 from "./services/dom.service"; import * as i3 from "./services/position.service"; import * as i4 from "./services/resize.service"; import * as i5 from "./services/scrollable.service"; import * as i6 from "./services/animation.service"; const DEFAULT_OFFSET = { left: -10000, top: 0 }; const ANIMATION_CONTAINER = 'k-animation-container'; const ANIMATION_CONTAINER_FIXED = 'k-animation-container-fixed'; /** * Represents the [Kendo UI Popup component for Angular]({% slug overview_popup %}). * * @example * ```html * <button #anchor (click)="show = !show">Toggle</button> * <kendo-popup *ngIf="show" [anchor]="anchor"> * <strong>Popup content!</strong> * </kendo-popup> * ``` */ export class PopupComponent { container; _alignService; domService; _positionService; _resizeService; _scrollableService; animationService; _renderer; _zone; /** * Controls the Popup animation. By default, the opening and closing animations are enabled ([see example]({% slug animations_popup %})). * @default true */ animate = true; /** * Sets the element to use as an anchor. The Popup opens next to this element. ([See example]({% slug alignmentpositioning_popup %}#toc-aligning-to-components)). */ anchor; /** * Sets the anchor pivot point ([see example]({% slug alignmentpositioning_popup %}#toc-positioning)). * @default '{ horizontal: "left", vertical: "bottom" }' */ anchorAlign = { horizontal: 'left', vertical: 'bottom' }; /** * Sets the collision behavior of the Popup ([see example]({% slug viewportboundarydetection_popup %})). * @default '{ horizontal: "fit", vertical: "flip" }' */ collision = { horizontal: 'fit', vertical: 'flip' }; /** * Sets the pivot point of the Popup ([see example]({% slug alignmentpositioning_popup %}#toc-positioning)). * @default '{ horizontal: "left", vertical: "top" }' */ popupAlign = { horizontal: 'left', vertical: 'top' }; /** * Controls whether the component copies the `anchor` font styles. * @default false */ copyAnchorStyles = false; /** * Sets a list of CSS classes to add to the internal animated element ([see example]({% slug appearance_popup %})). * * > To style the content of the Popup, use this property binding. */ // eslint-disable-next-line @typescript-eslint/ban-types popupClass; /** * Sets the position mode of the component. By default, the Popup uses fixed positioning. To use absolute positioning, set this option to `absolute`. * * To support mobile browsers with the zoom option, use the `absolute` positioning of the Popup. * @default 'fixed' */ positionMode = 'fixed'; /** * Sets the absolute position of the element ([see example]({% slug alignmentpositioning_popup %}#toc-aligning-to-absolute-points)). * The Popup opens next to this point. The Popup pivot point is defined by the `popupAlign` option. The boundary detection uses the window viewport. * @default '{ left: -10000, top: 0 }' */ offset = DEFAULT_OFFSET; /** * Sets the margin value in pixels. Adds blank space between the Popup and the anchor ([see example]({% slug alignmentpositioning_popup %}#toc-adding-a-margin)). */ margin; /** * Fires when the anchor scrolls outside the screen boundaries. ([See example]({% slug closing_popup %}#toc-after-leaving-the-viewport)). */ anchorViewportLeave = new EventEmitter(); /** * Fires after the component closes. */ close = new EventEmitter(); /** * Fires after the component opens and the opening animation ends. */ open = new EventEmitter(); /** * Fires after the component is opened and the Popup is positioned. */ positionChange = new EventEmitter(); /** * @hidden */ contentContainer; /** * @hidden */ resizeSensor; /** * @hidden */ content; /** * @hidden */ renderDefaultClass = true; resolvedPromise = Promise.resolve(null); _currentOffset; animationSubscriptions; repositionSubscription; initialCheck = true; constructor(container, _alignService, domService, _positionService, _resizeService, _scrollableService, animationService, _renderer, _zone) { this.container = container; this._alignService = _alignService; this.domService = domService; this._positionService = _positionService; this._resizeService = _resizeService; this._scrollableService = _scrollableService; this.animationService = animationService; this._renderer = _renderer; this._zone = _zone; validatePackage(packageMetadata); this._renderer.addClass(container.nativeElement, ANIMATION_CONTAINER); this.updateFixedClass(); } ngOnInit() { this.reposition = this.reposition.bind(this); this._resizeService.subscribe(this.reposition); this.animationSubscriptions = this.animationService.start.subscribe(this.onAnimationStart.bind(this)); this.animationSubscriptions.add(this.animationService.end.subscribe(this.onAnimationEnd.bind(this))); this._scrollableService.forElement(this.domService.nativeElement(this.anchor) || this.container.nativeElement).subscribe(this.onScroll.bind(this)); this.currentOffset = DEFAULT_OFFSET; this.setZIndex(); this.copyFontStyles(); this.updateFixedClass(); this.reposition(); } ngOnChanges(changes) { if (changes.copyAnchorStyles) { this.copyFontStyles(); } if (changes.positionMode) { this.updateFixedClass(); } } ngAfterViewInit() { if (!this.animate) { this.resolvedPromise.then(() => { this.onAnimationEnd(); }); } this.reposition(); } ngAfterViewChecked() { if (this.initialCheck) { this.initialCheck = false; return; } this._zone.runOutsideAngular(() => { // workarounds https://github.com/angular/angular/issues/19094 // uses promise because it is executed synchronously after the content is updated // does not use onStable in case the current zone is not the angular one. this.unsubscribeReposition(); this.repositionSubscription = from(this.resolvedPromise) .subscribe(this.reposition); }); } ngOnDestroy() { this.anchorViewportLeave.complete(); this.positionChange.complete(); this.close.emit(); this.close.complete(); this._resizeService.unsubscribe(); this._scrollableService.unsubscribe(); this.animationSubscriptions.unsubscribe(); this.unsubscribeReposition(); } /** * @hidden */ onResize() { this.reposition(); } onAnimationStart() { this._renderer.removeClass(this.container.nativeElement, 'k-animation-container-shown'); } onAnimationEnd() { this._renderer.addClass(this.container.nativeElement, 'k-animation-container-shown'); this.open.emit(); this.open.complete(); } get currentOffset() { return this._currentOffset; } set currentOffset(offset) { this.setContainerStyle('left', `${offset.left}px`); this.setContainerStyle('top', `${offset.top}px`); this._currentOffset = offset; } setZIndex() { if (this.anchor) { this.setContainerStyle('z-index', String(this.domService.zIndex(this.domService.nativeElement(this.anchor), this.container))); } } reposition() { if (!isDocumentAvailable()) { return; } const { flip, offset } = this.position(); if (!this.currentOffset || isDifferentOffset(this.currentOffset, offset)) { this.currentOffset = offset; if (hasObservers(this.positionChange)) { this._zone.run(() => this.positionChange.emit({ offset, flip })); } } if (this.animate) { this.animationService.play(this.contentContainer.nativeElement, this.animate, flip); } this.resizeSensor.acceptSize(); } position() { const alignedOffset = this._alignService.alignElement({ anchor: this.domService.nativeElement(this.anchor), anchorAlign: this.anchorAlign, element: this.container, elementAlign: this.popupAlign, margin: this.margin, offset: this.offset, positionMode: this.positionMode }); return this._positionService.positionElement({ anchor: this.domService.nativeElement(this.anchor), anchorAlign: this.anchorAlign, collisions: this.collision, currentLocation: alignedOffset, element: this.container, elementAlign: this.popupAlign, margin: this.margin }); } onScroll(isInViewPort) { const hasLeaveObservers = hasObservers(this.anchorViewportLeave); if (isInViewPort || !hasLeaveObservers) { this.reposition(); } else if (hasLeaveObservers) { this._zone.run(() => { this.anchorViewportLeave.emit(); }); } } copyFontStyles() { if (!this.anchor || !this.copyAnchorStyles) { return; } this.domService.getFontStyles(this.domService.nativeElement(this.anchor)) .forEach((s) => this.setContainerStyle(s.key, s.value)); } updateFixedClass() { const action = this.positionMode === 'fixed' ? 'addClass' : 'removeClass'; this._renderer[action](this.container.nativeElement, ANIMATION_CONTAINER_FIXED); } setContainerStyle(name, value) { this._renderer.setStyle(this.container.nativeElement, name, value); } unsubscribeReposition() { if (this.repositionSubscription) { this.repositionSubscription.unsubscribe(); } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupComponent, deps: [{ token: i0.ElementRef }, { token: i1.AlignService }, { token: i2.DOMService }, { token: i3.PositionService }, { token: i4.ResizeService }, { token: i5.ScrollableService }, { token: i6.AnimationService }, { token: i0.Renderer2 }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: PopupComponent, isStandalone: true, selector: "kendo-popup", inputs: { animate: "animate", anchor: "anchor", anchorAlign: "anchorAlign", collision: "collision", popupAlign: "popupAlign", copyAnchorStyles: "copyAnchorStyles", popupClass: "popupClass", positionMode: "positionMode", offset: "offset", margin: "margin" }, outputs: { anchorViewportLeave: "anchorViewportLeave", close: "close", open: "open", positionChange: "positionChange" }, providers: [AlignService, AnimationService, DOMService, PositionService, ResizeService, ScrollableService], viewQueries: [{ propertyName: "contentContainer", first: true, predicate: ["container"], descendants: true, static: true }, { propertyName: "resizeSensor", first: true, predicate: ResizeSensorComponent, descendants: true, static: true }], exportAs: ["kendo-popup"], usesOnChanges: true, ngImport: i0, template: ` <div class="k-child-animation-container"> <div [class.k-popup]="renderDefaultClass" [ngClass]="popupClass" #container> <ng-content></ng-content> <ng-template [ngTemplateOutlet]="content" [ngIf]="content"></ng-template> <kendo-resize-sensor [rateLimit]="100" (resize)="onResize()"> </kendo-resize-sensor> </div> </div> `, isInline: true, dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ResizeSensorComponent, selector: "kendo-resize-sensor", inputs: ["rateLimit"], outputs: ["resize"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupComponent, decorators: [{ type: Component, args: [{ exportAs: 'kendo-popup', providers: [AlignService, AnimationService, DOMService, PositionService, ResizeService, ScrollableService], selector: 'kendo-popup', template: ` <div class="k-child-animation-container"> <div [class.k-popup]="renderDefaultClass" [ngClass]="popupClass" #container> <ng-content></ng-content> <ng-template [ngTemplateOutlet]="content" [ngIf]="content"></ng-template> <kendo-resize-sensor [rateLimit]="100" (resize)="onResize()"> </kendo-resize-sensor> </div> </div> `, standalone: true, imports: [NgClass, NgTemplateOutlet, NgIf, ResizeSensorComponent] }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i1.AlignService }, { type: i2.DOMService }, { type: i3.PositionService }, { type: i4.ResizeService }, { type: i5.ScrollableService }, { type: i6.AnimationService }, { type: i0.Renderer2 }, { type: i0.NgZone }]; }, propDecorators: { animate: [{ type: Input }], anchor: [{ type: Input }], anchorAlign: [{ type: Input }], collision: [{ type: Input }], popupAlign: [{ type: Input }], copyAnchorStyles: [{ type: Input }], popupClass: [{ type: Input }], positionMode: [{ type: Input }], offset: [{ type: Input }], margin: [{ type: Input }], anchorViewportLeave: [{ type: Output }], close: [{ type: Output }], open: [{ type: Output }], positionChange: [{ type: Output }], contentContainer: [{ type: ViewChild, args: ['container', { static: true }] }], resizeSensor: [{ type: ViewChild, args: [ResizeSensorComponent, { static: true }] }] } });