@sumaris-net/ngx-components 21.4.3 → 21.4.4

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.
@@ -1,10 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { InjectionToken, Directive, inject, Pipe, Injectable, EventEmitter, Output, Optional, Inject, NgModule, forwardRef, ElementRef, booleanAttribute, ViewChildren, Input, ChangeDetectionStrategy, Component, HostListener, HostBinding, numberAttribute, ViewChild, isDevMode, ANIMATION_MODULE_TYPE, RendererStyleFlags2, DOCUMENT, TemplateRef, ContentChild, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Self, ViewEncapsulation, Injector, provideAppInitializer, APP_INITIALIZER, DestroyRef, input, output, Renderer2, effect, signal, SecurityContext, EnvironmentInjector, runInInjectionContext, viewChildren, viewChild, model, computed, untracked } from '@angular/core';
3
- import { firstValueFrom, shareReplay, tap, of, timer, Subject, merge, delay, isObservable, from, Subscription, BehaviorSubject, fromEvent, noop as noop$a, Observable, forkJoin, timeout, defer, combineLatest, debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1, mergeMap as mergeMap$1, switchMap as switchMap$1, EMPTY, interval } from 'rxjs';
2
+ import { InjectionToken, Directive, inject, Pipe, Injectable, EventEmitter, Output, Optional, Inject, NgModule, forwardRef, ElementRef, booleanAttribute, ViewChildren, Input, ChangeDetectionStrategy, Component, HostListener, HostBinding, numberAttribute, ViewChild, isDevMode, ANIMATION_MODULE_TYPE, RendererStyleFlags2, DOCUMENT, TemplateRef, ContentChild, CUSTOM_ELEMENTS_SCHEMA, ChangeDetectorRef, Self, ViewEncapsulation, viewChild, signal, computed, Injector, provideAppInitializer, APP_INITIALIZER, DestroyRef, input, output, Renderer2, effect, SecurityContext, EnvironmentInjector, runInInjectionContext, viewChildren, model, untracked } from '@angular/core';
3
+ import { firstValueFrom, shareReplay, tap, of, timer, Subject, merge, delay, isObservable, from, Subscription, BehaviorSubject, fromEvent, EMPTY, noop as noop$a, Observable, forkJoin, timeout, defer, combineLatest, debounceTime as debounceTime$1, distinctUntilChanged as distinctUntilChanged$1, mergeMap as mergeMap$1, switchMap as switchMap$1, interval } from 'rxjs';
4
4
  import { catchError, map, filter, takeUntil, first, switchMap, tap as tap$1, throttleTime, debounceTime, startWith, distinctUntilChanged, mergeMap, skip, finalize, distinctUntilKeyChanged, take } from 'rxjs/operators';
5
5
  import * as cloneImported from 'clone';
6
6
  import * as i2$3 from '@angular/common';
7
- import { CommonModule, APP_BASE_HREF, LocationStrategy, Location, NgIf, AsyncPipe, JsonPipe } from '@angular/common';
7
+ import { CommonModule, Location, APP_BASE_HREF, LocationStrategy, NgIf, AsyncPipe, JsonPipe } from '@angular/common';
8
8
  import { CdkTableModule } from '@angular/cdk/table';
9
9
  import * as i1$4 from '@angular/cdk/a11y';
10
10
  import { A11yModule, FocusMonitor } from '@angular/cdk/a11y';
@@ -109,9 +109,9 @@ import { base64Decode, cryptoWaitReady, ed25519PairFromSeed, ed25519Sign, base64
109
109
  import * as i4$5 from '@angular/cdk/drag-drop';
110
110
  import { DragDropModule } from '@angular/cdk/drag-drop';
111
111
  import * as i4$3 from 'ngx-markdown';
112
- import { MarkdownModule, MarkedRenderer, MARKED_OPTIONS, SANITIZE } from 'ngx-markdown';
112
+ import { MarkdownModule, MarkedRenderer, MARKED_OPTIONS, MARKED_EXTENSIONS, SANITIZE } from 'ngx-markdown';
113
113
  import * as i1$5 from '@angular/router';
114
- import { RouterModule, RouteReuseStrategy, ActivatedRoute, Router, NavigationEnd } from '@angular/router';
114
+ import { ActivatedRoute, Router, RouterModule, RouteReuseStrategy, NavigationEnd } from '@angular/router';
115
115
  import { Drivers } from '@ionic/storage';
116
116
  import * as LocalForage from 'localforage';
117
117
  import * as CordovaSQLiteDriver from 'localforage-cordovasqlitedriver';
@@ -121,7 +121,7 @@ import * as i4$4 from 'ngx-jdenticon';
121
121
  import { NgxJdenticonModule } from 'ngx-jdenticon';
122
122
  import { __decorate, __param } from 'tslib';
123
123
  import { Apollo, gql as gql$1, provideApollo } from 'apollo-angular';
124
- import { ApolloLink, NetworkStatus, gql, InMemoryCache } from '@apollo/client/core';
124
+ import { ApolloLink, NetworkStatus, gql, InMemoryCache, DocumentTransform } from '@apollo/client/core';
125
125
  import { trigger, transition, style, animate, state } from '@angular/animations';
126
126
  import { Network } from '@capacitor/network';
127
127
  import * as i3$6 from '@rx-angular/state';
@@ -130,6 +130,7 @@ import { SplashScreen } from '@capacitor/splash-screen';
130
130
  import * as i4$2 from 'ionic-cache';
131
131
  import { CacheModule } from 'ionic-cache';
132
132
  import { getMainDefinition } from '@apollo/client/utilities';
133
+ import { visit, Kind } from 'graphql';
133
134
  import { v4 } from 'uuid';
134
135
  import * as LoggerLinkModule from 'apollo-link-logger';
135
136
  import { scryptEncode } from '@polkadot/util-crypto/scrypt/encode';
@@ -155,6 +156,8 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
155
156
  import { Geolocation } from '@capacitor/geolocation';
156
157
  import * as i3$5 from '@e-is/ngx-material-table';
157
158
  import { ValidatorService, TableDataSource, AsyncTableDataSource } from '@e-is/ngx-material-table';
159
+ import { HttpLink as HttpLink$1 } from '@apollo/client/link/http';
160
+ import { Defer20220824Handler } from '@apollo/client/incremental';
158
161
  import { persistCacheSync } from 'apollo4-cache-persist';
159
162
  import { HttpLink } from 'apollo-angular/http';
160
163
  import { GraphQLWsLink } from '@apollo/client/link/subscriptions';
@@ -163,7 +166,6 @@ import { RetryLink } from '@apollo/client/link/retry';
163
166
  import { createFragmentRegistry } from '@apollo/client/cache';
164
167
  import { createOperation } from '@apollo/client/link/utils';
165
168
  import { checkDocument, getOperationDefinition } from '@apollo/client/utilities/internal';
166
- import { Kind } from 'graphql';
167
169
  import 'moment-timezone';
168
170
 
169
171
  const ENVIRONMENT = new InjectionToken('ENV');
@@ -203,6 +205,10 @@ class Environment {
203
205
  offline;
204
206
  // Apollo (graphQL) config
205
207
  apolloFetchPolicy;
208
+ // Enable the GraphQL @defer directive (incremental delivery) - default: false
209
+ // - true: queries with @defer are sent with a fetch link, and the response is streamed (multipart/mixed). Requires a pod that supports @defer.
210
+ // - false: @defer directives are removed from queries (all fields are fetched in a single response)
211
+ enableGraphqlDefer;
206
212
  // Default values
207
213
  defaultLocale;
208
214
  defaultLatLongFormat;
@@ -10895,8 +10901,9 @@ class MatAutocompleteField {
10895
10901
  this._openedSubscription.unsubscribe();
10896
10902
  this._subscription.remove(this._openedSubscription);
10897
10903
  }
10898
- const opened$ = this.autocomplete._isOpen ? timer(0) : this.autocomplete.opened.pipe(debounceTime(200));
10899
- const scrollEnd$ = opened$.pipe(map(() => this.getAutocompletePanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold)), takeUntil(this.autocomplete.closed));
10904
+ // Listen every opening: the 'more items' option (that calls this function) is not recreated when the panel reopens
10905
+ const opened$ = merge(this.autocomplete._isOpen ? timer(0) : EMPTY, this.autocomplete.opened.pipe(debounceTime(200)));
10906
+ const scrollEnd$ = opened$.pipe(map(() => this.getAutocompletePanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold).pipe(takeUntil(this.autocomplete.closed))));
10900
10907
  // Trigger fetch more, when end scroll reached
10901
10908
  this._openedSubscription = scrollEnd$.subscribe(() => this._fetchMore$.emit());
10902
10909
  // Register subscription
@@ -10911,8 +10918,9 @@ class MatAutocompleteField {
10911
10918
  this._openedSubscription.unsubscribe();
10912
10919
  this._subscription.remove(this._openedSubscription);
10913
10920
  }
10914
- const opened$ = this.matSelect.panelOpen ? timer(0) : this.matSelect._openedStream.pipe(debounceTime(200));
10915
- const scrollEnd$ = opened$.pipe(map(() => this.getMatSelectPanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold)), takeUntil(this.matSelect._closedStream));
10921
+ // Listen every opening: the 'more items' option (that calls this function) is not recreated when the panel reopens
10922
+ const opened$ = merge(this.matSelect.panelOpen ? timer(0) : EMPTY, this.matSelect._openedStream.pipe(debounceTime(200)));
10923
+ const scrollEnd$ = opened$.pipe(map(() => this.getMatSelectPanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold).pipe(takeUntil(this.matSelect._closedStream))));
10916
10924
  // Trigger fetch more, end end scroll reached
10917
10925
  this._openedSubscription = scrollEnd$.pipe(filter(() => this._moreItemsCount > 0)).subscribe(() => this._fetchMore$.emit());
10918
10926
  // Register subscription
@@ -14318,8 +14326,9 @@ class MatChipsField {
14318
14326
  this._openedSubscription.unsubscribe();
14319
14327
  this._subscription.remove(this._openedSubscription);
14320
14328
  }
14321
- const opened$ = this.autocomplete._isOpen ? timer(0) : this.autocomplete.opened.pipe(debounceTime(200));
14322
- const scrollEnd$ = opened$.pipe(map((_) => this.getAutocompletePanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold)), takeUntil(this.autocomplete.closed));
14329
+ // Listen every opening: the 'more items' option (that calls this function) is not recreated when the panel reopens
14330
+ const opened$ = merge(this.autocomplete._isOpen ? timer(0) : EMPTY, this.autocomplete.opened.pipe(debounceTime(200)));
14331
+ const scrollEnd$ = opened$.pipe(map((_) => this.getAutocompletePanel()), filter(isNotNil), switchMap((panel) => fromScrollEndEvent(panel, threshold).pipe(takeUntil(this.autocomplete.closed))));
14323
14332
  // Trigger fetch more, end end scroll reached
14324
14333
  this._openedSubscription = scrollEnd$.subscribe(() => this._fetchMore$.emit());
14325
14334
  // Register subscription
@@ -17677,11 +17686,11 @@ class UploadFileComponent {
17677
17686
  return waitFor(() => this.processingFilesCount === 0, opts);
17678
17687
  }
17679
17688
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UploadFileComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i2.TranslateService }], target: i0.ɵɵFactoryTarget.Component });
17680
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: UploadFileComponent, isStandalone: false, selector: "app-upload-file", inputs: { fileExtension: "fileExtension", fileExtensionsExcluded: "fileExtensionsExcluded", uniqueFile: "uniqueFile", instantUpload: "instantUpload", uploadFn: "uploadFn", deleteFn: "deleteFn", maxParallelUpload: "maxParallelUpload", maxFileSizeMb: "maxFileSizeMb", autoHideDropArea: "autoHideDropArea", showDropArea: "showDropArea", showThumbnails: "showThumbnails" }, outputs: { filesChange: "filesChange" }, viewQueries: [{ propertyName: "fileDropEl", first: true, predicate: ["fileDropRef"], descendants: true }], ngImport: i0, template: "@if (!showDropArea) {\n <!-- hidden file input (no drop area): used by browse() -->\n <input\n type=\"file\"\n #fileDropRef\n class=\"hidden-file-input\"\n hidden\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n} @else if ((!uniqueFile && !autoHideDropArea) || files.length === 0) {\n <div class=\"container\" appDragAndDrop (fileDropped)=\"onFileDropped($event)\">\n <input\n type=\"file\"\n #fileDropRef\n id=\"fileDropRef\"\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n <mat-icon style=\"font-size: xx-large\" color=\"accent\" class=\"text-size\">upload</mat-icon>\n <ion-label>\n <span\n [innerHTML]=\"\n 'FILE.UPLOAD.DRAG_AND_DROP' | translate: { extension: !fileExtension ? '' : ' (' + fileExtension + ')' }\n \"\n ></span>\n </ion-label>\n <ion-button color=\"tertiary\">{{ 'FILE.UPLOAD.BROWSE' | translate }}</ion-button>\n </div>\n}\n<!-- global progression (useful when many files are selected) -->\n@if (uploading && files.length > 1) {\n <div class=\"upload-summary\">\n <ion-label color=\"medium\">\n <small>{{ 'FILE.UPLOAD.DONE_COUNT' | translate: { count: doneFilesCount, total: files.length } }}</small>\n </ion-label>\n <ion-progress-bar [value]=\"doneFilesCount / files.length\" color=\"accent\"></ion-progress-bar>\n </div>\n}\n<div class=\"files-list\">\n <ion-list>\n @for (file of files; track file; let i = $index) {\n <ion-item>\n @if (showThumbnails && file.kind === 'image') {\n <!-- image thumbnail, or a skeleton until the image is ready -->\n <div slot=\"start\" class=\"file-thumbnail\">\n @if (file.error) {\n <mat-icon color=\"danger\">broken_image</mat-icon>\n } @else if (file.progress === 1 && file.response?.body) {\n <img [src]=\"file.response.body\" alt=\"\" />\n } @else {\n <ion-skeleton-text [animated]=\"true\"></ion-skeleton-text>\n }\n </div>\n } @else {\n <!-- icon of the file type (pdf, spreadsheet, document...) -->\n <div slot=\"start\" class=\"file-icon\" [class.file-thumbnail]=\"showThumbnails\" [attr.data-kind]=\"file.kind\">\n <mat-icon>{{ fileKindIcons[file.kind || 'other'] }}</mat-icon>\n </div>\n }\n <div class=\"single-file\">\n <ion-label>\n <h4 class=\"name\" [class.deleting]=\"file.deleting\">\n {{ file.name }}\n </h4>\n <p>\n {{ file.size | fileSize }}\n </p>\n </ion-label>\n @if (uploading && !file.error && !showThumbnails) {\n <ion-progress-bar\n [value]=\"file.progress || 0\"\n [type]=\"file.deleting || file.progress === -1 ? 'indeterminate' : 'determinate'\"\n ></ion-progress-bar>\n }\n @if (file.error) {\n <ion-text\n color=\"danger\"\n [innerHTML]=\"\n file.error\n | translate\n : {\n fileExtension: fileExtension,\n fileExtensionsExcluded: fileExtensionsExcluded.join(', '),\n maxFileSizeMb: maxFileSizeMb,\n }\n \"\n ></ion-text>\n }\n </div>\n <!-- remote from list (before importation) -->\n @if (!file.progress || file.error) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n <!-- delete file (remotely - after importation) -->\n @if (file.progress === 1 && !!deleteFn && !file.deleting) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n </ion-item>\n }\n </ion-list>\n</div>\n", styles: [".container{width:calc(100% - 3rem);min-height:200px;padding:2rem;text-align:center;border:dashed 1px #979797;position:relative;margin:1.5rem}.container input{opacity:0;position:absolute;z-index:2;width:100%;height:100%;top:0;left:0;cursor:pointer}.container ion-label{display:block;font-size:20px;font-weight:600;color:#38424c}.container label{display:inline-block;color:#fff;width:183px;height:44px;border-radius:21.5px;background-color:#db202f;padding:8px 16px}.fileover{border:solid 1px var(--ion-color-accent)}.upload-summary{padding:0 1.5rem .5rem}.upload-summary ion-progress-bar{margin-top:4px}.file-thumbnail{--thumbnail-size: 48px;width:var(--thumbnail-size);height:var(--thumbnail-size);flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:4px;overflow:hidden}.file-thumbnail img{width:100%;height:100%;object-fit:cover}.file-thumbnail ion-skeleton-text{width:100%;height:100%;margin:0}.file-icon{display:flex;align-items:center;justify-content:center;color:var(--ion-color-medium)}.file-icon.file-thumbnail mat-icon{--icon-size: 36px;font-size:var(--icon-size);width:var(--icon-size);height:var(--icon-size)}.file-icon[data-kind=pdf]{color:#d93025}.file-icon[data-kind=document]{color:#2b579a}.file-icon[data-kind=spreadsheet]{color:#217346}.file-icon[data-kind=presentation]{color:#d24726}.file-icon[data-kind=archive]{color:#b8860b}.file-icon[data-kind=image]{color:var(--ion-color-tertiary)}.file-icon[data-kind=video],.file-icon[data-kind=audio]{color:#7b1fa2}.files-list{max-height:300px;overflow:auto}.files-list .single-file{display:flex;flex-direction:column;flex:1 1 auto}.files-list .single-file .name{word-break:break-all}.files-list .single-file .name.deleting{color:gray!important;font-style:italic!important}\n"], dependencies: [{ kind: "component", type: i2$2.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$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonProgressBar, selector: "ion-progress-bar", inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }, { kind: "component", type: i2$2.IonSkeletonText, selector: "ion-skeleton-text", inputs: ["animated"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: DragAndDropDirective, selector: "[appDragAndDrop]", outputs: ["fileDropped"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }] });
17689
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: UploadFileComponent, isStandalone: false, selector: "app-upload-file", inputs: { fileExtension: "fileExtension", fileExtensionsExcluded: "fileExtensionsExcluded", uniqueFile: "uniqueFile", instantUpload: "instantUpload", uploadFn: "uploadFn", deleteFn: "deleteFn", maxParallelUpload: "maxParallelUpload", maxFileSizeMb: "maxFileSizeMb", autoHideDropArea: "autoHideDropArea", showDropArea: "showDropArea", showThumbnails: "showThumbnails" }, outputs: { filesChange: "filesChange" }, viewQueries: [{ propertyName: "fileDropEl", first: true, predicate: ["fileDropRef"], descendants: true }], ngImport: i0, template: "@if (!showDropArea) {\n <!-- hidden file input (no drop area): used by browse() -->\n <input\n type=\"file\"\n #fileDropRef\n class=\"hidden-file-input\"\n hidden\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n} @else if ((!uniqueFile && !autoHideDropArea) || files.length === 0) {\n <div class=\"container\" appDragAndDrop (fileDropped)=\"onFileDropped($event)\">\n <input\n type=\"file\"\n #fileDropRef\n id=\"fileDropRef\"\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n <mat-icon style=\"font-size: xx-large\" color=\"accent\" class=\"text-size\">upload</mat-icon>\n <ion-label>\n <span\n [innerHTML]=\"\n 'FILE.UPLOAD.DRAG_AND_DROP' | translate: { extension: !fileExtension ? '' : ' (' + fileExtension + ')' }\n \"\n ></span>\n </ion-label>\n <ion-button color=\"tertiary\">{{ 'FILE.UPLOAD.BROWSE' | translate }}</ion-button>\n </div>\n}\n<!-- global progression (useful when many files are selected) -->\n@if (uploading && files.length > 1) {\n <div class=\"upload-summary\">\n <ion-label color=\"medium\">\n <small>{{ 'FILE.UPLOAD.DONE_COUNT' | translate: { count: doneFilesCount, total: files.length } }}</small>\n </ion-label>\n <ion-progress-bar [value]=\"doneFilesCount / files.length\" color=\"accent\"></ion-progress-bar>\n </div>\n}\n<div class=\"files-list\">\n <ion-list>\n @for (file of files; track file; let i = $index) {\n <ion-item>\n @if (showThumbnails && file.kind === 'image') {\n <!-- image thumbnail, or a skeleton until the image is ready -->\n <div slot=\"start\" class=\"file-thumbnail\">\n @if (file.error) {\n <mat-icon color=\"danger\">broken_image</mat-icon>\n } @else if (file.progress === 1 && file.response?.body) {\n <img [src]=\"file.response.body\" alt=\"\" />\n } @else {\n <ion-skeleton-text [animated]=\"true\"></ion-skeleton-text>\n }\n </div>\n } @else {\n <!-- icon of the file type (pdf, spreadsheet, document...) -->\n <div slot=\"start\" class=\"file-icon\" [class.file-thumbnail]=\"showThumbnails\" [attr.data-kind]=\"file.kind\">\n <mat-icon>{{ fileKindIcons[file.kind || 'other'] }}</mat-icon>\n </div>\n }\n <div class=\"single-file\">\n <ion-label>\n <h4 class=\"name\" [class.deleting]=\"file.deleting\">\n {{ file.name }}\n </h4>\n <p>\n {{ file.size | fileSize }}\n </p>\n </ion-label>\n @if (uploading && !file.error && !showThumbnails) {\n <ion-progress-bar\n [value]=\"file.progress || 0\"\n [type]=\"file.deleting || file.progress === -1 ? 'indeterminate' : 'determinate'\"\n ></ion-progress-bar>\n }\n @if (file.error) {\n <ion-text\n color=\"danger\"\n [innerHTML]=\"\n file.error\n | translate\n : {\n fileExtension: fileExtension,\n fileExtensionsExcluded: fileExtensionsExcluded.join(', '),\n maxFileSizeMb: maxFileSizeMb,\n }\n \"\n ></ion-text>\n }\n </div>\n <!-- remote from list (before importation) -->\n @if (!file.progress || file.error) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n <!-- delete file (remotely - after importation) -->\n @if (file.progress === 1 && !!deleteFn && !file.deleting) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n </ion-item>\n }\n </ion-list>\n</div>\n", styles: [".container{width:calc(100% - 3rem);min-height:200px;padding:2rem;text-align:center;border:dashed 1px #979797;position:relative;margin:1.5rem}.container input{opacity:0;position:absolute;z-index:2;width:100%;height:100%;top:0;left:0;cursor:pointer}.container ion-label{display:block;font-size:20px;font-weight:600;color:#38424c}.container label{display:inline-block;color:#fff;width:183px;height:44px;border-radius:21.5px;background-color:#db202f;padding:8px 16px}.fileover{border:solid 1px var(--ion-color-accent)}.upload-summary{padding:0 1.5rem .5rem}.upload-summary ion-progress-bar{margin-top:4px}.file-thumbnail{--thumbnail-size: 48px;width:var(--thumbnail-size);height:var(--thumbnail-size);flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:4px;overflow:hidden}.file-thumbnail img{width:100%;height:100%;object-fit:cover}.file-thumbnail ion-skeleton-text{width:100%;height:100%;margin:0}.file-icon{display:flex;align-items:center;justify-content:center;color:var(--ion-color-medium)}.file-icon.file-thumbnail mat-icon{--icon-size: 36px;font-size:var(--icon-size);width:var(--icon-size);height:var(--icon-size)}.file-icon[data-kind=pdf]{color:#d93025}.file-icon[data-kind=document]{color:#2b579a}.file-icon[data-kind=spreadsheet]{color:#217346}.file-icon[data-kind=presentation]{color:#d24726}.file-icon[data-kind=archive]{color:#b8860b}.file-icon[data-kind=image]{color:var(--ion-color-tertiary)}.file-icon[data-kind=video],.file-icon[data-kind=audio]{color:#7b1fa2}.files-list{max-height:var(--app-upload-file-list-max-height, 300px);overflow:auto}.files-list .single-file{display:flex;flex-direction:column;flex:1 1 auto}.files-list .single-file .name{word-break:break-all}.files-list .single-file .name.deleting{color:gray!important;font-style:italic!important}\n"], dependencies: [{ kind: "component", type: i2$2.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$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonProgressBar, selector: "ion-progress-bar", inputs: ["buffer", "color", "mode", "reversed", "type", "value"] }, { kind: "component", type: i2$2.IonSkeletonText, selector: "ion-skeleton-text", inputs: ["animated"] }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: DragAndDropDirective, selector: "[appDragAndDrop]", outputs: ["fileDropped"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }] });
17681
17690
  }
