ipsos-components
Version:
Material Design components for Angular
355 lines (285 loc) • 12 kB
text/typescript
/**
* @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
*/
import {Direction} from '@angular/cdk/bidi';
import {ComponentPortal, Portal, PortalOutlet, TemplatePortal} from '@angular/cdk/portal';
import {ComponentRef, EmbeddedViewRef, NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {take} from 'rxjs/operators/take';
import {Subject} from 'rxjs/Subject';
import {OverlayKeyboardDispatcher} from './keyboard/overlay-keyboard-dispatcher';
import {OverlayConfig} from './overlay-config';
/** An object where all of its properties cannot be written. */
export type ImmutableObject<T> = {
readonly [P in keyof T]: T[P];
};
/**
* Reference to an overlay that has been created with the Overlay service.
* Used to manipulate or dispose of said overlay.
*/
export class OverlayRef implements PortalOutlet {
private _backdropElement: HTMLElement | null = null;
private _backdropClick: Subject<any> = new Subject();
private _attachments = new Subject<void>();
private _detachments = new Subject<void>();
/** Stream of keydown events dispatched to this overlay. */
_keydownEvents = new Subject<KeyboardEvent>();
constructor(
private _portalOutlet: PortalOutlet,
private _pane: HTMLElement,
private _config: ImmutableObject<OverlayConfig>,
private _ngZone: NgZone,
private _keyboardDispatcher: OverlayKeyboardDispatcher) {
if (_config.scrollStrategy) {
_config.scrollStrategy.attach(this);
}
}
/** The overlay's HTML element */
get overlayElement(): HTMLElement {
return this._pane;
}
attach<T>(portal: ComponentPortal<T>): ComponentRef<T>;
attach<T>(portal: TemplatePortal<T>): EmbeddedViewRef<T>;
attach(portal: any): any;
/**
* 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: Portal<any>): any {
let attachResult = this._portalOutlet.attach(portal);
if (this._config.positionStrategy) {
this._config.positionStrategy.attach(this);
}
// Update the pane element with the given configuration.
this._updateStackingOrder();
this._updateElementSize();
this._updateElementDirection();
if (this._config.scrollStrategy) {
this._config.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(() => {
this.updatePosition();
});
// Enable pointer events for the overlay pane element.
this._togglePointerEvents(true);
if (this._config.hasBackdrop) {
this._attachBackdrop();
}
if (this._config.panelClass) {
// We can't do a spread here, because IE doesn't support setting multiple classes.
if (Array.isArray(this._config.panelClass)) {
this._config.panelClass.forEach(cls => this._pane.classList.add(cls));
} else {
this._pane.classList.add(this._config.panelClass);
}
}
// 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);
return attachResult;
}
/**
* Detaches an overlay from a portal.
* @returns The portal detachment result.
*/
detach(): any {
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._config.positionStrategy && this._config.positionStrategy.detach) {
this._config.positionStrategy.detach();
}
if (this._config.scrollStrategy) {
this._config.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);
return detachmentResult;
}
/** Cleans up the overlay from the DOM. */
dispose(): void {
const isAttached = this.hasAttached();
if (this._config.positionStrategy) {
this._config.positionStrategy.dispose();
}
if (this._config.scrollStrategy) {
this._config.scrollStrategy.disable();
}
this.detachBackdrop();
this._keyboardDispatcher.remove(this);
this._portalOutlet.dispose();
this._attachments.complete();
this._backdropClick.complete();
this._keydownEvents.complete();
if (isAttached) {
this._detachments.next();
}
this._detachments.complete();
}
/** Whether the overlay has attached content. */
hasAttached(): boolean {
return this._portalOutlet.hasAttached();
}
/** Gets an observable that emits when the backdrop has been clicked. */
backdropClick(): Observable<void> {
return this._backdropClick.asObservable();
}
/** Gets an observable that emits when the overlay has been attached. */
attachments(): Observable<void> {
return this._attachments.asObservable();
}
/** Gets an observable that emits when the overlay has been detached. */
detachments(): Observable<void> {
return this._detachments.asObservable();
}
/** Gets an observable of keydown events targeted to this overlay. */
keydownEvents(): Observable<KeyboardEvent> {
return this._keydownEvents.asObservable();
}
/** Gets the the current overlay configuration, which is immutable. */
getConfig(): OverlayConfig {
return this._config;
}
/** Updates the position of the overlay based on the position strategy. */
updatePosition() {
if (this._config.positionStrategy) {
this._config.positionStrategy.apply();
}
}
/** Update the size properties of the overlay. */
updateSize(sizeConfig: OverlaySizeConfig) {
this._config = {...this._config, ...sizeConfig};
this._updateElementSize();
}
/** Sets the LTR/RTL direction for the overlay. */
setDirection(dir: Direction) {
this._config = {...this._config, direction: dir};
this._updateElementDirection();
}
/** Updates the text direction of the overlay panel. */
private _updateElementDirection() {
this._pane.setAttribute('dir', this._config.direction!);
}
/** Updates the size of the overlay element based on the overlay config. */
private _updateElementSize() {
if (this._config.width || this._config.width === 0) {
this._pane.style.width = formatCssUnit(this._config.width);
}
if (this._config.height || this._config.height === 0) {
this._pane.style.height = formatCssUnit(this._config.height);
}
if (this._config.minWidth || this._config.minWidth === 0) {
this._pane.style.minWidth = formatCssUnit(this._config.minWidth);
}
if (this._config.minHeight || this._config.minHeight === 0) {
this._pane.style.minHeight = formatCssUnit(this._config.minHeight);
}
if (this._config.maxWidth || this._config.maxWidth === 0) {
this._pane.style.maxWidth = formatCssUnit(this._config.maxWidth);
}
if (this._config.maxHeight || this._config.maxHeight === 0) {
this._pane.style.maxHeight = formatCssUnit(this._config.maxHeight);
}
}
/** Toggles the pointer events for the overlay pane element. */
private _togglePointerEvents(enablePointer: boolean) {
this._pane.style.pointerEvents = enablePointer ? 'auto' : 'none';
}
/** Attaches a backdrop for this overlay. */
private _attachBackdrop() {
this._backdropElement = document.createElement('div');
this._backdropElement.classList.add('cdk-overlay-backdrop');
if (this._config.backdropClass) {
this._backdropElement.classList.add(this._config.backdropClass);
}
// Insert the backdrop before the pane in the DOM order,
// in order to handle stacked overlays properly.
this._pane.parentElement!.insertBefore(this._backdropElement, this._pane);
// 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._backdropClick.next(null));
// Add class to fade-in the backdrop after one frame.
this._ngZone.runOutsideAngular(() => {
requestAnimationFrame(() => {
if (this._backdropElement) {
this._backdropElement.classList.add('cdk-overlay-backdrop-showing');
}
});
});
}
/**
* 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.
*/
private _updateStackingOrder() {
if (this._pane.nextSibling) {
this._pane.parentNode!.appendChild(this._pane);
}
}
/** Detaches the backdrop (if any) associated with the overlay. */
detachBackdrop(): void {
let backdropToDetach = this._backdropElement;
if (backdropToDetach) {
let finishDetach = () => {
// It may not be attached to anything in certain cases (e.g. unit tests).
if (backdropToDetach && 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;
}
};
backdropToDetach.classList.remove('cdk-overlay-backdrop-showing');
if (this._config.backdropClass) {
backdropToDetach.classList.remove(this._config.backdropClass);
}
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.
this._ngZone.runOutsideAngular(() => {
setTimeout(finishDetach, 500);
});
}
}
}
function formatCssUnit(value: number | string) {
return typeof value === 'string' ? value as string : `${value}px`;
}
/** Size properties for an overlay. */
export interface OverlaySizeConfig {
width?: number | string;
height?: number | string;
minWidth?: number | string;
minHeight?: number | string;
maxWidth?: number | string;
maxHeight?: number | string;
}