UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,103 lines (1,090 loc) 143 kB
import { __values, __assign, __extends } from 'tslib'; import { ScrollDispatcher, ViewportRuler, ScrollingModule } from '@angular/cdk/scrolling'; export { CdkScrollable, ScrollDispatcher, ViewportRuler } from '@angular/cdk/scrolling'; import { DOCUMENT, Location } from '@angular/common'; import { Injectable, NgZone, Inject, ɵɵdefineInjectable, ɵɵinject, Optional, SkipSelf, ElementRef, ApplicationRef, ComponentFactoryResolver, Injector, InjectionToken, Directive, EventEmitter, TemplateRef, ViewContainerRef, Input, Output, NgModule } from '@angular/core'; import { coerceCssPixelValue, coerceArray, coerceBooleanProperty } from '@angular/cdk/coercion'; import { Directionality, BidiModule } from '@angular/cdk/bidi'; import { DomPortalOutlet, TemplatePortal, PortalModule } from '@angular/cdk/portal'; import { Platform } from '@angular/cdk/platform'; import { Subject, Subscription, Observable, merge } from 'rxjs'; import { take, takeUntil } from 'rxjs/operators'; import { ESCAPE, hasModifierKey } from '@angular/cdk/keycodes'; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Strategy that will prevent the user from scrolling while the overlay is visible. */ var BlockScrollStrategy = /** @class */ (function () { function BlockScrollStrategy(_viewportRuler, document) { this._viewportRuler = _viewportRuler; this._previousHTMLStyles = { top: '', left: '' }; this._isEnabled = false; this._document = document; } /** Attaches this scroll strategy to an overlay. */ BlockScrollStrategy.prototype.attach = function () { }; /** Blocks page-level scroll while the attached overlay is open. */ BlockScrollStrategy.prototype.enable = function () { if (this._canBeEnabled()) { var 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. */ BlockScrollStrategy.prototype.disable = function () { if (this._isEnabled) { var html = this._document.documentElement; var body = this._document.body; var htmlStyle = html.style; var bodyStyle = body.style; var previousHtmlScrollBehavior = htmlStyle.scrollBehavior || ''; var 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 htmlStyle.scrollBehavior = bodyStyle.scrollBehavior = 'auto'; window.scroll(this._previousScrollPosition.left, this._previousScrollPosition.top); htmlStyle.scrollBehavior = previousHtmlScrollBehavior; bodyStyle.scrollBehavior = previousBodyScrollBehavior; } }; BlockScrollStrategy.prototype._canBeEnabled = function () { // 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. var html = this._document.documentElement; if (html.classList.contains('cdk-global-scrollblock') || this._isEnabled) { return false; } var body = this._document.body; var viewport = this._viewportRuler.getViewportSize(); return body.scrollHeight > viewport.height || body.scrollWidth > viewport.width; }; return BlockScrollStrategy; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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. */ var CloseScrollStrategy = /** @class */ (function () { function CloseScrollStrategy(_scrollDispatcher, _ngZone, _viewportRuler, _config) { var _this = this; this._scrollDispatcher = _scrollDispatcher; this._ngZone = _ngZone; this._viewportRuler = _viewportRuler; this._config = _config; this._scrollSubscription = null; /** Detaches the overlay ref and disables the scroll strategy. */ this._detach = function () { _this.disable(); if (_this._overlayRef.hasAttached()) { _this._ngZone.run(function () { return _this._overlayRef.detach(); }); } }; } /** Attaches this scroll strategy to an overlay. */ CloseScrollStrategy.prototype.attach = function (overlayRef) { if (this._overlayRef) { throw getMatScrollStrategyAlreadyAttachedError(); } this._overlayRef = overlayRef; }; /** Enables the closing of the attached overlay on scroll. */ CloseScrollStrategy.prototype.enable = function () { var _this = this; if (this._scrollSubscription) { return; } var stream = this._scrollDispatcher.scrolled(0); if (this._config && this._config.threshold && this._config.threshold > 1) { this._initialScrollPosition = this._viewportRuler.getViewportScrollPosition().top; this._scrollSubscription = stream.subscribe(function () { var 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. */ CloseScrollStrategy.prototype.disable = function () { if (this._scrollSubscription) { this._scrollSubscription.unsubscribe(); this._scrollSubscription = null; } }; CloseScrollStrategy.prototype.detach = function () { this.disable(); this._overlayRef = null; }; return CloseScrollStrategy; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** Scroll strategy that doesn't do anything. */ var NoopScrollStrategy = /** @class */ (function () { function NoopScrollStrategy() { } /** Does nothing, as this scroll strategy is a no-op. */ NoopScrollStrategy.prototype.enable = function () { }; /** Does nothing, as this scroll strategy is a no-op. */ NoopScrollStrategy.prototype.disable = function () { }; /** Does nothing, as this scroll strategy is a no-op. */ NoopScrollStrategy.prototype.attach = function () { }; return NoopScrollStrategy; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ // TODO(jelbourn): move this to live with the rest of the scrolling code // TODO(jelbourn): someday replace this with IntersectionObservers /** * 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(function (containerBounds) { var outsideAbove = element.bottom < containerBounds.top; var outsideBelow = element.top > containerBounds.bottom; var outsideLeft = element.right < containerBounds.left; var 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(function (scrollContainerRect) { var clippedAbove = element.top < scrollContainerRect.top; var clippedBelow = element.bottom > scrollContainerRect.bottom; var clippedLeft = element.left < scrollContainerRect.left; var clippedRight = element.right > scrollContainerRect.right; return clippedAbove || clippedBelow || clippedLeft || clippedRight; }); } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Strategy that will update the element position as the user is scrolling. */ var RepositionScrollStrategy = /** @class */ (function () { function RepositionScrollStrategy(_scrollDispatcher, _viewportRuler, _ngZone, _config) { this._scrollDispatcher = _scrollDispatcher; this._viewportRuler = _viewportRuler; this._ngZone = _ngZone; this._config = _config; this._scrollSubscription = null; } /** Attaches this scroll strategy to an overlay. */ RepositionScrollStrategy.prototype.attach = function (overlayRef) { if (this._overlayRef) { throw getMatScrollStrategyAlreadyAttachedError(); } this._overlayRef = overlayRef; }; /** Enables repositioning of the attached overlay on scroll. */ RepositionScrollStrategy.prototype.enable = function () { var _this = this; if (!this._scrollSubscription) { var throttle = this._config ? this._config.scrollThrottle : 0; this._scrollSubscription = this._scrollDispatcher.scrolled(throttle).subscribe(function () { _this._overlayRef.updatePosition(); // TODO(crisbeto): make `close` on by default once all components can handle it. if (_this._config && _this._config.autoClose) { var overlayRect = _this._overlayRef.overlayElement.getBoundingClientRect(); var _a = _this._viewportRuler.getViewportSize(), width = _a.width, height = _a.height; // TODO(crisbeto): include all ancestor scroll containers here once // we have a way of exposing the trigger element to the scroll strategy. var parentRects = [{ width: width, height: height, bottom: height, right: width, top: 0, left: 0 }]; if (isElementScrolledOutsideView(overlayRect, parentRects)) { _this.disable(); _this._ngZone.run(function () { return _this._overlayRef.detach(); }); } } }); } }; /** Disables repositioning of the attached overlay on scroll. */ RepositionScrollStrategy.prototype.disable = function () { if (this._scrollSubscription) { this._scrollSubscription.unsubscribe(); this._scrollSubscription = null; } }; RepositionScrollStrategy.prototype.detach = function () { this.disable(); this._overlayRef = null; }; return RepositionScrollStrategy; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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. */ var ScrollStrategyOptions = /** @class */ (function () { function ScrollStrategyOptions(_scrollDispatcher, _viewportRuler, _ngZone, document) { var _this = this; this._scrollDispatcher = _scrollDispatcher; this._viewportRuler = _viewportRuler; this._ngZone = _ngZone; /** Do nothing on scroll. */ this.noop = function () { return new NoopScrollStrategy(); }; /** * Close the overlay as soon as the user scrolls. * @param config Configuration to be used inside the scroll strategy. */ this.close = function (config) { return new CloseScrollStrategy(_this._scrollDispatcher, _this._ngZone, _this._viewportRuler, config); }; /** Block scrolling. */ this.block = function () { return 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. */ this.reposition = function (config) { return new RepositionScrollStrategy(_this._scrollDispatcher, _this._viewportRuler, _this._ngZone, config); }; this._document = document; } ScrollStrategyOptions.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ ScrollStrategyOptions.ctorParameters = function () { return [ { type: ScrollDispatcher }, { type: ViewportRuler }, { type: NgZone }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; }; ScrollStrategyOptions.ɵprov = ɵɵdefineInjectable({ factory: function ScrollStrategyOptions_Factory() { return new ScrollStrategyOptions(ɵɵinject(ScrollDispatcher), ɵɵinject(ViewportRuler), ɵɵinject(NgZone), ɵɵinject(DOCUMENT)); }, token: ScrollStrategyOptions, providedIn: "root" }); return ScrollStrategyOptions; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** Initial configuration used when creating an overlay. */ var OverlayConfig = /** @class */ (function () { function OverlayConfig(config) { var e_1, _a; /** Strategy to be used when handling scroll events while the overlay is open. */ this.scrollStrategy = new NoopScrollStrategy(); /** Custom class to add to the overlay pane. */ this.panelClass = ''; /** Whether the overlay has a backdrop. */ this.hasBackdrop = false; /** Custom class to add to the backdrop */ this.backdropClass = 'cdk-overlay-dark-backdrop'; /** * 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`). */ this.disposeOnNavigation = false; 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` var configKeys = Object.keys(config); try { for (var configKeys_1 = __values(configKeys), configKeys_1_1 = configKeys_1.next(); !configKeys_1_1.done; configKeys_1_1 = configKeys_1.next()) { var key = configKeys_1_1.value; 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 posible 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]; } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (configKeys_1_1 && !configKeys_1_1.done && (_a = configKeys_1.return)) _a.call(configKeys_1); } finally { if (e_1) throw e_1.error; } } } } return OverlayConfig; }()); /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** The points of the origin element and the overlay element to connect. */ var ConnectionPositionPair = /** @class */ (function () { function ConnectionPositionPair(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; } return ConnectionPositionPair; }()); /** * 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 */ var ScrollingVisibility = /** @class */ (function () { function ScrollingVisibility() { } return ScrollingVisibility; }()); /** The change event emitted by the strategy when a fallback position is used. */ var ConnectedOverlayPositionChange = /** @class */ (function () { function ConnectedOverlayPositionChange( /** The position used as a result of this change. */ connectionPair, /** @docs-private */ scrollableViewProperties) { this.connectionPair = connectionPair; this.scrollableViewProperties = scrollableViewProperties; } /** @nocollapse */ ConnectedOverlayPositionChange.ctorParameters = function () { return [ { type: ConnectionPositionPair }, { type: ScrollingVisibility, decorators: [{ type: Optional }] } ]; }; return ConnectedOverlayPositionChange; }()); /** * 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\"."); } } /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * 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. */ var OverlayKeyboardDispatcher = /** @class */ (function () { function OverlayKeyboardDispatcher(document) { var _this = this; /** Currently attached overlays in the order they were attached. */ this._attachedOverlays = []; /** Keyboard event listener that will be attached to the body. */ this._keydownListener = function (event) { var overlays = _this._attachedOverlays; for (var 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]._keydownEventSubscriptions > 0) { overlays[i]._keydownEvents.next(event); break; } } }; this._document = document; } OverlayKeyboardDispatcher.prototype.ngOnDestroy = function () { this._detach(); }; /** Add a new overlay to the list of attached overlay refs. */ OverlayKeyboardDispatcher.prototype.add = function (overlayRef) { // Ensure that we don't get the same overlay multiple times. this.remove(overlayRef); // Lazily start dispatcher once first overlay is added if (!this._isAttached) { this._document.body.addEventListener('keydown', this._keydownListener); this._isAttached = true; } this._attachedOverlays.push(overlayRef); }; /** Remove an overlay from the list of attached overlay refs. */ OverlayKeyboardDispatcher.prototype.remove = function (overlayRef) { var 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(); } }; /** Detaches the global keyboard event listener. */ OverlayKeyboardDispatcher.prototype._detach = function () { if (this._isAttached) { this._document.body.removeEventListener('keydown', this._keydownListener); this._isAttached = false; } }; OverlayKeyboardDispatcher.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ OverlayKeyboardDispatcher.ctorParameters = function () { return [ { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] } ]; }; OverlayKeyboardDispatcher.ɵprov = ɵɵdefineInjectable({ factory: function OverlayKeyboardDispatcher_Factory() { return new OverlayKeyboardDispatcher(ɵɵinject(DOCUMENT)); }, token: OverlayKeyboardDispatcher, providedIn: "root" }); return OverlayKeyboardDispatcher; }()); /** @docs-private @deprecated @breaking-change 8.0.0 */ function OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY(dispatcher, _document) { return dispatcher || new OverlayKeyboardDispatcher(_document); } /** @docs-private @deprecated @breaking-change 8.0.0 */ var OVERLAY_KEYBOARD_DISPATCHER_PROVIDER = { // If there is already an OverlayKeyboardDispatcher available, use that. // Otherwise, provide a new one. provide: OverlayKeyboardDispatcher, deps: [ [new Optional(), new SkipSelf(), OverlayKeyboardDispatcher], // Coerce to `InjectionToken` so that the `deps` match the "shape" // of the type expected by Angular DOCUMENT ], useFactory: OVERLAY_KEYBOARD_DISPATCHER_PROVIDER_FACTORY }; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Whether we're in a testing environment. * TODO(crisbeto): remove this once we have an overlay testing module. */ var isTestEnvironment = typeof window !== 'undefined' && !!window && !!(window.__karma__ || window.jasmine); /** Container inside which all overlays will render. */ var OverlayContainer = /** @class */ (function () { function OverlayContainer(document, /** * @deprecated `platform` parameter to become required. * @breaking-change 10.0.0 */ _platform) { this._platform = _platform; this._document = document; } OverlayContainer.prototype.ngOnDestroy = function () { var container = this._containerElement; if (container && container.parentNode) { container.parentNode.removeChild(container); } }; /** * 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 */ OverlayContainer.prototype.getContainerElement = function () { 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. */ OverlayContainer.prototype._createContainer = function () { // @breaking-change 10.0.0 Remove null check for `_platform`. var isBrowser = this._platform ? this._platform.isBrowser : typeof window !== 'undefined'; var containerClass = 'cdk-overlay-container'; if (isBrowser || isTestEnvironment) { var 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 (var i = 0; i < oppositePlatformContainers.length; i++) { oppositePlatformContainers[i].parentNode.removeChild(oppositePlatformContainers[i]); } } var 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 (!isBrowser) { container.setAttribute('platform', 'server'); } this._document.body.appendChild(container); this._containerElement = container; }; OverlayContainer.decorators = [ { type: Injectable, args: [{ providedIn: 'root' },] } ]; /** @nocollapse */ OverlayContainer.ctorParameters = function () { return [ { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT,] }] }, { type: Platform } ]; }; OverlayContainer.ɵprov = ɵɵdefineInjectable({ factory: function OverlayContainer_Factory() { return new OverlayContainer(ɵɵinject(DOCUMENT), ɵɵinject(Platform)); }, token: OverlayContainer, providedIn: "root" }); return OverlayContainer; }()); /** @docs-private @deprecated @breaking-change 8.0.0 */ function OVERLAY_CONTAINER_PROVIDER_FACTORY(parentContainer, _document) { return parentContainer || new OverlayContainer(_document); } /** @docs-private @deprecated @breaking-change 8.0.0 */ var OVERLAY_CONTAINER_PROVIDER = { // If there is already an OverlayContainer available, use that. Otherwise, provide a new one. provide: OverlayContainer, deps: [ [new Optional(), new SkipSelf(), OverlayContainer], DOCUMENT // We need to use the InjectionToken somewhere to keep TS happy ], useFactory: OVERLAY_CONTAINER_PROVIDER_FACTORY }; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ /** * Reference to an overlay that has been created with the Overlay service. * Used to manipulate or dispose of said overlay. */ var OverlayRef = /** @class */ (function () { function OverlayRef(_portalOutlet, _host, _pane, _config, _ngZone, _keyboardDispatcher, _document, // @breaking-change 8.0.0 `_location` parameter to be made required. _location) { var _this = this; 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._backdropElement = null; this._backdropClick = new Subject(); this._attachments = new Subject(); this._detachments = new Subject(); this._locationChanges = Subscription.EMPTY; this._backdropClickHandler = function (event) { return _this._backdropClick.next(event); }; this._keydownEventsObservable = new Observable(function (observer) { var subscription = _this._keydownEvents.subscribe(observer); _this._keydownEventSubscriptions++; return function () { subscription.unsubscribe(); _this._keydownEventSubscriptions--; }; }); /** Stream of keydown events dispatched to this overlay. */ this._keydownEvents = new Subject(); /** Amount of subscriptions to the keydown events. */ this._keydownEventSubscriptions = 0; if (_config.scrollStrategy) { this._scrollStrategy = _config.scrollStrategy; this._scrollStrategy.attach(this); } this._positionStrategy = _config.positionStrategy; } Object.defineProperty(OverlayRef.prototype, "overlayElement", { /** The overlay's HTML element */ get: function () { return this._pane; }, enumerable: true, configurable: true }); Object.defineProperty(OverlayRef.prototype, "backdropElement", { /** The overlay's backdrop HTML element. */ get: function () { return this._backdropElement; }, enumerable: true, configurable: true }); Object.defineProperty(OverlayRef.prototype, "hostElement", { /** * Wrapper around the panel element. Can be used for advanced * positioning where a wrapper with specific styling is * required around the overlay pane. */ get: function () { return this._host; }, enumerable: true, configurable: true }); /** * 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. */ OverlayRef.prototype.attach = function (portal) { var _this = this; var attachResult = this._portalOutlet.attach(portal); if (this._positionStrategy) { this._positionStrategy.attach(this); } // Update the pane element with the given configuration. if (!this._host.parentElement && this._previousHostParent) { this._previousHostParent.appendChild(this._host); } this._updateStackingOrder(); this._updateElementSize(); this._updateElementDirection(); if (this._scrollStrategy) { this._scrollStrategy.enable(); } // Update the position once the zone is stable so that the overlay will be fully rendered // before attempting to position it, as the position may depend on the size of the rendered // content. this._ngZone.onStable .asObservable() .pipe(take(1)) .subscribe(function () { // The overlay could've been detached before the zone has stabilized. if (_this.hasAttached()) { _this.updatePosition(); } }); // 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); // @breaking-change 8.0.0 remove the null check for `_location` // once the constructor parameter is made required. if (this._config.disposeOnNavigation && this._location) { this._locationChanges = this._location.subscribe(function () { return _this.dispose(); }); } return attachResult; }; /** * Detaches an overlay from a portal. * @returns The portal detachment result. */ OverlayRef.prototype.detach = function () { 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(); } var 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._detachContentWhenStable(); // Stop listening for location changes. this._locationChanges.unsubscribe(); return detachmentResult; }; /** Cleans up the overlay from the DOM. */ OverlayRef.prototype.dispose = function () { var isAttached = this.hasAttached(); if (this._positionStrategy) { this._positionStrategy.dispose(); } this._disposeScrollStrategy(); this.detachBackdrop(); this._locationChanges.unsubscribe(); this._keyboardDispatcher.remove(this); this._portalOutlet.dispose(); this._attachments.complete(); this._backdropClick.complete(); this._keydownEvents.complete(); if (this._host && this._host.parentNode) { this._host.parentNode.removeChild(this._host); this._host = null; } this._previousHostParent = this._pane = null; if (isAttached) { this._detachments.next(); } this._detachments.complete(); }; /** Whether the overlay has attached content. */ OverlayRef.prototype.hasAttached = function () { return this._portalOutlet.hasAttached(); }; /** Gets an observable that emits when the backdrop has been clicked. */ OverlayRef.prototype.backdropClick = function () { return this._backdropClick.asObservable(); }; /** Gets an observable that emits when the overlay has been attached. */ OverlayRef.prototype.attachments = function () { return this._attachments.asObservable(); }; /** Gets an observable that emits when the overlay has been detached. */ OverlayRef.prototype.detachments = function () { return this._detachments.asObservable(); }; /** Gets an observable of keydown events targeted to this overlay. */ OverlayRef.prototype.keydownEvents = function () { return this._keydownEventsObservable; }; /** Gets the current overlay configuration, which is immutable. */ OverlayRef.prototype.getConfig = function () { return this._config; }; /** Updates the position of the overlay based on the position strategy. */ OverlayRef.prototype.updatePosition = function () { if (this._positionStrategy) { this._positionStrategy.apply(); } }; /** Switches to a new position strategy and updates the overlay position. */ OverlayRef.prototype.updatePositionStrategy = function (strategy) { if (strategy === this._positionStrategy) { return; } if (this._positionStrategy) { this._positionStrategy.dispose(); } this._positionStrategy = strategy; if (this.hasAttached()) { strategy.attach(this); this.updatePosition(); } }; /** Update the size properties of the overlay. */ OverlayRef.prototype.updateSize = function (sizeConfig) { this._config = __assign(__assign({}, this._config), sizeConfig); this._updateElementSize(); }; /** Sets the LTR/RTL direction for the overlay. */ OverlayRef.prototype.setDirection = function (dir) { this._config = __assign(__assign({}, this._config), { direction: dir }); this._updateElementDirection(); }; /** Add a CSS class or an array of classes to the overlay pane. */ OverlayRef.prototype.addPanelClass = function (classes) { if (this._pane) { this._toggleClasses(this._pane, classes, true); } }; /** Remove a CSS class or an array of classes from the overlay pane. */ OverlayRef.prototype.removePanelClass = function (classes) { if (this._pane) { this._toggleClasses(this._pane, classes, false); } }; /** * Returns the layout direction of the overlay panel. */ OverlayRef.prototype.getDirection = function () { var direction = this._config.direction; if (!direction) { return 'ltr'; } return typeof direction === 'string' ? direction : direction.value; }; /** Switches to a new scroll strategy. */ OverlayRef.prototype.updateScrollStrategy = function (strategy) { if (strategy === this._scrollStrategy) { return; } this._disposeScrollStrategy(); this._scrollStrategy = strategy; if (this.hasAttached()) { strategy.attach(this); strategy.enable(); } }; /** Updates the text direction of the overlay panel. */ OverlayRef.prototype._updateElementDirection = function () { this._host.setAttribute('dir', this.getDirection()); }; /** Updates the size of the overlay element based on the overlay config. */ OverlayRef.prototype._updateElementSize = function () { if (!this._pane) { return; } var style = this._pane.style; style.width = coerceCssPixelValue(this._config.width); style.height = coerceCssPixelValue(this._config.height); style.minWidth = coerceCssPixelValue(this._config.minWidth); style.minHeight = coerceCssPixelValue(this._config.minHeight); style.maxWidth = coerceCssPixelValue(this._config.maxWidth); style.maxHeight = coerceCssPixelValue(this._config.maxHeight); }; /** Toggles the pointer events for the overlay pane element. */ OverlayRef.prototype._togglePointerEvents = function (enablePointer) { this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none'; }; /** Attaches a backdrop for this overlay. */ OverlayRef.prototype._attachBackdrop = function () { var _this = this; var showingClass = 'cdk-overlay-backdrop-showing'; this._backdropElement = this._document.createElement('div'); this._backdropElement.classList.add('cdk-overlay-backdrop'); if (this._config.backdropClass) { this._toggleClasses(this._backdropElement, this._config.backdropClass, true); } // Insert the backdrop before the pane in the DOM order, // in order to handle stacked overlays properly. this._host.parentElement.insertBefore(this._backdropElement, this._host); // Forward backdrop clicks such that the consumer of the overlay can perform whatever // action desired when such a click occurs (usually closing the overlay). this._backdropElement.addEventListener('click', this._backdropClickHandler); // Add class to fade-in the backdrop after one frame. if (typeof requestAnimationFrame !== 'undefined') { this._ngZone.runOutsideAngular(function () { requestAnimationFrame(function () { if (_this._backdropElement) { _this._backdropElement.classList.add(showingClass); } }); }); } else { this._backdropElement.classList.add(showingClass); } }; /** * Updates the stacking order of the element, moving it to the top if necessary. * This is required in cases where one overlay was detached, while another one, * that should be behind it, was destroyed. The next time both of them are opened, * the stacking will be wrong, because the detached element's pane will still be * in its original DOM position. */ OverlayRef.prototype._updateStackingOrder = function () { if (this._host.nextSibling) { this._host.parentNode.appendChild(this._host); } }; /** Detaches the backdrop (if any) associated with the overlay. */ OverlayRef.prototype.detachBackdrop = function () { var _this = this; var backdropToDetach = this._backdropElement; if (!backdropToDetach) { return; } var timeoutId; var finishDetach = function () { // It may not be attached to anything in certain cases (e.g. unit tests). if (backdropToDetach) { backdropToDetach.removeEventListener('click', _this._backdropClickHandler); backdropToDetach.removeEventListener('transitionend', finishDetach); if (backdropToDetach.parentNode) { backdropToDetach.parentNode.removeChild(backdropToDetach); } } // It is possible that a new portal has been attached to this overlay since we started // removing the backdrop. If that is the case, only clear the backdrop reference if it // is still the same instance that we started to remove. if (_this._backdropElement == backdropToDetach) { _this._backdropElement = null; } if (_this._config.backdropClass) { _this._toggleClasses(backdropToDetach, _this._config.backdropClass, false); } clearTimeout(timeoutId); }; backdropToDetach.classList.remove('cdk-overlay-backdrop-showing'); this._ngZone.runOutsideAngular(function () { backdropToDetach.addEventListener('transitionend', finishDetach); }); // 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. backdropToDetach.style.pointerEvents = 'none'; // Run this outside the Angular zone because there's nothing that Angular cares about. // If it were to run inside the Angular zone, every test that used Overlay would have to be // either async or fakeAsync. timeoutId = this._ngZone.runOutsideAngular(function () { return setTimeout(finishDetach, 500); }); }; /** Toggles a single CSS class or an array of classes on an element. */ OverlayRef.prototype._toggleClasses = function (element, cssClasses, isAdd) { var classList = element.classList; coerceArray(cssClasses).forEach(function (cssClass) { // We can't do a spread here, because IE doesn't support setting multiple classes. // Also trying to add an empty string to a DOMTokenList will throw. if (cssClass) { isAdd ? classList.add(cssClass) : classList.remove(cssClass); } }); }; /** Detaches the overlay content next time the zone stabilizes. */ OverlayRef.prototype._detachContentWhenStable = function () { var _this = this; // Normally we wouldn't have to explicitly run this outside the `NgZone`, however // if the consumer is using `zone-patch-rxjs`, the `Subscription.unsubscribe` call will // be patched to run inside the zone, which will throw us into an infinite loop. this._ngZone.runOutsideAngular(function () { // We can't remove the host here immediately, because the overlay pane's content // might still be animating. This stream helps us avoid interrupting the animation // by waiting for the pane to become empty. var subscription = _this._ngZone.onStable .asObservable() .pipe(takeUntil(merge(_this._attachments, _this._detachments))) .subscribe(function () { // Needs a couple of checks for the pane and host, because // they may have been removed by the time the zone stabilizes. if (!_this._pane || !_this._host || _this._pane.children.length === 0) { if (_this._pane && _this._config.panelClass) { _this._toggleClasses(_this._pane, _this._config.panelClass, false); } if (_this._host && _t