UNPKG

ng2-multi-modal

Version:
944 lines (937 loc) 62.5 kB
import * as i0 from '@angular/core'; import { Component, input, effect, untracked, TemplateRef, Directive, signal, model, output, computed, HostListener, ViewChild, createComponent, Injectable } from '@angular/core'; import * as i1 from '@angular/router'; import { NavigationStart } from '@angular/router'; import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import * as i2 from '@angular/common'; import { CommonModule } from '@angular/common'; import { trigger, transition, style, animate } from '@angular/animations'; class LoadingIcon { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: LoadingIcon, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: LoadingIcon, isStandalone: true, selector: "loading-icon", ngImport: i0, template: ` <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="50" height="50"> <path d="M512.056 908.647c-84.516 0-166.416-27.084-235.266-78.637-84.15-63.028-138.741-155.109-153.675-259.2-14.934-104.119 11.559-207.816 74.588-291.994 130.162-173.812 377.438-209.25 551.194-79.172 72.844 54.562 124.819 133.228 146.391 221.484 3.684 15.103-5.569 30.319-20.644 34.003-15.075 3.572-30.319-5.541-34.003-20.644-18.45-75.628-63-143.044-125.466-189.816-148.866-111.516-360.844-81.112-472.444 67.866-54.028 72.141-76.725 161.016-63.9 250.256 12.797 89.241 59.597 168.131 131.737 222.131 149.006 111.656 360.956 81.197 472.5-67.781 29.194-39.009 49.219-82.716 59.456-129.938 3.319-15.188 18.366-24.834 33.441-21.544 15.188 3.291 24.834 18.281 21.544 33.441-12.009 55.181-35.353 106.2-69.413 151.762-63.028 84.15-155.109 138.769-259.256 153.675-18.984 2.756-37.941 4.106-56.784 4.106z" fill="#272636"></path> <animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="1s" repeatCount="indefinite"></animateTransform> </svg> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: LoadingIcon, decorators: [{ type: Component, args: [{ selector: 'loading-icon', template: ` <svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="50" height="50"> <path d="M512.056 908.647c-84.516 0-166.416-27.084-235.266-78.637-84.15-63.028-138.741-155.109-153.675-259.2-14.934-104.119 11.559-207.816 74.588-291.994 130.162-173.812 377.438-209.25 551.194-79.172 72.844 54.562 124.819 133.228 146.391 221.484 3.684 15.103-5.569 30.319-20.644 34.003-15.075 3.572-30.319-5.541-34.003-20.644-18.45-75.628-63-143.044-125.466-189.816-148.866-111.516-360.844-81.112-472.444 67.866-54.028 72.141-76.725 161.016-63.9 250.256 12.797 89.241 59.597 168.131 131.737 222.131 149.006 111.656 360.956 81.197 472.5-67.781 29.194-39.009 49.219-82.716 59.456-129.938 3.319-15.188 18.366-24.834 33.441-21.544 15.188 3.291 24.834 18.281 21.544 33.441-12.009 55.181-35.353 106.2-69.413 151.762-63.028 84.15-155.109 138.769-259.256 153.675-18.984 2.756-37.941 4.106-56.784 4.106z" fill="#272636"></path> <animateTransform attributeName="transform" type="rotate" from="0 0 0" to="360 0 0" dur="1s" repeatCount="indefinite"></animateTransform> </svg> `, standalone: true }] }], ctorParameters: () => [] }); class CloseIcon { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: CloseIcon, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: CloseIcon, isStandalone: true, selector: "close-icon", ngImport: i0, template: ` <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" > <path class="color-path" fill-rule="evenodd" d="M5.707 5l3.647-3.646a.5.5 0 0 0-.708-.708L5 4.293 1.354.646a.5.5 0 0 0-.708.708L4.293 5 .646 8.646a.5.5 0 0 0 .708.708L5 5.707l3.646 3.647a.5.5 0 0 0 .708-.708L5.707 5z" /> </svg> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: CloseIcon, decorators: [{ type: Component, args: [{ selector: 'close-icon', template: ` <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" > <path class="color-path" fill-rule="evenodd" d="M5.707 5l3.647-3.646a.5.5 0 0 0-.708-.708L5 4.293 1.354.646a.5.5 0 0 0-.708.708L4.293 5 .646 8.646a.5.5 0 0 0 .708.708L5 5.707l3.646 3.647a.5.5 0 0 0 .708-.708L5.707 5z" /> </svg> `, standalone: true }] }], ctorParameters: () => [] }); class MaximizeIcon { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MaximizeIcon, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: MaximizeIcon, isStandalone: true, selector: "maximize-icon", ngImport: i0, template: ` <svg width="10" height="10" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"> <polygon class="apple-theme color-path" points="5,0 0,0 0,5" /> <polygon class="apple-theme color-path" points="10,10 10,5 5,10" /> <rect class="win-theme color-rect" x="0" y="0" width="10" height="10" stroke="#444" stroke-width="1" fill="none" /> <rect class="win-theme color-rect" x="1" y="1" width="8" height="1" stroke="#444" stroke-width="1" /> </svg> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MaximizeIcon, decorators: [{ type: Component, args: [{ selector: 'maximize-icon', template: ` <svg width="10" height="10" viewBox="0 0 10 10" xmlns="http://www.w3.org/2000/svg"> <polygon class="apple-theme color-path" points="5,0 0,0 0,5" /> <polygon class="apple-theme color-path" points="10,10 10,5 5,10" /> <rect class="win-theme color-rect" x="0" y="0" width="10" height="10" stroke="#444" stroke-width="1" fill="none" /> <rect class="win-theme color-rect" x="1" y="1" width="8" height="1" stroke="#444" stroke-width="1" /> </svg> `, standalone: true }] }], ctorParameters: () => [] }); class MinimizeIcon { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MinimizeIcon, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: MinimizeIcon, isStandalone: true, selector: "minimize-icon", ngImport: i0, template: ` <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" > <path class="color-path" fill-rule="evenodd" d="M0 5h10v1H0z" /> </svg> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MinimizeIcon, decorators: [{ type: Component, args: [{ selector: 'minimize-icon', template: ` <svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 10 10" > <path class="color-path" fill-rule="evenodd" d="M0 5h10v1H0z" /> </svg> `, standalone: true }] }], ctorParameters: () => [] }); class MaximizeDIcon { constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MaximizeDIcon, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.2", type: MaximizeDIcon, isStandalone: true, selector: "maximized-icon", ngImport: i0, template: ` <svg width="100" height="100" viewBox="-2 -2 14 14" xmlns="http://www.w3.org/2000/svg"> <polygon class="apple-theme color-path" points="-2,4.6 4.6,4.6 4.6,-2" ></polygon> <polygon class="apple-theme color-path" points="5.4,5.4 12,5.4 5.4,12" ></polygon> <!-- Two rectangles, one is under the other, one in the top right corner, one in the bottom left corner, and the one in the bottom left corner --> <rect class="win-theme color-path" x="2" y="-1" width="9" height="9" stroke="#444" stroke-width="1" fill="none" ></rect> <rect class="win-theme color-path" x="-1" y="2" width="9" height="9" stroke="#444" stroke-width="1" ></rect> </svg> `, isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: MaximizeDIcon, decorators: [{ type: Component, args: [{ selector: 'maximized-icon', template: ` <svg width="100" height="100" viewBox="-2 -2 14 14" xmlns="http://www.w3.org/2000/svg"> <polygon class="apple-theme color-path" points="-2,4.6 4.6,4.6 4.6,-2" ></polygon> <polygon class="apple-theme color-path" points="5.4,5.4 12,5.4 5.4,12" ></polygon> <!-- Two rectangles, one is under the other, one in the top right corner, one in the bottom left corner, and the one in the bottom left corner --> <rect class="win-theme color-path" x="2" y="-1" width="9" height="9" stroke="#444" stroke-width="1" fill="none" ></rect> <rect class="win-theme color-path" x="-1" y="2" width="9" height="9" stroke="#444" stroke-width="1" ></rect> </svg> `, standalone: true }] }], ctorParameters: () => [] }); class StringTemplateOutletDirective { viewContainer; templateRef; embeddedViewRef = null; context = new StringTemplateOutletContext(); // Using Angular 19's input() function instead of @Input decorator stringTemplateOutletContext = input(null); stringTemplateOutlet = input(null); static ngTemplateContextGuard(_dir, _ctx) { return true; } constructor(viewContainer, templateRef) { this.viewContainer = viewContainer; this.templateRef = templateRef; // Effect that reacts to changes in inputs effect(() => { // Read the current values of inputs const outlet = this.stringTemplateOutlet(); const _ = this.stringTemplateOutletContext(); // Update the implicit context when outlet changes if (outlet !== null) { this.context.$implicit = outlet; } // Determine if we should recreate the view untracked(() => { const isNewOutletTemplate = outlet instanceof TemplateRef; // Check if the embedded view exists and if the template type changed const shouldRecreate = !this.embeddedViewRef || (this.embeddedViewRef && isNewOutletTemplate); if (shouldRecreate) { this.recreateView(); } else if (this.embeddedViewRef) { this.updateContext(); } }); }); } recreateView() { this.viewContainer.clear(); const isTemplateRef = this.stringTemplateOutlet() instanceof TemplateRef; const templateRef = (isTemplateRef ? this.stringTemplateOutlet() : this.templateRef); this.embeddedViewRef = this.viewContainer.createEmbeddedView(templateRef, isTemplateRef ? this.stringTemplateOutletContext() : this.context); } updateContext() { if (!this.embeddedViewRef) return; const isTemplateRef = this.stringTemplateOutlet() instanceof TemplateRef; const newCtx = isTemplateRef ? this.stringTemplateOutletContext() : this.context; const oldCtx = this.embeddedViewRef.context; if (newCtx) { for (const propName of Object.keys(newCtx)) { oldCtx[propName] = newCtx[propName]; } } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: StringTemplateOutletDirective, deps: [{ token: i0.ViewContainerRef }, { token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "19.2.2", type: StringTemplateOutletDirective, isStandalone: true, selector: "[stringTemplateOutlet]", inputs: { stringTemplateOutletContext: { classPropertyName: "stringTemplateOutletContext", publicName: "stringTemplateOutletContext", isSignal: true, isRequired: false, transformFunction: null }, stringTemplateOutlet: { classPropertyName: "stringTemplateOutlet", publicName: "stringTemplateOutlet", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["stringTemplateOutlet"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: StringTemplateOutletDirective, decorators: [{ type: Directive, args: [{ selector: '[stringTemplateOutlet]', exportAs: 'stringTemplateOutlet', standalone: true }] }], ctorParameters: () => [{ type: i0.ViewContainerRef }, { type: i0.TemplateRef }] }); class StringTemplateOutletContext { $implicit; } class Ng2MultiModalComponent { modalService; modalId = signal('window' + Math.floor(Math.random() * 1000000)); titleHeight = signal(0); position = signal({}); dragging = signal(false); windowMouseEnterFlag = signal(false); windowMouseDownFlag = signal(false); windowMouseLeaveFlag = signal(true); clickedX = signal(0); clickedY = signal(0); mouseEventSignal = signal(null); mouseEnteredSignal = signal(null); borderWidth = signal(4); cursorStyle = signal('default'); display = signal('none'); border = signal({ isLeft: false, isRight: false, isTop: false, isBottom: false }); propertyBeforeMaximize = signal(null); // Input signals title = input('Modal Name'); icon = input(null); align = input('leftTop'); bodyStyle = input({}); closeOnNavigation = input(false); closable = input(true); content = input(); minHeight = input(100); minWidth = input(175); maximizable = input(true); minimizable = input(true); resizable = input(true); outOfBounds = input(false); loadingTip = input(this.getLocaleText('loading')); // Model signals height = model(300); width = model(300); zIndex = model(0); offsetY = model(200); offsetX = model(200); loading = model(true); theme = model('light'); draggable = model(true); contentScrollable = model(false); minimized = model(false); maximized = model(false); // Output events onReady = output(); onClose = output(); onResize = output(); onMaximize = output(); onMaximizeRestore = output(); onMinimize = output(); onMinimizeRestore = output(); onSelected = output(); onMove = output(); constructor(modalService) { this.modalService = modalService; } get language() { return this.modalService?.language() || 'en'; } windowSizeSignal = computed(() => ({ offsetX: this.offsetX(), offsetY: this.offsetY(), align: this.align(), width: this.width(), height: this.height() })); leftSignal = computed(() => (this.align() === 'leftTop' || this.align() === 'leftBottom') ? this.offsetX() : window.innerWidth - this.width() - this.offsetX()); rightSignal = computed(() => (this.align() === 'rightTop' || this.align() === 'rightBottom') ? window.innerWidth - this.offsetX() : this.width() + this.offsetX()); topSignal = computed(() => (this.align() === 'leftTop' || this.align() === 'rightTop') ? this.offsetY() : window.innerHeight - this.height() - this.offsetY()); bottomSignal = computed(() => (this.align() === 'leftBottom' || this.align() === 'rightBottom') ? window.innerHeight - this.offsetY() : this.height() + this.offsetY()); selectedSignal = computed(() => this.modalService.selectedWindow() === this.modalId()); // Create a signal for the element titleBarElement = signal(null); // Update the setter set titleBar(titleBar) { this.titleBarElement.set(titleBar); if (titleBar) { this.titleHeight.set(titleBar.nativeElement.offsetHeight); } } getLocaleText(text) { const dictionary = { 'es': { loading: 'Loading...', close: 'Close Window', maximize: 'Maximize', minimize: 'Minimize', windowMode: 'Window Mode', }, 'en': { loading: 'Loading...', close: 'Close Window', maximize: 'Maximize', minimize: 'Minimize', windowMode: 'Window Mode', } }; return dictionary[this.language][text]; } updateOffsetX(offsetX) { this.offsetX.set(offsetX); if (['leftTop', 'leftBottom'].includes(this.align())) { this.position.update((oldValue) => { return { ...oldValue, left: offsetX + 'px', right: null }; }); } else { this.position.update((oldValue) => { return { ...oldValue, left: null, right: offsetX + 'px' }; }); } } updateOffsetY(offsetY) { this.offsetY.set(offsetY); if (['leftTop', 'rightTop'].includes(this.align())) { this.position.update((oldValue) => { return { ...oldValue, top: offsetY + 'px', bottom: null }; }); } else { this.position.update((oldValue) => { return { ...oldValue, top: null, bottom: offsetY + 'px' }; }); } } preventTextSelection(prevent) { if (prevent) { // Create a temporary overlay div that covers the entire viewport // This catches all mouse events during resize/drag without affecting actual content if (!document.getElementById('ng2-modal-overlay')) { const overlay = document.createElement('div'); overlay.id = 'ng2-modal-overlay'; overlay.style.position = 'fixed'; overlay.style.top = '0'; overlay.style.left = '0'; overlay.style.width = '100%'; overlay.style.height = '100%'; overlay.style.zIndex = '999999'; overlay.style.pointerEvents = 'none'; // Allow events to pass through document.body.appendChild(overlay); } // Better to use a class to avoid direct style manipulation document.body.classList.add('ng2-modal-no-select'); // Add style if it doesn't exist if (!document.getElementById('ng2-modal-style')) { const style = document.createElement('style'); style.id = 'ng2-modal-style'; style.innerHTML = ` .ng2-modal-no-select { cursor: inherit !important; } .ng2-modal-no-select ::selection { background: transparent !important; } .ng2-modal-no-select ::-moz-selection { background: transparent !important; } `; document.head.appendChild(style); } } else { // Remove all elements we added document.body.classList.remove('ng2-modal-no-select'); const overlay = document.getElementById('ng2-modal-overlay'); if (overlay) { overlay.remove(); } } } documentMouseLeave() { // Ensure text selection is re-enabled when the mouse leaves the document this.preventTextSelection(false); this.dragging.set(false); this.windowMouseDownFlag.set(false); } onMouseMove(event) { if (!this.draggable() || this.maximized()) { return; } this.mouseEventSignal.set(event); // Only prevent text selection during actual dragging or resizing if (this.dragging() || (this.windowMouseDownFlag() && this.resizable() && Object.values(this.border()).some(value => value === true))) { this.preventTextSelection(true); } if (this.dragging()) { // Replace when(this.align) with switch or if/else let newOffsetX = 0; if (this.align() === 'leftTop' || this.align() === 'leftBottom') { if (!this.outOfBounds) { let offsetX = Math.max(event.clientX - this.clickedX(), 0); if (offsetX + this.width() > window.innerWidth) { offsetX = window.innerWidth - this.width(); } newOffsetX = offsetX; } else { newOffsetX = event.clientX - this.clickedX(); } } else if (this.align() === 'rightTop' || this.align() === 'rightBottom') { if (!this.outOfBounds) { let offsetX = Math.max(window.innerWidth - event.clientX + this.clickedX() - this.width(), 0); if (offsetX + this.width() > window.innerWidth) { offsetX = window.innerWidth - this.width(); } newOffsetX = offsetX; } else { newOffsetX = window.innerWidth - event.clientX + this.clickedX() - this.width(); } } this.updateOffsetX(newOffsetX); // Handle Y positioning let newOffsetY = 0; if (this.align() === 'leftTop' || this.align() === 'rightTop') { if (!this.outOfBounds) { let offsetY = Math.max(event.clientY - this.clickedY(), 0); if (offsetY + this.height() > window.innerHeight) { offsetY = window.innerHeight - this.height(); } newOffsetY = offsetY; } else { newOffsetY = event.clientY - this.clickedY(); } } else if (this.align() === 'leftBottom' || this.align() === 'rightBottom') { if (!this.outOfBounds) { let offsetY = Math.max(window.innerHeight - event.clientY + this.clickedY() - this.height(), 0); if (offsetY + this.height() > window.innerHeight) { offsetY = window.innerHeight - this.height(); } newOffsetY = offsetY; } else { newOffsetY = window.innerHeight - event.clientY + this.clickedY() - this.height(); } } this.updateOffsetY(newOffsetY); } if (this.windowMouseDownFlag() && this.resizable()) { this.resizeWindow(event); } else { this.onMove.emit({ ...this }); } let x = event.clientX; let y = event.clientY; let leftBorderX = Math.abs(this.leftSignal() - x) <= this.borderWidth(); let rightBorderX = Math.abs(this.leftSignal() + this.width() - x) <= this.borderWidth(); let rightLeftBorderY = (y > this.topSignal()) && (y < (this.topSignal() + this.height())); let topBorderY = Math.abs(this.topSignal() - y) <= this.borderWidth(); let bottomBorderY = Math.abs(this.topSignal() + this.height() - y) <= this.borderWidth(); let topBottomBorderX = x > this.leftSignal() && x < this.leftSignal() + this.width(); if (this.resizable()) { if (leftBorderX && bottomBorderY) { this.cursorStyle.set('sw-resize'); this.border.set({ isLeft: true, isBottom: true, }); this.contentScrollable.set(true); } else if (rightBorderX && bottomBorderY) { this.cursorStyle.set('se-resize'); this.border.set({ isRight: true, isBottom: true, }); this.contentScrollable.set(true); } else if (leftBorderX && topBorderY) { this.cursorStyle.set('nw-resize'); this.border.set({ isLeft: true, isTop: true, }); this.contentScrollable.set(true); } else if (rightBorderX && topBorderY) { this.cursorStyle.set('ne-resize'); this.border.set({ isRight: true, isTop: true, }); this.contentScrollable.set(true); } else if (leftBorderX && rightLeftBorderY) { this.cursorStyle.set('w-resize'); this.border.set({ isLeft: true, }); this.contentScrollable.set(true); } else if (rightBorderX && rightLeftBorderY) { this.cursorStyle.set('e-resize'); this.border.set({ isRight: true, }); this.contentScrollable.set(true); } else if (topBorderY && topBottomBorderX) { this.cursorStyle.set('n-resize'); this.border.set({ isTop: true, }); this.contentScrollable.set(true); } else if (bottomBorderY && topBottomBorderX) { this.cursorStyle.set('s-resize'); this.border.set({ isBottom: true, }); this.contentScrollable.set(true); } else { this.border.set({}); this.cursorStyle.set('auto'); this.contentScrollable.set(false); } } } resizeWindow(event) { if (!this.draggable()) { return; } if (this.dragging()) { return; } if (!this.border().isLeft && !this.border().isRight && !this.border().isTop && !this.border().isBottom) { return; } if (this.border().isLeft) { if (this.align().toLocaleLowerCase().includes('left')) { let r = this.rightSignal(); this.updateOffsetX(event.clientX); this.width.set(r - this.leftSignal()); } else { this.width.set(this.rightSignal() - event.clientX); } } if (this.border().isRight) { if (this.align().toLocaleLowerCase().includes('left')) { this.width.update(prev => prev + event.clientX - this.rightSignal()); } else { this.width.update(prev => prev + event.clientX - this.rightSignal()); ; this.updateOffsetX(Math.max(window.innerWidth - event.clientX, 0)); } } if (this.border().isTop) { if (this.align().toLocaleLowerCase().includes('top')) { let b = this.bottomSignal(); this.updateOffsetY(Math.max(event.clientY, 0)); this.height.set(b - this.topSignal()); } else { this.height.set(this.bottomSignal() - event.clientY); } } if (this.border().isBottom) { if (this.align().toLocaleLowerCase().includes('top')) { this.height.update(prev => prev + event.clientY - this.bottomSignal()); } else { this.height.update(prev => prev + event.clientY - this.bottomSignal()); this.updateOffsetY(Math.max(window.innerHeight - event.clientY, 0)); } } if (this.height() < this.minHeight()) { this.height.set(this.minHeight()); } if (this.width() < this.minWidth()) { this.width.set(this.minWidth()); } if (!this.outOfBounds() && this.height() + this.offsetY() > window.innerHeight) { this.updateOffsetY(Math.max(window.innerHeight - this.height(), 0)); } if (!this.outOfBounds && this.width() + this.offsetX() > window.innerWidth) { this.updateOffsetX(Math.max(window.innerWidth - this.width(), 0)); } if (this.offsetY() < 0) { this.updateOffsetY(0); } this.onResize.emit(this.windowSizeSignal()); } titleBarMouseDown(event) { if (event.button === 2) { return; } // Prevent default action to avoid text selection event.preventDefault(); // Enable text selection prevention this.preventTextSelection(true); this.dragging.set(true); this.clickedX.set(event.clientX - this.leftSignal()); this.clickedY.set(event.clientY - this.topSignal()); } titleBarMouseUp(event) { // We should always re-enable text selection on mouse up this.preventTextSelection(false); this.dragging.set(false); this.windowMouseDownFlag.set(false); } // Add a key event listener to ensure we clean up if Escape is pressed handleEscapeKey(event) { this.preventTextSelection(false); } windowMouseEnter(event) { if (!this.draggable()) { return; } this.mouseEnteredSignal.set(event); this.windowMouseEnterFlag.set(true); this.windowMouseLeaveFlag.set(false); } windowMouseDown(event) { this.modalService.selectedWindow.set(this.modalId()); if (!this.draggable() || event.button === 2) { return; } // Only prevent selection when clicking on a border for resizing const onBorder = Object.values(this.border()).some(value => value === true); if (onBorder && this.resizable()) { event.preventDefault(); this.preventTextSelection(true); } this.windowMouseDownFlag.set(true); this.onSelected.emit(this.modalId()); if (this.windowMouseDownFlag() && this.windowMouseEnterFlag()) { const idx = this.modalService.maxZIndex++; this.zIndex.set(idx); } } windowMouseLeave(event) { if (!this.draggable()) { return; } this.windowMouseLeaveFlag.set(true); this.windowMouseEnterFlag.set(false); } close() { if (this.closable()) { this.height.set(0); window.onresize = null; this.toggleBodyScrollable(true); this.display.set('none'); this.onClose.emit(this.modalId()); } } minimize() { window.onresize = null; if (this.minimized()) { this.minimized.set(false); this.display.set('block'); // this.modalService.dockComponentRef!.instance.docks = // this.modalService.dockComponentRef!.instance.docks.filter(win => win != this); this.modalService.dockComponentRef.instance.docks.update(prev => prev.filter(win => win !== this)); this.onMinimizeRestore.emit(this.windowSizeSignal()); } else { this.toggleBodyScrollable(true); this.minimized.set(true); setTimeout(() => { this.display.set('none'); }, 200); this.modalService.addMinimizeItem(this); this.onMinimize.emit(this.windowSizeSignal()); } this.onResize.emit(this.windowSizeSignal()); } maximize() { return new Promise(resolve => { const isCurrentlyMaximized = this.maximized(); const hasStoredProperties = !!this.propertyBeforeMaximize(); if (isCurrentlyMaximized && hasStoredProperties) { // Restore from maximized state this.toggleBodyScrollable(true); window.onresize = null; this.maximized.set(false); const { width, height, offsetX, offsetY } = this.propertyBeforeMaximize(); this.width.set(width); this.height.set(height); this.updateOffsetX(offsetX); this.updateOffsetY(offsetY); this.draggable.set(true); this.onResize.emit(this.windowSizeSignal()); this.onMaximizeRestore.emit(this.windowSizeSignal()); resolve(true); } else { // Save current state before maximizing this.propertyBeforeMaximize.set({ width: this.width(), height: this.height(), offsetX: this.offsetX(), offsetY: this.offsetY(), align: this.align() }); // Maximize the window this.toggleBodyScrollable(false); this.maximized.set(true); this.draggable.set(false); // Set dimensions and position this.updateOffsetX(0); this.updateOffsetY(0); this.height.set(window.innerHeight); this.width.set(window.innerWidth); // Handle window resize when maximized window.onresize = () => { this.width.set(window.innerWidth); this.height.set(window.innerHeight); this.updateOffsetX(0); this.updateOffsetY(0); }; this.onResize.emit(this.windowSizeSignal()); this.onMaximize.emit(this.windowSizeSignal()); resolve(true); } }); } //if html window resized, judge if window is out of screen, if so, move it to the screen resizeListener(event) { if (this.offsetY() + this.height() > window.innerHeight) { this.updateOffsetY(Math.max(window.innerHeight - this.height(), 0)); } if (this.offsetX() + this.width() > window.innerWidth) { this.updateOffsetX(Math.max(window.innerWidth - this.width(), 0)); } this.onResize.emit(this.windowSizeSignal()); } toggleBodyScrollable(scrollable = true) { setTimeout(() => { if (scrollable) { document.body.style.overflow = 'auto'; } else { document.body.style.overflow = 'hidden'; } }, 200); } ngOnDestroy() { // Clean up any remaining text selection prevention this.preventTextSelection(false); // Remove any window resize handlers window.onresize = null; } async ngAfterViewInit() { if (this.maximized()) { this.display.set('none'); this.maximized.set(false); await this.maximize(); } this.display.set('block'); this.loading.set(false); this.onReady.emit(this); this.updateOffsetX(this.offsetX()); this.updateOffsetY(this.offsetY()); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: Ng2MultiModalComponent, deps: [{ token: Ng2MultiModalService }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.2", type: Ng2MultiModalComponent, isStandalone: true, selector: "ng2-multi-modal", inputs: { title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null }, icon: { classPropertyName: "icon", publicName: "icon", isSignal: true, isRequired: false, transformFunction: null }, align: { classPropertyName: "align", publicName: "align", isSignal: true, isRequired: false, transformFunction: null }, bodyStyle: { classPropertyName: "bodyStyle", publicName: "bodyStyle", isSignal: true, isRequired: false, transformFunction: null }, closeOnNavigation: { classPropertyName: "closeOnNavigation", publicName: "closeOnNavigation", isSignal: true, isRequired: false, transformFunction: null }, closable: { classPropertyName: "closable", publicName: "closable", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "content", isSignal: true, isRequired: false, transformFunction: null }, minHeight: { classPropertyName: "minHeight", publicName: "minHeight", isSignal: true, isRequired: false, transformFunction: null }, minWidth: { classPropertyName: "minWidth", publicName: "minWidth", isSignal: true, isRequired: false, transformFunction: null }, maximizable: { classPropertyName: "maximizable", publicName: "maximizable", isSignal: true, isRequired: false, transformFunction: null }, minimizable: { classPropertyName: "minimizable", publicName: "minimizable", isSignal: true, isRequired: false, transformFunction: null }, resizable: { classPropertyName: "resizable", publicName: "resizable", isSignal: true, isRequired: false, transformFunction: null }, outOfBounds: { classPropertyName: "outOfBounds", publicName: "outOfBounds", isSignal: true, isRequired: false, transformFunction: null }, loadingTip: { classPropertyName: "loadingTip", publicName: "loadingTip", isSignal: true, isRequired: false, transformFunction: null }, height: { classPropertyName: "height", publicName: "height", isSignal: true, isRequired: false, transformFunction: null }, width: { classPropertyName: "width", publicName: "width", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null }, offsetY: { classPropertyName: "offsetY", publicName: "offsetY", isSignal: true, isRequired: false, transformFunction: null }, offsetX: { classPropertyName: "offsetX", publicName: "offsetX", isSignal: true, isRequired: false, transformFunction: null }, loading: { classPropertyName: "loading", publicName: "loading", isSignal: true, isRequired: false, transformFunction: null }, theme: { classPropertyName: "theme", publicName: "theme", isSignal: true, isRequired: false, transformFunction: null }, draggable: { classPropertyName: "draggable", publicName: "draggable", isSignal: true, isRequired: false, transformFunction: null }, contentScrollable: { classPropertyName: "contentScrollable", publicName: "contentScrollable", isSignal: true, isRequired: false, transformFunction: null }, minimized: { classPropertyName: "minimized", publicName: "minimized", isSignal: true, isRequired: false, transformFunction: null }, maximized: { classPropertyName: "maximized", publicName: "maximized", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { height: "heightChange", width: "widthChange", zIndex: "zIndexChange", offsetY: "offsetYChange", offsetX: "offsetXChange", loading: "loadingChange", theme: "themeChange", draggable: "draggableChange", contentScrollable: "contentScrollableChange", minimized: "minimizedChange", maximized: "maximizedChange", onReady: "onReady", onClose: "onClose", onResize: "onResize", onMaximize: "onMaximize", onMaximizeRestore: "onMaximizeRestore", onMinimize: "onMinimize", onMinimizeRestore: "onMinimizeRestore", onSelected: "onSelected", onMove: "onMove" }, host: { listeners: { "document:mouseleave": "documentMouseLeave()", "document:mousemove": "onMouseMove($event)", "document:mouseup": "titleBarMouseUp($event)", "document:keydown.escape": "handleEscapeKey($event)", "window: resize": "resizeListener($event)" } }, viewQueries: [{ propertyName: "titleBar", first: true, predicate: ["titleBar"], descendants: true }], ngImport: i0, template: "<div\r\n [ngClass]=\"[\r\n 'ng-modal',\r\n 'ng-modal-theme' + (this.theme() === 'dark' ? '-dark' : '')\r\n ]\"\r\n [hidden]=\"loading()\"\r\n [ngStyle]=\"position()\"\r\n [style.width.px]=\"width()\"\r\n [style.height.px]=\"height()\"\r\n [style.cursor]=\"cursorStyle()\"\r\n [style.z-index]=\"zIndex()\"\r\n [style.display]=\"display()\"\r\n></div>\r\n<div\r\n [class]=\"'ng-modal ng-modal-theme' + (this.theme() === 'dark' ? '-dark' : '')\"\r\n [hidden]=\"loading()\"\r\n [ngClass]=\"{\r\n selected: selectedSignal(),\r\n minimized: minimized(),\r\n maximized: maximized()\r\n }\"\r\n [ngStyle]=\"position()\"\r\n [style.width.px]=\"width()\"\r\n [style.height.px]=\"height()\"\r\n [style.cursor]=\"cursorStyle()\"\r\n [style.z-index]=\"zIndex()\"\r\n [style.display]=\"display()\"\r\n (mouseenter)=\"windowMouseEnter($event)\"\r\n (mousedown)=\"windowMouseDown($event)\"\r\n (mouseleave)=\"windowMouseLeave($event)\"\r\n>\r\n <div\r\n #titleBar\r\n class=\"win-title-bar\"\r\n *ngIf=\"!!title()\"\r\n (dblclick)=\"maximize()\"\r\n [ngClass]=\"{ 'no-drag': !draggable() }\"\r\n (mousedown)=\"titleBarMouseDown($event)\"\r\n >\r\n <div class=\"title-name\" [title]=\"title()\">\r\n <ng-container *stringTemplateOutlet=\"icon()\">\r\n @if (icon()) {\r\n <img class=\"icon\" draggable=\"false\" [src]=\"icon()\" alt=\"icon\" />\r\n }\r\n </ng-container>\r\n <ng-container *stringTemplateOutlet=\"title()\">{{ title() }}</ng-container>\r\n </div>\r\n <div class=\"win-icons\">\r\n <close-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('close')\"\r\n (click)=\"close()\"\r\n />\r\n <minimize-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('minimize')\"\r\n (click)=\"minimize()\"\r\n />\r\n <maximize-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('maximize')\"\r\n *ngIf=\"!maximized()\"\r\n (click)=\"maximize()\"\r\n />\r\n <maximized-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('windowMode')\"\r\n *ngIf=\"maximized()\"\r\n (click)=\"maximize()\"\r\n />\r\n </div>\r\n </div>\r\n <div\r\n class=\"body\"\r\n [ngClass]=\"contentScrollable() ? 'no-scroll' : ''\"\r\n [ngStyle]=\"bodyStyle()\"\r\n >\r\n <ng-container *stringTemplateOutlet=\"content\">{{ content() }}</ng-container>\r\n <ng-content></ng-content>\r\n </div>\r\n</div>\r\n\r\n<ng-container *stringTemplateOutlet=\"loadingTip()\">\r\n <div\r\n [class]=\"'window-loading' + (this.theme() === 'dark' ? '-dark' : '')\"\r\n [ngStyle]=\"position()\"\r\n *ngIf=\"loading()\"\r\n >\r\n <div>\r\n <loading-icon />\r\n </div>\r\n <ng-container *stringTemplateOutlet=\"loadingTip()\">{{\r\n loadingTip()\r\n }}</ng-container>\r\n </div>\r\n</ng-container>\r\n", dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "component", type: CloseIcon, selector: "close-icon" }, { kind: "component", type: LoadingIcon, selector: "loading-icon" }, { kind: "component", type: MaximizeIcon, selector: "maximize-icon" }, { kind: "component", type: MinimizeIcon, selector: "minimize-icon" }, { kind: "component", type: MaximizeDIcon, selector: "maximized-icon" }, { kind: "directive", type: StringTemplateOutletDirective, selector: "[stringTemplateOutlet]", inputs: ["stringTemplateOutletContext", "stringTemplateOutlet"], exportAs: ["stringTemplateOutlet"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.2", ngImport: i0, type: Ng2MultiModalComponent, decorators: [{ type: Component, args: [{ selector: 'ng2-multi-modal', imports: [CommonModule, CloseIcon, LoadingIcon, MaximizeIcon, MinimizeIcon, MaximizeDIcon, StringTemplateOutletDirective], standalone: true, template: "<div\r\n [ngClass]=\"[\r\n 'ng-modal',\r\n 'ng-modal-theme' + (this.theme() === 'dark' ? '-dark' : '')\r\n ]\"\r\n [hidden]=\"loading()\"\r\n [ngStyle]=\"position()\"\r\n [style.width.px]=\"width()\"\r\n [style.height.px]=\"height()\"\r\n [style.cursor]=\"cursorStyle()\"\r\n [style.z-index]=\"zIndex()\"\r\n [style.display]=\"display()\"\r\n></div>\r\n<div\r\n [class]=\"'ng-modal ng-modal-theme' + (this.theme() === 'dark' ? '-dark' : '')\"\r\n [hidden]=\"loading()\"\r\n [ngClass]=\"{\r\n selected: selectedSignal(),\r\n minimized: minimized(),\r\n maximized: maximized()\r\n }\"\r\n [ngStyle]=\"position()\"\r\n [style.width.px]=\"width()\"\r\n [style.height.px]=\"height()\"\r\n [style.cursor]=\"cursorStyle()\"\r\n [style.z-index]=\"zIndex()\"\r\n [style.display]=\"display()\"\r\n (mouseenter)=\"windowMouseEnter($event)\"\r\n (mousedown)=\"windowMouseDown($event)\"\r\n (mouseleave)=\"windowMouseLeave($event)\"\r\n>\r\n <div\r\n #titleBar\r\n class=\"win-title-bar\"\r\n *ngIf=\"!!title()\"\r\n (dblclick)=\"maximize()\"\r\n [ngClass]=\"{ 'no-drag': !draggable() }\"\r\n (mousedown)=\"titleBarMouseDown($event)\"\r\n >\r\n <div class=\"title-name\" [title]=\"title()\">\r\n <ng-container *stringTemplateOutlet=\"icon()\">\r\n @if (icon()) {\r\n <img class=\"icon\" draggable=\"false\" [src]=\"icon()\" alt=\"icon\" />\r\n }\r\n </ng-container>\r\n <ng-container *stringTemplateOutlet=\"title()\">{{ title() }}</ng-container>\r\n </div>\r\n <div class=\"win-icons\">\r\n <close-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('close')\"\r\n (click)=\"close()\"\r\n />\r\n <minimize-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('minimize')\"\r\n (click)=\"minimize()\"\r\n />\r\n <maximize-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('maximize')\"\r\n *ngIf=\"!maximized()\"\r\n (click)=\"maximize()\"\r\n />\r\n <maximized-icon\r\n class=\"win-icon\"\r\n [title]=\"getLocaleText('windowMode')\"\r\n *ngIf=\"maximized()\"\r\n (click)=\"maximize()\"\r\n />\r\n </div>\r\n </div>\r\n <div\r\n class=\"body\"\r\n [ngClass]=\"contentScrollable() ? 'no-scroll' : ''\"\r\n [ngStyle]=\"bodyStyle()\"\r\n >\r\n <ng-container *stringTemplateOutlet=\"content\">{{ content() }}</ng-container>\r\n <ng-content></ng-content>\r\n </div>\r\n</div>\r\n\r\n<ng-container *stringTemplateOutlet=\"loadingTip()\">\r\n <div\r\n [class]=\"'window-loading' + (this.theme() === 'dark' ? '-dark' : '')\"\r\n [ngStyle]=\"position()\"\r\n *ngIf=\"loading()\"\r\n >\r\n <div>\r\n <loading-icon />\r\n </div>\r\n <ng-container *stringTemplateOutlet=\"loadingTip()\">{{\r\n loadingTip()\r\n }}</ng-container>\r\n </div>\r\n</ng-container>\r\n" }] }], ctorParameters: () => [{ type: Ng2MultiModalService }], propDecorators: { titleBar: [{ type: ViewChild, args: ['titleBar', { static: false }] }], documentMouseLeave: [{ type: HostListener, args: ['document:mouseleave'] }], onMouseMove: [{ type: HostListener, args: ['document:mousemove', ['$event']] }], titleBarMouseUp: [{ type: HostListener, args: ['document:mouseup', ['$event']] }], handleEscapeKey: