UNPKG

ngx-light-modal

Version:

Lightweight and dependency-free Angular modal package using standalone components. Supports stacking, backdrops, dynamic injection, and custom content components.

244 lines (236 loc) 13.5 kB
import { NgClass } from '@angular/common'; import * as i0 from '@angular/core'; import { inputBinding, createComponent, Injectable, inject, input, ViewContainerRef, HostListener, ViewChild, Component } from '@angular/core'; import { Subject, filter as filter$1 } from 'rxjs'; import { filter } from 'rxjs/operators'; var CloseState; (function (CloseState) { CloseState["EXITED"] = "EXITED"; CloseState["BACKDROP_DISMISS"] = "BACKDROP_DISMISS"; CloseState["CLOSE_BUTTON"] = "CLOSE_BUTTON"; CloseState["ESCAPE"] = "ESCAPE"; CloseState["SUBMITTED"] = "SUBMITTED"; CloseState["CANCELLED"] = "CANCELLED"; })(CloseState || (CloseState = {})); class ModalRef { componentInstance; id; _afterClosed = new Subject(); _beforeClosed = new Subject(); _afterOpened = new Subject(); afterClosed$ = this._afterClosed.asObservable(); beforeClosed$ = this._beforeClosed.asObservable(); afterOpened$ = this._afterOpened.asObservable(); contentRef; constructor(componentInstance, id) { this.componentInstance = componentInstance; this.id = id; } onSubmitted() { return this.getStateObservable(CloseState.SUBMITTED); } onEscaped() { return this.getStateObservable(CloseState.ESCAPE); } onBackdropDismiss() { return this.getStateObservable(CloseState.BACKDROP_DISMISS); } onStateClose(state) { return this.getStateObservable(state); } onCancelled() { return this.getStateObservable(CloseState.CANCELLED); } onUserDismissed() { return this.afterClosed$.pipe(filter((result) => [CloseState.BACKDROP_DISMISS, CloseState.ESCAPE, CloseState.CLOSE_BUTTON].includes(result.state))); } close(result, state = CloseState.EXITED) { this._beforeClosed.next(); this._beforeClosed.complete(); this._afterClosed.next({ state, data: result }); this._afterClosed.complete(); } notifyAfterOpened() { if (this._afterOpened.closed) return; this._afterOpened.next(); this._afterOpened.complete(); } getStateObservable(state) { return this.afterClosed$.pipe(filter((result) => result.state === state)); } } class ModalService { injector; appRef; environmentInjector; modalStack = []; contentComponentRegistry = new WeakMap(); constructor(injector, appRef, environmentInjector) { this.injector = injector; this.appRef = appRef; this.environmentInjector = environmentInjector; } open(component, data, config = {}) { const zIndex = 1000 + (this.modalStack.length + 1) * 100; const bindings = [ inputBinding('showBackdrop', () => config.showBackdrop ?? true), inputBinding('dismissOnBackdropClick', () => config.dismissOnBackdropClick ?? true), inputBinding('showCloseButton', () => config.showCloseButton ?? true), inputBinding('escapeKeyClose', () => config.escapeKeyClose ?? true), inputBinding('modalClass', () => config.modalClass || ''), inputBinding('backdropClass', () => config.backdropClass || ''), inputBinding('zIndex', () => zIndex), ]; const modalContainerRef = createComponent(ModalContainerComponent, { environmentInjector: this.environmentInjector, elementInjector: this.injector, bindings, }); const contentRef = modalContainerRef.instance.createComponent(component); if (contentRef.instance && data) { Object.assign(contentRef.instance, data); } const modalRef = new ModalRef(contentRef.instance, this.generateModalId()); this.modalStack.push({ containerRef: modalContainerRef, modalRef }); this.contentComponentRegistry.set(contentRef.instance, modalRef); // Configure modal container Object.assign(modalContainerRef.instance, { modalRef: modalRef, }); document.body.appendChild(modalContainerRef.location.nativeElement); this.appRef.attachView(modalContainerRef.hostView); modalContainerRef.instance.focus(); modalRef.afterClosed$.subscribe((result) => { this.cleanupModal(modalRef.id); this.closeSource.next(result); }); Promise.resolve().then(() => modalRef.notifyAfterOpened()); return modalRef; } generateModalId() { return Date.now() + Math.floor(Math.random() * 1000); } getModalRef(contentComponent) { return this.contentComponentRegistry.get(contentComponent); } cleanupModal(modalId) { const index = this.modalStack.findIndex((m) => m.modalRef.id === modalId); if (index === -1) return; const [modal] = this.modalStack.splice(index, 1); this.contentComponentRegistry.delete(modal.modalRef.componentInstance); // Destroy content component if (modal.modalRef.contentRef) { modal.modalRef.contentRef.destroy(); } // Destroy container this.appRef.detachView(modal.containerRef.hostView); modal.containerRef.destroy(); // Update z-index and top status for remaining modals this.modalStack.forEach((m) => { m.containerRef.changeDetectorRef.detectChanges(); }); // Focus new top modal if (this.modalStack.length > 0) { const topModal = this.modalStack[this.modalStack.length - 1]; topModal.containerRef.instance.focus(); } } getTopModal() { return this.modalStack[this.modalStack.length - 1]?.modalRef; } isTopModal(modalRef) { const top = this.getTopModal(); console.log(modalRef); return top?.id === modalRef.id; } closeSource = new Subject(); // Listen to all closes onClose$() { return this.closeSource.asObservable(); } // Listen to specific states onStateClose$(state) { return this.closeSource.pipe(filter$1((result) => result.state === state)); } // Helper methods for common states onSubmitted$() { return this.onStateClose$(CloseState.SUBMITTED); } onDismissed$() { return this.onStateClose$(CloseState.BACKDROP_DISMISS); } onEscaped$() { return this.onStateClose$(CloseState.ESCAPE); } notifyClose(result) { this.closeSource.next(result); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, deps: [{ token: i0.Injector }, { token: i0.ApplicationRef }, { token: i0.EnvironmentInjector }], target: i0.ɵɵFactoryTarget.Injectable }); static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, providedIn: 'root' }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalService, decorators: [{ type: Injectable, args: [{ providedIn: 'root' }] }], ctorParameters: () => [{ type: i0.Injector }, { type: i0.ApplicationRef }, { type: i0.EnvironmentInjector }] }); class ModalContainerComponent { modalContent; modal; modalRef; contentComponent; modalService = inject(ModalService); showBackdrop = input.required(); dismissOnBackdropClick = input.required(); showCloseButton = input.required(); escapeKeyClose = input.required(); modalClass = input.required(); backdropClass = input.required(); zIndex = input.required(); focus() { this.modal.nativeElement.focus(); } handleEscape(event) { if (this.escapeKeyClose() && this.modalService.isTopModal(this.modalRef)) { this.close(CloseState.ESCAPE); } } createComponent(component) { this.modalContent.clear(); return this.modalContent.createComponent(component); } onBackdropClick(event) { if (this.dismissOnBackdropClick() && event.target === this.modal.nativeElement) { this.close(CloseState.BACKDROP_DISMISS); } } closeWithButton() { this.close(CloseState.CLOSE_BUTTON); } close(state) { this.modalRef?.close(undefined, state); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalContainerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.0", type: ModalContainerComponent, isStandalone: true, selector: "app-modal-container", inputs: { showBackdrop: { classPropertyName: "showBackdrop", publicName: "showBackdrop", isSignal: true, isRequired: true, transformFunction: null }, dismissOnBackdropClick: { classPropertyName: "dismissOnBackdropClick", publicName: "dismissOnBackdropClick", isSignal: true, isRequired: true, transformFunction: null }, showCloseButton: { classPropertyName: "showCloseButton", publicName: "showCloseButton", isSignal: true, isRequired: true, transformFunction: null }, escapeKeyClose: { classPropertyName: "escapeKeyClose", publicName: "escapeKeyClose", isSignal: true, isRequired: true, transformFunction: null }, modalClass: { classPropertyName: "modalClass", publicName: "modalClass", isSignal: true, isRequired: true, transformFunction: null }, backdropClass: { classPropertyName: "backdropClass", publicName: "backdropClass", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: true, transformFunction: null } }, host: { listeners: { "document:keydown.escape": "handleEscape($event)" } }, viewQueries: [{ propertyName: "modalContent", first: true, predicate: ["modalContent"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "modal", first: true, predicate: ["modal"], descendants: true, static: true }], ngImport: i0, template: " <div\r\n #modal\r\n [ngClass]=\"{ 'ngx-backdrop-active': showBackdrop() }\"\r\n [class]=\"'ngx-modal-backdrop ' + backdropClass()\"\r\n (click)=\"onBackdropClick($event)\"\r\n tabindex=\"-1\"\r\n [style.zIndex]=\"zIndex()\"\r\n >\r\n <div class=\"ngx-modal-content\" [ngClass]=\"modalClass()\" [style.zIndex]=\"zIndex() + 1\">\r\n @if (showCloseButton()) {\r\n <button class=\"ngx-close-button\" (click)=\"closeWithButton()\">&times;</button>\r\n }\r\n\r\n <ng-container #modalContent></ng-container>\r\n </div>\r\n </div>\r\n", styles: [".ngx-modal-backdrop{position:fixed;top:0;left:0;width:100dvw;height:100dvh;display:flex;justify-content:center;align-items:center}.ngx-modal-backdrop.ngx-backdrop-active{background-color:#00000080}:host{position:fixed;top:0;left:0;width:100dvw;height:100dvh;min-height:100vh}.ngx-modal-backdrop:focus{outline:none}.ngx-modal-content{background:#fff;padding:20px;border-radius:4px;position:relative;max-width:90dvw;max-height:90dvh;overflow:auto}.ngx-close-button{position:absolute;top:10px;right:10px;background:none;border:none;font-size:1.2rem;cursor:pointer;z-index:100}\n"], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.0", ngImport: i0, type: ModalContainerComponent, decorators: [{ type: Component, args: [{ selector: 'app-modal-container', imports: [NgClass], standalone: true, template: " <div\r\n #modal\r\n [ngClass]=\"{ 'ngx-backdrop-active': showBackdrop() }\"\r\n [class]=\"'ngx-modal-backdrop ' + backdropClass()\"\r\n (click)=\"onBackdropClick($event)\"\r\n tabindex=\"-1\"\r\n [style.zIndex]=\"zIndex()\"\r\n >\r\n <div class=\"ngx-modal-content\" [ngClass]=\"modalClass()\" [style.zIndex]=\"zIndex() + 1\">\r\n @if (showCloseButton()) {\r\n <button class=\"ngx-close-button\" (click)=\"closeWithButton()\">&times;</button>\r\n }\r\n\r\n <ng-container #modalContent></ng-container>\r\n </div>\r\n </div>\r\n", styles: [".ngx-modal-backdrop{position:fixed;top:0;left:0;width:100dvw;height:100dvh;display:flex;justify-content:center;align-items:center}.ngx-modal-backdrop.ngx-backdrop-active{background-color:#00000080}:host{position:fixed;top:0;left:0;width:100dvw;height:100dvh;min-height:100vh}.ngx-modal-backdrop:focus{outline:none}.ngx-modal-content{background:#fff;padding:20px;border-radius:4px;position:relative;max-width:90dvw;max-height:90dvh;overflow:auto}.ngx-close-button{position:absolute;top:10px;right:10px;background:none;border:none;font-size:1.2rem;cursor:pointer;z-index:100}\n"] }] }], propDecorators: { modalContent: [{ type: ViewChild, args: ['modalContent', { read: ViewContainerRef, static: true }] }], modal: [{ type: ViewChild, args: ['modal', { static: true }] }], handleEscape: [{ type: HostListener, args: ['document:keydown.escape', ['$event']] }] } }); /* * Public API Surface of ngx-modal */ /** * Generated bundle index. Do not edit. */ export { CloseState, ModalContainerComponent, ModalRef, ModalService }; //# sourceMappingURL=ngx-light-modal.mjs.map