From 830516628d816629bc683f1763b1e7f863e45fb5 Mon Sep 17 00:00:00 2001 From: Wilson Zeng Date: Wed, 17 Jan 2018 20:37:12 +0800 Subject: [PATCH] refactor(module:modal): refactor of modal with more flexible and ease-of-use. close #317, close #644 --- components/index.ts | 25 +- components/modal/css-unit.pipe.ts | 12 + components/modal/demo/async.md | 15 + components/modal/demo/async.ts | 34 ++ components/modal/demo/basic.md | 14 + components/modal/demo/basic.ts | 33 ++ components/modal/demo/confirm-promise.md | 15 + components/modal/demo/confirm-promise.ts | 25 + components/modal/demo/confirm.md | 14 + components/modal/demo/confirm.ts | 34 ++ components/modal/demo/footer.md | 20 + components/modal/demo/footer.ts | 53 ++ components/modal/demo/info.md | 14 + components/modal/demo/info.ts | 45 ++ components/modal/demo/locale.md | 14 + components/modal/demo/locale.ts | 52 ++ components/modal/demo/manual.md | 14 + components/modal/demo/manual.ts | 22 + components/modal/demo/position.md | 18 + components/modal/demo/position.ts | 63 +++ components/modal/demo/service.md | 18 + components/modal/demo/service.ts | 157 ++++++ components/modal/doc/index.en-US.md | 127 +++++ components/modal/doc/index.zh-CN.md | 127 +++++ components/modal/index.ts | 1 + components/modal/modal-public-agent.class.ts | 28 ++ components/modal/modal-util.ts | 24 + components/modal/nz-modal.component.html | 118 +++++ components/modal/nz-modal.component.ts | 325 ++++++++++++ components/modal/nz-modal.module.ts | 18 + components/modal/nz-modal.service.ts | 105 ++++ components/modal/nz-modal.spec.ts | 502 +++++++++++++++++++ components/modal/nz-modal.type.ts | 59 +++ components/modal/public-api.ts | 5 + components/modal/style/confirm.less | 70 +++ components/modal/style/index.less | 3 + components/modal/style/modal.less | 144 ++++++ components/test.ts | 19 +- 38 files changed, 2367 insertions(+), 19 deletions(-) create mode 100644 components/modal/css-unit.pipe.ts create mode 100644 components/modal/demo/async.md create mode 100644 components/modal/demo/async.ts create mode 100644 components/modal/demo/basic.md create mode 100644 components/modal/demo/basic.ts create mode 100644 components/modal/demo/confirm-promise.md create mode 100644 components/modal/demo/confirm-promise.ts create mode 100644 components/modal/demo/confirm.md create mode 100644 components/modal/demo/confirm.ts create mode 100644 components/modal/demo/footer.md create mode 100644 components/modal/demo/footer.ts create mode 100644 components/modal/demo/info.md create mode 100644 components/modal/demo/info.ts create mode 100644 components/modal/demo/locale.md create mode 100644 components/modal/demo/locale.ts create mode 100644 components/modal/demo/manual.md create mode 100644 components/modal/demo/manual.ts create mode 100644 components/modal/demo/position.md create mode 100644 components/modal/demo/position.ts create mode 100644 components/modal/demo/service.md create mode 100644 components/modal/demo/service.ts create mode 100644 components/modal/doc/index.en-US.md create mode 100644 components/modal/doc/index.zh-CN.md create mode 100644 components/modal/index.ts create mode 100644 components/modal/modal-public-agent.class.ts create mode 100644 components/modal/modal-util.ts create mode 100644 components/modal/nz-modal.component.html create mode 100644 components/modal/nz-modal.component.ts create mode 100644 components/modal/nz-modal.module.ts create mode 100644 components/modal/nz-modal.service.ts create mode 100644 components/modal/nz-modal.spec.ts create mode 100644 components/modal/nz-modal.type.ts create mode 100644 components/modal/public-api.ts create mode 100644 components/modal/style/confirm.less create mode 100644 components/modal/style/index.less create mode 100644 components/modal/style/modal.less diff --git a/components/index.ts b/components/index.ts index 51f87eae794..e21b229cf2a 100644 --- a/components/index.ts +++ b/components/index.ts @@ -23,6 +23,7 @@ import { NzListModule } from './list'; import { NzLocaleModule } from './locale'; import { NzMenuModule } from './menu'; import { NzMessageModule } from './message/nz-message.module'; +import { NzModalModule } from './modal/nz-modal.module'; import { NzNotificationModule } from './notification/nz-notification.module'; import { NzPaginationModule } from './pagination/nz-pagination.module'; import { NzPopconfirmModule } from './popconfirm/nz-popconfirm.module'; @@ -42,6 +43,16 @@ import { NzTimelineModule } from './timeline/nz-timeline.module'; import { NzToolTipModule } from './tooltip/nz-tooltip.module'; import { NzTransferModule } from './transfer/nz-transfer.module'; +import { NzMessageService } from './message/nz-message.service'; +import { NzNotificationService } from './notification/nz-notification.service'; + +export { NzNotificationService } from './notification/nz-notification.service'; +export { NzMessageService } from './message/nz-message.service'; + +// Tokens (eg. global services' config) +export { NZ_MESSAGE_CONFIG } from './message/nz-message-config'; +export { NZ_NOTIFICATION_CONFIG } from './notification/nz-notification-config'; + export * from './button'; export * from './grid'; export * from './layout'; @@ -52,16 +63,7 @@ export * from './input'; export * from './i18n'; export * from './locale/index'; export * from './list/index'; - -import { NzMessageService } from './message/nz-message.service'; -import { NzNotificationService } from './notification/nz-notification.service'; - -export { NzNotificationService } from './notification/nz-notification.service'; -export { NzMessageService } from './message/nz-message.service'; - -// Tokens (eg. global services' config) -export { NZ_MESSAGE_CONFIG } from './message/nz-message-config'; -export { NZ_NOTIFICATION_CONFIG } from './notification/nz-notification-config'; +export * from './modal/public-api'; @NgModule({ exports: [ @@ -106,7 +108,8 @@ export { NZ_NOTIFICATION_CONFIG } from './notification/nz-notification-config'; NzDividerModule, NzFormModule, NzListModule, - NzI18nModule + NzI18nModule, + NzModalModule ] }) export class NgZorroAntdModule { diff --git a/components/modal/css-unit.pipe.ts b/components/modal/css-unit.pipe.ts new file mode 100644 index 00000000000..88a8b1b06f4 --- /dev/null +++ b/components/modal/css-unit.pipe.ts @@ -0,0 +1,12 @@ +import { Pipe, PipeTransform } from '@angular/core'; + +@Pipe({ + name: 'toCssUnit' +}) + +export class CssUnitPipe implements PipeTransform { + transform(value: number | string, defaultUnit: string = 'px'): string { + const formatted = +value; // force convert + return isNaN(formatted) ? `${value}` : `${formatted}${defaultUnit}`; + } +} diff --git a/components/modal/demo/async.md b/components/modal/demo/async.md new file mode 100644 index 00000000000..f909576d378 --- /dev/null +++ b/components/modal/demo/async.md @@ -0,0 +1,15 @@ +--- +order: 1 +title: + zh-CN: 异步关闭 + en-US: Asynchronously close +--- + +## zh-CN + +点击确定后异步关闭对话框,例如提交表单。 + +## en-US + +Asynchronously close a modal dialog when a user clicked OK button, for example, +you can use this pattern when you submit a form. diff --git a/components/modal/demo/async.ts b/components/modal/demo/async.ts new file mode 100644 index 00000000000..ef332009f39 --- /dev/null +++ b/components/modal/demo/async.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-async', + template: ` + + +

对话框的内容

+
+ `, + styles: [] +}) +export class NzDemoModalAsyncComponent { + isVisible = false; + isOkLoading = false; + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + this.isOkLoading = true; + window.setTimeout(() => { + this.isVisible = false; + this.isOkLoading = false; + }, 3000); + } + + handleCancel($event: MouseEvent): void { + this.isVisible = false; + } +} diff --git a/components/modal/demo/basic.md b/components/modal/demo/basic.md new file mode 100644 index 00000000000..5cd9fe81550 --- /dev/null +++ b/components/modal/demo/basic.md @@ -0,0 +1,14 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +## zh-CN + +第一个对话框。 + +## en-US + +Basic modal. diff --git a/components/modal/demo/basic.ts b/components/modal/demo/basic.ts new file mode 100644 index 00000000000..bd6de93f062 --- /dev/null +++ b/components/modal/demo/basic.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-basic', + template: ` + + +

Content one

+

Content two

+

Content three

