UNPKG

@progress/kendo-angular-popup

Version:

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

462 lines (461 loc) 19.5 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 * ```ts * _@Component({ * selector: 'my-app', * template: ` * <button #anchor (click)="show=!show">Toggle</button> * <kendo-popup *ngIf="show" [anchor]="anchor"> * <strong>Popup content!</strong> * </kendo-popup> * ` * }) * class AppComponent { * public show: boolean = false; * } * ``` */ 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 %})). */ animate = true; /** * Specifies the element that will be used as an anchor. The Popup opens next to that element. * ([see example]({% slug alignmentpositioning_popup %}#toc-aligning-to-components)). */ anchor; /** * Specifies the anchor pivot point * ([see example]({% slug alignmentpositioning_popup %}#toc-positioning)). */ anchorAlign = { horizontal: 'left', vertical: 'bottom' }; /** * Configures the collision behavior of the Popup * ([see example]({% slug viewportboundarydetection_popup %})). */ collision = { horizontal: 'fit', vertical: 'flip' }; /** * Specifies the pivot point of the Popup * ([see example]({% slug alignmentpositioning_popup %}#toc-positioning)). */ popupAlign = { horizontal: 'left', vertical: 'top' }; /** * Controls whether the component will copy the `anchor` font styles. */ copyAnchorStyles = false; /** * Specifies a list of CSS classes that will be added 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; /** * Specifies the position mode of the component. By default, the Popup uses fixed positioning. * To make the Popup acquire absolute positioning, set this option to `absolute`. * * > If you need to support mobile browsers with the zoom option, * use the `absolute` positioning of the Popup. * * @example * ```html * <style> * .parent-content { * position: relative; * width: 200px; * height: 200px; * overflow: auto; * margin: 200px auto; * border: 1px solid red; * } * .content { * position: relative; * width: 100px; * height: 100px; * overflow: auto; * margin: 300px; * border: 1px solid blue; * } * .anchor { * position: absolute; * top: 200px; * left: 200px; * } * </style> * ``` * ```ts * _@Component({ * selector: 'my-app', * template: ` * <div class="example-config"> * Position mode: * <label><input type="radio" value="fixed" [(ngModel)]="mode" /> Fixed</label> * <label><input type="radio" value="absolute" [(ngModel)]="mode" /> Absolute</label> * </div> * <div class="example-config"> * Append to * <label> * <input type="radio" name="place" [value]="1" [(ngModel)]="checked" /> * Root component * </label> * <label> * <input type="radio" name="place" [value]="2" [(ngModel)]="checked" /> * <span [style.color]="'red'">Red Container</span> * </label> * <label> * <input type="radio" name="place" [value]="3" [(ngModel)]="checked" /> * <span [style.color]="'blue'">Blue Container</span> * </label> * </div> * <div class="example"> * <div class="parent-content" [scrollLeft]="250" [scrollTop]="230"> * <div class="content" [scrollLeft]="170" [scrollTop]="165"> * <button #anchor class="anchor" (click)="show = !show">Toggle</button> * <kendo-popup [positionMode]="mode" [anchor]="anchor" (anchorViewportLeave)="show=false" *ngIf="show && checked === 3"> * <ul> * <li>Item1</li> * <li>Item2</li> * <li>Item3</li> * </ul> * </kendo-popup> * <span [style.position]="'absolute'" [style.top.px]="400" [style.left.px]="400">Bottom/Right</span> * </div> * <kendo-popup [positionMode]="mode" [anchor]="anchor" (anchorViewportLeave)="show=false" *ngIf="show && checked === 2"> * <ul> * <li>Item1</li> * <li>Item2</li> * <li>Item3</li> * </ul> * </kendo-popup> * <span [style.position]="'absolute'" [style.top.px]="600" [style.left.px]="600">Bottom/Right</span> * </div> * <kendo-popup [positionMode]="mode" [anchor]="anchor" (anchorViewportLeave)="show=false" *ngIf="show && checked === 1"> * <ul> * <li>Item1</li> * <li>Item2</li> * <li>Item3</li> * </ul> * </kendo-popup> * </div> * ` * }) * class AppComponent { * public checked: number = 3; * public mode: string = 'absolute'; * public show: boolean = true; * } * ``` */ positionMode = 'fixed'; /** * Specifies the absolute position of the element * ([see example]({% slug alignmentpositioning_popup %}#toc-aligning-to-absolute-points)). * The Popup opens next to that point. The Popup pivot point is defined by the `popupAlign` configuration option. * The boundary detection is applied by using the window viewport. */ offset = DEFAULT_OFFSET; /** * Specifies the margin value that will be added to the popup dimensions in pixels and leaves a blank space * between the popup and the anchor ([see example]({% slug alignmentpositioning_popup %}#toc-adding-a-margin)). */ margin; /** * Fires when the anchor is scrolled outside the screen boundaries. * ([see example]({% slug closing_popup %}#toc-after-leaving-the-viewport)). */ anchorViewportLeave = new EventEmitter(); /** * Fires after the component is closed. */ close = new EventEmitter(); /** * Fires after the component is opened 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; 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" [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" [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 }] }] } });