diff --git a/modules/angular2/src/render/dom/events/event_manager.js b/modules/angular2/src/render/dom/events/event_manager.js index 408086d98f..424c3e3728 100644 --- a/modules/angular2/src/render/dom/events/event_manager.js +++ b/modules/angular2/src/render/dom/events/event_manager.js @@ -54,7 +54,7 @@ export class EventManagerPlugin { // We are assuming here that all plugins support bubbled and non-bubbled events. // That is equivalent to having supporting $event.target - // The bubbling flag (currently ^) is stripped before calling the supports and + // The bubbling flag (currently ^) is stripped before calling the supports and // addEventListener methods. supports(eventName: string): boolean { return false; diff --git a/modules/angular2_material/src/components/checkbox/checkbox.js b/modules/angular2_material/src/components/checkbox/checkbox.js index 5477095dd5..82fe116ac5 100644 --- a/modules/angular2_material/src/components/checkbox/checkbox.js +++ b/modules/angular2_material/src/components/checkbox/checkbox.js @@ -39,7 +39,7 @@ export class MdCheckbox { /** Setter for tabindex */ tabindex: number; - constructor(@Attribute('tabindex') tabindex: string) { + constructor(@Attribute('tabindex') tabindex: String) { this.role = 'checkbox'; this.checked = false; this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0; diff --git a/modules/angular2_material/src/components/dialog/dialog.html b/modules/angular2_material/src/components/dialog/dialog.html new file mode 100644 index 0000000000..d291f7a3e5 --- /dev/null +++ b/modules/angular2_material/src/components/dialog/dialog.html @@ -0,0 +1,33 @@ + + + +
+ diff --git a/modules/angular2_material/src/components/dialog/dialog.js b/modules/angular2_material/src/components/dialog/dialog.js new file mode 100644 index 0000000000..10d27b144f --- /dev/null +++ b/modules/angular2_material/src/components/dialog/dialog.js @@ -0,0 +1,265 @@ +import {DynamicComponentLoader, ElementRef, ComponentRef, onDestroy} from 'angular2/angular2'; +import {bind, 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(radokirov): Once the application is transpiled by TS instead of Traceur, +// add those imports back into 'angular2/angular2'; +import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations'; +import {Parent} from 'angular2/src/core/annotations_impl/visibility'; +import {View} from 'angular2/src/core/annotations_impl/view'; + + +// 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. + */ +export class MdDialog { + componentLoader: DynamicComponentLoader; + + constructor(loader: DynamicComponentLoader) { + this.componentLoader = loader; + } + + /** + * Opens a modal dialog. + * @param type The component to open. + * @param elementRef The logical location into which the component will be opened. + * @returns Promise for a reference to the dialog. + */ + open( + type: Type, + elementRef: ElementRef, + parentInjector: Injector, + options: MdDialogConfig = null): Promise { + var config = isPresent(options) ? options : new MdDialogConfig(); + + // 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 = DOM.createElement('div'); + 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); + } + + // Create the dialogRef here so that it can be injected into the content component. + var dialogRef = new MdDialogRef(); + + var dialogRefBinding = bind(MdDialogRef).toValue(dialogRef); + var contentInjector = parentInjector.resolveAndCreateChild([dialogRefBinding]); + + var backdropRefPromise = this._openBackdrop(elementRef, contentInjector); + + // First, load the MdDialogContainer, into which the given component will be loaded. + return this.componentLoader.loadIntoNewLocation( + MdDialogContainer, elementRef, dialogElement).then(containerRef => { + dialogRef.containerRef = containerRef; + + // Now load the given component into the MdDialogContainer. + return this.componentLoader.loadNextToExistingLocation( + type, containerRef.instance.contentRef, contentInjector).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, injector: Injector): Promise { + var backdropElement = DOM.createElement('div'); + DOM.addClass(backdropElement, 'md-backdrop'); + DOM.appendChild(DOM.query('body'), backdropElement); + + return this.componentLoader.loadIntoNewLocation( + MdBackdrop, elementRef, backdropElement, injector); + } + + alert(message: string, okMessage: string): Promise { + throw "Not implemented"; + } + + confirm(message: string, okMessage: string, cancelMessage: string): Promise { + 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 { + 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', + hostListeners: { + 'body:^keydown': 'documentKeypress($event)' + } +}) +@View({ + templateUrl: 'angular2_material/src/components/dialog/dialog.html', + directives: [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(); + } + } +} + + +/** Component for the dialog "backdrop", a transparent overlay over the rest of the page. */ +@Component({ + selector: 'md-backdrop', + hostListeners: { + '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(); + } +} + + +/** + * 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(@Parent() dialogContainer: MdDialogContainer, elementRef: ElementRef) { + dialogContainer.contentRef = elementRef; + } +} diff --git a/modules/angular2_material/src/components/progress-linear/progress_linear.js b/modules/angular2_material/src/components/progress-linear/progress_linear.js index c032a518e8..4601e0c170 100644 --- a/modules/angular2_material/src/components/progress-linear/progress_linear.js +++ b/modules/angular2_material/src/components/progress-linear/progress_linear.js @@ -42,7 +42,7 @@ export class MdProgressLinear { ariaValuemin: string; ariaValuemax: string; - constructor(@Attribute('md-mode') mode: string) { + constructor(@Attribute('md-mode') mode: String) { this.primaryBarTransform = ''; this.secondaryBarTransform = ''; diff --git a/modules/angular2_material/src/components/switcher/switch.js b/modules/angular2_material/src/components/switcher/switch.js index 78297995d4..6898fb0f1b 100644 --- a/modules/angular2_material/src/components/switcher/switch.js +++ b/modules/angular2_material/src/components/switcher/switch.js @@ -37,7 +37,7 @@ export class MdSwitch { tabindex: number; role: string; - constructor(@Attribute('tabindex') tabindex: string) { + constructor(@Attribute('tabindex') tabindex: String) { this.role = 'checkbox'; this.checked = false; this.tabindex = isPresent(tabindex) ? NumberWrapper.parseInt(tabindex, 10) : 0; diff --git a/modules/angular2_material/src/core/constants.js b/modules/angular2_material/src/core/constants.js index 6b724f074b..3ae5d38978 100644 --- a/modules/angular2_material/src/core/constants.js +++ b/modules/angular2_material/src/core/constants.js @@ -1,6 +1,7 @@ // TODO: switch to proper enums when we support them. // Key codes +export const KEY_ESC = 27; export const KEY_SPACE = 32; export const KEY_UP = 38; export const KEY_DOWN = 40; diff --git a/modules/examples/src/material/dialog/demo_app.html b/modules/examples/src/material/dialog/demo_app.html new file mode 100644 index 0000000000..eb47ad1ef9 --- /dev/null +++ b/modules/examples/src/material/dialog/demo_app.html @@ -0,0 +1,32 @@ +
+

Dialog demo

+ + + + + +

+ Last result: {{lastResult}} +

+ +
+ +

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+

Here are some paragaphs to make the page scrollable

+ +
diff --git a/modules/examples/src/material/dialog/index.html b/modules/examples/src/material/dialog/index.html new file mode 100644 index 0000000000..cfff28f660 --- /dev/null +++ b/modules/examples/src/material/dialog/index.html @@ -0,0 +1,18 @@ + + + + + ng-material dialog demo + + + + + +$SCRIPTS$ + Loading... + + diff --git a/modules/examples/src/material/dialog/index.js b/modules/examples/src/material/dialog/index.js new file mode 100644 index 0000000000..45cf6d1f8c --- /dev/null +++ b/modules/examples/src/material/dialog/index.js @@ -0,0 +1,103 @@ +import {bootstrap, ElementRef, ComponentRef} from 'angular2/angular2'; +import {MdDialog, MdDialogRef, MdDialogConfig} from 'angular2_material/src/components/dialog/dialog' +import {UrlResolver} from 'angular2/src/services/url_resolver'; +import {commonDemoSetup, DemoUrlResolver} from '../demo_common'; +import {bind, Injector} from 'angular2/di'; +import {isPresent} from 'angular2/src/facade/lang'; + +// TODO(radokirov): Once the application is transpiled by TS instead of Traceur, +// add those imports back into 'angular2/angular2'; +import {Component, Directive} from 'angular2/src/core/annotations_impl/annotations'; +import {View} from 'angular2/src/core/annotations_impl/view'; + + +@Component({ + selector: 'demo-app', + injectables: [MdDialog] +}) +@View({ + templateUrl: './demo_app.html', + directives: [] +}) +class DemoApp { + dialog: MdDialog; + elementRef: ElementRef; + dialogRef: MdDialogRef; + dialogConfig: MdDialogConfig; + injector: Injector; + lastResult: string; + + constructor(mdDialog: MdDialog, elementRef: ElementRef, injector: Injector) { + this.dialog = mdDialog; + this.elementRef = elementRef; + this.dialogConfig = new MdDialogConfig(); + this.injector = injector; + + this.dialogConfig.width = '60%'; + this.dialogConfig.height = '60%'; + this.lastResult = ''; + } + + open() { + if (isPresent(this.dialogRef)) { + return; + } + + this.dialog.open(SimpleDialogComponent, + this.elementRef, this.injector, this.dialogConfig).then(ref => { + this.dialogRef = ref; + ref.instance.numCoconuts = 777; + + ref.whenClosed.then(result => { + this.dialogRef = null; + this.lastResult = result; + }); + }); + } + + close() { + this.dialogRef.close(); + } +} + +@Component({ + selector: 'simple-dialog', + properties: {'numCoconuts': 'numCoconuts'} +}) +@View({ + template: ` +

This is the dialog content

+

There are {{numCoconuts}} coconuts.

+

Return:

+ + ` +}) +class SimpleDialogComponent { + numCoconuts: number; + dialogRef: MdDialogRef; + toReturn: string; + + constructor(dialogRef: MdDialogRef) { + this.numCoconuts = 0; + this.dialogRef = dialogRef; + this.toReturn = ''; + } + + updateValue(event) { + this.toReturn = event.target.value; + } + + done() { + this.dialogRef.close(this.toReturn); + } +} + + +export function main() { + commonDemoSetup(); + bootstrap(DemoApp, [ + bind(UrlResolver).toValue(new DemoUrlResolver()) + ]); +} + +