+
+ `, + styles: [] +}) +export class NzDemoModalBasicComponent { + isVisible = false; + + constructor() {} + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + console.log('Button ok clicked!'); + this.isVisible = false; + } + + handleCancel($event: MouseEvent): void { + console.log('Button cancel clicked!', $event); + this.isVisible = false; + } +} diff --git a/components/modal/demo/confirm-promise.md b/components/modal/demo/confirm-promise.md new file mode 100644 index 00000000000..4bee2719ad7 --- /dev/null +++ b/components/modal/demo/confirm-promise.md @@ -0,0 +1,15 @@ +--- +order: 5 +title: + zh-CN: 确认对话框 + en-US: Confirmation modal dialog +--- + +## zh-CN + +使用 `NzModalService.confirm()` 可以快捷地弹出确认框。NzOnCancel/NzOnOk 返回 promise 可以延迟关闭 + +## en-US + +To use `NzModalService.confirm()` to popup confirmation modal dialog. Let NzOnCancel/NzOnOk function return a promise object to +delay closing the dialog. diff --git a/components/modal/demo/confirm-promise.ts b/components/modal/demo/confirm-promise.ts new file mode 100644 index 00000000000..6d8b0299a3b --- /dev/null +++ b/components/modal/demo/confirm-promise.ts @@ -0,0 +1,25 @@ +import { Component } from '@angular/core'; +import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-confirm-promise', + template: ` + + `, + styles : [] +}) +export class NzDemoModalConfirmPromiseComponent { + confirmModal: ModalPublicAgent; // For testing by now + + constructor(private modal: NzModalService) { } + + showConfirm(): void { + this.confirmModal = this.modal.confirm({ + nzTitle: 'Do you Want to delete these items?', + nzContent: 'When clicked the OK button, this dialog will be closed after 1 second', + nzOnOk: () => new Promise((resolve, reject) => { + setTimeout(Math.random() > 0.5 ? resolve : reject, 1000); + }).catch(() => console.log('Oops errors!')) + }); + } +} diff --git a/components/modal/demo/confirm.md b/components/modal/demo/confirm.md new file mode 100644 index 00000000000..fca22b86fbc --- /dev/null +++ b/components/modal/demo/confirm.md @@ -0,0 +1,14 @@ +--- +order: 3 +title: + zh-CN: 确认对话框 + en-US: Confirmation modal dialog +--- + +## zh-CN + +使用 `NzModalService.confirm()` 可以快捷地弹出确认框。 + +## en-US + +To use `NzModalService.confirm()` to popup a confirmation modal dialog. diff --git a/components/modal/demo/confirm.ts b/components/modal/demo/confirm.ts new file mode 100644 index 00000000000..d72b6f20a40 --- /dev/null +++ b/components/modal/demo/confirm.ts @@ -0,0 +1,34 @@ +import { Component } from '@angular/core'; +import { NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-confirm', + template: ` + + + `, + styles : [] +}) +export class NzDemoModalConfirmComponent { + constructor(private modalService: NzModalService) { } + + showConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Do you Want to delete these items?', + nzContent: 'Some descriptions', + nzOnOk: () => console.log('OK') + }); + } + + showDeleteConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Are you sure delete this task?', + nzContent: 'Some descriptions', + nzOkText: 'Yes', + nzOkType: 'danger', + nzOnOk: () => console.log('OK'), + nzCancelText: 'No', + nzOnCancel: () => console.log('Cancel') + }); + } +} diff --git a/components/modal/demo/footer.md b/components/modal/demo/footer.md new file mode 100644 index 00000000000..bdae2846092 --- /dev/null +++ b/components/modal/demo/footer.md @@ -0,0 +1,20 @@ +--- +order: 2 +title: + zh-CN: 自定义页脚 + en-US: Customized Footer +--- + +## zh-CN + +更复杂的例子,自定义了页脚的按钮,点击提交后进入 loading 状态,完成后关闭。 + +不需要默认确定取消按钮时,你可以把 `nzFooter` 设为 `null`。 + +## en-US + +A more complex example which define a customized footer button bar, +the dialog will change to loading state after clicking submit button, when the loading is over, +the modal dialog will be closed. + +You could set `nzFooter` to `null` if you don't need default footer buttons. diff --git a/components/modal/demo/footer.ts b/components/modal/demo/footer.ts new file mode 100644 index 00000000000..c9dd076a1f2 --- /dev/null +++ b/components/modal/demo/footer.ts @@ -0,0 +1,53 @@ +import { Component } from '@angular/core'; + + +@Component({ + selector: 'nz-demo-modal-footer', + template: ` + + + + 自定义对话框标题 + + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ + + 自定义底部: + + + +
+ `, + styles: [] +}) +export class NzDemoModalFooterComponent { + isVisible = false; + isConfirmLoading = false; + + constructor() { } + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + this.isConfirmLoading = true; + setTimeout(() => { + this.isVisible = false; + this.isConfirmLoading = false; + }, 3000); + } + + handleCancel($event: MouseEvent): void { + this.isVisible = false; + } +} diff --git a/components/modal/demo/info.md b/components/modal/demo/info.md new file mode 100644 index 00000000000..4650b7c2fc7 --- /dev/null +++ b/components/modal/demo/info.md @@ -0,0 +1,14 @@ +--- +order: 5 +title: + zh-CN: 信息提示 + en-US: Information modal dialog +--- + +## zh-CN + +各种类型的信息提示,只提供一个按钮用于关闭。 + +## en-US + +In the various types of information modal dialog, only one button to close dialog is provided. diff --git a/components/modal/demo/info.ts b/components/modal/demo/info.ts new file mode 100644 index 00000000000..e5d0164b1c3 --- /dev/null +++ b/components/modal/demo/info.ts @@ -0,0 +1,45 @@ +import { Component } from '@angular/core'; +import { NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-info', + template: ` + + + + + `, + styles: [] +}) +export class NzDemoModalInfoComponent { + constructor(private modalService: NzModalService) { } + + info(): void { + this.modalService.info({ + nzTitle: 'This is a notification message', + nzContent: '

some messages...some messages...

some messages...some messages...

', + nzOnOk: () => console.log('Info OK') + }); + } + + success(): void { + this.modalService.success({ + nzTitle: 'This is a success message', + nzContent: 'some messages...some messages...' + }); + } + + error(): void { + this.modalService.error({ + nzTitle: 'This is an error message', + nzContent: 'some messages...some messages...' + }); + } + + warning(): void { + this.modalService.warning({ + nzTitle: 'This is an warning message', + nzContent: 'some messages...some messages...' + }); + } +} diff --git a/components/modal/demo/locale.md b/components/modal/demo/locale.md new file mode 100644 index 00000000000..6cfe9cef129 --- /dev/null +++ b/components/modal/demo/locale.md @@ -0,0 +1,14 @@ +--- +order: 6 +title: + zh-CN: 国际化 + en-US: Internationalization +--- + +## zh-CN + +设置 `nzOkText` 与 `nzCancelText` 以自定义按钮文字。 + +## en-US + +To customize the text of the buttons, you need to set `nzOkText` and `nzCancelText` props. diff --git a/components/modal/demo/locale.ts b/components/modal/demo/locale.ts new file mode 100644 index 00000000000..5467f744e2a --- /dev/null +++ b/components/modal/demo/locale.ts @@ -0,0 +1,52 @@ +import { Component } from '@angular/core'; +import { NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-locale', + template: ` +
+ + +

Bla bla ...

+

Bla bla ...

+

Bla bla ...

+
+
+
+ + `, + styles: [] +}) +export class NzDemoModalLocaleComponent { + isVisible = false; + + constructor(private modalService: NzModalService) { } + + showModal(): void { + this.isVisible = true; + } + + handleOk(): void { + this.isVisible = false; + } + + handleCancel(): void { + this.isVisible = false; + } + + showConfirm(): void { + this.modalService.confirm({ + nzTitle: 'Confirm', + nzContent: 'Bla bla ...', + nzOkText: '确认', + nzCancelText: '取消' + }); + } +} diff --git a/components/modal/demo/manual.md b/components/modal/demo/manual.md new file mode 100644 index 00000000000..8177bfe7200 --- /dev/null +++ b/components/modal/demo/manual.md @@ -0,0 +1,14 @@ +--- +order: 7 +title: + zh-CN: 手动移除 + en-US: Manual to destroy +--- + +## zh-CN + +手动关闭modal。 + +## en-US + +Manually destroying a modal. diff --git a/components/modal/demo/manual.ts b/components/modal/demo/manual.ts new file mode 100644 index 00000000000..65244bb6e05 --- /dev/null +++ b/components/modal/demo/manual.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-manual', + template: ` + + `, + styles: [] +}) +export class NzDemoModalManualComponent { + constructor(private modalService: NzModalService) { } + + success(): void { + const modal = this.modalService.success({ + nzTitle: 'This is a notification message', + nzContent: 'This modal will be destroyed after 1 second' + }); + + window.setTimeout(() => modal.destroy(), 1000); + } +} diff --git a/components/modal/demo/position.md b/components/modal/demo/position.md new file mode 100644 index 00000000000..a710b508c0a --- /dev/null +++ b/components/modal/demo/position.md @@ -0,0 +1,18 @@ +--- +order: 8 +title: + zh-CN: 自定义位置 + en-US: To customize the position of modal +--- + +## zh-CN + +您可以直接使用 `nzStyle.top` 或配合其他样式来设置对话框位置。 + +> **注意** 由于Angular的样式隔离,若在Component中没有加入`encapsulation: ViewEncapsulation.None`,则您可能需要在自定义样式内采用`::ng-deep`来覆盖NgZorro的样式 + +## en-US + +You can use `nzStyle.top` or other styles to set position of modal dialog. + +> **NOTE** Due to Angular's style isolation, you may need to override the NgZorro style with `:: ng-deep` within a custom style if `encapsulation: ViewEncapsulation.None` is not included in the Component diff --git a/components/modal/demo/position.ts b/components/modal/demo/position.ts new file mode 100644 index 00000000000..806ffe8b5c0 --- /dev/null +++ b/components/modal/demo/position.ts @@ -0,0 +1,63 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'nz-demo-modal-position', + template: ` + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ +

+ + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ `, + styles: [ ` + ::ng-deep .vertical-center-modal { + display: flex; + align-items: center; + justify-content: center; + } + + ::ng-deep .vertical-center-modal .ant-modal { + top: 0; + } + ` ] +}) +export class NzDemoModalPositionComponent { + isVisibleTop = false; + isVisibleMiddle = false; + + showModalTop(): void { + this.isVisibleTop = true; + } + + showModalMiddle(): void { + this.isVisibleMiddle = true; + } + + handleOkTop(): void { + console.log('点击了确定'); + this.isVisibleTop = false; + } + + handleCancelTop(): void { + this.isVisibleTop = false; + } + + handleOkMiddle(): void { + console.log('点击了确定'); + this.isVisibleMiddle = false; + } + + handleCancelMiddle(): void { + this.isVisibleMiddle = false; + } +} diff --git a/components/modal/demo/service.md b/components/modal/demo/service.md new file mode 100644 index 00000000000..8d0c7511014 --- /dev/null +++ b/components/modal/demo/service.md @@ -0,0 +1,18 @@ +--- +order: 9 +title: + zh-CN: 服务方式创建 + en-US: Modal's service +--- + +## zh-CN + +Modal的service用法,示例中演示了用户自定义模板、自定义component、以及注入模态框实例的方法。 + +> **注意** 如果使用Component模式,则需要在NgModule中的 `declarations` 和 `entryComponents` 加入自定义的Component + +## en-US + +Usage of Modal's service, examples demonstrate user-defined templates, custom components, and methods for injecting modal instances. + +> **NOTE** If you use Component mode, you need to add your custom Component into `declarations` and `entryComponents` for a `NgModule` diff --git a/components/modal/demo/service.ts b/components/modal/demo/service.ts new file mode 100644 index 00000000000..760ddbc889d --- /dev/null +++ b/components/modal/demo/service.ts @@ -0,0 +1,157 @@ +/* entryComponents: NzModalCustomComponent */ + +import { Component, Input, TemplateRef } from '@angular/core'; +import { ModalPublicAgent, NzModalService } from 'ng-zorro-antd'; + +@Component({ + selector: 'nz-demo-modal-service', + template: ` + + + + + 对话框标题模板 + + +

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+

