
BREAKING CHANGE The @Parent annotation has been removed. Use @Ancestor instead. @Parent was used to enforce a particular DOM structure (e.g., a pane component is a direct child of the tabs component). DI is not the right mechanism to do it. We should enforce it using schema instead.
270 lines
8.8 KiB
TypeScript
270 lines
8.8 KiB
TypeScript
import {
|
|
Component,
|
|
Directive,
|
|
View,
|
|
Ancestor,
|
|
ElementRef,
|
|
DynamicComponentLoader,
|
|
ComponentRef,
|
|
DomRenderer
|
|
} from 'angular2/angular2';
|
|
import {bind, Injectable, forwardRef, ResolvedBinding, Injector} from 'angular2/di';
|
|
|
|
import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
|
import {isPresent, Type} from 'angular2/src/facade/lang';
|
|
import {DOM} from 'angular2/src/dom/dom_adapter';
|
|
import {MouseEvent, KeyboardEvent} from 'angular2/src/facade/browser';
|
|
import {KEY_ESC} from 'angular2_material/src/core/constants';
|
|
|
|
// TODO(jelbourn): Opener of dialog can control where it is rendered.
|
|
// TODO(jelbourn): body scrolling is disabled while dialog is open.
|
|
// TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402
|
|
// TODO(jelbourn): Wrap focus from end of dialog back to the start. Blocked on #1251
|
|
// TODO(jelbourn): Focus the dialog element when it is opened.
|
|
// TODO(jelbourn): Real dialog styles.
|
|
// TODO(jelbourn): Pre-built `alert` and `confirm` dialogs.
|
|
// TODO(jelbourn): Animate dialog out of / into opening element.
|
|
|
|
/**
|
|
* Service for opening modal dialogs.
|
|
*/
|
|
@Injectable()
|
|
export class MdDialog {
|
|
componentLoader: DynamicComponentLoader;
|
|
domRenderer: DomRenderer;
|
|
|
|
constructor(loader: DynamicComponentLoader, domRenderer: DomRenderer) {
|
|
this.componentLoader = loader;
|
|
this.domRenderer = domRenderer;
|
|
}
|
|
|
|
/**
|
|
* Opens a modal dialog.
|
|
* @param type The component to open.
|
|
* @param elementRef The logical location into which the component will be opened.
|
|
* @param parentInjector
|
|
* @param options
|
|
* @returns Promise for a reference to the dialog.
|
|
*/
|
|
open(type: Type, elementRef: ElementRef, options: MdDialogConfig = null): Promise<MdDialogRef> {
|
|
var config = isPresent(options) ? options : new MdDialogConfig();
|
|
|
|
// Create the dialogRef here so that it can be injected into the content component.
|
|
var dialogRef = new MdDialogRef();
|
|
|
|
var bindings = Injector.resolve([bind(MdDialogRef).toValue(dialogRef)]);
|
|
|
|
var backdropRefPromise = this._openBackdrop(elementRef, bindings);
|
|
|
|
// First, load the MdDialogContainer, into which the given component will be loaded.
|
|
return this.componentLoader.loadNextToLocation(MdDialogContainer, elementRef)
|
|
.then(containerRef => {
|
|
// TODO(tbosch): clean this up when we have custom renderers
|
|
// (https://github.com/angular/angular/issues/1807)
|
|
// TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element
|
|
// directly on the document body (also needed for web workers stuff).
|
|
// Create a DOM node to serve as a physical host element for the dialog.
|
|
var dialogElement = containerRef.location.nativeElement;
|
|
DOM.appendChild(DOM.query('body'), dialogElement);
|
|
|
|
// TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed.
|
|
// Configure properties on the host element.
|
|
DOM.addClass(dialogElement, 'md-dialog');
|
|
DOM.setAttribute(dialogElement, 'tabindex', '0');
|
|
|
|
// TODO(jelbourn): Do this with hostProperties (or another rendering abstraction) once
|
|
// ready.
|
|
if (isPresent(config.width)) {
|
|
DOM.setStyle(dialogElement, 'width', config.width);
|
|
}
|
|
if (isPresent(config.height)) {
|
|
DOM.setStyle(dialogElement, 'height', config.height);
|
|
}
|
|
|
|
dialogRef.containerRef = containerRef;
|
|
|
|
// Now load the given component into the MdDialogContainer.
|
|
return this.componentLoader.loadNextToLocation(type, containerRef.instance.contentRef,
|
|
bindings)
|
|
.then(contentRef => {
|
|
|
|
// Wrap both component refs for the container and the content so that we can return
|
|
// the `instance` of the content but the dispose method of the container back to the
|
|
// opener.
|
|
dialogRef.contentRef = contentRef;
|
|
containerRef.instance.dialogRef = dialogRef;
|
|
|
|
backdropRefPromise.then(backdropRef => {
|
|
dialogRef.whenClosed.then((_) => { backdropRef.dispose(); });
|
|
});
|
|
|
|
return dialogRef;
|
|
});
|
|
});
|
|
}
|
|
|
|
/** Loads the dialog backdrop (transparent overlay over the rest of the page). */
|
|
_openBackdrop(elementRef: ElementRef, bindings: ResolvedBinding[]): Promise<ComponentRef> {
|
|
return this.componentLoader.loadNextToLocation(MdBackdrop, elementRef, bindings)
|
|
.then((componentRef) => {
|
|
// TODO(tbosch): clean this up when we have custom renderers
|
|
// (https://github.com/angular/angular/issues/1807)
|
|
var backdropElement = componentRef.location.nativeElement;
|
|
DOM.addClass(backdropElement, 'md-backdrop');
|
|
DOM.appendChild(DOM.query('body'), backdropElement);
|
|
return componentRef;
|
|
});
|
|
}
|
|
|
|
alert(message: string, okMessage: string): Promise<any> {
|
|
throw "Not implemented";
|
|
}
|
|
|
|
confirm(message: string, okMessage: string, cancelMessage: string): Promise<any> {
|
|
throw "Not implemented";
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Reference to an opened dialog.
|
|
*/
|
|
export class MdDialogRef {
|
|
// Reference to the MdDialogContainer component.
|
|
containerRef: ComponentRef;
|
|
|
|
// Reference to the Component loaded as the dialog content.
|
|
_contentRef: ComponentRef;
|
|
|
|
// Whether the dialog is closed.
|
|
isClosed: boolean;
|
|
|
|
// Deferred resolved when the dialog is closed. The promise for this deferred is publicly exposed.
|
|
whenClosedDeferred: any;
|
|
|
|
// Deferred resolved when the content ComponentRef is set. Only used internally.
|
|
contentRefDeferred: any;
|
|
|
|
constructor() {
|
|
this._contentRef = null;
|
|
this.containerRef = null;
|
|
this.isClosed = false;
|
|
|
|
this.contentRefDeferred = PromiseWrapper.completer();
|
|
this.whenClosedDeferred = PromiseWrapper.completer();
|
|
}
|
|
|
|
set contentRef(value: ComponentRef) {
|
|
this._contentRef = value;
|
|
this.contentRefDeferred.resolve(value);
|
|
}
|
|
|
|
/** Gets the component instance for the content of the dialog. */
|
|
get instance() {
|
|
if (isPresent(this._contentRef)) {
|
|
return this._contentRef.instance;
|
|
}
|
|
|
|
// The only time one could attempt to access this property before the value is set is if an
|
|
// access occurs during
|
|
// the constructor of the very instance they are trying to get (which is much more easily
|
|
// accessed as `this`).
|
|
throw "Cannot access dialog component instance *from* that component's constructor.";
|
|
}
|
|
|
|
|
|
/** Gets a promise that is resolved when the dialog is closed. */
|
|
get whenClosed(): Promise<any> {
|
|
return this.whenClosedDeferred.promise;
|
|
}
|
|
|
|
/** Closes the dialog. This operation is asynchronous. */
|
|
close(result: any = null) {
|
|
this.contentRefDeferred.promise.then((_) => {
|
|
if (!this.isClosed) {
|
|
this.isClosed = true;
|
|
this.containerRef.dispose();
|
|
this.whenClosedDeferred.resolve(result);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/** Confiuration for a dialog to be opened. */
|
|
export class MdDialogConfig {
|
|
width: string;
|
|
height: string;
|
|
|
|
constructor() {
|
|
// Default configuration.
|
|
this.width = null;
|
|
this.height = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Container for user-provided dialog content.
|
|
*/
|
|
@Component({
|
|
selector: 'md-dialog-container',
|
|
host: {'(body:^keydown)': 'documentKeypress($event)'},
|
|
})
|
|
@View({
|
|
templateUrl: 'angular2_material/src/components/dialog/dialog.html',
|
|
directives: [forwardRef(() => MdDialogContent)]
|
|
})
|
|
class MdDialogContainer {
|
|
// Ref to the dialog content. Used by the DynamicComponentLoader to load the dialog content.
|
|
contentRef: ElementRef;
|
|
|
|
// Ref to the open dialog. Used to close the dialog based on certain events.
|
|
dialogRef: MdDialogRef;
|
|
|
|
constructor() {
|
|
this.contentRef = null;
|
|
this.dialogRef = null;
|
|
}
|
|
|
|
wrapFocus() {
|
|
// Return the focus to the host element. Blocked on #1251.
|
|
}
|
|
|
|
documentKeypress(event: KeyboardEvent) {
|
|
if (event.keyCode == KEY_ESC) {
|
|
this.dialogRef.close();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Simple decorator used only to communicate an ElementRef to the parent MdDialogContainer as the
|
|
* location
|
|
* for where the dialog content will be loaded.
|
|
*/
|
|
@Directive({selector: 'md-dialog-content'})
|
|
class MdDialogContent {
|
|
constructor(@Ancestor() dialogContainer: MdDialogContainer, elementRef: ElementRef) {
|
|
dialogContainer.contentRef = elementRef;
|
|
}
|
|
}
|
|
|
|
/** Component for the dialog "backdrop", a transparent overlay over the rest of the page. */
|
|
@Component({
|
|
selector: 'md-backdrop',
|
|
host: {'(click)': 'onClick()'},
|
|
})
|
|
@View({template: ''})
|
|
class MdBackdrop {
|
|
dialogRef: MdDialogRef;
|
|
|
|
constructor(dialogRef: MdDialogRef) {
|
|
this.dialogRef = dialogRef;
|
|
}
|
|
|
|
onClick() {
|
|
// TODO(jelbourn): Use MdDialogConfig to capture option for whether dialog should close on
|
|
// clicking outside.
|
|
this.dialogRef.close();
|
|
}
|
|
}
|