259 lines
8.2 KiB
TypeScript
259 lines
8.2 KiB
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.dev/license
|
||
|
|
*/
|
||
|
|
|
||
|
|
/** Possible states of the lifecycle of a dialog. */
|
||
|
|
import {FocusOrigin} from '@angular/cdk/a11y';
|
||
|
|
import {merge, Observable, Subject} from 'rxjs';
|
||
|
|
import {DialogRef} from '@angular/cdk/dialog';
|
||
|
|
import {DialogPosition, MatDialogConfig} from './dialog-config';
|
||
|
|
import {MatDialogContainer} from './dialog-container';
|
||
|
|
import {filter, take} from 'rxjs/operators';
|
||
|
|
import {ESCAPE, hasModifierKey} from '@angular/cdk/keycodes';
|
||
|
|
import {GlobalPositionStrategy} from '@angular/cdk/overlay';
|
||
|
|
import {ComponentRef} from '@angular/core';
|
||
|
|
|
||
|
|
export enum MatDialogState {
|
||
|
|
OPEN,
|
||
|
|
CLOSING,
|
||
|
|
CLOSED,
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Reference to a dialog opened via the MatDialog service.
|
||
|
|
*/
|
||
|
|
export class MatDialogRef<T, R = any> {
|
||
|
|
/** The instance of component opened into the dialog. */
|
||
|
|
componentInstance: T;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* `ComponentRef` of the component opened into the dialog. Will be
|
||
|
|
* null when the dialog is opened using a `TemplateRef`.
|
||
|
|
*/
|
||
|
|
readonly componentRef: ComponentRef<T> | null;
|
||
|
|
|
||
|
|
/** Whether the user is allowed to close the dialog. */
|
||
|
|
disableClose: boolean | undefined;
|
||
|
|
|
||
|
|
/** Unique ID for the dialog. */
|
||
|
|
id: string;
|
||
|
|
|
||
|
|
/** Subject for notifying the user that the dialog has finished opening. */
|
||
|
|
private readonly _afterOpened = new Subject<void>();
|
||
|
|
|
||
|
|
/** Subject for notifying the user that the dialog has started closing. */
|
||
|
|
private readonly _beforeClosed = new Subject<R | undefined>();
|
||
|
|
|
||
|
|
/** 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: ReturnType<typeof setTimeout>;
|
||
|
|
|
||
|
|
/** Current state of the dialog. */
|
||
|
|
private _state = MatDialogState.OPEN;
|
||
|
|
|
||
|
|
// TODO(crisbeto): we shouldn't have to declare this property, because `DialogRef.close`
|
||
|
|
// already has a second `options` parameter that we can use. The problem is that internal tests
|
||
|
|
// have assertions like `expect(MatDialogRef.close).toHaveBeenCalledWith(foo)` which will break,
|
||
|
|
// because it'll be called with two arguments by things like `MatDialogClose`.
|
||
|
|
/** Interaction that caused the dialog to close. */
|
||
|
|
private _closeInteractionType: FocusOrigin | undefined;
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
private _ref: DialogRef<R, T>,
|
||
|
|
config: MatDialogConfig,
|
||
|
|
public _containerInstance: MatDialogContainer,
|
||
|
|
) {
|
||
|
|
this.disableClose = config.disableClose;
|
||
|
|
this.id = _ref.id;
|
||
|
|
|
||
|
|
// Used to target panels specifically tied to dialogs.
|
||
|
|
_ref.addPanelClass('mat-mdc-dialog-panel');
|
||
|
|
|
||
|
|
// Emit when opening animation completes
|
||
|
|
_containerInstance._animationStateChanged
|
||
|
|
.pipe(
|
||
|
|
filter(event => event.state === 'opened'),
|
||
|
|
take(1),
|
||
|
|
)
|
||
|
|
.subscribe(() => {
|
||
|
|
this._afterOpened.next();
|
||
|
|
this._afterOpened.complete();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Dispose overlay when closing animation is complete
|
||
|
|
_containerInstance._animationStateChanged
|
||
|
|
.pipe(
|
||
|
|
filter(event => event.state === 'closed'),
|
||
|
|
take(1),
|
||
|
|
)
|
||
|
|
.subscribe(() => {
|
||
|
|
clearTimeout(this._closeFallbackTimeout);
|
||
|
|
this._finishDialogClose();
|
||
|
|
});
|
||
|
|
|
||
|
|
_ref.overlayRef.detachments().subscribe(() => {
|
||
|
|
this._beforeClosed.next(this._result);
|
||
|
|
this._beforeClosed.complete();
|
||
|
|
this._finishDialogClose();
|
||
|
|
});
|
||
|
|
|
||
|
|
merge(
|
||
|
|
this.backdropClick(),
|
||
|
|
this.keydownEvents().pipe(
|
||
|
|
filter(event => event.keyCode === ESCAPE && !this.disableClose && !hasModifierKey(event)),
|
||
|
|
),
|
||
|
|
).subscribe(event => {
|
||
|
|
if (!this.disableClose) {
|
||
|
|
event.preventDefault();
|
||
|
|
_closeDialogVia(this, event.type === 'keydown' ? 'keyboard' : 'mouse');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 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.state === 'closing'),
|
||
|
|
take(1),
|
||
|
|
)
|
||
|
|
.subscribe(event => {
|
||
|
|
this._beforeClosed.next(dialogResult);
|
||
|
|
this._beforeClosed.complete();
|
||
|
|
this._ref.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._finishDialogClose(),
|
||
|
|
event.totalTime + 100,
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
this._state = MatDialogState.CLOSING;
|
||
|
|
this._containerInstance._startExitAnimation();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets an observable that is notified when the dialog is finished opening.
|
||
|
|
*/
|
||
|
|
afterOpened(): Observable<void> {
|
||
|
|
return this._afterOpened;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets an observable that is notified when the dialog is finished closing.
|
||
|
|
*/
|
||
|
|
afterClosed(): Observable<R | undefined> {
|
||
|
|
return this._ref.closed;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets an observable that is notified when the dialog has started closing.
|
||
|
|
*/
|
||
|
|
beforeClosed(): Observable<R | undefined> {
|
||
|
|
return this._beforeClosed;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets an observable that emits when the overlay's backdrop has been clicked.
|
||
|
|
*/
|
||
|
|
backdropClick(): Observable<MouseEvent> {
|
||
|
|
return this._ref.backdropClick;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Gets an observable that emits when keydown events are targeted on the overlay.
|
||
|
|
*/
|
||
|
|
keydownEvents(): Observable<KeyboardEvent> {
|
||
|
|
return this._ref.keydownEvents;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Updates the dialog's position.
|
||
|
|
* @param position New dialog position.
|
||
|
|
*/
|
||
|
|
updatePosition(position?: DialogPosition): this {
|
||
|
|
let strategy = this._ref.config.positionStrategy as GlobalPositionStrategy;
|
||
|
|
|
||
|
|
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._ref.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._ref.updateSize(width, height);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Add a CSS class or an array of classes to the overlay pane. */
|
||
|
|
addPanelClass(classes: string | string[]): this {
|
||
|
|
this._ref.addPanelClass(classes);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Remove a CSS class or an array of classes from the overlay pane. */
|
||
|
|
removePanelClass(classes: string | string[]): this {
|
||
|
|
this._ref.removePanelClass(classes);
|
||
|
|
return this;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Gets the current state of the dialog's lifecycle. */
|
||
|
|
getState(): MatDialogState {
|
||
|
|
return this._state;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Finishes the dialog close by updating the state of the dialog
|
||
|
|
* and disposing the overlay.
|
||
|
|
*/
|
||
|
|
private _finishDialogClose() {
|
||
|
|
this._state = MatDialogState.CLOSED;
|
||
|
|
this._ref.close(this._result, {focusOrigin: this._closeInteractionType});
|
||
|
|
this.componentInstance = null!;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Closes the dialog with the specified interaction type. This is currently not part of
|
||
|
|
* `MatDialogRef` as that would conflict with custom dialog ref mocks provided in tests.
|
||
|
|
* More details. See: https://github.com/angular/components/pull/9257#issuecomment-651342226.
|
||
|
|
*/
|
||
|
|
// TODO: Move this back into `MatDialogRef` when we provide an official mock dialog ref.
|
||
|
|
export function _closeDialogVia<R>(ref: MatDialogRef<R>, interactionType: FocusOrigin, result?: R) {
|
||
|
|
(ref as unknown as {_closeInteractionType: FocusOrigin})._closeInteractionType = interactionType;
|
||
|
|
return ref.close(result);
|
||
|
|
}
|