对话框的内容

+
+ + + + +

+ + + + + ` +}) +export class NzDemoModalServiceComponent { + tplModal: ModalPublicAgent; + tplModalButtonLoading = false; + + constructor(private modalService: NzModalService) { } + + createModal(): void { + this.modalService.create({ + nzTitle: '对话框标题', + nzContent: '纯文本内容,点确认 1 秒后关闭', + nzClosable: false, + nzOnOk: () => new Promise((resolve) => window.setTimeout(resolve, 1000)) + }); + } + + createTplModal(tplTitle: TemplateRef<{}>, tplContent: TemplateRef<{}>, tplFooter: TemplateRef<{}>): void { + this.tplModal = this.modalService.create({ + nzTitle: tplTitle, + nzContent: tplContent, + nzFooter: tplFooter, + nzMaskClosable: false, + nzClosable: false, + nzOnOk: () => console.log('Click ok') + }); + } + + destroyTplModal(): void { + this.tplModalButtonLoading = true; + window.setTimeout(() => { + this.tplModalButtonLoading = false; + this.tplModal.destroy(); + }, 1000); + } + + createComponentModal(): void { + const modal = this.modalService.create({ + nzTitle: '对话框标题', + nzContent: NzModalCustomComponent, + nzComponentParams: { + title: '这是Component内部标题', + subtitle: '这是Component中的副标题,2秒后会被动态改变' + }, + nzFooter: [{ + label: '从外部改变Component标题', + onClick: (componentInstance: NzModalCustomComponent) => { + componentInstance.title = '内部Component标题被改变啦!!!!!!!!!'; + } + }], + }); + + // 从外部改变副标题(注:当对话框实例还未初始化完毕时,getContentComponentRef()将返回undefined) + window.setTimeout(() => { + const instance = modal.getContentComponentRef().instance as NzModalCustomComponent; + instance.subtitle = '副标题已改变!!!!!!!!'; + }, 2000); + } + + createCustomButtonModal(): void { + const modal = this.modalService.create({ + nzTitle: '自定义按钮举例', + nzContent: '通过传入按钮配置数组到nzFooter,用于创建多个自定义按钮', + nzFooter: [ + { + label: 'X', + shape: 'circle', + onClick: () => modal.destroy(), + }, + { + label: '弹出确认框', + type: 'primary', + onClick: () => this.modalService.confirm({ nzTitle: '确认框标题!', nzContent: '确认框描述' }), + }, + { + label: '自动改变按钮状态', + type: 'danger', + loading: false, + onClick(): void { // 注:这里由于要得到this,所以不能用箭头函数 + this.loading = true; + window.setTimeout(() => this.loading = false, 1000); + window.setTimeout(() => { + this.loading = false; + this.disabled = true; + this.label = '不能点击了!'; + }, 2000); + } + }, + { + label: '异步加载', + type: 'dashed', + onClick: () => new Promise(resolve => window.setTimeout(resolve, 2000)) + } + ] + }); + } +} + +@Component({ + selector: 'nz-modal-custom-component', + template: ` +
+

{{ title }}

+

{{ subtitle }}

+

+ 可以在弹出框中的Component内访问到模态框实例 + +

