/** * @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 {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes'; import {GlobalPositionStrategy, OverlayRef} from '@angular/cdk/overlay'; import {Location} from '@angular/common'; import {Observable, Subject} from 'rxjs'; import {filter, take} from 'rxjs/operators'; import {DialogPosition} from './dialog-config'; import {MatDialogContainer} from './dialog-container'; // TODO(jelbourn): resizing // Counter for unique dialog ids. let uniqueId = 0; /** Possible states of the lifecycle of a dialog. */ export const enum MatDialogState {OPEN, CLOSING, CLOSED} /** * Reference to a dialog opened via the MatDialog service. */ export class MatDialogRef { /** The instance of component opened into the dialog. */ componentInstance: T; /** Whether the user is allowed to close the dialog. */ disableClose: boolean | undefined = this._containerInstance._config.disableClose; /** Subject for notifying the user that the dialog has finished opening. */ private readonly _afterOpened = new Subject(); /** Subject for notifying the user that the dialog has finished closing. */ private readonly _afterClosed = new Subject(); /** Subject for notifying the user that the dialog has started closing. */ private readonly _beforeClosed = new Subject(); /** Result to be passed to afterClosed. */ private _result: R | undefined; /** Handle to the timeout that's running as a fallback in case the exit animation doesn't fire. */ private _closeFallbackTimeout: number; /** Current state of the dialog. */ private _state = MatDialogState.OPEN; constructor( private _overlayRef: OverlayRef, public _containerInstance: MatDialogContainer, // @breaking-change 8.0.0 `_location` parameter to be removed. _location?: Location, readonly id: string = `mat-dialog-${uniqueId++}`) { // Pass the id along to the container. _containerInstance._id = id; // Emit when opening animation completes _containerInstance._animationStateChanged.pipe( filter(event => event.phaseName === 'done' && event.toState === 'enter'), take(1) ) .subscribe(() => { this._afterOpened.next(); this._afterOpened.complete(); }); // Dispose overlay when closing animation is complete _containerInstance._animationStateChanged.pipe( filter(event => event.phaseName === 'done' && event.toState === 'exit'), take(1) ).subscribe(() => { clearTimeout(this._closeFallbackTimeout); this._overlayRef.dispose(); }); _overlayRef.detachments().subscribe(() => { this._beforeClosed.next(this._result); this._beforeClosed.complete(); this._afterClosed.next(this._result); this._afterClosed.complete(); this.componentInstance = null!; this._overlayRef.dispose(); }); _overlayRef.keydownEvents() .pipe(filter(event => { return event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event); })) .subscribe(event => { event.preventDefault(); this.close(); }); } /** * Close the dialog. * @param dialogResult Optional result to return to the dialog opener. */ close(dialogResult?: R): void { this._result = dialogResult; // Transition the backdrop in parallel to the dialog. this._containerInstance._animationStateChanged.pipe( filter(event => event.phaseName === 'start'), take(1) ) .subscribe(event => { this._beforeClosed.next(dialogResult); this._beforeClosed.complete(); this._state = MatDialogState.CLOSED; this._overlayRef.detachBackdrop(); // The logic that disposes of the overlay depends on the exit animation completing, however // it isn't guaranteed if the parent view is destroyed while it's running. Add a fallback // timeout which will clean everything up if the animation hasn't fired within the specified // amount of time plus 100ms. We don't need to run this outside the NgZone, because for the // vast majority of cases the timeout will have been cleared before it has the chance to fire. this._closeFallbackTimeout = setTimeout(() => { this._overlayRef.dispose(); }, event.totalTime + 100); }); this._containerInstance._startExitAnimation(); this._state = MatDialogState.CLOSING; } /** * Gets an observable that is notified when the dialog is finished opening. */ afterOpened(): Observable { return this._afterOpened.asObservable(); } /** * Gets an observable that is notified when the dialog is finished closing. */ afterClosed(): Observable { return this._afterClosed.asObservable(); } /** * Gets an observable that is notified when the dialog has started closing. */ beforeClosed(): Observable { return this._beforeClosed.asObservable(); } /** * Gets an observable that emits when the overlay's backdrop has been clicked. */ backdropClick(): Observable { return this._overlayRef.backdropClick(); } /** * Gets an observable that emits when keydown events are targeted on the overlay. */ keydownEvents(): Observable { return this._overlayRef.keydownEvents(); } /** * Updates the dialog's position. * @param position New dialog position. */ updatePosition(position?: DialogPosition): this { let strategy = this._getPositionStrategy(); if (position && (position.left || position.right)) { position.left ? strategy.left(position.left) : strategy.right(position.right); } else { strategy.centerHorizontally(); } if (position && (position.top || position.bottom)) { position.top ? strategy.top(position.top) : strategy.bottom(position.bottom); } else { strategy.centerVertically(); } this._overlayRef.updatePosition(); return this; } /** * Updates the dialog's width and height. * @param width New width of the dialog. * @param height New height of the dialog. */ updateSize(width: string = '', height: string = ''): this { this._getPositionStrategy().width(width).height(height); this._overlayRef.updatePosition(); return this; } /** Add a CSS class or an array of classes to the overlay pane. */ addPanelClass(classes: string | string[]): this { this._overlayRef.addPanelClass(classes); return this; } /** Remove a CSS class or an array of classes from the overlay pane. */ removePanelClass(classes: string | string[]): this { this._overlayRef.removePanelClass(classes); return this; } /** * Gets an observable that is notified when the dialog is finished opening. * @deprecated Use `afterOpened` instead. * @breaking-change 8.0.0 */ afterOpen(): Observable { return this.afterOpened(); } /** * Gets an observable that is notified when the dialog has started closing. * @deprecated Use `beforeClosed` instead. * @breaking-change 8.0.0 */ beforeClose(): Observable { return this.beforeClosed(); } /** Gets the current state of the dialog's lifecycle. */ getState(): MatDialogState { return this._state; } /** Fetches the position strategy object from the overlay ref. */ private _getPositionStrategy(): GlobalPositionStrategy { return this._overlayRef.getConfig().positionStrategy as GlobalPositionStrategy; } }