UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,002 lines (987 loc) 142 kB
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