import { AfterViewInit, ApplicationRef, ChangeDetectorRef, Component, ComponentFactoryResolver, ComponentRef, Directive, EventEmitter, Inject, Injector, Input, OnChanges, OnDestroy, Output, SimpleChanges, TemplateRef, Type, ViewChild, ViewContainerRef } from "@angular/core"; import {ComponentPortal, DomPortalHost, PortalHost} from "@angular/cdk/portal"; import {WindowComponent} from "../window/window.component"; import {WindowRegistry} from "../window/window-state"; import {RenderComponentDirective} from "../core/render-component.directive"; import {ELECTRON_WINDOW, IN_DIALOG} from "../app/token"; import {Subscription} from "rxjs"; import {DOCUMENT} from "@angular/common"; import {DuiDialog} from "../dialog/dialog"; import {Electron} from "../../core/utils"; import {detectChangesNextFrame} from "../app"; function PopupCenter(url: string, title: string, w: number, h: number): Window { let top = window.screenTop + (window.outerHeight / 2) - w / 2; top = top > 0 ? top : 0; let left = window.screenLeft + (window.outerWidth / 2) - w / 2; left = left > 0 ? left : 0; const newWindow: Window = window.open(url, title, 'width=' + w + ', height=' + h + ', top=' + top + ', left=' + left)!; return newWindow; } @Component({ template: ` `, host: { '[attr.tabindex]': '1' } }) export class ExternalDialogWrapperComponent { @Input() component?: Type; @Input() componentInputs: { [name: string]: any } = {}; actions?: TemplateRef | undefined; container?: TemplateRef | undefined; content?: TemplateRef | undefined; @ViewChild(RenderComponentDirective, {static: false}) renderComponentDirective?: RenderComponentDirective; constructor( protected cd: ChangeDetectorRef, public injector: Injector, @Inject(ELECTRON_WINDOW) electron: any, ) { } public setDialogContainer(container: TemplateRef | undefined) { this.container = container; this.cd.detectChanges(); } } @Component({ selector: 'dui-external-dialog', template: ` `, }) export class ExternalWindowComponent implements AfterViewInit, OnDestroy, OnChanges { private portalHost?: PortalHost; @Input() alwaysRaised: boolean = false; @Input() visible: boolean = true; @Output() visibleChange = new EventEmitter(); @Output() closed = new EventEmitter(); @Input() component?: Type; @Input() componentInputs: { [name: string]: any } = {}; public wrapperComponentRef?: ComponentRef; @ViewChild('template', {static: false}) template?: TemplateRef; externalWindow?: Window; container?: TemplateRef | undefined; observerStyles?: MutationObserver; observerClass?: MutationObserver; parentFocusSub?: Subscription; electronWindow?: any; parentWindow?: WindowComponent; constructor( protected componentFactoryResolver: ComponentFactoryResolver, protected applicationRef: ApplicationRef, protected injector: Injector, protected dialog: DuiDialog, protected registry: WindowRegistry, protected cd: ChangeDetectorRef, protected viewContainerRef: ViewContainerRef, ) { } ngOnChanges(changes: SimpleChanges): void { if (this.visible) { this.show(); } else { this.close(); } } public setDialogContainer(container: TemplateRef | undefined) { this.container = container; if (this.wrapperComponentRef) { this.wrapperComponentRef.instance.setDialogContainer(container); } } public show() { if (this.externalWindow) { this.electronWindow.focus(); return; } this.externalWindow = PopupCenter('', '', 300, 300); if (!this.externalWindow) { this.dialog.alert('Error', 'Could not open window.'); return; } this.externalWindow.onunload = () => { this.close(); }; const cloned = new Map(); for (let i = 0; i < window.document.styleSheets.length; i++) { const style = window.document.styleSheets[i]; if (!style.ownerNode) continue; const clone: Node = style.ownerNode.cloneNode(true); cloned.set(style.ownerNode, clone); this.externalWindow!.document.head!.appendChild(clone); } this.observerStyles = new MutationObserver((mutations: MutationRecord[]) => { for (const mutation of mutations) { for (let i = 0; i < mutation.addedNodes.length; i++) { const node = mutation.addedNodes[i]; if (!cloned.has(node)) { const clone: Node = node.cloneNode(true); cloned.set(node, clone); this.externalWindow!.document.head!.appendChild(clone); } } for (let i = 0; i < mutation.removedNodes.length; i++) { const node = mutation.removedNodes[i]; if (cloned.has(node)) { const clone = cloned.get(node)!; clone.parentNode!.removeChild(clone); cloned.delete(node); } } } }); this.observerStyles.observe(window.document.head!, { childList: true, }); const copyBodyClass = () => { if (this.externalWindow) { this.externalWindow.document.body.className = window.document.body.className; } }; this.observerClass = new MutationObserver((mutations: MutationRecord[]) => { copyBodyClass(); }); this.observerClass.observe(window.document.body, { attributeFilter: ['class'] }); const document = this.externalWindow!.document; copyBodyClass(); this.electronWindow = Electron.isAvailable() ? Electron.getRemote().BrowserWindow.getAllWindows()[0] : undefined; this.parentWindow = this.registry.getOuterActiveWindow(); if (this.parentWindow && this.alwaysRaised) { this.parentWindow.windowState.disableInputs.next(true); if (this.parentWindow.electronWindow) { this.electronWindow.setParentWindow(this.parentWindow.electronWindow) } } window.addEventListener('beforeunload', () => { this.beforeUnload(); }); this.portalHost = new DomPortalHost( document.body, this.componentFactoryResolver, this.applicationRef, this.injector ); document.addEventListener('click', () => detectChangesNextFrame()); document.addEventListener('focus', () => detectChangesNextFrame()); document.addEventListener('blur', () => detectChangesNextFrame()); document.addEventListener('keydown', () => detectChangesNextFrame()); document.addEventListener('keyup', () => detectChangesNextFrame()); document.addEventListener('keypress', () => detectChangesNextFrame()); document.addEventListener('mousedown', () => detectChangesNextFrame()); //todo, add beforeclose event and call beforeUnload() to make sure all dialogs are closed when page is reloaded const injector = Injector.create({ parent: this.injector, providers: [ {provide: ExternalWindowComponent, useValue: this}, {provide: ELECTRON_WINDOW, useValue: this.electronWindow}, {provide: IN_DIALOG, useValue: false}, {provide: DOCUMENT, useValue: this.externalWindow!.document}, ], }); const portal = new ComponentPortal(ExternalDialogWrapperComponent, this.viewContainerRef, injector); this.wrapperComponentRef = this.portalHost.attach(portal); this.wrapperComponentRef!.instance.component = this.component!; this.wrapperComponentRef!.instance.componentInputs = this.componentInputs; this.wrapperComponentRef!.instance.content = this.template!; if (this.container) { this.wrapperComponentRef!.instance.setDialogContainer(this.container); } this.visible = true; this.visibleChange.emit(true); this.wrapperComponentRef!.changeDetectorRef.detectChanges(); this.wrapperComponentRef!.location.nativeElement.focus(); this.cd.detectChanges(); } beforeUnload() { if (this.externalWindow) { if (this.portalHost) { this.portalHost.detach(); this.portalHost.dispose(); delete this.portalHost; } this.closed.emit(); if (this.parentFocusSub) this.parentFocusSub.unsubscribe(); if (this.parentWindow && this.alwaysRaised) { this.parentWindow.windowState.disableInputs.next(false); } if (this.externalWindow) { this.externalWindow!.close(); } delete this.externalWindow; this.observerStyles!.disconnect(); this.observerClass!.disconnect(); } } ngAfterViewInit() { } public close() { this.visible = false; this.visibleChange.emit(false); this.beforeUnload(); requestAnimationFrame(() => { this.applicationRef.tick(); }); } ngOnDestroy(): void { this.beforeUnload(); } } /** * This directive is necessary if you want to load and render the dialog content * only when opening the dialog. Without it it is immediately render, which can cause * performance and injection issues. */ @Directive({ 'selector': '[externalDialogContainer]', }) export class ExternalDialogDirective { constructor(protected dialog: ExternalWindowComponent, public template: TemplateRef) { this.dialog.setDialogContainer(this.template); } }