17682
17691
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: UploadFileComponent, decorators: [{
17683
17692
  type: Component,
17684
- args: [{ selector: 'app-upload-file', standalone: false, template: "@if (!showDropArea) {\n <!-- hidden file input (no drop area): used by browse() -->\n <input\n type=\"file\"\n #fileDropRef\n class=\"hidden-file-input\"\n hidden\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n} @else if ((!uniqueFile && !autoHideDropArea) || files.length === 0) {\n <div class=\"container\" appDragAndDrop (fileDropped)=\"onFileDropped($event)\">\n <input\n type=\"file\"\n #fileDropRef\n id=\"fileDropRef\"\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n <mat-icon style=\"font-size: xx-large\" color=\"accent\" class=\"text-size\">upload</mat-icon>\n <ion-label>\n <span\n [innerHTML]=\"\n 'FILE.UPLOAD.DRAG_AND_DROP' | translate: { extension: !fileExtension ? '' : ' (' + fileExtension + ')' }\n \"\n ></span>\n </ion-label>\n <ion-button color=\"tertiary\">{{ 'FILE.UPLOAD.BROWSE' | translate }}</ion-button>\n </div>\n}\n<!-- global progression (useful when many files are selected) -->\n@if (uploading && files.length > 1) {\n <div class=\"upload-summary\">\n <ion-label color=\"medium\">\n <small>{{ 'FILE.UPLOAD.DONE_COUNT' | translate: { count: doneFilesCount, total: files.length } }}</small>\n </ion-label>\n <ion-progress-bar [value]=\"doneFilesCount / files.length\" color=\"accent\"></ion-progress-bar>\n </div>\n}\n<div class=\"files-list\">\n <ion-list>\n @for (file of files; track file; let i = $index) {\n <ion-item>\n @if (showThumbnails && file.kind === 'image') {\n <!-- image thumbnail, or a skeleton until the image is ready -->\n <div slot=\"start\" class=\"file-thumbnail\">\n @if (file.error) {\n <mat-icon color=\"danger\">broken_image</mat-icon>\n } @else if (file.progress === 1 && file.response?.body) {\n <img [src]=\"file.response.body\" alt=\"\" />\n } @else {\n <ion-skeleton-text [animated]=\"true\"></ion-skeleton-text>\n }\n </div>\n } @else {\n <!-- icon of the file type (pdf, spreadsheet, document...) -->\n <div slot=\"start\" class=\"file-icon\" [class.file-thumbnail]=\"showThumbnails\" [attr.data-kind]=\"file.kind\">\n <mat-icon>{{ fileKindIcons[file.kind || 'other'] }}</mat-icon>\n </div>\n }\n <div class=\"single-file\">\n <ion-label>\n <h4 class=\"name\" [class.deleting]=\"file.deleting\">\n {{ file.name }}\n </h4>\n <p>\n {{ file.size | fileSize }}\n </p>\n </ion-label>\n @if (uploading && !file.error && !showThumbnails) {\n <ion-progress-bar\n [value]=\"file.progress || 0\"\n [type]=\"file.deleting || file.progress === -1 ? 'indeterminate' : 'determinate'\"\n ></ion-progress-bar>\n }\n @if (file.error) {\n <ion-text\n color=\"danger\"\n [innerHTML]=\"\n file.error\n | translate\n : {\n fileExtension: fileExtension,\n fileExtensionsExcluded: fileExtensionsExcluded.join(', '),\n maxFileSizeMb: maxFileSizeMb,\n }\n \"\n ></ion-text>\n }\n </div>\n <!-- remote from list (before importation) -->\n @if (!file.progress || file.error) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n <!-- delete file (remotely - after importation) -->\n @if (file.progress === 1 && !!deleteFn && !file.deleting) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n </ion-item>\n }\n </ion-list>\n</div>\n", styles: [".container{width:calc(100% - 3rem);min-height:200px;padding:2rem;text-align:center;border:dashed 1px #979797;position:relative;margin:1.5rem}.container input{opacity:0;position:absolute;z-index:2;width:100%;height:100%;top:0;left:0;cursor:pointer}.container ion-label{display:block;font-size:20px;font-weight:600;color:#38424c}.container label{display:inline-block;color:#fff;width:183px;height:44px;border-radius:21.5px;background-color:#db202f;padding:8px 16px}.fileover{border:solid 1px var(--ion-color-accent)}.upload-summary{padding:0 1.5rem .5rem}.upload-summary ion-progress-bar{margin-top:4px}.file-thumbnail{--thumbnail-size: 48px;width:var(--thumbnail-size);height:var(--thumbnail-size);flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:4px;overflow:hidden}.file-thumbnail img{width:100%;height:100%;object-fit:cover}.file-thumbnail ion-skeleton-text{width:100%;height:100%;margin:0}.file-icon{display:flex;align-items:center;justify-content:center;color:var(--ion-color-medium)}.file-icon.file-thumbnail mat-icon{--icon-size: 36px;font-size:var(--icon-size);width:var(--icon-size);height:var(--icon-size)}.file-icon[data-kind=pdf]{color:#d93025}.file-icon[data-kind=document]{color:#2b579a}.file-icon[data-kind=spreadsheet]{color:#217346}.file-icon[data-kind=presentation]{color:#d24726}.file-icon[data-kind=archive]{color:#b8860b}.file-icon[data-kind=image]{color:var(--ion-color-tertiary)}.file-icon[data-kind=video],.file-icon[data-kind=audio]{color:#7b1fa2}.files-list{max-height:300px;overflow:auto}.files-list .single-file{display:flex;flex-direction:column;flex:1 1 auto}.files-list .single-file .name{word-break:break-all}.files-list .single-file .name.deleting{color:gray!important;font-style:italic!important}\n"] }]
17693
+ args: [{ selector: 'app-upload-file', standalone: false, template: "@if (!showDropArea) {\n <!-- hidden file input (no drop area): used by browse() -->\n <input\n type=\"file\"\n #fileDropRef\n class=\"hidden-file-input\"\n hidden\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n} @else if ((!uniqueFile && !autoHideDropArea) || files.length === 0) {\n <div class=\"container\" appDragAndDrop (fileDropped)=\"onFileDropped($event)\">\n <input\n type=\"file\"\n #fileDropRef\n id=\"fileDropRef\"\n [multiple]=\"!uniqueFile\"\n [accept]=\"fileExtension\"\n (change)=\"fileBrowseHandler($any($event.target).files)\"\n />\n <mat-icon style=\"font-size: xx-large\" color=\"accent\" class=\"text-size\">upload</mat-icon>\n <ion-label>\n <span\n [innerHTML]=\"\n 'FILE.UPLOAD.DRAG_AND_DROP' | translate: { extension: !fileExtension ? '' : ' (' + fileExtension + ')' }\n \"\n ></span>\n </ion-label>\n <ion-button color=\"tertiary\">{{ 'FILE.UPLOAD.BROWSE' | translate }}</ion-button>\n </div>\n}\n<!-- global progression (useful when many files are selected) -->\n@if (uploading && files.length > 1) {\n <div class=\"upload-summary\">\n <ion-label color=\"medium\">\n <small>{{ 'FILE.UPLOAD.DONE_COUNT' | translate: { count: doneFilesCount, total: files.length } }}</small>\n </ion-label>\n <ion-progress-bar [value]=\"doneFilesCount / files.length\" color=\"accent\"></ion-progress-bar>\n </div>\n}\n<div class=\"files-list\">\n <ion-list>\n @for (file of files; track file; let i = $index) {\n <ion-item>\n @if (showThumbnails && file.kind === 'image') {\n <!-- image thumbnail, or a skeleton until the image is ready -->\n <div slot=\"start\" class=\"file-thumbnail\">\n @if (file.error) {\n <mat-icon color=\"danger\">broken_image</mat-icon>\n } @else if (file.progress === 1 && file.response?.body) {\n <img [src]=\"file.response.body\" alt=\"\" />\n } @else {\n <ion-skeleton-text [animated]=\"true\"></ion-skeleton-text>\n }\n </div>\n } @else {\n <!-- icon of the file type (pdf, spreadsheet, document...) -->\n <div slot=\"start\" class=\"file-icon\" [class.file-thumbnail]=\"showThumbnails\" [attr.data-kind]=\"file.kind\">\n <mat-icon>{{ fileKindIcons[file.kind || 'other'] }}</mat-icon>\n </div>\n }\n <div class=\"single-file\">\n <ion-label>\n <h4 class=\"name\" [class.deleting]=\"file.deleting\">\n {{ file.name }}\n </h4>\n <p>\n {{ file.size | fileSize }}\n </p>\n </ion-label>\n @if (uploading && !file.error && !showThumbnails) {\n <ion-progress-bar\n [value]=\"file.progress || 0\"\n [type]=\"file.deleting || file.progress === -1 ? 'indeterminate' : 'determinate'\"\n ></ion-progress-bar>\n }\n @if (file.error) {\n <ion-text\n color=\"danger\"\n [innerHTML]=\"\n file.error\n | translate\n : {\n fileExtension: fileExtension,\n fileExtensionsExcluded: fileExtensionsExcluded.join(', '),\n maxFileSizeMb: maxFileSizeMb,\n }\n \"\n ></ion-text>\n }\n </div>\n <!-- remote from list (before importation) -->\n @if (!file.progress || file.error) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n <!-- delete file (remotely - after importation) -->\n @if (file.progress === 1 && !!deleteFn && !file.deleting) {\n <button mat-icon-button slot=\"end\" (click)=\"deleteFile(i)\">\n <mat-icon>clear</mat-icon>\n </button>\n }\n </ion-item>\n }\n </ion-list>\n</div>\n", styles: [".container{width:calc(100% - 3rem);min-height:200px;padding:2rem;text-align:center;border:dashed 1px #979797;position:relative;margin:1.5rem}.container input{opacity:0;position:absolute;z-index:2;width:100%;height:100%;top:0;left:0;cursor:pointer}.container ion-label{display:block;font-size:20px;font-weight:600;color:#38424c}.container label{display:inline-block;color:#fff;width:183px;height:44px;border-radius:21.5px;background-color:#db202f;padding:8px 16px}.fileover{border:solid 1px var(--ion-color-accent)}.upload-summary{padding:0 1.5rem .5rem}.upload-summary ion-progress-bar{margin-top:4px}.file-thumbnail{--thumbnail-size: 48px;width:var(--thumbnail-size);height:var(--thumbnail-size);flex-shrink:0;display:flex;align-items:center;justify-content:center;border-radius:4px;overflow:hidden}.file-thumbnail img{width:100%;height:100%;object-fit:cover}.file-thumbnail ion-skeleton-text{width:100%;height:100%;margin:0}.file-icon{display:flex;align-items:center;justify-content:center;color:var(--ion-color-medium)}.file-icon.file-thumbnail mat-icon{--icon-size: 36px;font-size:var(--icon-size);width:var(--icon-size);height:var(--icon-size)}.file-icon[data-kind=pdf]{color:#d93025}.file-icon[data-kind=document]{color:#2b579a}.file-icon[data-kind=spreadsheet]{color:#217346}.file-icon[data-kind=presentation]{color:#d24726}.file-icon[data-kind=archive]{color:#b8860b}.file-icon[data-kind=image]{color:var(--ion-color-tertiary)}.file-icon[data-kind=video],.file-icon[data-kind=audio]{color:#7b1fa2}.files-list{max-height:var(--app-upload-file-list-max-height, 300px);overflow:auto}.files-list .single-file{display:flex;flex-direction:column;flex:1 1 auto}.files-list .single-file .name{word-break:break-all}.files-list .single-file .name.deleting{color:gray!important;font-style:italic!important}\n"] }]
17685
17694
  }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i2.TranslateService }], propDecorators: { fileDropEl: [{
17686
17695
  type: ViewChild,
17687
17696
  args: ['fileDropRef', { static: false }]
@@ -17843,35 +17852,218 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
17843
17852
  }] } });
17844
17853
 
17845
17854
  const APP_TESTING_PAGES = new InjectionToken('testingPages');
