From 72362791d2f527065821b70604415578c61689b2 Mon Sep 17 00:00:00 2001 From: Justin DuJardin <justin@dujardinconsulting.com> Date: Fri, 25 Dec 2015 14:09:16 -0800 Subject: [PATCH] feat(dialog): support basic alert and confirm dialogs --- examples/all.ts | 2 + examples/components/dialog/basic_usage.html | 33 ++ examples/components/dialog/basic_usage.scss | 14 + examples/components/dialog/basic_usage.ts | 117 +++++++ ng2-material/all.scss | 1 + ng2-material/all.ts | 2 + ng2-material/components/dialog/dialog.scss | 94 +++++- ng2-material/components/dialog/dialog.ts | 296 +++++------------- .../components/dialog/dialog_basic.ts | 31 ++ .../components/dialog/dialog_config.ts | 59 ++++ .../components/dialog/dialog_container.ts | 64 ++++ ng2-material/components/dialog/dialog_ref.ts | 82 +++++ ng2-material/core/style/shadows.scss | 33 ++ 13 files changed, 595 insertions(+), 233 deletions(-) create mode 100644 examples/components/dialog/basic_usage.html create mode 100644 examples/components/dialog/basic_usage.scss create mode 100644 examples/components/dialog/basic_usage.ts create mode 100644 ng2-material/components/dialog/dialog_basic.ts create mode 100644 ng2-material/components/dialog/dialog_config.ts create mode 100644 ng2-material/components/dialog/dialog_container.ts create mode 100644 ng2-material/components/dialog/dialog_ref.ts diff --git a/examples/all.ts b/examples/all.ts index abd884e3..9d113712 100644 --- a/examples/all.ts +++ b/examples/all.ts @@ -7,6 +7,7 @@ import CardBasicUsage from './components/card/basic_usage'; import CardInlineActions from './components/card/inline_actions'; import ButtonBasicUsage from './components/button/basic_usage'; import CardActionButtons from './components/card/action_buttons'; +import DialogBasicUsage from './components/dialog/basic_usage'; import ToolbarBasicUsage from './components/toolbar/basic_usage'; import ToolbarScrollShrink from './components/toolbar/scroll_shrink'; import ProgressLinearBasicUsage from './components/progress_linear/basic_usage'; @@ -21,6 +22,7 @@ import TabsDynamicTabs from './components/tabs/dynamic_tabs'; export const DEMO_DIRECTIVES: Type[] = CONST_EXPR([ CardBasicUsage, CardInlineActions, CardActionButtons, ButtonBasicUsage, + DialogBasicUsage, RadioBasicUsage, SwitchBasicUsage, TabsDynamicHeight, diff --git a/examples/components/dialog/basic_usage.html b/examples/components/dialog/basic_usage.html new file mode 100644 index 00000000..6a1f1f7a --- /dev/null +++ b/examples/components/dialog/basic_usage.html @@ -0,0 +1,33 @@ +<div class="md-padding" id="popupContainer"> + <p class="inset"> + Open a dialog over the app's content. Press escape or click outside to close the dialog and + send focus back to the triggering button. + </p> + + <div class="dialog-demo-content" layout="row" layout-wrap layout-margin> + <button md-raised-button class="md-primary" (click)="showAlert($event)" flex="100" flex-gt-md="auto"> + Alert Dialog + </button> + <button md-raised-button class="md-primary" (click)="showConfirm($event)" flex="100" flex-gt-md="auto"> + Confirm Dialog + </button> + <button md-raised-button class="md-primary" (click)="showAdvanced($event)" flex="100" flex-gt-md="auto"> + Custom Dialog + </button> + <div hide-gt-sm layout="row" layout-align="center center" flex="100"> + <md-checkbox [(checked)]="customFullscreen" aria-label="Fullscreen Custom Dialog">Custom Dialog Fullscreen</md-checkbox> + </div> + <button md-raised-button class="md-primary" (click)="showTabDialog($event)" flex="100" flex-gt-md="auto"> + Tab Dialog + </button> + </div> + <p class="footer">Note: The <b>Confirm</b> dialog does not use <code>config.clickOutsideToClose(true)</code>.</p> + + <div *ngIf="status"> + <br/> + <b layout="row" layout-align="center center" class="md-padding"> + {{status}} + </b> + </div> + +</div> diff --git a/examples/components/dialog/basic_usage.scss b/examples/components/dialog/basic_usage.scss new file mode 100644 index 00000000..1e721c78 --- /dev/null +++ b/examples/components/dialog/basic_usage.scss @@ -0,0 +1,14 @@ +#popupContainer { + position:relative; +} + +.footer { + width:100%; + text-align: center; + margin-left:20px; +} + .footer, .footer > code { + font-size: 0.8em; + margin-top:50px; +} + diff --git a/examples/components/dialog/basic_usage.ts b/examples/components/dialog/basic_usage.ts new file mode 100644 index 00000000..d42215a9 --- /dev/null +++ b/examples/components/dialog/basic_usage.ts @@ -0,0 +1,117 @@ +import {View, Component} from 'angular2/core'; +import {MATERIAL_DIRECTIVES,MdDialog} from 'ng2-material/all'; +import {ElementRef} from "angular2/core"; +import {Input} from "angular2/core"; +import {DOM} from "angular2/src/platform/dom/dom_adapter"; +import {MdDialogConfig} from "ng2-material/components/dialog/dialog_config"; +import {MdDialogBasic} from "ng2-material/components/dialog/dialog_basic"; +import {MdDialogRef} from "ng2-material/components/dialog/dialog_ref"; + + +function hasMedia(size: string) { + // TODO: Implement as $mdMedia + return true; +} + +@Component({selector: 'dialog-basic-usage'}) +@View({ + templateUrl: 'examples/components/dialog/basic_usage.html', + styleUrls: ['examples/components/dialog/basic_usage.css'], + directives: [MATERIAL_DIRECTIVES] +}) +export default class DialogBasicUsage { + + status = ' '; + customFullscreen = hasMedia('xs') || hasMedia('sm'); + + constructor(public dialog: MdDialog, public element: ElementRef) { + + } + + showAlert(ev) { + let config = new MdDialogConfig() + .parent(DOM.query('#popupContainer')) + .textContent('You can specify some description text in here') + .title('This is an alert title') + .ok('Got it!') + .targetEvent(ev); + this.dialog.open(MdDialogBasic, this.element, config); + //// Appending dialog to document.body to cover sidenav in docs app + //// Modal dialogs should fully cover application + //// to prevent interaction outside of dialog + //this.dialog.show( + // this.dialog.alert() + // .parent(angular.element(document.querySelector('#popupContainer'))) + // .clickOutsideToClose(true) + // .title('This is an alert title') + // .textContent('You can specify some description text in here.') + // .ariaLabel('Alert Dialog Demo') + // .ok('Got it!') + // .targetEvent(ev) + //); + }; + + showConfirm(ev) { + let config = new MdDialogConfig() + .textContent('All of the banks have agreed to forgive you your debts.') + .clickOutsideToClose(false) + .title('Would you like to delete your debt?') + .ariaLabel('Lucky day') + .ok('Please do it!') + .cancel('Sounds like a scam') + .targetEvent(ev); + this.dialog + .open(MdDialogBasic, this.element, config) + .then((ref: MdDialogRef) => { + ref.whenClosed.then((result) => { + if (result) { + this.status = 'You decided to get rid of your debt.'; + } + else { + this.status = 'You decided to keep your debt.'; + } + }) + }); + }; + + showAdvanced(ev) { + //var useFullScreen = ($mdMedia('sm') || $mdMedia('xs')) && this.customFullscreen; + // + //this.dialog.show({ + // controller: DialogController, + // templateUrl: 'dialog1.tmpl.html', + // parent: angular.element(document.body), + // targetEvent: ev, + // clickOutsideToClose: true, + // fullscreen: useFullScreen + // }) + // .then((answer) => { + // $scope.status = 'You said the information was "' + answer + '".'; + // }, () =>{ + // $scope.status = 'You cancelled the dialog.'; + // }); + // + // + //$scope.$watch(() =>{ + // return $mdMedia('xs') || $mdMedia('sm'); + //}, (wantsFullScreen) => { + // this.customFullscreen = (wantsFullScreen === true); + //}); + + }; + + showTabDialog(ev) { + //this.dialog.show({ + // controller: DialogController, + // templateUrl: 'tabDialog.tmpl.html', + // parent: angular.element(document.body), + // targetEvent: ev, + // clickOutsideToClose: true + // }) + // .then((answer) => { + // this.status = 'You said the information was "' + answer + '".'; + // }, () =>{ + // this.status = 'You cancelled the dialog.'; + // }); + }; +} diff --git a/ng2-material/all.scss b/ng2-material/all.scss index a74753bc..c16d3cfe 100644 --- a/ng2-material/all.scss +++ b/ng2-material/all.scss @@ -6,6 +6,7 @@ @import "core/style/default-theme"; +@import "components/backdrop/backdrop"; @import "components/button/button"; @import "components/card/card"; @import "components/content/content"; diff --git a/ng2-material/all.ts b/ng2-material/all.ts index 03c7df31..b178344b 100644 --- a/ng2-material/all.ts +++ b/ng2-material/all.ts @@ -11,6 +11,7 @@ import {MdContent} from './components/content/content'; export * from './components/content/content'; export * from './components/dialog/dialog'; +import {MdDialog} from './components/dialog/dialog'; import {MdDivider} from './components/divider/divider'; export * from './components/divider/divider'; @@ -109,6 +110,7 @@ export class MaterialTemplateResolver extends UrlResolver { * Collection of Material Design component providers. */ export const MATERIAL_PROVIDERS: any[] = [ + MdDialog, MdRadioDispatcher, provide(UrlResolver, {useValue: new MaterialTemplateResolver()}) ]; diff --git a/ng2-material/components/dialog/dialog.scss b/ng2-material/components/dialog/dialog.scss index 50c6771c..6a481158 100644 --- a/ng2-material/components/dialog/dialog.scss +++ b/ng2-material/components/dialog/dialog.scss @@ -1,27 +1,91 @@ +@import "../../core/style/variables"; +@import "../../core/style/shadows"; +@import "../../core/style/default-theme"; + .md-dialog { - position: absolute; + position: fixed; z-index: 80; /** Center the dialog. */ top: 50%; left: 50%; - transform: translate(-50%, -50%); + min-width: 300px; + min-height: 100px; + + padding: $baseline-grid * 3; + + box-shadow: $whiteframe-shadow-13dp; + display: flex; + flex-direction: column; + + opacity: 0; + transition: $swift-ease-out; + transform: translate3d(-50%, -50%, 0) scale(0.2); + + order: 1; + overflow: auto; + -webkit-overflow-scrolling: touch; + + &:not([layout=row]) > * > *:first-child:not(.md-subheader) { + margin-top: 0; + } + + &:focus { + outline: none; + } + + &.md-active { + opacity: 1; + transition: $swift-ease-out; + transform: translate3d(-50%, -50%, 0) scale(1.0); + } + + &.md-dialog-absolute { + position: absolute; + } - width: 300px; - height: 300px; + .md-actions, md-dialog-actions { + display: flex; + order: 2; + box-sizing: border-box; + align-items: center; + justify-content: flex-end; + padding-top: $baseline-grid * 3; + padding-right: $baseline-grid; + padding-left: $baseline-grid * 2; - background-color: white; - border: 1px solid black; - box-shadow: 0 4px 4px;; + // Align md-actions outside of the padding of .md-dialog + margin-bottom: -$baseline-grid * 3; + margin-left: -$baseline-grid * 3; + margin-right: -$baseline-grid * 3; + + right: -$baseline-grid * 3; + min-height: $baseline-grid * 6.5; + overflow: hidden; + + [md-button], [md-raised-button] { + margin-bottom: $baseline-grid; + margin-left: $baseline-grid; + margin-right: 0; + margin-top: $baseline-grid; + } + } - padding: 20px; } -.md-backdrop { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.12); +// Theme + +$dialog-border-radius: 4px !default; + +.md-dialog { + border-radius: $dialog-border-radius; + background-color: md-color($md-background, lighter); //'{{background-color}}'; + + &.md-content-overflow { + .md-actions, md-dialog-actions { + border-top-color: md-color($md-foreground, divider); //'{{foreground-4}}'; + } + } } + + diff --git a/ng2-material/components/dialog/dialog.ts b/ng2-material/components/dialog/dialog.ts index 446fccaf..c817b00e 100644 --- a/ng2-material/components/dialog/dialog.ts +++ b/ng2-material/components/dialog/dialog.ts @@ -1,26 +1,23 @@ import { - bind, provide, - forwardRef, - Component, ComponentRef, - Directive, DynamicComponentLoader, ElementRef, - Host, Injectable, ResolvedProvider, - SkipSelf, - Injector, - View, - ViewEncapsulation + Injector } from 'angular2/core'; -import {ObservableWrapper, Promise, PromiseWrapper} from 'angular2/src/facade/async'; +import {Promise} from 'angular2/src/facade/async'; import {isPresent, Type} from 'angular2/src/facade/lang'; -import {DOM} from 'angular2/src/platform/dom/dom_adapter'; -import {MouseEvent, KeyboardEvent} from 'angular2/src/facade/browser'; -import {KeyCodes} from '../../core/key_codes'; +import {MdDialogRef} from './dialog_ref'; +import {MdDialogConfig} from './dialog_config'; +import {MdDialogContainer} from './dialog_container'; +import {MdBackdrop} from "../backdrop/backdrop"; +import {DOM} from "angular2/src/platform/dom/dom_adapter"; +import {Renderer} from "angular2/core"; +import {Animate} from '../../core/util/animate'; + // TODO(jelbourn): Opener of dialog can control where it is rendered. // TODO(jelbourn): body scrolling is disabled while dialog is open. @@ -31,17 +28,26 @@ import {KeyCodes} from '../../core/key_codes'; // TODO(jelbourn): Pre-built `alert` and `confirm` dialogs. // TODO(jelbourn): Animate dialog out of / into opening element. +/** + * Any components that are launched through MdDialog should implement this + * interface. The `dialog` will be injected into the component instance to + * allow dismissing or interacting with the dialog reference. + */ +export interface IDialogComponent { + dialog: MdDialogRef; +} + /** * Service for opening modal dialogs. */ @Injectable() export class MdDialog { - componentLoader: DynamicComponentLoader; - - constructor(loader: DynamicComponentLoader) { - this.componentLoader = loader; + constructor(public componentLoader: DynamicComponentLoader, + public renderer: Renderer) { } + private _defaultContainer = DOM.query('body'); + /** * Opens a modal dialog. * @param type The component to open. @@ -52,66 +58,74 @@ export class MdDialog { 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(); + config.dialogRef(dialogRef); var bindings = Injector.resolve([provide(MdDialogRef, {useValue: dialogRef})]); - var backdropRefPromise = this._openBackdrop(elementRef, bindings); + var backdropRefPromise = this._openBackdrop(elementRef, bindings, options); // 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): 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; + .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(config.container || this._defaultContainer, dialogElement); + + this.renderer.setElementClass(containerRef.location, 'md-dialog-absolute', !!options.container); + + if (isPresent(config.width)) { + this.renderer.setElementStyle(containerRef.location, 'width', config.width); + } + if (isPresent(config.height)) { + this.renderer.setElementStyle(containerRef.location, '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: ComponentRef) => { + Object.keys(config.context).forEach((key) => { + contentRef.instance[key] = config.context[key]; + }); + + // 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.backdropRef = backdropRef; + dialogRef.whenClosed.then((_) => { + backdropRef.dispose(); }); - }); + }); + + return Animate.enter(dialogElement, 'md-active').then(() => dialogRef); + }); + }); } /** Loads the dialog backdrop (transparent overlay over the rest of the page). */ - _openBackdrop(elementRef: ElementRef, bindings: ResolvedProvider[]): Promise<ComponentRef> { + _openBackdrop(elementRef: ElementRef, bindings: ResolvedProvider[], options: MdDialogConfig): 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; - }); + .then((componentRef) => { + let backdrop: MdBackdrop = componentRef.instance; + backdrop.clickClose = options.clickClose; + this.renderer.setElementClass(componentRef.location, 'md-backdrop', true); + this.renderer.setElementClass(componentRef.location, 'md-backdrop-absolute', !!options.container); + DOM.appendChild(options.container || this._defaultContainer, componentRef.location.nativeElement); + return componentRef; + }); } alert(message: string, okMessage: string): Promise<any> { @@ -122,157 +136,3 @@ export class MdDialog { 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: { - 'class': 'md-dialog', - 'tabindex': '0', - '(body:keydown)': 'documentKeypress($event)', - }, -}) -@View({ - encapsulation: ViewEncapsulation.None, - template: ` - <md-dialog-content></md-dialog-content> - <div tabindex="0" (focus)="wrapFocus()"></div>`, - 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 == KeyCodes.ESCAPE) { - 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(@Host() @SkipSelf() 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: '', encapsulation: ViewEncapsulation.None}) -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(); - } -} diff --git a/ng2-material/components/dialog/dialog_basic.ts b/ng2-material/components/dialog/dialog_basic.ts new file mode 100644 index 00000000..726ab826 --- /dev/null +++ b/ng2-material/components/dialog/dialog_basic.ts @@ -0,0 +1,31 @@ + +import {NgIf} from "angular2/common"; +import {MdButton} from "ng2-material/components/button/button"; +import {View} from "angular2/core"; +import {Component} from "angular2/core"; +import {IDialogComponent} from "./dialog"; +import {MdDialogRef} from "ng2-material/components/dialog/dialog_ref"; +import {Input} from "angular2/core"; +@Component({selector: 'md-dialog-basic'}) +@View({ + template: ` + <h2>{{ title }}</h2> + <p>{{ textContent }}</p> + <md-dialog-actions> + <button md-button *ngIf="cancel != ''" type="button" (click)="dialog.close(false)"> + <span>{{ cancel }}</span> + </button> + <button md-button *ngIf="ok != ''" class="md-primary" type="button" (click)="dialog.close(true)"> + <span>{{ ok }}</span> + </button> + </md-dialog-actions>`, + directives: [MdButton, NgIf] +}) +export class MdDialogBasic implements IDialogComponent { + dialog: MdDialogRef; + @Input() title: string = ''; + @Input() textContent: string = ''; + @Input() cancel: string = ''; + @Input() ok: string = ''; + @Input() type: string = 'alert'; +} diff --git a/ng2-material/components/dialog/dialog_config.ts b/ng2-material/components/dialog/dialog_config.ts new file mode 100644 index 00000000..d317dd34 --- /dev/null +++ b/ng2-material/components/dialog/dialog_config.ts @@ -0,0 +1,59 @@ + + +import {MdDialogRef} from "./dialog_ref"; + +/** Configuration for a dialog to be opened. */ +export class MdDialogConfig { + width: string = null; + height: string = null; + container: HTMLElement = null; + sourceEvent: Event = null; + clickClose: boolean = true; + context: any = {}; + + parent(element: HTMLElement): MdDialogConfig { + this.container = element; + return this; + } + + clickOutsideToClose(enabled: boolean): MdDialogConfig { + this.clickClose = enabled; + return this; + } + + title(text: string): MdDialogConfig { + this.context.title = text; + return this; + } + + textContent(text: string): MdDialogConfig { + this.context.textContent = text; + return this; + } + + ariaLabel(text: string): MdDialogConfig { + this.context.ariaLabel = text; + return this; + } + + ok(text: string): MdDialogConfig { + this.context.ok = text; + return this; + } + + cancel(text: string): MdDialogConfig { + this.context.cancel = text; + return this; + } + + targetEvent(ev: Event): MdDialogConfig { + this.sourceEvent = ev; + return this; + } + + dialogRef(ref: MdDialogRef): MdDialogConfig { + this.context.dialog = ref; + return this; + } + +} diff --git a/ng2-material/components/dialog/dialog_container.ts b/ng2-material/components/dialog/dialog_container.ts new file mode 100644 index 00000000..aa403142 --- /dev/null +++ b/ng2-material/components/dialog/dialog_container.ts @@ -0,0 +1,64 @@ +import {ViewEncapsulation} from "angular2/core"; +import {View} from "angular2/core"; +import {Component} from "angular2/core"; +import {ElementRef} from "angular2/core"; +import {MdDialogRef} from "./dialog_ref"; +import {KeyCodes} from "../../core/key_codes"; +import {forwardRef} from "angular2/core"; +import {Directive} from "angular2/core"; +import {Host} from "angular2/core"; +import {SkipSelf} from "angular2/core"; + +/** + * Container for user-provided dialog content. + */ +@Component({ + selector: 'md-dialog-container', + host: { + 'class': 'md-dialog', + 'tabindex': '0', + '(body:keydown)': 'documentKeypress($event)', + }, +}) +@View({ + encapsulation: ViewEncapsulation.None, + template: ` + <md-dialog-content></md-dialog-content> + <div tabindex="0" (focus)="wrapFocus()"></div>`, + directives: [forwardRef(() => MdDialogContent)] +}) +export 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 == KeyCodes.ESCAPE) { + 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' +}) +export class MdDialogContent { + constructor(@Host() @SkipSelf() dialogContainer: MdDialogContainer, elementRef: ElementRef) { + dialogContainer.contentRef = elementRef; + } +} diff --git a/ng2-material/components/dialog/dialog_ref.ts b/ng2-material/components/dialog/dialog_ref.ts new file mode 100644 index 00000000..735358c2 --- /dev/null +++ b/ng2-material/components/dialog/dialog_ref.ts @@ -0,0 +1,82 @@ +import {ComponentRef} from "angular2/core"; +import {PromiseWrapper} from "angular2/src/facade/promise"; +import {Animate} from "../../core/util/animate"; +import {isPresent} from "angular2/src/facade/lang"; + +/** + * Reference to an opened dialog. + */ +export class MdDialogRef { + // Reference to the MdDialogContainer component. + containerRef: ComponentRef; + + // Reference to the MdBackdrop component. + _backdropRef: 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 backdropRef(value: ComponentRef) { + this._backdropRef = value; + let subscription = this._backdropRef.instance.onHiding.subscribe(() => { + this.close(); + subscription.unsubscribe(); + }); + } + + 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): Promise<void> { + return Animate.leave(this.containerRef.location.nativeElement, 'md-active').then(() => { + this._backdropRef.instance.hide(); + return this.contentRefDeferred.promise.then((_) => { + if (!this.isClosed) { + this.isClosed = true; + this.containerRef.dispose(); + this.whenClosedDeferred.resolve(result); + } + }); + }); + } +} diff --git a/ng2-material/core/style/shadows.scss b/ng2-material/core/style/shadows.scss index 24267e02..d71471eb 100644 --- a/ng2-material/core/style/shadows.scss +++ b/ng2-material/core/style/shadows.scss @@ -13,3 +13,36 @@ $md-shadow-bottom-z-2: 0 4px 8px 0 rgba(0, 0, 0, 0.4); box-shadow: $whiteframe-shadow-z2; z-index: $whiteframe-zindex-z2; } + +// Whiteframes + +$shadow-key-umbra-opacity: 0.2; +$shadow-key-penumbra-opacity: 0.14; +$shadow-ambient-shadow-opacity: 0.12; + +// NOTE(shyndman): gulp-sass seems to be failing if I split the shadow defs across +// multiple lines. Ugly. Sorry. +$whiteframe-shadow-1dp: 0px 1px 3px 0px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 1px 1px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 2px 1px -1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-2dp: 0px 1px 5px 0px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 2px 2px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 3px 1px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-3dp: 0px 1px 8px 0px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 3px 4px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 3px 3px -2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-4dp: 0px 2px 4px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 4px 5px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 1px 10px 0px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-5dp: 0px 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 5px 8px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 1px 14px 0px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-6dp: 0px 3px 5px -1px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 6px 10px 0px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 1px 18px 0px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-7dp: 0px 4px 5px -2px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 7px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 2px 16px 1px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-8dp: 0px 5px 5px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 8px 10px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 3px 14px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-9dp: 0px 5px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 9px 12px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 3px 16px 2px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-10dp: 0px 6px 6px -3px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 10px 14px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 4px 18px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-11dp: 0px 6px 7px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 11px 15px 1px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 4px 20px 3px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-12dp: 0px 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 12px 17px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 5px 22px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-13dp: 0px 7px 8px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 13px 19px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 5px 24px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-14dp: 0px 7px 9px -4px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 14px 21px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 5px 26px 4px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-15dp: 0px 8px 9px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 15px 22px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 6px 28px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-16dp: 0px 8px 10px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 16px 24px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 6px 30px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-17dp: 0px 8px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 17px 26px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 6px 32px 5px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-18dp: 0px 9px 11px -5px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 18px 28px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 7px 34px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-19dp: 0px 9px 12px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 19px 29px 2px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 7px 36px 6px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-20dp: 0px 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 20px 31px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 8px 38px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-21dp: 0px 10px 13px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 21px 33px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 8px 40px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-22dp: 0px 10px 14px -6px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 22px 35px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 8px 42px 7px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-23dp: 0px 11px 14px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 23px 36px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 9px 44px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default; +$whiteframe-shadow-24dp: 0px 11px 15px -7px rgba(0, 0, 0, $shadow-key-umbra-opacity), 0px 24px 38px 3px rgba(0, 0, 0, $shadow-key-penumbra-opacity), 0px 9px 46px 8px rgba(0, 0, 0, $shadow-ambient-shadow-opacity) !default;