import { Component, ApplicationRef, OnInit, OnDestroy, HostListener, ViewChild, ComponentRef } from "@angular/core"; import { Router, Event, NavigationEnd, ActivationEnd } from "@angular/router"; import { MatSidenav, MatToolbar, MatDialog, MatIconRegistry } from "@angular/material"; import { DomSanitizer } from "@angular/platform-browser"; import { Observer, jalhydDateRev, jalhydVersion, CalculatorType, Session } from "jalhyd"; import { environment } from "../environments/environment"; import { I18nService } from "./services/internationalisation/internationalisation.service"; import { ErrorService } from "./services/error/error.service"; import { FormulaireService } from "./services/formulaire/formulaire.service"; import { FormulaireDefinition } from "./formulaire/definition/form-definition"; import { ServiceFactory } from "./services/service-factory"; import { HttpService } from "./services/http/http.service"; import { ApplicationSetupService } from "./services/app-setup/app-setup.service"; import { nghydDateRev, nghydVersion } from "../date_revision"; import { DialogConfirmEmptySessionComponent } from "./components/dialog-confirm-empty-session/dialog-confirm-empty-session.component"; import { DialogLoadSessionComponent } from "./components/dialog-load-session/dialog-load-session.component"; import { DialogSaveSessionComponent } from "./components/dialog-save-session/dialog-save-session.component"; import { NotificationsService } from "./services/notifications/notifications.service"; @Component({ selector: "nghyd-app", templateUrl: "./app.component.html", styleUrls: ["./app.component.scss"], providers: [ErrorService] }) export class AppComponent implements OnInit, OnDestroy, Observer { @ViewChild("sidenav") public sidenav: MatSidenav; @ViewChild("navbar") public navbar: MatToolbar; /** current calculator, inferred from _currentFormId by setActiveCalc() (used for navbar menu) */ public currentCalc: any; /** * liste des modules de calcul. Forme des objets : * "title": nom du module de calcul * "uid": id unique du formulaire */ private _calculators: any[] = []; /** * id du formulaire courant * on utilise pas directement FormulaireService.currentFormId pour éviter l'erreur * ExpressionChangedAfterItHasBeenCheckedError */ private _currentFormId: string; private _innerWidth: number; /** * composant actuellement affiché par l'élément <router-outlet> */ private _routerCurrentComponent: Component; constructor( private intlService: I18nService, private appSetupService: ApplicationSetupService, private appRef: ApplicationRef, private errorService: ErrorService, private router: Router, private formulaireService: FormulaireService, private httpService: HttpService, private notificationsService: NotificationsService, private confirmEmptySessionDialog: MatDialog, private saveSessionDialog: MatDialog, private loadSessionDialog: MatDialog, private matIconRegistry: MatIconRegistry, private domSanitizer: DomSanitizer ) { ServiceFactory.instance.httpService = httpService; ServiceFactory.instance.applicationSetupService = appSetupService; ServiceFactory.instance.i18nService = intlService; ServiceFactory.instance.formulaireService = formulaireService; ServiceFactory.instance.notificationsService = notificationsService; this.router.events.subscribe((event: Event) => { // close side navigation when clicking a calculator tab if (event instanceof NavigationEnd) { this.sidenav.close(); window.scrollTo(0, 0); } // [de]activate calc tabs depending on loaded route if (event instanceof ActivationEnd) { const path = event.snapshot.url[0].path; if (path === "calculator") { const calcUid = event.snapshot.params.uid; if (this.calculatorExists(calcUid)) { this.setActiveCalc(calcUid); } else { // if required calculator does not exist, redirect to list page this.toList(); } } else { this.setActiveCalc(null); } } }); // icônes personnalisées this.matIconRegistry.addSvgIcon( "file_excel", this.domSanitizer.bypassSecurityTrustResourceUrl("../../assets/icons/file-excel.svg") ); } /** * Triggered at app startup. * Preferences are loaded by app setup service * @see ApplicationSetupService.construct() */ ngOnInit() { this.formulaireService.addObserver(this); this.subscribeErrorService(); this._innerWidth = window.innerWidth; } ngOnDestroy() { this.unsubscribeErrorService(); this.formulaireService.removeObserver(this); } @HostListener("window:resize", ["$event"]) onResize(event) { // keep track of window size for navbar tabs arrangement this._innerWidth = window.innerWidth; } public get uitextSidenavNewCalc() { return this.intlService.localizeText("INFO_MENU_NOUVELLE_CALC"); } public get uitextSidenavParams() { return this.intlService.localizeText("INFO_SETUP_TITLE"); } public get uitextSidenavLoadSession() { return this.intlService.localizeText("INFO_MENU_LOAD_SESSION_TITLE"); } public get uitextSidenavSaveSession() { return this.intlService.localizeText("INFO_MENU_SAVE_SESSION_TITLE"); } public get uitextSidenavEmptySession() { return this.intlService.localizeText("INFO_MENU_EMPTY_SESSION_TITLE"); } public get uitextSidenavReportBug() { return this.intlService.localizeText("INFO_MENU_REPORT_BUG"); } public get uitextSidenavHelp() { return this.intlService.localizeText("INFO_MENU_HELP_TITLE"); } public get uitextSelectCalc() { return this.intlService.localizeText("INFO_MENU_SELECT_CALC"); } public getCalculatorLabel(t: CalculatorType) { return this.formulaireService.getLocalisedTitleFromCalculatorType(t); } public get calculators() { return this._calculators; } public get currentFormId() { return this._currentFormId; } public get currentRoute(): string { return this.router.url; } public setActiveCalc(uid: string) { this._calculators.forEach((calc) => { calc.active = (calc.uid === uid); }); // mark current calc for navbar menu const index = this.getCalculatorIndexFromId(uid); this.currentCalc = this._calculators[index]; } /** * Returns true if sum of open calculator tabs witdh is lower than navbar * available space (ie. if navbar is not overflowing), false otherwise */ public get tabsFitInNavbar() { // manual breakpoints // @WARNING keep in sync with .calculator-buttons sizes in app.component.scss let tabsLimit = 0; if (this._innerWidth > 480) { tabsLimit = 3; } if (this._innerWidth > 640) { tabsLimit = 4; } if (this._innerWidth > 800) { tabsLimit = 6; } /*if (this._innerWidth > 1200) { tabsLimit = 8; }*/ const fits = this._calculators.length <= tabsLimit; return fits; } /** * abonnement au service d'erreurs */ private subscribeErrorService() { this.errorService.addObserver(this); } private unsubscribeErrorService() { this.errorService.removeObserver(this); } // interface Observer update(sender: any, data: any): void { if (sender instanceof FormulaireService) { switch (data["action"]) { case "createForm": // add newly created form to calculators list const f: FormulaireDefinition = data["form"]; this._calculators.push( { "title": f.calculatorName, "type": f.calculatorType, "uid": f.uid } ); // abonnement en tant qu'observateur du nouveau formulaire f.addObserver(this); break; case "invalidFormId": this.toList(); break; case "currentFormChanged": this._currentFormId = data["formId"]; break; case "saveForm": this.saveForm(data["form"]); break; case "closeForm": const form: FormulaireDefinition = data["form"]; this.closeCalculator(form); break; } } else if (sender instanceof FormulaireDefinition) { switch (data["action"]) { case "nameChanged": this.updateCalculatorTitle(sender, data["name"]); break; } } } /** * Returns true if a form having "formUid" as UID exists * @param formId UID to look for */ private calculatorExists(formId: string): boolean { return (this.getCalculatorIndexFromId(formId) > -1); } private getCalculatorIndexFromId(formId: string) { const index = this._calculators.reduce((resultIndex, calc, currIndex) => { if (resultIndex === -1 && calc["uid"] === formId) { resultIndex = currIndex; } return resultIndex; }, -1); return index; } private updateCalculatorTitle(f: FormulaireDefinition, title: string) { const formIndex = this.getCalculatorIndexFromId(f.uid); this._calculators[formIndex]["title"] = title; } /** * Saves a JSON serialised session file, for one or more calc modules * @param calcList modules to save * @param filename */ private saveSession(calcList: any[], filename) { const elems = []; const serialiseOptions: { [key: string]: {} } = {}; for (const c of calcList) { if (c.selected) { serialiseOptions[c.uid] = { // GUI-dependent metadata to add to the session file title: c.title }; } } const session: string = Session.getInstance().serialise(serialiseOptions); this.formulaireService.downloadTextFile(session, filename); } /** * Supprime un module de calcul **de l'interface** * ATTENTION, ne supprime pas le module de calcul en mémoire ! * Pour cela, utiliser FormulaireService.requestCloseForm(form.uid); * @param form module de calcul à fermer */ private closeCalculator(form: FormulaireDefinition) { const formId: string = form.uid; // désabonnement en tant qu'observateur form.removeObserver(this); // recherche du module de calcul correspondant à formId const closedIndex = this.getCalculatorIndexFromId(formId); /* * détermination du nouveau module de calcul à afficher : * - celui après celui supprimé * - ou celui avant celui supprimé si on supprime le dernier */ let newId = null; const l = this._calculators.length; if (l > 1) { if (closedIndex === l - 1) { newId = this._calculators[closedIndex - 1]["uid"]; } else { newId = this._calculators[closedIndex + 1]["uid"]; } } // suppression this._calculators = this._calculators.filter(calc => { return formId !== calc["uid"]; }); // MAJ affichage if (newId === null) { this.toList(); this._currentFormId = null; } else { this.toCalc(newId); } } private toList() { this.router.navigate(["/list"]); } private toCalc(id: string) { this.router.navigate(["/calculator", id]); this.setActiveCalc(id); } /** * récupération du composant affiché par le routeur */ public onRouterOutletActivated(a) { this._routerCurrentComponent = a; } /** * restarts a fresh session by closing all calculators */ public emptySession() { const dialogRef = this.confirmEmptySessionDialog.open( DialogConfirmEmptySessionComponent, { disableClose: true } ); dialogRef.afterClosed().subscribe(result => { if (result) { this.doEmptySession(); } }); } public doEmptySession() { for (const c of this._calculators) { const form = this.formulaireService.getFormulaireFromId(c.uid); this.formulaireService.requestCloseForm(form.uid); } // just to be sure, get rid of any Nub possibly stuck in session without any form attached Session.getInstance().clear(); } public loadSession() { // création du dialogue de sélection des formulaires à sauver const dialogRef = this.loadSessionDialog.open( DialogLoadSessionComponent, { disableClose: true } ); dialogRef.afterClosed().subscribe(result => { if (result) { if (result.emptySession) { this.doEmptySession(); } this.formulaireService.loadSession(result.file, result.calculators); } }); } /** * Demande au client d'envoyer un email (génère un lien mailto:), pré-rempli * avec un texte standard, et le contenu de la session au format JSON */ public reportBug() { const recipient = "cassiopee@g-eau.fr"; const subject = "[ISSUE] " + this.intlService.localizeText("INFO_REPORT_BUG_SUBJECT"); let body = this.intlService.localizeText("INFO_REPORT_BUG_BODY"); // add session description body += Session.getInstance().serialise(); body = encodeURIComponent(body); const mailtoURL = `mailto:${recipient}?subject=${subject}&body=${body}`; // temporarily disable tab closing alert, as tab won't be closed for real this.appSetupService.warnBeforeTabClose = false; window.location.href = mailtoURL; this.appSetupService.warnBeforeTabClose = true; } public get revisionInfo(): any { return { jalhyd: { date: jalhydDateRev, version: jalhydVersion, }, nghyd: { date: nghydDateRev, version: nghydVersion } }; } /** * sauvegarde du/des formulaires * @param form formulaire à sélectionner par défaut dans la liste */ public saveForm(form?: FormulaireDefinition) { // liste des formulaires const list = []; for (const c of this._calculators) { const uid = c["uid"]; list.push({ "selected": form ? (uid === form.uid) : true, "title": c["title"], "uid": uid }); } // dialogue de sélection des formulaires à sauver const dialogRef = this.saveSessionDialog.open( DialogSaveSessionComponent, { data: { calculators: list }, disableClose: true } ); dialogRef.afterClosed().subscribe(result => { if (result) { let name = result.filename; // ajout extension ".json" const re = /.+\.json/; const match = re.exec(name.toLowerCase()); if (match === null) { name = name + ".json"; } this.saveSession(result.calculators, name); } }); } /** * détection de la fermeture de la page/navigateur et demande de confirmation */ @HostListener("window:beforeunload", ["$event"]) confirmExit($event) { if ( this.appSetupService.warnBeforeTabClose && environment.production // otherwise prevents dev server to reload app after recompiling ) { // affecter une valeur différente de null provoque l'affichage d'un dialogue de confirmation, mais le texte n'est pas affiché $event.returnValue = "Your data will be lost !"; } } }