@angular/cdk
Version:
Angular Material Component Development Kit
1,106 lines (1,093 loc) • 138 kB
JavaScript
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