UNPKG

@angular/cdk

Version:

Angular Material Component Development Kit

1,106 lines (1,093 loc) 138 kB
import * as i1 from '@angular/cdk/scrolling'; import { ScrollingModule } from '@angular/cdk/scrolling'; export { CdkScrollable, ScrollDispatcher, ViewportRuler } from '@angular/cdk/scrolling'; import * as i6 from '@angular/common'; import { DOCUMENT } from '@angular/common'; import * as i0 from '@angular/core'; import { Injectable, Inject, ElementRef, ApplicationRef, InjectionToken, Directive, EventEmitter, Optional, Input, Output, NgModule } from '@angular/core'; import { coerceCssPixelValue, coerceArray, coerceBooleanProperty } from '@angular/cdk/coercion'; import * as i1$1 from '@angular/cdk/platform'; import { supportsScrollBehavior, _isTestEnvironment, _getEventTarget } from '@angular/cdk/platform'; import * as i5 from '@angular/cdk/bidi'; import { BidiModule } from '@angular/cdk/bidi'; import { DomPortalOutlet, TemplatePortal, PortalModule } from '@angular/cdk/portal'; import { Subject, Subscription, merge } from 'rxjs'; import { take, takeUntil, takeWhile } 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 */ const scrollBehaviorSupported = supportsScrollBehavior(); /** * Strategy that will prevent the user from scrolling while the overlay is visible. */ class BlockScrollStrategy { constructor(_viewportRuler, document) { this._viewportRuler = _viewportRuler; this._previousHTMLStyles = { top: '', left: '' }; this._isEnabled = false; 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; } } /** * @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. */ class CloseScrollStrategy { constructor(_scrollDispatcher, _ngZone, _viewportRuler, _config) { 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 = () => { this.disable(); if (this._overlayRef.hasAttached()) { this._ngZone.run(() => this._overlayRef.detach()); } }; } /** 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); 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; } } /** * @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. */ 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() { } } /** * @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 */ /** * 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; }); } /** * @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. */ class RepositionScrollStrategy { constructor(_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. */ 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 { constructor(_scrollDispatcher, _viewportRuler, _ngZone, document) { this._scrollDispatcher = _scrollDispatcher; this._viewportRuler = _viewportRuler; this._ngZone = _ngZone; /** Do nothing on scroll. */ this.noop = () => new NoopScrollStrategy(); /** * Close the overlay as soon as the user scrolls. * @param config Configuration to be used inside the scroll strategy. */ this.close = (config) => new CloseScrollStrategy(this._scrollDispatcher, this._ngZone, this._viewportRuler, config); /** Block scrolling. */ this.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. */ this.reposition = (config) => new RepositionScrollStrategy(this._scrollDispatcher, this._viewportRuler, this._ngZone, config); this._document = document; } } ScrollStrategyOptions.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: ScrollStrategyOptions, deps: [{ token: i1.ScrollDispatcher }, { token: i1.ViewportRuler }, { token: i0.NgZone }, { token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); ScrollStrategyOptions.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: ScrollStrategyOptions, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: ScrollStrategyOptions, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: i1.ScrollDispatcher }, { type: i1.ViewportRuler }, { type: i0.NgZone }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }]; } }); /** * @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. */ class OverlayConfig { constructor(config) { /** 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` 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 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]; } } } } } /** * @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. */ class ConnectionPositionPair { 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 { } /** The change event emitted by the strategy when a fallback position is used. */ class ConnectedOverlayPositionChange { 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".`); } } /** * @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. */ class OverlayRef { constructor(_portalOutlet, _host, _pane, _config, _ngZone, _keyboardDispatcher, _document, _location, _outsideClickDispatcher) { 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._backdropElement = null; this._backdropClick = new Subject(); this._attachments = new Subject(); this._detachments = new Subject(); this._locationChanges = Subscription.EMPTY; this._backdropClickHandler = (event) => this._backdropClick.next(event); /** Stream of keydown events dispatched to this overlay. */ this._keydownEvents = new Subject(); /** Stream of mouse outside events dispatched to this overlay. */ this._outsidePointerEvents = new Subject(); if (_config.scrollStrategy) { this._scrollStrategy = _config.scrollStrategy; this._scrollStrategy.attach(this); } this._positionStrategy = _config.positionStrategy; } /** The overlay's HTML element */ get overlayElement() { return this._pane; } /** The overlay's backdrop HTML element. */ get backdropElement() { return this._backdropElement; } /** * 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) { let attachResult = this._portalOutlet.attach(portal); // Update the pane element with the given configuration. if (!this._host.parentElement && this._previousHostParent) { this._previousHostParent.appendChild(this._host); } if (this._positionStrategy) { this._positionStrategy.attach(this); } 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.pipe(take(1)).subscribe(() => { // 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); if (this._config.disposeOnNavigation) { this._locationChanges = this._location.subscribe(() => this.dispose()); } this._outsideClickDispatcher.add(this); 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._detachContentWhenStable(); this._locationChanges.unsubscribe(); this._outsideClickDispatcher.remove(this); return detachmentResult; } /** Cleans up the overlay from the DOM. */ dispose() { var _a; const isAttached = this.hasAttached(); if (this._positionStrategy) { this._positionStrategy.dispose(); } this._disposeScrollStrategy(); this._disposeBackdrop(this._backdropElement); 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); (_a = this._host) === null || _a === void 0 ? void 0 : _a.remove(); this._previousHostParent = this._pane = this._host = null; if (isAttached) { this._detachments.next(); } this._detachments.complete(); } /** Whether the overlay has attached content. */ hasAttached() { return this._portalOutlet.hasAttached(); } /** Gets an observable that emits when the backdrop has been clicked. */ backdropClick() { return this._backdropClick; } /** Gets an observable that emits when the overlay has been attached. */ attachments() { return this._attachments; } /** Gets an observable that emits when the overlay has been detached. */ detachments() { return this._detachments; } /** Gets an observable of keydown events targeted to this overlay. */ keydownEvents() { return this._keydownEvents; } /** Gets an observable of pointer events targeted outside this overlay. */ outsidePointerEvents() { return this._outsidePointerEvents; } /** Gets the current overlay configuration, which is immutable. */ getConfig() { return this._config; } /** Updates the position of the overlay based on the position strategy. */ updatePosition() { if (this._positionStrategy) { this._positionStrategy.apply(); } } /** Switches to a new position strategy and updates the overlay position. */ updatePositionStrategy(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. */ updateSize(sizeConfig) { this._config = Object.assign(Object.assign({}, this._config), sizeConfig); this._updateElementSize(); } /** Sets the LTR/RTL direction for the overlay. */ setDirection(dir) { this._config = Object.assign(Object.assign({}, this._config), { direction: dir }); this._updateElementDirection(); } /** Add a CSS class or an array of classes to the overlay pane. */ addPanelClass(classes) { if (this._pane) { this._toggleClasses(this._pane, classes, true); } } /** Remove a CSS class or an array of classes from the overlay pane. */ removePanelClass(classes) { if (this._pane) { this._toggleClasses(this._pane, classes, false); } } /** * Returns the layout direction of the overlay panel. */ getDirection() { const direction = this._config.direction; if (!direction) { return 'ltr'; } return typeof direction === 'string' ? direction : direction.value; } /** Switches to a new scroll strategy. */ updateScrollStrategy(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. */ _updateElementDirection() { this._host.setAttribute('dir', this.getDirection()); } /** Updates the size of the overlay element based on the overlay config. */ _updateElementSize() { if (!this._pane) { return; } const 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. */ _togglePointerEvents(enablePointer) { this._pane.style.pointerEvents = enablePointer ? '' : 'none'; } /** Attaches a backdrop for this overlay. */ _attachBackdrop() { const 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(() => { requestAnimationFrame(() => { 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. */ _updateStackingOrder() { if (this._host.nextSibling) { this._host.parentNode.appendChild(this._host); } } /** Detaches the backdrop (if any) associated with the overlay. */ detachBackdrop() { const backdropToDetach = this._backdropElement; if (!backdropToDetach) { return; } let timeoutId; const finishDetach = () => { // 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); this._disposeBackdrop(backdropToDetach); } if (this._config.backdropClass) { this._toggleClasses(backdropToDetach, this._config.backdropClass, false); } clearTimeout(timeoutId); }; backdropToDetach.classList.remove('cdk-overlay-backdrop-showing'); this._ngZone.runOutsideAngular(() => { 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(() => setTimeout(finishDetach, 500)); } /** Toggles a single CSS class or an array of classes on an element. */ _toggleClasses(element, cssClasses, isAdd) { const classes = coerceArray(cssClasses || []).filter(c => !!c); if (classes.length) { isAdd ? element.classList.add(...classes) : element.classList.remove(...classes); } } /** Detaches the overlay content next time the zone stabilizes. */ _detachContentWhenStable() { // 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(() => { // 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. const subscription = this._ngZone.onStable .pipe(takeUntil(merge(this._attachments, this._detachments))) .subscribe(() => { // 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 && this._host.parentElement) { this._previousHostParent = this._host.parentElement; this._host.remove(); } subscription.unsubscribe(); } }); }); } /** Disposes of a scroll strategy. */ _disposeScrollStrategy() { const scrollStrategy = this._scrollStrategy; if (scrollStrategy) { scrollStrategy.disable(); if (scrollStrategy.detach) { scrollStrategy.detach(); } } } /** Removes a backdrop element from the DOM. */ _disposeBackdrop(backdrop) { if (backdrop) { backdrop.remove(); // 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 === backdrop) { this._backdropElement = null; } } } } /** * @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 */ /** Container inside which all overlays will render. */ class OverlayContainer { constructor(document, _platform) { this._platform = _platform; this._document = document; } ngOnDestroy() { var _a; (_a = this._containerElement) === null || _a === void 0 ? void 0 : _a.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() { 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; } } OverlayContainer.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: OverlayContainer, deps: [{ token: DOCUMENT }, { token: i1$1.Platform }], target: i0.ɵɵFactoryTarget.Injectable }); OverlayContainer.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: OverlayContainer, providedIn: 'root' }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.2.0", ngImport: i0, type: OverlayContainer, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: i1$1.Platform }]; } }); /** * @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: refactor clipping detection into a separate thing (part of scrolling module) // TODO: doesn't handle both flexible width and height when it has to scroll along both axis. /** Class to be added to the overlay bounding box. */ const boundingBoxClass = 'cdk-overlay-connected-position-bounding-box'; /** Regex used to split a string on its CSS units. */ const cssUnitPattern = /([A-Za-z%]+)$/; /** * A strategy for positioning overlays. Using this strategy, an overlay is given an * implicit position relative some origin element. The relative position is defined in terms of * a point on the origin element that is connected to a point on the overlay element. For example, * a basic dropdown is connecting the bottom-left corner of the origin to the top-left corner * of the overlay. */ class FlexibleConnectedPositionStrategy { constructor(connectedTo, _viewportRuler, _document, _platform, _overlayContainer) { this._viewportRuler = _viewportRuler; this._document = _document; this._platform = _platform; this._overlayContainer = _overlayContainer; /** Last size used for the bounding box. Used to avoid resizing the overlay after open. */ this._lastBoundingBoxSize = { width: 0, height: 0 }; /** Whether the overlay was pushed in a previous positioning. */ this._isPushed = false; /** Whether the overlay can be pushed on-screen on the initial open. */ this._canPush = true; /** Whether the overlay can grow via flexible width/height after the initial open. */ this._growAfterOpen = false; /** Whether the overlay's width and height can be constrained to fit within the viewport. */ this._hasFlexibleDimensions = true; /** Whether the overlay position is locked. */ this._positionLocked = false; /** Amount of space that must be maintained between the overlay and the edge of the viewport. */ this._viewportMargin = 0; /** The Scrollable containers used to check scrollable view properties on position change. */ this._scrollables = []; /** Ordered list of preferred positions, from most to least desirable. */ this._preferredPositions = []; /** Subject that emits whenever the position changes. */ this._positionChanges = new Subject(); /** Subscription to viewport size changes. */ this._resizeSubscription = Subscription.EMPTY; /** Default offset for the overlay along the x axis. */ this._offsetX = 0; /** Default offset for the overlay along the y axis. */ this._offsetY = 0; /** Keeps track of the CSS classes that the position strategy has applied on the overlay panel. */ this._appliedPanelClasses = []; /** Observable sequence of position changes. */ this.positionChanges = this._positionChanges; this.setOrigin(connectedTo); } /** Ordered list of preferred positions, from most to least desirable. */ get positions() { return this._preferredPositions; } /** Attaches this position strategy to an overlay. */ attach(overlayRef) { if (this._overlayRef && overlayRef !== this._overlayRef && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw Error('This position strategy is already attached to an overlay'); } this._validatePositions(); overlayRef.hostElement.classList.add(boundingBoxClass); this._overlayRef = overlayRef; this._boundingBox = overlayRef.hostElement; this._pane = overlayRef.overlayElement; this._isDisposed = false; this._isInitialRender = true; this._lastPosition = null; this._resizeSubscription.unsubscribe(); this._resizeSubscription = this._viewportRuler.change().subscribe(() => { // When the window is resized, we want to trigger the next reposition as if it // was an initial render, in order for the strategy to pick a new optimal position, // otherwise position locking will cause it to stay at the old one. this._isInitialRender = true; this.apply(); }); } /** * Updates the position of the overlay element, using whichever preferred position relative * to the origin best fits on-screen. * * The selection of a position goes as follows: * - If any positions fit completely within the viewport as-is, * choose the first position that does so. * - If flexible dimensions are enabled and at least one satifies the given minimum width/height, * choose the position with the greatest available size modified by the positions' weight. * - If pushing is enabled, take the position that went off-screen the least and push it * on-screen. * - If none of the previous criteria were met, use the position that goes off-screen the least. * @docs-private */ apply() { // We shouldn't do anything if the strategy was disposed or we're on the server. if (this._isDisposed || !this._platform.isBrowser) { return; } // If the position has been applied already (e.g. when the overlay was opened) and the // consumer opted into locking in the position, re-use the old position, in order to // prevent the overlay from jumping around. if (!this._isInitialRender && this._positionLocked && this._lastPosition) { this.reapplyLastPosition(); return; } this._clearPanelClasses(); this._resetOverlayElementStyles(); this._resetBoundingBoxStyles(); // We need the bounding rects for the origin, the overlay and the container to determine how to position // the overlay relative to the origin. // We use the viewport rect to determine whether a position would go off-screen. this._viewportRect = this._getNarrowedViewportRect(); this._originRect = this._getOriginRect(); this._overlayRect = this._pane.getBoundingClientRect(); this._containerRect = this._overlayContainer.getContainerElement().getBoundingClientRect(); const originRect = this._originRect; const overlayRect = this._overlayRect; const viewportRect = this._viewportRect; const containerRect = this._containerRect; // Positions where the overlay will fit with flexible dimensions. const flexibleFits = []; // Fallback if none of the preferred positions fit within the viewport. let fallback; // Go through each of the preferred positions looking for a good fit. // If a good fit is found, it will be applied immediately. for (let pos of this._preferredPositions) { // Get the exact (x, y) coordinate for the point-of-origin on the origin element. let originPoint = this._getOriginPoint(originRect, containerRect, pos); // From that point-of-origin, get the exact (x, y) coordinate for the top-left corner of the // overlay in this position. We use the top-left corner for calculations and later translate // this into an appropriate (top, left, bottom, right) style. let overlayPoint = this._getOverlayPoint(originPoint, overlayRect, pos); // Calculate how well the overlay would fit into the viewport with this point. let overlayFit = this._getOverlayFit(overlayPoint, overlayRect, viewportRect, pos); // If the overlay, without any further work, fits into the viewport, use this position. if (overlayFit.isCompletelyWithinViewport) { this._isPushed = false; this._applyPosition(pos, originPoint); return; } // If the overlay has flexible dimensions, we can use this position // so long as there's enough space for the minimum dimensions. if (this._canFitWithFlexibleDimensions(overlayFit, overlayPoint, viewportRect)) { // Save positions where the overlay will fit with flexible dimensions. We will use these // if none of the positions fit *without* flexible dimensions. flexibleFits.push({ position: pos, origin: originPoint, overlayRect, boundingBoxRect: this._calculateBoundingBoxRect(originPoint, pos), }); continue; } // If the current preferred position d