import { AfterViewInit, Directive, HostListener, inject, OnDestroy, signal } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { debounceTime, firstValueFrom, fromEvent, merge, of, Subscription } from 'rxjs'; import { tap } from 'rxjs/operators'; import { ConfirmOptions } from '.'; import { IconButtonTypes } from '../buttons'; import { LoadingService } from '../loading'; import { Toaster } from '../services'; import { hasFormChanged, IqserEventTarget } from '../utils'; import { ConfirmationDialogService } from './confirmation-dialog.service'; const TARGET_NODE = 'mat-dialog-container'; export interface SaveOptions { closeAfterSave?: boolean; addMembers?: boolean; } @Directive() export abstract class BaseDialogComponent implements AfterViewInit, OnDestroy { readonly #confirmationDialogService = inject(ConfirmationDialogService); readonly #dialog = inject(MatDialog); readonly #hasErrors = signal(true); protected readonly _formBuilder = inject(UntypedFormBuilder); protected readonly _loadingService = inject(LoadingService); protected readonly _toaster = inject(Toaster); protected readonly _subscriptions = new Subscription(); readonly iconButtonTypes = IconButtonTypes; form?: UntypedFormGroup; initialFormValue!: Record; protected constructor( protected readonly _dialogRef: MatDialogRef, private readonly _isInEditMode = false, ) {} get valid(): boolean { return !this.form || this.form.valid; } get changed(): boolean { return !this.form || hasFormChanged(this.form, this.initialFormValue); } get disabled(): boolean { return !this.valid || !this.changed || this.#hasErrors(); } ngAfterViewInit() { this._subscriptions.add(this._dialogRef.backdropClick().subscribe(() => this.close())); const valueChanges = this.form?.valueChanges ?? of(null); const events = [fromEvent(window, 'keyup'), fromEvent(window, 'input'), valueChanges]; const events$ = merge(...events).pipe( debounceTime(10), tap(() => { this.#hasErrors.set(!!document.getElementsByClassName('ng-invalid')[0]); }), ); this._subscriptions.add(events$.subscribe()); } abstract save(options?: SaveOptions): void; ngOnDestroy(): void { this._subscriptions.unsubscribe(); } close() { if (!this._isInEditMode || !this.changed) { return this._dialogRef.close(); } this._openConfirmDialog().then(result => { if (result) { if (result === ConfirmOptions.CONFIRM) { this.save({ closeAfterSave: true }); } else { this._dialogRef.close(); } } }); } @HostListener('window:keydown.Enter', ['$event']) onEnter(event: KeyboardEvent): void { event?.stopImmediatePropagation(); const node = (event.target as IqserEventTarget).localName?.trim()?.toLowerCase(); if (this.valid && !this.disabled && this.changed && node === TARGET_NODE) { this.save(); } } @HostListener('window:keydown.Escape', ['$event']) onEscape(): void { if (this.#dialog.openDialogs.length === 1) { this.close(); } } protected _openConfirmDialog() { const dialogRef = this.#confirmationDialogService.open({ disableConfirm: !this.valid }); return firstValueFrom(dialogRef.afterClosed()); } }