ngx-modal-dialog
Version:
Dynamic modal dialog for Angular
417 lines (407 loc) • 14.4 kB
JavaScript
import { Component, ElementRef, ComponentFactoryResolver, ViewChild, ViewContainerRef, HostListener, Injectable, Inject, InjectionToken, Optional, SkipSelf, NgModule } from '@angular/core';
import { Subject, from } from 'rxjs';
import { CommonModule } from '@angular/common';
/**
* Modal dialog component
*/
class ModalDialogComponent {
/**
* CTOR
* @param _element
* @param componentFactoryResolver
*/
constructor(_element, componentFactoryResolver) {
this._element = _element;
this.componentFactoryResolver = componentFactoryResolver;
/** Modal dialog style settings */
this.settings = {
overlayClass: 'modal-backdrop fade',
overlayAnimationTriggerClass: 'show',
modalClass: 'modal ngx-modal fade',
modalAnimationTriggerClass: 'show',
modalDialogClass: 'modal-dialog modal-dialog-centered',
contentClass: 'modal-content',
headerClass: 'modal-header',
headerTitleClass: 'modal-title',
closeButtonClass: 'close glyphicon glyphicon-remove',
closeButtonTitle: 'CLOSE',
bodyClass: 'modal-body',
footerClass: 'modal-footer',
alertClass: 'ngx-modal-shake',
alertDuration: 250,
notifyWithAlert: true,
buttonClass: 'btn btn-primary'
};
this.showAlert = false;
this.animateOverlayClass = '';
this.animateModalClass = '';
this.showOverlay = false;
this._inProgress = false;
}
onClick(event) {
if (event.target !== this.dialogElement.nativeElement) {
return;
}
this.close();
}
/**
* Initialize dialog with reference to instance and options
* @param reference
* @param options
*/
dialogInit(reference, options = {}) {
this.reference = reference;
// inject component
if (options.childComponent) {
let factory = this.componentFactoryResolver.resolveComponentFactory(options.childComponent);
let componentRef = this.dynamicComponentTarget.createComponent(factory);
this._childInstance = componentRef.instance;
this._closeDialog$ = new Subject();
this._closeDialog$.subscribe(() => {
this._finalizeAndDestroy();
});
options.closeDialogSubject = this._closeDialog$;
this._childInstance['dialogInit'](componentRef, options);
document.activeElement != null ?
document.activeElement.blur() :
document.body.blur();
}
// set options
this._setOptions(options);
}
ngOnInit() {
// a trick to defer css animations
setTimeout(() => {
this.animateOverlayClass = this.settings.overlayAnimationTriggerClass;
this.animateModalClass = this.settings.modalAnimationTriggerClass;
}, 0);
}
/**
* Cleanup on destroy
*/
ngOnDestroy() {
// run animations
this.animateOverlayClass = '';
this.animateModalClass = '';
// cleanup listeners
if (this._alertTimeout) {
clearTimeout(this._alertTimeout);
this._alertTimeout = null;
}
if (this._closeDialog$) {
this._closeDialog$.unsubscribe();
}
}
/**
* Run action defined on action button
* @param action
*/
doAction(action) {
// disable multi clicks
if (this._inProgress) {
return;
}
this._inProgress = true;
this._closeIfSuccessful(action);
}
/**
* Method to run on close
* if action buttons are defined, it will not close
*/
close() {
if (this._inProgress) {
return;
}
if (this.actionButtons && this.actionButtons.length) {
return;
}
this._inProgress = true;
if (this.onClose) {
this._closeIfSuccessful(this.onClose);
return;
}
this._finalizeAndDestroy();
}
/**
* Pass options from dialog setup to component
* @param {IModalDialogOptions} options?
*/
_setOptions(options) {
if (options.onClose && options.actionButtons && options.actionButtons.length) {
throw new Error(`OnClose callback and ActionButtons are not allowed to be defined on the same dialog.`);
}
// set references
this.title = (options && options.title) || '';
this.onClose = (options && options.onClose) || null;
this.actionButtons = (this._childInstance && this._childInstance['actionButtons']) ||
(options && options.actionButtons) || null;
if (options && options.settings) {
Object.assign(this.settings, options.settings);
}
}
/**
* Close if successful
* @param callback
*/
_closeIfSuccessful(callback) {
if (!callback) {
return this._finalizeAndDestroy();
}
let response = callback();
if (typeof response === 'boolean') {
if (response) {
return this._finalizeAndDestroy();
}
return this._triggerAlert();
}
if (this.isPromise(response)) {
response = from(response);
}
if (this.isObservable(response)) {
response.subscribe(() => {
this._finalizeAndDestroy();
}, () => {
this._triggerAlert();
});
}
else {
this._inProgress = false;
}
}
_finalizeAndDestroy() {
this._inProgress = false;
this.reference.destroy();
}
_triggerAlert() {
if (this.settings.notifyWithAlert) {
this.showAlert = true;
this._alertTimeout = window.setTimeout(() => {
this.showAlert = false;
this._inProgress = false;
clearTimeout(this._alertTimeout);
this._alertTimeout = null;
}, this.settings.alertDuration);
}
}
isPromise(value) {
return value && typeof value.subscribe !== 'function' && typeof value.then === 'function';
}
isObservable(value) {
return value && typeof value.subscribe === 'function';
}
}
ModalDialogComponent.decorators = [
{ type: Component, args: [{
selector: 'modal-dialog',
template: `
<div *ngIf="settings.overlayClass && showOverlay" [ngClass]="[settings.overlayClass, animateOverlayClass]"></div>
<div [ngClass]="[settings.modalClass, animateModalClass]" #dialog>
<div [ngClass]="settings.modalDialogClass">
<div [ngClass]="[ showAlert ? settings.alertClass : '', settings.contentClass]">
<div [ngClass]="settings.headerClass">
<h4 [ngClass]="settings.headerTitleClass">{{title}}</h4>
<button (click)="close()" *ngIf="!actionButtons || !actionButtons.length" type="button"
[title]="settings.closeButtonTitle"
[ngClass]="settings.closeButtonClass">
</button>
</div>
<div [ngClass]="settings.bodyClass">
<i #modalDialogBody></i>
</div>
<div [ngClass]="settings.footerClass" *ngIf="actionButtons && actionButtons.length">
<button *ngFor="let button of actionButtons" (click)="doAction(button.onAction)"
[ngClass]="button.buttonClass || settings.buttonClass">{{button.text}}
</button>
</div>
</div>
</div>
</div>
`,
styles: [`
@-moz-keyframes shake {
from, to { transform: translate3d(0, 0, 0); }
10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); }
20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); }
}
@-webkit-keyframes shake {
from, to { transform: translate3d(0, 0, 0); }
10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); }
20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); }
}
shake {
from, to { transform: translate3d(0, 0, 0); }
10%, 30%, 50%, 70%, 90% { transform: translate3d(-2rem, 0, 0); }
20%, 40%, 60%, 80% { transform: translate3d(2rem, 0, 0); }
}
.ngx-modal {
display: flex;
}
.ngx-modal-shake {
backface-visibility: hidden;
-webkit-animation-duration: 0.5s;
-moz-animation-duration: 0.5s;
animation-duration: 0.5s;
-webkit-animation-fill-mode: both;
-moz-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation-iteration-count: infinite;
-moz-animation-iteration-count: infinite;
animation-iteration-count: infinite;
-webkit-animation-name: shake;
-moz-animation-name: shake;
animation-name: shake;
}
`]
},] }
];
ModalDialogComponent.ctorParameters = () => [
{ type: ElementRef },
{ type: ComponentFactoryResolver }
];
ModalDialogComponent.propDecorators = {
dynamicComponentTarget: [{ type: ViewChild, args: ['modalDialogBody', { read: ViewContainerRef, static: true },] }],
dialogElement: [{ type: ViewChild, args: ['dialog',] }],
onClick: [{ type: HostListener, args: ['click', ['$event'],] }]
};
class ModalDialogInstanceService {
constructor() {
/**
* Used to make sure there is exactly one instance of Modal Dialog
*/
this.componentRefs = [];
}
/**
* Closes existing modal dialog
*/
closeAnyExistingModalDialog() {
while (this.componentRefs.length) {
this.componentRefs[this.componentRefs.length - 1].destroy();
}
}
/**
* Save component ref for future comparison
* @param componentRef
*/
saveExistingModalDialog(componentRef) {
// remove overlay from top element
this.setOverlayForTopDialog(false);
// add new component
this.componentRefs = [...this.componentRefs, componentRef];
componentRef.instance.showOverlay = true;
componentRef.onDestroy(() => {
this.componentRefs.pop();
this.setOverlayForTopDialog(true);
});
}
setOverlayForTopDialog(value) {
if (this.componentRefs.length) {
this.componentRefs[this.componentRefs.length - 1].instance.showOverlay = value;
}
}
}
ModalDialogInstanceService.decorators = [
{ type: Injectable }
];
class ModalDialogService {
/**
* CTOR
* @param componentFactoryResolver
* @param modalDialogInstanceService
*/
constructor(componentFactoryResolver, modalDialogInstanceService) {
this.componentFactoryResolver = componentFactoryResolver;
this.modalDialogInstanceService = modalDialogInstanceService;
}
/**
* Open dialog in given target element with given options
* @param {ViewContainerRef} target
* @param {IModalDialogOptions} options?
*/
openDialog(target, options = {}) {
if (!options.placeOnTop) {
this.modalDialogInstanceService.closeAnyExistingModalDialog();
}
const factory = this.componentFactoryResolver.resolveComponentFactory(ModalDialogComponent);
const componentRef = target.createComponent(factory);
componentRef.instance.dialogInit(componentRef, options);
this.modalDialogInstanceService.saveExistingModalDialog(componentRef);
}
}
ModalDialogService.decorators = [
{ type: Injectable }
];
ModalDialogService.ctorParameters = () => [
{ type: ComponentFactoryResolver, decorators: [{ type: Inject, args: [ComponentFactoryResolver,] }] },
{ type: ModalDialogInstanceService, decorators: [{ type: Inject, args: [ModalDialogInstanceService,] }] }
];
class SimpleModalComponent {
/**
* Initialize dialog with simple HTML content
* @param {ComponentRef<IModalDialog>} reference
* @param {Partial<IModalDialogOptions>} options
*/
dialogInit(reference, options) {
if (!options.data) {
throw new Error(`Data information for simple modal dialog is missing`);
}
this.text = options.data.text;
}
}
SimpleModalComponent.decorators = [
{ type: Component, args: [{
selector: 'simple-modal-dialog',
template: ``,
host: {
'[innerHTML]': 'text'
},
styles: [':host { display: block; }']
},] }
];
// components and directives
/**
* Guard to make sure we have single initialization of forRoot
* @type {InjectionToken<ModalDialogModule>}
*/
const MODAL_DIALOG_FORROOT_GUARD = new InjectionToken('MODAL_DIALOG_FORROOT_GUARD');
class ModalDialogModule {
static forRoot() {
return {
ngModule: ModalDialogModule,
providers: [
{
provide: MODAL_DIALOG_FORROOT_GUARD,
useFactory: provideForRootGuard,
deps: [ModalDialogModule, new Optional(), new SkipSelf()]
},
ModalDialogInstanceService
]
};
}
}
ModalDialogModule.decorators = [
{ type: NgModule, args: [{
imports: [CommonModule],
declarations: [ModalDialogComponent, SimpleModalComponent],
entryComponents: [ModalDialogComponent, SimpleModalComponent],
exports: [ModalDialogComponent, SimpleModalComponent],
providers: [ModalDialogService, ModalDialogInstanceService]
},] }
];
/**
* @param dialogModule
* @returns {string}
*/
function provideForRootGuard(dialogModule) {
if (dialogModule) {
throw new Error(`ModalDialogModule.forRoot() called twice.`);
}
return 'guarded';
}
/*
* Public API Surface of ngx-modal-dialog
*/
/**
* Generated bundle index. Do not edit.
*/
export { MODAL_DIALOG_FORROOT_GUARD, ModalDialogComponent, ModalDialogInstanceService, ModalDialogModule, ModalDialogService, SimpleModalComponent, provideForRootGuard };
//# sourceMappingURL=ngx-modal-dialog.js.map