diff --git a/src/lib/dialog/base-dialog.component.ts b/src/lib/dialog/base-dialog.component.ts index 13d1e7b..a361d0f 100644 --- a/src/lib/dialog/base-dialog.component.ts +++ b/src/lib/dialog/base-dialog.component.ts @@ -1,5 +1,15 @@ -import { Directive, HostListener } from '@angular/core'; -import { IqserEventTarget } from '../utils'; +import { Directive, HostListener, Injector, OnInit } from '@angular/core'; +import { MatDialogRef } from '@angular/material/dialog'; +import { FormGroup } from '@angular/forms'; +import * as moment from 'moment'; +import { AutoUnsubscribe, IqserEventTarget } from '../utils'; +import { ConfirmOptions } from '../misc'; +import { ConfirmationDialogService } from './confirmation-dialog.service'; + +export interface SaveOptions { + closeAfterSave?: boolean; + addMembers?: boolean; +} @Directive() /** @@ -12,12 +22,87 @@ import { IqserEventTarget } from '../utils'; * Make sure to remove the (submit)="save()" property from the form and to set type="button" on the save button * (otherwise the save request will be triggered twice). * */ -export abstract class BaseDialogComponent { - abstract changed: boolean; - abstract valid: boolean; - abstract disabled: boolean; +export abstract class BaseDialogComponent extends AutoUnsubscribe implements OnInit { - abstract save(): void; + protected readonly _dialogService: ConfirmationDialogService = this._injector.get(ConfirmationDialogService); + protected _waitingForConfirmation = false; + + form!: FormGroup; + initialFormValue; + + constructor(protected readonly _injector: Injector, protected readonly _dialogRef: MatDialogRef) { + super(); + } + + abstract save(options?: SaveOptions): void; + + ngOnInit(): void { + this.addSubscription = this._dialogRef.backdropClick().subscribe(() => { + this.close(); + }); + } + + get valid(): boolean { + return this.form.valid; + } + + get changed(): boolean { + for (const key of Object.keys(this.form.getRawValue())) { + const initialValue = this.initialFormValue[key]; + const updatedValue = this.form.get(key).value; + + if (initialValue == null && updatedValue != null) { + const updatedValueType = typeof updatedValue; + if (updatedValueType !== 'string' && updatedValueType !== 'boolean') { + return true; + } else if (updatedValueType === 'string' && updatedValue.length > 0) { + return true; + } else if (updatedValueType === 'boolean' && updatedValue === true) { + return true; + } + } else if (initialValue !== updatedValue) { + if (Array.isArray(updatedValue)) { + if (JSON.stringify(initialValue) !== JSON.stringify(updatedValue)) { + return true; + } + } else if (updatedValue instanceof moment) { + if (!moment(updatedValue).isSame(moment(initialValue))) { + return true; + } + } else { + return true; + } + } + } + return false; + } + + get disabled(): boolean { + return !this.valid || !this.changed; + } + + close(): void { + if (this.changed) { + this._openConfirmDialog().then(result => { + if (result in ConfirmOptions) { + if (result === ConfirmOptions.CONFIRM) { + this.save({ closeAfterSave: true }); + } else { + this._dialogRef.close(); + } + } + this._waitingForConfirmation = false; + }); + } else { + this._dialogRef.close(); + } + } + + protected _openConfirmDialog() { + this._waitingForConfirmation = true; + const dialogRef = this._dialogService.openDialog({ disableConfirm: !this.valid }); + return dialogRef.afterClosed().toPromise(); + } @HostListener('window:keydown.Enter', ['$event']) onEnter(event: KeyboardEvent): void { @@ -26,4 +111,11 @@ export abstract class BaseDialogComponent { this.save(); } } + + @HostListener('window:keydown.Escape', ['$event']) + onEscape(): void { + if (!this._waitingForConfirmation) { + this.close(); + } + } } diff --git a/src/lib/dialog/confirmation-dialog.service.ts b/src/lib/dialog/confirmation-dialog.service.ts new file mode 100644 index 0000000..16f8a1e --- /dev/null +++ b/src/lib/dialog/confirmation-dialog.service.ts @@ -0,0 +1,39 @@ +import { Injectable } from '@angular/core'; +import { MatDialog, MatDialogRef } from '@angular/material/dialog'; +import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker'; +import { ConfirmationDialogComponent, ConfirmationDialogInput, DialogConfig, TitleColors } from '@iqser/common-ui'; +import { DialogService } from '../services/dialog.service'; + +type DialogType = 'confirm'; + +@Injectable({ + providedIn: 'root', +}) +export class ConfirmationDialogService extends DialogService { + protected readonly _config: DialogConfig = { + confirm: { + component: ConfirmationDialogComponent, + dialogConfig: { disableClose: false }, + }, + }; + + constructor(protected readonly _dialog: MatDialog) { + super(_dialog); + } + + openDialog(data?): MatDialogRef { + return super.openDialog( + 'confirm', + null, + new ConfirmationDialogInput({ + title: _('confirmation-dialog.unsaved-changes.title'), + question: _('confirmation-dialog.unsaved-changes.question'), + details: _('confirmation-dialog.unsaved-changes.details'), + confirmationText: _('confirmation-dialog.unsaved-changes.confirmation-text'), + discardChangesText: _('confirmation-dialog.unsaved-changes.discard-changes-text'), + disableConfirm: data.disableConfirm, + titleColor: TitleColors.WARN, + }), + ); + } +} diff --git a/src/lib/misc/confirmation-dialog/confirmation-dialog.component.html b/src/lib/misc/confirmation-dialog/confirmation-dialog.component.html index f94bfb0..7b2f309 100644 --- a/src/lib/misc/confirmation-dialog/confirmation-dialog.component.html +++ b/src/lib/misc/confirmation-dialog/confirmation-dialog.component.html @@ -15,8 +15,8 @@
-
+
+ {{ config.discardChangesText }} +
+ +
{{ config.denyText }}
diff --git a/src/lib/misc/confirmation-dialog/confirmation-dialog.component.ts b/src/lib/misc/confirmation-dialog/confirmation-dialog.component.ts index ae4d744..acfb9cf 100644 --- a/src/lib/misc/confirmation-dialog/confirmation-dialog.component.ts +++ b/src/lib/misc/confirmation-dialog/confirmation-dialog.component.ts @@ -10,6 +10,12 @@ export enum TitleColors { WARN = 'warn', } +export enum ConfirmOptions { + CONFIRM = 1, + SECOND_CONFIRM = 2, + DISCARD_CHANGES = 3, +} + export class ConfirmationDialogInput { title?: string; titleColor?: TitleColor; @@ -17,7 +23,9 @@ export class ConfirmationDialogInput { details?: string; confirmationText?: string; alternativeConfirmationText?: string; + discardChangesText?: string; requireInput?: boolean; + disableConfirm?: boolean; denyText?: string; translateParams?: Record; @@ -28,7 +36,9 @@ export class ConfirmationDialogInput { this.details = options?.details || ''; this.confirmationText = options?.confirmationText || _('common.confirmation-dialog.confirm'); this.alternativeConfirmationText = options?.alternativeConfirmationText; + this.discardChangesText = options?.discardChangesText; this.requireInput = options?.requireInput || false; + this.disableConfirm = options?.disableConfirm || false; this.denyText = options?.denyText || _('common.confirmation-dialog.deny'); this.translateParams = options?.translateParams || {}; } @@ -43,6 +53,7 @@ export class ConfirmationDialogComponent { config: ConfirmationDialogInput; inputValue = ''; readonly inputLabel: string; + readonly ConfirmOptions = ConfirmOptions; constructor( private readonly _dialogRef: MatDialogRef, @@ -75,7 +86,7 @@ export class ConfirmationDialogComponent { this._dialogRef.close(); } - confirm(option: number): void { + confirm(option: ConfirmOptions): void { this._dialogRef.close(option); } @@ -86,6 +97,7 @@ export class ConfirmationDialogComponent { 'details', 'confirmationText', 'alternativeConfirmationText', + 'discardChangesText', 'denyText', ]; diff --git a/src/lib/services/dialog.service.ts b/src/lib/services/dialog.service.ts index 58647ca..a63ff35 100644 --- a/src/lib/services/dialog.service.ts +++ b/src/lib/services/dialog.service.ts @@ -9,6 +9,7 @@ export const largeDialogConfig: MatDialogConfig = { maxWidth: '90vw', maxHeight: '90vh', autoFocus: false, + disableClose: true, } as const; export const defaultDialogConfig: MatDialogConfig = { @@ -16,6 +17,7 @@ export const defaultDialogConfig: MatDialogConfig = { maxWidth: '90vw', maxHeight: '90vh', autoFocus: false, + disableClose: true, } as const; export type DialogConfig = {