@angular/cdk
Version:
Angular Material Component Development Kit
1,002 lines (987 loc) • 142 kB
JavaScript
import * as i0 from '@angular/core';
import { inject, NgZone, Injectable, RendererFactory2, Component, ChangeDetectionStrategy, ViewEncapsulation, untracked, afterRender, afterNextRender, ElementRef, Injector, ANIMATION_MODULE_TYPE, EnvironmentInjector, ApplicationRef, InjectionToken, Directive, EventEmitter, TemplateRef, ViewContainerRef, booleanAttribute, Input, Output, NgModule } from '@angular/core';
import { DOCUMENT, Location } from '@angular/common';
import { P as Platform } from './platform-20fc4de8.mjs';
import { _ as _bindEventWithOptions } from './backwards-compatibility-08253a84.mjs';
import { _ as _getEventTarget } from './shadow-dom-318658ae.mjs';
import { _ as _isTestEnvironment } from './test-environment-f6f8bc13.mjs';
import { _ as _CdkPrivateStyleLoader } from './style-loader-09eecacc.mjs';
import { Subject, Subscription, merge } from 'rxjs';
import { filter, takeUntil, takeWhile } from 'rxjs/operators';
import { c as coerceCssPixelValue } from './css-pixel-value-5d0cae55.mjs';
import { c as coerceArray } from './array-6239d2f8.mjs';
import { S as ScrollDispatcher, V as ViewportRuler, a as ScrollingModule } from './scrolling-module-722545e3.mjs';
import { s as supportsScrollBehavior } from './scrolling-59340c46.mjs';
import { D as DomPortalOutlet, T as TemplatePortal, P as PortalModule } from './portal-directives-dced6d68.mjs';
import { D as Directionality } from './directionality-9d44e426.mjs';
import { _ as _IdGenerator } from './id-generator-0b91c6f7.mjs';
import { e as ESCAPE } from './keycodes-0e4398c6.mjs';
import { h as hasModifierKey } from './modifiers-3e8908bb.mjs';
import { B as BidiModule } from './bidi-module-04c03e58.mjs';
const scrollBehaviorSupported = supportsScrollBehavior();
/**
* Strategy that will prevent the user from scrolling while the overlay is visible.
*/
class BlockScrollStrategy {
_viewportRuler;
_previousHTMLStyles = { top: '', left: '' };
_previousScrollPosition;
_isEnabled = false;
_document;
constructor(_viewportRuler, document) {
this._viewportRuler = _viewportRuler;
this._document = document;
}
/** Attaches this scroll strategy to an overlay. */
attach() { }
/** Blocks page-level scroll while the attached overlay is open. */
enable() {
if (this._canBeEnabled()) {
const root = this._document.documentElement;
this._previousScrollPosition = this._viewportRuler.getViewportScrollPosition();
// Cache the previous inline styles in case the user had set them.
this._previousHTMLStyles.left = root.style.left || '';
this._previousHTMLStyles.top = root.style.top || '';
// Note: we're using the `html` node, instead of the `body`, because the `body` may
// have the user agent margin, whereas the `html` is guaranteed not to have one.
root.style.left = coerceCssPixelValue(-this._previousScrollPosition.left);
root.style.top = coerceCssPixelValue(-this._previousScrollPosition.top);
root.classList.add('cdk-global-scrollblock');
this._isEnabled = true;
}
}
/** Unblocks page-level scroll while the attached overlay is open. */
disable() {
if (this._isEnabled) {
const html = this._document.documentElement;
const body = this._document.body;
const htmlStyle = html.style;
const bodyStyle = body.style;
const previousHtmlScrollBehavior = htmlStyle.scrollBehavior || '';
const previousBodyScrollBehavior = bodyStyle.scrollBehavior || '';
this._isEnabled = false;
htmlStyle.left = this._previousHTMLStyles.left;
htmlStyle.top = this._previousHTMLStyles.top;
html.classList.remove('cdk-global-scrollblock');
// Disable user-defined smooth scrolling temporarily while we restore the scroll position.
// See https://developer.mozilla.org/en-US/docs/Web/CSS/scroll-behavior
// Note that we don't mutate the property if the browser doesn't support `scroll-behavior`,
// because it can throw off feature detections in `supportsScrollBehavior` which
// checks for `'scrollBehavior' in documentElement.style`.
if (scrollBehaviorSupported) {
htmlStyle.scrollBehavior = bodyStyle.scrollBehavior = 'auto';
}
window.scroll(this._previousScrollPosition.left, this._previousScrollPosition.top);
if (scrollBehaviorSupported) {
htmlStyle.scrollBehavior = previousHtmlScrollBehavior;
bodyStyle.scrollBehavior = previousBodyScrollBehavior;
}
}
}
_canBeEnabled() {
// Since the scroll strategies can't be singletons, we have to use a global CSS class
// (`cdk-global-scrollblock`) to make sure that we don't try to disable global
// scrolling multiple times.
const html = this._document.documentElement;
if (html.classList.contains('cdk-global-scrollblock') || this._isEnabled) {
return false;
}
const body = this._document.body;
const viewport = this._viewportRuler.getViewportSize();
return body.scrollHeight > viewport.height || body.scrollWidth > viewport.width;
}
}
/**
* Returns an error to be thrown when attempting to attach an already-attached scroll strategy.
*/
function getMatScrollStrategyAlreadyAttachedError() {
return Error(`Scroll strategy has already been attached.`);
}
/**
* Strategy that will close the overlay as soon as the user starts scrolling.
*/
class CloseScrollStrategy {
_scrollDispatcher;
_ngZone;
_viewportRuler;
_config;
_scrollSubscription = null;
_overlayRef;
_initialScrollPosition;
constructor(_scrollDispatcher, _ngZone, _viewportRuler, _config) {
this._scrollDispatcher = _scrollDispatcher;
this._ngZone = _ngZone;
this._viewportRuler = _viewportRuler;
this._config = _config;
}
/** Attaches this scroll strategy to an overlay. */
attach(overlayRef) {
if (this._overlayRef && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw getMatScrollStrategyAlreadyAttachedError();
}
this._overlayRef = overlayRef;
}
/** Enables the closing of the attached overlay on scroll. */
enable() {
if (this._scrollSubscription) {
return;
}
const stream = this._scrollDispatcher.scrolled(0).pipe(filter(scrollable => {
return (!scrollable ||
!this._overlayRef.overlayElement.contains(scrollable.getElementRef().nativeElement));
}));
if (this._config && this._config.threshold && this._config.threshold > 1) {
this._initialScrollPosition = this._viewportRuler.getViewportScrollPosition().top;
this._scrollSubscription = stream.subscribe(() => {
const scrollPosition = this._viewportRuler.getViewportScrollPosition().top;
if (Math.abs(scrollPosition - this._initialScrollPosition) > this._config.threshold) {
this._detach();
}
else {
this._overlayRef.updatePosition();
}
});
}
else {
this._scrollSubscription = stream.subscribe(this._detach);
}
}
/** Disables the closing the attached overlay on scroll. */
disable() {
if (this._scrollSubscription) {
this._scrollSubscription.unsubscribe();
this._scrollSubscription = null;
}
}
detach() {
this.disable();
this._overlayRef = null;
}
/** Detaches the overlay ref and disables the scroll strategy. */
_detach = () => {
this.disable();
if (this._overlayRef.hasAttached()) {
this._ngZone.run(() => this._overlayRef.detach());
}
};
}
/** Scroll strategy that doesn't do anything. */
class NoopScrollStrategy {
/** Does nothing, as this scroll strategy is a no-op. */
enable() { }
/** Does nothing, as this scroll strategy is a no-op. */
disable() { }
/** Does nothing, as this scroll strategy is a no-op. */
attach() { }
}
/**
* Gets whether an element is scrolled outside of view by any of its parent scrolling containers.
* @param element Dimensions of the element (from getBoundingClientRect)
* @param scrollContainers Dimensions of element's scrolling containers (from getBoundingClientRect)
* @returns Whether the element is scrolled out of view
* @docs-private
*/
function isElementScrolledOutsideView(element, scrollContainers) {
return scrollContainers.some(containerBounds => {
const outsideAbove = element.bottom < containerBounds.top;
const outsideBelow = element.top > containerBounds.bottom;
const outsideLeft = element.right < containerBounds.left;
const outsideRight = element.left > containerBounds.right;
return outsideAbove || outsideBelow || outsideLeft || outsideRight;
});
}
/**
* Gets whether an element is clipped by any of its scrolling containers.
* @param element Dimensions of the element (from getBoundingClientRect)
* @param scrollContainers Dimensions of element's scrolling containers (from getBoundingClientRect)
* @returns Whether the element is clipped
* @docs-private
*/
function isElementClippedByScrolling(element, scrollContainers) {
return scrollContainers.some(scrollContainerRect => {
const clippedAbove = element.top < scrollContainerRect.top;
const clippedBelow = element.bottom > scrollContainerRect.bottom;
const clippedLeft = element.left < scrollContainerRect.left;
const clippedRight = element.right > scrollContainerRect.right;
return clippedAbove || clippedBelow || clippedLeft || clippedRight;
});
}
/**
* Strategy that will update the element position as the user is scrolling.
*/
class RepositionScrollStrategy {
_scrollDispatcher;
_viewportRuler;
_ngZone;
_config;
_scrollSubscription = null;
_overlayRef;
constructor(_scrollDispatcher, _viewportRuler, _ngZone, _config) {
this._scrollDispatcher = _scrollDispatcher;
this._viewportRuler = _viewportRuler;
this._ngZone = _ngZone;
this._config = _config;
}
/** Attaches this scroll strategy to an overlay. */
attach(overlayRef) {
if (this._overlayRef && (typeof ngDevMode === 'undefined' || ngDevMode)) {
throw getMatScrollStrategyAlreadyAttachedError();
}
this._overlayRef = overlayRef;
}
/** Enables repositioning of the attached overlay on scroll. */
enable() {
if (!this._scrollSubscription) {
const throttle = this._config ? this._config.scrollThrottle : 0;
this._scrollSubscription = this._scrollDispatcher.scrolled(throttle).subscribe(() => {
this._overlayRef.updatePosition();
// TODO(crisbeto): make `close` on by default once all components can handle it.
if (this._config && this._config.autoClose) {
const overlayRect = this._overlayRef.overlayElement.getBoundingClientRect();
const { width, height } = this._viewportRuler.getViewportSize();
// TODO(crisbeto): include all ancestor scroll containers here once
// we have a way of exposing the trigger element to the scroll strategy.
const parentRects = [{ width, height, bottom: height, right: width, top: 0, left: 0 }];
if (isElementScrolledOutsideView(overlayRect, parentRects)) {
this.disable();
this._ngZone.run(() => this._overlayRef.detach());
}
}
});
}
}
/** Disables repositioning of the attached overlay on scroll. */
disable() {
if (this._scrollSubscription) {
this._scrollSubscription.unsubscribe();
this._scrollSubscription = null;
}
}
detach() {
this.disable();
this._overlayRef = null;
}
}
/**
* Options for how an overlay will handle scrolling.
*
* Users can provide a custom value for `ScrollStrategyOptions` to replace the default
* behaviors. This class primarily acts as a factory for ScrollStrategy instances.
*/
class ScrollStrategyOptions {
_scrollDispatcher = inject(ScrollDispatcher);
_viewportRuler = inject(ViewportRuler);
_ngZone = inject(NgZone);
_document = inject(DOCUMENT);
constructor() { }
/** Do nothing on scroll. */
noop = () => new NoopScrollStrategy();
/**
* Close the overlay as soon as the user scrolls.
* @param config Configuration to be used inside the scroll strategy.
*/
close = (config) => new CloseScrollStrategy(this._scrollDispatcher, this._ngZone, this._viewportRuler, config);
/** Block scrolling. */
block = () => new BlockScrollStrategy(this._viewportRuler, this._document);
/**
* Update the overlay's position on scroll.
* @param config Configuration to be used inside the scroll strategy.
* Allows debouncing the reposition calls.
*/
reposition = (config) => new RepositionScrollStrategy(this._scrollDispatcher, this._viewportRuler, this._ngZone, config);
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ScrollStrategyOptions, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ScrollStrategyOptions, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: ScrollStrategyOptions, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/** Initial configuration used when creating an overlay. */
class OverlayConfig {
/** Strategy with which to position the overlay. */
positionStrategy;
/** Strategy to be used when handling scroll events while the overlay is open. */
scrollStrategy = new NoopScrollStrategy();
/** Custom class to add to the overlay pane. */
panelClass = '';
/** Whether the overlay has a backdrop. */
hasBackdrop = false;
/** Custom class to add to the backdrop */
backdropClass = 'cdk-overlay-dark-backdrop';
/** The width of the overlay panel. If a number is provided, pixel units are assumed. */
width;
/** The height of the overlay panel. If a number is provided, pixel units are assumed. */
height;
/** The min-width of the overlay panel. If a number is provided, pixel units are assumed. */
minWidth;
/** The min-height of the overlay panel. If a number is provided, pixel units are assumed. */
minHeight;
/** The max-width of the overlay panel. If a number is provided, pixel units are assumed. */
maxWidth;
/** The max-height of the overlay panel. If a number is provided, pixel units are assumed. */
maxHeight;
/**
* Direction of the text in the overlay panel. If a `Directionality` instance
* is passed in, the overlay will handle changes to its value automatically.
*/
direction;
/**
* Whether the overlay should be disposed of when the user goes backwards/forwards in history.
* Note that this usually doesn't include clicking on links (unless the user is using
* the `HashLocationStrategy`).
*/
disposeOnNavigation = false;
constructor(config) {
if (config) {
// Use `Iterable` instead of `Array` because TypeScript, as of 3.6.3,
// loses the array generic type in the `for of`. But we *also* have to use `Array` because
// typescript won't iterate over an `Iterable` unless you compile with `--downlevelIteration`
const configKeys = Object.keys(config);
for (const key of configKeys) {
if (config[key] !== undefined) {
// TypeScript, as of version 3.5, sees the left-hand-side of this expression
// as "I don't know *which* key this is, so the only valid value is the intersection
// of all the possible values." In this case, that happens to be `undefined`. TypeScript
// is not smart enough to see that the right-hand-side is actually an access of the same
// exact type with the same exact key, meaning that the value type must be identical.
// So we use `any` to work around this.
this[key] = config[key];
}
}
}
}
}
/** The points of the origin element and the overlay element to connect. */
class ConnectionPositionPair {
offsetX;
offsetY;
panelClass;
/** X-axis attachment point for connected overlay origin. Can be 'start', 'end', or 'center'. */
originX;
/** Y-axis attachment point for connected overlay origin. Can be 'top', 'bottom', or 'center'. */
originY;
/** X-axis attachment point for connected overlay. Can be 'start', 'end', or 'center'. */
overlayX;
/** Y-axis attachment point for connected overlay. Can be 'top', 'bottom', or 'center'. */
overlayY;
constructor(origin, overlay,
/** Offset along the X axis. */
offsetX,
/** Offset along the Y axis. */
offsetY,
/** Class(es) to be applied to the panel while this position is active. */
panelClass) {
this.offsetX = offsetX;
this.offsetY = offsetY;
this.panelClass = panelClass;
this.originX = origin.originX;
this.originY = origin.originY;
this.overlayX = overlay.overlayX;
this.overlayY = overlay.overlayY;
}
}
/**
* Set of properties regarding the position of the origin and overlay relative to the viewport
* with respect to the containing Scrollable elements.
*
* The overlay and origin are clipped if any part of their bounding client rectangle exceeds the
* bounds of any one of the strategy's Scrollable's bounding client rectangle.
*
* The overlay and origin are outside view if there is no overlap between their bounding client
* rectangle and any one of the strategy's Scrollable's bounding client rectangle.
*
* ----------- -----------
* | outside | | clipped |
* | view | --------------------------
* | | | | | |
* ---------- | ----------- |
* -------------------------- | |
* | | | Scrollable |
* | | | |
* | | --------------------------
* | Scrollable |
* | |
* --------------------------
*
* @docs-private
*/
class ScrollingVisibility {
isOriginClipped;
isOriginOutsideView;
isOverlayClipped;
isOverlayOutsideView;
}
/** The change event emitted by the strategy when a fallback position is used. */
class ConnectedOverlayPositionChange {
connectionPair;
scrollableViewProperties;
constructor(
/** The position used as a result of this change. */
connectionPair,
/** @docs-private */
scrollableViewProperties) {
this.connectionPair = connectionPair;
this.scrollableViewProperties = scrollableViewProperties;
}
}
/**
* Validates whether a vertical position property matches the expected values.
* @param property Name of the property being validated.
* @param value Value of the property being validated.
* @docs-private
*/
function validateVerticalPosition(property, value) {
if (value !== 'top' && value !== 'bottom' && value !== 'center') {
throw Error(`ConnectedPosition: Invalid ${property} "${value}". ` +
`Expected "top", "bottom" or "center".`);
}
}
/**
* Validates whether a horizontal position property matches the expected values.
* @param property Name of the property being validated.
* @param value Value of the property being validated.
* @docs-private
*/
function validateHorizontalPosition(property, value) {
if (value !== 'start' && value !== 'end' && value !== 'center') {
throw Error(`ConnectedPosition: Invalid ${property} "${value}". ` +
`Expected "start", "end" or "center".`);
}
}
/**
* Service for dispatching events that land on the body to appropriate overlay ref,
* if any. It maintains a list of attached overlays to determine best suited overlay based
* on event target and order of overlay opens.
*/
class BaseOverlayDispatcher {
/** Currently attached overlays in the order they were attached. */
_attachedOverlays = [];
_document = inject(DOCUMENT);
_isAttached;
constructor() { }
ngOnDestroy() {
this.detach();
}
/** Add a new overlay to the list of attached overlay refs. */
add(overlayRef) {
// Ensure that we don't get the same overlay multiple times.
this.remove(overlayRef);
this._attachedOverlays.push(overlayRef);
}
/** Remove an overlay from the list of attached overlay refs. */
remove(overlayRef) {
const index = this._attachedOverlays.indexOf(overlayRef);
if (index > -1) {
this._attachedOverlays.splice(index, 1);
}
// Remove the global listener once there are no more overlays.
if (this._attachedOverlays.length === 0) {
this.detach();
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BaseOverlayDispatcher, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BaseOverlayDispatcher, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: BaseOverlayDispatcher, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/**
* Service for dispatching keyboard events that land on the body to appropriate overlay ref,
* if any. It maintains a list of attached overlays to determine best suited overlay based
* on event target and order of overlay opens.
*/
class OverlayKeyboardDispatcher extends BaseOverlayDispatcher {
_ngZone = inject(NgZone);
_renderer = inject(RendererFactory2).createRenderer(null, null);
_cleanupKeydown;
/** Add a new overlay to the list of attached overlay refs. */
add(overlayRef) {
super.add(overlayRef);
// Lazily start dispatcher once first overlay is added
if (!this._isAttached) {
this._ngZone.runOutsideAngular(() => {
this._cleanupKeydown = this._renderer.listen('body', 'keydown', this._keydownListener);
});
this._isAttached = true;
}
}
/** Detaches the global keyboard event listener. */
detach() {
if (this._isAttached) {
this._cleanupKeydown?.();
this._isAttached = false;
}
}
/** Keyboard event listener that will be attached to the body. */
_keydownListener = (event) => {
const overlays = this._attachedOverlays;
for (let i = overlays.length - 1; i > -1; i--) {
// Dispatch the keydown event to the top overlay which has subscribers to its keydown events.
// We want to target the most recent overlay, rather than trying to match where the event came
// from, because some components might open an overlay, but keep focus on a trigger element
// (e.g. for select and autocomplete). We skip overlays without keydown event subscriptions,
// because we don't want overlays that don't handle keyboard events to block the ones below
// them that do.
if (overlays[i]._keydownEvents.observers.length > 0) {
this._ngZone.run(() => overlays[i]._keydownEvents.next(event));
break;
}
}
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayKeyboardDispatcher, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayKeyboardDispatcher, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayKeyboardDispatcher, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/**
* Service for dispatching mouse click events that land on the body to appropriate overlay ref,
* if any. It maintains a list of attached overlays to determine best suited overlay based
* on event target and order of overlay opens.
*/
class OverlayOutsideClickDispatcher extends BaseOverlayDispatcher {
_platform = inject(Platform);
_ngZone = inject(NgZone);
_renderer = inject(RendererFactory2).createRenderer(null, null);
_cursorOriginalValue;
_cursorStyleIsSet = false;
_pointerDownEventTarget;
_cleanups;
/** Add a new overlay to the list of attached overlay refs. */
add(overlayRef) {
super.add(overlayRef);
// Safari on iOS does not generate click events for non-interactive
// elements. However, we want to receive a click for any element outside
// the overlay. We can force a "clickable" state by setting
// `cursor: pointer` on the document body. See:
// https://developer.mozilla.org/en-US/docs/Web/API/Element/click_event#Safari_Mobile
// https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/HandlingEvents/HandlingEvents.html
if (!this._isAttached) {
const body = this._document.body;
const eventOptions = { capture: true };
this._cleanups = this._ngZone.runOutsideAngular(() => [
_bindEventWithOptions(this._renderer, body, 'pointerdown', this._pointerDownListener, eventOptions),
_bindEventWithOptions(this._renderer, body, 'click', this._clickListener, eventOptions),
_bindEventWithOptions(this._renderer, body, 'auxclick', this._clickListener, eventOptions),
_bindEventWithOptions(this._renderer, body, 'contextmenu', this._clickListener, eventOptions),
]);
// click event is not fired on iOS. To make element "clickable" we are
// setting the cursor to pointer
if (this._platform.IOS && !this._cursorStyleIsSet) {
this._cursorOriginalValue = body.style.cursor;
body.style.cursor = 'pointer';
this._cursorStyleIsSet = true;
}
this._isAttached = true;
}
}
/** Detaches the global keyboard event listener. */
detach() {
if (this._isAttached) {
this._cleanups?.forEach(cleanup => cleanup());
this._cleanups = undefined;
if (this._platform.IOS && this._cursorStyleIsSet) {
this._document.body.style.cursor = this._cursorOriginalValue;
this._cursorStyleIsSet = false;
}
this._isAttached = false;
}
}
/** Store pointerdown event target to track origin of click. */
_pointerDownListener = (event) => {
this._pointerDownEventTarget = _getEventTarget(event);
};
/** Click event listener that will be attached to the body propagate phase. */
_clickListener = (event) => {
const target = _getEventTarget(event);
// In case of a click event, we want to check the origin of the click
// (e.g. in case where a user starts a click inside the overlay and
// releases the click outside of it).
// This is done by using the event target of the preceding pointerdown event.
// Every click event caused by a pointer device has a preceding pointerdown
// event, unless the click was programmatically triggered (e.g. in a unit test).
const origin = event.type === 'click' && this._pointerDownEventTarget
? this._pointerDownEventTarget
: target;
// Reset the stored pointerdown event target, to avoid having it interfere
// in subsequent events.
this._pointerDownEventTarget = null;
// We copy the array because the original may be modified asynchronously if the
// outsidePointerEvents listener decides to detach overlays resulting in index errors inside
// the for loop.
const overlays = this._attachedOverlays.slice();
// Dispatch the mouse event to the top overlay which has subscribers to its mouse events.
// We want to target all overlays for which the click could be considered as outside click.
// As soon as we reach an overlay for which the click is not outside click we break off
// the loop.
for (let i = overlays.length - 1; i > -1; i--) {
const overlayRef = overlays[i];
if (overlayRef._outsidePointerEvents.observers.length < 1 || !overlayRef.hasAttached()) {
continue;
}
// If it's a click inside the overlay, just break - we should do nothing
// If it's an outside click (both origin and target of the click) dispatch the mouse event,
// and proceed with the next overlay
if (containsPierceShadowDom(overlayRef.overlayElement, target) ||
containsPierceShadowDom(overlayRef.overlayElement, origin)) {
break;
}
const outsidePointerEvents = overlayRef._outsidePointerEvents;
/** @breaking-change 14.0.0 _ngZone will be required. */
if (this._ngZone) {
this._ngZone.run(() => outsidePointerEvents.next(event));
}
else {
outsidePointerEvents.next(event);
}
}
};
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayOutsideClickDispatcher, deps: null, target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayOutsideClickDispatcher, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayOutsideClickDispatcher, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
/** Version of `Element.contains` that transcends shadow DOM boundaries. */
function containsPierceShadowDom(parent, child) {
const supportsShadowRoot = typeof ShadowRoot !== 'undefined' && ShadowRoot;
let current = child;
while (current) {
if (current === parent) {
return true;
}
current =
supportsShadowRoot && current instanceof ShadowRoot ? current.host : current.parentNode;
}
return false;
}
class _CdkOverlayStyleLoader {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: _CdkOverlayStyleLoader, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.0", type: _CdkOverlayStyleLoader, isStandalone: true, selector: "ng-component", host: { attributes: { "cdk-overlay-style-loader": "" } }, ngImport: i0, template: '', isInline: true, styles: [".cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed}@layer cdk-overlay{.cdk-overlay-container{z-index:1000}}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute}@layer cdk-overlay{.cdk-global-overlay-wrapper{z-index:1000}}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;display:flex;max-width:100%;max-height:100%}@layer cdk-overlay{.cdk-overlay-pane{z-index:1000}}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:auto;-webkit-tap-highlight-color:rgba(0,0,0,0);opacity:0}@layer cdk-overlay{.cdk-overlay-backdrop{z-index:1000;transition:opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}}.cdk-overlay-backdrop-showing{opacity:1}@media(forced-colors: active){.cdk-overlay-backdrop-showing{opacity:.6}}@layer cdk-overlay{.cdk-overlay-dark-backdrop{background:rgba(0,0,0,.32)}}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing,.cdk-high-contrast-active .cdk-overlay-transparent-backdrop{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;display:flex;flex-direction:column;min-width:1px;min-height:1px}@layer cdk-overlay{.cdk-overlay-connected-position-bounding-box{z-index:1000}}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: _CdkOverlayStyleLoader, decorators: [{
type: Component,
args: [{ template: '', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, host: { 'cdk-overlay-style-loader': '' }, styles: [".cdk-overlay-container,.cdk-global-overlay-wrapper{pointer-events:none;top:0;left:0;height:100%;width:100%}.cdk-overlay-container{position:fixed}@layer cdk-overlay{.cdk-overlay-container{z-index:1000}}.cdk-overlay-container:empty{display:none}.cdk-global-overlay-wrapper{display:flex;position:absolute}@layer cdk-overlay{.cdk-global-overlay-wrapper{z-index:1000}}.cdk-overlay-pane{position:absolute;pointer-events:auto;box-sizing:border-box;display:flex;max-width:100%;max-height:100%}@layer cdk-overlay{.cdk-overlay-pane{z-index:1000}}.cdk-overlay-backdrop{position:absolute;top:0;bottom:0;left:0;right:0;pointer-events:auto;-webkit-tap-highlight-color:rgba(0,0,0,0);opacity:0}@layer cdk-overlay{.cdk-overlay-backdrop{z-index:1000;transition:opacity 400ms cubic-bezier(0.25, 0.8, 0.25, 1)}}.cdk-overlay-backdrop-showing{opacity:1}@media(forced-colors: active){.cdk-overlay-backdrop-showing{opacity:.6}}@layer cdk-overlay{.cdk-overlay-dark-backdrop{background:rgba(0,0,0,.32)}}.cdk-overlay-transparent-backdrop{transition:visibility 1ms linear,opacity 1ms linear;visibility:hidden;opacity:1}.cdk-overlay-transparent-backdrop.cdk-overlay-backdrop-showing,.cdk-high-contrast-active .cdk-overlay-transparent-backdrop{opacity:0;visibility:visible}.cdk-overlay-backdrop-noop-animation{transition:none}.cdk-overlay-connected-position-bounding-box{position:absolute;display:flex;flex-direction:column;min-width:1px;min-height:1px}@layer cdk-overlay{.cdk-overlay-connected-position-bounding-box{z-index:1000}}.cdk-global-scrollblock{position:fixed;width:100%;overflow-y:scroll}"] }]
}] });
/** Container inside which all overlays will render. */
class OverlayContainer {
_platform = inject(Platform);
_containerElement;
_document = inject(DOCUMENT);
_styleLoader = inject(_CdkPrivateStyleLoader);
constructor() { }
ngOnDestroy() {
this._containerElement?.remove();
}
/**
* This method returns the overlay container element. It will lazily
* create the element the first time it is called to facilitate using
* the container in non-browser environments.
* @returns the container element
*/
getContainerElement() {
this._loadStyles();
if (!this._containerElement) {
this._createContainer();
}
return this._containerElement;
}
/**
* Create the overlay container element, which is simply a div
* with the 'cdk-overlay-container' class on the document body.
*/
_createContainer() {
const containerClass = 'cdk-overlay-container';
// TODO(crisbeto): remove the testing check once we have an overlay testing
// module or Angular starts tearing down the testing `NgModule`. See:
// https://github.com/angular/angular/issues/18831
if (this._platform.isBrowser || _isTestEnvironment()) {
const oppositePlatformContainers = this._document.querySelectorAll(`.${containerClass}[platform="server"], ` + `.${containerClass}[platform="test"]`);
// Remove any old containers from the opposite platform.
// This can happen when transitioning from the server to the client.
for (let i = 0; i < oppositePlatformContainers.length; i++) {
oppositePlatformContainers[i].remove();
}
}
const container = this._document.createElement('div');
container.classList.add(containerClass);
// A long time ago we kept adding new overlay containers whenever a new app was instantiated,
// but at some point we added logic which clears the duplicate ones in order to avoid leaks.
// The new logic was a little too aggressive since it was breaking some legitimate use cases.
// To mitigate the problem we made it so that only containers from a different platform are
// cleared, but the side-effect was that people started depending on the overly-aggressive
// logic to clean up their tests for them. Until we can introduce an overlay-specific testing
// module which does the cleanup, we try to detect that we're in a test environment and we
// always clear the container. See #17006.
// TODO(crisbeto): remove the test environment check once we have an overlay testing module.
if (_isTestEnvironment()) {
container.setAttribute('platform', 'test');
}
else if (!this._platform.isBrowser) {
container.setAttribute('platform', 'server');
}
this._document.body.appendChild(container);
this._containerElement = container;
}
/** Loads the structural styles necessary for the overlay to work. */
_loadStyles() {
this._styleLoader.load(_CdkOverlayStyleLoader);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayContainer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayContainer, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.0", ngImport: i0, type: OverlayContainer, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
/** Encapsulates the logic for attaching and detaching a backdrop. */
class BackdropRef {
_renderer;
_ngZone;
element;
_cleanupClick;
_cleanupTransitionEnd;
_fallbackTimeout;
constructor(document, _renderer, _ngZone, onClick) {
this._renderer = _renderer;
this._ngZone = _ngZone;
this.element = document.createElement('div');
this.element.classList.add('cdk-overlay-backdrop');
this._cleanupClick = _renderer.listen(this.element, 'click', onClick);
}
detach() {
this._ngZone.runOutsideAngular(() => {
const element = this.element;
clearTimeout(this._fallbackTimeout);
this._cleanupTransitionEnd?.();
this._cleanupTransitionEnd = this._renderer.listen(element, 'transitionend', this.dispose);
this._fallbackTimeout = setTimeout(this.dispose, 500);
// If the backdrop doesn't have a transition, the `transitionend` event won't fire.
// In this case we make it unclickable and we try to remove it after a delay.
element.style.pointerEvents = 'none';
element.classList.remove('cdk-overlay-backdrop-showing');
});
}
dispose = () => {
clearTimeout(this._fallbackTimeout);
this._cleanupClick?.();
this._cleanupTransitionEnd?.();
this._cleanupClick = this._cleanupTransitionEnd = this._fallbackTimeout = undefined;
this.element.remove();
};
}
/**
* Reference to an overlay that has been created with the Overlay service.
* Used to manipulate or dispose of said overlay.
*/
class OverlayRef {
_portalOutlet;
_host;
_pane;
_config;
_ngZone;
_keyboardDispatcher;
_document;
_location;
_outsideClickDispatcher;
_animationsDisabled;
_injector;
_renderer;
_backdropClick = new Subject();
_attachments = new Subject();
_detachments = new Subject();
_positionStrategy;
_scrollStrategy;
_locationChanges = Subscription.EMPTY;
_backdropRef = null;
/**
* Reference to the parent of the `_host` at the time it was detached. Used to restore
* the `_host` to its original position in the DOM when it gets re-attached.
*/
_previousHostParent;
/** Stream of keydown events dispatched to this overlay. */
_keydownEvents = new Subject();
/** Stream of mouse outside events dispatched to this overlay. */
_outsidePointerEvents = new Subject();
_renders = new Subject();
_afterRenderRef;
/** Reference to the currently-running `afterNextRender` call. */
_afterNextRenderRef;
constructor(_portalOutlet, _host, _pane, _config, _ngZone, _keyboardDispatcher, _document, _location, _outsideClickDispatcher, _animationsDisabled = false, _injector, _renderer) {
this._portalOutlet = _portalOutlet;
this._host = _host;
this._pane = _pane;
this._config = _config;
this._ngZone = _ngZone;
this._keyboardDispatcher = _keyboardDispatcher;
this._document = _document;
this._location = _location;
this._outsideClickDispatcher = _outsideClickDispatcher;
this._animationsDisabled = _animationsDisabled;
this._injector = _injector;
this._renderer = _renderer;
if (_config.scrollStrategy) {
this._scrollStrategy = _config.scrollStrategy;
this._scrollStrategy.attach(this);
}
this._positionStrategy = _config.positionStrategy;
// Users could open the overlay from an `effect`, in which case we need to
// run the `afterRender` as `untracked`. We don't recommend that users do
// this, but we also don't want to break users who are doing it.
this._afterRenderRef = untracked(() => afterRender(() => {
this._renders.next();
}, { injector: this._injector }));
}
/** The overlay's HTML element */
get overlayElement() {
return this._pane;
}
/** The overlay's backdrop HTML element. */
get backdropElement() {
return this._backdropRef?.element || null;
}
/**
* Wrapper around the panel element. Can be used for advanced
* positioning where a wrapper with specific styling is
* required around the overlay pane.
*/
get hostElement() {
return this._host;
}
/**
* Attaches content, given via a Portal, to the overlay.
* If the overlay is configured to have a backdrop, it will be created.
*
* @param portal Portal instance to which to attach the overlay.
* @returns The portal attachment result.
*/
attach(portal) {
// Insert the host into the DOM before attaching the portal, otherwise
// the animations module will skip animations on repeat attachments.
if (!this._host.parentElement && this._previousHostParent) {
this._previousHostParent.appendChild(this._host);
}
const attachResult = this._portalOutlet.attach(portal);
if (this._positionStrategy) {
this._positionStrategy.attach(this);
}
this._updateStackingOrder();
this._updateElementSize();
this._updateElementDirection();
if (this._scrollStrategy) {
this._scrollStrategy.enable();
}
// We need to clean this up ourselves, because we're passing in an
// `EnvironmentInjector` below which won't ever be destroyed.
// Otherwise it causes some callbacks to be retained (see #29696).
this._afterNextRenderRef?.destroy();
// Update the position once the overlay is fully rendered before attempting to position it,
// as the position may depend on the size of the rendered content.
this._afterNextRenderRef = afterNextRender(() => {
// The overlay could've been detached before the callback executed.
if (this.hasAttached()) {
this.updatePosition();
}
}, { injector: this._injector });
// Enable pointer events for the overlay pane element.
this._togglePointerEvents(true);
if (this._config.hasBackdrop) {
this._attachBackdrop();
}
if (this._config.panelClass) {
this._toggleClasses(this._pane, this._config.panelClass, true);
}
// Only emit the `attachments` event once all other setup is done.
this._attachments.next();
// Track this overlay by the keyboard dispatcher
this._keyboardDispatcher.add(this);
if (this._config.disposeOnNavigation) {
this._locationChanges = this._location.subscribe(() => this.dispose());
}
this._outsideClickDispatcher.add(this);
// TODO(crisbeto): the null check is here, because the portal outlet returns `any`.
// We should be guaranteed for the result to be `ComponentRef | EmbeddedViewRef`, but
// `instanceof EmbeddedViewRef` doesn't appear to work at the moment.
if (typeof attachResult?.onDestroy === 'function') {
// In most cases we control the portal and we know when it is being detached so that
// we can finish the disposal process. The exception is if the user passes in a custom
// `ViewContainerRef` that isn't destroyed through the overlay API. Note that we use
// `detach` here instead of `dispose`, because we don't know if the user intends to
// reattach the overlay at a later point. It also has the advantage of waiting for animations.
attachResult.onDestroy(() => {
if (this.hasAttached()) {
// We have to delay the `detach` call, because detaching immediately prevents
// other destroy hooks from running. This is likely a framework bug similar to
// https://github.com/angular/angular/issues/46119
this._ngZone.runOutsideAngular(() => Promise.resolve().then(() => this.detach()));
}
});
}
return attachResult;
}
/**
* Detaches an overlay from a portal.
* @returns The portal detachment result.
*/
detach() {
if (!this.hasAttached()) {
return;
}
this.detachBackdrop();
// When the overlay is detached, the pane element should disable pointer events.
// This is necessary because otherwise the pane element will cover the page and disable
// pointer events therefore. Depends on the position strategy and the applied pane boundaries.
this._togglePointerEvents(false);
if (this._positionStrategy && this._positionStrategy.detach) {
this._positionStrategy.detach();
}
if (this._scrollStrategy) {
this._scrollStrategy.disable();
}
const detachmentResult = this._portalOutlet.detach();
// Only emit after everything is detached.
this._detachments.next();
// Remove this overlay from keyboard dispatcher tracking.
this._keyboardDispatcher.remove(this);
// Keeping the host element in the DOM can cause scroll jank, because it still gets
// rendered, even though it's transparent and unclickable which is why we remove it.
this._detachContentWhenEmpty();
this._locationChanges.unsubscribe();
this._outsideClickDispatcher.remove(this);
return detachmentResult;
}
/** Cleans up the overlay from the DOM. */
dispose() {
const isAttached = this.hasAttached();
if (this._positionStrategy) {
this._positionStrategy.dispose();
}
this._disposeScrollStrategy();
this._backdropRef?.dispose();
this._locationChanges.unsubscribe();
this._keyboardDispatcher.remove(this);
this._portalOutlet.dispose();
this._attachments.complete();
this._backdropClick.complete();
this._keydownEvents.complete();
this._outsidePointerEvents.complete();
this._outsideClickDispatcher.remove(this);
this._host?.remove();
this._afterNextRenderRef?.destroy();
this._previousHostParent = this._pane = this._host = this._backdropRef = null;
if (isAttached) {
this._detachments.next();
}
this._detachments.complete();
this._afterRenderRef.destroy();
this._renders.complete();
}
/** Whether the overlay has attached content. */
hasAttached() {
return this._portalOutlet.hasAttached();
}
/** Gets an observable that emits when the backdrop has been clicked. */
backd