UNPKG

@progress/kendo-angular-popup

Version:

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

1,328 lines (1,315 loc) 50.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 { Injectable, InjectionToken, Inject, Optional, EventEmitter, isDevMode, Component, Input, Output, ViewChild, TemplateRef, NgModule } from '@angular/core'; import { siblingContainer, parents, addScroll, align, boundingOffset, offset, positionWithScroll, removeScroll, restrictToView, scrollPosition, getWindowViewPort } from '@progress/kendo-popup-common'; import * as i1$1 from '@progress/kendo-angular-common'; import { isDocumentAvailable, hasObservers, ResizeSensorComponent, ResizeBatchService, KENDO_RESIZESENSOR } from '@progress/kendo-angular-common'; import { fromEvent, merge, from } from 'rxjs'; import { auditTime } from 'rxjs/operators'; import * as i1 from '@angular/animations'; import { style, animate } from '@angular/animations'; import { validatePackage } from '@progress/kendo-licensing'; import { NgClass, NgTemplateOutlet, NgIf } from '@angular/common'; /** * @hidden */ const eitherRect = (rect, offset) => { if (!rect) { return { height: 0, left: offset.left, top: offset.top, width: 0 }; } return rect; }; /** * @hidden */ const replaceOffset = (rect, offset) => { if (!offset) { return rect; } const result = { height: rect.height, left: offset.left, top: offset.top, width: rect.width }; return result; }; /** * @hidden */ const removeStackingOffset = (rect, stackingOffset) => { if (!stackingOffset) { return rect; } const result = { height: rect.height, left: rect.left - stackingOffset.left, top: rect.top - stackingOffset.top, width: rect.width }; return result; }; /** * @hidden */ const isDifferentOffset = (oldOffset, newOffset) => { const { left: oldLeft, top: oldTop } = oldOffset; const { left: newLeft, top: newTop } = newOffset; return Math.abs(oldLeft - newLeft) >= 1 || Math.abs(oldTop - newTop) >= 1; }; /** * @hidden */ const isWindowAvailable = () => { return typeof window !== 'undefined'; }; /** * @hidden */ const hasBoundingRect = (elem) => !!elem.getBoundingClientRect; /** * @hidden */ const OVERFLOW_REGEXP = /auto|scroll/; const overflowElementStyle = (element) => { return `${element.style.overflow}${element.style.overflowX}${element.style.overflowY}`; }; const overflowComputedStyle = (element) => { const styles = window.getComputedStyle(element); return `${styles.overflow}${styles.overflowX}${styles.overflowY}`; }; const overflowStyle = (element) => { return overflowElementStyle(element) || overflowComputedStyle(element); }; /** * @hidden */ const scrollableParents = (element) => { const parentElements = []; if (!isDocumentAvailable() || !isWindowAvailable()) { return parentElements; } let parent = element.parentElement; while (parent) { if (OVERFLOW_REGEXP.test(overflowStyle(parent)) || parent.hasAttribute('data-scrollable')) { parentElements.push(parent); } parent = parent.parentElement; } parentElements.push(window); return parentElements; }; /** * @hidden */ const FRAME_DURATION = 1000 / 60; //1000ms divided by 60fps // eslint-disable-next-line @typescript-eslint/ban-types function memoize(fun) { let result; let called = false; return (...args) => { if (called) { return result; } result = fun(...args); called = true; return result; }; } /** * @hidden */ const hasRelativeStackingContext = memoize(() => { if (!isDocumentAvailable() && document.body !== null) { return false; } const top = 10; const parent = document.createElement("div"); parent.style.transform = "matrix(10, 0, 0, 10, 0, 0)"; const childElement = document.createElement("div"); childElement.style.position = 'fixed'; childElement.style.top = `${top}px`; childElement.textContent = 'child'; parent.appendChild(childElement); document.body.appendChild(parent); const isDifferent = parent.children[0].getBoundingClientRect().top !== top; document.body.removeChild(parent); return isDifferent; }); /** * @hidden */ const zIndex = (anchor, container) => { if (!anchor || !isDocumentAvailable() || !isWindowAvailable()) { return null; } const sibling = siblingContainer(anchor, container); if (!sibling) { return null; } const result = [anchor].concat(parents(anchor, sibling)).reduce((index, p) => { const zIndexStyle = p.style.zIndex || window.getComputedStyle(p).zIndex; const current = parseInt(zIndexStyle, 10); return current > index ? current : index; }, 0); return result ? (result + 1) : null; }; /** * @hidden */ const scaleRect = (rect, scale) => { if (!rect || scale === 1) { return rect; } return { height: rect.height / scale, left: rect.left / scale, top: rect.top / scale, width: rect.width / scale }; }; const STYLES = [ 'font-size', 'font-family', 'font-stretch', 'font-style', 'font-weight', 'line-height' ]; /** * @hidden */ class DOMService { _dummy; addOffset(current, addition) { return { left: current.left + addition.left, top: current.top + addition.top }; } addScroll(rect, scroll) { return addScroll(rect, scroll); } align(settings) { return align(settings); } boundingOffset(el) { return boundingOffset(el); } getFontStyles(el) { const window = this.getWindow(); if (!window || !el) { return []; } const computedStyles = window.getComputedStyle(el); return STYLES.map(key => ({ key: key, value: computedStyles[key] })); } getWindow() { return isWindowAvailable() ? window : null; } hasOffsetParent(el) { if (!el || !isDocumentAvailable()) { return false; } return !!this.nativeElement(el).offsetParent; } offset(el) { if (!el || !isDocumentAvailable()) { return null; } return offset(el); } offsetAtPoint(el, currentLocation) { if (!el || !isDocumentAvailable()) { return null; } const element = this.nativeElement(el); const { left, top, transition } = element.style; element.style.transition = 'none'; element.style.left = `${currentLocation.left}px`; element.style.top = `${currentLocation.top}px`; const currentOffset = offset(element); element.style.left = left; element.style.top = top; // prevents elements with transition to be animated because of the change this._dummy = element.offsetHeight; element.style.transition = transition; return currentOffset; } nativeElement(el) { if (!el || !isDocumentAvailable()) { return null; } return el.nativeElement || el; } position(element, popup, scale = 1) { if (!element || !popup) { return null; } return positionWithScroll(element, this.nativeElement(popup), scale); } removeScroll(rect, scroll) { return removeScroll(rect, scroll); } restrictToView(settings) { return restrictToView(settings); } scrollPosition(el) { return scrollPosition(this.nativeElement(el)); } scrollableParents(el) { return scrollableParents(el); } stackingElementOffset(el) { const relativeContextElement = this.getRelativeContextElement(el); if (!relativeContextElement) { return null; } return offset(relativeContextElement); } stackingElementScroll(el) { const relativeContextElement = this.getRelativeContextElement(el); if (!relativeContextElement) { return { x: 0, y: 0 }; } return { x: relativeContextElement.scrollLeft, y: relativeContextElement.scrollTop }; } getRelativeContextElement(el) { if (!el || !hasRelativeStackingContext()) { return null; } let parent = this.nativeElement(el).parentElement; while (parent) { if (window.getComputedStyle(parent).transform !== 'none') { return parent; } parent = parent.parentElement; } return null; } useRelativePosition(el) { return !!this.getRelativeContextElement(el); } windowViewPort(el) { return getWindowViewPort(this.nativeElement(el)); } zIndex(anchor, container) { return zIndex(anchor, this.nativeElement(container)); } zoomLevel() { if (!isDocumentAvailable() || !isWindowAvailable()) { return 1; } return parseFloat((document.documentElement.clientWidth / window.innerWidth).toFixed(2)) || 1; } isZoomed() { return this.zoomLevel() > 1; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DOMService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DOMService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DOMService, decorators: [{ type: Injectable }] }); /** * Use the `SCALE` injection token to set the document scale when you use a [scale transform](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale). * * The document or container scale is required to compute the popup position correctly. Set the value for `SCALE` to ensure correct positioning. See [Support for Document Scale]({% slug documentscale_popup %}). * * > You do not need to use this token for user-applied browser zoom. */ const SCALE = new InjectionToken('Popup Document Scale'); /** * @hidden */ class AlignService { _dom; scale; constructor(_dom, scale = 1) { this._dom = _dom; this.scale = scale; } alignElement(settings) { const { anchor, element, anchorAlign, elementAlign, margin, offset, positionMode } = settings; const scale = this.scale || 1; const fixedMode = positionMode === 'fixed' || !this._dom.hasOffsetParent(element); const anchorRect = fixedMode ? this.absoluteRect(anchor, element, offset, scale) : this.relativeRect(anchor, element, offset, scale); const elementRect = scaleRect(this._dom.offset(element.nativeElement), scale); const result = this._dom.align({ anchorAlign: anchorAlign, anchorRect: anchorRect, elementAlign: elementAlign, elementRect: elementRect, margin }); return result; } absoluteRect(anchor, element, offset, scale) { const scrollPos = this.elementScrollPosition(anchor, element); const rect = eitherRect(this._dom.offset(anchor), offset); const stackScale = 2 * scale; const stackScroll = this._dom.stackingElementScroll(element); if (scale !== 1 && stackScroll) { stackScroll.x /= stackScale; stackScroll.y /= stackScale; } const stackOffset = this._dom.stackingElementOffset(element); if (scale !== 1 && stackOffset) { stackOffset.left /= stackScale; stackOffset.top /= stackScale; } return this._dom.removeScroll(this._dom.addScroll(removeStackingOffset(scaleRect(rect, scale), stackOffset), stackScroll), scrollPos); } elementScrollPosition(anchor, element) { return anchor ? { x: 0, y: 0 } : this._dom.scrollPosition(element); } relativeRect(anchor, element, offset, scale) { const rect = eitherRect(this._dom.position(anchor, element, scale), offset); return scaleRect(rect, scale); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AlignService, deps: [{ token: DOMService }, { token: SCALE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AlignService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AlignService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: DOMService }, { type: undefined, decorators: [{ type: Inject, args: [SCALE] }, { type: Optional }] }]; } }); /** * @hidden */ class PositionService { _dom; scale; constructor(_dom, scale = 1) { this._dom = _dom; this.scale = scale; } positionElement(settings) { const { anchor, currentLocation, element, anchorAlign, elementAlign, collisions, margin } = settings; const dom = this._dom; const scale = this.scale || 1; const elementOffset = dom.offsetAtPoint(element, currentLocation); const elementRect = scaleRect(elementOffset, scale); const anchorOffset = scaleRect(dom.offset(anchor), scale); const anchorRect = eitherRect(anchorOffset, currentLocation); const viewPort = settings.viewPort || dom.windowViewPort(element); viewPort.width = viewPort.width / scale; viewPort.height = viewPort.height / scale; const result = dom.restrictToView({ anchorAlign, anchorRect, collisions, elementAlign, elementRect, margin, viewPort }); const offset = dom.addOffset(currentLocation, result.offset); return { flip: result.flip, flipped: result.flipped, offset: offset }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PositionService, deps: [{ token: DOMService }, { token: SCALE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PositionService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PositionService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: DOMService }, { type: undefined, decorators: [{ type: Inject, args: [SCALE] }, { type: Optional }] }]; } }); /** * @hidden */ class ResizeService { _dom; _zone; subscription; constructor(_dom, _zone) { this._dom = _dom; this._zone = _zone; } subscribe(callback) { if (!isDocumentAvailable()) { return; } this._zone.runOutsideAngular(() => { this.subscription = fromEvent(this._dom.getWindow(), "resize") .pipe(auditTime(FRAME_DURATION)) .subscribe(() => callback()); }); } unsubscribe() { if (!this.subscription) { return; } this.subscription.unsubscribe(); } isUnsubscribed() { return this.subscription && this.subscription.closed; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ResizeService, deps: [{ token: DOMService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ResizeService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ResizeService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: DOMService }, { type: i0.NgZone }]; } }); /** * @hidden */ const THRESHOLD_DIFF = 1; /** * @hidden */ class ScrollableService { _dom; _zone; element; subscription; constructor(_dom, _zone) { this._dom = _dom; this._zone = _zone; } forElement(element) { this.unsubscribe(); this.element = element; return this; } subscribe(callback) { if (!callback || !isDocumentAvailable() || !this.element) { return; } const parents = this._dom.scrollableParents(this.element); this._zone.runOutsideAngular(() => { const observables = parents.map(p => fromEvent(p, "scroll").pipe(auditTime(FRAME_DURATION))); const subscriber = (e) => { const target = e.target; const isParent = parents.filter(p => p === target).length > 0; const isDocument = target === document; const isWindow = target === window; if (isParent || isDocument || isWindow) { callback(this.isVisible(this.element, target)); } }; this.subscription = merge(...observables).subscribe(subscriber); }); } unsubscribe() { if (!this.subscription) { return; } this.subscription.unsubscribe(); } isVisible(elem, container) { const elemRect = this._dom.boundingOffset(elem); const containerRect = this._dom.boundingOffset(this._dom.nativeElement(container)); if (THRESHOLD_DIFF < (containerRect.top - elemRect.bottom)) { return false; } if (THRESHOLD_DIFF < (elemRect.top - containerRect.bottom)) { return false; } if (THRESHOLD_DIFF < (elemRect.left - containerRect.right)) { return false; } if (THRESHOLD_DIFF < (containerRect.left - elemRect.right)) { return false; } return true; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ScrollableService, deps: [{ token: DOMService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ScrollableService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: ScrollableService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: DOMService }, { type: i0.NgZone }]; } }); const LEFT = 'left'; const RIGHT = 'right'; const DOWN = 'down'; const UP = 'up'; const DEFAULT_TYPE = 'slide'; const DEFAULT_DURATION = 100; const animationTypes = {}; animationTypes.expand = (direction) => { const scale = direction === UP || direction === DOWN ? 'scaleY' : 'scaleX'; const startScale = 0; const endScale = 1; let origin; if (direction === DOWN) { origin = 'top'; } else if (direction === LEFT) { origin = RIGHT; } else if (direction === RIGHT) { origin = LEFT; } else { origin = 'bottom'; } return { start: { transform: `${scale}(${startScale})`, transformOrigin: origin }, end: { transform: `${scale}(${endScale})` } }; }; animationTypes.slide = (direction) => { const translate = direction === LEFT || direction === RIGHT ? 'translateX' : 'translateY'; const start = direction === RIGHT || direction === DOWN ? -100 : 100; const end = 0; return { start: { transform: `${translate}(${start}%)` }, end: { transform: `${translate}(${end}%)` } }; }; animationTypes.fade = () => { return { start: { opacity: 0 }, end: { opacity: 1 } }; }; animationTypes.zoom = () => { const start = 0; const end = 1; return { start: { transform: `scale(${start})` }, end: { transform: `scale(${end})` } }; }; /** * @hidden */ class AnimationService { animationBuilder; start = new EventEmitter(); end = new EventEmitter(); flip; player; constructor(animationBuilder) { this.animationBuilder = animationBuilder; } play(element, options, flip) { if (!this.flip || this.flip.horizontal !== flip.horizontal || this.flip.vertical !== flip.vertical) { this.flip = flip; const type = options.type || DEFAULT_TYPE; const statesFn = animationTypes[type]; if (statesFn) { const direction = this.getDirection(flip, options); const states = statesFn(direction); this.playStates(element, states, options); } else if (isDevMode()) { throw new Error(`Unsupported animation type: "${type}". The supported types are slide, expand, fade and zoom.`); } } } ngOnDestroy() { this.stopPlayer(); } playStates(element, states, options) { this.stopPlayer(); const duration = options.duration || DEFAULT_DURATION; const factory = this.animationBuilder.build([ style(states.start), animate(`${duration}ms ease-in`, style(states.end)) ]); const player = this.player = factory.create(element); player.onDone(() => { this.end.emit(); this.stopPlayer(); }); this.start.emit(); player.play(); } getDirection(flip, options) { let direction = options.direction || DOWN; if (flip.horizontal) { if (direction === LEFT) { direction = RIGHT; } else if (direction === RIGHT) { direction = LEFT; } } if (flip.vertical) { if (direction === DOWN) { direction = UP; } else if (direction === UP) { direction = DOWN; } } return direction; } stopPlayer() { if (this.player) { this.player.destroy(); this.player = null; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AnimationService, deps: [{ token: i1.AnimationBuilder }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AnimationService }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: AnimationService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i1.AnimationBuilder }]; } }); /** * @hidden */ const packageMetadata = { name: '@progress/kendo-angular-popup', productName: 'Kendo UI for Angular', productCode: 'KENDOUIANGULAR', productCodes: ['KENDOUIANGULAR'], publishDate: 1756992569, version: '20.0.3', licensingDocsUrl: 'https://www.telerik.com/kendo-angular-ui/my-license/?utm_medium=product&utm_source=kendoangular&utm_campaign=kendo-ui-angular-purchase-license-keys-warning' }; 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> * ``` */ 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: AlignService }, { token: DOMService }, { token: PositionService }, { token: ResizeService }, { token: ScrollableService }, { token: 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: AlignService }, { type: DOMService }, { type: PositionService }, { type: ResizeService }, { type: ScrollableService }, { type: 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 }] }] } }); const removeElement = (element) => { if (element && element.parentNode) { element.parentNode.removeChild(element); } }; /** * Injects the Popup container. If not set, uses the first root component of the application. * * > Use `POPUP_CONTAINER` only with the `PopupService` class ([see example](slug:service_popup)). * * In standalone components: * * @example * ```ts * import { Component } from '@angular/core'; * import { KENDO_POPUP, PopupService } from '@progress/kendo-angular-popup'; * * @Component({ * standalone: true, * imports: [KENDO_POPUP], * providers: [PopupService, { * provide: POPUP_CONTAINER, * useFactory: () => { * //return the container ElementRef, where the popup will be injected * return { nativeElement: document.body } as ElementRef; * } * }], * selector: 'app-root', * templateUrl: './app.component.html', * }) * export class AppComponent {} * ``` * * In NgModule-based applications: * * @example * ```ts * import { PopupModule, POPUP_CONTAINER } from '@progress/kendo-angular-popup'; * import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; * import { ElementRef, NgModule } from '@angular/core'; * import { AppComponent } from './app.component'; * * _@NgModule({ * declarations: [AppComponent], * imports: [BrowserModule, PopupModule], * bootstrap: [AppComponent], * providers: [{ * provide: POPUP_CONTAINER, * useFactory: () => { * //return the container ElementRef, where the popup will be injected * return { nativeElement: document.body } as ElementRef; * } * }] * }) * export class AppModule {} * * platformBrowserDynamic().bootstrapModule(AppModule); * ``` */ const POPUP_CONTAINER = new InjectionToken('Popup Container'); /** * Provides a service for opening Popup components dynamically ([see example]({% slug service_popup %})). * * @export * @class PopupService */ class PopupService { applicationRef; componentFactoryResolver; injector; container; /** * Gets the root view container for injecting the component. * * @returns {ComponentRef<any>} The root view container reference. */ get rootViewContainer() { // https://github.com/angular/angular/blob/4.0.x/packages/core/src/application_ref.ts#L571 const rootComponents = this.applicationRef.components || []; if (rootComponents[0]) { return rootComponents[0]; } throw new Error(` View Container not found! Inject the POPUP_CONTAINER or define a specific ViewContainerRef via the appendTo option. See https://www.telerik.com/kendo-angular-ui/components/popup/api/POPUP_CONTAINER/ for more details. `); } /** * Gets the HTML element of the root component container. * * @returns {HTMLElement} The root container HTML element. */ get rootViewContainerNode() { return this.container ? this.container.nativeElement : this.getComponentRootNode(this.rootViewContainer); } constructor(applicationRef, componentFactoryResolver, injector, container) { this.applicationRef = applicationRef; this.componentFactoryResolver = componentFactoryResolver; this.injector = injector; this.container = container; } /** * Opens a Popup component. The Popup mounts in the DOM under the root application component. * * @param {PopupSettings} options - The options for the Popup. * @returns {ComponentRef<PopupComponent>} A reference to the Popup object. */ open(options = {}) { const { component, nodes } = this.contentFrom(options.content); const popupComponentRef = this.appendPopup(nodes, options.appendTo); const popupInstance = popupComponentRef.instance; this.projectComponentInputs(popupComponentRef, options); popupComponentRef.changeDetectorRef.detectChanges(); if (component) { component.changeDetectorRef.detectChanges(); } const popupElement = this.getComponentRootNode(popupComponentRef); return { close: () => { if (component) { component.destroy(); } popupComponentRef.destroy(); // Issue in Chrome causes https://github.com/telerik/kendo-angular/issues/4434 // To be fixed in Chrome, remove try..catch afterwards // https://chromestatus.com/feature/5128696823545856 // https://issues.chromium.org/issues/41484175 try { // Angular will not remove the element unless the change detection is triggered removeElement(popupElement); } catch { /* noop */ } }, content: component, popup: popupComponentRef, popupAnchorViewportLeave: popupInstance.anchorViewportLeave, popupClose: popupInstance.close, popupElement: popupElement, popupOpen: popupInstance.open, popupPositionChange: popupInstance.positionChange }; } appendPopup(nodes, container) { const popupComponentRef = this.createComponent(PopupComponent, nodes, container); if (!container) { this.rootViewContainerNode.appendChild(this.getComponentRootNode(popupComponentRef)); } return popupComponentRef; } /** * Gets the HTML element for a component reference. * * @param {ComponentRef<any>} componentRef The component reference. * @returns {HTMLElement} The root HTML element of the component. */ getComponentRootNode(componentRef) { return componentRef.location.nativeElement; } /** * Gets the `ComponentFactory` instance by type. * * @param {*} componentClass The component class. * @returns {ComponentFactory<any>} The component factory instance. */ getComponentFactory(componentClass) { return this.componentFactoryResolver.resolveComponentFactory(componentClass); } /** * Creates a component reference from a `Component` class. * * @param {*} componentClass The component class. * @param {*} nodes The nodes to project. * @param {ViewContainerRef} container The container to use. * @returns {ComponentRef<any>} The created component reference. */ createComponent(componentClass, nodes, container) { const factory = this.getComponentFactory(componentClass); if (container) { return container.createComponent(factory, undefined, this.injector, nodes); } else { const component = factory.create(this.injector, nodes); this.applicationRef.attachView(component.hostView); return component; } } /** * Projects the input options onto the component instance. * * @param {ComponentRef<any>} component The component reference. * @param {*} options The options to project. * @returns {ComponentRef<any>} The updated component reference. */ projectComponentInputs(component, options) { Object.getOwnPropertyNames(options) .filter(prop => prop !== 'content' || options.content instanceof TemplateRef) .map((prop) => { component.instance[prop] = options[prop]; }); return component; } /** * Gets the component and nodes to append from the `content` option. * * @param {*} content The content to use. * @returns {any} The component and nodes for projection. */ contentFrom(content) { if (!content || content instanceof TemplateRef) { return { component: null, nodes: [[]] }; } const component = this.createComponent(content); const nodes = component ? [component.location.nativeElement] : []; return { component: component, nodes: [ nodes // <ng-content> ] }; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupService, deps: [{ token: i0.ApplicationRef }, { token: i0.ComponentFactoryResolver }, { token: i0.Injector }, { token: POPUP_CONTAINER, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: i0.ApplicationRef }, { type: i0.ComponentFactoryResolver }, { type: i0.Injector }, { type: i0.ElementRef, decorators: [{ type: Inject, args: [POPUP_CONTAINER] }, { type: Optional }] }]; } }); /** * Use this utility array to access all `@progress/kendo-angular-popup`-related components and directives in a standalone Angular component. * * @example * ```typescript * import { Component } from '@angular/core'; * import { KENDO_POPUP } from '@progress/kendo-angular-popup'; * * @Component({ * selector: 'my-app', * standalone: true, * imports: [KENDO_POPUP], * template: `<kendo-popup>Popup content.</kendo-popup>` * }) * export class AppComponent {} * ``` */ const KENDO_POPUP = [ PopupComponent ]; //IMPORTANT: NgModule export kept for backwards compatibility /** * Required for adding all Popup features in NgModule-based Angular applications. * * @example * ```ts-no-run * // Import the Popup module * import { PopupModule } from '@progress/kendo-angular-popup'; * * // The browser platform with a compiler * import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; * * import { NgModule } from '@angular/core'; * * // Import the app component * import { AppComponent } from './app.component'; * * // Define the app module * _@NgModule({ * declarations: [AppComponent], // declare app component * imports: [BrowserModule, PopupModule], // import Popup module * bootstrap: [AppComponent] * }) * export class AppModule {} * * // Compile and launch the module * platformBrowserDynamic().bootstrapModule(AppModule); * * ``` */ class PopupModule { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.12", ngImport: i0, type: PopupModule, imports: [i1$1.ResizeSensorComponent, PopupComponent], exports: [PopupComponent] }); static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupModule, providers: [PopupService, ResizeBatchService], imports: [KENDO_RESIZESENSOR, KENDO_POPUP] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: PopupModule, decorators: [{ type: NgModule, args: [{ exports: [...KENDO_POPUP], imports: [...KENDO_RESIZESENSOR, ...KENDO_POPUP], providers: [PopupService, ResizeBatc