+
+ ` +}) +export class NzModalCustomComponent { + @Input() title: string; + @Input() subtitle: string; + + constructor(private modal: ModalPublicAgent) { } + + destroyModal(): void { + this.modal.destroy(); + } +} diff --git a/components/modal/doc/index.en-US.md b/components/modal/doc/index.en-US.md new file mode 100644 index 00000000000..aaa97a85736 --- /dev/null +++ b/components/modal/doc/index.en-US.md @@ -0,0 +1,127 @@ +--- +type: Feedback +category: Components +title: Modal +--- + +Modal dialogs. + +## When To Use + +When requiring users to interact with application, but without jumping to a new page to interrupt +the user's workflow, you can use `Modal` to create a new floating layer over the current page for user +getting feedback or information purposes. +Additionally, if you need show a simple confirmation dialog, you can use `NzModalService.confirm()`, +and so on. + +推荐使用加载Component的方式弹出Modal,这样弹出层的Component逻辑可以与外层Component完全隔离,并且做到可以随时复用,在弹出层Component中可以通过依赖注入`ModalPublicAgent`方式直接获取模态框的组件实例,用于控制在弹出层组件中控制模态框行为。 + +## API + +对话框当前分为2种模式,`普通模式` 和 `确认框模式`(即`Confirm`对话框,通过调用`confirm/info/success/error/warning`弹出),两种模式对API的支持程度稍有不同。 + +| Property | Description | Type | Default | +|----|----|----|----| +| nzAfterClose | Modal 完全关闭后的回调 | EventEmitter | 无 | +| nzBodyStyle | Modal body 样式 | object | 无 | +| nzCancelText | 取消按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 取消 | +| nzClosable | 是否显示右上角的关闭按钮。确认框模式下该值无效(默认会被隐藏) | boolean | true | +| nzOkLoading | 确定按钮 loading | boolean | false | +| nzCancelLoading | 取消按钮 loading | boolean | false | +| nzFooter | 底部内容。1. 仅在普通模式下有效。
2. 可通过传入 ModalButtonOptions 来最大程度自定义按钮(详见案例或下方说明)。
3. 当不需要底部时,可以设为 null
| string
TemplateRef
ModalButtonOptions | 默认的确定取消按钮 | +| nzGetContainer | 指定 Modal 挂载的 HTML 节点 | HTMLElement
() => HTMLElement| document.body | +| nzMask | 是否展示遮罩 | boolean | true | +| nzMaskClosable | 点击蒙层是否允许关闭 | boolean | true | +| nzMaskStyle | 遮罩样式 | object | 无 | +| nzOkText | 确认按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 确定 | +| nzOkType | 确认按钮类型。与button的type类型值一致 | string | primary | +| nzStyle | 可用于设置浮层的样式,调整浮层位置等 | object | 无 | +| nzTitle | 标题。留空表示不展示标题。TemplateRef的使用方法可参考案例 | string
TemplateRef | 无 | +| nzVisible | 对话框是否可见。当以 `` 标签使用时,请务必使用双向绑定,例如:`[(nzVisible)]="visible"` | boolean | false | +| nzWidth | 宽度。使用数字时,默认单位为px | string
number | 520 | +| nzWrapClassName | 对话框外层容器的类名 | string | 无 | +| nzZIndex | 设置 Modal 的 `z-index` | number | 1000 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。注:当以`NzModalService.create`创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | EventEmitter | 无 | +| nzOnOk | 点击确定回调 | EventEmitter | 无 | +| nzContent | 内容 | string
TemplateRef
Component
ng-content | 无 | +| nzComponentParams | 当nzContent为组件类(Component)时,该参数中的属性将传入nzContent实例中 | object | 无 | +| nzIconType | 图标 Icon 类型。仅 确认框模式 下有效 | string | question-circle | + +#### 注意 + +> `` 默认关闭后状态不会自动清空, 如果希望每次打开都是新内容,请采用 `NzModalService` 服务方式创建对话框(当以服务方式创建时,默认会监听 `nzAfterClose` 并销毁对话框)。 + +#### 采用服务方式创建普通模式对话框 + +> 您可调用 `NzModalService.create(options)` 来动态创建**普通模式**对话框,这里的 `options` 是一个对象,支持上方API中给出的支持 **普通模式** 的参数 + +### 确认框模式 - NzModalService.method() + +There are five ways to display the information based on the content's nature: + +- `NzModalService.info` +- `NzModalService.success` +- `NzModalService.error` +- `NzModalService.warning` +- `NzModalService.confirm` + +The items listed above are all functions, expecting a settings object as parameter. +Consistent with the above API, some property types or initial values are different as follows: + +| Property | Description | Type | Default | +|------------|----------------|------------------|---------------| +| nzOnOk | 点击确定按钮时将执行的回调函数。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzWidth | 宽度 | string
number | 416 | +| nzMaskClosable | 点击蒙层是否允许关闭 | boolean | false | + +All the `NzModalService.method`s will return a reference, and then we can close the popup by the reference. + +```ts +constructor(modal: NzModalService) { + const ref: ModalPublicAgent = modal.info(); + ref.destroy(); // 注:这里将直接销毁对话框 +} +``` + +### 相关类型定义 + +#### ModalPublicAgent(用于控制对话框) + +通过服务方式 `NzModalService.xxx()` 创建的对话框,都会返回一个 `ModalPublicAgent` 对象,用于操控该对话框(若使用nzContent为Component时,也可通过依赖注入 `ModalPublicAgent` 方式获得此对象),该对象具有以下方法: + +| 方法 | 说明 | +|----|----| +| open() | 打开(显示)对话框。若对话框已销毁,则调用此函数将失效 | +| close() | 关闭(隐藏)对话框。注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法) | +| destroy() | 销毁对话框。注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框) | +| getContentComponentRef() | 获取对话框内容中`nzContent`的Component引用(类型为`ComponentRef`)。注:当对话框还未初始化完毕(`ngOnInit`未执行)时,此函数将返回`undefined` | + +#### ModalButtonOptions(用于自定义底部按钮) + +可将此类型数组传入 `nzFooter`,用于自定义底部按钮。 + +按钮配置项如下(与button组件保持一致): + +```ts +nzFooter: [{ + label: string; // 按钮文本 + type?: string; // 类型 + shape?: string; // 形状 + ghost?: boolean; // 是否ghost + size?: string; // 大小 + autoLoading?: boolean; // 默认为true,若为true时,当onClick返回promise时此按钮将自动置为loading状态 + + // 提示:下方方法的this指向该配置对象自身。当nzContent为组件类时,下方方法传入的contentComponentInstance参数为该组件类的实例 + // 是否显示该按钮 + show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 是否显示为loading + loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 是否禁用 + disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 按钮点击回调 + onClick?(this: ModalButtonOptions, contentComponentInstance?: object): void | Promise<void> | any; +}] +``` + +以上配置项也可在运行态实时改变,来触发按钮行为改变。 diff --git a/components/modal/doc/index.zh-CN.md b/components/modal/doc/index.zh-CN.md new file mode 100644 index 00000000000..00358ceefba --- /dev/null +++ b/components/modal/doc/index.zh-CN.md @@ -0,0 +1,127 @@ +--- +type: Feedback +category: Components +subtitle: 对话框 +title: Modal +--- + +模态对话框。 + +## 何时使用 + +需要用户处理事务,又不希望跳转页面以致打断工作流程时,可以使用 `Modal` 在当前页面正中打开一个浮层,承载相应的操作。 + +另外当需要一个简洁的确认框询问用户时,可以使用精心封装好的 `NzModalService.confirm()` 等方法。 + +推荐使用加载Component的方式弹出Modal,这样弹出层的Component逻辑可以与外层Component完全隔离,并且做到可以随时复用, + +在弹出层Component中可以通过依赖注入`ModalPublicAgent`方式直接获取模态框的组件实例,用于控制在弹出层组件中控制模态框行为。 + +## API + +对话框当前分为2种模式,`普通模式` 和 `确认框模式`(即`Confirm`对话框,通过调用`confirm/info/success/error/warning`弹出),两种模式对API的支持程度稍有不同。 + +| 参数 | 说明 | 类型 | 默认值 | +|----|----|----|----| +| nzAfterClose | Modal 完全关闭后的回调 | EventEmitter | 无 | +| nzBodyStyle | Modal body 样式 | object | 无 | +| nzCancelText | 取消按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 取消 | +| nzClosable | 是否显示右上角的关闭按钮。确认框模式下该值无效(默认会被隐藏) | boolean | true | +| nzOkLoading | 确定按钮 loading | boolean | false | +| nzCancelLoading | 取消按钮 loading | boolean | false | +| nzFooter | 底部内容。1. 仅在普通模式下有效。
2. 可通过传入 ModalButtonOptions 来最大程度自定义按钮(详见案例或下方说明)。
3. 当不需要底部时,可以设为 null
| string
TemplateRef
ModalButtonOptions | 默认的确定取消按钮 | +| nzGetContainer | 指定 Modal 挂载的 HTML 节点 | HTMLElement
() => HTMLElement| document.body | +| nzMask | 是否展示遮罩 | boolean | true | +| nzMaskClosable | 点击蒙层是否允许关闭 | boolean | true | +| nzMaskStyle | 遮罩样式 | object | 无 | +| nzOkText | 确认按钮文字。设为 null 表示不显示取消按钮(若在普通模式下使用了 nzFooter 参数,则该值无效) | string | 确定 | +| nzOkType | 确认按钮类型。与button的type类型值一致 | string | primary | +| nzStyle | 可用于设置浮层的样式,调整浮层位置等 | object | 无 | +| nzTitle | 标题。留空表示不展示标题。TemplateRef的使用方法可参考案例 | string
TemplateRef | 无 | +| nzVisible | 对话框是否可见。当以 `` 标签使用时,请务必使用双向绑定,例如:`[(nzVisible)]="visible"` | boolean | false | +| nzWidth | 宽度。使用数字时,默认单位为px | string
number | 520 | +| nzWrapClassName | 对话框外层容器的类名 | string | 无 | +| nzZIndex | 设置 Modal 的 `z-index` | number | 1000 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。注:当以`NzModalService.create`创建时,此参数应传入function(回调函数)。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | EventEmitter | 无 | +| nzOnOk | 点击确定回调 | EventEmitter | 无 | +| nzContent | 内容 | string
TemplateRef
Component
ng-content | 无 | +| nzComponentParams | 当nzContent为组件类(Component)时,该参数中的属性将传入nzContent实例中 | object | 无 | +| nzIconType | 图标 Icon 类型。仅 确认框模式 下有效 | string | question-circle | + +#### 注意 + +> `` 默认关闭后状态不会自动清空, 如果希望每次打开都是新内容,请采用 `NzModalService` 服务方式创建对话框(当以服务方式创建时,默认会监听 `nzAfterClose` 并销毁对话框)。 + +#### 采用服务方式创建普通模式对话框 + +> 您可调用 `NzModalService.create(options)` 来动态创建**普通模式**对话框,这里的 `options` 是一个对象,支持上方API中给出的支持 **普通模式** 的参数 + +### 确认框模式 - NzModalService.method() + +包括: + +- `NzModalService.info` +- `NzModalService.success` +- `NzModalService.error` +- `NzModalService.warning` +- `NzModalService.confirm` + +以上均为一个函数,参数为 object,与上方API一致。部分属性类型或初始值有所不同,已列在下方: + +| 参数 | 说明 | 类型 | 默认值 | +|------------|----------------|------------------|--------------| +| nzOnOk | 点击确定按钮时将执行的回调函数。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzOnCancel | 点击遮罩层或右上角叉或取消按钮的回调。该函数可返回promise,待执行完毕或promise结束时,将自动关闭对话框(返回false可阻止关闭) | function | 无 | +| nzWidth | 宽度 | string
number | 416 | +| nzMaskClosable | 点击蒙层是否允许关闭 | boolean | false | + +以上函数调用后,会返回一个引用,可以通过该引用关闭弹窗。 + +```ts +constructor(modal: NzModalService) { + const ref: ModalPublicAgent = modal.info(); + ref.destroy(); // 注:这里将直接销毁对话框 +} +``` + +### 相关类型定义 + +#### ModalPublicAgent(用于控制对话框) + +通过服务方式 `NzModalService.xxx()` 创建的对话框,都会返回一个 `ModalPublicAgent` 对象,用于操控该对话框(若使用nzContent为Component时,也可通过依赖注入 `ModalPublicAgent` 方式获得此对象),该对象具有以下方法: + +| 方法 | 说明 | +|----|----| +| open() | 打开(显示)对话框。若对话框已销毁,则调用此函数将失效 | +| close() | 关闭(隐藏)对话框。注:当用于以服务方式创建的对话框,此方法将直接 销毁 对话框(同destroy方法) | +| destroy() | 销毁对话框。注:仅用于服务方式创建的对话框(非服务方式创建的对话框,此方法只会隐藏对话框) | +| getContentComponentRef() | 获取对话框内容中`nzContent`的Component引用(类型为`ComponentRef`)。注:当对话框还未初始化完毕(`ngOnInit`未执行)时,此函数将返回`undefined` | + +#### ModalButtonOptions(用于自定义底部按钮) + +可将此类型数组传入 `nzFooter`,用于自定义底部按钮。 + +按钮配置项如下(与button组件保持一致): + +```ts +nzFooter: [{ + label: string; // 按钮文本 + type?: string; // 类型 + shape?: string; // 形状 + ghost?: boolean; // 是否ghost + size?: string; // 大小 + autoLoading?: boolean; // 默认为true,若为true时,当onClick返回promise时此按钮将自动置为loading状态 + + // 提示:下方方法的this指向该配置对象自身。当nzContent为组件类时,下方方法传入的contentComponentInstance参数为该组件类的实例 + // 是否显示该按钮 + show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 是否显示为loading + loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 是否禁用 + disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + // 按钮点击回调 + onClick?(this: ModalButtonOptions, contentComponentInstance?: object): void | Promise<void> | any; +}] +``` + +以上配置项也可在运行态实时改变,来触发按钮行为改变。 diff --git a/components/modal/index.ts b/components/modal/index.ts new file mode 100644 index 00000000000..7e1a213e3ea --- /dev/null +++ b/components/modal/index.ts @@ -0,0 +1 @@ +export * from './public-api'; diff --git a/components/modal/modal-public-agent.class.ts b/components/modal/modal-public-agent.class.ts new file mode 100644 index 00000000000..6423ef86472 --- /dev/null +++ b/components/modal/modal-public-agent.class.ts @@ -0,0 +1,28 @@ +import { ComponentRef } from '@angular/core'; +import { NzModalComponent } from './nz-modal.component'; + +/** + * API class that public to users to handle the modal instance. + * ModalPublicAgent is aim to avoid accessing to the modal instance directly by users. + */ +export abstract class ModalPublicAgent { + abstract open(): void; + abstract close(): void; + abstract destroy(): void; + + /** + * Return the ComponentRef of nzContent when specify nzContent as a Component + * Note: this method may return undefined if the Component has not ready yet. (it only available after Modal's ngOnInit) + */ + abstract getContentComponentRef(): ComponentRef<{}>; + + /** + * Get the dom element of this Modal + */ + abstract getElement(): HTMLElement; + + /** + * Get the instance of the Modal itself + */ + abstract getInstance(): NzModalComponent; +} diff --git a/components/modal/modal-util.ts b/components/modal/modal-util.ts new file mode 100644 index 00000000000..91496842957 --- /dev/null +++ b/components/modal/modal-util.ts @@ -0,0 +1,24 @@ +export interface ClickPosition { + x: number; + y: number; +} + +export class ModalUtil { + private lastPosition: ClickPosition = null; + + constructor(private document: Document) { + this.listenDocumentClick(); + } + + getLastClickPosition(): ClickPosition | null { + return this.lastPosition; + } + + listenDocumentClick(): void { + this.document.addEventListener('click', (event: MouseEvent) => { + this.lastPosition = { x: event.clientX, y: event.clientY }; + }); + } +} + +export default new ModalUtil(document); diff --git a/components/modal/nz-modal.component.html b/components/modal/nz-modal.component.html new file mode 100644 index 00000000000..f6f2a2920f9 --- /dev/null +++ b/components/modal/nz-modal.component.html @@ -0,0 +1,118 @@ + + +
+
+ +
+ + + +
+
+ + +
+
+
+
+
+ + + +
+ +
+
+
+ +
+ + + + +
+
+
+ + + + + + + +
+ + + +
+ +
+
+
+
+
+ + +
+
+
+
+ diff --git a/components/modal/nz-modal.component.ts b/components/modal/nz-modal.component.ts new file mode 100644 index 00000000000..68fa14fabd4 --- /dev/null +++ b/components/modal/nz-modal.component.ts @@ -0,0 +1,325 @@ +import { DOCUMENT } from '@angular/common'; +import { + AfterViewInit, + Component, + ComponentFactoryResolver, + ComponentRef, + ElementRef, + EmbeddedViewRef, + EventEmitter, + Inject, + InjectionToken, + Injector, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + TemplateRef, + Type, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { NzLocaleService } from '../locale'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import ModalUtil from './modal-util'; +import { ModalButtonOptions, ModalOptions, ModalType, OnClickCallback } from './nz-modal.type'; + +export const MODAL_ANIMATE_DURATION = 200; // Duration when perform animations (ms) + +interface ClassMap { + [index: string]: boolean; +} + +type AnimationState = 'enter' | 'leave' | null; + +@Component({ + selector: 'nz-modal', + encapsulation: ViewEncapsulation.None, + templateUrl: './nz-modal.component.html', + styleUrls: [ './style/index.less' ] +}) + +export class NzModalComponent extends ModalPublicAgent implements OnInit, OnChanges, AfterViewInit, ModalOptions { + @Input() nzModalType: ModalType = 'default'; + @Input() nzContent: string | TemplateRef<{}> | Type<{}>; // [STATIC] If not specified, will use + @Input() nzComponentParams: object; // [STATIC] ONLY avaliable when nzContent is a component + @Input() nzFooter: string | TemplateRef<{}> | ModalButtonOptions[]; // [STATIC] Default Modal ONLY + @Input() nzGetContainer: HTMLElement | (() => HTMLElement) = this.document.body; // [STATIC] + + @Input() nzVisible = false; + @Output() nzVisibleChange = new EventEmitter(); + @Input() nzZIndex: number = 1000; + @Input() nzWidth: number | string = 520; + @Input() nzWrapClassName: string; + @Input() nzClassName: string; + @Input() nzStyle: object; + @Input() nzIconType: string = 'question-circle'; // Confirm Modal ONLY + @Input() nzTitle: string | TemplateRef<{}>; + @Input() nzClosable = true; + @Input() nzMask = true; + @Input() nzMaskClosable = true; + @Input() nzMaskStyle: object; + @Input() nzBodyStyle: object; + @Output() nzAfterClose = new EventEmitter(); // Trigger when modal is hidden + + // --- Predefined OK & Cancel buttons + @Input() nzOkText: string = this.locale.translate('Modal.okText'); + @Input() nzOkType = 'primary'; + @Input() nzOkLoading = false; + @Input() @Output() nzOnOk: EventEmitter | OnClickCallback = new EventEmitter(); + @ViewChild('autoFocusButtonOk', { read: ElementRef }) autoFocusButtonOk: ElementRef; // Only aim to focus the ok button that needs to be auto focused + @Input() nzCancelText: string = this.locale.translate('Modal.cancelText'); + @Input() nzCancelLoading = false; + @Input() @Output() nzOnCancel: EventEmitter | OnClickCallback = new EventEmitter(); + + @ViewChild('modalContainer') modalContainer: ElementRef; + @ViewChild('bodyContainer', { read: ViewContainerRef }) bodyContainer: ViewContainerRef; + + get hidden(): boolean { return !this.nzVisible && !this.animationState; } // Indicate whether this dialog should hidden + maskAnimationClassMap: object; + modalAnimationClassMap: object; + transformOrigin = '0px 0px 0px'; // The origin point that animation based on + + private contentComponentRef: ComponentRef<{}>; // Handle the reference when using nzContent as Component + private animationState: AnimationState; // Current animation state + + constructor( + private locale: NzLocaleService, + private cfr: ComponentFactoryResolver, + private elementRef: ElementRef, + private viewContainer: ViewContainerRef, + @Inject(DOCUMENT) private document: any // tslint:disable-line:no-any + ) { + super(); + } + + ngOnInit(): void { + if (this.isComponent(this.nzContent)) { + this.createDynamicComponent(this.nzContent as Type<{}>); // Create component along without View + } + + if (this.isModalButtons(this.nzFooter)) { // Setup default button options + this.nzFooter = this.formatModalButtons(this.nzFooter as ModalButtonOptions[]); + } + + const container = typeof this.nzGetContainer === 'function' ? this.nzGetContainer() : this.nzGetContainer; + if (container instanceof HTMLElement) { + container.appendChild(this.elementRef.nativeElement); + } + } + + // [NOTE] NOT available when using by service! + // Because ngOnChanges never be called when using by service, + // here we can't support "nzContent"(Component) etc. as inputs that initialized dynamically. + // BUT: User also can change "nzContent" dynamically to trigger UI changes (provided you don't use Component that needs initializations) + ngOnChanges(changes: SimpleChanges): void { + if (changes.nzVisible) { + this.changeBodyOverflow(this.nzVisible); + if (!changes.nzVisible.firstChange) { // Do not trigger animation while initializing + this.animateTo(this.nzVisible); + } + } + } + + ngAfterViewInit(): void { + // If using Component, it is the time to attach View while bodyContainer is ready + if (this.contentComponentRef) { + this.bodyContainer.insert(this.contentComponentRef.hostView); + } + + if (this.autoFocusButtonOk) { + (this.autoFocusButtonOk.nativeElement as HTMLButtonElement).focus(); + } + } + + open(): void { + this.changeVisibleFromInside(true); + } + + close(): void { + this.changeVisibleFromInside(false).then(() => this.nzAfterClose.emit()); + } + + destroy(): void { // Destroy equals Close + this.close(); + } + + getInstance(): NzModalComponent { + return this; + } + + getContentComponentRef(): ComponentRef<{}> { + return this.contentComponentRef; + } + + getElement(): HTMLElement { + return this.elementRef && this.elementRef.nativeElement; + } + + onClickMask($event: MouseEvent): void { + if (this.nzMask && this.nzMaskClosable && ($event.target as HTMLElement).classList.contains('ant-modal-wrap')) { + // this.close(); + this.onClickOkCancel($event, 'cancel'); + } + } + + isModalType(type: ModalType): boolean { + return this.nzModalType === type; + } + + private onClickCloseBtn($event: MouseEvent): void { + // this.close(); + this.onClickOkCancel($event, 'cancel'); + } + + private onClickOkCancel($event: MouseEvent, type: 'ok' | 'cancel'): void { + const trigger = { 'ok': this.nzOnOk, 'cancel': this.nzOnCancel }[ type ]; + const loadingKey = { 'ok': 'nzOkLoading', 'cancel': 'nzCancelLoading' }[ type ]; + if (trigger instanceof EventEmitter) { + trigger.emit($event); + } else if (typeof trigger === 'function') { + const result = trigger($event); + const caseClose = (doClose: boolean | void | {}) => (doClose !== false) && this.close(); // Users can return "false" to prevent closing by default + if (isPromise(result)) { + this[ loadingKey ] = true; + const handleThen = (doClose) => { this[ loadingKey ] = false; caseClose(doClose); }; + (result as Promise).then(handleThen).catch(handleThen); + } else { + caseClose(result); + } + } + } + + private isNonEmptyString(value: {}): boolean { + return typeof value === 'string' && value !== ''; + } + + private isTemplateRef(value: {}): boolean { + return value instanceof TemplateRef; + } + + private isComponent(value: {}): boolean { + return value instanceof Type; + } + + private isModalButtons(value: {}): boolean { + return Array.isArray(value) && value.length > 0; + } + + // Lookup a button's property, if the prop is a function, call & then return the result, otherwise, return itself. + private getButtonCallableProp(options: ModalButtonOptions, prop: string): {} { + const value = options[prop]; + const args = []; + if (this.contentComponentRef) { args.push(this.contentComponentRef.instance); } + return typeof value === 'function' ? value.apply(options, args) : value; + } + + // On nzFooter's modal button click + private onButtonClick(button: ModalButtonOptions): void { + const result = this.getButtonCallableProp(button, 'onClick'); // Call onClick directly + if (isPromise(result)) { + button.loading = true; + (result as Promise<{}>).then(() => button.loading = false).catch(() => button.loading = false); + } + } + + // Change nzVisible from inside + private changeVisibleFromInside(visible: boolean): Promise { + if (this.nzVisible !== visible) { + // Change nzVisible value immediately + this.nzVisible = visible; + this.changeBodyOverflow(this.nzVisible); + this.nzVisibleChange.emit(visible); + return this.animateTo(visible); + } + return Promise.resolve(); + } + + private changeAnimationState(state: AnimationState): void { + this.animationState = state; + if (state) { + this.maskAnimationClassMap = { + [`fade-${state}`]: true, + [`fade-${state}-active`]: true, + }; + this.modalAnimationClassMap = { + [`zoom-${state}`]: true, + [`zoom-${state}-active`]: true, + }; + } else { + this.maskAnimationClassMap = this.modalAnimationClassMap = null; + } + } + + private animateTo(isVisible: boolean): Promise { + if (isVisible) { // Figure out the lastest click position when shows up + window.setTimeout(() => this.updateTransformOrigin()); // [NOTE] Using timeout due to the document.click event is fired later than visible change, so if not postponed to next event-loop, we can't get the lastest click position + } + + this.changeAnimationState(isVisible ? 'enter' : 'leave'); + return new Promise((resolve) => window.setTimeout(() => { // Return when animation is over + this.changeAnimationState(null); + resolve(); + }, MODAL_ANIMATE_DURATION)); + } + + private formatModalButtons(buttons: ModalButtonOptions[]): ModalButtonOptions[] { + return buttons.map((button) => { + const mixedButton = { + ...{ + type: 'default', + size: 'large', + autoLoading: true, + show: true, + loading: false, + disabled: false, + }, + ...button + }; + + // if (mixedButton.autoLoading) { mixedButton.loading = false; } // Force loading to false when autoLoading=true + + return mixedButton; + }); + } + + /** + * Create a component dynamically but not attach to any View (this action will be executed when bodyContainer is ready) + * @param component Component class + */ + private createDynamicComponent(component: Type<{}>): void { + const factory = this.cfr.resolveComponentFactory(component); + const childInjector = Injector.create([{ provide: ModalPublicAgent, useValue: this }], this.viewContainer.parentInjector); + this.contentComponentRef = factory.create(childInjector); + if (this.nzComponentParams) { + Object.assign(this.contentComponentRef.instance, this.nzComponentParams); + } + // Do the first change detection immediately (or we do detection at ngAfterViewInit, multi-changes error will be thrown) + this.contentComponentRef.changeDetectorRef.detectChanges(); + } + + // Update transform-origin to the last click position on document + private updateTransformOrigin(): void { + const modalElement = this.modalContainer.nativeElement as HTMLElement; + const lastPosition = ModalUtil.getLastClickPosition(); + if (lastPosition) { + this.transformOrigin = `${lastPosition.x - modalElement.offsetLeft}px ${lastPosition.y - modalElement.offsetTop}px 0px`; + } + // else { + // this.transformOrigin = '0px 0px 0px'; + // } + } + + // TODO: We should detect if there are modals remained in this page, if 0 modals that we chould to remove overflow, otherwise, we should leave it 'hidden'. + private changeBodyOverflow(visible: boolean): void { + this.document.body.style.overflow = visible ? 'hidden' : ''; + } +} + +//////////// + +function isPromise(obj: {} | void): boolean { + return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof (obj as Promise<{}>).then === 'function' && typeof (obj as Promise<{}>).catch === 'function'; +} diff --git a/components/modal/nz-modal.module.ts b/components/modal/nz-modal.module.ts new file mode 100644 index 00000000000..e50c3755a6c --- /dev/null +++ b/components/modal/nz-modal.module.ts @@ -0,0 +1,18 @@ +import { OverlayModule } from '@angular/cdk/overlay'; +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { NzButtonModule } from '../button/nz-button.module'; +import { LoggerModule } from '../core/util/logger/logger.module'; +import { NzLocaleModule } from '../locale'; +import { CssUnitPipe } from './css-unit.pipe'; +import { NzModalComponent } from './nz-modal.component'; +import { NzModalService } from './nz-modal.service'; + +@NgModule({ + imports: [ CommonModule, OverlayModule, NzLocaleModule, NzButtonModule, LoggerModule ], + exports: [ NzModalComponent ], + declarations: [ NzModalComponent, CssUnitPipe ], + entryComponents: [ NzModalComponent ], + providers: [ NzModalService ], +}) +export class NzModalModule { } diff --git a/components/modal/nz-modal.service.ts b/components/modal/nz-modal.service.ts new file mode 100644 index 00000000000..c61980d24d4 --- /dev/null +++ b/components/modal/nz-modal.service.ts @@ -0,0 +1,105 @@ +import { Overlay, OverlayRef } from '@angular/cdk/overlay'; +import { ComponentPortal } from '@angular/cdk/portal'; +import { ApplicationRef, ComponentFactoryResolver, ComponentRef, EventEmitter, Injectable, Injector, TemplateRef, Type } from '@angular/core'; +import { LoggerService } from '../core/util/logger/logger.service'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import { NzModalComponent } from './nz-modal.component'; +import { ConfirmType, ModalOptions, ModalOptionsForService } from './nz-modal.type'; + +// A builder used for managing service creating modals +export class ModalBuilderForService { + private modalRef: ComponentRef; // Modal ComponentRef, "null" means it has been destroyed + private overlayRef: OverlayRef; + + constructor(private overlay: Overlay, options: ModalOptionsForService = {}) { + this.createModal(); + + if (!('nzGetContainer' in options)) { // As we use CDK to create modal in service, there is no need to append DOM to body by default + options.nzGetContainer = null; + } + + this.changeProps(options); + this.modalRef.instance.open(); + this.modalRef.instance.nzAfterClose.subscribe(() => this.destroyModal()); // [NOTE] By default, close equals destroy when using as Service + } + + getInstance(): NzModalComponent { + return this.modalRef && this.modalRef.instance; + } + + destroyModal(): void { + if (this.modalRef) { + this.overlayRef.dispose(); + this.modalRef = null; + } + } + + private changeProps(options: ModalOptions): void { + if (this.modalRef) { + Object.assign(this.modalRef.instance, options); // DANGER: here not limit user's inputs at runtime + } + } + + // Create component to ApplicationRef + private createModal(): void { + this.overlayRef = this.overlay.create(); + this.modalRef = this.overlayRef.attach(new ComponentPortal(NzModalComponent)); + } +} + +@Injectable() +export class NzModalService { + + constructor(private overlay: Overlay, private logger: LoggerService) { } + + create(options: ModalOptionsForService = {}): ModalPublicAgent { + if (typeof options.nzOnCancel !== 'function') { + options.nzOnCancel = () => {}; // Leave a empty function to close this modal by default + } + + return new ModalBuilderForService(this.overlay, options).getInstance(); + } + + confirm(options: ModalOptionsForService = {}, confirmType: ConfirmType = 'confirm'): ModalPublicAgent { + if ('nzFooter' in options) { + this.logger.warn(`The Confirm-Modal doesn't support "nzFooter", this property will be ignored.`); + } + if (!('nzWidth' in options)) { + options.nzWidth = 416; + } + if (typeof options.nzOnOk !== 'function') { // NOTE: only support function currently by calling confirm() + options.nzOnOk = () => {}; // Leave a empty function to close this modal by default + } + + options.nzModalType = 'confirm'; + options.nzClassName = `ant-confirm ant-confirm-${confirmType} ${options.nzClassName || ''}`; + options.nzMaskClosable = false; + return this.create(options); + } + + info(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'info'); + } + + success(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'success'); + } + + error(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'error'); + } + + warning(options: ModalOptionsForService = {}): ModalPublicAgent { + return this.simpleConfirm(options, 'warning'); + } + + private simpleConfirm(options: ModalOptionsForService = {}, confirmType: ConfirmType): ModalPublicAgent { + if (!('nzIconType' in options)) { + options.nzIconType = { 'info': 'info-circle', 'success': 'check-circle', 'error': 'cross-circle', 'warning': 'exclamation-circle' }[ confirmType ]; + } + if (!('nzCancelText' in options)) { // Remove the Cancel button if the user not specify a Cancel button + options.nzCancelText = null; + } + return this.confirm(options, confirmType); + } +} diff --git a/components/modal/nz-modal.spec.ts b/components/modal/nz-modal.spec.ts new file mode 100644 index 00000000000..4b90be57266 --- /dev/null +++ b/components/modal/nz-modal.spec.ts @@ -0,0 +1,502 @@ +import { Component, DebugElement, ElementRef, Input, NgModule, OnInit } from '@angular/core'; +import { async, fakeAsync, flush, tick, ComponentFixture, ComponentFixtureAutoDetect, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; + +import { NzButtonComponent } from '../button/nz-button.component'; +import { NzButtonModule } from '../button/nz-button.module'; +import { CssUnitPipe } from './css-unit.pipe'; +import { ModalPublicAgent } from './modal-public-agent.class'; +import { MODAL_ANIMATE_DURATION, NzModalComponent } from './nz-modal.component'; +import { NzModalModule } from './nz-modal.module'; +import { NzModalService } from './nz-modal.service'; + +const WAIT_ANIMATE_TIME = MODAL_ANIMATE_DURATION + 50; + +describe('modal', () => { + let instance; + let fixture: ComponentFixture<{}>; + + describe('demo-async', () => { + let modalElement: HTMLElement; + let buttonShow: HTMLButtonElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ NzButtonModule, NzModalModule ], + declarations: [ NzDemoModalAsyncComponent ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NzDemoModalAsyncComponent); + instance = fixture.debugElement.componentInstance; + modalElement = fixture.debugElement.query(By.directive(NzModalComponent)).nativeElement; + buttonShow = fixture.debugElement.query(By.directive(NzButtonComponent)).nativeElement; + }); + + it('should show and hide after 3000ms with loading', fakeAsync(() => { + buttonShow.click(); + fixture.detectChanges(); + flush(); + expectModalHidden(modalElement, false); + + const buttonOk = getButtonOk(modalElement); + buttonOk.click(); // Click Ok button + fixture.detectChanges(); + expect(isButtonLoading(buttonOk)).not.toBeFalsy(); + + tick(3000 + 10); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); // In order to trigger ModalInstance's UI updating after finished hiding + expectModalHidden(modalElement, true); + })); + }); // /async + + describe('demo-confirm-promise', () => { + const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service + let modalAgent: ModalPublicAgent; + let buttonShow: HTMLButtonElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ NzButtonModule, NzModalModule ], + declarations: [ NzDemoModalConfirmPromiseComponent ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NzDemoModalConfirmPromiseComponent); + instance = fixture.debugElement.componentInstance; + buttonShow = fixture.debugElement.query(By.directive(NzButtonComponent)).nativeElement; + + buttonShow.click(); + fixture.detectChanges(); + modalAgent = instance.confirmModal; + modalAgent.getElement().classList.add(tempModalId); + }); + + it('should open and click Ok to destroy after 1000ms', fakeAsync(() => { + expectModalDestroyed(tempModalId, false); + + getButtonOk(modalAgent.getElement()).click(); // Click Ok button + fixture.detectChanges(); + tick(1000 + 10); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expectModalDestroyed(tempModalId, true); + })); + + it('should open and destroy immediately when click Cancel', fakeAsync(() => { + expectModalDestroyed(tempModalId, false); + + getButtonClose(modalAgent.getElement()).click(); // Click Close button + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expectModalDestroyed(tempModalId, true); + })); + }); // /confirm-promise + + describe('NormalModal: created by service with most APIs', () => { + const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service + let modalAgent: ModalPublicAgent; + let modalElement: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ NzModalModule ], + declarations: [ TestBasicServiceComponent ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestBasicServiceComponent); + instance = fixture.debugElement.componentInstance; + modalAgent = instance.basicModal; + modalElement = modalAgent.getElement(); + modalElement.classList.add(tempModalId); // Mark with id + }); + + it('should correctly render all basic props', fakeAsync(() => { + const modalInstance = modalAgent.getInstance(); + spyOn(console, 'log'); + + // [Hack] Codes that can't be covered by normal operations + // tslint:disable-next-line:no-any + expect((modalInstance as any).changeVisibleFromInside(true) instanceof Promise).toBe(true); + + expect((modalElement.querySelector('.ant-modal-wrap') as HTMLElement).style.zIndex).toBe('1888'); + expect((modalElement.querySelector('.ant-modal-wrap') as HTMLElement).classList.contains('test-wrap-class-name')).toBe(true); + expect((modalElement.querySelector('.ant-modal') as HTMLElement).style.width).toBe('250px'); + expect((modalElement.querySelector('.ant-modal') as HTMLElement).classList.contains('test-class-name')).toBe(true); + expect((modalElement.querySelector('.ant-modal') as HTMLElement).style.top).toBe('20pt'); + expect((modalElement.querySelector('.ant-modal-title') as HTMLElement).innerHTML.indexOf('TEST BOLD TITLE')).toBeGreaterThan(-1); + // expect((modalElement.querySelector('.ant-modal-footer') as HTMLElement).innerHTML.indexOf('
custom html footer: OK
')).toBeGreaterThan(-1); + expect((modalElement.querySelector('.ant-modal-body') as HTMLElement).innerHTML.indexOf('

test html content

')).toBeGreaterThan(-1); + expect((modalElement.querySelector('.ant-modal-body') as HTMLElement).style.background).toBe('gray'); + expect(getButtonOk(modalElement).innerHTML.indexOf('custom ok')).toBeGreaterThan(-1); + expect(getButtonOk(modalElement).classList.contains('ant-btn-success')).toBe(true); + expect(isButtonLoading(getButtonOk(modalElement))).toBeFalsy(); + expect(getButtonCancel(modalElement).innerHTML.indexOf('custom cancel')).toBeGreaterThan(-1); + expect(isButtonLoading(getButtonCancel(modalElement))).not.toBeFalsy(); + expect(modalElement.querySelector('.ant-modal-close')).toBeFalsy(); + expect(modalElement.querySelector('.ant-modal-mask')).toBeFalsy(); + + // click ok button + getButtonOk(modalElement).click(); + expect(console.log).toHaveBeenCalledWith('click ok'); + expectModalDestroyed(tempModalId, false); // shouldn't destroy when ok button returns false + // change and click mask + modalInstance.nzMask = true; + fixture.detectChanges(); + // should show mask + expect((modalElement.querySelector('.ant-modal-mask') as HTMLElement).style.opacity).toBe('0.4'); + // should not trigger nzOnCancel if click mask + (modalElement.querySelector('.ant-modal-wrap') as HTMLElement).click(); + expect(console.log).not.toHaveBeenCalledWith('click cancel'); + // change nzMaskClosable to true then click, should be called and destroyed + modalInstance.nzMaskClosable = true; + (modalElement.querySelector('.ant-modal-wrap') as HTMLElement).click(); + expect(console.log).toHaveBeenCalledWith('click cancel'); + flush(); + expectModalDestroyed(tempModalId, true); // should be destroyed + })); // /basic props + }); + + describe('NormalModal: created by service with vary nzContent and nzFooter', () => { + const tempModalId = generateUniqueId(); // Temp unique id to mark the confirm modal that created by service + let modalAgent: ModalPublicAgent; + let modalElement: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ NzModalModule ], + declarations: [ TestVaryServiceComponent, TestVaryServiceCustomComponent ] + }); + TestBed.overrideModule(BrowserDynamicTestingModule, { + set: { entryComponents: [ TestVaryServiceCustomComponent ] } + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestVaryServiceComponent); + instance = fixture.debugElement.componentInstance; + modalAgent = instance.createWithVary(); + modalElement = modalAgent.getElement(); + modalElement.classList.add(tempModalId); // Mark with id + }); + + it('should change title from in/outside and trigger button', fakeAsync(() => { + fixture.detectChanges(); // Initial change detecting + + const contentComponent = modalAgent.getContentComponentRef().instance as TestVaryServiceCustomComponent; + const contentElement = contentComponent.elementRef.nativeElement as HTMLElement; + // change title from outside + const firstButton = modalElement.querySelector('.ant-modal-footer button:first-child') as HTMLButtonElement; + firstButton.click(); + fixture.detectChanges(); + expect(contentComponent.title).toBe('internal title changed'); + expect(isButtonLoading(firstButton)).toBe(false); // stopped immediately + + // button loading for Promise + const lastButton = modalElement.querySelector('.ant-modal-footer button:last-child') as HTMLButtonElement; + lastButton.click(); + fixture.detectChanges(); + expect(isButtonLoading(lastButton)).toBe(false); // stopped immediately + + // destroy from inside + contentElement.querySelector('button').click(); + fixture.detectChanges(); + flush(); + expectModalDestroyed(tempModalId, true); + })); // /vary with component + }); + + describe('ConfirmModal: should apply options correctly', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ NzModalModule ], + declarations: [ TestConfirmModalComponent ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestConfirmModalComponent); + instance = fixture.debugElement.componentInstance; + }); + + it('boundary detection for options', fakeAsync(() => { + const logger = instance.modalService.logger; + spyOn(logger, 'warn'); + + const tempModalId = generateUniqueId(); + const modalAgent = instance.createConfirm() as ModalPublicAgent; + const modalElement = modalAgent.getElement(); + modalElement.classList.add(tempModalId); + fixture.detectChanges(); + // nzFooter + expect(logger.warn).toHaveBeenCalled(); + // nzOnOk: close modal when clicked + getButtonOk(modalElement).click(); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + expectModalDestroyed(tempModalId, true); + })); + + it('should render other confirm modals', fakeAsync(() => { + const ids: string[] = instance.createOtherModals(); + fixture.detectChanges(); + flush(); + fixture.detectChanges(); + ids.forEach(id => expectModalDestroyed(id, false)); + })); + }); + + describe('css-unit.pipe', () => { + let testElement: HTMLDivElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ CssUnitPipe, TestCssUnitPipeComponent ] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TestCssUnitPipeComponent); + testElement = fixture.debugElement.query(By.css('div')).nativeElement; + fixture.detectChanges(); + }); + + it('should "width" & "height" to be 100px', () => { + // fixture.detectChanges(); + expect(testElement.style.width).toBe('100px'); + expect(testElement.style.height).toBe('100px'); + }); + + it('should "top" to be 100pt', () => { + // fixture.detectChanges(); + expect(testElement.style.top).toBe('100pt'); + }); + }); +}); + +// ------------------------------------------- +// | Testing Components +// ------------------------------------------- + +@Component({ + selector: 'nz-demo-modal-async', + template: ` + + +

content

+
+ `, + styles: [] +}) +class NzDemoModalAsyncComponent { + isVisible = false; + isOkLoading = false; + + showModal(): void { + this.isVisible = true; + } + + handleOk($event: MouseEvent): void { + this.isOkLoading = true; + window.setTimeout(() => { + this.isVisible = false; + this.isOkLoading = false; + }, 3000); + } + + handleCancel($event: MouseEvent): void { + this.isVisible = false; + } +} + +@Component({ + selector: 'nz-demo-modal-confirm-promise', + template: ` + + `, + styles : [] +}) +class NzDemoModalConfirmPromiseComponent { + confirmModal: ModalPublicAgent; // For testing by now + + constructor(private modal: NzModalService) { } + + showConfirm(): void { + this.confirmModal = this.modal.confirm({ + nzTitle: 'Do you Want to delete these items?', + nzContent: 'When clicked the OK button, this dialog will be closed after 1 second', + nzOnOk: () => new Promise((resolve, reject) => { + setTimeout(Math.random() > 0.5 ? resolve : reject, 1000); + }).catch(() => console.log('Oops errors!')) + }); + } +} + +@Component({ + template: `` +}) +class TestBasicServiceComponent { + basicModal: ModalPublicAgent; + + constructor(private modalService: NzModalService) { + // Testing for creating modal immediately + this.basicModal = this.modalService.create({ + nzGetContainer: () => document.body, + nzZIndex: 1888, + nzWidth: 250, + nzWrapClassName: 'test-wrap-class-name', + nzClassName: 'test-class-name', + nzStyle: { left: '10px', top: '20pt', border: '2px solid red' }, + nzTitle: 'TEST BOLD TITLE', + nzContent: '

test html content

', + nzClosable: false, + nzMask: false, + nzMaskClosable: false, + nzMaskStyle: { opacity: 0.4 }, + nzBodyStyle: { background: 'gray' }, + // nzFooter: '
custom html footer: OK
', + nzOkText: 'custom ok', + nzOkType: 'success', + nzOkLoading: false, + nzOnOk: () => { console.log('click ok'); return false; }, + nzCancelText: 'custom cancel', + nzCancelLoading: true, + nzOnCancel: () => console.log('click cancel') + }); + } +} + +@Component({ + template: `` +}) +class TestVaryServiceComponent { + constructor(private modalService: NzModalService) {} + + createWithVary(): ModalPublicAgent { + const modal = this.modalService.create({ + nzContent: TestVaryServiceCustomComponent, + nzComponentParams: { title: 'internal title', subtitle: 'subtitle' }, + nzFooter: [ + { + label: 'change title from outside', + onClick: (componentInstance: TestVaryServiceCustomComponent) => { + componentInstance.title = 'internal title changed'; + return Promise.resolve(); + } + }, + { + label: 'show loading', + onClick: () => Promise.reject(null) + } + ], + }); + + return modal; + } +} +@Component({ + template: ` +

{{ title }}

{{ subtitle }}

+ + ` +}) +export class TestVaryServiceCustomComponent { + @Input() title: string; + @Input() subtitle: string; + + constructor(private modal: ModalPublicAgent, public elementRef: ElementRef) { } + + destroyModal(): void { + this.modal.destroy(); + } +} + +@Component({ + template: `` +}) +export class TestConfirmModalComponent { + constructor(public modalService: NzModalService) { } + + createConfirm(): ModalPublicAgent { + // Boundary detection for options: nzFooter, nzOnOk + return this.modalService.confirm({ + nzFooter: 'should warning', + nzOkText: 'close' + }); + } + + createOtherModals(): string[] { + return [ 'info', 'success', 'error', 'warning' ].map(type => { + const modalId = generateUniqueId(); + this.modalService[type]({ + nzClassName: modalId + }); + return modalId; + }); + } +} + +@Component({ + template: `
` +}) +class TestCssUnitPipeComponent { } + +// ------------------------------------------- +// | Local tool functions +// ------------------------------------------- + +function expectModalHidden(modalElement: HTMLElement, hidden: boolean): void { + const display = (modalElement.querySelector('.ant-modal-wrap') as HTMLElement).style.display; + if (hidden) { + expect(display).toBe('none'); + } else { + expect(display).not.toBe('none'); + } + expect(modalElement.querySelector('.ant-modal-mask').classList.contains('ant-modal-mask-hidden')).toBe(hidden); +} + +function expectModalDestroyed(classId: string, destroyed: boolean): void { + const element = document.querySelector(`.${classId}`); + if (destroyed) { + expect(element).toBeFalsy(); + } else { + expect(element).not.toBeFalsy(); + } +} + +let counter = 0; +function generateUniqueId(): string { + return `testing-uniqueid-${counter++}`; +} + +function getButtonOk(modalElement: HTMLElement): HTMLButtonElement { + return isConfirmModal(modalElement) ? modalElement.querySelector('.ant-confirm-btns button:last-child') as HTMLButtonElement : modalElement.querySelector('.ant-modal-footer button:last-child') as HTMLButtonElement; +} + +function getButtonCancel(modalElement: HTMLElement): HTMLButtonElement { + return isConfirmModal(modalElement) ? modalElement.querySelector('.ant-confirm-btns button:first-child') as HTMLButtonElement : modalElement.querySelector('.ant-modal-footer button:first-child') as HTMLButtonElement; +} + +function getButtonClose(modalElement: HTMLElement): HTMLButtonElement { // For normal modal only + return modalElement.querySelector('.ant-modal-close') as HTMLButtonElement; +} + +function isConfirmModal(modalElement: HTMLElement): boolean { + return !!modalElement.querySelector('.ant-confirm'); +} + +function isButtonLoading(buttonElement: HTMLButtonElement): boolean { + return !!buttonElement.querySelector('i.anticon-loading'); +} diff --git a/components/modal/nz-modal.type.ts b/components/modal/nz-modal.type.ts new file mode 100644 index 00000000000..3a99bf2673c --- /dev/null +++ b/components/modal/nz-modal.type.ts @@ -0,0 +1,59 @@ +import { EventEmitter, TemplateRef, Type } from '@angular/core'; + +export type OnClickCallback = (($event: MouseEvent) => (false | void | {}) | Promise); + +export type ModalType = 'default' | 'confirm'; // Different modal styles we have supported + +export type ConfirmType = 'confirm' | 'info' | 'success' | 'error' | 'warning'; // Subtypes of Confirm Modal + +// Public options for using by service +export interface ModalOptions { + nzModalType?: ModalType; + nzVisible?: boolean; + nzZIndex?: number; + nzWidth?: number | string; + nzWrapClassName?: string; + nzClassName?: string; + nzStyle?: object; + nzIconType?: string; // Confirm Modal ONLY + nzTitle?: string | TemplateRef<{}>; + nzContent?: string | TemplateRef<{}> | Type<{}>; + nzComponentParams?: object; + nzClosable?: boolean; + nzMask?: boolean; + nzMaskClosable?: boolean; + nzMaskStyle?: object; + nzBodyStyle?: object; + nzFooter?: string | TemplateRef<{}> | ModalButtonOptions[]; // Default Modal ONLY + nzGetContainer?: HTMLElement | (() => HTMLElement); // STATIC + nzAfterClose?: EventEmitter; + + // --- Predefined OK & Cancel buttons + nzOkText?: string; + nzOkType?: string; + nzOkLoading?: boolean; + nzOnOk?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback) + nzCancelText?: string; + nzCancelLoading?: boolean; + nzOnCancel?: EventEmitter | OnClickCallback; // Mixed using ng's Input/Output (Should care of "this" when using OnClickCallback) +} + +export interface ModalOptionsForService extends ModalOptions { // Limitations for using by service + nzOnOk?: OnClickCallback; + nzOnCancel?: OnClickCallback; +} + +export interface ModalButtonOptions { + label: string; + type?: string; + shape?: string; + ghost?: boolean; + size?: string; + autoLoading?: boolean; // Default: true, indicate whether show loading automatically while onClick returned a Promise + + // [NOTE] "componentInstance" will refer to the component's instance when using Component + show?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + loading?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); // This prop CAN'T use with autoLoading=true + disabled?: boolean | ((this: ModalButtonOptions, contentComponentInstance?: object) => boolean); + onClick?(this: ModalButtonOptions, contentComponentInstance?: object): (void | {}) | Promise<(void | {})>; +} diff --git a/components/modal/public-api.ts b/components/modal/public-api.ts new file mode 100644 index 00000000000..e009a4bf342 --- /dev/null +++ b/components/modal/public-api.ts @@ -0,0 +1,5 @@ +export { NzModalComponent } from './nz-modal.component'; +export { ModalPublicAgent } from './modal-public-agent.class'; +export { NzModalModule } from './nz-modal.module'; +export { NzModalService } from './nz-modal.service'; +export * from './nz-modal.type'; diff --git a/components/modal/style/confirm.less b/components/modal/style/confirm.less new file mode 100644 index 00000000000..a2170172398 --- /dev/null +++ b/components/modal/style/confirm.less @@ -0,0 +1,70 @@ +@import "../../style/mixins/index"; + +@confirm-prefix-cls: ~"@{ant-prefix}-confirm"; + +.@{confirm-prefix-cls} { + .@{ant-prefix}-modal-header { + display: none; + } + + .@{ant-prefix}-modal-close { + display: none; + } + + .@{ant-prefix}-modal-body { + padding: 30px 40px; + } + + &-body-wrapper { + .clearfix(); + } + + &-body { + .@{confirm-prefix-cls}-title { + color: @text-color; + font-weight: bold; + font-size: @font-size-lg; + } + + .@{confirm-prefix-cls}-content { + margin-left: 42px; + font-size: @font-size-base; + color: @text-color; + margin-top: 8px; + } + + > .@{iconfont-css-prefix} { + font-size: 24px; + margin-right: 16px; + padding: 0 1px; + float: left; + } + } + + .@{confirm-prefix-cls}-btns { + margin-top: 30px; + float: right; + + button + button { + margin-left: 10px; + margin-bottom: 0; + } + } + + &-error &-body > .@{iconfont-css-prefix} { + color: @error-color; + } + + &-warning &-body > .@{iconfont-css-prefix}, + &-confirm &-body > .@{iconfont-css-prefix} { + color: @warning-color; + } + + &-info &-body > .@{iconfont-css-prefix} { + color: @info-color; + } + + &-success &-body > .@{iconfont-css-prefix} { + color: @success-color; + } +} diff --git a/components/modal/style/index.less b/components/modal/style/index.less new file mode 100644 index 00000000000..17d29353d42 --- /dev/null +++ b/components/modal/style/index.less @@ -0,0 +1,3 @@ +@import "../../style/themes/default"; +@import "./modal"; +@import "./confirm"; diff --git a/components/modal/style/modal.less b/components/modal/style/modal.less new file mode 100644 index 00000000000..755717e913c --- /dev/null +++ b/components/modal/style/modal.less @@ -0,0 +1,144 @@ +@dialog-prefix-cls: ~"@{ant-prefix}-modal"; + +.@{dialog-prefix-cls} { + position: relative; + width: auto; + margin: 0 auto; + top: 100px; + padding-bottom: 24px; + + &-wrap { + position: fixed; + overflow: auto; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: @zindex-modal; + -webkit-overflow-scrolling: touch; + outline: 0; + } + + &-title { + margin: 0; + font-size: @font-size-lg; + line-height: 21px; + font-weight: 500; + color: @heading-color; + } + + &-content { + position: relative; + background-color: @component-background; + border: 0; + border-radius: @border-radius-base; + background-clip: padding-box; + box-shadow: @shadow-2; + } + + &-close { + cursor: pointer; + border: 0; + background: transparent; + position: absolute; + right: 0; + top: 0; + z-index: 10; + font-weight: 700; + line-height: 1; + text-decoration: none; + transition: color .3s ease; + color: @text-color-secondary; + outline: 0; + + &-x { + display: block; + font-style: normal; + vertical-align: baseline; + text-align: center; + text-transform: none; + text-rendering: auto; + width: 48px; + height: 48px; + line-height: 48px; + font-size: @font-size-lg; + + &:before { + content: "\e633"; + display: block; + font-family: "anticon" !important; + } + } + + &:focus, + &:hover { + color: #444; + text-decoration: none; + } + } + + &-header { + padding: 13px 16px; + border-radius: @border-radius-base @border-radius-base 0 0; + background: @component-background; + color: @text-color; + border-bottom: @border-width-base @border-style-base @border-color-split; + } + + &-body { + padding: 16px; + font-size: @font-size-base; + line-height: 1.5; + } + + &-footer { + border-top: @border-width-base @border-style-base @border-color-split; + padding: 10px 16px 10px 10px; + text-align: right; + border-radius: 0 0 @border-radius-base @border-radius-base; + button + button { + margin-left: 8px; + margin-bottom: 0; + } + } + + &.zoom-enter, + &.zoom-appear { + animation-duration: @animation-duration-slow; + transform: none; // reset scale avoid mousePosition bug + opacity: 0; + } + + &-mask { + position: fixed; + top: 0; + right: 0; + left: 0; + bottom: 0; + background-color: #373737; + background-color: @modal-mask-bg; // lesshint duplicateProperty: false + height: 100%; + z-index: @zindex-modal-mask; + filter: ~"alpha(opacity=50)"; + + &-hidden { + display: none; + } + } + + &-open { + overflow: hidden; + } +} + +@media (max-width: 768px) { + .@{dialog-prefix-cls} { + width: auto !important; + margin: 10px; + } + .vertical-center-modal { + .@{dialog-prefix-cls} { + flex: 1; + } + } +} diff --git a/components/test.ts b/components/test.ts index 587aec6035d..d6f8acbffad 100644 --- a/components/test.ts +++ b/components/test.ts @@ -1,23 +1,27 @@ +/* tslint:disable:no-import-side-effect */ + // This file is required by karma.conf.js and loads recursively all the .spec and framework files -import 'zone.js/dist/long-stack-trace-zone'; import 'zone.js/dist/proxy.js'; import 'zone.js/dist/sync-test'; -import 'zone.js/dist/jasmine-patch'; + import 'zone.js/dist/async-test'; import 'zone.js/dist/fake-async-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/long-stack-trace-zone'; + import { getTestBed } from '@angular/core/testing'; import { - BrowserDynamicTestingModule, - platformBrowserDynamicTesting + platformBrowserDynamicTesting, + BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. -declare const __karma__: any; -declare const require: any; +declare const __karma__: any; // tslint:disable-line:no-any +declare const require: any; // tslint:disable-line:no-any // Prevent Karma from running prematurely. -__karma__.loaded = function () {}; +__karma__.loaded = function (): void {}; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( @@ -26,7 +30,6 @@ getTestBed().initTestEnvironment( ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); -// const context = require.context('./', true, /nz-affix\.component\.spec\.ts$/); // And load the modules. context.keys().map(context); // Finally, start Karma to run the tests.