ngx-modal-ease
Version:
ngx-modal-ease is a versatile Angular library providing a lightweight, simple, and performant modal. This library supports data communication between components, opening of multiple modals, custom animations, and a range of customisable options.
268 lines (262 loc) • 14.4 kB
JavaScript
import * as i0 from '@angular/core';
import { ViewChild, ViewEncapsulation, ChangeDetectionStrategy, Component, createComponent, PLATFORM_ID, Inject, Injectable } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
class ModalComponent {
constructor(modalService, element) {
this.modalService = modalService;
this.element = element;
this.modalLeaveAnimation = '';
this.overlayLeaveAnimation = '';
this.overlayClosed = false;
this.modalClosed = false;
this.layerLevel = 0;
/**
* Multiple modals might register multiple event listener, hence the 'layerLevel' variable and two times the condition check for the escape option.
* Arrow function to respect the this instance.
*/
this.handleEscape = (event) => {
if (event.key === 'Escape') {
if (this.options?.actions?.escape === false)
return;
if (this.layerLevel === this.modalService.layerLevel) {
this.modalService.closedOnClickOrEscape = true;
this.modalService.close();
}
}
};
}
ngOnInit() {
this.options = this.modalService.options;
this.modalService.modalInstances.push(this);
this.modalService.layerLevel += 1;
this.layerLevel = this.modalService.layerLevel;
if (this.options?.actions?.escape === false)
return;
document.addEventListener('keydown', this.handleEscape);
}
ngAfterViewInit() {
this.addOptionsAndAnimations();
}
onClose() {
if (this.options?.actions?.click === false)
return;
this.modalService.closedOnClickOrEscape = true;
this.modalService.close();
}
/**
* Add options and animations
* Apply user style and animations, listen to animation ends. Apply z-indexes on overlay and modal, with 1000 as incremental value.
*/
addOptionsAndAnimations() {
this.modal.nativeElement.style.width = this.options?.size?.width || '';
this.modal.nativeElement.style.maxWidth =
this.options?.size?.maxWidth || '';
this.modal.nativeElement.style.height = this.options?.size?.height || '';
this.modal.nativeElement.style.maxHeight =
this.options?.size?.maxHeight || '';
this.modal.nativeElement.style.padding =
this.options?.size?.padding || '0.5rem';
const overlayZIndex = 1000 * this.modalService.modalInstances.length;
this.overlay.nativeElement.style.zIndex = `${overlayZIndex}`;
this.modal.nativeElement.style.zIndex = `${overlayZIndex + 1000}`;
this.modalLeaveAnimation = this.options?.modal?.leave || '';
this.overlayLeaveAnimation = this.options?.overlay?.leave || '';
this.modal.nativeElement.style.animation = this.options?.modal?.enter || '';
this.modal.nativeElement.style.top = this.options?.modal?.top || '50%';
this.modal.nativeElement.style.left = this.options?.modal?.left || '50%';
this.overlay.nativeElement.style.animation =
this.options?.overlay?.enter || '';
this.overlay.nativeElement.style.backgroundColor =
this.options?.overlay?.backgroundColor || '';
}
removeElementIfNotAnimated(element, animation) {
if (!animation) {
element.remove();
if (element.classList.contains('ngx-modal')) {
this.modalClosed = true;
}
else {
this.overlayClosed = true;
}
}
}
/**
* Clean the DOM
* Apply the leaving animations and clean the DOM. Three different use cases.
* Last In First Out
*/
close(userComponent) {
this.modalService.layerLevel -= 1;
this.modal.nativeElement.style.animation = this.modalLeaveAnimation;
this.overlay.nativeElement.style.animation = this.overlayLeaveAnimation;
document.removeEventListener('keydown', this.handleEscape);
// First: no animations on both elements
if (!this.modalLeaveAnimation && !this.overlayLeaveAnimation) {
this.element.nativeElement.remove();
userComponent.destroy();
return;
}
// Second: 1/2 animated, remove directly element if not animated
this.removeElementIfNotAnimated(this.modal.nativeElement, this.modalLeaveAnimation);
this.removeElementIfNotAnimated(this.overlay?.nativeElement, this.overlayLeaveAnimation);
// Third: Both animated with differents animation time, remove modal component as soon as last one ends
this.modal.nativeElement.addEventListener('animationend', () => {
this.modal.nativeElement.remove();
this.modalClosed = true;
this.removeModalComponent(userComponent);
});
this.overlay.nativeElement.addEventListener('animationend', () => {
this.overlay.nativeElement.remove();
this.overlayClosed = true;
this.removeModalComponent(userComponent);
});
}
/**
* Remove modal when both animations come to an end.
*/
removeModalComponent(userComponent) {
if (this.modalClosed && this.overlayClosed) {
this.element.nativeElement.remove();
userComponent.destroy();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ModalComponent, deps: [{ token: ModalService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.1.0", type: ModalComponent, isStandalone: true, selector: "app-modal", viewQueries: [{ propertyName: "modal", first: true, predicate: ["modal"], descendants: true }, { propertyName: "overlay", first: true, predicate: ["overlay"], descendants: true }], ngImport: i0, template: "<div class=\"modal-container\">\n <section class=\"ngx-modal\" #modal>\n <ng-content></ng-content>\n </section>\n\n <div class=\"ngx-overlay\" #overlay (click)=\"onClose()\"></div>\n</div>\n", styles: [".ngx-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#0006;z-index:1000}.ngx-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:.5rem;max-width:100%;z-index:2000}@keyframes enter-going-down{0%{transform:translate(-50%,-60%)}to{transform:translate(-50%,-50%)}}@keyframes enter-scaling{0%{transform:scale(.8) translate(-50%,-50%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}@keyframes enter-scale-down{0%{transform:scale(1.5) translate(-50%,-60%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes bounce-in{0%{transform:translate(-50%,-85%)}20%,80%,to{transform:translate(-50%,-50%)}60%{transform:translate(-50%,-65%)}90%{transform:translate(-50%,-53%)}}@keyframes scale-rotate{30%{transform:scale(1.05) translate(-50%,-50%);transform-origin:left}40%,60%{transform:rotate(-3deg) scale(1.05) translate(-50%,-50%);transform-origin:left}50%{transform:rotate(3deg) scale(1.05) translate(-50%,-50%);transform-origin:left}70%{transform:rotate(0) scale(1.05) translate(-50%,-50%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ModalComponent, decorators: [{
type: Component,
args: [{ selector: 'app-modal', imports: [], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"modal-container\">\n <section class=\"ngx-modal\" #modal>\n <ng-content></ng-content>\n </section>\n\n <div class=\"ngx-overlay\" #overlay (click)=\"onClose()\"></div>\n</div>\n", styles: [".ngx-overlay{position:fixed;top:0;left:0;width:100%;height:100%;background-color:#0006;z-index:1000}.ngx-modal{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);padding:.5rem;max-width:100%;z-index:2000}@keyframes enter-going-down{0%{transform:translate(-50%,-60%)}to{transform:translate(-50%,-50%)}}@keyframes enter-scaling{0%{transform:scale(.8) translate(-50%,-50%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}@keyframes enter-scale-down{0%{transform:scale(1.5) translate(-50%,-60%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-out{0%{opacity:1}to{opacity:0}}@keyframes bounce-in{0%{transform:translate(-50%,-85%)}20%,80%,to{transform:translate(-50%,-50%)}60%{transform:translate(-50%,-65%)}90%{transform:translate(-50%,-53%)}}@keyframes scale-rotate{30%{transform:scale(1.05) translate(-50%,-50%);transform-origin:left}40%,60%{transform:rotate(-3deg) scale(1.05) translate(-50%,-50%);transform-origin:left}50%{transform:rotate(3deg) scale(1.05) translate(-50%,-50%);transform-origin:left}70%{transform:rotate(0) scale(1.05) translate(-50%,-50%);transform-origin:left}to{transform:scale(1) translate(-50%,-50%);transform-origin:left}}\n"] }]
}], ctorParameters: () => [{ type: ModalService }, { type: i0.ElementRef }], propDecorators: { modal: [{
type: ViewChild,
args: ['modal']
}], overlay: [{
type: ViewChild,
args: ['overlay']
}] } });
class ModalService {
constructor(appRef, injector, platformId) {
this.appRef = appRef;
this.injector = injector;
/**
* Internal use only.
*/
this.modalInstances = [];
/**
* Internal use only.
*/
this.layerLevel = 0;
/**
* Internal use only.
*/
this.closedOnClickOrEscape = false;
this.isBrowser = true;
this.promiseContainer = [];
this.isBrowser = isPlatformBrowser(platformId);
}
/**
* Opens a custom component within a modal.
* @param componentToCreate The custom component to display within the modal.
* @param options Additional options for configuring the modal appearance and animations.
* @returns A Promise that will emit custom data on closing the modal.
* ```
* this.modalService.open(ModalContentComponent, {
* modal: {
* enter: 'enter-scale-down 0.1s ease-out',
* leave: 'fade-out 0.5s',
* },
* overlay: {
* leave: 'fade-out 0.3s',
* },
* data: {
* type: 'Angular modal library',
* },
* })
* .then((dataFromComponent) => {
* ...
* });
* ```
*/
open(componentToCreate, options) {
this.options = options;
this.openComponent(componentToCreate, options);
return new Promise((resolve) => {
if (!this.isBrowser)
return;
this.promiseContainer.push({
resolve,
userComponent: this.userComponent,
libraryComponent: this.libraryComponent,
});
});
}
openComponent(componentToCreate, options) {
if (!this.isBrowser)
return;
this.userComponent = createComponent(componentToCreate, {
environmentInjector: this.injector,
elementInjector: options?.injector,
});
this.libraryComponent = createComponent(ModalComponent, {
environmentInjector: this.injector,
projectableNodes: [[this.userComponent.location.nativeElement]],
});
this.instantiateProps(options?.data);
this.appRef.attachView(this.userComponent.hostView);
this.appRef.attachView(this.libraryComponent.hostView);
document.body.appendChild(this.libraryComponent.location.nativeElement);
}
/**
* Set user provided data into the component instance.
*/
instantiateProps(data = {}) {
for (const key of Object.keys(data)) {
this.userComponent.instance[key] = data[key];
}
}
/**
* Close the current modal.
* @param data The optional data to emit on closing the modal (communication from modal to caller).
*/
close(data) {
if (this.promiseContainer.length === 0)
return;
const { userComponent, libraryComponent, resolve } = this.promiseContainer.pop();
this.modalInstances.pop()?.close(userComponent);
const response = {
closedOnClickOrEscape: this.closedOnClickOrEscape,
data,
};
this.closedOnClickOrEscape = false;
this.appRef.detachView(userComponent.hostView);
this.appRef.detachView(libraryComponent.hostView);
return resolve(response);
}
/**
* Close all modal instances.
* Respective animations will be applied.
*/
closeAll() {
for (let i = this.modalInstances.length - 1; i > -1; i--) {
this.close();
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ModalService, deps: [{ token: i0.ApplicationRef }, { token: i0.EnvironmentInjector }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ModalService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ModalService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }, { type: Object, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }] });
/*
* Public API Surface of ngx-modal-ease
*/
/**
* Generated bundle index. Do not edit.
*/
export { ModalService };
//# sourceMappingURL=ngx-modal-ease.mjs.map