17855
+ const SEARCH_QUERY_PARAM = 'q';
17856
+ function normalize(value) {
17857
+ return removeDiacritics(value || '').toLowerCase();
17858
+ }
17859
+ /**
17860
+ * Split a text into parts, flagging parts that match one of the (normalized) tokens.
17861
+ * Assumes removeDiacritics() keeps the string length (true for latin accents).
17862
+ */
17863
+ function splitByTokens(text, tokens) {
17864
+ if (!text)
17865
+ return [];
17866
+ if (!tokens.length)
17867
+ return [{ text, match: false }];
17868
+ const normalized = normalize(text);
17869
+ const matches = new Array(text.length).fill(false);
17870
+ for (const token of tokens) {
17871
+ let start = normalized.indexOf(token);
17872
+ while (start !== -1) {
17873
+ matches.fill(true, start, start + token.length);
17874
+ start = normalized.indexOf(token, start + token.length);
17875
+ }
17876
+ }
17877
+ const parts = [];
17878
+ for (let i = 0; i < text.length; i++) {
17879
+ const last = parts[parts.length - 1];
17880
+ if (last && last.match === matches[i])
17881
+ last.text += text[i];
17882
+ else
17883
+ parts.push({ text: text[i], match: matches[i] });
17884
+ }
17885
+ return parts;
17886
+ }
17846
17887
  class SharedTestsPage {
17847
- route;
17848
- router;
17849
- translate;
17850
- pages;
17851
- constructor(route, router, translate, pages) {
17852
- this.route = route;
17853
- this.router = router;
17854
- this.translate = translate;
17855
- this.pages = pages;
17856
- console.info('[testing] Loaded tests pages:', pages);
17888
+ // Code under free license (GPL version 3)
17889
+ searchbar = viewChild(IonSearchbar, ...(ngDevMode ? [{ debugName: "searchbar" }] : /* istanbul ignore next */ []));
17890
+ searchText = signal('', ...(ngDevMode ? [{ debugName: "searchText" }] : /* istanbul ignore next */ []));
17891
+ activeIndex = signal(0, ...(ngDevMode ? [{ debugName: "activeIndex" }] : /* istanbul ignore next */ []));
17892
+ showSearchbar = signal(false, ...(ngDevMode ? [{ debugName: "showSearchbar" }] : /* istanbul ignore next */ []));
17893
+ totalCount;
17894
+ initialSearchText; // Not bound to searchText(), to avoid overriding the input while typing
17895
+ sections = computed(() => this.filter(this.searchText()), ...(ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []));
17896
+ visibleCount = computed(() => this.sections().reduce((count, section) => count + section.items.length, 0), ...(ngDevMode ? [{ debugName: "visibleCount" }] : /* istanbul ignore next */ []));
17897
+ route = inject(ActivatedRoute);
17898
+ router = inject(Router);
17899
+ location = inject(Location);
17900
+ pages = inject(APP_TESTING_PAGES, { optional: true });
17901
+ constructor() {
17902
+ console.info('[testing] Loaded tests pages:', this.pages);
17903
+ this.totalCount = (this.pages || []).filter((p) => !p.divider && p.page).length;
17904
+ // Restore the search text (e.g. when going back from a test page)
17905
+ this.initialSearchText = this.route.snapshot.queryParamMap.get(SEARCH_QUERY_PARAM) || '';
17906
+ this.searchText.set(this.initialSearchText);
17907
+ this.showSearchbar.set(!!this.initialSearchText);
17908
+ }
17909
+ ionViewDidEnter() {
17910
+ // Focus the searchbar (if visible), to be able to continue typing
17911
+ if (this.showSearchbar())
17912
+ this.focusSearchbar();
17913
+ }
17914
+ onDocumentKeydown(domEvent) {
17915
+ const event = domEvent;
17916
+ // Open the searchbar on '/' or Ctrl+K, if not already typing in a field
17917
+ const target = event.target;
17918
+ const isTyping = target?.closest?.('input, textarea, [contenteditable="true"]');
17919
+ if ((event.key === '/' && !isTyping) || (event.key === 'k' && (event.ctrlKey || event.metaKey))) {
17920
+ event.preventDefault();
17921
+ this.openSearchbar();
17922
+ }
17923
+ }
17924
+ toggleSearchbar() {
17925
+ if (this.showSearchbar())
17926
+ this.closeSearchbar();
17927
+ else
17928
+ this.openSearchbar();
17929
+ }
17930
+ openSearchbar() {
17931
+ this.showSearchbar.set(true);
17932
+ this.focusSearchbar();
17933
+ }
17934
+ closeSearchbar() {
17935
+ // Reset the filter, to never hide pages without a visible searchbar
17936
+ if (this.searchText())
17937
+ this.onSearchInput('');
17938
+ this.initialSearchText = '';
17939
+ this.showSearchbar.set(false);
17940
+ }
17941
+ onSearchInput(value) {
17942
+ const searchText = value || '';
17943
+ this.searchText.set(searchText);
17944
+ this.activeIndex.set(0);
17945
+ // Keep the search text in the URL (restored on back navigation), without triggering a navigation
17946
+ const urlTree = this.router.createUrlTree([], {
17947
+ relativeTo: this.route,
17948
+ queryParams: { [SEARCH_QUERY_PARAM]: searchText.trim() || null },
17949
+ queryParamsHandling: 'merge',
17950
+ });
17951
+ this.location.replaceState(this.router.serializeUrl(urlTree));
17857
17952
  }
17858
- async openPage(page) {
17953
+ onSearchKeydown(event) {
17954
+ const count = this.visibleCount();
17955
+ switch (event.key) {
17956
+ case 'ArrowDown':
17957
+ event.preventDefault();
17958
+ if (count)
17959
+ this.setActiveIndex((this.activeIndex() + 1) % count);
17960
+ break;
17961
+ case 'ArrowUp':
17962
+ event.preventDefault();
17963
+ if (count)
17964
+ this.setActiveIndex((this.activeIndex() - 1 + count) % count);
17965
+ break;
17966
+ case 'Enter': {
17967
+ event.preventDefault();
17968
+ const item = this.findItem(this.activeIndex());
17969
+ if (item)
17970
+ this.openPage(item.page, event);
17971
+ break;
17972
+ }
17973
+ case 'Escape':
17974
+ // Clear the search text, or close the searchbar if already empty
17975
+ event.preventDefault();
17976
+ if (this.searchText())
17977
+ this.clearSearch();
17978
+ else
17979
+ this.closeSearchbar();
17980
+ break;
17981
+ }
17982
+ }
17983
+ clearSearch() {
17984
+ const searchbar = this.searchbar();
17985
+ if (searchbar)
17986
+ searchbar.value = '';
17987
+ this.onSearchInput('');
17988
+ this.focusSearchbar();
17989
+ }
17990
+ focusSearchbar() {
17991
+ // Wait the searchbar to be rendered
17992
+ setTimeout(() => this.searchbar()?.setFocus(), 150);
17993
+ }
17994
+ async openPage(page, event) {
17995
+ // Open in a new tab, on Ctrl/Cmd + click (or Ctrl/Cmd + Enter)
17996
+ if (event && (event.ctrlKey || event.metaKey)) {
17997
+ const url = this.router.serializeUrl(this.router.createUrlTree([page]));
17998
+ window.open(url, '_blank');
17999
+ return false;
18000
+ }
17859
18001
  return this.router.navigate([page], {
17860
18002
  relativeTo: this.route,
17861
18003
  });
17862
18004
  }
17863
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedTestsPage, deps: [{ token: i1$5.ActivatedRoute }, { token: i1$5.Router }, { token: i2.TranslateService }, { token: APP_TESTING_PAGES, optional: true }], target: i0.ɵɵFactoryTarget.Component });
17864
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: SharedTestsPage, isStandalone: false, selector: "app-tests-page", ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-menu-button></ion-menu-button>\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Tests</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n @if (pages?.length) {\n <ion-list>\n @for (item of pages; track item) {\n @if (item.divider) {\n <ion-item-divider>{{ item.label }}</ion-item-divider>\n } @else {\n <ion-item tappable class=\"text-1x\" (click)=\"openPage(item.page)\">{{ item.label }}</ion-item>\n }\n }\n </ion-list>\n }\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$2.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: i2$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonItemDivider, selector: "ion-item-divider", inputs: ["color", "mode", "sticky"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonMenuButton, selector: "ion-menu-button", inputs: ["autoHide", "color", "disabled", "menu", "mode", "type"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }] });
18005
+ setActiveIndex(index) {
18006
+ this.activeIndex.set(index);
18007
+ // Scroll the active item into view
18008
+ setTimeout(() => document.querySelector('app-tests-page ion-item.active')?.scrollIntoView({ block: 'nearest' }));
18009
+ }
18010
+ findItem(index) {
18011
+ for (const section of this.sections()) {
18012
+ const item = section.items.find((i) => i.index === index);
18013
+ if (item)
18014
+ return item;
18015
+ }
18016
+ return undefined;
18017
+ }
18018
+ filter(searchText) {
18019
+ const tokens = normalize(searchText)
18020
+ .split(/\s+/)
18021
+ .filter((token) => token.length > 0);
18022
+ // Group pages by section (divider)
18023
+ const groups = [];
18024
+ for (const page of this.pages || []) {
18025
+ if (page.divider)
18026
+ groups.push({ label: page.label, pages: [] });
18027
+ else if (page.page) {
18028
+ if (!groups.length)
18029
+ groups.push({ pages: [] });
18030
+ groups[groups.length - 1].pages.push(page);
18031
+ }
18032
+ }
18033
+ // Filter: each token must match the item label, its path, or its section label
18034
+ let index = 0;
18035
+ const sections = [];
18036
+ for (const group of groups) {
18037
+ const sectionText = normalize(group.label);
18038
+ const items = group.pages
18039
+ .filter((p) => {
18040
+ if (!tokens.length)
18041
+ return true;
18042
+ const text = `${sectionText} ${normalize(p.label)} ${normalize(p.page)}`;
18043
+ return tokens.every((token) => text.includes(token));
18044
+ })
18045
+ .map((p) => ({
18046
+ index: index++,
18047
+ page: p.page,
18048
+ labelParts: splitByTokens(p.label, tokens),
18049
+ pageParts: splitByTokens(p.page, tokens),
18050
+ }));
18051
+ if (items.length) {
18052
+ sections.push({ labelParts: group.label ? splitByTokens(group.label, tokens) : undefined, items });
18053
+ }
18054
+ }
18055
+ return sections;
18056
+ }
18057
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedTestsPage, deps: [], target: i0.ɵɵFactoryTarget.Component });
18058
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: SharedTestsPage, isStandalone: false, selector: "app-tests-page", host: { listeners: { "document:keydown": "onDocumentKeydown($event)" } }, viewQueries: [{ propertyName: "searchbar", first: true, predicate: IonSearchbar, descendants: true, isSignal: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-menu-button></ion-menu-button>\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n @if (showSearchbar()) {\n <!-- Search bar (replace the title, when expanded) -->\n <ion-searchbar\n color=\"light\"\n [value]=\"initialSearchText\"\n [debounce]=\"0\"\n [placeholder]=\"'TESTING_PAGE.SEARCH_PLACEHOLDER' | translate\"\n showClearButton=\"always\"\n autocapitalize=\"off\"\n (ionInput)=\"onSearchInput($event.detail.value)\"\n (ionClear)=\"clearSearch()\"\n (keydown)=\"onSearchKeydown($event)\"\n ></ion-searchbar>\n } @else {\n <ion-title>{{ 'TESTING_PAGE.TITLE' | translate }}</ion-title>\n }\n\n <ion-buttons slot=\"end\">\n <!-- Toggle the search bar -->\n <ion-button\n [class.search-active]=\"showSearchbar()\"\n [title]=\"(showSearchbar() ? 'TESTING_PAGE.BTN_HIDE_SEARCH' : 'TESTING_PAGE.BTN_SHOW_SEARCH') | translate\"\n (click)=\"toggleSearchbar()\"\n >\n <ion-icon slot=\"icon-only\" name=\"search\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n\n <!-- Result count (under the search bar) -->\n @if (showSearchbar() && searchText().trim()) {\n <div class=\"search-count\">\n <ion-label [color]=\"visibleCount() === 0 ? 'danger' : 'primary100'\" class=\"ion-padding\">\n {{ (visibleCount() ? 'COMMON.RESULT_COUNT' : 'COMMON.NO_RESULT') | translate: { count: visibleCount() } }}\n </ion-label>\n </div>\n }\n</ion-header>\n\n<ion-content>\n @if (sections().length) {\n <ion-list>\n @for (section of sections(); track $index) {\n <ion-item-group>\n @if (section.labelParts) {\n <ion-item-divider sticky>\n <ion-label>\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: section.labelParts }\"></ng-container>\n </ion-label>\n </ion-item-divider>\n }\n @for (item of section.items; track item.page) {\n <ion-item\n tappable\n class=\"text-1x\"\n [class.active]=\"searchText() && item.index === activeIndex()\"\n [title]=\"item.page\"\n (click)=\"openPage(item.page, $event)\"\n (auxclick)=\"$event.button === 1 && openPage(item.page, { ctrlKey: true })\"\n >\n <ion-label>\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: item.labelParts }\"></ng-container>\n </ion-label>\n <ion-note slot=\"end\" class=\"page-path ion-hide-sm-down\">\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: item.pageParts }\"></ng-container>\n </ion-note>\n </ion-item>\n }\n </ion-item-group>\n }\n </ion-list>\n } @else if (searchText()) {\n <!-- No result: the message is displayed under the search bar -->\n <div class=\"ion-padding ion-text-center\">\n <ion-button fill=\"clear\" (click)=\"clearSearch()\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'TESTING_PAGE.BTN_CLEAR_SEARCH' | translate }}\n </ion-button>\n </div>\n }\n</ion-content>\n\n<ng-template #textParts let-parts>\n <!-- Keep on a single line, to avoid extra spaces around <mark> -->\n <!-- prettier-ignore -->\n @for (part of parts; track $index) {@if (part.match) {<mark>{{ part.text }}</mark>} @else {{{ part.text }}}}\n</ng-template>\n", styles: [":host ion-toolbar ion-searchbar{padding-top:4px;padding-bottom:4px}:host .search-count{display:flex;background:var(--ion-background-color, #fff);border-bottom:1px solid var(--ion-border-color, var(--ion-color-light-shade))}:host ion-button.search-active{--background: rgba(var(--ion-color-primary-contrast-rgb, 255, 255, 255), .2);--border-radius: 50%}:host ion-item.active{--background: var(--ion-color-light);box-shadow:inset 3px 0 0 var(--ion-color-primary)}:host .page-path{font-family:monospace;font-size:.8em}:host mark{background-color:var(--ion-color-warning-tint, #ffe187);color:inherit;padding:0;border-radius:2px}\n"], dependencies: [{ kind: "directive", type: i2$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$2.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: i2$2.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$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonItemDivider, selector: "ion-item-divider", inputs: ["color", "mode", "sticky"] }, { kind: "component", type: i2$2.IonItemGroup, selector: "ion-item-group" }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonMenuButton, selector: "ion-menu-button", inputs: ["autoHide", "color", "disabled", "menu", "mode", "type"] }, { kind: "component", type: i2$2.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonSearchbar, selector: "ion-searchbar", inputs: ["animated", "autocomplete", "autocorrect", "cancelButtonIcon", "cancelButtonText", "clearIcon", "color", "debounce", "disabled", "enterkeyhint", "inputmode", "mode", "name", "placeholder", "searchIcon", "showCancelButton", "showClearButton", "spellcheck", "type", "value"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
17865
18059
  }
17866
18060
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedTestsPage, decorators: [{
17867
18061
  type: Component,
17868
- args: [{ selector: 'app-tests-page', standalone: false, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-menu-button></ion-menu-button>\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n <ion-title>Tests</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n @if (pages?.length) {\n <ion-list>\n @for (item of pages; track item) {\n @if (item.divider) {\n <ion-item-divider>{{ item.label }}</ion-item-divider>\n } @else {\n <ion-item tappable class=\"text-1x\" (click)=\"openPage(item.page)\">{{ item.label }}</ion-item>\n }\n }\n </ion-list>\n }\n</ion-content>\n" }]
17869
- }], ctorParameters: () => [{ type: i1$5.ActivatedRoute }, { type: i1$5.Router }, { type: i2.TranslateService }, { type: undefined, decorators: [{
17870
- type: Optional
17871
- }, {
17872
- type: Inject,
17873
- args: [APP_TESTING_PAGES]
17874
- }] }] });
18062
+ args: [{ selector: 'app-tests-page', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-menu-button></ion-menu-button>\n <ion-back-button></ion-back-button>\n </ion-buttons>\n\n @if (showSearchbar()) {\n <!-- Search bar (replace the title, when expanded) -->\n <ion-searchbar\n color=\"light\"\n [value]=\"initialSearchText\"\n [debounce]=\"0\"\n [placeholder]=\"'TESTING_PAGE.SEARCH_PLACEHOLDER' | translate\"\n showClearButton=\"always\"\n autocapitalize=\"off\"\n (ionInput)=\"onSearchInput($event.detail.value)\"\n (ionClear)=\"clearSearch()\"\n (keydown)=\"onSearchKeydown($event)\"\n ></ion-searchbar>\n } @else {\n <ion-title>{{ 'TESTING_PAGE.TITLE' | translate }}</ion-title>\n }\n\n <ion-buttons slot=\"end\">\n <!-- Toggle the search bar -->\n <ion-button\n [class.search-active]=\"showSearchbar()\"\n [title]=\"(showSearchbar() ? 'TESTING_PAGE.BTN_HIDE_SEARCH' : 'TESTING_PAGE.BTN_SHOW_SEARCH') | translate\"\n (click)=\"toggleSearchbar()\"\n >\n <ion-icon slot=\"icon-only\" name=\"search\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n\n <!-- Result count (under the search bar) -->\n @if (showSearchbar() && searchText().trim()) {\n <div class=\"search-count\">\n <ion-label [color]=\"visibleCount() === 0 ? 'danger' : 'primary100'\" class=\"ion-padding\">\n {{ (visibleCount() ? 'COMMON.RESULT_COUNT' : 'COMMON.NO_RESULT') | translate: { count: visibleCount() } }}\n </ion-label>\n </div>\n }\n</ion-header>\n\n<ion-content>\n @if (sections().length) {\n <ion-list>\n @for (section of sections(); track $index) {\n <ion-item-group>\n @if (section.labelParts) {\n <ion-item-divider sticky>\n <ion-label>\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: section.labelParts }\"></ng-container>\n </ion-label>\n </ion-item-divider>\n }\n @for (item of section.items; track item.page) {\n <ion-item\n tappable\n class=\"text-1x\"\n [class.active]=\"searchText() && item.index === activeIndex()\"\n [title]=\"item.page\"\n (click)=\"openPage(item.page, $event)\"\n (auxclick)=\"$event.button === 1 && openPage(item.page, { ctrlKey: true })\"\n >\n <ion-label>\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: item.labelParts }\"></ng-container>\n </ion-label>\n <ion-note slot=\"end\" class=\"page-path ion-hide-sm-down\">\n <ng-container *ngTemplateOutlet=\"textParts; context: { $implicit: item.pageParts }\"></ng-container>\n </ion-note>\n </ion-item>\n }\n </ion-item-group>\n }\n </ion-list>\n } @else if (searchText()) {\n <!-- No result: the message is displayed under the search bar -->\n <div class=\"ion-padding ion-text-center\">\n <ion-button fill=\"clear\" (click)=\"clearSearch()\">\n <ion-icon slot=\"start\" name=\"close\"></ion-icon>\n {{ 'TESTING_PAGE.BTN_CLEAR_SEARCH' | translate }}\n </ion-button>\n </div>\n }\n</ion-content>\n\n<ng-template #textParts let-parts>\n <!-- Keep on a single line, to avoid extra spaces around <mark> -->\n <!-- prettier-ignore -->\n @for (part of parts; track $index) {@if (part.match) {<mark>{{ part.text }}</mark>} @else {{{ part.text }}}}\n</ng-template>\n", styles: [":host ion-toolbar ion-searchbar{padding-top:4px;padding-bottom:4px}:host .search-count{display:flex;background:var(--ion-background-color, #fff);border-bottom:1px solid var(--ion-border-color, var(--ion-color-light-shade))}:host ion-button.search-active{--background: rgba(var(--ion-color-primary-contrast-rgb, 255, 255, 255), .2);--border-radius: 50%}:host ion-item.active{--background: var(--ion-color-light);box-shadow:inset 3px 0 0 var(--ion-color-primary)}:host .page-path{font-family:monospace;font-size:.8em}:host mark{background-color:var(--ion-color-warning-tint, #ffe187);color:inherit;padding:0;border-radius:2px}\n"] }]
18063
+ }], ctorParameters: () => [], propDecorators: { searchbar: [{ type: i0.ViewChild, args: [i0.forwardRef(() => IonSearchbar), { isSignal: true }] }], onDocumentKeydown: [{
18064
+ type: HostListener,
18065
+ args: ['document:keydown', ['$event']]
18066
+ }] } });
17875
18067
 
17876
18068
  const StorageDrivers = {
17877
18069
  SQLLite: CordovaSQLiteDriver._driver,
@@ -22663,6 +22855,47 @@ function isSubscriptionOperation(operation) {
22663
22855
  const def = getMainDefinition(operation.query);
22664
22856
  return def.kind === 'OperationDefinition' && def.operation === 'subscription';
22665
22857
  }
22858
+ const DEFER_DIRECTIVE_NAME = 'defer';
22859
+ /**
22860
+ * Return true if the document uses the @defer directive
22861
+ */
22862
+ function hasDeferDirective(document) {
22863
+ if (!document)
22864
+ return false;
22865
+ let found = false;
22866
+ visit(document, {
22867
+ [Kind.DIRECTIVE]: (node) => {
22868
+ if (node.name.value === DEFER_DIRECTIVE_NAME)
22869
+ found = true;
22870
+ },
22871
+ });
22872
+ return found;
22873
+ }
22874
+ /**
22875
+ * Remove the @defer directives from a document (deferred fragments are then fetched with the rest of the query)
22876
+ */
22877
+ function removeDeferDirectives(document) {
22878
+ if (!hasDeferDirective(document))
22879
+ return document;
22880
+ return visit(document, {
22881
+ [Kind.DIRECTIVE]: (node) => (node.name.value === DEFER_DIRECTIVE_NAME ? null : undefined),
22882
+ });
22883
+ }
22884
+ function isDeferredOperation(operation) {
22885
+ return hasDeferDirective(operation.query);
22886
+ }
22887
+ /**
22888
+ * Convert Angular HttpHeaders (used by the apollo-angular HttpLink) into a plain object, as expected by the Apollo fetch HttpLink
22889
+ */
22890
+ const httpHeadersToRecordLink = new ApolloLink((operation, forward) => {
22891
+ const { headers } = operation.getContext();
22892
+ if (headers instanceof HttpHeaders) {
22893
+ const record = {};
22894
+ headers.keys().forEach((key) => (record[key] = headers.getAll(key).join(', ')));
22895
+ operation.setContext({ headers: record });
22896
+ }
22897
+ return forward(operation);
22898
+ });
22666
22899
  const TRACKED_QUERIES_STORAGE_KEY = 'apollo-tracker-persist';
22667
22900
  function createTrackerLink(opts) {
22668
22901
  const trackedQueriesUpdated = new EventEmitter();
@@ -22777,6 +23010,12 @@ class GraphqlService extends StartableService {
22777
23010
  get cache() {
22778
23011
  return this.apollo.client.cache;
22779
23012
  }
23013
+ /**
23014
+ * Is the @defer directive enabled (see environment.enableGraphqlDefer)? If not, @defer directives are removed from queries
23015
+ */
23016
+ get deferEnabled() {
23017
+ return this.environment.enableGraphqlDefer === true;
23018
+ }
22780
23019
  get defaultFetchPolicy() {
22781
23020
  return this._defaultFetchPolicy;
22782
23021
  }
@@ -22823,7 +23062,11 @@ class GraphqlService extends StartableService {
22823
23062
  });
22824
23063
  }
22825
23064
  queryRefValuesChanges(queryRef, opts) {
22826
- return queryRef.valueChanges.pipe(catchError(async (error) => this.toApolloError(error, opts.error)), filter((value) => value.networkStatus === NetworkStatus.ready || value.networkStatus === NetworkStatus.error), map(({ data, error }) => {
23065
+ return queryRef.valueChanges.pipe(catchError(async (error) => this.toApolloError(error, opts.error)),
23066
+ // Emit complete results, errors, and partial results of a @defer query (streaming)
23067
+ filter((value) => value.networkStatus === NetworkStatus.ready ||
23068
+ value.networkStatus === NetworkStatus.error ||
23069
+ value.networkStatus === NetworkStatus.streaming), map(({ data, error }) => {
22827
23070
  if (error) {
22828
23071
  throw error;
22829
23072
  }
@@ -33534,6 +33777,11 @@ class GitlabUtils {
33534
33777
  }
33535
33778
  }
33536
33779
 
33780
+ /** Default placeholder inserted inside wrap markers, when the selection is empty. */
33781
+ const WRAP_PLACEHOLDER = 'text';
33782
+ /** Default placeholders inserted by insertLink, when the selection is empty. */
33783
+ const LINK_PLACEHOLDER = 'link';
33784
+ const LINK_URL_PLACEHOLDER = 'url';
33537
33785
  class MarkdownUtils {
33538
33786
  static HTML_ENTITIES_MAP = {
33539
33787
  '&#39;': "'", // Single quote
@@ -33608,6 +33856,128 @@ class MarkdownUtils {
33608
33856
  return UrlUtils.getRootUrl(url);
33609
33857
  }
33610
33858
  }
33859
+ /** Markdown heading markers, longest first (used to replace an existing heading level). */
33860
+ const HEADING_MARKERS = ['###### ', '##### ', '#### ', '### ', '## ', '# '];
33861
+ /**
33862
+ * Wraps (or unwraps when already wrapped) the textarea selection with the given markdown markers.
33863
+ * Pure function: the caller is responsible for applying the result to the textarea and the control value.
33864
+ *
33865
+ * @param state current text and selection
33866
+ * @param prefix left marker (e.g. '**' for bold, '==' for highlight)
33867
+ * @param suffix right marker, defaults to prefix
33868
+ * @param placeholder text inserted when the selection is empty
33869
+ */
33870
+ function wrapSelection(state, prefix, suffix = prefix, placeholder = WRAP_PLACEHOLDER) {
33871
+ const text = state?.text ?? '';
33872
+ let start = state?.selectionStart ?? 0;
33873
+ let end = state?.selectionEnd ?? 0;
33874
+ // Toggle off: selection directly surrounded by markers -> remove them
33875
+ if (start !== end && text.substring(Math.max(0, start - prefix.length), start) === prefix && text.substring(end, end + suffix.length) === suffix) {
33876
+ const before = text.substring(0, Math.max(0, start - prefix.length));
33877
+ const selection = text.substring(start, end);
33878
+ const after = text.substring(end + suffix.length);
33879
+ return {
33880
+ text: before + selection + after,
33881
+ selectionStart: start - prefix.length,
33882
+ selectionEnd: end - prefix.length,
33883
+ };
33884
+ }
33885
+ // Toggle off: selection itself contains the markers -> unwrap them
33886
+ let innerStart = start;
33887
+ let innerEnd = end;
33888
+ if (text.substring(start, Math.min(end, start + prefix.length)) === prefix && text.substring(Math.max(start, end - suffix.length), end) === suffix) {
33889
+ innerStart += prefix.length;
33890
+ innerEnd -= suffix.length;
33891
+ }
33892
+ const isEmptySelection = start === end;
33893
+ // When the markers are inside the selection (toggle off), they are removed:
33894
+ // - content before/after the selection keeps its original positions;
33895
+ // - the content starts at the original selection start.
33896
+ const didUnwrap = innerStart !== start;
33897
+ const before = text.substring(0, didUnwrap ? start : innerStart);
33898
+ const selection = text.substring(innerStart, innerEnd) || placeholder;
33899
+ // The trailing content continues after the original selection end (markers, if any, were inside it)
33900
+ const after = text.substring(end);
33901
+ // Caret right after the prefix marker; the selected text stays selected when wrapping existing text
33902
+ const selectionStart = didUnwrap ? start : start + prefix.length;
33903
+ const selectionEnd = didUnwrap ? start + selection.length : isEmptySelection ? selectionStart : selectionStart + selection.length;
33904
+ // Toggle off: the markers were inside the selection, just remove them
33905
+ const nextText = didUnwrap ? before + selection + after : before + prefix + selection + suffix + after;
33906
+ return {
33907
+ text: nextText,
33908
+ selectionStart,
33909
+ selectionEnd,
33910
+ };
33911
+ }
33912
+ /**
33913
+ * Toggles a line marker (e.g. '# ' for heading 1) on every involved line of the selection.
33914
+ * When all involved lines already have the marker, it is removed; another heading marker is replaced.
33915
+ */
33916
+ function toggleLinePrefix(state, marker) {
33917
+ const text = state?.text ?? '';
33918
+ let start = state?.selectionStart ?? 0;
33919
+ let end = state?.selectionEnd ?? 0;
33920
+ // Line boundaries covering the selection
33921
+ const lineStart = text.lastIndexOf('\n', Math.max(0, start - 1)) + 1;
33922
+ const nextNewLine = text.indexOf('\n', end);
33923
+ const lineEnd = nextNewLine === -1 ? text.length : nextNewLine;
33924
+ const block = text.substring(lineStart, lineEnd);
33925
+ const lines = block.length ? block.split('\n') : [''];
33926
+ if (lines.every((line) => line.startsWith(marker))) {
33927
+ // Toggle off: remove the marker on each involved line
33928
+ return {
33929
+ text: text.substring(0, lineStart) + lines.map((line) => line.substring(marker.length)).join('\n') + text.substring(lineEnd),
33930
+ selectionStart: lineStart,
33931
+ selectionEnd: lineEnd - marker.length * lines.length,
33932
+ };
33933
+ }
33934
+ // Add (or replace an existing heading marker) on each involved line
33935
+ const replaceExisting = HEADING_MARKERS.filter((m) => m !== marker);
33936
+ const prefixed = lines
33937
+ .map((line) => {
33938
+ const existing = replaceExisting.find((m) => line.startsWith(m));
33939
+ return existing ? marker + line.substring(existing.length) : marker + line;
33940
+ })
33941
+ .join('\n');
33942
+ const diff = prefixed.length - block.length;
33943
+ return {
33944
+ text: text.substring(0, lineStart) + prefixed + text.substring(lineEnd),
33945
+ // Empty line(s): place the caret after the inserted marker; otherwise select the prefixed block
33946
+ selectionStart: block.length ? lineStart : lineStart + diff,
33947
+ selectionEnd: lineEnd + diff,
33948
+ };
33949
+ }
33950
+ /**
33951
+ * Inserts a markdown link: turns the selected text into a link label, or inserts a template
33952
+ * with the given placeholders when the selection is empty. The label stays selected afterwards.
33953
+ */
33954
+ function insertLink(state, opts) {
33955
+ const text = state?.text ?? '';
33956
+ const start = Math.min(state?.selectionStart ?? 0, text.length);
33957
+ const end = Math.min(state?.selectionEnd ?? 0, text.length);
33958
+ const url = opts?.url ?? LINK_URL_PLACEHOLDER;
33959
+ const label = start === end ? (opts?.label ?? LINK_PLACEHOLDER) : text.substring(start, end);
33960
+ const link = `[${label}](${url})`;
33961
+ const before = text.substring(0, start);
33962
+ const after = text.substring(end);
33963
+ let labelStart;
33964
+ let labelEnd;
33965
+ if (start === end) {
33966
+ // Empty selection: select the default label, so the user can type over it
33967
+ labelStart = before.length + 1;
33968
+ labelEnd = labelStart + label.length;
33969
+ }
33970
+ else {
33971
+ // Reuse the existing text as label: keep it selected
33972
+ labelStart = start + 1;
33973
+ labelEnd = labelStart + label.length;
33974
+ }
33975
+ return {
33976
+ text: before + link + after,
33977
+ selectionStart: labelStart,
33978
+ selectionEnd: labelEnd,
33979
+ };
33980
+ }
33611
33981
 
33612
33982
  class MarkdownService {
33613
33983
  route = inject(ActivatedRoute);
@@ -34712,6 +35082,148 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
34712
35082
  }]
34713
35083
  }] });
34714
35084
 
35085
+ /**
35086
+ * Marked inline extension rendering `==highlighted text==` as `<mark>` elements.
35087
+ *
35088
+ * The highlight syntax has no GFM standard: this extension completes the renderer
35089
+ * used by ngx-markdown, so composed messages (feed, inbox) render the same way as they are previewed.
35090
+ *
35091
+ * Registered through `SharedMarkdownModule.forRoot()` (`markedExtensions` option),
35092
+ * and testable without Angular by applying it on a `Marked` instance.
35093
+ */
35094
+ const compositionMarkdownExtensions = [
35095
+ {
35096
+ extensions: [
35097
+ {
35098
+ name: 'highlight',
35099
+ level: 'inline',
35100
+ start(src) {
35101
+ const index = src.indexOf('==');
35102
+ return index === -1 ? undefined : index;
35103
+ },
35104
+ // Same-line content only, and no unbalanced side (e.g. 'a == b' stays as-is)
35105
+ tokenizer(src) {
35106
+ const match = /^==(?!\s)([^=\n]+?)(?<!\s)==/.exec(src);
35107
+ if (!match) {
35108
+ return undefined;
35109
+ }
35110
+ return {
35111
+ type: 'highlight',
35112
+ raw: match[0],
35113
+ text: match[1],
35114
+ // Keep nested inline syntaxes renderable (e.g. '==a **bold** b==')
35115
+ tokens: this.lexer.inlineTokens(match[1]),
35116
+ };
35117
+ },
35118
+ renderer(token) {
35119
+ return `<mark>${this.parser.parseInline(token.tokens ?? token.text)}</mark>`;
35120
+ },
35121
+ },
35122
+ ],
35123
+ },
35124
+ ];
35125
+
35126
+ /**
35127
+ * Markdown composition toolbar: a row of buttons that insert markdown syntax
35128
+ * at the caret of the linked textarea (bold, underline, strikethrough, highlight, headings, bullet list, link, attachment).
35129
+ *
35130
+ * Purely presentational towards the text: the component emits the new text through
35131
+ * {@link textChange}, so the host remains owner of the form control value.
35132
+ * The caret and selection are restored on the textarea after each action.
35133
+ */
35134
+ class MarkdownToolbarComponent {
35135
+ /** The textarea to edit (native element). */
35136
+ textarea;
35137
+ /** Disable all buttons (e.g. when the host form is disabled). */
35138
+ disabled = false;
35139
+ /** Disable the edition buttons, but not the preview toggle (e.g. when the preview replaces the text). */
35140
+ editDisabled = false;
35141
+ /** Show the attachment (paper clip) button. */
35142
+ canAttach = false;
35143
+ /** Reflects the current preview state (highlight the toggle button when active). */
35144
+ preview = false;
35145
+ /** Emits the new text value after each insertion. */
35146
+ textChange = new EventEmitter();
35147
+ /** Emits when the attachment button is clicked. */
35148
+ attachClick = new EventEmitter();
35149
+ /** Emits when the preview toggle button is clicked. */
35150
+ previewToggle = new EventEmitter();
35151
+ bold() {
35152
+ this.apply(wrapSelection(this.currentState(), '**', '**'));
35153
+ }
35154
+ /** Underline: no markdown syntax exists, so use the inline HTML `<u>` tag (rendered by marked, and portable) */
35155
+ underline() {
35156
+ this.apply(wrapSelection(this.currentState(), '<u>', '</u>'));
35157
+ }
35158
+ /** Strikethrough (GFM syntax) */
35159
+ strikethrough() {
35160
+ this.apply(wrapSelection(this.currentState(), '~~', '~~'));
35161
+ }
35162
+ highlight() {
35163
+ this.apply(wrapSelection(this.currentState(), '==', '=='));
35164
+ }
35165
+ heading1() {
35166
+ this.apply(toggleLinePrefix(this.currentState(), '# '));
35167
+ }
35168
+ heading2() {
35169
+ this.apply(toggleLinePrefix(this.currentState(), '## '));
35170
+ }
35171
+ /** Bullet list: toggle the '- ' marker on each selected line */
35172
+ bulletList() {
35173
+ this.apply(toggleLinePrefix(this.currentState(), '- '));
35174
+ }
35175
+ link() {
35176
+ this.apply(insertLink(this.currentState()));
35177
+ }
35178
+ onAttachClick(event) {
35179
+ this.attachClick.emit(event);
35180
+ }
35181
+ togglePreview() {
35182
+ this.previewToggle.emit();
35183
+ }
35184
+ currentState() {
35185
+ const el = this.textarea;
35186
+ return {
35187
+ text: el?.value ?? '',
35188
+ selectionStart: el?.selectionStart ?? 0,
35189
+ selectionEnd: el?.selectionEnd ?? 0,
35190
+ };
35191
+ }
35192
+ apply(next) {
35193
+ if (!this.textarea) {
35194
+ return;
35195
+ }
35196
+ // Update the textarea value first, then the host's control value (synchronous),
35197
+ // then restore the caret, as updating the control may re-write the textarea value.
35198
+ this.textarea.value = next.text;
35199
+ this.textChange.emit(next.text);
35200
+ this.textarea.focus();
35201
+ this.textarea.setSelectionRange(next.selectionStart, next.selectionEnd);
35202
+ }
35203
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownToolbarComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
35204
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MarkdownToolbarComponent, isStandalone: false, selector: "app-markdown-toolbar", inputs: { textarea: "textarea", disabled: "disabled", editDisabled: "editDisabled", canAttach: "canAttach", preview: "preview" }, outputs: { textChange: "textChange", attachClick: "attachClick", previewToggle: "previewToggle" }, ngImport: i0, template: "<ion-toolbar role=\"toolbar\">\n <ion-buttons slot=\"start\">\n <!-- Bold -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"bold()\"\n [title]=\"'MARKDOWN.TOOLBAR.BOLD' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_bold</mat-icon>\n </ion-button>\n\n <!-- Underline (<u>text</u>) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"underline()\"\n [title]=\"'MARKDOWN.TOOLBAR.UNDERLINE' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_underlined</mat-icon>\n </ion-button>\n\n <!-- Strikethrough (~~text~~) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"strikethrough()\"\n [title]=\"'MARKDOWN.TOOLBAR.STRIKETHROUGH' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_strikethrough</mat-icon>\n </ion-button>\n\n <!-- Highlight (==text==) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"highlight()\"\n [title]=\"'MARKDOWN.TOOLBAR.HIGHLIGHT' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_color_text</mat-icon>\n </ion-button>\n\n <!-- Heading 1 -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"heading1()\"\n [title]=\"'MARKDOWN.TOOLBAR.HEADING1' | translate\"\n tabindex=\"-1\"\n >\n H1\n </ion-button>\n\n <!-- Heading 2 -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"heading2()\"\n [title]=\"'MARKDOWN.TOOLBAR.HEADING2' | translate\"\n tabindex=\"-1\"\n >\n H2\n </ion-button>\n\n <!-- Bullet list (- item) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"bulletList()\"\n [title]=\"'MARKDOWN.TOOLBAR.BULLET_LIST' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_list_bulleted</mat-icon>\n </ion-button>\n\n <!-- Link -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"link()\"\n [title]=\"'MARKDOWN.TOOLBAR.LINK' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">link</mat-icon>\n </ion-button>\n\n <!-- Attachment (paper clip) -->\n @if (canAttach) {\n <ion-button\n data-testid=\"attach-button\"\n [disabled]=\"disabled || editDisabled\"\n (click)=\"onAttachClick($event)\"\n [title]=\"'MARKDOWN.TOOLBAR.ATTACHMENT' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">attach_file</mat-icon>\n </ion-button>\n }\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n <!-- Preview toggle -->\n <ion-button\n data-testid=\"preview-button\"\n [disabled]=\"disabled\"\n [color]=\"preview ? 'primary' : undefined\"\n [class.active]=\"preview\"\n (click)=\"togglePreview()\"\n [title]=\"'MARKDOWN.TOOLBAR.PREVIEW' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">{{ preview ? 'visibility_off' : 'visibility' }}</mat-icon>\n </ion-button>\n </ion-buttons>\n</ion-toolbar>\n", styles: [":host{display:block}ion-toolbar{--min-height: 40px;--background: transparent}\n"], dependencies: [{ kind: "component", type: i2$2.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$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
35205
+ }
35206
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MarkdownToolbarComponent, decorators: [{
35207
+ type: Component,
35208
+ args: [{ selector: 'app-markdown-toolbar', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-toolbar role=\"toolbar\">\n <ion-buttons slot=\"start\">\n <!-- Bold -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"bold()\"\n [title]=\"'MARKDOWN.TOOLBAR.BOLD' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_bold</mat-icon>\n </ion-button>\n\n <!-- Underline (<u>text</u>) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"underline()\"\n [title]=\"'MARKDOWN.TOOLBAR.UNDERLINE' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_underlined</mat-icon>\n </ion-button>\n\n <!-- Strikethrough (~~text~~) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"strikethrough()\"\n [title]=\"'MARKDOWN.TOOLBAR.STRIKETHROUGH' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_strikethrough</mat-icon>\n </ion-button>\n\n <!-- Highlight (==text==) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"highlight()\"\n [title]=\"'MARKDOWN.TOOLBAR.HIGHLIGHT' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_color_text</mat-icon>\n </ion-button>\n\n <!-- Heading 1 -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"heading1()\"\n [title]=\"'MARKDOWN.TOOLBAR.HEADING1' | translate\"\n tabindex=\"-1\"\n >\n H1\n </ion-button>\n\n <!-- Heading 2 -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"heading2()\"\n [title]=\"'MARKDOWN.TOOLBAR.HEADING2' | translate\"\n tabindex=\"-1\"\n >\n H2\n </ion-button>\n\n <!-- Bullet list (- item) -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"bulletList()\"\n [title]=\"'MARKDOWN.TOOLBAR.BULLET_LIST' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">format_list_bulleted</mat-icon>\n </ion-button>\n\n <!-- Link -->\n <ion-button\n [disabled]=\"disabled || editDisabled\"\n (click)=\"link()\"\n [title]=\"'MARKDOWN.TOOLBAR.LINK' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">link</mat-icon>\n </ion-button>\n\n <!-- Attachment (paper clip) -->\n @if (canAttach) {\n <ion-button\n data-testid=\"attach-button\"\n [disabled]=\"disabled || editDisabled\"\n (click)=\"onAttachClick($event)\"\n [title]=\"'MARKDOWN.TOOLBAR.ATTACHMENT' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">attach_file</mat-icon>\n </ion-button>\n }\n </ion-buttons>\n\n <ion-buttons slot=\"end\">\n <!-- Preview toggle -->\n <ion-button\n data-testid=\"preview-button\"\n [disabled]=\"disabled\"\n [color]=\"preview ? 'primary' : undefined\"\n [class.active]=\"preview\"\n (click)=\"togglePreview()\"\n [title]=\"'MARKDOWN.TOOLBAR.PREVIEW' | translate\"\n tabindex=\"-1\"\n >\n <mat-icon slot=\"icon-only\">{{ preview ? 'visibility_off' : 'visibility' }}</mat-icon>\n </ion-button>\n </ion-buttons>\n</ion-toolbar>\n", styles: [":host{display:block}ion-toolbar{--min-height: 40px;--background: transparent}\n"] }]
35209
+ }], propDecorators: { textarea: [{
35210
+ type: Input
35211
+ }], disabled: [{
35212
+ type: Input
35213
+ }], editDisabled: [{
35214
+ type: Input
35215
+ }], canAttach: [{
35216
+ type: Input
35217
+ }], preview: [{
35218
+ type: Input
35219
+ }], textChange: [{
35220
+ type: Output
35221
+ }], attachClick: [{
35222
+ type: Output
35223
+ }], previewToggle: [{
35224
+ type: Output
35225
+ }] } });
35226
+
34715
35227
  // function that returns `MarkedOptions` with renderer override
34716
35228
  class SharedMarkdownModule {
34717
35229
  static forRoot() {
@@ -34726,6 +35238,12 @@ class SharedMarkdownModule {
34726
35238
  provide: SANITIZE,
34727
35239
  useValue: SecurityContext.NONE,
34728
35240
  },
35241
+ // Composition extensions (e.g. ==highlight==), used by the markdown toolbar
35242
+ markedExtensions: compositionMarkdownExtensions.map((extension) => ({
35243
+ provide: MARKED_EXTENSIONS,
35244
+ useValue: extension,
35245
+ multi: true,
35246
+ })),
34729
35247
  markedOptions: {
34730
35248
  provide: MARKED_OPTIONS,
34731
35249
  deps: [MarkdownService],
@@ -34750,15 +35268,18 @@ class SharedMarkdownModule {
34750
35268
  // Component
34751
35269
  AppMarkdownContent,
34752
35270
  AppMarkdownModal,
35271
+ MarkdownToolbarComponent,
34753
35272
  // Directive
34754
35273
  MarkdownDirective], imports: [CommonModule,
34755
35274
  SharedIonicModule,
34756
- TranslatePipe, i4$3.MarkdownModule,
35275
+ TranslatePipe,
35276
+ SharedMaterialModule, i4$3.MarkdownModule,
34757
35277
  // App module
34758
35278
  SharedToolbarModule,
34759
- SharedDebugModule], exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective] });
35279
+ SharedDebugModule], exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective, MarkdownToolbarComponent] });
34760
35280
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SharedMarkdownModule, providers: [provideChildTranslateService()], imports: [CommonModule,
34761
35281
  SharedIonicModule,
35282
+ SharedMaterialModule,
34762
35283
  MarkdownModule.forChild(),
34763
35284
  // App module
34764
35285
  SharedToolbarModule,
@@ -34771,6 +35292,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
34771
35292
  CommonModule,
34772
35293
  SharedIonicModule,
34773
35294
  TranslatePipe,
35295
+ SharedMaterialModule,
34774
35296
  MarkdownModule.forChild(),
34775
35297
  // App module
34776
35298
  SharedToolbarModule,
@@ -34780,10 +35302,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
34780
35302
  // Component
34781
35303
  AppMarkdownContent,
34782
35304
  AppMarkdownModal,
35305
+ MarkdownToolbarComponent,
34783
35306
  // Directive
34784
35307
  MarkdownDirective,
34785
35308
  ],
34786
- exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective],
35309
+ exports: [MarkdownModule, AppMarkdownContent, MarkdownDirective, MarkdownToolbarComponent],
34787
35310
  providers: [provideChildTranslateService()],
