diff --git a/src/app/shared/dialog/README.md b/src/app/shared/dialog/README.md new file mode 100644 index 0000000..d0b36ed --- /dev/null +++ b/src/app/shared/dialog/README.md @@ -0,0 +1,53 @@ +# Dialog + +## Services + +### open 打开方法 +打开一个包含给定组件的模式对话框。 + +#### Parameters +componentRef + +加载到对话框中的组件的类型,或作为对话框内容实例化的TemplateRef。 + +config + +额外的配置选项。参考Config + +#### Returns + +DialogRef + +引用新打开的对话框。 + +## Directives + +### appDialogHeader +对话框头部 +Selector:[app-dialog-header],[appDialogHeader] + +### appDialogBody +对话框内容 +Selector:[app-dialog-body],[appDialogBody] + +### appDialogFooter +对话框尾部 +Selector:[app-dialog-footer],[appDialogFooter] + +可选配置: +align 按钮对齐方式(默认左对齐) +center 居中对齐 +end 右对齐 + +### appDialogClose +关闭对话框 +Selector:[appDialogClose],[app-dialog-close] + +## Config + +resolve?: {} | null = null; // 给组件传入数据 keys 会注入子组件的inputs里 +zIndex?: number; // 层级 默认Backdrop 1040 Dialog 1050 Dialog总是比Backdrop多10 +height?: string; // 高度 默认宽度600px +width?: string; // 宽度 auto +hasBackdrop?: boolean; // 是否有遮罩背景 +customClass?: string; // 自定义样式 dialog-backdrop-customClass dialog-customClass \ No newline at end of file diff --git a/src/app/shared/dialog/dialog-body/dialog-body.component.html b/src/app/shared/dialog/dialog-body/dialog-body.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/src/app/shared/dialog/dialog-body/dialog-body.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/shared/dialog/dialog-body/dialog-body.component.scss b/src/app/shared/dialog/dialog-body/dialog-body.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/dialog/dialog-body/dialog-body.component.spec.ts b/src/app/shared/dialog/dialog-body/dialog-body.component.spec.ts new file mode 100644 index 0000000..0bae2fa --- /dev/null +++ b/src/app/shared/dialog/dialog-body/dialog-body.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogBodyComponent } from './dialog-body.component'; + +describe('DialogBodyComponent', () => { + let component: DialogBodyComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogBodyComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogBodyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog-body/dialog-body.component.ts b/src/app/shared/dialog/dialog-body/dialog-body.component.ts new file mode 100644 index 0000000..d3d8226 --- /dev/null +++ b/src/app/shared/dialog/dialog-body/dialog-body.component.ts @@ -0,0 +1,22 @@ +import { Component, OnInit, HostBinding } from '@angular/core'; + +@Component({ + // tslint:disable-next-line:component-selector + selector: '[app-dialog-body],[appDialogBody]', + templateUrl: './dialog-body.component.html', + styleUrls: ['./dialog-body.component.scss'] +}) +export class DialogBodyComponent implements OnInit { + /** + * 绑定类 + */ + @HostBinding('class.dialog-body') + get _setDialogBodyClass() { + return true; + } + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/shared/dialog/dialog-close.directive.spec.ts b/src/app/shared/dialog/dialog-close.directive.spec.ts new file mode 100644 index 0000000..481bb5f --- /dev/null +++ b/src/app/shared/dialog/dialog-close.directive.spec.ts @@ -0,0 +1,8 @@ +import { DialogCloseDirective } from './dialog-close.directive'; + +describe('DialogCloseDirective', () => { + it('should create an instance', () => { + const directive = new DialogCloseDirective(); + expect(directive).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog-close.directive.ts b/src/app/shared/dialog/dialog-close.directive.ts new file mode 100644 index 0000000..623c297 --- /dev/null +++ b/src/app/shared/dialog/dialog-close.directive.ts @@ -0,0 +1,21 @@ +import { Directive, HostListener } from '@angular/core'; +import { DialogSubjectService } from './dialog-subject.service'; + +@Directive({ + selector: '[appDialogClose],[app-dialog-close]' +}) +export class DialogCloseDirective { + + constructor(private subject: DialogSubjectService) { } + + /** clear all item selected status except this */ + @HostListener('click', ['$event']) + _onClickItem(event: Event) { + event.preventDefault(); + event.stopPropagation(); + // 如果没有dialogId 说明不是一个弹窗 + if (this.subject.dialogId) { + this.subject.next('onCancel'); + } + } +} diff --git a/src/app/shared/dialog/dialog-config.service.spec.ts b/src/app/shared/dialog/dialog-config.service.spec.ts new file mode 100644 index 0000000..d34c9b8 --- /dev/null +++ b/src/app/shared/dialog/dialog-config.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { DialogConfigService } from './dialog-config.service'; + +describe('DialogConfigService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DialogConfigService] + }); + }); + + it('should be created', inject([DialogConfigService], (service: DialogConfigService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/shared/dialog/dialog-config.service.ts b/src/app/shared/dialog/dialog-config.service.ts new file mode 100644 index 0000000..397e70c --- /dev/null +++ b/src/app/shared/dialog/dialog-config.service.ts @@ -0,0 +1,12 @@ +/** + * dialog 配置参数 + */ +export class DialogConfig { + /** 传递给组件的数据 */ + resolve?: D | null = null; // 给组件传入数据 + zIndex?: number; // 层级 + height?: string; // 高度 + width?: string; // 宽度 + hasBackdrop?: boolean; // 是否有遮罩背景 + customClass?: string; // 自定义样式 dialog-backdrop-customClass dialog-customClass +} diff --git a/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.html b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.html new file mode 100644 index 0000000..e61be8d --- /dev/null +++ b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.html @@ -0,0 +1,12 @@ +
+ +
+ icon {{ _content }} +
+
+ +
+
+ + +
\ No newline at end of file diff --git a/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.scss b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.spec.ts b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.spec.ts new file mode 100644 index 0000000..a1853ae --- /dev/null +++ b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogConfirmComponent } from './dialog-confirm.component'; + +describe('DialogConfirmComponent', () => { + let component: DialogConfirmComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogConfirmComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogConfirmComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.ts b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.ts new file mode 100644 index 0000000..cf3b336 --- /dev/null +++ b/src/app/shared/dialog/dialog-confirm/dialog-confirm.component.ts @@ -0,0 +1,34 @@ +import { Component, OnInit, OnDestroy, Input, TemplateRef } from '@angular/core'; +import { DialogSubjectService } from '../dialog-subject.service'; + +@Component({ + selector: 'app-dialog-confirm', + templateUrl: './dialog-confirm.component.html', + styleUrls: ['./dialog-confirm.component.scss'] +}) +export class DialogConfirmComponent implements OnInit { + _contentTpl: TemplateRef; + _content: string; + + @Input() + set content(value: string | TemplateRef) { + if (value instanceof TemplateRef) { + this._contentTpl = value; + } else { + this._content = value; + } + } + constructor(private subject: DialogSubjectService) { } + + ngOnInit() { + } + + clickCancel() { + this.subject.next('onCancel'); + } + + clickConfirm() { + this.subject.next({ data: 'hahah' }); + this.subject.next('onConfirm'); + } +} diff --git a/src/app/shared/dialog/dialog-footer/dialog-footer.component.html b/src/app/shared/dialog/dialog-footer/dialog-footer.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/src/app/shared/dialog/dialog-footer/dialog-footer.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/shared/dialog/dialog-footer/dialog-footer.component.scss b/src/app/shared/dialog/dialog-footer/dialog-footer.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/dialog/dialog-footer/dialog-footer.component.spec.ts b/src/app/shared/dialog/dialog-footer/dialog-footer.component.spec.ts new file mode 100644 index 0000000..8f04e02 --- /dev/null +++ b/src/app/shared/dialog/dialog-footer/dialog-footer.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogFooterComponent } from './dialog-footer.component'; + +describe('DialogFooterComponent', () => { + let component: DialogFooterComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogFooterComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogFooterComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog-footer/dialog-footer.component.ts b/src/app/shared/dialog/dialog-footer/dialog-footer.component.ts new file mode 100644 index 0000000..3d9cc2e --- /dev/null +++ b/src/app/shared/dialog/dialog-footer/dialog-footer.component.ts @@ -0,0 +1,26 @@ +import { Component, OnInit, HostBinding, Input, ViewEncapsulation } from '@angular/core'; + + +@Component({ + // tslint:disable-next-line:component-selector + selector: '[app-dialog-footer],[appDialogFooter]', + templateUrl: './dialog-footer.component.html', + styleUrls: ['./dialog-footer.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DialogFooterComponent implements OnInit { + + /** + * 绑定类 + */ + @HostBinding('class.dialog-footer') + get _setDialogFooterClass() { + return true; + } + + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/shared/dialog/dialog-header/dialog-header.component.html b/src/app/shared/dialog/dialog-header/dialog-header.component.html new file mode 100644 index 0000000..95a0b70 --- /dev/null +++ b/src/app/shared/dialog/dialog-header/dialog-header.component.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/app/shared/dialog/dialog-header/dialog-header.component.scss b/src/app/shared/dialog/dialog-header/dialog-header.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/shared/dialog/dialog-header/dialog-header.component.spec.ts b/src/app/shared/dialog/dialog-header/dialog-header.component.spec.ts new file mode 100644 index 0000000..00f3af0 --- /dev/null +++ b/src/app/shared/dialog/dialog-header/dialog-header.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogHeaderComponent } from './dialog-header.component'; + +describe('DialogHeaderComponent', () => { + let component: DialogHeaderComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogHeaderComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogHeaderComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog-header/dialog-header.component.ts b/src/app/shared/dialog/dialog-header/dialog-header.component.ts new file mode 100644 index 0000000..0aa5170 --- /dev/null +++ b/src/app/shared/dialog/dialog-header/dialog-header.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit, HostBinding, ViewEncapsulation } from '@angular/core'; + +@Component({ + // tslint:disable-next-line:component-selector + selector: '[app-dialog-header],[appDialogHeader]', + templateUrl: './dialog-header.component.html', + styleUrls: ['./dialog-header.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class DialogHeaderComponent implements OnInit { + /** + * 绑定类 + */ + @HostBinding('class.dialog-header') + get _setDialogHeaderClass() { + return true; + } + constructor() { } + + ngOnInit() { + } + +} diff --git a/src/app/shared/dialog/dialog-subject.service.spec.ts b/src/app/shared/dialog/dialog-subject.service.spec.ts new file mode 100644 index 0000000..46731de --- /dev/null +++ b/src/app/shared/dialog/dialog-subject.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { DialogSubjectService } from './dialog-subject.service'; + +describe('DialogSubjectService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DialogSubjectService] + }); + }); + + it('should be created', inject([DialogSubjectService], (service: DialogSubjectService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/shared/dialog/dialog-subject.service.ts b/src/app/shared/dialog/dialog-subject.service.ts new file mode 100644 index 0000000..c9cc1ce --- /dev/null +++ b/src/app/shared/dialog/dialog-subject.service.ts @@ -0,0 +1,46 @@ +import { Injectable } from '@angular/core'; +import { Subject } from 'rxjs/Subject'; + +/* dialog的事件枚举 */ +const enum dialogEvent { + onShow, + onShown, + onHide, + onHidden, + onConfirm, + onCancel, + onDestroy +} + +@Injectable() +export class DialogSubjectService extends Subject { + dialogId: string; + eventsQueue = {}; + + constructor() { + super(); + this.subscribe((value: string) => { + const eventQueue: Array = this.eventsQueue[value] || []; + eventQueue.forEach(cb => { + if (cb) { + cb(); + } + }); + }); + } + + destroy(type: any = 'onCancel') { + if (!this.isStopped && !this.closed) { + this.next(type); + } + } + + on(eventType: string, cb: Function) { + if (this.eventsQueue[eventType]) { + this.eventsQueue[eventType].push(cb); + } else { + this.eventsQueue[eventType] = [cb]; + } + } + +} diff --git a/src/app/shared/dialog/dialog.component.html b/src/app/shared/dialog/dialog.component.html new file mode 100644 index 0000000..cbe4fcf --- /dev/null +++ b/src/app/shared/dialog/dialog.component.html @@ -0,0 +1,8 @@ +
+ \ No newline at end of file diff --git a/src/app/shared/dialog/dialog.component.scss b/src/app/shared/dialog/dialog.component.scss new file mode 100644 index 0000000..416cafa --- /dev/null +++ b/src/app/shared/dialog/dialog.component.scss @@ -0,0 +1,96 @@ +.dialog { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1050; + display: block; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; + &-backdrop { + position: fixed; + pointer-events: auto; + z-index: 1000; + top: 0; + bottom: 0; + left: 0; + right: 0; + -webkit-tap-highlight-color: transparent; + transition: opacity .4s cubic-bezier(.25, .8, .25, 1); + background: rgba(0, 0, 0, .6); + &-hidden { + display: none; + } + } + &-container { + position: relative; + top: 40%; + width: 600px; + margin: 0 auto; + .zoom-leave, + .zoom-enter { + animation-duration: .3s; + transform: none; + opacity: 0 + } + } + &-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + background-color: white; + } + &-header { + padding: 15px; + border-bottom: 1px solid #e5e5e5; + } + &-title { + margin: 0; + line-height: 1.42857143; + font-size: 18px; + } + &-close { + cursor: pointer; + float: right; + font-size: 21px; + font-weight: 700; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + opacity: .2; + border: none; + background: none; + &:hover { + opacity: .5; + } + } + &-body { + position: relative; + padding: 15px; + } + &-footer { + font-size: 0; + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; + .u-btn+.u-btn { + margin-bottom: 0; + margin-left: 10px; + } + &[align=end] { + text-align: right; + } + &[align=center] { + text-align: center; + } + } +} \ No newline at end of file diff --git a/src/app/shared/dialog/dialog.component.spec.ts b/src/app/shared/dialog/dialog.component.spec.ts new file mode 100644 index 0000000..a6bce8d --- /dev/null +++ b/src/app/shared/dialog/dialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DialogComponent } from './dialog.component'; + +describe('DialogComponent', () => { + let component: DialogComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ DialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/dialog/dialog.component.ts b/src/app/shared/dialog/dialog.component.ts new file mode 100644 index 0000000..38e7332 --- /dev/null +++ b/src/app/shared/dialog/dialog.component.ts @@ -0,0 +1,161 @@ +import { + Component, + OnInit, + ViewEncapsulation, + ViewChild, + ViewContainerRef, + ComponentFactory, + AfterViewInit, + ComponentRef, + Input, + Inject, + ElementRef, + OnDestroy +} from '@angular/core'; +import { DialogSubjectService } from './dialog-subject.service'; +import { DOCUMENT } from '@angular/platform-browser'; + +@Component({ + selector: 'app-dialog', + templateUrl: './dialog.component.html', + styleUrls: ['./dialog.component.scss'], + encapsulation: ViewEncapsulation.None, + viewProviders: [DialogSubjectService] +}) +export class DialogComponent implements OnInit, AfterViewInit, OnDestroy { + // 显示状态 + _visible: boolean; + // 动画状态 + _animationStatus: string; + // backdrop样式 + _backdropClassMap: object; + // dialog样式 + _dialogClassMap: object; + // 插入组件的容器 + _dialogComponent: ComponentFactory; + // 子预加载数据 + _componentResolve: object; + // 载入子组件 + @ViewChild('dialogComponent', { read: ViewContainerRef }) dialogComponentEl: ViewContainerRef; + // 获取dialog容器 + @ViewChild('dialogContainer') dialogEl: ElementRef; + @Input() + set dialogComponent(value: ComponentFactory) { + // 如果容器对象已存在,则直接渲染,如果不存在,则设置到_bodyComponent,在ngAfterViewInit中执行 + if (this.dialogComponentEl) { + const compRef: ComponentRef = this.dialogComponentEl.createComponent(value, null, this._vcr.injector); + Object.assign(compRef.instance, this._componentResolve); + } + } + + // 显示状态 + @Input() + get isOpen(): boolean { + return this._visible; + } + + set isOpen(value: boolean) { + if (this._visible === value) { + return; + } + if (value) { + this._anmiateFade('enter'); + this.subject.next('onShow'); + } else { + this._anmiateFade('leave'); + this.subject.next('onHide'); + } + this._visible = value; + // this.dialogOnChange.emit(this._visible); + // 设置全局的overflow样式 + this._setDocumentOverflowHidden(value); + } + + // 配置子组件输入数据 + @Input() + set dialogResolve(value: Object) { + this._componentResolve = value; + } + + constructor(private subject: DialogSubjectService, private _vcr: ViewContainerRef, @Inject(DOCUMENT) private doc: Document) { + this.subject.dialogId = (new Date).getTime() + ''; + } + + ngOnInit() { + } + + ngAfterViewInit() { + if (this._dialogComponent) { + const compRef: ComponentRef = this.dialogComponentEl.createComponent(this._dialogComponent, null, this._vcr.injector); + Object.assign(compRef.instance, this._componentResolve); + } + } + + ngOnDestroy() { + if (this.isOpen) { + this._setDocumentOverflowHidden(false); + } + this.subject.next('onDestroy'); + this.subject.unsubscribe(); + this.subject = null; + } + + /** + * 点击遮罩关闭 + * @param event + */ + closeBackdrop(event): void { + if (event.target.getAttribute('role') === 'dialog') { + this.subject.next('onCancel'); + } + } + + /** + * 入场出场动画 + * @param status + */ + private _anmiateFade(status: string) { + this._animationStatus = status; + this._setClassMap(); + setTimeout(_ => { + this._animationStatus = ''; + this._setClassMap(); + this.subject.next(status === 'enter' ? 'onShown' : 'onHidden'); + // modal打开后,默认焦点设置到modal上 + if (status === 'enter') { + this.dialogEl.nativeElement.focus(); + } + }, 200); + } + + /** + * 设置样式 + */ + private _setClassMap(): void { + this._backdropClassMap = { + 'dialog-backdrop': true, + 'dialog-backdrop-hidden': !this._visible && !this._animationStatus, + 'fade-enter': this._animationStatus === 'enter', + 'fade-enter-active': this._animationStatus === 'enter', + 'fade-leave': this._animationStatus === 'leave', + 'fade-leave-active': this._animationStatus === 'leave' + }; + + this._dialogClassMap = { + 'dialog-container': true, + 'zoom-enter': this._animationStatus === 'enter', + 'zoom-enter-active': this._animationStatus === 'enter', + 'zoom-leave': this._animationStatus === 'leave', + 'zoom-leave-active': this._animationStatus === 'leave' + }; + } + + /** + * 设置全局的overflow样式 + * @param status + */ + private _setDocumentOverflowHidden(status: Boolean) { + this.doc.body.style.overflow = status ? 'hidden' : ''; + } + +} diff --git a/src/app/shared/dialog/dialog.module.ts b/src/app/shared/dialog/dialog.module.ts new file mode 100644 index 0000000..baf9c81 --- /dev/null +++ b/src/app/shared/dialog/dialog.module.ts @@ -0,0 +1,39 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { DialogComponent } from './dialog.component'; +import { DialogService } from './dialog.service'; +import { DialogSubjectService } from './dialog-subject.service'; +import { DialogHeaderComponent } from './dialog-header/dialog-header.component'; +import { DialogBodyComponent } from './dialog-body/dialog-body.component'; +import { DialogFooterComponent } from './dialog-footer/dialog-footer.component'; +import { DialogConfirmComponent } from './dialog-confirm/dialog-confirm.component'; +import { DialogCloseDirective } from './dialog-close.directive'; + +@NgModule({ + imports: [ + CommonModule + ], + providers: [ + DialogService, + DialogSubjectService + ], + declarations: [ + DialogComponent, + DialogHeaderComponent, + DialogBodyComponent, + DialogFooterComponent, + DialogCloseDirective, + DialogConfirmComponent + ], + entryComponents: [ + DialogComponent, + DialogConfirmComponent + ], + exports: [ + DialogHeaderComponent, + DialogBodyComponent, + DialogFooterComponent, + DialogCloseDirective + ] +}) +export class DialogModule { } diff --git a/src/app/shared/dialog/dialog.service.spec.ts b/src/app/shared/dialog/dialog.service.spec.ts new file mode 100644 index 0000000..364b39b --- /dev/null +++ b/src/app/shared/dialog/dialog.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed, inject } from '@angular/core/testing'; + +import { DialogService } from './dialog.service'; + +describe('DialogService', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [DialogService] + }); + }); + + it('should be created', inject([DialogService], (service: DialogService) => { + expect(service).toBeTruthy(); + })); +}); diff --git a/src/app/shared/dialog/dialog.service.ts b/src/app/shared/dialog/dialog.service.ts new file mode 100644 index 0000000..696cd6d --- /dev/null +++ b/src/app/shared/dialog/dialog.service.ts @@ -0,0 +1,143 @@ +import { DialogConfig } from './dialog-config.service'; +import { Injectable, ComponentFactory, ComponentFactoryResolver, ApplicationRef, ComponentRef, Type, TemplateRef } from '@angular/core'; +import { DialogComponent } from './dialog.component'; +import { DialogConfirmComponent } from './dialog-confirm/dialog-confirm.component'; +import { DialogSubjectService } from './dialog-subject.service'; +@Injectable() +export class DialogService { + // 默认层级 + private _zIndex = 1040; + _dailogCompFactory: ComponentFactory; + constructor(private _cfr: ComponentFactoryResolver, private _appRef: ApplicationRef) { + this._dailogCompFactory = this._cfr.resolveComponentFactory(DialogComponent); + } + open(dialogRef: Type, config: DialogConfig = {}): DialogSubjectService { + if (!(dialogRef instanceof Type)) { + throw Error('dialogRef is not ComponentRef'); + } + const options: DialogConfig = new DialogConfig(); + config['component'] = dialogRef; + config['zIndex'] = this._zIndex; + this._zIndex += 20; + const props: DialogConfig = this._initConfig(config, options); + // hasBackdrop 如果没有传递,默认为true + if (props['hasBackdrop'] === undefined) { + props['hasBackdrop'] = true; + } + return this._open(props, this._dailogCompFactory); + } + + /** + * 初始化配置参数 + * @param config 传递参数 + * @param options 默认参数 + */ + private _initConfig(config: DialogConfig, options: {}) { + const props = {}; + const optionalParams: string[] = [ + 'resolve', // 将componentParams放在第一位是因为必须在component赋值前进行赋值 + 'visible', + 'component', + 'width', + 'height', + 'zIndex', + 'onConfirm', + 'onCancel', + 'wrapClassName' + ]; + + config = Object.assign(options, config); + optionalParams.forEach(key => { + if (config[key] !== undefined) { + const modalKey = 'dialog' + key.replace(/^\w{1}/, (a) => { + return a.toLocaleUpperCase(); + }); + props[modalKey] = config[key]; + } + }); + props['onConfirm'] = this._getConfirmCb(props['dialogOnConfirm'], false); + props['onCancel'] = this._getConfirmCb(props['dialogOnCancel']); + // 在service模式下,不需要dialogOnConfirm,防止触发this.dialogOnConfirm.emit(e); + delete props['dialogOnConfirm']; + delete props['dialogOnCancel']; + return props; + } + + private _getConfirmCb(fn?: Function, isShowConfirmLoading: boolean = false): Function { + return (_close, _instance) => { + /* if (isShowConfirmLoading) { + _instance.nzConfirmLoading = true; + } */ + if (fn) { + const ret = fn(); + if (!ret) { + _close(); + } else if (ret.then) { + ret.then(_close); + } + } else { + _close(); + } + }; + } + + /** + * 打开方法 + * @param props 参数 + * @param factory 模块 + */ + private _open(props: DialogConfig, factory: ComponentFactory): DialogSubjectService { + // 在body的内部最前插入一个方便进行ApplicationRef.bootstrap + document.body.insertBefore(document.createElement(factory.selector), document.body.firstChild); + // 自定义组件 + let customComponentFactory: ComponentFactory; + let compRef: ComponentRef; + let instance: any; + let subject: any; + + if (props['dialogComponent'] instanceof Type) { + customComponentFactory = this._cfr.resolveComponentFactory(props['dialogComponent']); + // 将编译出来的ngmodule中的用户component的factory作为modal内容存入 + props['dialogComponent'] = customComponentFactory; + } + compRef = this._appRef.bootstrap(factory); + instance = compRef.instance; + subject = instance.subject; + ['onConfirm', 'onCancel'].forEach((eventType: string) => { + subject.on(eventType, () => { + const eventHandler = props[eventType]; + if (eventHandler) { + eventHandler(() => { + instance.isOpen = false; + setTimeout(() => { + compRef.destroy(); + this._zIndex -= 20; + }, 200); + }, instance); + } + }); + }); + Object.assign(instance, props, { + isOpen: true + }); + return subject; + } + + confirm(contentRef: string | TemplateRef, config: DialogConfig = {}): DialogSubjectService { + let intercept: boolean; + if (typeof contentRef === 'string' && contentRef !== '') { + intercept = true; + } + if (contentRef instanceof TemplateRef) { + intercept = true; + } + if (!intercept) { + throw Error('contentRef is not TemplateRef of string'); + } + return this.open(DialogConfirmComponent, Object.assign(config, { + resolve: { + content: contentRef + } + })); + } +} diff --git a/src/app/shared/dialog/index.ts b/src/app/shared/dialog/index.ts new file mode 100644 index 0000000..57d26b5 --- /dev/null +++ b/src/app/shared/dialog/index.ts @@ -0,0 +1,4 @@ +export * from './dialog.module'; +export * from './dialog.service'; +export * from './dialog-subject.service'; +export * from './dialog-config.service';