-
Mathias Chouet authored
Markdown documentation remove dependency to ngx-md add dependency to ngx-markdown
13221038
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();
}
}
}