@sumaris-net/ngx-components 18.0.7 → 18.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8674,14 +8674,7 @@ class MatLatLongField {
8674
8674
  else {
8675
8675
  this.formControl.setValidators(this.type === 'latitude' ? SharedValidators.latitude : SharedValidators.longitude);
8676
8676
  }
8677
- this._subscription.add(this.signFormControl.valueChanges.subscribe(() => {
8678
- const strValue = this.textFormControl.value;
8679
- const parsedValue = this.parseValue(strValue);
8680
- // TODO Need to check this or delegate the work on validator
8681
- // if (this.formControl.valid) {
8682
- // }
8683
- this.formControl.patchValue(parsedValue, { emitEvent: false });
8684
- }));
8677
+ this._subscription.add(merge(this.textFormControl.valueChanges, this.signFormControl.valueChanges).subscribe(() => this.onFormChange()));
8685
8678
  // Listen status changes (when done outside the component - e.g. when setErrors() is calling on the formControl)
8686
8679
  this._subscription.add(this.formControl.statusChanges
8687
8680
  .pipe(filter(() => !this.readonly && !this._writing && !this._disabling) // Skip
@@ -8773,7 +8766,37 @@ class MatLatLongField {
8773
8766
  _format(value, opts = {}) {
8774
8767
  return formatLatLong(value, this.type, { ...this._formatOptions, ...opts }) ?? this.maskitoPlaceholder;
8775
8768
  }
8769
+ _checkIfTouched() {
8770
+ if (this.textFormControl.touched) {
8771
+ this._onTouchedCallback();
8772
+ this.markForCheck();
8773
+ }
8774
+ }
8776
8775
  /* -- private method -- */
8776
+ onFormChange() {
8777
+ if (this._writing)
8778
+ return; // Skip if call by self
8779
+ this._writing = true;
8780
+ let strValue = this.textFormControl.value;
8781
+ // Reset mask if value is empty
8782
+ if (!/\d/.test(strValue)) {
8783
+ strValue = null;
8784
+ }
8785
+ const parsedValue = this.parseValue(strValue);
8786
+ this.emitChange(parsedValue);
8787
+ this._writing = false;
8788
+ this.markForCheck();
8789
+ }
8790
+ emitChange(value) {
8791
+ const formControlValue = this.formControl.value;
8792
+ if (formControlValue !== value) {
8793
+ if (this.textFormControl.valid) {
8794
+ this.formControl.setValue(value, { emitEvent: false });
8795
+ this._onChangeCallback(value);
8796
+ this._checkIfTouched();
8797
+ }
8798
+ }
8799
+ }
8777
8800
  updateTranslations(translation) {
8778
8801
  // No change
8779
8802
  if (this.maskitoPlaceholder === translation)
@@ -14194,6 +14217,7 @@ class Hotkeys {
14194
14217
  modalController;
14195
14218
  popoverController;
14196
14219
  _debug = false;
14220
+ _id = 0;
14197
14221
  _hotkeys = new Map();
14198
14222
  _document = inject(DOCUMENT);
14199
14223
  _defaults = {
@@ -14201,9 +14225,13 @@ class Hotkeys {
14201
14225
  preventDefault: true,
14202
14226
  };
14203
14227
  _dialogRef;
14228
+ _dialogIsOpened = false;
14204
14229
  _config;
14205
14230
  defaultControlKey;
14206
14231
  defaultControlKeyName;
14232
+ get isHelpModalOpened() {
14233
+ return !!this._dialogRef;
14234
+ }
14207
14235
  constructor(platform, eventManager, dialog, modalController, popoverController, config) {
14208
14236
  this.eventManager = eventManager;
14209
14237
  this.dialog = dialog;
@@ -14216,9 +14244,9 @@ class Hotkeys {
14216
14244
  };
14217
14245
  // Add shortcut to open help modal
14218
14246
  if (this._config.enableHelpModal) {
14219
- this.addShortcut({ keys: this._config.openHelpModalShortcut }).subscribe(() => this.openHelpModal());
14220
14247
  if (this._debug)
14221
14248
  console.debug(`[hotkeys] Starting hotkeys service... Press ${this._config.openHelpModalShortcut} to get help modal.`);
14249
+ this.addShortcut({ keys: this._config.openHelpModalShortcut }).subscribe(() => this.toggleHelpModal());
14222
14250
  }
14223
14251
  const isIOS = platform.is('ios');
14224
14252
  this.defaultControlKey = isIOS ? 'meta' : 'control';
@@ -14228,11 +14256,14 @@ class Hotkeys {
14228
14256
  }
14229
14257
  addShortcut(options) {
14230
14258
  const merged = { ...this._defaults, ...options };
14259
+ if (!merged.keys)
14260
+ throw new Error("Missing 'keys' or 'description' attribute");
14231
14261
  const event = `keydown.${merged.keys}`;
14262
+ const id = this._id++;
14263
+ this._hotkeys.set(id, merged);
14232
14264
  if (isNotNilOrBlank(merged.description)) {
14233
14265
  if (this._debug)
14234
14266
  console.debug(`[hotkeys] Add shortcut {${options.keys}}: ${merged.description}`);
14235
- this._hotkeys.set(merged.keys, merged.description);
14236
14267
  }
14237
14268
  else {
14238
14269
  if (this._debug)
@@ -14258,27 +14289,48 @@ class Hotkeys {
14258
14289
  const dispose = this.eventManager.addEventListener(merged.element, event, handler);
14259
14290
  return () => {
14260
14291
  dispose();
14261
- this._hotkeys.delete(merged.keys);
14292
+ this._hotkeys.delete(id);
14262
14293
  };
14263
14294
  });
14264
14295
  }
14265
- openHelpModal() {
14266
- if (this._dialogRef) {
14267
- this._dialogRef.close(true);
14268
- return;
14296
+ toggleHelpModal() {
14297
+ if (this.isHelpModalOpened) {
14298
+ this.closeHelpModal();
14269
14299
  }
14300
+ else {
14301
+ this.openHelpModal();
14302
+ }
14303
+ }
14304
+ openHelpModal() {
14305
+ if (this.isHelpModalOpened)
14306
+ return; // Skip if already opened
14307
+ // Get keys with a not blank description
14308
+ const hotkeys = Array.from(this._hotkeys.values()).reduce((map, item) => {
14309
+ if (isNotNilOrBlank(item?.description)) {
14310
+ map.set(item.keys, item.description);
14311
+ }
14312
+ return map;
14313
+ }, new Map());
14314
+ if (hotkeys.size === 0)
14315
+ return; // Nothing to display
14270
14316
  this._dialogRef = this.dialog.open(HotkeysDialogComponent, {
14271
14317
  width: '500px',
14272
- data: this._hotkeys,
14318
+ data: hotkeys,
14273
14319
  });
14274
14320
  this._dialogRef
14275
14321
  .afterClosed()
14276
- .pipe(first())
14322
+ .pipe(first(),
14323
+ // Avoid too many call (e.g. in Safari)
14324
+ delay(250))
14277
14325
  .subscribe(() => {
14278
14326
  // Forget the dialog ref
14279
14327
  this._dialogRef = null;
14328
+ console.debug('[hotkeys] Closing help modal');
14280
14329
  });
14281
14330
  }
14331
+ closeHelpModal() {
14332
+ this._dialogRef?.close(true);
14333
+ }
14282
14334
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: Hotkeys, deps: [{ token: i2$1.Platform }, { token: i1$2.EventManager }, { token: i3$1.MatDialog }, { token: i2$1.ModalController }, { token: i2$1.PopoverController }, { token: APP_HOTKEYS_CONFIG, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
14283
14335
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: Hotkeys, providedIn: 'root' });
14284
14336
  }
@@ -14928,6 +14980,11 @@ const CORE_CONFIG_OPTIONS = Object.freeze({
14928
14980
  label: 'CONFIGURATION.OPTIONS.FORUM_URL',
14929
14981
  type: 'string',
14930
14982
  },
14983
+ REPORT_ISSUE_URL: {
14984
+ key: 'sumaris.reportIssue.url',
14985
+ label: 'CONFIGURATION.OPTIONS.REPORT_ISSUE_URL',
14986
+ type: 'string',
14987
+ },
14931
14988
  LOGO_LARGE: {
14932
14989
  key: 'sumaris.logo.large',
14933
14990
  label: 'CONFIGURATION.OPTIONS.HOME.LOGO_LARGE',
@@ -22237,7 +22294,7 @@ class ModalToolbarComponent {
22237
22294
  this.canValidate = toBoolean(this.canValidate, this.validate.observed);
22238
22295
  // Ctrl+S
22239
22296
  this._subscription.add(this.hotkeys
22240
- .addShortcut({ keys: 'control.s', description: 'COMMON.BTN_VALIDATE', elementName: this.modalName })
22297
+ .addShortcut({ keys: `${this.hotkeys.defaultControlKey}.s`, description: 'COMMON.BTN_VALIDATE', elementName: this.modalName })
22241
22298
  .pipe(filter(() => this.canValidate))
22242
22299
  .subscribe((event) => this.validate.emit(event)));
22243
22300
  // Escape
@@ -26428,8 +26485,8 @@ class AboutModal {
26428
26485
  name;
26429
26486
  version;
26430
26487
  sourceUrl;
26431
- reportIssueUrl;
26432
26488
  config$;
26489
+ reportIssueUrl;
26433
26490
  forumUrl;
26434
26491
  helpUrl;
26435
26492
  _subscription = new Subscription();
@@ -26463,13 +26520,14 @@ class AboutModal {
26463
26520
  setConfig(config) {
26464
26521
  this.forumUrl = config?.getProperty(CORE_CONFIG_OPTIONS.FORUM_URL) || this.environment.forumUrl;
26465
26522
  this.helpUrl = config?.getProperty(CORE_CONFIG_OPTIONS.HELP_URL) || this.environment.helpUrl;
26523
+ this.reportIssueUrl = config?.getProperty(CORE_CONFIG_OPTIONS.REPORT_ISSUE_URL) || this.environment.reportIssueUrl;
26466
26524
  }
26467
26525
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AboutModal, deps: [{ token: i1$1.TranslateService }, { token: i2$1.ModalController }, { token: ConfigService }, { token: ENVIRONMENT, optional: true }, { token: APP_ABOUT_DEVELOPERS, optional: true }, { token: APP_ABOUT_PARTNERS, optional: true }], target: i0.ɵɵFactoryTarget.Component });
26468
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.8", type: AboutModal, selector: "app-about-modal", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n <ion-item *ngIf=\"config$ | async; let config\">\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label }}</b>\n -\n <span [innerHTML]=\"config.name\"></span>\n </h2>\n <p *ngIf=\"config.description\">\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <small *ngIf=\"name\">{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n <span *ngIf=\"version\" [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n </h2>\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n <ion-item *ngIf=\"forumUrl || helpUrl\">\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text translate>ABOUT.USER_MANUAL</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text translate>ABOUT.FORUM</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Report issue -->\n <ion-item *ngIf=\"reportIssueUrl\">\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\" translate>ABOUT.BTN_REPORT_ISSUE</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Source code -->\n <ion-item *ngIf=\"sourceUrl\">\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Developers -->\n <ion-item *ngIf=\"developers | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n <ion-item *ngFor=\"let item of developers\" class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n </ion-list>\n </ion-label>\n </ion-item>\n\n <!-- Partners -->\n <ion-item class=\"item-partners\" *ngIf=\"partners | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n <a *ngFor=\"let item of partners\" href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n <img *ngIf=\"item.logo; else partnerName\" [src]=\"item.logo\" [alt]=\"item.label\" />\n <ng-template #partnerName>\n <ion-text>{{ item.label }}</ion-text>\n </ng-template>\n </a>\n </p>\n </ion-text>\n </ion-item>\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"], dependencies: [{ kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: i6$4.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }] });
26526
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.8", type: AboutModal, selector: "app-about-modal", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <small *ngIf=\"name\">{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n <span *ngIf=\"version\" [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n </h2>\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n <ion-item *ngIf=\"forumUrl || helpUrl\">\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text translate>ABOUT.USER_MANUAL</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text translate>ABOUT.FORUM</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Report issue -->\n <ion-item *ngIf=\"reportIssueUrl\">\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\" translate>ABOUT.BTN_REPORT_ISSUE</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Source code -->\n <ion-item *ngIf=\"sourceUrl\">\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Developers -->\n <ion-item *ngIf=\"developers | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n <ion-item *ngFor=\"let item of developers\" class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n </ion-list>\n </ion-label>\n </ion-item>\n\n <!-- Partners -->\n <ion-item class=\"item-partners\" *ngIf=\"partners | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n <a *ngFor=\"let item of partners\" href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n <img *ngIf=\"item.logo; else partnerName\" [src]=\"item.logo\" [alt]=\"item.label\" />\n <ng-template #partnerName>\n <ion-text>{{ item.label }}</ion-text>\n </ng-template>\n </a>\n </p>\n </ion-text>\n </ion-item>\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"], dependencies: [{ kind: "directive", type: i3.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i3.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: i2$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "component", type: i2$1.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$1.IonCol, selector: "ion-col", inputs: ["offset", "offsetLg", "offsetMd", "offsetSm", "offsetXl", "offsetXs", "pull", "pullLg", "pullMd", "pullSm", "pullXl", "pullXs", "push", "pushLg", "pushMd", "pushSm", "pushXl", "pushXs", "size", "sizeLg", "sizeMd", "sizeSm", "sizeXl", "sizeXs"] }, { kind: "component", type: i2$1.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$1.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$1.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$1.IonImg, selector: "ion-img", inputs: ["alt", "src"] }, { kind: "component", type: i2$1.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$1.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$1.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$1.IonRow, selector: "ion-row" }, { kind: "component", type: i2$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$1.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$1.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$1.TranslateDirective, selector: "[translate],[ngx-translate]", inputs: ["translate", "translateParams"] }, { kind: "component", type: i6$4.MarkdownComponent, selector: "markdown, [markdown]", inputs: ["data", "src", "disableSanitizer", "inline", "clipboard", "clipboardButtonComponent", "clipboardButtonTemplate", "emoji", "katex", "katexOptions", "mermaid", "mermaidOptions", "lineHighlight", "line", "lineOffset", "lineNumbers", "start", "commandLine", "filterOutput", "host", "prompt", "output", "user"], outputs: ["error", "load", "ready"] }, { kind: "component", type: i8.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i3.AsyncPipe, name: "async" }, { kind: "pipe", type: i1$1.TranslatePipe, name: "translate" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: CapitalizePipe, name: "capitalize" }] });
26469
26527
  }
26470
26528
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AboutModal, decorators: [{
26471
26529
  type: Component,
26472
- args: [{ selector: 'app-about-modal', template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n <ion-item *ngIf=\"config$ | async; let config\">\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label }}</b>\n -\n <span [innerHTML]=\"config.name\"></span>\n </h2>\n <p *ngIf=\"config.description\">\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <small *ngIf=\"name\">{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n <span *ngIf=\"version\" [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n </h2>\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n <ion-item *ngIf=\"forumUrl || helpUrl\">\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text translate>ABOUT.USER_MANUAL</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text translate>ABOUT.FORUM</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Report issue -->\n <ion-item *ngIf=\"reportIssueUrl\">\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\" translate>ABOUT.BTN_REPORT_ISSUE</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Source code -->\n <ion-item *ngIf=\"sourceUrl\">\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Developers -->\n <ion-item *ngIf=\"developers | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n <ion-item *ngFor=\"let item of developers\" class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n </ion-list>\n </ion-label>\n </ion-item>\n\n <!-- Partners -->\n <ion-item class=\"item-partners\" *ngIf=\"partners | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n <a *ngFor=\"let item of partners\" href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n <img *ngIf=\"item.logo; else partnerName\" [src]=\"item.logo\" [alt]=\"item.label\" />\n <ng-template #partnerName>\n <ion-text>{{ item.label }}</ion-text>\n </ng-template>\n </a>\n </p>\n </ion-text>\n </ion-item>\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"] }]
26530
+ args: [{ selector: 'app-about-modal', template: "<ion-header>\n <ion-toolbar color=\"light\">\n <ion-title>\n {{ 'ABOUT.TITLE' | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button (click)=\"close()\" visible-xs visible-sm visible-mobile>\n {{ 'COMMON.BTN_CLOSE' | translate }}\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n <ion-list lines=\"none\">\n <!-- Peer info -->\n @if (config$ | async; as config) {\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <b>{{ config.label | capitalize }}</b>\n @if (config.name) {\n -\n <span [innerHTML]=\"config.name\"></span>\n }\n </h2>\n\n @if (config.description) {\n <p>\n <markdown [data]=\"config.description\" emoji></markdown>\n </p>\n }\n </ion-label>\n </ion-item>\n }\n\n <!-- Version info -->\n <ion-item>\n <span slot=\"start\">&nbsp;</span>\n <ion-label class=\"ion-text-wrap\">\n <h2>\n <small *ngIf=\"name\">{{ 'ABOUT.POWERED_BY' | translate }}</small>\n {{ name }}\n <span *ngIf=\"version\" [innerHTML]=\"'ABOUT.VERSION' | translate: { version: version }\"></span>\n </h2>\n <p [innerHTML]=\"'ABOUT.LICENSE' | translate\"></p>\n </ion-label>\n </ion-item>\n\n <!-- Help -->\n <ion-item *ngIf=\"forumUrl || helpUrl\">\n <mat-icon slot=\"start\">help_outline</mat-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.HELP' | translate }}\n </p>\n <p>\n <ion-button fill=\"solid\" color=\"accent\" href=\"{{ helpUrl }}\" target=\"help\" size=\"default\">\n <mat-icon slot=\"start\">description</mat-icon>\n <ion-text translate>ABOUT.USER_MANUAL</ion-text>\n </ion-button>\n <ion-button fill=\"solid\" color=\"secondary\" href=\"{{ forumUrl }}\" target=\"forum\" size=\"default\">\n <ion-icon slot=\"start\" name=\"chatbubbles\"></ion-icon>\n <ion-text translate>ABOUT.FORUM</ion-text>\n </ion-button>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Report issue -->\n <ion-item *ngIf=\"reportIssueUrl\">\n <ion-icon slot=\"start\" name=\"bug\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.REPORT_ISSUE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ reportIssueUrl }}\" target=\"_system\" translate>ABOUT.BTN_REPORT_ISSUE</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Source code -->\n <ion-item *ngIf=\"sourceUrl\">\n <ion-icon slot=\"start\" name=\"code\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.SOURCE_CODE' | translate }}\n </p>\n <p>\n <a color=\"primary\" href=\"{{ sourceUrl }}\" target=\"_system\">{{ sourceUrl }}</a>\n </p>\n </ion-label>\n </ion-item>\n\n <!-- Developers -->\n <ion-item *ngIf=\"developers | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"people\" color=\"dark\"></ion-icon>\n <ion-label>\n <p>\n {{ 'ABOUT.DEVELOPED_BY' | translate }}\n </p>\n <ion-list lines=\"none\">\n <ion-item *ngFor=\"let item of developers\" class=\"ion-no-padding\">\n <ion-label>\n <p>\n <a color=\"primary\" href=\"{{ item.siteUrl }}\" target=\"_system\">{{ item.name || item.label }}</a>\n </p>\n </ion-label>\n <ion-img slot=\"end\" [src]=\"item.logo\" />\n </ion-item>\n </ion-list>\n </ion-label>\n </ion-item>\n\n <!-- Partners -->\n <ion-item class=\"item-partners\" *ngIf=\"partners | isNotEmptyArray\">\n <ion-icon slot=\"start\" name=\"megaphone\" color=\"dark\"></ion-icon>\n <ion-text>\n <p>{{ 'ABOUT.PARTNERS' | translate }}</p>\n <p>\n <a *ngFor=\"let item of partners\" href=\"{{ item.siteUrl }}\" class=\"partners\" target=\"_system\">\n <img *ngIf=\"item.logo; else partnerName\" [src]=\"item.logo\" [alt]=\"item.label\" />\n <ng-template #partnerName>\n <ion-text>{{ item.label }}</ion-text>\n </ng-template>\n </a>\n </p>\n </ion-text>\n </ion-item>\n </ion-list>\n</ion-content>\n\n<ion-footer hidden-xs hidden-sm hidden-mobile>\n <ion-toolbar>\n <ion-row class=\"ion-no-padding\" nowrap>\n <ion-col></ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"solid\" color=\"tertiary\" (click)=\"close()\">{{ 'COMMON.BTN_CLOSE' | translate }}</ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", styles: [".item-partners ion-text{height:auto}.item-partners ion-text p:last-child{display:flex;flex-wrap:wrap;flex-direction:row;justify-content:space-between}.item-partners ion-text p:last-child a{padding:2px}.item-partners ion-text p:last-child a img{text-align:center;display:inline-block;max-height:40px!important}\n"] }]
26473
26531
  }], ctorParameters: () => [{ type: i1$1.TranslateService }, { type: i2$1.ModalController }, { type: ConfigService }, { type: undefined, decorators: [{
26474
26532
  type: Optional
26475
26533
  }, {
@@ -29109,13 +29167,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImpor
29109
29167
 
29110
29168
  class AppAboutModalModule {
29111
29169
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
29112
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, declarations: [AboutModal], imports: [SharedModule, IonicModule, i1$1.TranslateModule], exports: [TranslateModule, AboutModal] });
29113
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, imports: [SharedModule, IonicModule, TranslateModule.forChild(), TranslateModule] });
29170
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, declarations: [AboutModal], imports: [SharedModule, IonicModule, i1$1.TranslateModule, MarkdownModule], exports: [TranslateModule, AboutModal] });
29171
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, imports: [SharedModule, IonicModule, TranslateModule.forChild(), MarkdownModule, TranslateModule] });
29114
29172
  }