34788
35311
  }]
34789
35312
  }] });
@@ -39529,6 +40052,7 @@ class MessageForm extends AppForm {
39529
40052
  /**
39530
40053
  * Allow the user to attach files to the message.
39531
40054
  * When enabled, {@link uploadFn} and {@link deleteFn} should also be provided (see `UploadFileComponent`).
40055
+ * Also used by the markdown toolbar to show its attachment (paper clip) button.
39532
40056
  */
39533
40057
  canAttach = false;
39534
40058
  /** Upload callback, called for each added file (usually a call to the REST endpoint `POST api/upload`) */
@@ -39537,7 +40061,18 @@ class MessageForm extends AppForm {
39537
40061
  deleteFn;
39538
40062
  /** Max size of an attached file, in megabytes (default: 20 Mo) */
39539
40063
  maxAttachmentSizeMb = 20;
40064
+ /**
40065
+ * Layout of the body preview: side by side with the body text (2 columns), or replacing the text (e.g. on small screens).
40066
+ * The edition toolbar buttons are disabled when the preview replaces the text.
40067
+ */
40068
+ bodyPreviewSideBySide = false;
39540
40069
  searchRecipients = new EventEmitter();
40070
+ /** Emits when the attachment button is clicked (after opening the file browser) */
40071
+ attach = new EventEmitter();
40072
+ /** Emits when the body preview is toggled (e.g. to let the parent modal grow, if side by side) */
40073
+ bodyPreviewChange = new EventEmitter();
40074
+ /** Whether the markdown preview of the body is displayed. */
40075
+ bodyPreview = false;
39541
40076
  attachmentsUpload;
39542
40077
  types = MessageTypeList;
39543
40078
  /** Existing attachments of the message (already stored by the pod, e.g. when editing a feed item) */
@@ -39592,6 +40127,18 @@ class MessageForm extends AppForm {
39592
40127
  isSamePerson(o1, o2) {
39593
40128
  return EntityUtils.equals(o1, o2, 'id');
39594
40129
  }
40130
+ /**
40131
+ * New body value, emitted by the markdown toolbar after an insertion at the caret.
40132
+ * The textarea value (and its caret) has already been updated by the toolbar.
40133
+ */
40134
+ onBodyTextChange(value) {
40135
+ this.form.controls.body.setValue(value);
40136
+ }
40137
+ toggleBodyPreview() {
40138
+ this.bodyPreview = !this.bodyPreview;
40139
+ this.bodyPreviewChange.emit(this.bodyPreview);
40140
+ this.cd.markForCheck();
40141
+ }
39595
40142
  setValue(data, opts) {
39596
40143
  this.existingAttachments = (data?.attachments || []).filter((a) => isNotNil(a?.id));
39597
40144
  this.uploadedAttachments = [];
@@ -39613,9 +40160,13 @@ class MessageForm extends AppForm {
39613
40160
  /**
39614
40161
  * Open the file browser, to add an attachment
39615
40162
  */
40163
+ /**
40164
+ * Open the file browser (called by the markdown toolbar attachment button), then emit {@link attach}
40165
+ */
39616
40166
  addAttachment(event) {
39617
40167
  event?.preventDefault();
39618
40168
  this.attachmentsUpload?.browse();
40169
+ this.attach.emit(event);
39619
40170
  }
39620
40171
  /**
39621
40172
  * Highlight the form when a file is dragged over it
@@ -39733,11 +40284,11 @@ class MessageForm extends AppForm {
39733
40284
  this.cd.markForCheck();
39734
40285
  }
39735
40286
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageForm, deps: [{ token: i0.Injector }, { token: i1$2.UntypedFormBuilder }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
39736
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MessageForm, isStandalone: false, selector: "app-message-form", inputs: { mobile: "mobile", suggestFn: "suggestFn", subjectMinLength: "subjectMinLength", subjectMaxLength: "subjectMaxLength", bodyMaxLength: "bodyMaxLength", bodyAutoHeight: "bodyAutoHeight", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount", canSearchRecipients: "canSearchRecipients", canAttach: ["canAttach", "canAttach", toBoolean], uploadFn: "uploadFn", deleteFn: "deleteFn", maxAttachmentSizeMb: "maxAttachmentSizeMb" }, outputs: { searchRecipients: "searchRecipients" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "class.file-over": "this.fileOver" } }, viewQueries: [{ propertyName: "attachmentsUpload", first: true, predicate: ["attachmentsUpload"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n @if (canSelectType) {\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n class=\"ion-flex-auto\"\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n [style.flex-grow]=\"'1'\"\n >\n <!-- Fix #2090 (Lot 7): search button inside the field frame (as suffix), using the 'list' icon\n (ellipsis-horizontal is not the expected icon, see GitLab #2090) -->\n @if (canSearchRecipients) {\n <button\n matSuffix\n mat-icon-button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"searchRecipients.emit($event)\"\n [title]=\"'COMMON.BTN_SEARCH' | translate\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n </mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n @if (form.controls.subject.hasError('required')) {\n <mat-error>{{ 'ERROR.FIELD_REQUIRED' | translate }}</mat-error>\n }\n @if (form.controls.subject.hasError('minlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (form.controls.subject.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (subjectMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n @if (form.controls.body.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n }\n @if (bodyMaxLength) {\n <mat-hint align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Attachments -->\n @if (canAttach) {\n <div class=\"message-attachments\">\n <ion-button\n class=\"add-attachment-button\"\n fill=\"clear\"\n size=\"small\"\n [disabled]=\"disabled\"\n (click)=\"addAttachment($event)\"\n >\n <mat-icon slot=\"start\">attach_file</mat-icon>\n <ion-label>{{ 'SOCIAL.MESSAGE.ATTACHMENTS.BTN_ADD' | translate }}</ion-label>\n </ion-button>\n\n <!-- existing attachments (already stored by the pod) -->\n @if (existingAttachments | isNotEmptyArray) {\n <ion-list class=\"existing-attachments\" lines=\"none\">\n @for (attachment of existingAttachments; track attachment.id) {\n <ion-item class=\"existing-attachment\">\n <mat-icon slot=\"start\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label>\n <h4>{{ attachment | map: getAttachmentName }}</h4>\n @if (attachment.size) {\n <p>{{ attachment.size | fileSize }}</p>\n }\n </ion-label>\n <button\n mat-icon-button\n slot=\"end\"\n class=\"remove-attachment\"\n [disabled]=\"disabled\"\n [title]=\"'SOCIAL.MESSAGE.ATTACHMENTS.BTN_REMOVE' | translate\"\n (click)=\"removeExistingAttachment($event, attachment)\"\n >\n <mat-icon>clear</mat-icon>\n </button>\n </ion-item>\n }\n </ion-list>\n }\n\n <!-- files list only (no drop area: files are added by the button above, or dropped anywhere in the form) -->\n <app-upload-file\n #attachmentsUpload\n [showDropArea]=\"false\"\n [instantUpload]=\"true\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxFileSizeMb]=\"maxAttachmentSizeMb\"\n (filesChange)=\"onAttachmentsChange($event)\"\n ></app-upload-file>\n @if (form.controls.attachments.getError('uploading')) {\n <ion-note color=\"danger\">\n {{ 'SOCIAL.MESSAGE.ATTACHMENTS.UPLOADING_ERROR' | translate }}\n </ion-note>\n }\n </div>\n }\n</form>\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json }}\n <br />\n recipients: {{ _form | formGetValue: 'recipients' | personToString }}\n <br />\n recipientFilter: {{ _form | formGetValue: 'recipientFilter' | json }}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}.message-attachments{margin-top:8px}:host.file-over{display:block;outline:2px dashed var(--ion-color-accent);outline-offset:4px}\n"], dependencies: [{ kind: "directive", type: i2$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$2.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$2.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$2.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i1$3.CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i9$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatChipsField, selector: "mat-chips-field", inputs: ["equals", "logPrefix", "formControl", "formControlName", "floatLabel", "placeholder", "suggestFn", "required", "mobile", "readonly", "clearable", "debounceTime", "displaySeparator", "displayWith", "displayAttributes", "displayColumnSizes", "displayColumnNames", "highlightAccent", "showAllOnFocus", "showPanelOnFocus", "autofocus", "config", "i18nPrefix", "noResultMessage", "panelClass", "panelWidth", "disableRipple", "matAutocompletePosition", "itemSize", "fetchMoreThreshold", "suggestLengthThreshold", "showLoadingSpinner", "chipColor", "debug", "applyImplicitValue", "dropButtonTitle", "clearButtonTitle", "trimSearchText", "hideRequiredMarker", "colSizes", "separatorKeysCodes", "showChips", "appearance", "subscriptSizing", "class", "filter", "tabindex", "items"], outputs: ["click", "blur", "focus", "dropButtonClick", "keydown.escape", "keyup.enter"] }, { kind: "directive", type: AutoResizeDirective, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMaxRows", "cdkAutosizeMinRows"] }, { kind: "component", type: UploadFileComponent, selector: "app-upload-file", inputs: ["fileExtension", "fileExtensionsExcluded", "uniqueFile", "instantUpload", "uploadFn", "deleteFn", "maxParallelUpload", "maxFileSizeMb", "autoHideDropArea", "showDropArea", "showThumbnails"], outputs: ["filesChange"] }, { kind: "component", type: i11$1.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "component", type: i11$1.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "pipe", type: i2$3.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$3.JsonPipe, name: "json" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: FormErrorPipe, name: "formError" }, { kind: "pipe", type: FormGetValuePipe, name: "formGetValue" }, { kind: "pipe", type: MapPipe, name: "map" }, { kind: "pipe", type: PersonToStringPipe, name: "personToString" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
40287
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MessageForm, isStandalone: false, selector: "app-message-form", inputs: { mobile: "mobile", suggestFn: "suggestFn", subjectMinLength: "subjectMinLength", subjectMaxLength: "subjectMaxLength", bodyMaxLength: "bodyMaxLength", bodyAutoHeight: "bodyAutoHeight", canSelectType: "canSelectType", canRecipientFilter: "canRecipientFilter", recipientFilterCount: "recipientFilterCount", canSearchRecipients: "canSearchRecipients", canAttach: ["canAttach", "canAttach", toBoolean], uploadFn: "uploadFn", deleteFn: "deleteFn", maxAttachmentSizeMb: "maxAttachmentSizeMb", bodyPreviewSideBySide: ["bodyPreviewSideBySide", "bodyPreviewSideBySide", toBoolean] }, outputs: { searchRecipients: "searchRecipients", attach: "attach", bodyPreviewChange: "bodyPreviewChange" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "class.file-over": "this.fileOver" } }, viewQueries: [{ propertyName: "attachmentsUpload", first: true, predicate: ["attachmentsUpload"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n @if (canSelectType) {\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n class=\"ion-flex-auto\"\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n [style.flex-grow]=\"'1'\"\n >\n <!-- Fix #2090 (Lot 7): search button inside the field frame (as suffix), using the 'list' icon\n (ellipsis-horizontal is not the expected icon, see GitLab #2090) -->\n @if (canSearchRecipients) {\n <button\n matSuffix\n mat-icon-button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"searchRecipients.emit($event)\"\n [title]=\"'COMMON.BTN_SEARCH' | translate\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n </mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n @if (form.controls.subject.hasError('required')) {\n <mat-error>{{ 'ERROR.FIELD_REQUIRED' | translate }}</mat-error>\n }\n @if (form.controls.subject.hasError('minlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (form.controls.subject.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (subjectMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Body -->\n <!-- Markdown composition toolbar (bold, highlight, headings, link, attachment) -->\n <app-markdown-toolbar\n [textarea]=\"bodyTextarea\"\n [canAttach]=\"canAttach\"\n [disabled]=\"disabled\"\n (attachClick)=\"addAttachment($event)\"\n (textChange)=\"onBodyTextChange($event)\"\n [preview]=\"bodyPreview\"\n [editDisabled]=\"bodyPreview && !bodyPreviewSideBySide\"\n (previewToggle)=\"toggleBodyPreview()\"\n ></app-markdown-toolbar>\n <!-- Body text and preview: side by side (2 columns), or the preview replaces the text -->\n <ion-row class=\"ion-no-padding\">\n <ion-col\n class=\"ion-no-padding\"\n [size]=\"bodyPreview && bodyPreviewSideBySide ? 6 : 12\"\n [hidden]=\"bodyPreview && !bodyPreviewSideBySide\"\n >\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n #bodyTextarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n @if (form.controls.body.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n }\n @if (bodyMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n </ion-col>\n\n <!-- Body preview (markdown rendering, toggled by the toolbar) -->\n @if (bodyPreview) {\n <ion-col\n class=\"ion-no-padding\"\n [size]=\"bodyPreviewSideBySide ? 6 : 12\"\n [class.ion-padding-start]=\"bodyPreviewSideBySide\"\n >\n <div class=\"body-preview\" data-testid=\"body-preview\">\n <markdown [data]=\"form.controls.body.value || ''\" emoji></markdown>\n </div>\n </ion-col>\n }\n </ion-row>\n\n <!-- Attachments -->\n @if (canAttach) {\n <div class=\"message-attachments\">\n <!-- existing attachments (already stored by the pod) -->\n @if (existingAttachments | isNotEmptyArray) {\n <ion-list class=\"existing-attachments\" lines=\"none\">\n @for (attachment of existingAttachments; track attachment.id) {\n <ion-item class=\"existing-attachment\">\n <mat-icon slot=\"start\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label>\n <h4>{{ attachment | map: getAttachmentName }}</h4>\n @if (attachment.size) {\n <p>{{ attachment.size | fileSize }}</p>\n }\n </ion-label>\n <button\n mat-icon-button\n slot=\"end\"\n class=\"remove-attachment\"\n [disabled]=\"disabled\"\n [title]=\"'SOCIAL.MESSAGE.ATTACHMENTS.BTN_REMOVE' | translate\"\n (click)=\"removeExistingAttachment($event, attachment)\"\n >\n <mat-icon>clear</mat-icon>\n </button>\n </ion-item>\n }\n </ion-list>\n }\n\n <!-- files list only (no drop area: files are added by the toolbar attachment button, or dropped anywhere in the form) -->\n <app-upload-file\n #attachmentsUpload\n [showDropArea]=\"false\"\n [instantUpload]=\"true\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxFileSizeMb]=\"maxAttachmentSizeMb\"\n (filesChange)=\"onAttachmentsChange($event)\"\n ></app-upload-file>\n @if (form.controls.attachments.getError('uploading')) {\n <ion-note color=\"danger\">\n {{ 'SOCIAL.MESSAGE.ATTACHMENTS.UPLOADING_ERROR' | translate }}\n </ion-note>\n }\n </div>\n }\n</form>\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json }}\n <br />\n recipients: {{ _form | formGetValue: 'recipients' | personToString }}\n <br />\n recipientFilter: {{ _form | formGetValue: 'recipientFilter' | json }}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}app-markdown-toolbar{display:block;margin-bottom:4px}.body-preview{box-sizing:border-box;height:calc(100% - 22px);min-height:calc(11.5em + 32px);padding:0 1em;overflow:auto;border:1px dashed var(--ion-item-border-color, #c8cdd4);border-radius:4px}.body-preview>markdown{display:block}.message-attachments{margin-top:8px;--app-upload-file-list-max-height: none}:host.file-over{display:block;outline:2px dashed var(--ion-color-accent);outline-offset:4px}\n"], dependencies: [{ kind: "directive", type: i2$3.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: i2$2.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$2.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonList, selector: "ion-list", inputs: ["inset", "lines", "mode"] }, { kind: "component", type: i2$2.IonNote, selector: "ion-note", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "directive", type: i1$2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: i4$3.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: i3.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i3.MatLabel, selector: "mat-label" }, { kind: "directive", type: i3.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i3.MatError, selector: "mat-error, [matError]", inputs: ["id"] }, { kind: "directive", type: i3.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "directive", type: i4.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "directive", type: i1$3.CdkTextareaAutosize, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMinRows", "cdkAutosizeMaxRows", "cdkTextareaAutosize", "placeholder"], exportAs: ["cdkTextareaAutosize"] }, { kind: "component", type: i6$2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: i9.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i9$1.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i9$1.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "component", type: MatChipsField, selector: "mat-chips-field", inputs: ["equals", "logPrefix", "formControl", "formControlName", "floatLabel", "placeholder", "suggestFn", "required", "mobile", "readonly", "clearable", "debounceTime", "displaySeparator", "displayWith", "displayAttributes", "displayColumnSizes", "displayColumnNames", "highlightAccent", "showAllOnFocus", "showPanelOnFocus", "autofocus", "config", "i18nPrefix", "noResultMessage", "panelClass", "panelWidth", "disableRipple", "matAutocompletePosition", "itemSize", "fetchMoreThreshold", "suggestLengthThreshold", "showLoadingSpinner", "chipColor", "debug", "applyImplicitValue", "dropButtonTitle", "clearButtonTitle", "trimSearchText", "hideRequiredMarker", "colSizes", "separatorKeysCodes", "showChips", "appearance", "subscriptSizing", "class", "filter", "tabindex", "items"], outputs: ["click", "blur", "focus", "dropButtonClick", "keydown.escape", "keyup.enter"] }, { kind: "directive", type: AutoResizeDirective, selector: "textarea[cdkTextareaAutosize]", inputs: ["cdkAutosizeMaxRows", "cdkAutosizeMinRows"] }, { kind: "component", type: UploadFileComponent, selector: "app-upload-file", inputs: ["fileExtension", "fileExtensionsExcluded", "uniqueFile", "instantUpload", "uploadFn", "deleteFn", "maxParallelUpload", "maxFileSizeMb", "autoHideDropArea", "showDropArea", "showThumbnails"], outputs: ["filesChange"] }, { kind: "component", type: i11$1.MatChipGrid, selector: "mat-chip-grid", inputs: ["disabled", "placeholder", "required", "value", "errorStateMatcher"], outputs: ["change", "valueChange"] }, { kind: "component", type: i11$1.MatChipRow, selector: "mat-chip-row, [mat-chip-row], mat-basic-chip-row, [mat-basic-chip-row]", inputs: ["editable"], outputs: ["edited"] }, { kind: "component", type: DebugComponent, selector: "app-debug", inputs: ["titlePrefix", "title", "enable", "expanded"] }, { kind: "directive", type: MarkdownDirective, selector: "markdown,[markdown]" }, { kind: "component", type: MarkdownToolbarComponent, selector: "app-markdown-toolbar", inputs: ["textarea", "disabled", "editDisabled", "canAttach", "preview"], outputs: ["textChange", "attachClick", "previewToggle"] }, { kind: "pipe", type: i2$3.AsyncPipe, name: "async" }, { kind: "pipe", type: i2$3.JsonPipe, name: "json" }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }, { kind: "pipe", type: FileSizePipe, name: "fileSize" }, { kind: "pipe", type: NotEmptyArrayPipe, name: "isNotEmptyArray" }, { kind: "pipe", type: FormErrorPipe, name: "formError" }, { kind: "pipe", type: FormGetValuePipe, name: "formGetValue" }, { kind: "pipe", type: MapPipe, name: "map" }, { kind: "pipe", type: PersonToStringPipe, name: "personToString" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
39737
40288
  }
39738
40289
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageForm, decorators: [{
39739
40290
  type: Component,
39740
- args: [{ selector: 'app-message-form', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n @if (canSelectType) {\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n class=\"ion-flex-auto\"\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n [style.flex-grow]=\"'1'\"\n >\n <!-- Fix #2090 (Lot 7): search button inside the field frame (as suffix), using the 'list' icon\n (ellipsis-horizontal is not the expected icon, see GitLab #2090) -->\n @if (canSearchRecipients) {\n <button\n matSuffix\n mat-icon-button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"searchRecipients.emit($event)\"\n [title]=\"'COMMON.BTN_SEARCH' | translate\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n </mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n @if (form.controls.subject.hasError('required')) {\n <mat-error>{{ 'ERROR.FIELD_REQUIRED' | translate }}</mat-error>\n }\n @if (form.controls.subject.hasError('minlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (form.controls.subject.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (subjectMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Body -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n @if (form.controls.body.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n }\n @if (bodyMaxLength) {\n <mat-hint align=\"end\">\n {{ 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength } }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Attachments -->\n @if (canAttach) {\n <div class=\"message-attachments\">\n <ion-button\n class=\"add-attachment-button\"\n fill=\"clear\"\n size=\"small\"\n [disabled]=\"disabled\"\n (click)=\"addAttachment($event)\"\n >\n <mat-icon slot=\"start\">attach_file</mat-icon>\n <ion-label>{{ 'SOCIAL.MESSAGE.ATTACHMENTS.BTN_ADD' | translate }}</ion-label>\n </ion-button>\n\n <!-- existing attachments (already stored by the pod) -->\n @if (existingAttachments | isNotEmptyArray) {\n <ion-list class=\"existing-attachments\" lines=\"none\">\n @for (attachment of existingAttachments; track attachment.id) {\n <ion-item class=\"existing-attachment\">\n <mat-icon slot=\"start\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label>\n <h4>{{ attachment | map: getAttachmentName }}</h4>\n @if (attachment.size) {\n <p>{{ attachment.size | fileSize }}</p>\n }\n </ion-label>\n <button\n mat-icon-button\n slot=\"end\"\n class=\"remove-attachment\"\n [disabled]=\"disabled\"\n [title]=\"'SOCIAL.MESSAGE.ATTACHMENTS.BTN_REMOVE' | translate\"\n (click)=\"removeExistingAttachment($event, attachment)\"\n >\n <mat-icon>clear</mat-icon>\n </button>\n </ion-item>\n }\n </ion-list>\n }\n\n <!-- files list only (no drop area: files are added by the button above, or dropped anywhere in the form) -->\n <app-upload-file\n #attachmentsUpload\n [showDropArea]=\"false\"\n [instantUpload]=\"true\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxFileSizeMb]=\"maxAttachmentSizeMb\"\n (filesChange)=\"onAttachmentsChange($event)\"\n ></app-upload-file>\n @if (form.controls.attachments.getError('uploading')) {\n <ion-note color=\"danger\">\n {{ 'SOCIAL.MESSAGE.ATTACHMENTS.UPLOADING_ERROR' | translate }}\n </ion-note>\n }\n </div>\n }\n</form>\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json }}\n <br />\n recipients: {{ _form | formGetValue: 'recipients' | personToString }}\n <br />\n recipientFilter: {{ _form | formGetValue: 'recipientFilter' | json }}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}.message-attachments{margin-top:8px}:host.file-over{display:block;outline:2px dashed var(--ion-color-accent);outline-offset:4px}\n"] }]
40291
+ args: [{ selector: 'app-message-form', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<!-- debug -->\n@if (debug) {\n <ng-container *ngTemplateOutlet=\"debugPanel\"></ng-container>\n}\n\n<form [formGroup]=\"form\" class=\"form-container\">\n <!-- type -->\n @if (canSelectType) {\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.TYPE' | translate }}</mat-label>\n <mat-select formControlName=\"type\">\n @for (item of types; track item.id) {\n @if (canRecipientFilter || item.id !== 'FEED') {\n <mat-option [value]=\"item.id\">\n <ion-icon [name]=\"item.icon\"></ion-icon>\n {{ item.label | translate }}\n </mat-option>\n }\n }\n </mat-select>\n </mat-form-field>\n }\n\n <!-- Recipients -->\n @if ((form | formGetValue: 'type') !== 'FEED') {\n <mat-chips-field\n class=\"ion-flex-auto\"\n formControlName=\"recipients\"\n chipColor=\"accent\"\n [placeholder]=\"'SOCIAL.MESSAGE.RECIPIENTS' | translate\"\n [config]=\"autocompleteFields.recipients\"\n [equals]=\"isSamePerson\"\n [style.flex-grow]=\"'1'\"\n >\n <!-- Fix #2090 (Lot 7): search button inside the field frame (as suffix), using the 'list' icon\n (ellipsis-horizontal is not the expected icon, see GitLab #2090) -->\n @if (canSearchRecipients) {\n <button\n matSuffix\n mat-icon-button\n type=\"button\"\n tabindex=\"-1\"\n (click)=\"searchRecipients.emit($event)\"\n [title]=\"'COMMON.BTN_SEARCH' | translate\"\n >\n <mat-icon>list</mat-icon>\n </button>\n }\n </mat-chips-field>\n } @else {\n <mat-form-field>\n <input matInput hidden formControlName=\"recipients\" type=\"text\" />\n <mat-chip-grid>\n <mat-chip-row>\n {{ 'SOCIAL.MESSAGE.RECIPIENT_FILTER_COUNT' | translate: { count: recipientFilterCount } }}\n </mat-chip-row>\n </mat-chip-grid>\n </mat-form-field>\n }\n\n <!-- Subject -->\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.SUBJECT' | translate }}</mat-label>\n <input matInput type=\"text\" formControlName=\"subject\" autocomplete=\"off\" required />\n @if (form.controls.subject.hasError('required')) {\n <mat-error>{{ 'ERROR.FIELD_REQUIRED' | translate }}</mat-error>\n }\n @if (form.controls.subject.hasError('minlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MIN_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (form.controls.subject.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH_COMPACT' | translate }}\n </mat-error>\n }\n @if (subjectMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.subject.value?.length || 0, max: subjectMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n\n <!-- Body -->\n <!-- Markdown composition toolbar (bold, highlight, headings, link, attachment) -->\n <app-markdown-toolbar\n [textarea]=\"bodyTextarea\"\n [canAttach]=\"canAttach\"\n [disabled]=\"disabled\"\n (attachClick)=\"addAttachment($event)\"\n (textChange)=\"onBodyTextChange($event)\"\n [preview]=\"bodyPreview\"\n [editDisabled]=\"bodyPreview && !bodyPreviewSideBySide\"\n (previewToggle)=\"toggleBodyPreview()\"\n ></app-markdown-toolbar>\n <!-- Body text and preview: side by side (2 columns), or the preview replaces the text -->\n <ion-row class=\"ion-no-padding\">\n <ion-col\n class=\"ion-no-padding\"\n [size]=\"bodyPreview && bodyPreviewSideBySide ? 6 : 12\"\n [hidden]=\"bodyPreview && !bodyPreviewSideBySide\"\n >\n <mat-form-field>\n <mat-label>{{ 'SOCIAL.MESSAGE.BODY_HELP' | translate }}</mat-label>\n <textarea\n #bodyTextarea\n matInput\n type=\"text\"\n formControlName=\"body\"\n [class.fixed-height]=\"!bodyAutoHeight\"\n [cdkTextareaAutosize]=\"bodyAutoHeight\"\n (keydown.control.enter)=\"doSubmit($event)\"\n ></textarea>\n @if (form.controls.body.hasError('maxlength')) {\n <mat-error>\n {{ 'ERROR.FIELD_MAX_LENGTH' | translate: form.controls.body.errors.maxlength }}\n </mat-error>\n }\n @if (bodyMaxLength) {\n <mat-hint align=\"end\">\n {{\n 'INFO.TEXT_PROGRESS' | translate: { current: form.controls.body.value?.length || 0, max: bodyMaxLength }\n }}\n </mat-hint>\n }\n </mat-form-field>\n </ion-col>\n\n <!-- Body preview (markdown rendering, toggled by the toolbar) -->\n @if (bodyPreview) {\n <ion-col\n class=\"ion-no-padding\"\n [size]=\"bodyPreviewSideBySide ? 6 : 12\"\n [class.ion-padding-start]=\"bodyPreviewSideBySide\"\n >\n <div class=\"body-preview\" data-testid=\"body-preview\">\n <markdown [data]=\"form.controls.body.value || ''\" emoji></markdown>\n </div>\n </ion-col>\n }\n </ion-row>\n\n <!-- Attachments -->\n @if (canAttach) {\n <div class=\"message-attachments\">\n <!-- existing attachments (already stored by the pod) -->\n @if (existingAttachments | isNotEmptyArray) {\n <ion-list class=\"existing-attachments\" lines=\"none\">\n @for (attachment of existingAttachments; track attachment.id) {\n <ion-item class=\"existing-attachment\">\n <mat-icon slot=\"start\">{{ attachment | map: getAttachmentIcon }}</mat-icon>\n <ion-label>\n <h4>{{ attachment | map: getAttachmentName }}</h4>\n @if (attachment.size) {\n <p>{{ attachment.size | fileSize }}</p>\n }\n </ion-label>\n <button\n mat-icon-button\n slot=\"end\"\n class=\"remove-attachment\"\n [disabled]=\"disabled\"\n [title]=\"'SOCIAL.MESSAGE.ATTACHMENTS.BTN_REMOVE' | translate\"\n (click)=\"removeExistingAttachment($event, attachment)\"\n >\n <mat-icon>clear</mat-icon>\n </button>\n </ion-item>\n }\n </ion-list>\n }\n\n <!-- files list only (no drop area: files are added by the toolbar attachment button, or dropped anywhere in the form) -->\n <app-upload-file\n #attachmentsUpload\n [showDropArea]=\"false\"\n [instantUpload]=\"true\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxFileSizeMb]=\"maxAttachmentSizeMb\"\n (filesChange)=\"onAttachmentsChange($event)\"\n ></app-upload-file>\n @if (form.controls.attachments.getError('uploading')) {\n <ion-note color=\"danger\">\n {{ 'SOCIAL.MESSAGE.ATTACHMENTS.UPLOADING_ERROR' | translate }}\n </ion-note>\n }\n </div>\n }\n</form>\n\n<ng-template #debugPanel>\n <app-debug title=\"Message form\">\n <ion-grid>\n <ion-row>\n <ion-col>\n ready: {{ readySubject | async }}\n <br />\n loading: {{ loadingSubject | async }}\n <br />\n enabled: {{ enabled }}\n <br />\n dirty: {{ dirty }}\n <br />\n valid: {{ valid }}\n <br />\n <br />\n error: {{ _form | formError | json }}\n <br />\n recipients: {{ _form | formGetValue: 'recipients' | personToString }}\n <br />\n recipientFilter: {{ _form | formGetValue: 'recipientFilter' | json }}\n </ion-col>\n </ion-row>\n </ion-grid>\n </app-debug>\n</ng-template>\n", styles: ["textarea.fixed-height{height:11.5em}textarea{min-height:11.5em}app-markdown-toolbar{display:block;margin-bottom:4px}.body-preview{box-sizing:border-box;height:calc(100% - 22px);min-height:calc(11.5em + 32px);padding:0 1em;overflow:auto;border:1px dashed var(--ion-item-border-color, #c8cdd4);border-radius:4px}.body-preview>markdown{display:block}.message-attachments{margin-top:8px;--app-upload-file-list-max-height: none}:host.file-over{display:block;outline:2px dashed var(--ion-color-accent);outline-offset:4px}\n"] }]
39741
40292
  }], ctorParameters: () => [{ type: i0.Injector }, { type: i1$2.UntypedFormBuilder }, { type: i0.ChangeDetectorRef }], propDecorators: { mobile: [{
39742
40293
  type: Input
39743
40294
  }], suggestFn: [{
@@ -39767,8 +40318,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
39767
40318
  type: Input
39768
40319
  }], maxAttachmentSizeMb: [{
39769
40320
  type: Input
40321
+ }], bodyPreviewSideBySide: [{
40322
+ type: Input,
40323
+ args: [{ transform: toBoolean }]
39770
40324
  }], searchRecipients: [{
39771
40325
  type: Output
40326
+ }], attach: [{
40327
+ type: Output
40328
+ }], bodyPreviewChange: [{
40329
+ type: Output
39772
40330
  }], attachmentsUpload: [{
39773
40331
  type: ViewChild,
39774
40332
  args: ['attachmentsUpload']
@@ -39802,6 +40360,12 @@ class MessageModal {
39802
40360
  accountService;
39803
40361
  cd;
39804
40362
  userController;
40363
+ /** Margin (in px) kept between the modal and the window borders, when the modal grows */
40364
+ static FIT_HEIGHT_MARGIN = 50;
40365
+ /** Min window width (in px) to display the body preview side by side with the text (2 columns), in a wider modal */
40366
+ static PREVIEW_SIDE_BY_SIDE_MIN_WINDOW_WIDTH = 1000;
40367
+ /** Max modal width (in px), when the body preview is displayed side by side */
40368
+ static PREVIEW_SIDE_BY_SIDE_MAX_WIDTH = 1200;
39805
40369
  mobile;
39806
40370
  debug = false;
39807
40371
  title = 'SOCIAL.MESSAGE.NEW.TITLE';
@@ -39816,6 +40380,14 @@ class MessageModal {
39816
40380
  deleteFn;
39817
40381
  maxAttachmentSizeMb = 20;
39818
40382
  form;
40383
+ elementRef = inject(ElementRef);
40384
+ /** Initial (default) height of the modal, used as the min height when fitting the content */
40385
+ initialHeight;
40386
+ /** Attachments count, at the last fit (used to allow the modal to shrink only when an attachment has been removed) */
40387
+ fittedAttachmentsCount = 0;
40388
+ resizeObserver;
40389
+ /** Display the body preview side by side with the text (if enough space), or replacing the text */
40390
+ bodyPreviewSideBySide = false;
39819
40391
  constructor(settings, viewCtrl, accountService, cd, userController) {
39820
40392
  this.settings = settings;
39821
40393
  this.viewCtrl = viewCtrl;
@@ -39837,8 +40409,13 @@ class MessageModal {
39837
40409
  }
39838
40410
  this.form.markAsLoaded();
39839
40411
  this.form.enable();
40412
+ // Grow the modal when attachments are added (or shrink back when removed)
40413
+ this.observeAttachmentsResize();
39840
40414
  });
39841
40415
  }
40416
+ ngOnDestroy() {
40417
+ this.resizeObserver?.disconnect();
40418
+ }
39842
40419
  cancel() {
39843
40420
  this.viewCtrl.dismiss();
39844
40421
  }
@@ -39904,12 +40481,77 @@ class MessageModal {
39904
40481
  markAsLoaded(opts) {
39905
40482
  this.form.markAsLoaded(opts);
39906
40483
  }
40484
+ /**
40485
+ * Observe the attachments area: when its size changes, fit the modal height to its content
40486
+ */
40487
+ observeAttachmentsResize() {
40488
+ if (this.mobile || typeof ResizeObserver === 'undefined')
40489
+ return; // Skip on mobile (full screen modal)
40490
+ const attachmentsElement = this.elementRef.nativeElement.querySelector('.message-attachments');
40491
+ if (!attachmentsElement)
40492
+ return; // No attachments allowed
40493
+ this.resizeObserver = new ResizeObserver(() => this.fitHeightToContent());
40494
+ this.resizeObserver.observe(attachmentsElement);
40495
+ }
40496
+ /**
40497
+ * Fit the modal height to its content (to avoid any scroll), between its initial height,
40498
+ * and the window height (minus a margin). The modal never shrinks below its initial height.
40499
+ *
40500
+ * To avoid any flickering, the modal shrinks only when an attachment has been removed (or if `allowShrink`): the upload progress
40501
+ * (global and per file progress bars) makes the content grow temporarily, so the reached height is kept after the upload.
40502
+ */
40503
+ fitHeightToContent(opts) {
40504
+ const page = this.elementRef.nativeElement; // The modal page (.ion-page), filling the modal
40505
+ const modal = page.closest('ion-modal');
40506
+ const content = page.querySelector('ion-content');
40507
+ const formElement = page.querySelector('app-message-form');
40508
+ if (!modal || !content || !formElement)
40509
+ return;
40510
+ const currentHeight = page.offsetHeight;
40511
+ this.initialHeight = this.initialHeight ?? currentHeight;
40512
+ const maxHeight = window.innerHeight - MessageModal.FIT_HEIGHT_MARGIN;
40513
+ if (this.initialHeight >= maxHeight)
40514
+ return; // Already full height: nothing to do
40515
+ // Required height = current height + content overflow (positive) or free space (negative)
40516
+ const contentPaddingBottom = parseFloat(getComputedStyle(content).getPropertyValue('--padding-bottom')) || 0;
40517
+ const delta = formElement.getBoundingClientRect().bottom + contentPaddingBottom - content.getBoundingClientRect().bottom;
40518
+ let height = Math.round(Math.max(this.initialHeight, Math.min(currentHeight + delta, maxHeight)));
40519
+ // Shrink only if an attachment has been removed
40520
+ const attachmentsCount = page.querySelectorAll('.message-attachments ion-item').length;
40521
+ if (height < currentHeight && !opts?.allowShrink && attachmentsCount >= this.fittedAttachmentsCount) {
40522
+ height = currentHeight;
40523
+ }
40524
+ this.fittedAttachmentsCount = attachmentsCount;
40525
+ if (Math.abs(height - currentHeight) >= 1) {
40526
+ modal.style.setProperty('--height', `${height}px`);
40527
+ }
40528
+ }
40529
+ /**
40530
+ * When the body preview is toggled: if enough space, widen the modal to display the preview side by side with the text;
40531
+ * otherwise (e.g. mobile, or small window), the preview replaces the text
40532
+ */
40533
+ onBodyPreviewChange(preview) {
40534
+ const modal = this.elementRef.nativeElement.closest('ion-modal');
40535
+ if (preview) {
40536
+ this.bodyPreviewSideBySide = !this.mobile && window.innerWidth >= MessageModal.PREVIEW_SIDE_BY_SIDE_MIN_WINDOW_WIDTH;
40537
+ }
40538
+ if (preview && this.bodyPreviewSideBySide) {
40539
+ modal?.style.setProperty('--width', `min(${MessageModal.PREVIEW_SIDE_BY_SIDE_MAX_WIDTH}px, calc(100% - ${MessageModal.FIT_HEIGHT_MARGIN * 2}px))`);
40540
+ }
40541
+ else {
40542
+ modal?.style.removeProperty('--width');
40543
+ }
40544
+ this.markForCheck();
40545
+ // Fit the height to the new content (after the rendering)
40546
+ if (!this.mobile)
40547
+ setTimeout(() => this.fitHeightToContent({ allowShrink: true }));
40548
+ }
39907
40549
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModal, deps: [{ token: LocalSettingsService }, { token: i2$2.ModalController }, { token: AccountService }, { token: i0.ChangeDetectorRef }, { token: USER_CONTROLLER, optional: true }], target: i0.ɵɵFactoryTarget.Component });
39908
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MessageModal, isStandalone: false, selector: "app-message-modal", inputs: { debug: ["debug", "debug", booleanAttribute], title: "title", suggestFn: "suggestFn", data: "data", canSelectType: ["canSelectType", "canSelectType", booleanAttribute], canRecipientFilter: ["canRecipientFilter", "canRecipientFilter", booleanAttribute], canSearchRecipients: ["canSearchRecipients", "canSearchRecipients", booleanAttribute], recipientFilterCount: ["recipientFilterCount", "recipientFilterCount", booleanAttribute], canAttach: ["canAttach", "canAttach", booleanAttribute], uploadFn: "uploadFn", deleteFn: "deleteFn", maxAttachmentSizeMb: "maxAttachmentSizeMb" }, viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, static: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (form.error) {\n <ion-item visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\"><span [innerHTML]=\"form.error | translate\"></span></ion-label>\n </ion-item>\n }\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [canSearchRecipients]=\"canSearchRecipients\"\n [canAttach]=\"canAttach\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxAttachmentSizeMb]=\"maxAttachmentSizeMb\"\n (searchRecipients)=\"openSelectUsers($event)\"\n [debug]=\"debug\"\n ></app-message-form>\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\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-text>{{ 'COMMON.BTN_CANCEL' | translate }}</ion-text>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-text>{{ 'COMMON.BTN_SEND' | translate }}</ion-text>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", dependencies: [{ kind: "component", type: i2$2.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$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.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$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: MessageForm, selector: "app-message-form", inputs: ["mobile", "suggestFn", "subjectMinLength", "subjectMaxLength", "bodyMaxLength", "bodyAutoHeight", "canSelectType", "canRecipientFilter", "recipientFilterCount", "canSearchRecipients", "canAttach", "uploadFn", "deleteFn", "maxAttachmentSizeMb"], outputs: ["searchRecipients"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
40550
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MessageModal, isStandalone: false, selector: "app-message-modal", inputs: { debug: ["debug", "debug", booleanAttribute], title: "title", suggestFn: "suggestFn", data: "data", canSelectType: ["canSelectType", "canSelectType", booleanAttribute], canRecipientFilter: ["canRecipientFilter", "canRecipientFilter", booleanAttribute], canSearchRecipients: ["canSearchRecipients", "canSearchRecipients", booleanAttribute], recipientFilterCount: ["recipientFilterCount", "recipientFilterCount", booleanAttribute], canAttach: ["canAttach", "canAttach", booleanAttribute], uploadFn: "uploadFn", deleteFn: "deleteFn", maxAttachmentSizeMb: "maxAttachmentSizeMb" }, viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, static: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (form.error) {\n <ion-item visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\"><span [innerHTML]=\"form.error | translate\"></span></ion-label>\n </ion-item>\n }\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [canSearchRecipients]=\"canSearchRecipients\"\n [canAttach]=\"canAttach\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxAttachmentSizeMb]=\"maxAttachmentSizeMb\"\n [bodyPreviewSideBySide]=\"bodyPreviewSideBySide\"\n (bodyPreviewChange)=\"onBodyPreviewChange($event)\"\n (searchRecipients)=\"openSelectUsers($event)\"\n [debug]=\"debug\"\n ></app-message-form>\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\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-text>{{ 'COMMON.BTN_CANCEL' | translate }}</ion-text>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-text>{{ 'COMMON.BTN_SEND' | translate }}</ion-text>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n", dependencies: [{ kind: "component", type: i2$2.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$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.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$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonFooter, selector: "ion-footer", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }, { kind: "component", type: i2$2.IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "component", type: i2$2.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "component", type: MessageForm, selector: "app-message-form", inputs: ["mobile", "suggestFn", "subjectMinLength", "subjectMaxLength", "bodyMaxLength", "bodyAutoHeight", "canSelectType", "canRecipientFilter", "recipientFilterCount", "canSearchRecipients", "canAttach", "uploadFn", "deleteFn", "maxAttachmentSizeMb", "bodyPreviewSideBySide"], outputs: ["searchRecipients", "attach", "bodyPreviewChange"] }, { kind: "pipe", type: i2.TranslatePipe, name: "translate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
39909
40551
  }
39910
40552
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModal, decorators: [{
39911
40553
  type: Component,
39912
- args: [{ selector: 'app-message-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (form.error) {\n <ion-item visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\"><span [innerHTML]=\"form.error | translate\"></span></ion-label>\n </ion-item>\n }\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [canSearchRecipients]=\"canSearchRecipients\"\n [canAttach]=\"canAttach\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxAttachmentSizeMb]=\"maxAttachmentSizeMb\"\n (searchRecipients)=\"openSelectUsers($event)\"\n [debug]=\"debug\"\n ></app-message-form>\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\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-text>{{ 'COMMON.BTN_CANCEL' | translate }}</ion-text>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-text>{{ 'COMMON.BTN_SEND' | translate }}</ion-text>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n" }]
40554
+ args: [{ selector: 'app-message-modal', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"secondary\">\n <ion-buttons slot=\"start\">\n <ion-button class=\"back-button\" (click)=\"cancel()\" visible-xs visible-sm visible-mobile>\n <ion-icon slot=\"icon-only\" name=\"arrow-back\"></ion-icon>\n </ion-button>\n </ion-buttons>\n\n <ion-title>\n {{ title | translate }}\n </ion-title>\n\n <ion-buttons slot=\"end\">\n <ion-button\n class=\"back-button\"\n (click)=\"doSubmit()\"\n [disabled]=\"!form.valid\"\n visible-xs\n visible-sm\n visible-mobile\n >\n <ion-icon slot=\"icon-only\" name=\"checkmark\"></ion-icon>\n </ion-button>\n </ion-buttons>\n </ion-toolbar>\n</ion-header>\n\n<ion-content class=\"ion-padding\">\n @if (form.error) {\n <ion-item visible-xs visible-sm visible-mobile lines=\"none\">\n <ion-icon color=\"danger\" slot=\"start\" name=\"alert-circle\"></ion-icon>\n <ion-label color=\"danger\" class=\"error\"><span [innerHTML]=\"form.error | translate\"></span></ion-label>\n </ion-item>\n }\n\n <app-message-form\n #form\n (onSubmit)=\"doSubmit()\"\n (onCancel)=\"cancel()\"\n [canSelectType]=\"canSelectType\"\n [canRecipientFilter]=\"canRecipientFilter\"\n [recipientFilterCount]=\"recipientFilterCount\"\n [suggestFn]=\"suggestFn\"\n [canSearchRecipients]=\"canSearchRecipients\"\n [canAttach]=\"canAttach\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [maxAttachmentSizeMb]=\"maxAttachmentSizeMb\"\n [bodyPreviewSideBySide]=\"bodyPreviewSideBySide\"\n (bodyPreviewChange)=\"onBodyPreviewChange($event)\"\n (searchRecipients)=\"openSelectUsers($event)\"\n [debug]=\"debug\"\n ></app-message-form>\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\n <!-- buttons -->\n <ion-col size=\"auto\">\n <ion-button fill=\"clear\" color=\"dark\" (click)=\"cancel()\">\n <ion-text>{{ 'COMMON.BTN_CANCEL' | translate }}</ion-text>\n </ion-button>\n\n <ion-button\n [fill]=\"form.invalid ? 'clear' : 'solid'\"\n [disabled]=\"form.loading || form.invalid\"\n (keyup.enter)=\"doSubmit()\"\n (click)=\"doSubmit()\"\n color=\"tertiary\"\n >\n <ion-text>{{ 'COMMON.BTN_SEND' | translate }}</ion-text>\n </ion-button>\n </ion-col>\n </ion-row>\n </ion-toolbar>\n</ion-footer>\n" }]
39913
40555
  }], ctorParameters: () => [{ type: LocalSettingsService }, { type: i2$2.ModalController }, { type: AccountService }, { type: i0.ChangeDetectorRef }, { type: undefined, decorators: [{
39914
40556
  type: Optional
39915
40557
  }, {
@@ -45174,14 +45816,14 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
45174
45816
 
45175
45817
  class MessageModule {
45176
45818
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
45177
- static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, declarations: [MessageModal, MessageForm], imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule], exports: [MessageModal, MessageForm] });
45178
- static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, providers: [provideChildTranslateService(), { provide: USER_CONTROLLER, useExisting: UserController }], imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule] });
45819
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, declarations: [MessageModal, MessageForm], imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule, SharedMarkdownModule], exports: [MessageModal, MessageForm] });
45820
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, providers: [provideChildTranslateService(), { provide: USER_CONTROLLER, useExisting: UserController }], imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule, SharedMarkdownModule] });
45179
45821
  }
45180
45822
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageModule, decorators: [{
45181
45823
  type: NgModule,
45182
45824
  args: [{
45183
45825
  // Do NOT import CoreModule here (circular dependency: CoreModule > home > FeedModule > MessageModule)
45184
- imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule],
45826
+ imports: [CommonModule, SharedModule, CorePipesModule, MatChipsModule, SharedDebugModule, SharedMarkdownModule],
45185
45827
  declarations: [MessageModal, MessageForm],
45186
45828
  exports: [MessageModal, MessageForm],
45187
45829
  providers: [provideChildTranslateService(), { provide: USER_CONTROLLER, useExisting: UserController }],
@@ -52163,6 +52805,18 @@ function createApolloClientOptions() {
52163
52805
  const httpLinkInstance = httpLink.create({
52164
52806
  uri: (o) => $url.value,
52165
52807
  });
52808
+ // Queries link
52809
+ let queryLink = ApolloLink.from([retryLink, authLink, httpLinkInstance]);
52810
+ // Enable @defer (incremental delivery): send queries with @defer using the Apollo fetch link,
52811
+ // because the apollo-angular HttpLink (Angular HttpClient) cannot read a streamed multipart/mixed response
52812
+ const enableDefer = environment.enableGraphqlDefer === true;
52813
+ if (enableDefer) {
52814
+ console.info('[graphql] Enable @defer directive (incremental delivery)');
52815
+ const fetchHttpLink = new HttpLink$1({
52816
+ uri: () => $url.value,
52817
+ });
52818
+ queryLink = ApolloLink.split(isDeferredOperation, ApolloLink.from([retryLink, authLink, httpHeadersToRecordLink, fetchHttpLink]), queryLink);
52819
+ }
52166
52820
  // Cache
52167
52821
  const cache = new InMemoryCache({
52168
52822
  typePolicies,
@@ -52218,7 +52872,12 @@ function createApolloClientOptions() {
52218
52872
  // Handle subscriptions
52219
52873
  isSubscriptionOperation, wsLink,
52220
52874
  // Handle queries
52221
- ApolloLink.from([retryLink, authLink, httpLinkInstance]))),
52875
+ queryLink)),
52876
+ // @defer support
52877
+ ...(enableDefer
52878
+ ? { incrementalHandler: new Defer20220824Handler() }
52879
+ : // Remove @defer directives, when disabled
52880
+ { documentTransform: new DocumentTransform(removeDeferDirectives) }),
52222
52881
  // For @apollo/client < 3.11
52223
52882
  //connectToDevTools: !environment.production,
52224
52883
  // For @apollo/client 3.11+
@@ -58800,12 +59459,117 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
58800
59459
  }]
58801
59460
  }] });
