/** * @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.dev/license */ import {ComponentType, Overlay, ScrollStrategy} from '@angular/cdk/overlay'; import { ComponentRef, Injectable, InjectionToken, OnDestroy, TemplateRef, Type, inject, } from '@angular/core'; import {MatDialogConfig} from './dialog-config'; import {MatDialogContainer} from './dialog-container'; import {MatDialogRef} from './dialog-ref'; import {defer, Observable, Subject} from 'rxjs'; import {Dialog, DialogConfig} from '@angular/cdk/dialog'; import {startWith} from 'rxjs/operators'; import {_IdGenerator} from '@angular/cdk/a11y'; /** Injection token that can be used to access the data that was passed in to a dialog. */ export const MAT_DIALOG_DATA = new InjectionToken('MatMdcDialogData'); /** Injection token that can be used to specify default dialog options. */ export const MAT_DIALOG_DEFAULT_OPTIONS = new InjectionToken( 'mat-mdc-dialog-default-options', ); /** Injection token that determines the scroll handling while the dialog is open. */ export const MAT_DIALOG_SCROLL_STRATEGY = new InjectionToken<() => ScrollStrategy>( 'mat-mdc-dialog-scroll-strategy', { providedIn: 'root', factory: () => { const overlay = inject(Overlay); return () => overlay.scrollStrategies.block(); }, }, ); /** * @docs-private * @deprecated No longer used. To be removed. * @breaking-change 19.0.0 */ export function MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY( overlay: Overlay, ): () => ScrollStrategy { return () => overlay.scrollStrategies.block(); } /** * @docs-private * @deprecated No longer used. To be removed. * @breaking-change 19.0.0 */ export const MAT_DIALOG_SCROLL_STRATEGY_PROVIDER = { provide: MAT_DIALOG_SCROLL_STRATEGY, deps: [Overlay], useFactory: MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY, }; /** * Service to open Material Design modal dialogs. */ @Injectable({providedIn: 'root'}) export class MatDialog implements OnDestroy { private _overlay = inject(Overlay); private _defaultOptions = inject(MAT_DIALOG_DEFAULT_OPTIONS, {optional: true}); private _scrollStrategy = inject(MAT_DIALOG_SCROLL_STRATEGY); private _parentDialog = inject(MatDialog, {optional: true, skipSelf: true}); private _idGenerator = inject(_IdGenerator); protected _dialog = inject(Dialog); private readonly _openDialogsAtThisLevel: MatDialogRef[] = []; private readonly _afterAllClosedAtThisLevel = new Subject(); private readonly _afterOpenedAtThisLevel = new Subject>(); protected dialogConfigClass = MatDialogConfig; private readonly _dialogRefConstructor: Type>; private readonly _dialogContainerType: Type; private readonly _dialogDataToken: InjectionToken; /** Keeps track of the currently-open dialogs. */ get openDialogs(): MatDialogRef[] { return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogsAtThisLevel; } /** Stream that emits when a dialog has been opened. */ get afterOpened(): Subject> { return this._parentDialog ? this._parentDialog.afterOpened : this._afterOpenedAtThisLevel; } private _getAfterAllClosed(): Subject { const parent = this._parentDialog; return parent ? parent._getAfterAllClosed() : this._afterAllClosedAtThisLevel; } /** * Stream that emits when all open dialog have finished closing. * Will emit on subscribe if there are no open dialogs to begin with. */ readonly afterAllClosed: Observable = defer(() => this.openDialogs.length ? this._getAfterAllClosed() : this._getAfterAllClosed().pipe(startWith(undefined)), ) as Observable; constructor(...args: unknown[]); constructor() { this._dialogRefConstructor = MatDialogRef; this._dialogContainerType = MatDialogContainer; this._dialogDataToken = MAT_DIALOG_DATA; } /** * Opens a modal dialog containing the given component. * @param component Type of the component to load into the dialog. * @param config Extra configuration options. * @returns Reference to the newly-opened dialog. */ open( component: ComponentType, config?: MatDialogConfig, ): MatDialogRef; /** * Opens a modal dialog containing the given template. * @param template TemplateRef to instantiate as the dialog content. * @param config Extra configuration options. * @returns Reference to the newly-opened dialog. */ open( template: TemplateRef, config?: MatDialogConfig, ): MatDialogRef; open( template: ComponentType | TemplateRef, config?: MatDialogConfig, ): MatDialogRef; open( componentOrTemplateRef: ComponentType | TemplateRef, config?: MatDialogConfig, ): MatDialogRef { let dialogRef: MatDialogRef; config = {...(this._defaultOptions || new MatDialogConfig()), ...config}; config.id = config.id || this._idGenerator.getId('mat-mdc-dialog-'); config.scrollStrategy = config.scrollStrategy || this._scrollStrategy(); const cdkRef = this._dialog.open(componentOrTemplateRef, { ...config, positionStrategy: this._overlay.position().global().centerHorizontally().centerVertically(), // Disable closing since we need to sync it up to the animation ourselves. disableClose: true, // Disable closing on destroy, because this service cleans up its open dialogs as well. // We want to do the cleanup here, rather than the CDK service, because the CDK destroys // the dialogs immediately whereas we want it to wait for the animations to finish. closeOnDestroy: false, // Disable closing on detachments so that we can sync up the animation. // The Material dialog ref handles this manually. closeOnOverlayDetachments: false, container: { type: this._dialogContainerType, providers: () => [ // Provide our config as the CDK config as well since it has the same interface as the // CDK one, but it contains the actual values passed in by the user for things like // `disableClose` which we disable for the CDK dialog since we handle it ourselves. {provide: this.dialogConfigClass, useValue: config}, {provide: DialogConfig, useValue: config}, ], }, templateContext: () => ({dialogRef}), providers: (ref, cdkConfig, dialogContainer) => { dialogRef = new this._dialogRefConstructor(ref, config, dialogContainer); dialogRef.updatePosition(config?.position); return [ {provide: this._dialogContainerType, useValue: dialogContainer}, {provide: this._dialogDataToken, useValue: cdkConfig.data}, {provide: this._dialogRefConstructor, useValue: dialogRef}, ]; }, }); // This can't be assigned in the `providers` callback, because // the instance hasn't been assigned to the CDK ref yet. (dialogRef! as {componentRef: ComponentRef}).componentRef = cdkRef.componentRef!; dialogRef!.componentInstance = cdkRef.componentInstance!; this.openDialogs.push(dialogRef!); this.afterOpened.next(dialogRef!); dialogRef!.afterClosed().subscribe(() => { const index = this.openDialogs.indexOf(dialogRef); if (index > -1) { this.openDialogs.splice(index, 1); if (!this.openDialogs.length) { this._getAfterAllClosed().next(); } } }); return dialogRef!; } /** * Closes all of the currently-open dialogs. */ closeAll(): void { this._closeDialogs(this.openDialogs); } /** * Finds an open dialog by its id. * @param id ID to use when looking up the dialog. */ getDialogById(id: string): MatDialogRef | undefined { return this.openDialogs.find(dialog => dialog.id === id); } ngOnDestroy() { // Only close the dialogs at this level on destroy // since the parent service may still be active. this._closeDialogs(this._openDialogsAtThisLevel); this._afterAllClosedAtThisLevel.complete(); this._afterOpenedAtThisLevel.complete(); } private _closeDialogs(dialogs: MatDialogRef[]) { let i = dialogs.length; while (i--) { dialogs[i].close(); } } }