@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
JavaScript
/**-----------------------------------------------------------------------------------------
* 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