58802
59461
 
59462
+ // Free license (GPL version 3)
59463
+ // Fake recipients, used by the autocomplete field (no pod needed)
59464
+ const FAKE_PERSONS = [
59465
+ { id: 1, firstName: 'Demo', lastName: 'Admin', department: { id: 1, label: 'FROM-NORD', name: 'FROM Nord' } },
59466
+ { id: 2, firstName: 'Demo', lastName: 'Observer', department: { id: 1, label: 'FROM-NORD', name: 'FROM Nord' } },
59467
+ { id: 3, firstName: 'Charles', lastName: 'Dupond', department: { id: 2, label: 'IFREMER', name: 'Ifremer' } },
59468
+ ].map(Person.fromObject);
59469
+ const SAMPLE_BODY = `# Title
59470
+
59471
+ Some **bold**, <u>underlined</u>, ~~strikethrough~~ text, and a ==highlighted== word.
59472
+
59473
+ ## Sub title
59474
+
59475
+ - see [the docs](https://gitlab.ifremer.fr/sih-public/sumaris/ngx-sumaris-components)
59476
+ `;
59477
+ class MessageFormTestingPage {
59478
+ modalCtrl;
59479
+ cd;
59480
+ form;
59481
+ debug = false;
59482
+ canSelectType = true;
59483
+ canAttach = true;
59484
+ bodyPreviewSideBySide = false;
59485
+ modalResult;
59486
+ suggestFn = async (value) => {
59487
+ const search = typeof value === 'string' ? value.toLowerCase() : '';
59488
+ return FAKE_PERSONS.filter((p) => !search || `${p.lastName} ${p.firstName}`.toLowerCase().includes(search));
59489
+ };
59490
+ /** Simulate an upload (with progression) */
59491
+ uploadFn = (file) => {
59492
+ const steps = 5;
59493
+ return timer(0, 100).pipe(take(steps + 1), map((i) => i < steps
59494
+ ? { type: HttpEventType.UploadProgress, total: file.size, loaded: (file.size * i) / steps }
59495
+ : new HttpResponse({ body: { finalName: file.name } })));
59496
+ };
59497
+ deleteFn = async (_file) => {
59498
+ await sleep(500);
59499
+ return true;
59500
+ };
59501
+ constructor(modalCtrl, cd) {
59502
+ this.modalCtrl = modalCtrl;
59503
+ this.cd = cd;
59504
+ }
59505
+ ngAfterViewInit() {
59506
+ setTimeout(() => this.reset());
59507
+ }
59508
+ async reset(withSample) {
59509
+ this.form.markAsReady({ emitEvent: false });
59510
+ await this.form.setValue(Message.fromObject({
59511
+ type: MessageTypes.INBOX_MESSAGE,
59512
+ recipients: [FAKE_PERSONS[0]],
59513
+ subject: withSample ? 'Markdown sample' : null,
59514
+ body: withSample ? SAMPLE_BODY : null,
59515
+ }));
59516
+ this.form.markAsLoaded();
59517
+ this.form.enable();
59518
+ this.cd.markForCheck();
59519
+ }
59520
+ async openModal() {
59521
+ const modal = await this.modalCtrl.create({
59522
+ component: MessageModal,
59523
+ componentProps: {
59524
+ suggestFn: this.suggestFn,
59525
+ canSelectType: this.canSelectType,
59526
+ canAttach: this.canAttach,
59527
+ uploadFn: this.uploadFn,
59528
+ deleteFn: this.deleteFn,
59529
+ debug: this.debug,
59530
+ data: Message.fromObject({ recipients: [FAKE_PERSONS[0]] }),
59531
+ },
59532
+ keyboardClose: true,
59533
+ });
59534
+ await modal.present();
59535
+ const { data } = await modal.onDidDismiss();
59536
+ console.info('[message-form-testing] Modal result:', data);
59537
+ this.modalResult = data;
59538
+ this.cd.markForCheck();
59539
+ }
59540
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingPage, deps: [{ token: i2$2.ModalController }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
59541
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.22", type: MessageFormTestingPage, isStandalone: false, selector: "app-message-form-testing", viewQueries: [{ propertyName: "form", first: true, predicate: ["form"], descendants: true, static: true }], ngImport: i0, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n <ion-title>Message form test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <!-- options -->\n <ion-row>\n <ion-col>\n <mat-checkbox [(ngModel)]=\"canSelectType\">canSelectType</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"canAttach\">canAttach</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"bodyPreviewSideBySide\">bodyPreviewSideBySide</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"debug\">debug</mat-checkbox>\n </ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"outline\" (click)=\"reset()\">Reset</ion-button>\n <ion-button fill=\"outline\" (click)=\"reset(true)\">Markdown sample</ion-button>\n <ion-button (click)=\"openModal()\">Open in a modal</ion-button>\n </ion-col>\n </ion-row>\n\n <!-- inline form -->\n <ion-row>\n <ion-col size=\"12\" size-lg=\"6\">\n <ion-card>\n <ion-card-content>\n <app-message-form\n #form\n [suggestFn]=\"suggestFn\"\n [canSelectType]=\"canSelectType\"\n [canAttach]=\"canAttach\"\n [bodyPreviewSideBySide]=\"bodyPreviewSideBySide\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [debug]=\"debug\"\n ></app-message-form>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <!-- form value -->\n <ion-col size=\"12\" size-lg=\"6\">\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle>Form value (valid: {{ form.valid }})</ion-card-subtitle>\n </ion-card-header>\n <ion-card-content>\n <pre>{{ form.form?.value | json }}</pre>\n </ion-card-content>\n </ion-card>\n\n @if (modalResult) {\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle>Modal result</ion-card-subtitle>\n </ion-card-header>\n <ion-card-content>\n <pre>{{ modalResult | json }}</pre>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n</ion-content>\n", dependencies: [{ kind: "component", type: i2$2.IonBackButton, selector: "ion-back-button" }, { kind: "component", type: i2$2.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$2.IonButtons, selector: "ion-buttons", inputs: ["collapse"] }, { kind: "component", type: i2$2.IonCard, selector: "ion-card", inputs: ["button", "color", "disabled", "download", "href", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: i2$2.IonCardContent, selector: "ion-card-content", inputs: ["mode"] }, { kind: "component", type: i2$2.IonCardHeader, selector: "ion-card-header", inputs: ["color", "mode", "translucent"] }, { kind: "component", type: i2$2.IonCardSubtitle, selector: "ion-card-subtitle", inputs: ["color", "mode"] }, { kind: "component", type: i2$2.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$2.IonContent, selector: "ion-content", inputs: ["color", "fixedSlotPlacement", "forceOverscroll", "fullscreen", "scrollEvents", "scrollX", "scrollY"] }, { kind: "component", type: i2$2.IonGrid, selector: "ion-grid", inputs: ["fixed"] }, { kind: "component", type: i2$2.IonHeader, selector: "ion-header", inputs: ["collapse", "mode", "translucent"] }, { kind: "component", type: i2$2.IonRow, selector: "ion-row" }, { kind: "component", type: i2$2.IonTitle, selector: "ion-title", inputs: ["color", "size"] }, { kind: "component", type: i2$2.IonToolbar, selector: "ion-toolbar", inputs: ["color", "mode"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "component", type: i5.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MessageForm, selector: "app-message-form", inputs: ["mobile", "suggestFn", "subjectMinLength", "subjectMaxLength", "bodyMaxLength", "bodyAutoHeight", "canSelectType", "canRecipientFilter", "recipientFilterCount", "canSearchRecipients", "canAttach", "uploadFn", "deleteFn", "maxAttachmentSizeMb", "bodyPreviewSideBySide"], outputs: ["searchRecipients", "attach", "bodyPreviewChange"] }, { kind: "pipe", type: i2$3.JsonPipe, name: "json" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
59542
+ }
59543
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingPage, decorators: [{
59544
+ type: Component,
59545
+ args: [{ selector: 'app-message-form-testing', changeDetection: ChangeDetectionStrategy.OnPush, standalone: false, template: "<ion-header>\n <ion-toolbar color=\"primary\">\n <ion-buttons slot=\"start\">\n <ion-back-button></ion-back-button>\n </ion-buttons>\n <ion-title>Message form test page</ion-title>\n </ion-toolbar>\n</ion-header>\n\n<ion-content>\n <ion-grid class=\"form-container ion-padding-top\">\n <!-- options -->\n <ion-row>\n <ion-col>\n <mat-checkbox [(ngModel)]=\"canSelectType\">canSelectType</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"canAttach\">canAttach</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"bodyPreviewSideBySide\">bodyPreviewSideBySide</mat-checkbox>\n <mat-checkbox [(ngModel)]=\"debug\">debug</mat-checkbox>\n </ion-col>\n <ion-col size=\"auto\">\n <ion-button fill=\"outline\" (click)=\"reset()\">Reset</ion-button>\n <ion-button fill=\"outline\" (click)=\"reset(true)\">Markdown sample</ion-button>\n <ion-button (click)=\"openModal()\">Open in a modal</ion-button>\n </ion-col>\n </ion-row>\n\n <!-- inline form -->\n <ion-row>\n <ion-col size=\"12\" size-lg=\"6\">\n <ion-card>\n <ion-card-content>\n <app-message-form\n #form\n [suggestFn]=\"suggestFn\"\n [canSelectType]=\"canSelectType\"\n [canAttach]=\"canAttach\"\n [bodyPreviewSideBySide]=\"bodyPreviewSideBySide\"\n [uploadFn]=\"uploadFn\"\n [deleteFn]=\"deleteFn\"\n [debug]=\"debug\"\n ></app-message-form>\n </ion-card-content>\n </ion-card>\n </ion-col>\n\n <!-- form value -->\n <ion-col size=\"12\" size-lg=\"6\">\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle>Form value (valid: {{ form.valid }})</ion-card-subtitle>\n </ion-card-header>\n <ion-card-content>\n <pre>{{ form.form?.value | json }}</pre>\n </ion-card-content>\n </ion-card>\n\n @if (modalResult) {\n <ion-card>\n <ion-card-header>\n <ion-card-subtitle>Modal result</ion-card-subtitle>\n </ion-card-header>\n <ion-card-content>\n <pre>{{ modalResult | json }}</pre>\n </ion-card-content>\n </ion-card>\n }\n </ion-col>\n </ion-row>\n </ion-grid>\n</ion-content>\n" }]
59546
+ }], ctorParameters: () => [{ type: i2$2.ModalController }, { type: i0.ChangeDetectorRef }], propDecorators: { form: [{
59547
+ type: ViewChild,
59548
+ args: ['form', { static: true }]
59549
+ }] } });
59550
+
59551
+ class MessageFormTestingModule {
59552
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
59553
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingModule, declarations: [MessageFormTestingPage], imports: [SharedModule, FormsModule, MessageModule], exports: [MessageFormTestingPage] });
59554
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingModule, providers: [provideChildTranslateService()], imports: [SharedModule, FormsModule, MessageModule] });
59555
+ }
59556
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: MessageFormTestingModule, decorators: [{
59557
+ type: NgModule,
59558
+ args: [{
59559
+ imports: [SharedModule, FormsModule, MessageModule],
59560
+ declarations: [MessageFormTestingPage],
59561
+ exports: [MessageFormTestingPage],
59562
+ providers: [provideChildTranslateService()],
59563
+ }]
59564
+ }] });
59565
+
58803
59566
  // Testing pages, declared outside the testing module, to be imported without loading the (lazy) testing module
