|
| 1 | +import {DynamicComponentLoader, ElementRef, ComponentRef, onDestroy} from 'angular2/angular2'; |
| 2 | +import {bind, Injector} from 'angular2/di'; |
| 3 | +import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async'; |
| 4 | +import {isPresent, Type} from 'angular2/src/facade/lang'; |
| 5 | +import {DOM} from 'angular2/src/dom/dom_adapter'; |
| 6 | +import {MouseEvent, KeyboardEvent} from 'angular2/src/facade/browser'; |
| 7 | +import {KEY_ESC} from 'angular2_material/src/core/constants' |
| 8 | + |
| 9 | +// TODO(radokirov): Once the application is transpiled by TS instead of Traceur, |
| 10 | +// add those imports back into 'angular2/angular2'; |
| 11 | +import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations'; |
| 12 | +import {Parent} from 'angular2/src/core/annotations_impl/visibility'; |
| 13 | +import {View} from 'angular2/src/core/annotations_impl/view'; |
| 14 | + |
| 15 | + |
| 16 | +// TODO(jelbourn): Opener of dialog can control where it is rendered. |
| 17 | +// TODO(jelbourn): body scrolling is disabled while dialog is open. |
| 18 | +// TODO(jelbourn): Don't manually construct and configure a DOM element. See #1402 |
| 19 | +// TODO(jelbourn): Wrap focus from end of dialog back to the start. Blocked on #1251 |
| 20 | +// TODO(jelbourn): Focus the dialog element when it is opened. |
| 21 | +// TODO(jelbourn): Real dialog styles. |
| 22 | +// TODO(jelbourn): Pre-built `alert` and `confirm` dialogs. |
| 23 | +// TODO(jelbourn): Animate dialog out of / into opening element. |
| 24 | + |
| 25 | + |
| 26 | +/** |
| 27 | + * Service for opening modal dialogs. |
| 28 | + */ |
| 29 | +export class MdDialog { |
| 30 | + componentLoader: DynamicComponentLoader; |
| 31 | + |
| 32 | + constructor(loader: DynamicComponentLoader) { |
| 33 | + this.componentLoader = loader; |
| 34 | + } |
| 35 | + |
| 36 | + /** |
| 37 | + * Opens a modal dialog. |
| 38 | + * @param type The component to open. |
| 39 | + * @param elementRef The logical location into which the component will be opened. |
| 40 | + * @returns Promise for a reference to the dialog. |
| 41 | + */ |
| 42 | + open( |
| 43 | + type: Type, |
| 44 | + elementRef: ElementRef, |
| 45 | + parentInjector: Injector, |
| 46 | + options: MdDialogConfig = null): Promise<MdDialogRef> { |
| 47 | + var config = isPresent(options) ? options : new MdDialogConfig(); |
| 48 | + |
| 49 | + // TODO(jelbourn): Don't use direct DOM access. Need abstraction to create an element |
| 50 | + // directly on the document body (also needed for web workers stuff). |
| 51 | + // Create a DOM node to serve as a physical host element for the dialog. |
| 52 | + var dialogElement = DOM.createElement('div'); |
| 53 | + DOM.appendChild(DOM.query('body'), dialogElement); |
| 54 | + |
| 55 | + // TODO(jelbourn): Use hostProperties binding to set these once #1539 is fixed. |
| 56 | + // Configure properties on the host element. |
| 57 | + DOM.addClass(dialogElement, 'md-dialog'); |
| 58 | + DOM.setAttribute(dialogElement, 'tabindex', '0'); |
| 59 | + |
| 60 | + // TODO(jelbourn): Do this with hostProperties (or another rendering abstraction) once ready. |
| 61 | + if (isPresent(config.width)) { |
| 62 | + DOM.setStyle(dialogElement, 'width', config.width); |
| 63 | + } |
| 64 | + if (isPresent(config.height)) { |
| 65 | + DOM.setStyle(dialogElement, 'height', config.height); |
| 66 | + } |
| 67 | + |
| 68 | + // Create the dialogRef here so that it can be injected into the content component. |
| 69 | + var dialogRef = new MdDialogRef(); |
| 70 | + |
| 71 | + var dialogRefBinding = bind(MdDialogRef).toValue(dialogRef); |
| 72 | + var contentInjector = parentInjector.resolveAndCreateChild([dialogRefBinding]); |
| 73 | + |
| 74 | + var backdropRefPromise = this._openBackdrop(elementRef, contentInjector); |
| 75 | + |
| 76 | + // First, load the MdDialogContainer, into which the given component will be loaded. |
| 77 | + return this.componentLoader.loadIntoNewLocation( |
| 78 | + MdDialogContainer, elementRef, dialogElement).then(containerRef => { |
| 79 | + dialogRef.containerRef = containerRef; |
| 80 | + |
| 81 | + // Now load the given component into the MdDialogContainer. |
| 82 | + return this.componentLoader.loadNextToExistingLocation( |
| 83 | + type, containerRef.instance.contentRef, contentInjector).then(contentRef => { |
| 84 | + |
| 85 | + // Wrap both component refs for the container and the content so that we can return |
| 86 | + // the `instance` of the content but the dispose method of the container back to the |
| 87 | + // opener. |
| 88 | + dialogRef.contentRef = contentRef; |
| 89 | + containerRef.instance.dialogRef = dialogRef; |
| 90 | + |
| 91 | + backdropRefPromise.then(backdropRef => { |
| 92 | + dialogRef.whenClosed.then((_) => { |
| 93 | + backdropRef.dispose(); |
| 94 | + }); |
| 95 | + }); |
| 96 | + |
| 97 | + return dialogRef; |
| 98 | + }); |
| 99 | + }); |
| 100 | + } |
| 101 | + |
| 102 | + /** Loads the dialog backdrop (transparent overlay over the rest of the page). */ |
| 103 | + _openBackdrop(elementRef:ElementRef, injector: Injector): Promise<ComponentRef> { |
| 104 | + var backdropElement = DOM.createElement('div'); |
| 105 | + DOM.addClass(backdropElement, 'md-backdrop'); |
| 106 | + DOM.appendChild(DOM.query('body'), backdropElement); |
| 107 | + |
| 108 | + return this.componentLoader.loadIntoNewLocation( |
| 109 | + MdBackdrop, elementRef, backdropElement, injector); |
| 110 | + } |
| 111 | + |
| 112 | + alert(message: string, okMessage: string): Promise { |
| 113 | + throw "Not implemented"; |
| 114 | + } |
| 115 | + |
| 116 | + confirm(message: string, okMessage: string, cancelMessage: string): Promise { |
| 117 | + throw "Not implemented"; |
| 118 | + } |
| 119 | +} |
| 120 | + |
| 121 | + |
| 122 | +/** |
| 123 | + * Reference to an opened dialog. |
| 124 | + */ |
| 125 | +export class MdDialogRef { |
| 126 | + // Reference to the MdDialogContainer component. |
| 127 | + containerRef: ComponentRef; |
| 128 | + |
| 129 | + // Reference to the Component loaded as the dialog content. |
| 130 | + _contentRef: ComponentRef; |
| 131 | + |
| 132 | + // Whether the dialog is closed. |
| 133 | + isClosed: boolean; |
| 134 | + |
| 135 | + // Deferred resolved when the dialog is closed. The promise for this deferred is publicly exposed. |
| 136 | + whenClosedDeferred: any; |
| 137 | + |
| 138 | + // Deferred resolved when the content ComponentRef is set. Only used internally. |
| 139 | + contentRefDeferred: any; |
| 140 | + |
| 141 | + constructor() { |
| 142 | + this._contentRef = null; |
| 143 | + this.containerRef = null; |
| 144 | + this.isClosed = false; |
| 145 | + |
| 146 | + this.contentRefDeferred = PromiseWrapper.completer(); |
| 147 | + this.whenClosedDeferred = PromiseWrapper.completer(); |
| 148 | + } |
| 149 | + |
| 150 | + set contentRef(value: ComponentRef) { |
| 151 | + this._contentRef = value; |
| 152 | + this.contentRefDeferred.resolve(value); |
| 153 | + } |
| 154 | + |
| 155 | + /** Gets the component instance for the content of the dialog. */ |
| 156 | + get instance() { |
| 157 | + if (isPresent(this._contentRef)) { |
| 158 | + return this._contentRef.instance; |
| 159 | + } |
| 160 | + |
| 161 | + // The only time one could attempt to access this property before the value is set is if an access occurs during |
| 162 | + // the constructor of the very instance they are trying to get (which is much more easily accessed as `this`). |
| 163 | + throw "Cannot access dialog component instance *from* that component's constructor."; |
| 164 | + } |
| 165 | + |
| 166 | + |
| 167 | + /** Gets a promise that is resolved when the dialog is closed. */ |
| 168 | + get whenClosed(): Promise { |
| 169 | + return this.whenClosedDeferred.promise; |
| 170 | + } |
| 171 | + |
| 172 | + /** Closes the dialog. This operation is asynchronous. */ |
| 173 | + close(result: any = null) { |
| 174 | + this.contentRefDeferred.promise.then((_) => { |
| 175 | + if (!this.isClosed) { |
| 176 | + this.isClosed = true; |
| 177 | + this.containerRef.dispose(); |
| 178 | + this.whenClosedDeferred.resolve(result); |
| 179 | + } |
| 180 | + }); |
| 181 | + } |
| 182 | +} |
| 183 | + |
| 184 | +/** Confiuration for a dialog to be opened. */ |
| 185 | +export class MdDialogConfig { |
| 186 | + width: string; |
| 187 | + height: string; |
| 188 | + |
| 189 | + constructor() { |
| 190 | + // Default configuration. |
| 191 | + this.width = null; |
| 192 | + this.height = null; |
| 193 | + } |
| 194 | +} |
| 195 | + |
| 196 | +/** |
| 197 | + * Container for user-provided dialog content. |
| 198 | + */ |
| 199 | +@Component({ |
| 200 | + selector: 'md-dialog-container', |
| 201 | + hostListeners: { |
| 202 | + 'body:^keydown': 'documentKeypress($event)' |
| 203 | + } |
| 204 | +}) |
| 205 | +@View({ |
| 206 | + templateUrl: 'angular2_material/src/components/dialog/dialog.html', |
| 207 | + directives: [MdDialogContent] |
| 208 | +}) |
| 209 | +class MdDialogContainer { |
| 210 | + // Ref to the dialog content. Used by the DynamicComponentLoader to load the dialog content. |
| 211 | + contentRef: ElementRef; |
| 212 | + |
| 213 | + // Ref to the open dialog. Used to close the dialog based on certain events. |
| 214 | + dialogRef: MdDialogRef; |
| 215 | + |
| 216 | + constructor() { |
| 217 | + this.contentRef = null; |
| 218 | + this.dialogRef = null; |
| 219 | + } |
| 220 | + |
| 221 | + wrapFocus() { |
| 222 | + // Return the focus to the host element. Blocked on #1251. |
| 223 | + } |
| 224 | + |
| 225 | + documentKeypress(event: KeyboardEvent) { |
| 226 | + if (event.keyCode == KEY_ESC) { |
| 227 | + this.dialogRef.close(); |
| 228 | + } |
| 229 | + } |
| 230 | +} |
| 231 | + |
| 232 | + |
| 233 | +/** Component for the dialog "backdrop", a transparent overlay over the rest of the page. */ |
| 234 | +@Component({ |
| 235 | + selector: 'md-backdrop', |
| 236 | + hostListeners: { |
| 237 | + 'click': 'onClick()' |
| 238 | + } |
| 239 | +}) |
| 240 | +@View({template: ''}) |
| 241 | +class MdBackdrop { |
| 242 | + dialogRef: MdDialogRef; |
| 243 | + |
| 244 | + constructor(dialogRef: MdDialogRef) { |
| 245 | + this.dialogRef = dialogRef; |
| 246 | + } |
| 247 | + |
| 248 | + onClick() { |
| 249 | + // TODO(jelbourn): Use MdDialogConfig to capture option for whether dialog should close on |
| 250 | + // clicking outside. |
| 251 | + this.dialogRef.close(); |
| 252 | + } |
| 253 | +} |
| 254 | + |
| 255 | + |
| 256 | +/** |
| 257 | + * Simple decorator used only to communicate an ElementRef to the parent MdDialogContainer as the location |
| 258 | + * for where the dialog content will be loaded. |
| 259 | + */ |
| 260 | +@Directive({selector: 'md-dialog-content'}) |
| 261 | +class MdDialogContent { |
| 262 | + constructor(@Parent() dialogContainer: MdDialogContainer, elementRef: ElementRef) { |
| 263 | + dialogContainer.contentRef = elementRef; |
| 264 | + } |
| 265 | +} |
0 commit comments