29115
29173
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.8", ngImport: i0, type: AppAboutModalModule, decorators: [{
29116
29174
  type: NgModule,
29117
29175
  args: [{
29118
- imports: [SharedModule, IonicModule, TranslateModule.forChild()],
29176
+ imports: [SharedModule, IonicModule, TranslateModule.forChild(), MarkdownModule],
29119
29177
  declarations: [AboutModal],
29120
29178
  exports: [TranslateModule, AboutModal],
29121
29179
  }]
@@ -29262,12 +29320,12 @@ class FormButtonsBarComponent {
29262
29320
  this.menu = menu;
29263
29321
  // Ctrl+S
29264
29322
  this._subscription.add(hotkeys
29265
- .addShortcut({ keys: 'control.s', description: 'COMMON.BTN_SAVE' })
29323
+ .addShortcut({ keys: `${hotkeys.defaultControlKey}.s`, description: 'COMMON.BTN_SAVE' })
29266
29324
  .pipe(filter((_) => !this.disabled))
29267
29325
  .subscribe((event) => this.onSave.emit(event)));
29268
29326
  // Ctrl+Z
29269
29327
  this._subscription.add(hotkeys
29270
- .addShortcut({ keys: 'control.z', description: 'COMMON.BTN_RESET' })
29328
+ .addShortcut({ keys: `${hotkeys.defaultControlKey}.z`, description: 'COMMON.BTN_RESET' })
29271
29329
  .pipe(filter((_) => !this.disabled && !this.disabledCancel))
29272
29330
  .subscribe((event) => this.onCancel.emit(event)));
29273
29331
  // Escape