58804
59567
  const SOCIAL_TESTING_PAGES = [
58805
59568
  { label: 'Social components', divider: true },
58806
59569
  { label: 'Job progression', page: '/testing/social/job-progression' },
58807
59570
  { label: 'User event', page: '/testing/social/user-event' },
58808
59571
  { label: 'Feed', page: '/testing/social/feed' },
59572
+ { label: 'Message form', page: '/testing/social/message-form' },
58809
59573
  ];
58810
59574
 
58811
59575
  const routes = [
@@ -58824,6 +59588,11 @@ const routes = [
58824
59588
  pathMatch: 'full',
58825
59589
  component: FeedTestingPage,
58826
59590
  },
59591
+ {
59592
+ path: 'message-form',
59593
+ pathMatch: 'full',
59594
+ component: MessageFormTestingPage,
59595
+ },
58827
59596
  {
58828
59597
  path: 'feed/page',
58829
59598
  pathMatch: 'full',
@@ -58836,13 +59605,15 @@ class SocialTestingModule {
58836
59605
  // Sub modules
58837
59606
  JobTestingModule,
58838
59607
  UserEventTestingModule,
58839
- FeedTestingModule] });
59608
+ FeedTestingModule,
59609
+ MessageFormTestingModule] });
58840
59610
  static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SocialTestingModule, providers: [provideChildTranslateService()], imports: [CommonModule,
58841
59611
  RouterModule.forChild(routes),
58842
59612
  // Sub modules
58843
59613
  JobTestingModule,
58844
59614
  UserEventTestingModule,
58845
- FeedTestingModule] });
59615
+ FeedTestingModule,
59616
+ MessageFormTestingModule] });
58846
59617
  }
