UNPKG

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.

259 lines (253 loc) 14.2 kB
import * as i0 from '@angular/core'; import { Component, ChangeDetectionStrategy, ViewEncapsulation, ViewChild, createComponent, PLATFORM_ID, Injectable, Inject } from '@angular/core'; import { CommonModule, 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(contentCp) { 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(); contentCp.contentCpRef.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(contentCp); }); this.overlay.nativeElement.addEventListener('animationend', () => { this.overlay.nativeElement.remove(); this.overlayClosed = true; this.removeModalComponent(contentCp); }); } /** * Remove modal when both animations come to an end. */ removeModalComponent(contentCp) { if (this.modalClosed && this.overlayClosed) { this.element.nativeElement.remove(); contentCp.contentCpRef.destroy(); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.9", ngImport: i0, type: ModalComponent, deps: [{ token: ModalService }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "17.0.9", 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"], dependencies: [{ kind: "ngmodule", type: CommonModule }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.9", ngImport: i0, type: ModalComponent, decorators: [{ type: Component, args: [{ selector: 'app-modal', imports: [CommonModule], standalone: true, 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, contentCpRef: this.newComponent }); }); } openComponent(componentToCreate, options) { if (!this.isBrowser) return; this.newComponent = createComponent(componentToCreate, { environmentInjector: this.injector, elementInjector: options?.injector, }); this.newModalComponent = createComponent(ModalComponent, { environmentInjector: this.injector, projectableNodes: [[this.newComponent.location.nativeElement]], }); this.instantiateProps(options?.data); document.body.appendChild(this.newModalComponent.location.nativeElement); this.appRef.attachView(this.newComponent.hostView); this.appRef.attachView(this.newModalComponent.hostView); } instantiateProps(data = {}) { for (const key of Object.keys(data)) { this.newComponent.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 modalPromise = this.promiseContainer.pop(); this.modalInstances.pop()?.close(modalPromise); const response = { closedOnClickOrEscape: this.closedOnClickOrEscape, data, }; this.closedOnClickOrEscape = false; return modalPromise.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: "17.0.9", 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: "17.0.9", ngImport: i0, type: ModalService, providedIn: 'root' }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.9", 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