app.component.ts 22.22 KiB
import { Component, OnInit, OnDestroy, HostListener, ViewChild } from "@angular/core";
import { Router, Event, NavigationEnd, ActivationEnd, NavigationStart, NavigationCancel, NavigationError } from "@angular/router";
import { MatDialog } from "@angular/material/dialog";
import { MatSidenav } from "@angular/material/sidenav";
import { MatToolbar } from "@angular/material/toolbar";
import { CdkDragDrop, moveItemInArray } from "@angular/cdk/drag-drop";
import { Observer, jalhydDateRev, jalhydVersion, CalculatorType, Session } from "jalhyd";
import { environment } from "../environments/environment";
import { I18nService } from "./services/internationalisation.service";
import { ErrorService } from "./services/error.service";
import { FormulaireService } from "./services/formulaire.service";
import { FormulaireDefinition } from "./formulaire/definition/form-definition";
import { ServiceFactory } from "./services/service-factory";
import { HttpService } from "./services/http.service";
import { ApplicationSetupService } from "./services/app-setup.service";
import { nghydDateRev, nghydVersion } from "../date_revision";
import { DialogConfirmCloseCalcComponent } from "./components/dialog-confirm-close-calc/dialog-confirm-close-calc.component";
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 { QuicknavComponent } from "./components/quicknav/quicknav.component";
import { NotificationsService } from "./services/notifications.service";
import { HotkeysService, Hotkey } from "angular2-hotkeys";
import * as pako from "pako";
@Component({
  selector: "nghyd-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.scss"],
  providers: [ErrorService]
export class AppComponent implements OnInit, OnDestroy, Observer {
  @ViewChild("sidenav", { static: false })
  public sidenav: MatSidenav;
  @ViewChild("navbar", { static: false })
  public navbar: MatToolbar;
  /** current calculator, inferred from _currentFormId by setActiveCalc() (used for navbar menu) */
  public currentCalc: any;
  /** shows or hides the progressbar under the navbar */
  public showProgressBar = false;
  /** if true, progress bar will be in "determinate" mode, else in "indeterminate" mode */
  public progressBarDeterminate = true;
  /** progress bar percentage, for "determinate" mode */
  public progessBarValue = 0;
  /** liste des modules de calcul ouverts */
  private _calculators: Array<{
    title: string,
    type: CalculatorType,
    uid: string,
    active?: boolean,
    latestAnchor?: string
  }> = [];
  /**
   * id du formulaire courant
   * on utilise pas directement FormulaireService.currentFormId pour éviter l'erreur
   * ExpressionChangedAfterItHasBeenCheckedError
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
private _currentFormId: string; private _innerWidth: number; constructor( private intlService: I18nService, private appSetupService: ApplicationSetupService, 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 confirmCloseCalcDialog: MatDialog, private hotkeysService: HotkeysService ) { 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) => { // show loading bar when changing route if (event instanceof NavigationStart) { this.showLoading(true); } // close side navigation when clicking a calculator tab if (event instanceof NavigationEnd) { this.sidenav.close(); window.scrollTo(0, 0); this.showLoading(false); } // [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); } } // hide loading bar on routing errors if (event instanceof NavigationCancel || event instanceof NavigationError) { this.showLoading(false); } }); // hotkeys listeners this.hotkeysService.add(new Hotkey("alt+s", AppComponent.onHotkey(this.saveForm, this))); this.hotkeysService.add(new Hotkey("alt+o", AppComponent.onHotkey(this.loadSession, this))); this.hotkeysService.add(new Hotkey("alt+q", AppComponent.onHotkey(this.emptySession, this))); this.hotkeysService.add(new Hotkey("alt+n", AppComponent.onHotkey(this.toList, this))); } /** * Wrapper for hotkeys triggers, that executes given function only if * hotkeys are enabled in app preferences * @param func function to execute when hotkey is entered */ public static onHotkey(func: any, that: any) { return (event: KeyboardEvent): boolean => {
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
if (ServiceFactory.instance.applicationSetupService.enableHotkeys) { func.call(that); return false; // Prevent bubbling } else { console.log("Hotkeys are disabled in app preferences"); } }; } /** * 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 uitextSidenavDiagram() { return this.intlService.localizeText("INFO_MENU_DIAGRAM_TITLE"); } public get uitextSidenavSessionProps() { return this.intlService.localizeText("INFO_MENU_SESSION_PROPS"); } 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"); }
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
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 get progressBarMode() { return this.progressBarDeterminate ? "determinate" : "indeterminate"; } 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]; } /** * Close calculator using middle click on tab */ public onMouseUp(event: any, uid: string) { if (event.which === 2) { const dialogRef = this.confirmCloseCalcDialog.open( DialogConfirmCloseCalcComponent, { data: { uid: uid }, disableClose: true } ); dialogRef.afterClosed().subscribe(result => { if (result) { this.formulaireService.requestCloseForm(uid); } }); } } /** * 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) {
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
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); } private showLoading(show: boolean) { this.showProgressBar = show; this.progressBarDeterminate = ! show; } public get enableHeaderDoc(): boolean { return this.currentRoute === "/list" && this._calculators.length === 0; } public get enableSaveSessionMenu(): boolean { return this._calculators.length > 0; } public get enableModulesDiagramMenu(): boolean { return this._calculators.length > 0; } public get enableSessionPropertiesMenu(): boolean { return this._calculators.length > 0; } public get enableEmptySessionMenu(): boolean { return this._calculators.length > 0; } // 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;
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
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: string) { const session: string = this.buildSessionFile(calcList); this.formulaireService.downloadTextFile(session, filename); } /** * Builds a session file including Nubs, GUI-specific Nubs metadata, * model settings, GUI settings * @param calcList Nubs to save */ private buildSessionFile(calcList: any[]): string { 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 settings = { precision: this.appSetupService.computePrecision, maxIterations: this.appSetupService.maxIterations,
421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
displayPrecision: this.appSetupService.displayPrecision, }; return Session.getInstance().serialise(serialiseOptions, settings); } /** * 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"]); } public toDiagram() { this.router.navigate(["/diagram"]); } public toCalc(id: string) { this.router.navigate(["/calculator", id]); this.setActiveCalc(id); setTimeout(() => { // @WARNING clodo trick to wait for Angular refresh this.scrollToLatestQuicknav(id); }, 50); } /** * restarts a fresh session by closing all calculators */ public emptySession() {
491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560
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: false } ); dialogRef.afterClosed().subscribe(result => { if (result) { if (result.emptySession) { this.doEmptySession(); } this.loadSessionFile(result.file, result.calculators); } }); } public loadSessionFile(f: File, info?: any) { this.formulaireService.loadSession(f, info) .then((data) => { if (data.hasErrors) { this.notificationsService.notify(this.intlService.localizeText("ERROR_PROBLEM_LOADING_SESSION"), 3500); } else { if (data.loaded && data.loaded.length > 0) { if (data.loaded.length > 1) { this.toDiagram(); } else { this.toCalc(data.loaded[0]); } } } }) .catch((err) => { this.notificationsService.notify(this.intlService.localizeText("ERROR_LOADING_SESSION"), 3500); console.error("error loading session - ", err); // rollback to ensure session is clean this.doEmptySession(); }); } /** * 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 = "bug@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
561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630
// get all forms const list = []; for (const c of this._calculators) { list.push({ title: c.title, uid: c.uid, selected: true }); } let session = this.buildSessionFile(list); // compress session = pako.deflate(session, { to: "string" }); // gzip (zlib) session = btoa(session); // base64 body += session + "\n"; 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"]; const nub = Session.getInstance().findNubByUid(uid); let required = nub.getTargettedNubs().map((req) => { return req.uid; }); required = required.filter( (item, index) => required.indexOf(item) === index // deduplicate ); list.push({ "children": nub.getChildren().map((child) => { return child.uid; }), "requires": required, "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: {
631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
calculators: list }, disableClose: false } ); 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); } }); } /** * Moves the view to one of the Quicknav anchors in the page, and saves this anchor * as the latest visited, in _calculators list * @param itemId a Quicknav anchor id (ex: "input" or "results") */ public scrollToQuicknav(itemId: string, behavior: ScrollBehavior = "smooth") { const idx = this.getCalculatorIndexFromId(this.currentFormId); if (idx > -1) { const id = QuicknavComponent.prefix + itemId; // Scroll https://stackoverflow.com/a/56391657/5986614 const element = document.getElementById(id); if (element && element.offsetParent !== null) { // offsetParent is null when element is not visible const yCoordinate = element.getBoundingClientRect().top + window.pageYOffset; window.scrollTo({ top: yCoordinate - 60, // substract a little more than navbar height behavior: behavior }); // Save position this._calculators[idx].latestAnchor = itemId; } else { throw Error("scrollToQuicknav: cannot find anchor " + id); } } } /** * Moves the view to the latest known Quicknav anchor of the current module */ public scrollToLatestQuicknav(formId: string) { // Get position const idx = this.getCalculatorIndexFromId(formId); if (idx > -1) { const itemId = this._calculators[idx].latestAnchor; // Scroll if (itemId) { this.scrollToQuicknav(itemId, "auto"); } } } public dropCalcButton(event: CdkDragDrop<string[]>) { moveItemInArray(this.calculators, event.previousIndex, event.currentIndex); } /** * détection de la fermeture de la page/navigateur et demande de confirmation */ @HostListener("window:beforeunload", [ "$event" ]) confirmExit($event) { if (
701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
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 !"; } } /** * Disable value modification on mouse wheel or up/down arrows, in input type="number" */ /* @HostListener("mousewheel", [ "$event" ]) onMouseWheelChrome(event: any) { this.disableScroll(event); } @HostListener("DOMMouseScroll", [ "$event" ]) onMouseWheelFirefox(event: any) { this.disableScroll(event); } @HostListener("onmousewheel", [ "$event" ]) onMouseWheelIE(event: any) { this.disableScroll(event); } disableScroll(event: any) { if (event.srcElement.type === "number") { event.preventDefault(); // @TODO how to send event to parent (or Window ?) so that scrolling the page works ? } } */ @HostListener("keydown", [ "$event" ]) onKeydown(event: any) { if (event.which === 38 || event.which === 40) { // up / down arrow event.preventDefault(); } } }