58847
59618
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImport: i0, type: SocialTestingModule, decorators: [{
58848
59619
  type: NgModule,
@@ -58854,6 +59625,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
58854
59625
  JobTestingModule,
58855
59626
  UserEventTestingModule,
58856
59627
  FeedTestingModule,
59628
+ MessageFormTestingModule,
58857
59629
  ],
58858
59630
  providers: [provideChildTranslateService()],
58859
59631
  }]
@@ -58894,5 +59666,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.22", ngImpo
58894
59666
  * Generated bundle index. Do not edit.
58895
59667
  */
58896
59668
 
58897
- export { ADMIN_ICONS, APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FILE_TRANSFER_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_SERVICE, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_INPUT_FIELD, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_MESSAGE_SERVICE_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TABLE_OPTIONS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppHomePageModule, AppIconComponent, AppIconModule, AppIconSelectorField, AppIconSelectorModal, AppIconSelectorModule, AppImageGalleryComponent, AppImageGallerySlideshowComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppWebSocket, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFindPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_ICONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EmptyMissingTranslationHandler, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FILE_KIND_ICONS, FeedAttachmentUtils, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileAttachmentComponent, FileAttachmentPreviewModal, FileAttachmentUtils, FileDownloadService, FileResponse, FileService, FileSizePipe, FileTypeUtils, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GitlabUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, HttpUtils, IMAGE_DEFAULTS, ION_ICONS, IPosition, IconSelectorTestPage, IconSelectorTestingModule, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImageSizes, ImagesUtils, InMemoryEntitiesService, InputFieldTestPage, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, ListFormTestPage, ListFormTestingModule, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageAttachment, MessageAttachmentUtils, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NativeWebSocket, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_ICONS, SHARED_IONIC_COMPONENTS, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_ICONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFileAttachmentModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedIonicModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StorageServiceWrapper, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, TRACKED_QUERIES_STORAGE_KEY, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, TimeFormatPipe, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, USER_CONTROLLER, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserController, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createApolloClientOptions, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, createTestingImageDataUrl, createTrackerLink, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormControlName, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hasFocusWithin, hexToRgb, hexToRgbArray, initArrayControlsFromValues, initializeSharedModule, interpolateString, intersectArrays, isAndroid, isAppInputField, isAppTextInputField, isBlankString, isCapacitor, isChrome, isControlHasInput, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isMutationOperation, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isSubscriptionOperation, isTouchUi, isValidVersion, isVersionCompatible, isWebAnimationsSupported, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, loggerLink, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseBooleanText, parseLatLongText, parseLatitudeOrLongitude, parseVersion, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, provideAllIonicIcons, provideIonicIcons, provideIonicIconsInitializer, providePersonService, referentialToString, referentialsToString, registerIonicIcons, remove, removeAll, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, restoreTrackedQueries, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, trimEmptyToNull, truncateHtml, typeTextIntoInput, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending };
59669
+ export { ADMIN_ICONS, APP_ABOUT_DEVELOPERS, APP_ABOUT_PARTNERS, APP_ACCOUNT_SERVICE, APP_ACCOUNT_SERVICE_OPTIONS, APP_CELL_SELECTION_SERVICE_CONFIG_TOKEN, APP_CELL_SELECTION_SERVICE_TOKEN, APP_CONFIG_OPTIONS, APP_DEBUG_DATA_SERVICE, APP_FEED_SERVICE, APP_FILE_TRANSFER_SERVICE, APP_FORM_ERROR_I18N_KEYS, APP_GRAPHQL_FRAGMENTS, APP_GRAPHQL_SERVICE, APP_GRAPHQL_TYPE_POLICIES, APP_HOME_BUTTONS, APP_HOME_CONFIG, APP_HOME_TOOLBAR_BUTTONS, APP_HOTKEYS_CONFIG, APP_INPUT_FIELD, APP_JOB_PROGRESSION_SERVICE, APP_LOCALES, APP_LOCAL_SETTINGS, APP_LOCAL_SETTINGS_OPTIONS, APP_LOCAL_STORAGE_TYPE_POLICIES, APP_LOGGING_SERVICE, APP_MENU_ITEMS, APP_MENU_OPTIONS, APP_MESSAGE_SERVICE_OPTIONS, APP_NAMED_FILTER_SERVICE, APP_PERSON_SERVICE, APP_PERSON_SERVICE_OPTIONS, APP_PROGRESS_BAR_SERVICE, APP_SETTINGS_MENU_ITEMS, APP_SHOW_TOOLTIP, APP_STORAGE, APP_STORAGE_EXPLORER_PROTECTED_KEYS, APP_TABLE_OPTIONS, APP_TESTING_PAGES, APP_USER_EVENT_LIST_INFINITE_SCROLL_THRESHOLD, APP_USER_EVENT_SERVICE, APP_USER_SETTINGS_OPTIONS, APP_USER_TOKEN_SCOPES, AboutModal, AbstractNamedFilterService, AbstractPersonService, AbstractSelectionModelPipe, AbstractTableSelectionPipe, AbstractUserEventService, Account, AccountPage, AccountService, AccountToStringPipe, AccountUtils, ActionsColumnComponent, AdminModule, AdminRoutingModule, AdminUsersModule, Alerts, AndroidOsEnvironment, AppAboutModalModule, AppAccountModule, AppAsyncTable, AppAuthForm, AppAuthModal, AppAuthModule, AppChangePasswordModule, AppChangePasswordPage, AppEditor, AppEditorOptions, AppEntityEditor, AppEntityEditorModal, AppEntityEditorModalOptions, AppEntityFormModule, AppForm, AppFormArray, AppFormButtonsBarModule, AppFormContainer, AppFormField, AppFormModule, AppFormProvider, AppFormUtils, AppGestureConfig, AppHomePageModule, AppIconComponent, AppIconModule, AppIconSelectorField, AppIconSelectorModal, AppIconSelectorModule, AppImageGalleryComponent, AppImageGallerySlideshowComponent, AppInMemoryTable, AppInstallUpgradeCard, AppInstallUpgradeCardModule, AppListForm, AppListFormModule, AppLoadingSpinner, AppMarkdownContent, AppMarkdownModal, AppMenuModule, AppNullForm, AppPropertiesForm, AppPropertiesFormModule, AppPropertiesTable, AppPropertiesUtils, AppPropertyUtils, AppRegisterModule, AppResetPasswordModal, AppRowField, AppSelectPeerModule, AppSelectUsersModal, AppSettingsPageModule, AppTabEditor, AppTabEditorOptions, AppTable, AppTableModule, AppTableUtils, AppTextFormModule, AppTextPopoverModule, AppUpdateOfflineModeCard, AppUpdateOfflineModeCardModule, AppValidatorService, AppWebSocket, AppendQueryParamsPipePipe, ArrayDistinctPipe, ArrayFilterPipe, ArrayFindByPropertyPipe, ArrayFindPipe, ArrayFirstPipe, ArrayFormTestPage, ArrayIncludesPipe, ArrayJoinPipe, ArrayLastPipe, ArrayLengthPipe, ArrayMapPipe, ArrayPluckPipe, ArraySlicePipe, ArraySortPipe, AsAnyPipe, AsArrayPipe, AsBooleanPipe, AsFloatLabelTypePipe, AsObservablePipe, AudioProvider, AudioTestingModule, AudioTestingPage, AuthGuardService, AutoResizeDirective, AutoTitleDirective, AutoTooltipDirective, AutocompleteTestPage, AutofocusDirective, BadgeDirective, BadgeNumberPipe, Base58, BaseEntityService, BaseGraphqlService, BaseGraphqlServiceOptions, BaseReferential, Beans, BooleanFormatPipe, BooleanTestPage, CORE_CONFIG_OPTIONS, CORE_ICONS, CORE_TESTING_PAGES, CapitalizePipe, CellIdentifierDirective, CellSelectionDirective, CellSelectionService, CellValueChangeListener, ChangeCaseToUnderscorePipe, ChangePasswordForm, ChipsTestPage, Color, ColorScale, ComponentDirtyGuard, ConfigFragments, ConfigService, Configuration, CoreModule, CorePipesModule, CoreTestingModule, CryptoService, CsvUtils, DATE_ISO_PATTERN, DATE_ISO_PATTERNS, DATE_MATCH_REGEXP, DATE_PATTERN, DATE_UNIX_MS_TIMESTAMP, DATE_UNIX_TIMESTAMP, DEFAULT_JOIN_ARRAY_VALUES_SEPARATOR, DEFAULT_JOIN_PROPERTIES_SEPARATOR, DEFAULT_MENU_SHOW_WHEN, DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS, DEFAULT_PLACEHOLDER_CHAR, DEFAULT_REQUIRED_COLUMNS, DEFER_DIRECTIVE_NAME, DateDiffDurationPipe, DateFormatPipe, DateFormatService, DateFromNowPipe, DateFromPipe, DateShortTestPage, DateTestPage, DateTimeTestPage, DateUtils, DebugComponent, Department, DepartmentToStringPipe, DisplayWithPipe, DragAndDropDirective, DurationPipe, DurationTestPage, ED25519_SEED_LENGTH, EMPTY_PLACEHOLDER_CHAR, EMPTY_PLACEHOLDER_CHAR_REGEXP_GLOBAL, ENTITIES_STORAGE_KEY_PREFIX, ENVIRONMENT, EmptyArrayPipe, EmptyMissingTranslationHandler, EntitiesAsyncTableDataSource, EntitiesStorage, EntitiesTableDataSource, Entity, EntityClass, EntityClasses, EntityFilter, EntityFilterUtils, EntityMetadataComponent, EntityStore, EntityUtils, Environment, EnvironmentHttpLoader, EnvironmentLoader, ErrorCodes, EvenPipe, FILE_KIND_ICONS, FeedAttachmentUtils, FeedDirective, FeedModule, FeedPage, FeedService, FeedsComponent, FileAttachmentComponent, FileAttachmentPreviewModal, FileAttachmentUtils, FileDownloadService, FileResponse, FileService, FileSizePipe, FileTypeUtils, FilesUtils, FirstFalsePipe, FirstPipe, FirstTruePipe, FormArrayAtControlPipe, FormArrayAtGroupPipe, FormArrayHelper, FormArrayTestModule, FormButtonsBarComponent, FormButtonsBarToken, FormErrorPipe, FormErrorTranslatePipe, FormErrorTranslator, FormFieldDefinitionUtils, FormFieldValuesHolder, FormGetArrayPipe, FormGetControlPipe, FormGetGroupPipe, FormGetNamePipe, FormGetPipe, FormGetValuePipe, GalleryTestPage, GeolocationUtils, GitlabUtils, GraphqlService, HAMMER_PRESS_TIME, HAMMER_TAP_TIME, HighlightPipe, HomePage, Hotkeys, HotkeysDialogComponent, HttpUtils, IMAGE_DEFAULTS, ION_ICONS, IPosition, IconSelectorTestPage, IconSelectorTestingModule, ImageAttachment, ImageAttachmentFilter, ImageAttachmentService, ImageGalleryModule, ImageGalleryTestingModule, ImageModule, ImageService, ImageSizes, ImagesUtils, InMemoryEntitiesService, InputFieldTestPage, IsAllSelectedPipe, IsEmptySelectionPipe, IsLoginAccountPipe, IsMultipleSelectionPipe, IsNilOrBlankPipe, IsNilOrNaNPipe, IsNilPipe, IsNotAllSelectedPipe, IsNotEmptySelectionPipe, IsNotNilOrBlankPipe, IsNotNilOrNaNPipe, IsNotNilPipe, IsOnDeskPipe, IsOnFieldPipe, IsSelectedPipe, IsSingleSelectionPipe, IsValidDatePipe, JobModule, JobProgression, JobProgressionComponent, JobProgressionIcon, JobProgressionList, JobProgressionService, JobProgressionTestService, JobProgressionTestingPage, JobTestingModule, JobUtils, JsonFeedUtils, JsonUtils, KEYBOARD_HIDE_DELAY_MS, LAT_LONG_PATTERNS, LAT_LONG_PATTERN_MAX_DECIMALS, LAT_LONG_VALUE_MAX_DECIMALS, LatLongFormatPipe, LatLongTestPage, LatitudeFormatPipe, ListFormTestPage, ListFormTestingModule, LocalSettingsService, LogLevel, LogUtils, Logger, LoggingService, LoggingServiceModule, LongitudeFormatPipe, MASKS, MASK_RANGES, MAT_FORM_FIELD_DEFAULT_APPEARANCE, MAT_FORM_FIELD_DEFAULT_SUBSCRIPT_SIZING, MINIFY_ENTITY_FOR_LOCAL_STORAGE, MINIFY_ENTITY_FOR_POD, MOMENT_NO_TIME_PROPERTY, MapGetPipe, MapKeysPipe, MapPipe, MapToPipe, MapValuesPipe, MarkdownDirective, MarkdownService, MarkdownTestPage, MarkdownTestingModule, MarkdownToolbarComponent, MarkdownUtils, MaskitoPlaceholderPipe, MaskitoTestPage, MatAutocompleteConfigHolder, MatAutocompleteField, MatAutocompleteFieldUtils, MatBadgeTestPage, MatBooleanField, MatChipsField, MatColorPipe, MatCommonTestPage, MatDate, MatDateShort, MatDateTime, MatDuration, MatLatLongField, MatLatLongFieldInput, MatPaginatorI18n, MatStepperI18n, MatSwipeField, MaterialAutocompleteFooterDirective, MaterialAutocompleteHeaderDirective, MaterialTestingModule, MathAbsPipe, MenuComponent, MenuItem, MenuItems, MenuOptions, MenuService, MenuTestingModule, MenuTestingPage, Message, MessageAttachment, MessageAttachmentUtils, MessageFilter, MessageForm, MessageModal, MessageModule, MessageService, MessageTypeList, MessageTypes, MimeTypes, ModalToolbarComponent, NETWORK_DEFAULT_CONNECTION_TIMEOUT, NamedFilter, NamedFilterFilter, NamedFilterSelector, NamedFilterSelectorTestingModule, NamedFilterSelectorTestingPage, NativeWebSocket, NavActionsColumnComponent, NestedTableTestPage, NetworkService, NetworkUtils, NewTokenForm, NewTokenModal, NgInitDirective, NgVarDirective, NoHtmlPipe, NotEmptyArrayPipe, NumberFormatPipe, ObservableTestPage, OddPipe, OtherMenuTestingPage, PEER_URL_REGEXP, PLUS_PLACEHOLDER_CHAR_REGEXP_GLOBAL, PRINT_ID_QUERY_PARAM, PRINT_LOADING_STORAGE_KEY_PREFIX, PRIORITIZED_AUTHORITIES, PUBKEY_REGEXP, Peer, Person, PersonFilter, PersonFilterAdditionalFields, PersonFragments, PersonService, PersonToStringPipe, PersonUtils, PersonValidatorService, PlatformService, PrintService, ProgressBarService, ProgressInterceptor, PropertiesFormTestPage, PropertiesFormTestingModule, PropertyEntity, PropertyEntityFilter, PropertyEntityValidator, PropertyFormatPipe, PropertyGetPipe, RESERVED_END_COLUMNS, RESERVED_START_COLUMNS, Referential, ReferentialFilter, ReferentialRef, ReferentialToStringPipe, ReferentialUtils, ReferentialValidatorService, ReferentialsToStringPipe, RegExpUtils, RegisterConfirmPage, RegisterForm, RegisterModal, ResizableComponent, ResizableDirective, ResizableModule, RoundPipe, RxStateComputed, RxStateModule, RxStateOutput, RxStateProperty, RxStateRegister, RxStateSelect, SCRYPT_PARAMS, SETTINGS_COMPACT_ROWS, SETTINGS_DISPLAY_COLUMNS, SETTINGS_FILTER, SETTINGS_PAGE_SIZE, SETTINGS_SORTED_COLUMN, SETTINGS_STORAGE_KEY, SETTINGS_TRANSIENT_PROPERTIES, SHARED_ICONS, SHARED_IONIC_COMPONENTS, SHARED_MATERIAL_TESTING_PAGES, SHARED_STORAGE_TESTING_PAGES, SHARED_TESTING_PAGES, SOCIAL_CONFIG_OPTIONS, SOCIAL_ICONS, SOCIAL_TESTING_PAGES, SPACE_PLACEHOLDER_CHAR, SPACE_PLACEHOLDER_CHAR_REGEXP_GLOBAL, STARTUP_DATA_STORAGE_KEY, SafeHtmlPipe, SafeStylePipe, SelectPeerModal, SelectionLengthPipe, ServerErrorCodes, SettingsPage, SharedAsyncValidators, SharedBadgeModule, SharedDebugModule, SharedDirectivesModule, SharedFileAttachmentModule, SharedFormArrayValidators, SharedFormGroupValidators, SharedHotkeysModule, SharedIonicModule, SharedMarkdownModule, SharedMatAutocompleteModule, SharedMatBooleanModule, SharedMatChipsModule, SharedMatDateTimeModule, SharedMatDurationModule, SharedMatLatLongModule, SharedMatSwipeModule, SharedMaterialModule, SharedModule, SharedNamedFilterModule, SharedPipesModule, SharedRoutingModule, SharedTestingModule, SharedTestsPage, SharedToolbarModule, SharedValidators, SocialErrorCodes, SocialModule, SocialModuleOptionsToken, SocialTestingModule, Software, SplitArrayInChunksPipe, StartableService, StartupService, StatusById, StatusIds, StatusList, StorageDrivers, StorageExplorerComponent, StorageExplorerModule, StorageExplorerTestingModule, StorageExplorerTestingRoutingModule, StorageService, StorageServiceWrapper, StrIncludesPipe, StrLengthPipe, StrReplacePipe, SubMenuTabDirective, SwipeTestPage, TABLE_SETTINGS_ENUM, TOOLBAR_HEADER_ID, TRACKED_QUERIES_STORAGE_KEY, Table2TestPage, TableSelectColumnsComponent, TableTestPage, TableTestingModule, TableValidatorService, TextForm, TextFormTestingModule, TextFormTestingPage, TextPopover, TextPopoverTestingModule, TextPopoverTestingPage, ThrottledClickDirective, TimeFormatPipe, ToStringPipe, ToastTestingModule, ToastTestingPage, Toasts, TokenScope, ToolbarComponent, ToolbarToken, TranslatablePipe, TranslateContextPipe, TranslateContextService, TreeItemEntityUtils, TruncHtmlPipe, TruncTextPipe, TruncateHtmlPipe, USER_CONTROLLER, UploadFile, UploadFileComponent, UploadFilePopover, UploadFileTestingModule, UploadFileTestingPage, UriUtils, UrlUtils, UserController, UserEventModule, UserEventNotificationIcon, UserEventNotificationList, UserEventNotificationModal, UserEventTestService, UserEventTestingModule, UserEventTestingPage, UserSettings, UserToken, UserTokenTable, UsersPage, ValueFormatPipe, VersionUtils, accountToString, adaptValueToControl, addValueInArray, arrayDistinct, arrayResize, arraySize, asInputElement, assignSkipUndefined, base64ArrayBuffer, booleanToString, canHaveFocus, capitalizeFirstLetter, chainPromises, changeCaseToUnderscore, clearValueInArray, collectByProperty, collectByPropertyPath, compareValues, compareValuesDesc, compareVersionNumbers, composeComparators, compositionMarkdownExtensions, computeDecimalDegrees, computeDecimalPart, copyEntity2Form, createApolloClientOptions, createAppStartupInitializer, createPromiseEvent, createPromiseEventEmitter, createTestingImageDataUrl, createTrackerLink, decorateWithTakeUntil, deepMergeSkipUndefined, departmentToString, departmentsToString, disableAndClearControl, disableAndClearControls, disableControl, disableControls, emitPromiseEvent, enableControl, enableControls, enableRxStateProdMode, entityToString, equals, equalsOrNil, escapeRegExp, expansionAnimation, fadeInAnimation, fadeInOutAnimation, fadeInSlowAnimation, filterFalse, filterFormErrors, filterFormErrorsByPath, filterFormErrorsByPrefix, filterNotNil, filterNumberInput, filterTrue, findParentWithClass, firstArrayValue, firstFalse, firstFalsePromise, firstNotNil, firstNotNilPromise, firstPromise, firstTrue, firstTruePromise, focusInput, focusNextInput, focusPreviousInput, formatLatLong, formatLatitude, formatLongitude, fromDateISOString, fromScrollEndEvent, fromUnixMsTimestamp, fromUnixTimestamp, getCaretPosition, getColorContrast, getColorLuminance, getColorShade, getColorTint, getControlFromPath, getFocusableInputElements, getFormControlName, getFormErrors, getFormValueFromEntity, getInputRangeFromCaretIndex, getInputSelectionRangesFromMask, getProperty, getPropertyByPath, getPropertyByPathAsString, getRandomImage, getRandomImageWithCredit, getUserAgent, hasDeferDirective, hasFocusWithin, hexToRgb, hexToRgbArray, httpHeadersToRecordLink, initArrayControlsFromValues, initializeSharedModule, insertLink, interpolateString, intersectArrays, isAndroid, isAppInputField, isAppTextInputField, isBlankString, isCapacitor, isChrome, isControlHasInput, isDeferredOperation, isEdge, isEmptyArray, isEntityService, isFirefox, isFocusableElement, isIOS, isInputElement, isInstanceOf, isInt, isIpad, isLightColor, isMacOS, isMobile, isMutationOperation, isNil, isNilOrBlank, isNilOrNaN, isNotEmptyArray, isNotNil, isNotNilBoolean, isNotNilObject, isNotNilOrBlank, isNotNilOrNaN, isNotNilString, isNumber, isNumberRange, isOnFieldMode, isPrint, isProgressEvent, isPromise, isResponseEvent, isSafari, isSameVersion, isStartableService, isSubscriptionOperation, isTouchUi, isValidVersion, isVersionCompatible, isWebAnimationsSupported, isWindows, joinProperties, joinPropertiesPath, lastArrayValue, logFormErrors, loggerLink, markAllAsTouched, markAsUntouched, markControlAsTouched, markFormGroupAsTouched, maskitoAutoSelectByMaskPattern, maskitoPrefixPlugin, matchMedia, matchUpperCase, mergeLoadResult, mergeObjectsWithoutUndefined, mixHex, moveInputCaretToSeparator, newArray, noHtml, noTrailingSlash, notNilOrDefault, nullIfNilOrBlank, nullIfUndefined, numberOrNilAttribute, numberToString, parseBooleanText, parseLatLongText, parseLatitudeOrLongitude, parseVersion, propertiesPathComparator, propertyComparator, propertyPathComparator, provideAccountService, provideAllIonicIcons, provideIonicIcons, provideIonicIconsInitializer, providePersonService, referentialToString, referentialsToString, registerIonicIcons, remove, removeAll, removeDeferDirectives, removeDiacritics, removeDuplicatesFromArray, removeEnd, removeValueInArray, replaceAll, resetCalculatedValue, resizeArray, restoreTrackedQueries, rgbArrayToHex, rgbToHex, round, scrollFactory, selectInputContent, selectInputContentFromEvent, selectInputRange, setCalculatedValue, setControlEnabled, setControlRequired, setControlsEnabled, setFormErrors, setPropertyByPath, setTabIndex, sleep, slideDownAnimation, slideInAnimation, slideInOutAnimation, slideUpDownAnimation, sort, splitArrayInChunks, splitById, splitByProperty, splitDegreesToDDArray, splitDegreesToDDMMArray, splitDegreesToDDMMSSArray, startsWithUpperCase, suggestFromArray, suggestFromStringArray, tabindexComparator, testUserAgent, toBoolean, toDateISOString, toDuration, toFloat, toInt, toLoadData, toLoadResult, toNotNil, toNumber, toggleLinePrefix, trimEmptyToNull, truncateHtml, typeTextIntoInput, uncapitalizeFirstLetter, undefinedIfNull, underscoreToChangeCase, updateValueAndValidity, waitFor, waitForFalse, waitForTrue, waitIdle, waitWhilePending, wrapSelection };
58898
59670
  //# sourceMappingURL=sumaris-net.ngx-components.mjs.map