mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
Create Dialog in CDK. (#8844)
This commit is contained in:
committed by
Andrew Seguin
parent
dc045967d8
commit
4d675d6aac
@@ -74,6 +74,7 @@
|
||||
|
||||
# Material experimental package
|
||||
/src/material-experimental/** @jelbourn
|
||||
/src/material-experimental/dialog/** @jelbourn @josephperrott @crisbeto
|
||||
|
||||
# Docs examples & guides
|
||||
/guides/** @amcdnl @jelbourn
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* @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 {ViewContainerRef} from '@angular/core';
|
||||
import {Direction} from '@angular/cdk/bidi';
|
||||
import {ComponentType} from '@angular/cdk/overlay';
|
||||
import {CdkDialogContainer} from './dialog-container';
|
||||
|
||||
/** Valid ARIA roles for a dialog element. */
|
||||
export type DialogRole = 'dialog' | 'alertdialog';
|
||||
|
||||
/** Possible overrides for a dialog's position. */
|
||||
export interface DialogPosition {
|
||||
top?: string;
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
}
|
||||
|
||||
export class DialogConfig<D = any> {
|
||||
/** Component to use as the container for the dialog. */
|
||||
containerComponent?: ComponentType<CdkDialogContainer>;
|
||||
|
||||
/**
|
||||
* Where the attached component should live in Angular's *logical* component tree.
|
||||
* This affects what is available for injection and the change detection order for the
|
||||
* component instantiated inside of the dialog. This does not affect where the dialog
|
||||
* content will be rendered.
|
||||
*/
|
||||
viewContainerRef?: ViewContainerRef;
|
||||
|
||||
/** The id of the dialog. */
|
||||
id?: string;
|
||||
|
||||
/** The ARIA role of the dialog. */
|
||||
role?: DialogRole = 'dialog';
|
||||
|
||||
/** Custom class(es) for the overlay panel. */
|
||||
panelClass?: string | string[] = '';
|
||||
|
||||
/** Custom class(es) for the dialog container. */
|
||||
containerClass?: string | string[] = '';
|
||||
|
||||
/** Whether the dialog has a background. */
|
||||
hasBackdrop?: boolean = true;
|
||||
|
||||
/** Custom class(es) for the backdrop. */
|
||||
backdropClass?: string | undefined = '';
|
||||
|
||||
/** Whether the dialog can be closed by user interaction. */
|
||||
disableClose?: boolean = false;
|
||||
|
||||
/** The width of the dialog. */
|
||||
width?: string = '';
|
||||
|
||||
/** The height of the dialog. */
|
||||
height?: string = '';
|
||||
|
||||
/** The minimum width of the dialog. */
|
||||
minWidth?: string | number = '';
|
||||
|
||||
/** The minimum height of the dialog. */
|
||||
minHeight?: string | number = '';
|
||||
|
||||
/** The maximum width of the dialog. */
|
||||
maxWidth?: string | number = '80vw';
|
||||
|
||||
/** The maximum height of the dialog. */
|
||||
maxHeight?: string | number = '';
|
||||
|
||||
/** The position of the dialog. */
|
||||
position?: DialogPosition;
|
||||
|
||||
/** Data to be injected into the dialog content. */
|
||||
data?: D | null = null;
|
||||
|
||||
/** The layout direction for the dialog content. */
|
||||
direction?: Direction = 'ltr';
|
||||
|
||||
/** ID of the element that describes the dialog. */
|
||||
ariaDescribedBy?: string | null = null;
|
||||
|
||||
/** Aria label to assign to the dialog element */
|
||||
ariaLabel?: string | null = null;
|
||||
|
||||
/** Whether the dialog should focus the first focusable element on open. */
|
||||
autoFocus?: boolean = true;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<ng-template cdkPortalOutlet></ng-template>
|
||||
@@ -0,0 +1,6 @@
|
||||
cdk-dialog-container {
|
||||
background: white;
|
||||
border-radius: 5px;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* @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 {
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
ViewChild,
|
||||
ComponentRef,
|
||||
EmbeddedViewRef,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
Optional,
|
||||
Inject,
|
||||
ViewEncapsulation,
|
||||
ChangeDetectionStrategy,
|
||||
} from '@angular/core';
|
||||
import {DOCUMENT} from '@angular/common';
|
||||
import {trigger, state, style, transition, animate, AnimationEvent} from '@angular/animations';
|
||||
import {
|
||||
BasePortalOutlet,
|
||||
PortalHostDirective,
|
||||
ComponentPortal,
|
||||
TemplatePortal
|
||||
} from '@angular/cdk/portal';
|
||||
import {FocusTrapFactory} from '@angular/cdk/a11y';
|
||||
import {DialogConfig} from './dialog-config';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
|
||||
export function throwDialogContentAlreadyAttachedError() {
|
||||
throw Error('Attempting to attach dialog content after content is already attached');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal component that wraps user-provided dialog content.
|
||||
* @docs-private
|
||||
*/
|
||||
@Component({
|
||||
moduleId: module.id,
|
||||
selector: 'cdk-dialog-container',
|
||||
templateUrl: './dialog-container.html',
|
||||
styleUrls: ['dialog-container.css'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
preserveWhitespaces: false,
|
||||
// Using OnPush for dialogs caused some G3 sync issues. Disabled until we can track them down.
|
||||
// tslint:disable-next-line:validate-decorators
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
animations: [
|
||||
trigger('dialog', [
|
||||
state('enter', style({ opacity: 1 })),
|
||||
state('exit, void', style({ opacity: 0 })),
|
||||
transition('* => *', animate(225)),
|
||||
])
|
||||
],
|
||||
host: {
|
||||
'[@dialog]': '_state',
|
||||
'(@dialog.start)': '_onAnimationStart($event)',
|
||||
'(@dialog.done)': '_onAnimationDone($event)',
|
||||
},
|
||||
})
|
||||
export class CdkDialogContainer extends BasePortalOutlet {
|
||||
/** State of the dialog animation. */
|
||||
_state: 'void' | 'enter' | 'exit' = 'enter';
|
||||
|
||||
/** Element that was focused before the dialog was opened. Save this to restore upon close. */
|
||||
private _elementFocusedBeforeDialogWasOpened: HTMLElement | null = null;
|
||||
|
||||
/** The class that traps and manages focus within the dialog. */
|
||||
private _focusTrap = this._focusTrapFactory.create(this._elementRef.nativeElement, false);
|
||||
|
||||
// @HostBinding is used in the class as it is expected to be extended. Since @Component decorator
|
||||
// metadata is not inherited by child classes, instead the host binding data is defined in a way
|
||||
// that can be inherited.
|
||||
// tslint:disable:no-host-decorator-in-concrete
|
||||
@HostBinding('attr.aria-label') get _ariaLabel() { return this._config.ariaLabel || null; }
|
||||
|
||||
@HostBinding('attr.aria-describedby')
|
||||
get _ariaDescribedBy() { return this._config ? this._config.ariaDescribedBy : null; }
|
||||
|
||||
@HostBinding('attr.role') get _role() { return this._config ? this._config.role : null; }
|
||||
|
||||
@HostBinding('attr.tabindex') get _tabindex() { return -1; }
|
||||
// tslint:disable:no-host-decorator-in-concrete
|
||||
|
||||
/** The portal host inside of this container into which the dialog content will be loaded. */
|
||||
@ViewChild(PortalHostDirective) _portalHost: PortalHostDirective;
|
||||
|
||||
/** A subject emitting before the dialog enters the view. */
|
||||
_beforeEnter: Subject<void> = new Subject();
|
||||
|
||||
/** A subject emitting after the dialog enters the view. */
|
||||
_afterEnter: Subject<void> = new Subject();
|
||||
|
||||
/** A subject emitting before the dialog exits the view. */
|
||||
_beforeExit: Subject<void> = new Subject();
|
||||
|
||||
/** A subject emitting after the dialog exits the view. */
|
||||
_afterExit: Subject<void> = new Subject();
|
||||
|
||||
/** The dialog configuration. */
|
||||
_config: DialogConfig;
|
||||
|
||||
constructor(
|
||||
private _elementRef: ElementRef,
|
||||
private _focusTrapFactory: FocusTrapFactory,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@Optional() @Inject(DOCUMENT) private _document: any) {
|
||||
super();
|
||||
}
|
||||
|
||||
/** Destroy focus trap to place focus back to the element focused before the dialog opened. */
|
||||
ngOnDestroy() {
|
||||
this._focusTrap.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a ComponentPortal as content to this dialog container.
|
||||
* @param portal Portal to be attached as the dialog content.
|
||||
*/
|
||||
attachComponentPortal<T>(portal: ComponentPortal<T>): ComponentRef<T> {
|
||||
if (this._portalHost.hasAttached()) {
|
||||
throwDialogContentAlreadyAttachedError();
|
||||
}
|
||||
|
||||
this._savePreviouslyFocusedElement();
|
||||
return this._portalHost.attachComponentPortal(portal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a TemplatePortal as content to this dialog container.
|
||||
* @param portal Portal to be attached as the dialog content.
|
||||
*/
|
||||
attachTemplatePortal<C>(portal: TemplatePortal<C>): EmbeddedViewRef<C> {
|
||||
if (this._portalHost.hasAttached()) {
|
||||
throwDialogContentAlreadyAttachedError();
|
||||
}
|
||||
|
||||
this._savePreviouslyFocusedElement();
|
||||
return this._portalHost.attachTemplatePortal(portal);
|
||||
}
|
||||
|
||||
/** Emit lifecycle events based on animation `start` callback. */
|
||||
_onAnimationStart(event: AnimationEvent) {
|
||||
if (event.toState === 'enter') {
|
||||
this._beforeEnter.next();
|
||||
}
|
||||
if (event.toState === 'void' || event.toState === 'exit') {
|
||||
this._beforeExit.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Emit lifecycle events based on animation `done` callback. */
|
||||
_onAnimationDone(event: AnimationEvent) {
|
||||
if (event.toState === 'enter') {
|
||||
this._autoFocusFirstTabbableElement();
|
||||
this._afterEnter.next();
|
||||
}
|
||||
if (event.toState === 'void' || event.toState === 'exit') {
|
||||
this._returnFocusAfterDialog();
|
||||
this._afterExit.next();
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the dialog exit animation. */
|
||||
_startExiting(): void {
|
||||
this._state = 'exit';
|
||||
|
||||
// Mark the container for check so it can react if the
|
||||
// view container is using OnPush change detection.
|
||||
this._changeDetectorRef.markForCheck();
|
||||
}
|
||||
|
||||
/** Saves a reference to the element that was focused before the dialog was opened. */
|
||||
private _savePreviouslyFocusedElement() {
|
||||
if (this._document) {
|
||||
this._elementFocusedBeforeDialogWasOpened = this._document.activeElement as HTMLElement;
|
||||
|
||||
// Move focus onto the dialog immediately in order to prevent the user from accidentally
|
||||
// opening multiple dialogs at the same time. Needs to be async, because the element
|
||||
// may not be focusable immediately.
|
||||
Promise.resolve().then(() => this._elementRef.nativeElement.focus());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Autofocus the first tabbable element inside of the dialog, if there is not a tabbable element,
|
||||
* focus the dialog instead.
|
||||
*/
|
||||
private _autoFocusFirstTabbableElement() {
|
||||
// If were to attempt to focus immediately, then the content of the dialog would not yet be
|
||||
// ready in instances where change detection has to run first. To deal with this, we simply
|
||||
// wait for the microtask queue to be empty.
|
||||
if (this._config.autoFocus) {
|
||||
this._focusTrap.focusInitialElementWhenReady().then(hasMovedFocus => {
|
||||
// If we didn't find any focusable elements inside the dialog, focus the
|
||||
// container so the user can't tab into other elements behind it.
|
||||
if (!hasMovedFocus) {
|
||||
this._elementRef.nativeElement.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the focus to the element focused before the dialog was open. */
|
||||
private _returnFocusAfterDialog() {
|
||||
const toFocus = this._elementFocusedBeforeDialogWasOpened;
|
||||
// We need the extra check, because IE can set the `activeElement` to null in some cases.
|
||||
if (toFocus && typeof toFocus.focus === 'function') {
|
||||
toFocus.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @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 {InjectionToken} from '@angular/core';
|
||||
import {ComponentType, Overlay, ScrollStrategy, BlockScrollStrategy} from '@angular/cdk/overlay';
|
||||
import {DialogRef} from './dialog-ref';
|
||||
import {CdkDialogContainer} from './dialog-container';
|
||||
import {DialogConfig} from './dialog-config';
|
||||
|
||||
/** Injection token for the Dialog's ScrollStrategy. */
|
||||
export const DIALOG_SCROLL_STRATEGY =
|
||||
new InjectionToken<() => ScrollStrategy>('DialogScrollStrategy');
|
||||
|
||||
/** Injection token for the Dialog's Data. */
|
||||
export const DIALOG_DATA = new InjectionToken<any>('DialogData');
|
||||
|
||||
/** Injection token for the DialogRef constructor. */
|
||||
export const DIALOG_REF = new InjectionToken<DialogRef<any>>('DialogRef');
|
||||
|
||||
/** Injection token for the DialogConfig. */
|
||||
export const DIALOG_CONFIG = new InjectionToken<DialogConfig>('DialogConfig');
|
||||
|
||||
/** Injection token for the Dialog's DialogContainer component. */
|
||||
export const DIALOG_CONTAINER =
|
||||
new InjectionToken<ComponentType<CdkDialogContainer>>('DialogContainer');
|
||||
|
||||
/** @docs-private */
|
||||
export function MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY(overlay: Overlay):
|
||||
() => BlockScrollStrategy {
|
||||
return () => overlay.scrollStrategies.block();
|
||||
}
|
||||
|
||||
/** @docs-private */
|
||||
export const MAT_DIALOG_SCROLL_STRATEGY_PROVIDER = {
|
||||
provide: DIALOG_SCROLL_STRATEGY,
|
||||
deps: [Overlay],
|
||||
useFactory: MAT_DIALOG_SCROLL_STRATEGY_PROVIDER_FACTORY,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @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 {NgModule} from '@angular/core';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {OverlayModule} from '@angular/cdk/overlay';
|
||||
import {PortalModule} from '@angular/cdk/portal';
|
||||
import {A11yModule} from '@angular/cdk/a11y';
|
||||
import {Dialog} from './dialog';
|
||||
import {CdkDialogContainer} from './dialog-container';
|
||||
import {DialogConfig} from './dialog-config';
|
||||
import {DialogRef} from './dialog-ref';
|
||||
import {
|
||||
DIALOG_CONFIG,
|
||||
DIALOG_CONTAINER,
|
||||
DIALOG_REF,
|
||||
MAT_DIALOG_SCROLL_STRATEGY_PROVIDER
|
||||
} from './dialog-injectors';
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
OverlayModule,
|
||||
PortalModule,
|
||||
A11yModule,
|
||||
],
|
||||
exports: [
|
||||
CdkDialogContainer,
|
||||
],
|
||||
declarations: [
|
||||
CdkDialogContainer,
|
||||
],
|
||||
providers: [
|
||||
Dialog,
|
||||
MAT_DIALOG_SCROLL_STRATEGY_PROVIDER,
|
||||
{provide: DIALOG_REF, useValue: DialogRef},
|
||||
{provide: DIALOG_CONTAINER, useValue: CdkDialogContainer},
|
||||
{provide: DIALOG_CONFIG, useValue: DialogConfig},
|
||||
],
|
||||
entryComponents: [CdkDialogContainer],
|
||||
})
|
||||
export class DialogModule {}
|
||||
@@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @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 {OverlayRef, GlobalPositionStrategy, OverlaySizeConfig} from '@angular/cdk/overlay';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {map} from 'rxjs/operators/map';
|
||||
import {DialogPosition} from './dialog-config';
|
||||
import {CdkDialogContainer} from './dialog-container';
|
||||
|
||||
/** Unique id for the created dialog. */
|
||||
let uniqueId = 0;
|
||||
|
||||
/**
|
||||
* Reference to a dialog opened via the Dialog service.
|
||||
*/
|
||||
export class DialogRef<T, R = any> {
|
||||
/** The instance of the component in the dialog. */
|
||||
componentInstance: T;
|
||||
|
||||
/** Whether the user is allowed to close the dialog. */
|
||||
disableClose: boolean | undefined;
|
||||
|
||||
/** Result to be passed to afterClosed. */
|
||||
private _result: R | undefined;
|
||||
|
||||
constructor(
|
||||
public _overlayRef: OverlayRef,
|
||||
protected _containerInstance: CdkDialogContainer,
|
||||
readonly id: string = `dialog-${uniqueId++}`) {
|
||||
|
||||
// If the dialog has a backdrop, handle clicks from the backdrop.
|
||||
if (_containerInstance._config.hasBackdrop) {
|
||||
_overlayRef.backdropClick().subscribe(() => {
|
||||
if (!this.disableClose) {
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.beforeClose().subscribe(() => {
|
||||
this._overlayRef.detachBackdrop();
|
||||
});
|
||||
|
||||
this.afterClosed().subscribe(() => {
|
||||
this._overlayRef.detach();
|
||||
this._overlayRef.dispose();
|
||||
this.componentInstance = null!;
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets an observable that emits when the overlay's backdrop has been clicked. */
|
||||
backdropClick(): Observable<void> {
|
||||
return this._overlayRef.backdropClick();
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the dialog.
|
||||
* @param dialogResult Optional result to return to the dialog opener.
|
||||
*/
|
||||
close(dialogResult?: R): void {
|
||||
this._result = dialogResult;
|
||||
this._containerInstance._startExiting();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an observable that emits when keydown events are targeted on the overlay.
|
||||
*/
|
||||
keydownEvents(): Observable<KeyboardEvent> {
|
||||
return this._overlayRef.keydownEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the dialog's width and height, defined, min and max.
|
||||
* @param size New size for the overlay.
|
||||
*/
|
||||
updateSize(size: OverlaySizeConfig): this {
|
||||
if (size.width) {
|
||||
this._getPositionStrategy().width(size.width.toString());
|
||||
}
|
||||
if (size.height) {
|
||||
this._getPositionStrategy().height(size.height.toString());
|
||||
}
|
||||
this._overlayRef.updateSize(size);
|
||||
this._overlayRef.updatePosition();
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Fetches the position strategy object from the overlay ref. */
|
||||
private _getPositionStrategy(): GlobalPositionStrategy {
|
||||
return this._overlayRef.getConfig().positionStrategy as GlobalPositionStrategy;
|
||||
}
|
||||
|
||||
/** Gets an observable that emits when dialog begins opening. */
|
||||
beforeOpen(): Observable<void> {
|
||||
return this._containerInstance._beforeEnter.asObservable();
|
||||
}
|
||||
|
||||
/** Gets an observable that emits when dialog is finished opening. */
|
||||
afterOpen(): Observable<void> {
|
||||
return this._containerInstance._afterEnter.asObservable();
|
||||
}
|
||||
|
||||
/** Gets an observable that emits when dialog begins closing. */
|
||||
beforeClose(): Observable<R | undefined> {
|
||||
return this._containerInstance._beforeExit.pipe(map(() => this._result));
|
||||
}
|
||||
|
||||
/** Gets an observable that emits when dialog is finished closing. */
|
||||
afterClosed(): Observable<R | undefined> {
|
||||
return this._containerInstance._afterExit.pipe(map(() => this._result));
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,309 @@
|
||||
/**
|
||||
* @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 {
|
||||
TemplateRef,
|
||||
SkipSelf,
|
||||
Optional,
|
||||
Injector,
|
||||
Inject,
|
||||
ComponentRef
|
||||
} from '@angular/core';
|
||||
import {ComponentPortal, PortalInjector, TemplatePortal} from '@angular/cdk/portal';
|
||||
import {ESCAPE} from '@angular/cdk/keycodes';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
import {DialogRef} from './dialog-ref';
|
||||
import {Location} from '@angular/common';
|
||||
import {DialogConfig} from './dialog-config';
|
||||
import {Directionality} from '@angular/cdk/bidi';
|
||||
import {of as observableOf} from 'rxjs/observable/of';
|
||||
import {CdkDialogContainer} from './dialog-container';
|
||||
import {
|
||||
ComponentType,
|
||||
Overlay,
|
||||
OverlayRef,
|
||||
OverlayConfig,
|
||||
} from '@angular/cdk/overlay';
|
||||
import {startWith} from 'rxjs/operators/startWith';
|
||||
import {defer} from 'rxjs/observable/defer';
|
||||
|
||||
import {
|
||||
DIALOG_SCROLL_STRATEGY,
|
||||
DIALOG_DATA,
|
||||
DIALOG_REF,
|
||||
DIALOG_CONTAINER,
|
||||
DIALOG_CONFIG,
|
||||
} from './dialog-injectors';
|
||||
|
||||
|
||||
/**
|
||||
* Service to open modal dialogs.
|
||||
*/
|
||||
export class Dialog {
|
||||
/** A the handleKeydown method bound to the class instance. */
|
||||
private _boundKeydown = this._handleKeydown.bind(this);
|
||||
|
||||
/** Stream that emits when all dialogs are closed. */
|
||||
get _afterAllClosed(): Observable<void> {
|
||||
return this._parentDialog ? this._parentDialog.afterAllClosed : this._afterAllClosedBase;
|
||||
}
|
||||
_afterAllClosedBase = new Subject<void>();
|
||||
afterAllClosed: Observable<void> = defer<void>(() => this.openDialogs.length ?
|
||||
this._afterAllClosed : this._afterAllClosed.pipe(startWith(undefined)));
|
||||
|
||||
/** Stream that emits when a dialog is opened. */
|
||||
get afterOpen(): Subject<DialogRef<any>> {
|
||||
return this._parentDialog ? this._parentDialog.afterOpen : this._afterOpen;
|
||||
}
|
||||
_afterOpen: Subject<DialogRef<any>> = new Subject();
|
||||
|
||||
/** Stream that emits when a dialog is opened. */
|
||||
get openDialogs(): DialogRef<any>[] {
|
||||
return this._parentDialog ? this._parentDialog.openDialogs : this._openDialogs;
|
||||
}
|
||||
_openDialogs: DialogRef<any>[] = [];
|
||||
|
||||
constructor(
|
||||
private overlay: Overlay,
|
||||
private injector: Injector,
|
||||
@Inject(DIALOG_REF) private dialogRefConstructor,
|
||||
@Inject(DIALOG_SCROLL_STRATEGY) private _scrollStrategy,
|
||||
@Optional() @SkipSelf() private _parentDialog: Dialog,
|
||||
@Optional() location: Location) {
|
||||
|
||||
// Close all of the dialogs when the user goes forwards/backwards in history or when the
|
||||
// location hash changes. Note that this usually doesn't include clicking on links (unless
|
||||
// the user is using the `HashLocationStrategy`).
|
||||
if (!_parentDialog && location) {
|
||||
location.subscribe(() => this.closeAll());
|
||||
}
|
||||
|
||||
this.afterAllClosed.subscribe(() => {
|
||||
document.removeEventListener('keydown', this._boundKeydown);
|
||||
});
|
||||
}
|
||||
|
||||
/** Gets an open dialog by id. */
|
||||
getById(id: string): DialogRef<any> | undefined {
|
||||
return this._openDialogs.find(ref => ref.id === id);
|
||||
}
|
||||
|
||||
/** Closes all open dialogs. */
|
||||
closeAll(): void {
|
||||
this.openDialogs.forEach(ref => ref.close());
|
||||
}
|
||||
|
||||
/** Opens a dialog from a component. */
|
||||
openFromComponent<T>(component: ComponentType<T>, config?: DialogConfig): DialogRef<any> {
|
||||
config = this._applyConfigDefaults(config);
|
||||
|
||||
if (config.id && this.getById(config.id)) {
|
||||
throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
|
||||
}
|
||||
|
||||
const overlayRef = this._createOverlay(config);
|
||||
const dialogContainer = this._attachDialogContainer(overlayRef, config);
|
||||
const dialogRef = this._attachDialogContentForComponent(component, dialogContainer,
|
||||
overlayRef, config);
|
||||
|
||||
this.registerDialogRef(dialogRef);
|
||||
return dialogRef;
|
||||
}
|
||||
|
||||
/** Opens a dialog from a template. */
|
||||
openFromTemplate<T>(template: TemplateRef<T>, config?: DialogConfig): DialogRef<any> {
|
||||
config = this._applyConfigDefaults(config);
|
||||
|
||||
if (config.id && this.getById(config.id)) {
|
||||
throw Error(`Dialog with id "${config.id}" exists already. The dialog id must be unique.`);
|
||||
}
|
||||
|
||||
const overlayRef = this._createOverlay(config);
|
||||
const dialogContainer = this._attachDialogContainer(overlayRef, config);
|
||||
const dialogRef = this._attachDialogContentForTemplate(template, dialogContainer,
|
||||
overlayRef, config);
|
||||
|
||||
this.registerDialogRef(dialogRef);
|
||||
return dialogRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a keydown listener, and forwards emitting events for when dialogs are opened and all
|
||||
* dialogs are closed.
|
||||
*/
|
||||
private registerDialogRef(dialogRef: DialogRef<any>): void {
|
||||
if (!this.openDialogs.length) {
|
||||
document.addEventListener('keydown', this._boundKeydown);
|
||||
}
|
||||
this.openDialogs.push(dialogRef);
|
||||
let dialogOpenSub = dialogRef.afterOpen().subscribe(() => {
|
||||
this.afterOpen.next(dialogRef);
|
||||
dialogOpenSub.unsubscribe();
|
||||
});
|
||||
let dialogCloseSub = dialogRef.afterClosed().subscribe(() => {
|
||||
let dialogIdx = this._openDialogs.indexOf(dialogRef);
|
||||
if (dialogIdx !== -1) { this._openDialogs.splice(dialogIdx, 1); }
|
||||
|
||||
if (!this._openDialogs.length) {
|
||||
this._afterAllClosedBase.next();
|
||||
dialogCloseSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles global key presses while there are open dialogs. Closes the
|
||||
* top dialog when the user presses escape.
|
||||
*/
|
||||
private _handleKeydown(event: KeyboardEvent): void {
|
||||
const topDialog = this.openDialogs[this.openDialogs.length - 1];
|
||||
const canClose = topDialog ? !topDialog.disableClose : false;
|
||||
|
||||
if (event.keyCode === ESCAPE && canClose) {
|
||||
topDialog.close();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates an overlay config from a dialog config.
|
||||
* @param config The dialog configuration.
|
||||
* @returns The overlay configuration.
|
||||
*/
|
||||
protected _createOverlay(config: DialogConfig): OverlayRef {
|
||||
const overlayConfig = new OverlayConfig({
|
||||
positionStrategy: this.overlay.position().global(),
|
||||
scrollStrategy: this._scrollStrategy(),
|
||||
panelClass: config.panelClass,
|
||||
hasBackdrop: config.hasBackdrop,
|
||||
direction: config.direction,
|
||||
minWidth: config.minWidth,
|
||||
minHeight: config.minHeight,
|
||||
maxWidth: config.maxWidth,
|
||||
maxHeight: config.maxHeight
|
||||
});
|
||||
|
||||
if (config.backdropClass) {
|
||||
overlayConfig.backdropClass = config.backdropClass;
|
||||
}
|
||||
return this.overlay.create(overlayConfig);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attaches an MatDialogContainer to a dialog's already-created overlay.
|
||||
* @param overlay Reference to the dialog's underlying overlay.
|
||||
* @param config The dialog configuration.
|
||||
* @returns A promise resolving to a ComponentRef for the attached container.
|
||||
*/
|
||||
protected _attachDialogContainer(overlay: OverlayRef, config: DialogConfig): CdkDialogContainer {
|
||||
const container = config.containerComponent || this.injector.get(DIALOG_CONTAINER);
|
||||
let containerPortal = new ComponentPortal(container, config.viewContainerRef);
|
||||
let containerRef: ComponentRef<CdkDialogContainer> = overlay.attach(containerPortal);
|
||||
containerRef.instance._config = config;
|
||||
|
||||
return containerRef.instance;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attaches the user-provided component to the already-created MatDialogContainer.
|
||||
* @param componentOrTemplateRef The type of component being loaded into the dialog,
|
||||
* or a TemplateRef to instantiate as the content.
|
||||
* @param dialogContainer Reference to the wrapping MatDialogContainer.
|
||||
* @param overlayRef Reference to the overlay in which the dialog resides.
|
||||
* @param config The dialog configuration.
|
||||
* @returns A promise resolving to the MatDialogRef that should be returned to the user.
|
||||
*/
|
||||
protected _attachDialogContentForComponent<T>(
|
||||
componentOrTemplateRef: ComponentType<T>,
|
||||
dialogContainer: CdkDialogContainer,
|
||||
overlayRef: OverlayRef,
|
||||
config: DialogConfig): DialogRef<any> {
|
||||
|
||||
// Create a reference to the dialog we're creating in order to give the user a handle
|
||||
// to modify and close it.
|
||||
const dialogRef = new this.dialogRefConstructor(overlayRef, dialogContainer, config.id);
|
||||
|
||||
const injector = this._createInjector<T>(config, dialogRef, dialogContainer);
|
||||
const contentRef = dialogContainer.attachComponentPortal(
|
||||
new ComponentPortal(componentOrTemplateRef, undefined, injector));
|
||||
dialogRef.componentInstance = contentRef.instance;
|
||||
|
||||
dialogRef.updateSize({width: config.width, height: config.height})
|
||||
.updatePosition(config.position);
|
||||
|
||||
return dialogRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches the user-provided component to the already-created MatDialogContainer.
|
||||
* @param componentOrTemplateRef The type of component being loaded into the dialog,
|
||||
* or a TemplateRef to instantiate as the content.
|
||||
* @param dialogContainer Reference to the wrapping MatDialogContainer.
|
||||
* @param overlayRef Reference to the overlay in which the dialog resides.
|
||||
* @param config The dialog configuration.
|
||||
* @returns A promise resolving to the MatDialogRef that should be returned to the user.
|
||||
*/
|
||||
protected _attachDialogContentForTemplate<T>(
|
||||
componentOrTemplateRef: TemplateRef<T>,
|
||||
dialogContainer: CdkDialogContainer,
|
||||
overlayRef: OverlayRef,
|
||||
config: DialogConfig): DialogRef<any> {
|
||||
|
||||
// Create a reference to the dialog we're creating in order to give the user a handle
|
||||
// to modify and close it.
|
||||
const dialogRef = new this.dialogRefConstructor(overlayRef, dialogContainer, config.id);
|
||||
|
||||
dialogContainer.attachTemplatePortal(
|
||||
new TemplatePortal<T>(componentOrTemplateRef, null!,
|
||||
<any>{$implicit: config.data, dialogRef}));
|
||||
dialogRef.updateSize({width: config.width, height: config.height})
|
||||
.updatePosition(config.position);
|
||||
|
||||
return dialogRef;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a custom injector to be used inside the dialog. This allows a component loaded inside
|
||||
* of a dialog to close itself and, optionally, to return a value.
|
||||
* @param config Config object that is used to construct the dialog.
|
||||
* @param dialogRef Reference to the dialog.
|
||||
* @param container Dialog container element that wraps all of the contents.
|
||||
* @returns The custom injector that can be used inside the dialog.
|
||||
*/
|
||||
private _createInjector<T>(
|
||||
config: DialogConfig,
|
||||
dialogRef: DialogRef<T>,
|
||||
dialogContainer: CdkDialogContainer): PortalInjector {
|
||||
|
||||
const userInjector = config && config.viewContainerRef && config.viewContainerRef.injector;
|
||||
const injectionTokens = new WeakMap();
|
||||
|
||||
injectionTokens.set(this.injector.get(DIALOG_REF), dialogRef);
|
||||
injectionTokens.set(this.injector.get(DIALOG_CONTAINER), dialogContainer);
|
||||
injectionTokens.set(DIALOG_DATA, config.data);
|
||||
injectionTokens.set(Directionality, {
|
||||
value: config.direction,
|
||||
change: observableOf()
|
||||
});
|
||||
|
||||
return new PortalInjector(userInjector || this.injector, injectionTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the provided configuration object to include the default values for properties which
|
||||
* are undefined.
|
||||
*/
|
||||
private _applyConfigDefaults(config?: DialogConfig): DialogConfig {
|
||||
const dialogConfig = this.injector.get(DIALOG_CONFIG) as typeof DialogConfig;
|
||||
return {...new dialogConfig(), ...config};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './public-api';
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
export * from './dialog-container';
|
||||
export * from './dialog-config';
|
||||
export * from './dialog-ref';
|
||||
export * from './dialog';
|
||||
export * from './dialog-module';
|
||||
export * from './dialog-injectors';
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": "../tsconfig-build",
|
||||
"files": [
|
||||
"public-api.ts",
|
||||
"../typings.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
"strictMetadataEmit": true,
|
||||
"flatModuleOutFile": "index.js",
|
||||
"flatModuleId": "@angular/material-experimental/dialog",
|
||||
"skipTemplateCodegen": true
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,8 @@
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"public-api.ts"
|
||||
"public-api.ts",
|
||||
"typings.d.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"annotateForClosureCompiler": true,
|
||||
|
||||
+1
@@ -0,0 +1 @@
|
||||
declare var module: {id: string};
|
||||
Reference in New Issue
Block a user