ngx-ntk-smart-module
Version:
Ntk Cms Modal
1,465 lines • 57.3 kB
JavaScript
import * as i1 from '@angular/common';
import { isPlatformBrowser, DOCUMENT, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { EventEmitter, PLATFORM_ID, ViewContainerRef, HostListener, ViewChildren, Output, Input, Inject, Component, Injectable, Type, TemplateRef, NgModule } from '@angular/core';
const NtkSmartModalConfig = {
bodyClassOpen: 'dialog-open',
prefixEvent: 'ntk-smart-modal.'
};
class NtkSmartModalComponent {
constructor(_renderer, _changeDetectorRef, componentFactoryResolver, _document, _platformId) {
this._renderer = _renderer;
this._changeDetectorRef = _changeDetectorRef;
this.componentFactoryResolver = componentFactoryResolver;
this._document = _document;
this._platformId = _platformId;
this.closable = true;
this.escapable = true;
this.dismissable = true;
this.identifier = '';
this.customClass = 'nsm-dialog-animation-fade';
this.visible = false;
this.backdrop = true;
this.force = true;
this.hideDelay = 500;
this.autostart = false;
this.target = '';
this.ariaLabel = null;
this.ariaLabelledBy = null;
this.ariaDescribedBy = null;
this.refocus = true;
this.visibleChange = new EventEmitter();
this.onClose = new EventEmitter();
this.onCloseFinished = new EventEmitter();
this.onDismiss = new EventEmitter();
this.onDismissFinished = new EventEmitter();
this.onAnyCloseEvent = new EventEmitter();
this.onAnyCloseEventFinished = new EventEmitter();
this.onOpen = new EventEmitter();
this.onOpenFinished = new EventEmitter();
this.onEscape = new EventEmitter();
this.onDataAdded = new EventEmitter();
this.onDataRemoved = new EventEmitter();
this.layerPosition = 1041;
this.overlayVisible = false;
this.openedClass = false;
this.createFrom = 'html';
}
ngOnInit() {
if (!this.identifier || !this.identifier.length) {
throw new Error('identifier field isn’t set. Please set one before calling <ngx-smart-modal> in a template.');
}
this._sendEvent('create');
}
ngAfterViewInit() {
if (this.contentComponent) {
const factory = this.componentFactoryResolver.resolveComponentFactory(this.contentComponent);
this.createDynamicContent(this.dynamicContentContainer, factory);
this.dynamicContentContainer.changes.subscribe((contentViewContainers) => {
this.createDynamicContent(contentViewContainers, factory);
});
}
}
ngOnDestroy() {
this._sendEvent('delete');
}
/**
* Open the modal instance
*
* @param top open the modal top of all other
* @returns the modal component
*/
open(top) {
this._sendEvent('open', { top: top });
return this;
}
/**
* Close the modal instance
*
* @returns the modal component
*/
close() {
this._sendEvent('close');
return this;
}
/**
* Dismiss the modal instance
*
* @param e the event sent by the browser
* @returns the modal component
*/
dismiss(e) {
if (!this.dismissable || !e.target.classList.contains('overlay')) {
return this;
}
this._sendEvent('dismiss');
return this;
}
/**
* Toggle visibility of the modal instance
*
* @param top open the modal top of all other
* @returns the modal component
*/
toggle(top) {
this._sendEvent('toggle', { top: top });
return this;
}
/**
* Add a custom class to the modal instance
*
* @param className the class to add
* @returns the modal component
*/
addCustomClass(className) {
if (!this.customClass.length) {
this.customClass = className;
}
else {
this.customClass += ' ' + className;
}
return this;
}
/**
* Remove a custom class to the modal instance
*
* @param className the class to remove
* @returns the modal component
*/
removeCustomClass(className) {
if (className) {
this.customClass = this.customClass.replace(className, '').trim();
}
else {
this.customClass = '';
}
return this;
}
/**
* Returns the visibility state of the modal instance
*/
isVisible() {
return this.visible;
}
/**
* Checks if data is attached to the modal instance
*/
hasData() {
return this._data !== undefined;
}
/**
* Attach data to the modal instance
*
* @param data the data to attach
* @param force override potentially attached data
* @returns the modal component
*/
setData(data, force) {
if (!this.hasData() || (this.hasData() && force)) {
this._data = data;
this.onDataAdded.emit(this._data);
this.markForCheck();
}
return this;
}
/**
* Retrieve the data attached to the modal instance
*/
getData() {
return this._data;
}
/**
* Remove the data attached to the modal instance
*
* @returns the modal component
*/
removeData() {
this._data = undefined;
this.onDataRemoved.emit(true);
this.markForCheck();
return this;
}
/**
* Add body class modal opened
*
* @returns the modal component
*/
addBodyClass() {
this._renderer.addClass(this._document.body, NtkSmartModalConfig.bodyClassOpen);
return this;
}
/**
* Add body class modal opened
*
* @returns the modal component
*/
removeBodyClass() {
this._renderer.removeClass(this._document.body, NtkSmartModalConfig.bodyClassOpen);
return this;
}
markForCheck() {
try {
this._changeDetectorRef.detectChanges();
}
catch (e) {
}
this._changeDetectorRef.markForCheck();
}
/**
* Listens for window resize event and recalculates modal instance position if it is element-relative
*/
targetPlacement() {
if (!this.isBrowser || !this.nsmDialog.length || !this.nsmContent.length || !this.nsmOverlay.length || !this.target) {
return false;
}
const targetElement = this._document.querySelector(this.target);
if (!targetElement) {
return false;
}
const targetElementRect = targetElement.getBoundingClientRect();
const bodyRect = this.nsmOverlay.first.nativeElement.getBoundingClientRect();
const nsmContentRect = this.nsmContent.first.nativeElement.getBoundingClientRect();
const nsmDialogRect = this.nsmDialog.first.nativeElement.getBoundingClientRect();
const marginLeft = parseInt(getComputedStyle(this.nsmContent.first.nativeElement).marginLeft, 10);
const marginTop = parseInt(getComputedStyle(this.nsmContent.first.nativeElement).marginTop, 10);
let offsetTop = targetElementRect.top - nsmDialogRect.top - ((nsmContentRect.height - targetElementRect.height) / 2);
let offsetLeft = targetElementRect.left - nsmDialogRect.left - ((nsmContentRect.width - targetElementRect.width) / 2);
if (offsetLeft + nsmDialogRect.left + nsmContentRect.width + (marginLeft * 2) > bodyRect.width) {
offsetLeft = bodyRect.width - (nsmDialogRect.left + nsmContentRect.width) - (marginLeft * 2);
}
else if (offsetLeft + nsmDialogRect.left < 0) {
offsetLeft = -nsmDialogRect.left;
}
if (offsetTop + nsmDialogRect.top + nsmContentRect.height + marginTop > bodyRect.height) {
offsetTop = bodyRect.height - (nsmDialogRect.top + nsmContentRect.height) - marginTop;
}
this._renderer.setStyle(this.nsmContent.first.nativeElement, 'top', (offsetTop < 0 ? 0 : offsetTop) + 'px');
this._renderer.setStyle(this.nsmContent.first.nativeElement, 'left', offsetLeft + 'px');
}
_sendEvent(name, extraData) {
if (!this.isBrowser) {
return false;
}
const data = {
extraData: extraData,
instance: { id: this.identifier, modal: this }
};
const event = new CustomEvent(NtkSmartModalConfig.prefixEvent + name, { detail: data });
return window.dispatchEvent(event);
}
/**
* Is current platform browser
*/
get isBrowser() {
return isPlatformBrowser(this._platformId);
}
/**
* Creates content inside provided ViewContainerRef
*/
createDynamicContent(changes, factory) {
changes.forEach((viewContainerRef) => {
viewContainerRef.clear();
viewContainerRef.createComponent(factory);
this.markForCheck();
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalComponent, deps: [{ token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i0.ComponentFactoryResolver }, { token: DOCUMENT }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NtkSmartModalComponent, isStandalone: false, selector: "ntk-smart-modal", inputs: { closable: "closable", escapable: "escapable", dismissable: "dismissable", identifier: "identifier", customClass: "customClass", visible: "visible", backdrop: "backdrop", force: "force", hideDelay: "hideDelay", autostart: "autostart", target: "target", ariaLabel: "ariaLabel", ariaLabelledBy: "ariaLabelledBy", ariaDescribedBy: "ariaDescribedBy", refocus: "refocus" }, outputs: { visibleChange: "visibleChange", onClose: "onClose", onCloseFinished: "onCloseFinished", onDismiss: "onDismiss", onDismissFinished: "onDismissFinished", onAnyCloseEvent: "onAnyCloseEvent", onAnyCloseEventFinished: "onAnyCloseEventFinished", onOpen: "onOpen", onOpenFinished: "onOpenFinished", onEscape: "onEscape", onDataAdded: "onDataAdded", onDataRemoved: "onDataRemoved" }, host: { listeners: { "window:resize": "targetPlacement()" } }, viewQueries: [{ propertyName: "nsmContent", predicate: ["nsmContent"], descendants: true }, { propertyName: "nsmDialog", predicate: ["nsmDialog"], descendants: true }, { propertyName: "nsmOverlay", predicate: ["nsmOverlay"], descendants: true }, { propertyName: "dynamicContentContainer", predicate: ["dynamicContent"], descendants: true, read: ViewContainerRef }], ngImport: i0, template: `
<div *ngIf="overlayVisible"
[style.z-index]="visible ? layerPosition-1 : -1"
[ngClass]="{'transparent':!backdrop, 'overlay':true, 'nsm-overlay-open':openedClass}"
(click)="dismiss($event)" #nsmOverlay>
<div [style.z-index]="visible ? layerPosition : -1"
[ngClass]="['nsm-dialog', customClass, openedClass ? 'nsm-dialog-open': 'nsm-dialog-close']" #nsmDialog
[attr.aria-hidden]="openedClass ? false : true"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledBy"
[attr.aria-describedby]="ariaDescribedBy">
<div class="nsm-content" #nsmContent>
<div class="nsm-body">
<ng-template #dynamicContent></ng-template>
<ng-content></ng-content>
</div>
<button type="button" *ngIf="closable" (click)="close()" aria-label="Close" class="nsm-dialog-btn-close">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512"
xml:space="preserve" width="16px" height="16px" role="img" aria-labelledby="closeIconTitle closeIconDesc">
<title id="closeIconTitle">Close Icon</title>
<desc id="closeIconDesc">A light-gray close icon used to close the modal</desc>
<g>
<path d="M505.943,6.058c-8.077-8.077-21.172-8.077-29.249,0L6.058,476.693c-8.077,8.077-8.077,21.172,0,29.249 C10.096,509.982,15.39,512,20.683,512c5.293,0,10.586-2.019,14.625-6.059L505.943,35.306 C514.019,27.23,514.019,14.135,505.943,6.058z"
fill="currentColor"/>
</g>
<g>
<path d="M505.942,476.694L35.306,6.059c-8.076-8.077-21.172-8.077-29.248,0c-8.077,8.076-8.077,21.171,0,29.248l470.636,470.636 c4.038,4.039,9.332,6.058,14.625,6.058c5.293,0,10.587-2.019,14.624-6.057C514.018,497.866,514.018,484.771,505.942,476.694z"
fill="currentColor"/>
</g>
</svg>
</button>
</div>
</div>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalComponent, decorators: [{
type: Component,
args: [{
selector: 'ntk-smart-modal',
standalone: false,
template: `
<div *ngIf="overlayVisible"
[style.z-index]="visible ? layerPosition-1 : -1"
[ngClass]="{'transparent':!backdrop, 'overlay':true, 'nsm-overlay-open':openedClass}"
(click)="dismiss($event)" #nsmOverlay>
<div [style.z-index]="visible ? layerPosition : -1"
[ngClass]="['nsm-dialog', customClass, openedClass ? 'nsm-dialog-open': 'nsm-dialog-close']" #nsmDialog
[attr.aria-hidden]="openedClass ? false : true"
[attr.aria-label]="ariaLabel"
[attr.aria-labelledby]="ariaLabelledBy"
[attr.aria-describedby]="ariaDescribedBy">
<div class="nsm-content" #nsmContent>
<div class="nsm-body">
<ng-template #dynamicContent></ng-template>
<ng-content></ng-content>
</div>
<button type="button" *ngIf="closable" (click)="close()" aria-label="Close" class="nsm-dialog-btn-close">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="Layer_1" x="0px" y="0px" viewBox="0 0 512 512"
xml:space="preserve" width="16px" height="16px" role="img" aria-labelledby="closeIconTitle closeIconDesc">
<title id="closeIconTitle">Close Icon</title>
<desc id="closeIconDesc">A light-gray close icon used to close the modal</desc>
<g>
<path d="M505.943,6.058c-8.077-8.077-21.172-8.077-29.249,0L6.058,476.693c-8.077,8.077-8.077,21.172,0,29.249 C10.096,509.982,15.39,512,20.683,512c5.293,0,10.586-2.019,14.625-6.059L505.943,35.306 C514.019,27.23,514.019,14.135,505.943,6.058z"
fill="currentColor"/>
</g>
<g>
<path d="M505.942,476.694L35.306,6.059c-8.076-8.077-21.172-8.077-29.248,0c-8.077,8.076-8.077,21.171,0,29.248l470.636,470.636 c4.038,4.039,9.332,6.058,14.625,6.058c5.293,0,10.587-2.019,14.624-6.057C514.018,497.866,514.018,484.771,505.942,476.694z"
fill="currentColor"/>
</g>
</svg>
</button>
</div>
</div>
</div>
`
}]
}], ctorParameters: () => [{ type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i0.ComponentFactoryResolver }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }], propDecorators: { closable: [{
type: Input
}], escapable: [{
type: Input
}], dismissable: [{
type: Input
}], identifier: [{
type: Input
}], customClass: [{
type: Input
}], visible: [{
type: Input
}], backdrop: [{
type: Input
}], force: [{
type: Input
}], hideDelay: [{
type: Input
}], autostart: [{
type: Input
}], target: [{
type: Input
}], ariaLabel: [{
type: Input
}], ariaLabelledBy: [{
type: Input
}], ariaDescribedBy: [{
type: Input
}], refocus: [{
type: Input
}], visibleChange: [{
type: Output
}], onClose: [{
type: Output
}], onCloseFinished: [{
type: Output
}], onDismiss: [{
type: Output
}], onDismissFinished: [{
type: Output
}], onAnyCloseEvent: [{
type: Output
}], onAnyCloseEventFinished: [{
type: Output
}], onOpen: [{
type: Output
}], onOpenFinished: [{
type: Output
}], onEscape: [{
type: Output
}], onDataAdded: [{
type: Output
}], onDataRemoved: [{
type: Output
}], nsmContent: [{
type: ViewChildren,
args: ['nsmContent']
}], nsmDialog: [{
type: ViewChildren,
args: ['nsmDialog']
}], nsmOverlay: [{
type: ViewChildren,
args: ['nsmOverlay']
}], dynamicContentContainer: [{
type: ViewChildren,
args: ['dynamicContent', { read: ViewContainerRef }]
}], targetPlacement: [{
type: HostListener,
args: ['window:resize']
}] } });
class NtkSmartModalStackService {
constructor() {
this.modalStack = [];
}
/**
* Add a new modal instance. This step is essential and allows to retrieve any modal at any time.
* It stores an object that contains the given modal identifier and the modal itself directly in the `modalStack`.
*
* @param modalInstance The object that contains the given modal identifier and the modal itself.
* @param force Optional parameter that forces the overriding of modal instance if it already exists.
* @returns nothing special.
*/
addModal(modalInstance, force) {
if (force) {
const i = this.modalStack.findIndex((o) => o.id === modalInstance.id);
if (i > -1) {
this.modalStack[i].modal = modalInstance.modal;
}
else {
this.modalStack.push(modalInstance);
}
return;
}
this.modalStack.push(modalInstance);
}
/**
* Retrieve a modal instance by its identifier.
*
* @param id The modal identifier used at creation time.
*/
getModal(id) {
const i = this.modalStack.find((o) => o.id === id);
if (i !== undefined) {
return i.modal;
}
else {
throw new Error(`Cannot find modal with identifier ${id}`);
}
}
/**
* Retrieve all the created modals.
*
* @returns an array that contains all modal instances.
*/
getModalStack() {
return this.modalStack;
}
/**
* Retrieve all the opened modals. It looks for all modal instances with their `visible` property set to `true`.
*
* @returns an array that contains all the opened modals.
*/
getOpenedModals() {
return this.modalStack.filter((o) => o.modal.visible);
}
/**
* Retrieve the opened modal with highest z-index.
*
* @returns the opened modal with highest z-index.
*/
getTopOpenedModal() {
if (!this.getOpenedModals().length) {
throw new Error('No modal is opened');
}
return this.getOpenedModals()
.map((o) => o.modal)
.reduce((highest, item) => item.layerPosition > highest.layerPosition ? item : highest, this.getOpenedModals()[0].modal);
}
/**
* Get the higher `z-index` value between all the modal instances. It iterates over the `ModalStack` array and
* calculates a higher value (it takes the highest index value between all the modal instances and adds 1).
* Use it to make a modal appear foreground.
*
* @returns a higher index from all the existing modal instances.
*/
getHigherIndex() {
return Math.max(...this.modalStack.map((o) => o.modal.layerPosition), 1041) + 1;
}
/**
* It gives the number of modal instances. It's helpful to know if the modal stack is empty or not.
*
* @returns the number of modal instances.
*/
getModalStackCount() {
return this.modalStack.length;
}
/**
* Remove a modal instance from the modal stack.
*
* @param id The modal identifier.
* @returns the removed modal instance.
*/
removeModal(id) {
const i = this.modalStack.findIndex((o) => o.id === id);
if (i > -1) {
this.modalStack.splice(i, 1);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalStackService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalStackService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalStackService, decorators: [{
type: Injectable
}], ctorParameters: () => [] });
class NtkSmartModalService {
constructor(_componentFactoryResolver, _appRef, _injector, _modalStack, applicationRef, _document, _platformId) {
this._componentFactoryResolver = _componentFactoryResolver;
this._appRef = _appRef;
this._injector = _injector;
this._modalStack = _modalStack;
this.applicationRef = applicationRef;
this._document = _document;
this._platformId = _platformId;
/**
* Close the latest opened modal if escape key event is emitted
* @param event The Keyboard Event
*/
this._escapeKeyboardEvent = (event) => {
if (event.key === 'Escape') {
try {
const modal = this.getTopOpenedModal();
if (!modal.escapable) {
return false;
}
modal.onEscape.emit(modal);
this.closeLatestModal();
return true;
}
catch (e) {
return false;
}
}
return false;
};
/**
* While modal is open, the focus stay on it
* @param event The Keyboar dEvent
*/
this._trapFocusModal = (event) => {
if (event.key === 'Tab') {
try {
const modal = this.getTopOpenedModal();
if (!modal.nsmDialog.first.nativeElement.contains(document.activeElement)) {
event.preventDefault();
event.stopPropagation();
modal.nsmDialog.first.nativeElement.focus();
}
return true;
}
catch (e) {
return false;
}
}
return false;
};
this._addEvents();
}
/**
* Add a new modal instance. This step is essential and allows to retrieve any modal at any time.
* It stores an object that contains the given modal identifier and the modal itself directly in the `modalStack`.
*
* @param modalInstance The object that contains the given modal identifier and the modal itself.
* @param force Optional parameter that forces the overriding of modal instance if it already exists.
* @returns nothing special.
*/
addModal(modalInstance, force) {
this._modalStack.addModal(modalInstance, force);
}
/**
* Retrieve a modal instance by its identifier.
*
* @param id The modal identifier used at creation time.
*/
getModal(id) {
return this._modalStack.getModal(id);
}
/**
* Alias of `getModal` to retrieve a modal instance by its identifier.
*
* @param id The modal identifier used at creation time.
*/
get(id) {
return this.getModal(id);
}
/**
* Open a given modal
*
* @param id The modal identifier used at creation time.
* @param force Tell the modal to open top of all other opened modals
*/
open(id, force = false) {
return this._openModal(this.get(id), force);
}
/**
* Close a given modal
*
* @param id The modal identifier used at creation time.
*/
close(id) {
return this._closeModal(this.get(id));
}
/**
* Close all opened modals
*/
closeAll() {
this.getOpenedModals().forEach((instance) => {
this._closeModal(instance.modal);
});
}
/**
* Toggles a given modal
* If the retrieved modal is opened it closes it, else it opens it.
*
* @param id The modal identifier used at creation time.
* @param force Tell the modal to open top of all other opened modals
*/
toggle(id, force = false) {
return this._toggleModal(this.get(id), force);
}
/**
* Retrieve all the created modals.
*
* @returns an array that contains all modal instances.
*/
getModalStack() {
return this._modalStack.getModalStack();
}
/**
* Retrieve all the opened modals. It looks for all modal instances with their `visible` property set to `true`.
*
* @returns an array that contains all the opened modals.
*/
getOpenedModals() {
return this._modalStack.getOpenedModals();
}
/**
* Retrieve the opened modal with highest z-index.
*
* @returns the opened modal with highest z-index.
*/
getTopOpenedModal() {
return this._modalStack.getTopOpenedModal();
}
/**
* Get the higher `z-index` value between all the modal instances. It iterates over the `ModalStack` array and
* calculates a higher value (it takes the highest index value between all the modal instances and adds 1).
* Use it to make a modal appear foreground.
*
* @returns a higher index from all the existing modal instances.
*/
getHigherIndex() {
return this._modalStack.getHigherIndex();
}
/**
* It gives the number of modal instances. It's helpful to know if the modal stack is empty or not.
*
* @returns the number of modal instances.
*/
getModalStackCount() {
return this._modalStack.getModalStackCount();
}
/**
* Remove a modal instance from the modal stack.
*
* @param id The modal identifier.
* @returns the removed modal instance.
*/
removeModal(id) {
this._modalStack.removeModal(id);
}
/**
* Associate data to an identified modal. If the modal isn't already associated to some data, it creates a new
* entry in the `modalData` array with its `id` and the given `data`. If the modal already has data, it rewrites
* them with the new ones. Finally if no modal found it returns an error message in the console and false value
* as method output.
*
* @param data The data you want to associate to the modal.
* @param id The modal identifier.
* @param force If true, overrides the previous stored data if there was.
* @returns true if the given modal exists and the process has been tried, either false.
*/
setModalData(data, id, force) {
let i;
if (i = this.get(id)) {
i.setData(data, force);
return true;
}
else {
return false;
}
}
/**
* Retrieve modal data by its identifier.
*
* @param id The modal identifier used at creation time.
* @returns the associated modal data.
*/
getModalData(id) {
let i;
if (i = this.get(id)) {
return i.getData();
}
return null;
}
/**
* Reset the data attached to a given modal.
*
* @param id The modal identifier used at creation time.
* @returns the removed data or false if modal doesn't exist.
*/
resetModalData(id) {
if (!!this._modalStack.getModalStack().find((o) => o.id === id)) {
const removed = this.getModal(id).getData();
this.getModal(id).removeData();
return removed;
}
else {
return false;
}
}
/**
* Close the latest opened modal if it has been declared as escapable
* Using a debounce system because one or more modals could be listening
* escape key press event.
*/
closeLatestModal() {
this.getTopOpenedModal().close();
}
/**
* Create dynamic NtkSmartModalComponent
* @param id The modal identifier used at creation time.
* @param content The modal content ( string, templateRef or Component )
*/
create(id, content, options = {}) {
try {
return this.getModal(id);
}
catch (e) {
const componentFactory = this._componentFactoryResolver.resolveComponentFactory(NtkSmartModalComponent);
const ngContent = this._resolveNgContent(content);
const componentRef = componentFactory.create(this._injector, ngContent);
if (content instanceof Type) {
componentRef.instance.contentComponent = content;
}
componentRef.instance.identifier = id;
componentRef.instance.createFrom = 'service';
if (typeof options.closable === 'boolean') {
componentRef.instance.closable = options.closable;
}
if (typeof options.escapable === 'boolean') {
componentRef.instance.escapable = options.escapable;
}
if (typeof options.dismissable === 'boolean') {
componentRef.instance.dismissable = options.dismissable;
}
if (typeof options.customClass === 'string') {
componentRef.instance.customClass = options.customClass;
}
if (typeof options.backdrop === 'boolean') {
componentRef.instance.backdrop = options.backdrop;
}
if (typeof options.force === 'boolean') {
componentRef.instance.force = options.force;
}
if (typeof options.hideDelay === 'number') {
componentRef.instance.hideDelay = options.hideDelay;
}
if (typeof options.autostart === 'boolean') {
componentRef.instance.autostart = options.autostart;
}
if (typeof options.target === 'string') {
componentRef.instance.target = options.target;
}
if (typeof options.ariaLabel === 'string') {
componentRef.instance.ariaLabel = options.ariaLabel;
}
if (typeof options.ariaLabelledBy === 'string') {
componentRef.instance.ariaLabelledBy = options.ariaLabelledBy;
}
if (typeof options.ariaDescribedBy === 'string') {
componentRef.instance.ariaDescribedBy = options.ariaDescribedBy;
}
if (typeof options.refocus === 'boolean') {
componentRef.instance.refocus = options.refocus;
}
this._appRef.attachView(componentRef.hostView);
const domElem = componentRef.hostView.rootNodes[0];
this._document.body.appendChild(domElem);
return componentRef.instance;
}
}
_addEvents() {
if (!this.isBrowser) {
return false;
}
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'create', ((e) => {
this._initModal(e.detail.instance);
}));
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'delete', ((e) => {
this._deleteModal(e.detail.instance);
}));
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'open', ((e) => {
this._openModal(e.detail.instance.modal, e.detail.top);
}));
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'toggle', ((e) => {
this._toggleModal(e.detail.instance.modal, e.detail.top);
}));
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'close', ((e) => {
this._closeModal(e.detail.instance.modal);
}));
window.addEventListener(NtkSmartModalConfig.prefixEvent + 'dismiss', ((e) => {
this._dismissModal(e.detail.instance.modal);
}));
window.addEventListener('keyup', this._escapeKeyboardEvent);
return true;
}
_initModal(modalInstance) {
modalInstance.modal.layerPosition += this.getModalStackCount();
this.addModal(modalInstance, modalInstance.modal.force);
if (modalInstance.modal.autostart) {
this.open(modalInstance.id);
}
}
_openModal(modal, top) {
if (modal.visible) {
return false;
}
this.lastElementFocused = document.activeElement;
if (modal.escapable) {
window.addEventListener('keyup', this._escapeKeyboardEvent);
}
if (modal.backdrop) {
window.addEventListener('keydown', this._trapFocusModal);
}
if (top) {
modal.layerPosition = this.getHigherIndex();
}
modal.addBodyClass();
modal.overlayVisible = true;
modal.visible = true;
modal.onOpen.emit(modal);
modal.markForCheck();
setTimeout(() => {
modal.openedClass = true;
if (modal.target) {
modal.targetPlacement();
}
modal.nsmDialog.first.nativeElement.setAttribute('role', 'dialog');
modal.nsmDialog.first.nativeElement.setAttribute('tabIndex', '-1');
modal.nsmDialog.first.nativeElement.setAttribute('aria-modal', 'true');
modal.nsmDialog.first.nativeElement.focus();
modal.markForCheck();
modal.onOpenFinished.emit(modal);
});
return true;
}
_toggleModal(modal, top) {
if (modal.visible) {
return this._closeModal(modal);
}
else {
return this._openModal(modal, top);
}
}
_closeModal(modal) {
if (!modal.openedClass) {
return false;
}
modal.openedClass = false;
modal.onClose.emit(modal);
modal.onAnyCloseEvent.emit(modal);
if (this.getOpenedModals().length < 2) {
modal.removeBodyClass();
window.removeEventListener('keyup', this._escapeKeyboardEvent);
window.removeEventListener('keydown', this._trapFocusModal);
}
setTimeout(() => {
modal.visibleChange.emit(modal.visible);
modal.visible = false;
modal.overlayVisible = false;
modal.nsmDialog.first.nativeElement.removeAttribute('tabIndex');
modal.markForCheck();
modal.onCloseFinished.emit(modal);
modal.onAnyCloseEventFinished.emit(modal);
if (modal.refocus) {
this.lastElementFocused.focus();
}
}, modal.hideDelay);
return true;
}
_dismissModal(modal) {
if (!modal.openedClass) {
return false;
}
modal.openedClass = false;
modal.onDismiss.emit(modal);
modal.onAnyCloseEvent.emit(modal);
if (this.getOpenedModals().length < 2) {
modal.removeBodyClass();
}
setTimeout(() => {
modal.visible = false;
modal.visibleChange.emit(modal.visible);
modal.overlayVisible = false;
modal.markForCheck();
modal.onDismissFinished.emit(modal);
modal.onAnyCloseEventFinished.emit(modal);
}, modal.hideDelay);
return true;
}
_deleteModal(modalInstance) {
this.removeModal(modalInstance.id);
if (!this.getModalStack().length) {
modalInstance.modal.removeBodyClass();
}
}
/**
* Resolve content according to the types
* @param content The modal content ( string, templateRef or Component )
*/
_resolveNgContent(content) {
if (typeof content === 'string') {
const element = this._document.createTextNode(content);
return [[element]];
}
if (content instanceof TemplateRef) {
const viewRef = content.createEmbeddedView(null);
this.applicationRef.attachView(viewRef);
return [viewRef.rootNodes];
}
return [];
}
/**
* Is current platform browser
*/
get isBrowser() {
return isPlatformBrowser(this._platformId);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalService, deps: [{ token: i0.ComponentFactoryResolver }, { token: i0.ApplicationRef }, { token: i0.Injector }, { token: NtkSmartModalStackService }, { token: i0.ApplicationRef }, { token: DOCUMENT }, { token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalService, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: i0.ComponentFactoryResolver }, { type: i0.ApplicationRef }, { type: i0.Injector }, { type: NtkSmartModalStackService }, { type: i0.ApplicationRef }, { type: undefined, decorators: [{
type: Inject,
args: [DOCUMENT]
}] }, { type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }] });
class NtkSmartModalModule {
/**
* Use in AppModule: new instance of NtkSmartModal.
*/
static forRoot() {
return {
ngModule: NtkSmartModalModule,
providers: [
NtkSmartModalService,
NtkSmartModalStackService
],
};
}
/**
* Use in features modules with lazy loading: new instance of NtkSmartModal.
*/
static forChild() {
return {
ngModule: NtkSmartModalModule,
providers: [
NtkSmartModalService,
NtkSmartModalStackService
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalModule, declarations: [NtkSmartModalComponent], imports: [CommonModule], exports: [NtkSmartModalComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartModalModule, decorators: [{
type: NgModule,
args: [{
declarations: [NtkSmartModalComponent],
exports: [NtkSmartModalComponent],
imports: [CommonModule],
}]
}] });
class NtkSmartLoaderService {
constructor() {
this.privateloaderStack = [];
this.privateActions = [];
}
/**
* Add a new loader instance. This step is essential and allows to retrieve any loader at any time.
* It stores an object that contains the given loader identifier and the loader itself directly in the `loaderStack`.
*
* @param loaderInstance The object that contains the given loader identifier and the loader itself.
* @param force Optional parameter that forces the overriding of loader instance if it already exists.
* @returns Returns nothing special.
*/
addLoader(loaderInstance, force) {
if (force) {
const i = this.privateloaderStack.findIndex((o) => {
return o.id === loaderInstance.id;
});
if (i > -1) {
this.privateloaderStack[i].component = loaderInstance.component;
}
else {
this.privateloaderStack.push(loaderInstance);
}
return;
}
let loader;
// tslint:disable-next-line: no-conditional-assignment
if (loader = this._getLoader(loaderInstance.id)) {
throw (new Error('Loader with ' + loaderInstance.id + ' identifier already exist'));
}
else {
this.privateloaderStack.push(loaderInstance);
}
}
/**
* Remove a loader instance from the loader stack.
*
* @param id The loader identifier.
*/
removeLoader(id) {
this.privateloaderStack = this.privateloaderStack.filter((loader) => loader.id !== id);
this._removeAction(id, '*');
}
/**
* Retrieve all the created loaders.
*
* @returns Returns an array that contains all loader instances.
*/
getLoaderStack() {
return this.privateloaderStack;
}
/**
* It gives the number of loader instances. It's helpful to know if the loader stack is empty or not.
*
* @returns Returns the number of loader instances.
*/
getLoaderStackCount() {
return this.privateloaderStack.length;
}
/**
* Retrieve all the opened loaders. It looks for all loader instances with their `visible` property set to `true`.
*
* @returns Returns an array that contains all the opened loaders.
*/
getOpenedLoaders() {
return this.privateloaderStack.filter((loader) => loader.component.visible);
}
/**
* Retrieve all the active loaders. It looks for all loader instances with their `loading` property set to `true`.
*
* @returns Returns an array that contains all the active loaders.
*/
getActiveLoaders() {
return this.privateloaderStack.filter((loader) => loader.component.loading);
}
/**
* Get the higher `z-index` value between all the loader instances. It iterates over the `LoaderStack` array and
* calculates a higher value (it takes the highest index value between all the loader instances and adds 1).
* Use it to make a loader appear foreground.
*
* @returns Returns a higher index from all the existing loader instances.
*/
getHigherIndex() {
const index = this.getOpenedLoaders().map((loader) => loader.component.layerPosition);
return Math.max(...index) + 1;
}
/**
* Enable loading state to one or several loaders.
*
* @param id The loader identifier.
*/
start(id) {
let loader;
if (Array.isArray(id)) {
id.forEach((i) => {
this.start(i);
});
}
else if (loader = this._getLoader(id)) {
loader.component.start();
this._removeAction(id, 'start');
}
else {
this._addAction(id, 'start');
}
}
/**
* Enable loading state to all loaders.
*/
startAll() {
this.privateloaderStack.forEach((loader) => this.start(loader.id));
}
/**
* Disable loading state to one or several loaders.
*
* @param id The loader identifier.
*/
stop(id) {
let loader;
if (Array.isArray(id)) {
id.forEach((i) => {
this.stop(i);
});
}
else if (loader = this._getLoader(id)) {
loader.component.stop();
this._removeAction(id, 'stop');
}
else {
this._addAction(id, 'stop');
}
}
/**
* Disable loading state to all loaders.
*/
stopAll() {
this.privateloaderStack.forEach((loader) => this.stop(loader.id));
}
isLoading(id) {
let loader;
if (Array.isArray(id)) {
const tmp = [];
id.forEach((i) => {
this.privateloaderStack.forEach((load) => {
if (load.id === i) {
tmp.push(load.component.loading);
}
});
});
return tmp.indexOf(false) === -1;
}
else if (loader = this._getLoader(id)) {
return loader.component.loading;
}
else {
return false;
}
}
/**
* Execute an action on loaders
*
* @param id The loader identifier.
* @param action Name of the action.
*/
executeAction(id, action) {
// First check if the action exists in privateActions
if (this.privateActions.find((act) => act.identifier === id && act.action === action)) {
switch (action) {
case 'start':
this.start(id);
break;
case 'stop':
this.stop(id);
break;
}
}
else {
// If no action exists, execute it directly if the loader exists
let loader;
if (loader = this._getLoader(id)) {
switch (action) {
case 'start':
loader.component.start();
break;
case 'stop':
loader.component.stop();
break;
}
}
else {
// If loader doesn't exist, add the action to be executed later
this._addAction(id, action);
}
}
}
/**
* Retrieve a loader instance by its identifier.
* If there's several loaders with same identifier, the first is returned.
*
* @param id The loader identifier used at creation time.
*/
_getLoader(id) {
return this.privateloaderStack.find((load) => load.id === id) || null;
}
/**
* Adds an action on one or more loaders
*
* @param id The loader identifier.
* @param action Name of the action.
*/
_addAction(id, action) {
if (Array.isArray(id)) {
id.forEach((i) => {
this._addAction(i, action);
});
}
else {
this.privateActions.push({ identifier: id, action: action });
}
}
/**
* Remove an action on one or more loaders
*
* @param id The loader identifier.
* @param action Name of the action.
*/
_removeAction(id, action) {
if (Array.isArray(id)) {
id.forEach((i) => {
this._removeAction(i, action);
});
}
else {
this.privateActions = this.privateActions.filter((act) => act.identifier !== id || (act.action !== action && action !== '*'));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
class LoaderInstance {
constructor(component) {
this.id = component.identifier;
this.component = component;
}
}
class NtkSmartLoaderComponent {
constructor(ntkSmartLoaderService, changeDetectorRef) {
this.ntkSmartLoaderService = ntkSmartLoaderService;
this.changeDetectorRef = changeDetectorRef;
this.identifier = '';
this.customClass = '';
this.force = false;
this.delayIn = 0;
this.delayOut = 0;
this.autostart = false;
this.onStart = new EventEmitter();
this.onStop = new EventEmitter();
this.onVisibleChange = new EventEmitter();
this.loading = false;
this.visible = false;
this.layerPosition = 999;
this.privateIsProcessing = false;
this.privateLoaderBodyClass = 'loader-open';
this.privateEnterClass = 'enter';
this.privateLeaveClass = 'leave';
}
ngOnInit() {
try {
console.log('SmartLoader Component initialized with identifier:', this.identifier);
const loader = new LoaderInstance(this);
this.ntkSmartLoaderService.addLoader(loader, this.force);
console.log('Loader added to service. Stack count:', this.ntkSmartLoaderService.getLoaderStackCount());
this.layerPosition += this.ntkSmartLoaderService.getLoaderStackCount();
this.addCustomClass(this.identifier.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase());
if (this.autostart) {
console.log('Autostart enabled, starting loader');
this.ntkSmartLoaderService.start(this.identifier);
}
// Don't auto-start if autostart is false - let user control it manually
}
catch (error) {
console.error('Error in smart loader initialization:', error);
throw error;
}
}
ngOnDestroy() {
this.ntkSmartLoaderService.removeLoader(this.identifier);
}
start(top) {
console.log('SmartLoader start called for identifier:', this.identifier);
this.privateIsProcessing = true;
clearInterval(this.privateDebouncer);
this.visible = true;
console.log('Loader visible set to true');
setTimeout(() => {
this.addCustomClass(this.privateEnterClass);
});
this.privateDebouncer = setTimeout(() => {
if (top) {
this.layerPosition = this.ntkSmartLoaderService.getHigherIndex();
}
if (!document.body.classList.contains(this.privateLoaderBodyClass)) {
document.body.classList.add(this.privateLoaderBodyClass);
}
this.loading = true;
console.log('Loader loading set to true');
this.onStart.emit(this);
this.onVisibleChange.emit(this);
this.removeCustomClass(this.privateEnterClass);
this.privateIsProcessing = false;
}, this.delayIn);
}
stop() {
if (this.privateIsProcessing) {
this.visible = false;
this.loading = false;
}
clearInterval(this.privateDebouncer);
this.addCustomClass(this.privateLeaveClass);
this.loading = false;
this.privateDebouncer = setTimeout(() => {
if (document.body.classList.contains(this.privateLoaderBodyClass)) {
document.body.classList.remove(this.privateLoaderBodyClass);
}
this.visible = false;
this.onStop.emit(this);
this.onVisibleChange.emit(this);
this.removeCustomClass(this.privateLeaveClass);
setTimeout(() => {
this.changeDetectorRef.markForCheck();
});
}, this.delayOut);
}
addCustomClass(className) {
if (!this.customClass.length) {
this.customClass = className;
}
else {
if (this.customClass.indexOf(className) === -1) {
this.customClass += ' ' + className;
}
}
}
removeCustomClass(className) {
if (className) {
this.customClass = this.customClass.replace(className, '').trim();
}
else {
this.customClass = '';
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderComponent, deps: [{ token: NtkSmartLoaderService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NtkSmartLoaderComponent, isStandalone: false, selector: "ntk-smart-loader", inputs: { identifier: "identifier", customClass: "customClass", force: "force", delayIn: "delayIn", delayOut: "delayOut", autostart: "autostart" }, outputs: { onStart: "onStart", onStop: "onStop", onVisibleChange: "onVisibleChange" }, ngImport: i0, template: `
<div
class="loader-container {{ customClass }}"
[ngClass]="{ active: loading }"
[style.z-index]="layerPosition - 1"
*ngIf="visible"
>
<ng-content></ng-content>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderComponent, decorators: [{
type: Component,
args: [{
selector: 'ntk-smart-loader',
standalone: false,
template: `
<div
class="loader-container {{ customClass }}"
[ngClass]="{ active: loading }"
[style.z-index]="layerPosition - 1"
*ngIf="visible"
>
<ng-content></ng-content>
</div>
`,
}]
}], ctorParameters: () => [{ type: NtkSmartLoaderService }, { type: i0.ChangeDetectorRef }], propDecorators: { identifier: [{
type: Input
}], customClass: [{
type: Input
}], force: [{
type: Input
}], delayIn: [{
type: Input
}], delayOut: [{
type: Input
}], autostart: [{
type: Input
}], onStart: [{
type: Output
}], onStop: [{
type: Output
}], onVisibleChange: [{
type: Output
}] } });
class NtkSmartLoaderModule {
/**
* Use in AppModule: new instance of NtkSmartLoader.
*/
static forRoot() {
return {
ngModule: NtkSmartLoaderModule,
providers: [NtkSmartLoaderService],
};
}
/**
* Use in features modules with lazy loading: new instance of NtkSmartLoader.
*/
static forChild() {
return {
ngModule: NtkSmartLoaderModule,
providers: [NtkSmartLoaderService],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderModule, declarations: [NtkSmartLoaderComponent], imports: [CommonModule], exports: [NtkSmartLoaderComponent] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderModule, imports: [CommonModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NtkSmartLoaderModule, decorators: [{
type: NgModule,
args: [{
declarations: [NtkSmartLoaderComponent],
exports: [NtkSmartLoaderComponent],
imports: [CommonModule],
}]
}] });
/*
* Public API Surface of ntk-cms-module
*/
/**
* Generated bundle index. Do not edit.
*/
export { NtkSmartLoaderComponent, NtkSmartLoaderModule, NtkSmartLoaderService, NtkSmartModalComponent, NtkSmartModalModule, NtkSmartModalService, NtkSmartModalStackService };
//# sourceMappingURL=ngx-ntk-smart-module.mjs.map