@sinequa/atomic-angular 2.5.0 → 2.7.0
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.
- package/README.md +11 -2
- package/features/did-you-mean/i18n/de.json +2 -1
- package/features/did-you-mean/i18n/en.json +2 -1
- package/features/did-you-mean/i18n/fr.json +2 -1
- package/features/user-profile/i18n/de.json +10 -0
- package/features/user-profile/i18n/en.json +10 -0
- package/features/user-profile/i18n/fr.json +10 -0
- package/fesm2022/sinequa-atomic-angular.mjs +757 -211
- package/fesm2022/sinequa-atomic-angular.mjs.map +1 -1
- package/package.json +1 -1
- package/providers/session-expiry/i18n/de.json +4 -2
- package/providers/session-expiry/i18n/en.json +4 -2
- package/providers/session-expiry/i18n/fr.json +4 -2
- package/types/sinequa-atomic-angular-document-workspaces.d.ts +1 -1
- package/types/sinequa-atomic-angular.d.ts +636 -310
|
@@ -2,11 +2,11 @@ import * as i0 from "@angular/core";
|
|
|
2
2
|
import { ChangeDetectionStrategy, ChangeDetectorRef, Component, DOCUMENT, DestroyRef, Directive, ElementRef, EnvironmentInjector, HostBinding, HostListener, Inject, Injectable, InjectionToken, Injector, LOCALE_ID, Optional, Pipe, Service, TemplateRef, ViewEncapsulation, afterNextRender, afterRenderEffect, assertInInjectionContext, booleanAttribute, computed, contentChild, contentChildren, effect, inject, input, isDevMode, linkedSignal, makeEnvironmentProviders, model, numberAttribute, output, provideEnvironmentInitializer, resource, runInInjectionContext, signal, untracked, viewChild, viewChildren } from "@angular/core";
|
|
3
3
|
import { BehaviorSubject, EMPTY, Subject, Subscription, catchError, debounceTime, filter, firstValueFrom, from, fromEvent, map, of, scan, shareReplay, switchMap, tap, throwError } from "rxjs";
|
|
4
4
|
import { TranslocoPipe, TranslocoService, provideTranslocoScope } from "@jsverse/transloco";
|
|
5
|
-
import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BookmarkIcon, BreakpointObserverService, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SlashIcon, SlidersIcon, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, isApplePlatform } from "@sinequa/galactik";
|
|
5
|
+
import { ArrowDownIcon, ArrowDownZaIcon, ArrowLeftIcon, ArrowRightFromBracketIcon, ArrowRightIcon, ArrowUpAzIcon, ArrowUpIcon, ArrowUpRightFromSquareIcon, ArrowsMaximizeIcon, AutocompleteComponent, AvatarComponent, AvatarFallbackComponent, AvatarImageComponent, BadgeComponent, BarsIcon, BellIcon, BookmarkIcon, BreakpointObserverService, ButtonComponent, CalendarDayIcon, CheckIcon, CheckboxComponent, ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, ChevronsLeftIcon, ChevronsRightIcon, CircleCheckIcon, CommentIcon, CommentsIcon, DIALOG_REF, DesktopIcon, DialogBodyComponent, DialogComponent, DialogContentComponent, DialogDescriptionComponent, DialogFooterComponent, DialogHeaderComponent, DialogTitleComponent, DropdownDirective, EditIcon, EllipsisIcon, EllipsisVerticalIcon, EyeIcon, EyeSlashIcon, FileHTMLIcon, FileIcon, FileOutputIcon, FileSpreadsheetIcon, FileTextIcon, FilterIcon, FilterXIcon, FlagEnglishIcon, FlagFrenchIcon, FlagGermanIcon, FlashlightIcon, FolderIcon, FolderOpenIcon, FrownIcon, GearIcon, GitForkIcon, GlobeIcon, HighlighterIcon, HistoryIcon, HomeIcon, ImageIcon, InboxIcon, InfoCircleIcon, InputComponent, InputControlDirective, InputIconRightDirective, KbdComponent, KbdGroupComponent, KeyIcon, KeyboardIcon, LanguageIcon, LightbulbIcon, LightbulbSlashIcon, LinkComponent, ListComponent, ListFilterIcon, ListItemActionComponent, ListItemAvatarDirective, ListItemComponent, ListItemLeftDirective, ListItemTextComponent, LoadingCircleIcon, LockIcon, MagnifyingGlassIcon, MagnifyingGlassMinusIcon, MagnifyingGlassPlusIcon, MenuComponent, MenuContentComponent, MenuItemComponent, MenuTriggerDirective, MoonIcon, PaletteIcon, PaperclipIcon, PenToSquareIcon, PlusIcon, PopoverDirective, PresentationIcon, QuestionCircleIcon, RadioComponent, RobotIcon, SaveIcon, SearchBarComponent, SearchInputComponent, SelectComponent, SelectTriggerDirective, SendIcon, SheetBodyComponent, SheetComponent, SheetHeaderComponent, SidebarInsetDirective, SlashIcon, SlidersIcon, SparklesIcon, SpinnerIcon, SquareCheckIcon, SquareIcon, SquareMinusIcon, StarIcon, SunBrightIcon, SwitchComponent, SyncIcon, TabComponent, TabListComponent, TabPanelComponent, TabsComponent, TagComponent, TagIcon, TagInputComponent, TagsIcon, TextareaComponent, TextareaControlDirective, ThumbsDownIcon, ThumbsUpIcon, ToggleComponent, ToggleGroupComponent, ToggleItemComponent, ToggleLeftIcon, ToggleRightIcon, TooltipDirective, TrashCanIcon, TrashIcon, TreeComponent, TreeItemComponent, TreeItemExpanderComponent, TreeItemGroupComponent, TriangleAlertIcon, UndoIcon, UserIcon, UserSecretIcon, VideoIcon, XMarkIcon, createCallable, injectCallRef, isApplePlatform } from "@sinequa/galactik";
|
|
6
6
|
import { withDevtools } from "@angular-architects/ngrx-toolkit";
|
|
7
7
|
import { getState, patchState, signalStore, signalStoreFeature, withComputed, withMethods, withState } from "@ngrx/signals";
|
|
8
8
|
import { ActivatedRoute, NavigationEnd, Router, RouterLink, RouterModule } from "@angular/router";
|
|
9
|
-
import { Audit, AuditEventType, EngineType, addAuditAdditionalInfo, addConcepts, aggItemRegex, bisect, buildPathsAndLevels, createAtomicClient, createHeaders, debug, error, escapeExpr, expiresSoon, extraColumns, filter as filter$1, flattenLegacyFilterValues, getDefaultClient, getMetadata, getQueryParamsFromUrl, getRelativeDate, getToken, getUrlParamsFromQueryParams, globalConfig, info, isExpired, isExternalAuthMode, isJsonable, isNotInputEvent, isObject, notify, parseValueAndOperatorFromItem,
|
|
9
|
+
import { Audit, AuditEventType, EngineType, addAuditAdditionalInfo, addConcepts, aggItemRegex, bisect, buildPathsAndLevels, createAtomicClient, createHeaders, debug, error, escapeExpr, expiresSoon, extraColumns, filter as filter$1, flattenLegacyFilterValues, getDefaultClient, getMetadata, getQueryParamsFromUrl, getRelativeDate, getToken, getUrlParamsFromQueryParams, globalConfig, info, isExpired, isExternalAuthMode, isJsonable, isNotInputEvent, isObject, notify, parseValueAndOperatorFromItem, resolveBearerToken, resolveOAuthProvider, resolveSAMLProvider, resultPages, setToken, suggestionsToTreeAggregationNodes, sysLang, translateAggregationToDateOptions, warn } from "@sinequa/atomic";
|
|
10
10
|
import { DomSanitizer, Title } from "@angular/platform-browser";
|
|
11
11
|
import MarkdownIt from "markdown-it";
|
|
12
12
|
import { DATE_PIPE_DEFAULT_OPTIONS, DATE_PIPE_DEFAULT_TIMEZONE, DatePipe, Location, NgComponentOutlet, NgStyle, NgTemplateOutlet } from "@angular/common";
|
|
@@ -214,47 +214,55 @@ function withAppFeatures() {
|
|
|
214
214
|
data: {}
|
|
215
215
|
}), withMethods((store) => {
|
|
216
216
|
const client = inject(ATOMIC_CLIENT);
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
Object.
|
|
229
|
-
|
|
217
|
+
const hydrate = (app) => {
|
|
218
|
+
patchState(store, app);
|
|
219
|
+
const columnMap = {};
|
|
220
|
+
Object.values(extraColumns).forEach((col) => {
|
|
221
|
+
columnMap[col.name.toLocaleLowerCase()] = col;
|
|
222
|
+
if (col.aliases) col.aliases.forEach((alias) => {
|
|
223
|
+
columnMap[alias.toLocaleLowerCase()] = col;
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
const schema = app.indexes._?.columns;
|
|
227
|
+
if (schema) {
|
|
228
|
+
Object.keys(schema).forEach((schemaKey) => {
|
|
229
|
+
const col = schema[schemaKey.toLocaleLowerCase()];
|
|
230
|
+
if (!col) return;
|
|
231
|
+
columnMap[schemaKey.toLocaleLowerCase()] = col;
|
|
230
232
|
if (col.aliases) col.aliases.forEach((alias) => {
|
|
231
233
|
columnMap[alias.toLocaleLowerCase()] = col;
|
|
232
234
|
});
|
|
233
235
|
});
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
Object.
|
|
237
|
-
const col = schema[
|
|
236
|
+
Object.keys(app.queries).forEach((queryKey) => {
|
|
237
|
+
const query = app.queries[queryKey];
|
|
238
|
+
if (query.columnsInfo?.columns) Object.values(query.columnsInfo.columns).forEach((c) => {
|
|
239
|
+
const col = schema[c.name.toLocaleLowerCase()];
|
|
238
240
|
if (!col) return;
|
|
239
|
-
columnMap[
|
|
240
|
-
if (
|
|
241
|
+
columnMap[c.name.toLocaleLowerCase()] = col;
|
|
242
|
+
if (c.aliases) c.aliases.split(/[,;|]/).map((a) => a.trim()).forEach((alias) => {
|
|
241
243
|
columnMap[alias.toLocaleLowerCase()] = col;
|
|
242
244
|
});
|
|
243
245
|
});
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
246
|
+
});
|
|
247
|
+
patchState(store, { columnMap });
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const fail = (error) => {
|
|
251
|
+
console.error("Error fetching app data:", error);
|
|
252
|
+
throw error;
|
|
253
|
+
};
|
|
254
|
+
return {
|
|
255
|
+
async initialize() {
|
|
256
|
+
let app;
|
|
257
|
+
try {
|
|
258
|
+
app = await client.api.app.get();
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return fail(error);
|
|
256
261
|
}
|
|
262
|
+
hydrate(app);
|
|
257
263
|
},
|
|
264
|
+
hydrate,
|
|
265
|
+
fail,
|
|
258
266
|
async initializeWithAppName(appName) {
|
|
259
267
|
const app = await client.api.app.get(appName);
|
|
260
268
|
patchState(store, app);
|
|
@@ -288,8 +296,9 @@ function withAppFeatures() {
|
|
|
288
296
|
};
|
|
289
297
|
},
|
|
290
298
|
getQueryByName(name) {
|
|
299
|
+
if (!name) return void 0;
|
|
291
300
|
const queries = store.queries();
|
|
292
|
-
return Object.values(queries).find((query) => query.name
|
|
301
|
+
return Object.values(queries).find((query) => query.name?.toLowerCase() === name.toLowerCase());
|
|
293
302
|
},
|
|
294
303
|
getQueryByIndex(index) {
|
|
295
304
|
const queries = store.queries();
|
|
@@ -500,7 +509,23 @@ const PrincipalStore = signalStore({ providedIn: "root" }, withDevtools("Princip
|
|
|
500
509
|
})), withMethods((store) => {
|
|
501
510
|
const client = inject(ATOMIC_CLIENT);
|
|
502
511
|
let pendingInitialization = null;
|
|
512
|
+
const hydrate = (principal) => {
|
|
513
|
+
const { userOverrideActive = false } = client.config;
|
|
514
|
+
patchState(store, {
|
|
515
|
+
...initialPrincipal,
|
|
516
|
+
...principal,
|
|
517
|
+
userOverrideActive,
|
|
518
|
+
state: "loaded"
|
|
519
|
+
});
|
|
520
|
+
};
|
|
521
|
+
const fail = (error) => {
|
|
522
|
+
console.error("Principal fetch failed", error);
|
|
523
|
+
patchState(store, { state: "error" });
|
|
524
|
+
throw error;
|
|
525
|
+
};
|
|
503
526
|
return {
|
|
527
|
+
hydrate,
|
|
528
|
+
fail,
|
|
504
529
|
principal() {
|
|
505
530
|
const { userOverrideActive: _, ...rest } = getState(store);
|
|
506
531
|
return Object.entries(rest).reduce((acc, [k, v]) => {
|
|
@@ -513,18 +538,9 @@ const PrincipalStore = signalStore({ providedIn: "root" }, withDevtools("Princip
|
|
|
513
538
|
pendingInitialization = (async () => {
|
|
514
539
|
patchState(store, { state: "loading" });
|
|
515
540
|
try {
|
|
516
|
-
|
|
517
|
-
const { userOverrideActive = false } = client.config;
|
|
518
|
-
patchState(store, {
|
|
519
|
-
...initialPrincipal,
|
|
520
|
-
...principal,
|
|
521
|
-
userOverrideActive,
|
|
522
|
-
state: "loaded"
|
|
523
|
-
});
|
|
541
|
+
hydrate(await client.api.principal.get());
|
|
524
542
|
} catch (error) {
|
|
525
|
-
|
|
526
|
-
patchState(store, { state: "error" });
|
|
527
|
-
throw error;
|
|
543
|
+
fail(error);
|
|
528
544
|
} finally {
|
|
529
545
|
pendingInitialization = null;
|
|
530
546
|
}
|
|
@@ -790,6 +806,9 @@ var QueryService = class QueryService {
|
|
|
790
806
|
result = signal({}, ...ngDevMode ? [{ debugName: "result" }] : /* istanbul ignore next */ []);
|
|
791
807
|
failure = computed(() => this.result().$error, ...ngDevMode ? [{ debugName: "failure" }] : /* istanbul ignore next */ []);
|
|
792
808
|
audit;
|
|
809
|
+
warnEmptySearch() {
|
|
810
|
+
firstValueFrom(this.transloco.selectTranslate("emptySearch", {}, "searches")).catch(() => "Please enter a search query.").then((message) => notify.warning(message));
|
|
811
|
+
}
|
|
793
812
|
search(q, includeQueryParams = true, audit) {
|
|
794
813
|
const $auditRecord = { auditEvents: [audit ?? this.audit ?? {
|
|
795
814
|
type: "Search_Text",
|
|
@@ -805,7 +824,7 @@ var QueryService = class QueryService {
|
|
|
805
824
|
if (!((this.queryParamsStore.filters?.() || []).length > 0) && !query.basket && query.action !== "open") {
|
|
806
825
|
if (this.appStore.allowEmptySearch(query?.name || "") === false && query?.text === "") {
|
|
807
826
|
warn("QueryService.search: Empty search query is not allowed. Returning empty result.");
|
|
808
|
-
|
|
827
|
+
this.warnEmptySearch();
|
|
809
828
|
const emptyResult = { records: [] };
|
|
810
829
|
this.result.set(emptyResult);
|
|
811
830
|
return of(emptyResult);
|
|
@@ -1256,6 +1275,16 @@ function AuthGuard() {
|
|
|
1256
1275
|
return true;
|
|
1257
1276
|
};
|
|
1258
1277
|
}
|
|
1278
|
+
const STARTUP_DATA_SOURCE = new InjectionToken("STARTUP_DATA_SOURCE", { factory: () => null });
|
|
1279
|
+
function provideStartupAggregation(endpoint = "bff/bootstrap") {
|
|
1280
|
+
return makeEnvironmentProviders([{
|
|
1281
|
+
provide: STARTUP_DATA_SOURCE,
|
|
1282
|
+
useFactory: () => {
|
|
1283
|
+
const client = inject(ATOMIC_CLIENT);
|
|
1284
|
+
return () => client.http.get(endpoint);
|
|
1285
|
+
}
|
|
1286
|
+
}]);
|
|
1287
|
+
}
|
|
1259
1288
|
var UserSettingsPersistence = class UserSettingsPersistence {
|
|
1260
1289
|
loaded = signal(false, ...ngDevMode ? [{ debugName: "loaded" }] : /* istanbul ignore next */ []);
|
|
1261
1290
|
isLoaded = this.loaded.asReadonly();
|
|
@@ -1735,18 +1764,25 @@ function withUserSettingsFeatures() {
|
|
|
1735
1764
|
}), withMethods((store) => {
|
|
1736
1765
|
const client = inject(ATOMIC_CLIENT);
|
|
1737
1766
|
const persistence = inject(UserSettingsPersistence);
|
|
1767
|
+
const hydrate = (settings) => {
|
|
1768
|
+
patchState(store, {
|
|
1769
|
+
...USER_SETTINGS_DEFAULTS,
|
|
1770
|
+
...settings
|
|
1771
|
+
});
|
|
1772
|
+
persistence.markLoaded();
|
|
1773
|
+
};
|
|
1774
|
+
const fail = (err) => {
|
|
1775
|
+
error("Error fetching user settings:", err);
|
|
1776
|
+
persistence.markUnloaded();
|
|
1777
|
+
};
|
|
1738
1778
|
return {
|
|
1779
|
+
hydrate,
|
|
1780
|
+
fail,
|
|
1739
1781
|
async initialize() {
|
|
1740
1782
|
try {
|
|
1741
|
-
|
|
1742
|
-
patchState(store, {
|
|
1743
|
-
...USER_SETTINGS_DEFAULTS,
|
|
1744
|
-
...settings
|
|
1745
|
-
});
|
|
1746
|
-
persistence.markLoaded();
|
|
1783
|
+
hydrate(await client.api.userSettings.get());
|
|
1747
1784
|
} catch (err) {
|
|
1748
|
-
|
|
1749
|
-
persistence.markUnloaded();
|
|
1785
|
+
fail(err);
|
|
1750
1786
|
}
|
|
1751
1787
|
},
|
|
1752
1788
|
async reset() {
|
|
@@ -1776,6 +1812,14 @@ function withAgentsFeatures() {
|
|
|
1776
1812
|
}));
|
|
1777
1813
|
}
|
|
1778
1814
|
const UserSettingsStore = signalStore({ providedIn: "root" }, withDevtools("UserSettings"), withBookmarkFeatures(), withRecentSearchesFeatures(), withSavedSearchesFeatures(), withBasketsFeatures(), withAssistantFeatures(), withUserSettingsFeatures(), withAlertsFeatures(), withAgentsFeatures(), withDarkModeFeatures(), withHighlightCategoriesFeatures());
|
|
1815
|
+
const toPart = (result) => result.status === "fulfilled" ? {
|
|
1816
|
+
ok: true,
|
|
1817
|
+
value: result.value
|
|
1818
|
+
} : {
|
|
1819
|
+
ok: false,
|
|
1820
|
+
error: result.reason
|
|
1821
|
+
};
|
|
1822
|
+
const provided = (part) => part && typeof part === "object" && part.ok === true ? Promise.resolve(part.value) : void 0;
|
|
1779
1823
|
const ROUTE_COMPONENTS = new InjectionToken("ROUTE_COMPONENTS", { factory: () => [] });
|
|
1780
1824
|
var ApplicationService = class ApplicationService {
|
|
1781
1825
|
userSettingsStore = inject(UserSettingsStore);
|
|
@@ -1785,6 +1829,7 @@ var ApplicationService = class ApplicationService {
|
|
|
1785
1829
|
router = inject(Router);
|
|
1786
1830
|
titleService = inject(Title);
|
|
1787
1831
|
client = inject(ATOMIC_CLIENT);
|
|
1832
|
+
startupDataSource = inject(STARTUP_DATA_SOURCE);
|
|
1788
1833
|
components = inject(ROUTE_COMPONENTS);
|
|
1789
1834
|
defaultComponent = computed(() => this.components.find((c) => c.path === "all")?.component, ...ngDevMode ? [{ debugName: "defaultComponent" }] : /* istanbul ignore next */ []);
|
|
1790
1835
|
defaultLayoutComponent = computed(() => this.components.find((c) => c.isRoot)?.component, ...ngDevMode ? [{ debugName: "defaultLayoutComponent" }] : /* istanbul ignore next */ []);
|
|
@@ -1807,18 +1852,46 @@ var ApplicationService = class ApplicationService {
|
|
|
1807
1852
|
}
|
|
1808
1853
|
async initializeStores(options) {
|
|
1809
1854
|
try {
|
|
1810
|
-
await this.
|
|
1855
|
+
const data = await this.loadStartupData();
|
|
1856
|
+
this.apply(data.app, this.appStore);
|
|
1811
1857
|
info("appStore", getState(this.appStore));
|
|
1812
|
-
|
|
1858
|
+
this.apply(data.principal, this.principalStore);
|
|
1813
1859
|
info("principalStore", getState(this.principalStore));
|
|
1814
1860
|
this.queryParamsStore.reset({ syncUrl: options?.syncUrl ?? !options?.isBootstrap });
|
|
1815
|
-
|
|
1861
|
+
this.apply(data.userSettings, this.userSettingsStore);
|
|
1816
1862
|
info("userSettingsStore", getState(this.userSettingsStore));
|
|
1817
1863
|
} catch (err) {
|
|
1818
1864
|
error("Error during application initialization", err);
|
|
1819
1865
|
throw err;
|
|
1820
1866
|
}
|
|
1821
1867
|
}
|
|
1868
|
+
async loadStartupData() {
|
|
1869
|
+
const source = await this.readStartupDataSource();
|
|
1870
|
+
const [app, principal, userSettings] = await Promise.allSettled([
|
|
1871
|
+
provided(source?.app) ?? this.client.api.app.get(),
|
|
1872
|
+
provided(source?.principal) ?? this.client.api.principal.get(),
|
|
1873
|
+
provided(source?.userSettings) ?? this.client.api.userSettings.get()
|
|
1874
|
+
]);
|
|
1875
|
+
return {
|
|
1876
|
+
app: toPart(app),
|
|
1877
|
+
principal: toPart(principal),
|
|
1878
|
+
userSettings: toPart(userSettings)
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
async readStartupDataSource() {
|
|
1882
|
+
if (!this.startupDataSource) return void 0;
|
|
1883
|
+
try {
|
|
1884
|
+
const data = await this.startupDataSource();
|
|
1885
|
+
if (data && typeof data === "object") return data;
|
|
1886
|
+
warn("Start-up aggregation answered no data, reading each part instead", data);
|
|
1887
|
+
} catch (err) {
|
|
1888
|
+
warn("Start-up aggregation failed, reading each part instead", err);
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
apply(part, store) {
|
|
1892
|
+
if (part.ok) store.hydrate(part.value);
|
|
1893
|
+
else store.fail(part.error);
|
|
1894
|
+
}
|
|
1822
1895
|
createRoutes() {
|
|
1823
1896
|
const { queries, data: custom, customJSONs = [] } = getState(this.appStore);
|
|
1824
1897
|
const queriesMap = Object.entries(queries).map(([key, value]) => ({
|
|
@@ -3174,11 +3247,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
3174
3247
|
});
|
|
3175
3248
|
var UserProfileService = class UserProfileService {
|
|
3176
3249
|
client = inject(ATOMIC_CLIENT);
|
|
3250
|
+
injector = inject(Injector);
|
|
3177
3251
|
resources = /* @__PURE__ */ new WeakMap();
|
|
3178
3252
|
getUserProfile(userId) {
|
|
3179
3253
|
let resourceRef = this.resources.get(userId);
|
|
3180
3254
|
if (!resourceRef) {
|
|
3181
3255
|
resourceRef = resource({
|
|
3256
|
+
injector: this.injector,
|
|
3182
3257
|
params: () => {
|
|
3183
3258
|
const id = userId();
|
|
3184
3259
|
return id ? { id } : void 0;
|
|
@@ -3357,17 +3432,61 @@ const QUERY_PARAMS_DEFAULTS = {
|
|
|
3357
3432
|
neuralSearch: void 0,
|
|
3358
3433
|
aggregation_overrides: void 0
|
|
3359
3434
|
};
|
|
3435
|
+
function parseHref(href) {
|
|
3436
|
+
let url;
|
|
3437
|
+
try {
|
|
3438
|
+
url = new URL(href, window.location.origin);
|
|
3439
|
+
} catch {
|
|
3440
|
+
return {
|
|
3441
|
+
path: "",
|
|
3442
|
+
params: new URLSearchParams()
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
if (url.hash.startsWith("#/")) {
|
|
3446
|
+
const route = url.hash.slice(1);
|
|
3447
|
+
const queryIndex = route.indexOf("?");
|
|
3448
|
+
const path = queryIndex === -1 ? route : route.slice(0, queryIndex);
|
|
3449
|
+
const search = queryIndex === -1 ? "" : route.slice(queryIndex + 1);
|
|
3450
|
+
return {
|
|
3451
|
+
path,
|
|
3452
|
+
params: new URLSearchParams(search)
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
return {
|
|
3456
|
+
path: "",
|
|
3457
|
+
params: url.searchParams
|
|
3458
|
+
};
|
|
3459
|
+
}
|
|
3460
|
+
function parseFilters(raw) {
|
|
3461
|
+
if (!raw) return [];
|
|
3462
|
+
const candidates = [raw];
|
|
3463
|
+
try {
|
|
3464
|
+
const decoded = decodeURIComponent(raw);
|
|
3465
|
+
if (decoded !== raw) candidates.push(decoded);
|
|
3466
|
+
} catch {}
|
|
3467
|
+
for (const candidate of candidates) try {
|
|
3468
|
+
const parsed = JSON.parse(candidate);
|
|
3469
|
+
if (Array.isArray(parsed)) return parsed;
|
|
3470
|
+
} catch {}
|
|
3471
|
+
return [];
|
|
3472
|
+
}
|
|
3360
3473
|
const QueryParamsStore = signalStore({ providedIn: "root" }, withDevtools("QueryParams"), withQueryParamsFeatures());
|
|
3361
3474
|
function withQueryParamsFeatures() {
|
|
3362
3475
|
return signalStoreFeature(withState(QUERY_PARAMS_DEFAULTS), withMethods((store, injector = inject(Injector), router = inject(Router), route = inject(ActivatedRoute), appStore = inject(AppStore)) => ({
|
|
3363
3476
|
setFromUrl(href) {
|
|
3364
3477
|
if (!href) return;
|
|
3365
|
-
const path =
|
|
3366
|
-
const
|
|
3367
|
-
const filters =
|
|
3368
|
-
const
|
|
3369
|
-
|
|
3370
|
-
|
|
3478
|
+
const { path, params } = parseHref(href);
|
|
3479
|
+
const text = params.get("q") ?? "";
|
|
3480
|
+
const filters = parseFilters(params.get("f"));
|
|
3481
|
+
const id = params.get("id") ?? "";
|
|
3482
|
+
const sort = params.get("s") ?? "";
|
|
3483
|
+
const tab = params.get("t") ?? "";
|
|
3484
|
+
const basket = params.get("b") ?? "";
|
|
3485
|
+
const name = params.get("n") ?? "";
|
|
3486
|
+
const spellingCorrectionMode = params.get("c") || void 0;
|
|
3487
|
+
const p = params.get("p");
|
|
3488
|
+
const parsedPage = p ? parseInt(p, 10) : NaN;
|
|
3489
|
+
const page = Number.isNaN(parsedPage) ? void 0 : parsedPage;
|
|
3371
3490
|
patchState(store, (state) => {
|
|
3372
3491
|
return {
|
|
3373
3492
|
...state,
|
|
@@ -4865,14 +4984,15 @@ var LoadingComponent = class LoadingComponent {
|
|
|
4865
4984
|
clearTimeout(timeout);
|
|
4866
4985
|
setTimeout(() => {
|
|
4867
4986
|
const url = this.route.snapshot.queryParams["returnUrl"] || null;
|
|
4868
|
-
if (
|
|
4869
|
-
this.router.navigateByUrl(url);
|
|
4987
|
+
if (url) {
|
|
4870
4988
|
this.queryParamsStore.setFromUrl(url);
|
|
4989
|
+
this.router.navigateByUrl(url);
|
|
4871
4990
|
} else this.router.navigate(["/"]);
|
|
4872
4991
|
}, 700);
|
|
4873
4992
|
}
|
|
4874
4993
|
});
|
|
4875
|
-
if (
|
|
4994
|
+
if (this.client.auth.isAuthenticated()) this.application.updateReadyState(true);
|
|
4995
|
+
else {
|
|
4876
4996
|
this.application.updateReadyState(false);
|
|
4877
4997
|
this.client.auth.login().then(() => {
|
|
4878
4998
|
this.application.updateReadyState(true);
|
|
@@ -6652,7 +6772,9 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
|
|
|
6652
6772
|
}
|
|
6653
6773
|
return `search-results-assistant`;
|
|
6654
6774
|
}, ...ngDevMode ? [{ debugName: "instanceId" }] : /* istanbul ignore next */ []);
|
|
6655
|
-
enabledUserInput = computed(() => this.appStore.assistants()[this.instanceId()]?.modeSettings
|
|
6775
|
+
enabledUserInput = computed(() => this.appStore.assistants()[this.instanceId()]?.modeSettings?.enabledUserInput === true, ...ngDevMode ? [{ debugName: "enabledUserInput" }] : /* istanbul ignore next */ []);
|
|
6776
|
+
allowAI = input(false, ...ngDevMode ? [{ debugName: "allowAI" }] : /* istanbul ignore next */ []);
|
|
6777
|
+
showAssistantAction = computed(() => this.allowAI() && this.appStore.isAssistantAllowed(this.instanceId()) && this.enabledUserInput(), ...ngDevMode ? [{ debugName: "showAssistantAction" }] : /* istanbul ignore next */ []);
|
|
6656
6778
|
showHint = signal(false, ...ngDevMode ? [{ debugName: "showHint" }] : /* istanbul ignore next */ []);
|
|
6657
6779
|
armed = true;
|
|
6658
6780
|
constructor() {
|
|
@@ -6717,6 +6839,13 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
|
|
|
6717
6839
|
isSignal: true,
|
|
6718
6840
|
isRequired: false,
|
|
6719
6841
|
transformFunction: null
|
|
6842
|
+
},
|
|
6843
|
+
allowAI: {
|
|
6844
|
+
classPropertyName: "allowAI",
|
|
6845
|
+
publicName: "allowAI",
|
|
6846
|
+
isSignal: true,
|
|
6847
|
+
isRequired: false,
|
|
6848
|
+
transformFunction: null
|
|
6720
6849
|
}
|
|
6721
6850
|
},
|
|
6722
6851
|
outputs: {
|
|
@@ -6775,7 +6904,7 @@ var MultiSelectionToolbarComponent = class MultiSelectionToolbarComponent {
|
|
|
6775
6904
|
</button>
|
|
6776
6905
|
</div>
|
|
6777
6906
|
|
|
6778
|
-
@if (
|
|
6907
|
+
@if (showAssistantAction()) {
|
|
6779
6908
|
<div class="pe-0!">
|
|
6780
6909
|
<button class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
|
|
6781
6910
|
<sparkles-icon aria-hidden="true" />
|
|
@@ -6879,7 +7008,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6879
7008
|
</button>
|
|
6880
7009
|
</div>
|
|
6881
7010
|
|
|
6882
|
-
@if (
|
|
7011
|
+
@if (showAssistantAction()) {
|
|
6883
7012
|
<div class="pe-0!">
|
|
6884
7013
|
<button class="cursor-pointer inline-flex items-center gap-1" (click)="attachToAssistant()">
|
|
6885
7014
|
<sparkles-icon aria-hidden="true" />
|
|
@@ -6929,6 +7058,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
6929
7058
|
cleared: [{
|
|
6930
7059
|
type: i0.Output,
|
|
6931
7060
|
args: ["cleared"]
|
|
7061
|
+
}],
|
|
7062
|
+
allowAI: [{
|
|
7063
|
+
type: i0.Input,
|
|
7064
|
+
args: [{
|
|
7065
|
+
isSignal: true,
|
|
7066
|
+
alias: "allowAI",
|
|
7067
|
+
required: false
|
|
7068
|
+
}]
|
|
6932
7069
|
}]
|
|
6933
7070
|
}
|
|
6934
7071
|
});
|
|
@@ -7117,17 +7254,18 @@ var InfinityScrollDirective = class InfinityScrollDirective {
|
|
|
7117
7254
|
alias: "scroll-options"
|
|
7118
7255
|
});
|
|
7119
7256
|
onScroll = output();
|
|
7120
|
-
observer
|
|
7121
|
-
if (entry.isIntersecting) this.onScroll.emit();
|
|
7122
|
-
}, this.options());
|
|
7257
|
+
observer;
|
|
7123
7258
|
el = inject(ElementRef);
|
|
7124
7259
|
constructor() {
|
|
7125
7260
|
afterNextRender(() => {
|
|
7261
|
+
this.observer = new IntersectionObserver(([entry]) => {
|
|
7262
|
+
if (entry.isIntersecting) this.onScroll.emit();
|
|
7263
|
+
}, this.options());
|
|
7126
7264
|
this.observer.observe(this.el.nativeElement);
|
|
7127
7265
|
});
|
|
7128
7266
|
}
|
|
7129
7267
|
ngOnDestroy() {
|
|
7130
|
-
this.observer
|
|
7268
|
+
this.observer?.disconnect();
|
|
7131
7269
|
}
|
|
7132
7270
|
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
7133
7271
|
minVersion: "12.0.0",
|
|
@@ -18606,7 +18744,6 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18606
18744
|
isBookmarked = computed(() => {
|
|
18607
18745
|
return this.userSettingsStore.isBookmarked(this.article());
|
|
18608
18746
|
}, ...ngDevMode ? [{ debugName: "isBookmarked" }] : /* istanbul ignore next */ []);
|
|
18609
|
-
title = computed(() => this.transloco.translate("bookmarks.bookmarkDocument"), ...ngDevMode ? [{ debugName: "title" }] : /* istanbul ignore next */ []);
|
|
18610
18747
|
async bookmark(e) {
|
|
18611
18748
|
e.stopPropagation();
|
|
18612
18749
|
const article = this.article();
|
|
@@ -18670,11 +18807,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18670
18807
|
properties: { "class.visible": "isBookmarked()" },
|
|
18671
18808
|
classAttribute: "cursor-pointer"
|
|
18672
18809
|
},
|
|
18673
|
-
providers: [provideTranslocoScope("
|
|
18810
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18674
18811
|
ngImport: i0,
|
|
18675
18812
|
template: `
|
|
18676
18813
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18677
|
-
[tooltip]="
|
|
18814
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18678
18815
|
[attr.aria-pressed]="isBookmarked()"
|
|
18679
18816
|
[attr.tabindex]="tabIndex()"
|
|
18680
18817
|
(click)="bookmark($event)">
|
|
@@ -18684,7 +18821,7 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18684
18821
|
@else {
|
|
18685
18822
|
<bookmark-icon />
|
|
18686
18823
|
}
|
|
18687
|
-
<span class="sr-only">{{
|
|
18824
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18688
18825
|
</button>
|
|
18689
18826
|
`,
|
|
18690
18827
|
isInline: true,
|
|
@@ -18732,6 +18869,11 @@ var BookmarkButtonComponent = class BookmarkButtonComponent {
|
|
|
18732
18869
|
"tooltip-offset"
|
|
18733
18870
|
],
|
|
18734
18871
|
exportAs: ["tooltip"]
|
|
18872
|
+
},
|
|
18873
|
+
{
|
|
18874
|
+
kind: "pipe",
|
|
18875
|
+
type: TranslocoPipe,
|
|
18876
|
+
name: "transloco"
|
|
18735
18877
|
}
|
|
18736
18878
|
]
|
|
18737
18879
|
});
|
|
@@ -18748,12 +18890,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18748
18890
|
imports: [
|
|
18749
18891
|
BookmarkIcon,
|
|
18750
18892
|
ButtonComponent,
|
|
18751
|
-
TooltipDirective
|
|
18893
|
+
TooltipDirective,
|
|
18894
|
+
TranslocoPipe
|
|
18752
18895
|
],
|
|
18753
|
-
providers: [provideTranslocoScope("
|
|
18896
|
+
providers: [provideTranslocoScope("bookmarks")],
|
|
18754
18897
|
template: `
|
|
18755
18898
|
<button [variant]="variant()" [size]="size()" [iconOnly]="true"
|
|
18756
|
-
[tooltip]="
|
|
18899
|
+
[tooltip]="'bookmarks.bookmarkDocument' | transloco"
|
|
18757
18900
|
[attr.aria-pressed]="isBookmarked()"
|
|
18758
18901
|
[attr.tabindex]="tabIndex()"
|
|
18759
18902
|
(click)="bookmark($event)">
|
|
@@ -18763,7 +18906,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
18763
18906
|
@else {
|
|
18764
18907
|
<bookmark-icon />
|
|
18765
18908
|
}
|
|
18766
|
-
<span class="sr-only">{{
|
|
18909
|
+
<span class="sr-only">{{ 'bookmarks.bookmarkDocument' | transloco }}</span>
|
|
18767
18910
|
</button>
|
|
18768
18911
|
`,
|
|
18769
18912
|
host: {
|
|
@@ -21804,7 +21947,7 @@ var DidYouMeanComponent = class DidYouMeanComponent {
|
|
|
21804
21947
|
<p>
|
|
21805
21948
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21806
21949
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21807
|
-
<span
|
|
21950
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21808
21951
|
</p>
|
|
21809
21952
|
}
|
|
21810
21953
|
@case ('correction') {
|
|
@@ -21854,7 +21997,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
21854
21997
|
<p>
|
|
21855
21998
|
<span class="me-1">{{ 'didYouMean.didYouMean' | transloco }}</span>
|
|
21856
21999
|
<a class="text-(--font-primary-base) hover:underline cursor-pointer" role="button" (click)="selectCorrected()">{{ correction() }}</a>
|
|
21857
|
-
<span
|
|
22000
|
+
<span>{{ 'didYouMean.didYouMeanSuffix' | transloco }}</span>
|
|
21858
22001
|
</p>
|
|
21859
22002
|
}
|
|
21860
22003
|
@case ('correction') {
|
|
@@ -23021,6 +23164,7 @@ var AggregationDateComponent = class AggregationDateComponent {
|
|
|
23021
23164
|
uid = nextAggregationDateId++;
|
|
23022
23165
|
startId = `datepicker-range-start-${this.uid}`;
|
|
23023
23166
|
endId = `datepicker-range-end-${this.uid}`;
|
|
23167
|
+
class = input("", ...ngDevMode ? [{ debugName: "class" }] : /* istanbul ignore next */ []);
|
|
23024
23168
|
name = input(null, ...ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []);
|
|
23025
23169
|
column = input.required(...ngDevMode ? [{ debugName: "column" }] : /* istanbul ignore next */ []);
|
|
23026
23170
|
id = input(null, ...ngDevMode ? [{ debugName: "id" }] : /* istanbul ignore next */ []);
|
|
@@ -23507,6 +23651,13 @@ var AggregationDateComponent = class AggregationDateComponent {
|
|
|
23507
23651
|
isStandalone: true,
|
|
23508
23652
|
selector: "aggregation-date, AggregationDate, aggregationdate",
|
|
23509
23653
|
inputs: {
|
|
23654
|
+
class: {
|
|
23655
|
+
classPropertyName: "class",
|
|
23656
|
+
publicName: "class",
|
|
23657
|
+
isSignal: true,
|
|
23658
|
+
isRequired: false,
|
|
23659
|
+
transformFunction: null
|
|
23660
|
+
},
|
|
23510
23661
|
name: {
|
|
23511
23662
|
classPropertyName: "name",
|
|
23512
23663
|
publicName: "name",
|
|
@@ -23622,10 +23773,10 @@ var AggregationDateComponent = class AggregationDateComponent {
|
|
|
23622
23773
|
aggregationOverridesChange: "aggregationOverridesChange",
|
|
23623
23774
|
searchText: "searchTextChange"
|
|
23624
23775
|
},
|
|
23625
|
-
host: { properties: { "class": "cn(\"@container
|
|
23776
|
+
host: { properties: { "class": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" } },
|
|
23626
23777
|
providers: [provideTranslocoScope("filters")],
|
|
23627
23778
|
ngImport: i0,
|
|
23628
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details
|
|
23779
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
|
|
23629
23780
|
styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"],
|
|
23630
23781
|
dependencies: [
|
|
23631
23782
|
{
|
|
@@ -23759,13 +23910,21 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
23759
23910
|
RadioComponent,
|
|
23760
23911
|
TooltipDirective
|
|
23761
23912
|
],
|
|
23762
|
-
host: { "[class]": "cn(\"@container
|
|
23763
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details
|
|
23913
|
+
host: { "[class]": "cn(\"@container flex h-[inherit] max-h-[inherit] flex-col\", class(), isEmpty() && \"hidden\")" },
|
|
23914
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion). A facet is a\n disclosure, not a grid row: see `aggregation-panel.html` for the why (same pattern, duplicated here\n since this facet type doesn't go through `AggregationPanelComponent`). The `<summary>` itself is\n `tabindex=\"-1\"`: the header's buttons (apply, clear, chevron) are the keyboard stops, see the\n chevron below. -->\n<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col space-y-2 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\">\n <!-- scroll-mb-3/scroll-mt-16: see aggregation-panel-row.html for the full rationale\n (bottom margin against the container's edge, much bigger top margin to also clear\n <More>'s sticky facet-name search box when navigating ArrowUp). -->\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible()\"\n [class]=\"rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n\n @if (hasFilters()) {\n @let clearLabel = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"clearLabel\"\n [tooltip]=\"clearLabel\"\n (click)=\"$event.stopPropagation(); clear()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n\n @if (\n (!isControlled() || emitOn() === \"apply\") &&\n selection() &&\n validSelection()\n ) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); apply()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [aria-label]=\"'filters.toggle' | transloco\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n </button>\n }\n </summary>\n\n <!-- content wrapper -->\n <!-- RGAA 11.5 / WCAG 1.3.1: the radios below all share one `name`, so they are one set\n and have to be exposed as one. `role=\"group\"` here rather than `role=\"radiogroup\"`\n on the list: a radiogroup owns its radios directly, and the <li> in between would be\n invalid owned children — the list would also stop announcing how many options it\n holds. The name is the aggregation's own title, the words the header already shows;\n with no title the group stays unnamed rather than carrying an empty label. -->\n @let groupName = aggregation()?.display;\n <form\n class=\"flex min-h-0 flex-1 flex-col\"\n (keydown.enter)=\"onOptionEnter($event)\">\n <div\n class=\"flex min-h-0 flex-1 flex-col\"\n role=\"group\"\n [attr.aria-label]=\"groupName ? (groupName | syslang | transloco) : null\">\n <ul\n class=\"flex min-h-0 flex-1 snap-y snap-start scrollbar-thin flex-col gap-0.5 overflow-auto pt-2\"\n role=\"list\">\n @for (option of dateOptions(); track option.display) {\n <li\n role=\"listitem\"\n [attr.aria-label]=\"\n maskedLabel(option) ?? (option.display | syslang | transloco)\n \"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === option.display &&\n 'bg-(--bg-primary-lighter)',\n option.hidden && 'hidden',\n option.disabled && 'pointer-events-none opacity-40'\n )\n \"\n [attr.aria-hidden]=\"option.disabled\">\n <radio\n name=\"date-filter\"\n class=\"grow\"\n [checked]=\"dateModel().option === option.display\"\n [disabled]=\"option.disabled\"\n (checkedChange)=\"selectOption(option.display)\">\n {{\n maskedLabel(option) ?? (option.display | syslang | transloco)\n }}\n </radio>\n </li>\n }\n\n @if (allowCustomRange) {\n <li\n role=\"listitem\"\n aria-label=\"open date range picker\"\n [class]=\"\n cn(\n itemStateClass,\n 'flex cursor-pointer items-center gap-(--spacing-xs) p-2 text-sm text-(--font-neutral-base) select-none',\n dateModel().option === 'custom-range' &&\n 'bg-(--bg-primary-lighter)'\n )\n \">\n <radio\n name=\"date-filter\"\n class=\"grow items-center\"\n [checked]=\"dateModel().option === 'custom-range'\"\n (checkedChange)=\"selectOption('custom-range')\">\n <div class=\"flex flex-wrap items-center gap-x-1 gap-y-1\">\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"startId\"\n class=\"radio-label min-w-8 truncate\"\n >{{ \"filters.from\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"startId\"\n name=\"start\"\n type=\"text\"\n readonly\n [value]=\"customRangeFrom()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n <div class=\"flex items-center gap-1\">\n <label\n [attr.for]=\"endId\"\n class=\"radio-label min-w-8 truncate text-right\"\n >{{ \"filters.to\" | transloco }}</label\n >\n <input-group\n class=\"h-8 max-w-[18ch] min-w-[18ch] has-[[readonly]]:border-(--stroke-primary-muted)\">\n <input\n input-control\n [id]=\"endId\"\n name=\"end\"\n type=\"text\"\n readonly\n [value]=\"customRangeTo()\"\n (click)=\"selectAndOpenDialog()\"\n (keydown.enter)=\"onRangeInputEnter($event)\" />\n </input-group>\n </div>\n </div>\n </radio>\n </li>\n }\n </ul>\n </div>\n </form>\n</details>\n",
|
|
23764
23915
|
styles: [":host{min-width:200px}ul[role=list]{scrollbar-width:thin}\n"]
|
|
23765
23916
|
}]
|
|
23766
23917
|
}],
|
|
23767
23918
|
ctorParameters: () => [],
|
|
23768
23919
|
propDecorators: {
|
|
23920
|
+
class: [{
|
|
23921
|
+
type: i0.Input,
|
|
23922
|
+
args: [{
|
|
23923
|
+
isSignal: true,
|
|
23924
|
+
alias: "class",
|
|
23925
|
+
required: false
|
|
23926
|
+
}]
|
|
23927
|
+
}],
|
|
23769
23928
|
name: [{
|
|
23770
23929
|
type: i0.Input,
|
|
23771
23930
|
args: [{
|
|
@@ -24091,6 +24250,7 @@ var AggregationPanelComponent = class AggregationPanelComponent {
|
|
|
24091
24250
|
allUnselected: "allUnselected",
|
|
24092
24251
|
loadedMore: "loadedMore"
|
|
24093
24252
|
},
|
|
24253
|
+
host: { classAttribute: "flex min-h-0 flex-1 flex-col" },
|
|
24094
24254
|
providers: [provideTranslocoScope("filters")],
|
|
24095
24255
|
viewQueries: [{
|
|
24096
24256
|
propertyName: "searchBox",
|
|
@@ -24100,7 +24260,7 @@ var AggregationPanelComponent = class AggregationPanelComponent {
|
|
|
24100
24260
|
isSignal: true
|
|
24101
24261
|
}],
|
|
24102
24262
|
ngImport: i0,
|
|
24103
|
-
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's buttons (apply, clear,\n select-all, chevron), never the header row, and the chevron is the keyboard way to collapse.\n Label, header actions and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (!isCollapsed()) {\n @if (hasFilters()) {\n @let label = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); cleared.emit()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n @if (selection()) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); applied.emit()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n\n <!-- Nothing is projected into this box, so it had no accessible name at all (RGAA 11.1) —\n only its position next to the facet's title said what it selects. The name is stable\n whichever way it is about to act: `aria-checked` (true / mixed / false) is what tells the\n user where the selection stands. -->\n <checkbox\n class=\"shrink-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selection() && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\" />\n }\n\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div>\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <!-- No `hotkey`: this field is one of many facet search boxes on the page, so it must not\n advertise a global shortcut. It used to pass `''` to neutralize SearchInput's old\n `\"shift+/\"` default, which left an empty `aria-keyshortcuts` attribute behind; the default\n is gone, so omitting the input is enough. -->\n <!-- A placeholder is not a label (RGAA 11.1): it goes away on the first keystroke, and it is\n the same word in every facet panel of the page. The name says WHICH facet this box\n filters — the one thing that tells a dozen identical fields apart. -->\n <!-- mt-2: without it, the search box sits flush under <summary>, and the header's focus ring reads as touching it. -->\n <SearchInput\n class=\"mt-2 mb-3\"\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group p-0.5\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"(isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\"\n class=\"m-0 flex h-8 w-full scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n</details>\n",
|
|
24263
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's buttons (apply, clear,\n select-all, chevron), never the header row, and the chevron is the keyboard way to collapse.\n Label, header actions and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (!isCollapsed()) {\n @if (hasFilters()) {\n @let label = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); cleared.emit()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n @if (selection()) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); applied.emit()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n\n <!-- Nothing is projected into this box, so it had no accessible name at all (RGAA 11.1) —\n only its position next to the facet's title said what it selects. The name is stable\n whichever way it is about to act: `aria-checked` (true / mixed / false) is what tells the\n user where the selection stands. -->\n <checkbox\n class=\"shrink-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selection() && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\" />\n }\n\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <!-- No `hotkey`: this field is one of many facet search boxes on the page, so it must not\n advertise a global shortcut. It used to pass `''` to neutralize SearchInput's old\n `\"shift+/\"` default, which left an empty `aria-keyshortcuts` attribute behind; the default\n is gone, so omitting the input is enough. -->\n <!-- A placeholder is not a label (RGAA 11.1): it goes away on the first keystroke, and it is\n the same word in every facet panel of the page. The name says WHICH facet this box\n filters — the one thing that tells a dozen identical fields apart. -->\n <!-- mt-2: without it, the search box sits flush under <summary>, and the header's focus ring reads as touching it. -->\n <SearchInput\n class=\"mt-2 mb-3 shrink-0\"\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"(isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n</details>\n",
|
|
24104
24264
|
dependencies: [
|
|
24105
24265
|
{
|
|
24106
24266
|
kind: "directive",
|
|
@@ -24243,7 +24403,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
24243
24403
|
TooltipDirective
|
|
24244
24404
|
],
|
|
24245
24405
|
providers: [provideTranslocoScope("filters")],
|
|
24246
|
-
|
|
24406
|
+
host: { class: "flex min-h-0 flex-1 flex-col" },
|
|
24407
|
+
template: "<!-- `<details>`/`<summary>` stay native (open/close, the `name`-based exclusive accordion): a facet is\n a disclosure, not a grid row. They used to sit in an `@angular/aria` grid so ArrowUp/ArrowDown could\n jump from one facet header to the next, but a grid without headers reads as a layout table\n (RGAA 5.3). The `<summary>` itself is `tabindex=\"-1\"`: Tab walks the header's buttons (apply, clear,\n select-all, chevron), never the header row, and the chevron is the keyboard way to collapse.\n Label, header actions and body are `<ng-template>`s: `<ng-content select=\"…\">` can only be\n declared once, and the body is projected content. -->\n\n<ng-template #labelSlot>\n <ng-content select=\"label\">\n @let icon = aggregation()?.icon;\n @if (icon) {\n <FaIcon [faClass]=\"icon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n</ng-template>\n\n<ng-template #headerActionsSlot>\n @if (showFiltersCount() && filtersCount() > 0) {\n <Badge variant=\"number\" fill=\"primary\" scheme=\"sage\" size=\"xs\" class=\"ml-1\">\n {{ filtersCount() }}\n </Badge>\n }\n @if (!isCollapsed()) {\n @if (hasFilters()) {\n @let label = \"filters.clearFilters\" | transloco;\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n class=\"shrink-0\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); cleared.emit()\">\n <filter-x-icon class=\"shrink-0\" />\n </button>\n }\n @if (selection()) {\n @let label = \"filters.apply\" | transloco;\n <button\n type=\"button\"\n variant=\"accent\"\n size=\"sm\"\n class=\"overflow-hidden\"\n [aria-label]=\"label\"\n [tooltip]=\"label\"\n (click)=\"$event.stopPropagation(); applied.emit()\">\n <FilterIcon class=\"shrink-0\" />\n <span class=\"truncate\">{{ label }}</span>\n </button>\n }\n\n <!-- Nothing is projected into this box, so it had no accessible name at all (RGAA 11.1) —\n only its position next to the facet's title said what it selects. The name is stable\n whichever way it is about to act: `aria-checked` (true / mixed / false) is what tells the\n user where the selection stands. -->\n <checkbox\n class=\"shrink-0\"\n [checked]=\"isAllSelected()\"\n [indeterminate]=\"selection() && !isAllSelected()\"\n [aria-label]=\"'filters.selectAllFilters' | transloco\"\n (checkedChange)=\"$event ? allSelected.emit() : allUnselected.emit()\" />\n }\n\n @if (collapsible()) {\n <!-- The `<summary>` is out of the tab order (`tabindex=\"-1\"`), so this button is the keyboard way to\n collapse the facet. Its click bubbles to the `<summary>`, whose handler does the toggle, and it\n exposes `aria-expanded`. -->\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n size=\"sm\"\n [attr.aria-expanded]=\"!isCollapsed()\"\n [tooltip]=\"'filters.toggle' | transloco\"\n class=\"shrink-0 cursor-pointer [&_svg]:transition-transform [&_svg]:duration-150 group-open:[&_svg]:rotate-90\">\n <chevron-right class=\"shrink-0\" />\n <span class=\"sr-only\">{{ \"filters.toggle\" | transloco }}</span>\n </button>\n }\n</ng-template>\n\n<ng-template #bodySlot>\n <div class=\"flex min-h-0 flex-1 flex-col\">\n @if (aggregation()?.searchable && itemsLength() > 0) {\n <!-- No `hotkey`: this field is one of many facet search boxes on the page, so it must not\n advertise a global shortcut. It used to pass `''` to neutralize SearchInput's old\n `\"shift+/\"` default, which left an empty `aria-keyshortcuts` attribute behind; the default\n is gone, so omitting the input is enough. -->\n <!-- A placeholder is not a label (RGAA 11.1): it goes away on the first keystroke, and it is\n the same word in every facet panel of the page. The name says WHICH facet this box\n filters — the one thing that tells a dozen identical fields apart. -->\n <!-- mt-2: without it, the search box sits flush under <summary>, and the header's focus ring reads as touching it. -->\n <SearchInput\n class=\"mt-2 mb-3 shrink-0\"\n [placeholder]=\"'search' | transloco\"\n [clearLabel]=\"'filters.clearSearch' | transloco\"\n [aria-label]=\"\n 'filters.searchInFacet'\n | transloco\n : { facet: (aggregation()?.display | syslang | transloco) }\n \"\n [(value)]=\"searchText\">\n <ng-content select=\"[search-addon]\" />\n </SearchInput>\n }\n\n <ng-content />\n\n @if (hasMore() && searchedItemsLength() === 0) {\n <button\n type=\"button\"\n class=\"mt-1 flex w-full shrink-0 justify-center\"\n [attr.aria-label]=\"'loadMore' | transloco\"\n (click)=\"loadedMore.emit()\">\n {{ \"loadMore\" | transloco }}\n </button>\n }\n </div>\n</ng-template>\n\n<details\n [attr.open]=\"expanded()\"\n [attr.name]=\"id()\"\n class=\"group flex min-h-0 flex-1 flex-col p-0.5 details-content:flex details-content:min-h-0 details-content:flex-1 details-content:flex-col\"\n (toggle)=\"onToggle($event)\">\n <summary\n tabindex=\"-1\"\n [class.cursor-pointer]=\"collapsible() && !isEmpty()\"\n [class]=\"(isEmpty() ? 'text-(--font-neutral-muted) ' : '') + rowStateClass\"\n class=\"m-0 flex h-8 w-full shrink-0 scroll-mt-16 scroll-mb-3 items-center gap-1 pl-1 font-semibold select-none\"\n (click)=\"onHeaderClick($event)\">\n <ng-container [ngTemplateOutlet]=\"labelSlot\" />\n <ng-container [ngTemplateOutlet]=\"headerActionsSlot\" />\n </summary>\n <ng-container [ngTemplateOutlet]=\"bodySlot\" />\n</details>\n"
|
|
24247
24408
|
}]
|
|
24248
24409
|
}],
|
|
24249
24410
|
ctorParameters: () => [],
|
|
@@ -25074,7 +25235,7 @@ var AggregationTreeComponent = class AggregationTreeComponent {
|
|
|
25074
25235
|
filtersChange: "filtersChange",
|
|
25075
25236
|
searchText: "searchTextChange"
|
|
25076
25237
|
},
|
|
25077
|
-
host: { properties: { "class": "cn(\"
|
|
25238
|
+
host: { properties: { "class": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isHidden())) && \"hidden\")" } },
|
|
25078
25239
|
queries: [{
|
|
25079
25240
|
propertyName: "itemDirective",
|
|
25080
25241
|
first: true,
|
|
@@ -25083,7 +25244,7 @@ var AggregationTreeComponent = class AggregationTreeComponent {
|
|
|
25083
25244
|
isSignal: true
|
|
25084
25245
|
}],
|
|
25085
25246
|
ngImport: i0,
|
|
25086
|
-
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
|
|
25247
|
+
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n",
|
|
25087
25248
|
dependencies: [
|
|
25088
25249
|
{
|
|
25089
25250
|
kind: "directive",
|
|
@@ -25257,8 +25418,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
25257
25418
|
TreeItemGroup,
|
|
25258
25419
|
AggregationPanelComponent
|
|
25259
25420
|
],
|
|
25260
|
-
host: { "[class]": "cn(\"
|
|
25261
|
-
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n class=\"max-h-(--scroll-height,20rem) w-full scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
|
|
25421
|
+
host: { "[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isHidden())) && \"hidden\")" },
|
|
25422
|
+
template: "@if (!aggregation()?.isTree) {\n <div role=\"alert\" class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregationTree component does not support list aggregations. Please use\n the <Aggregation /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto pb-3\">\n <ul\n ngTree\n #tree=\"ngTree\"\n [multi]=\"true\"\n [(value)]=\"selected\"\n (valueChange)=\"onSelectionChange($event)\"\n /* Translated like the flat list's own label (aggregation-list.html): this was the raw\n `display`, so the accessible name of a whole facet came out in the source language.\n The pipes apply to the whole nullish chain, `|` being the loosest operator Angular\n parses — no parentheses needed. */\n [attr.aria-label]=\"aggregation()?.display ?? name() | syslang | transloco\"\n class=\"basic-tree m-1\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: nodes(), parent: tree }\" />\n </ul>\n </div>\n</aggregation-panel>\n\n<ng-template #treeNodes let-nodes=\"nodes\" let-parent=\"parent\">\n @for (node of nodes; track node.$path) {\n @let label = node.display ?? node.value ?? \"\";\n <li\n ngTreeItem\n #treeItem=\"ngTreeItem\"\n [parent]=\"parent\"\n [value]=\"node.$path\"\n [label]=\"label\"\n [disabled]=\"node.disabled\"\n [(expanded)]=\"node.$opened\"\n (expandedChange)=\"onExpandedChange(node, $event)\"\n [attr.aria-label]=\"label\"\n [attr.aria-describedby]=\"\n nodeDescription(node) ? descriptionId(node.$path) : null\n \"\n [class]=\"cn('m-0 block w-full p-0', itemStateClass)\"\n (keydown.enter)=\"onTreeItemEnterKey(node, $event)\"\n (keydown.space)=\"onTreeItemSpaceKey(node, $event)\">\n <!-- 2. Ce div prend tout l'espace et intercepte tous les clics de la ligne -->\n <div\n class=\"flex w-full cursor-pointer items-center gap-2 py-0.5 text-sm leading-6\"\n (click)=\"onTreeItemRowClick(node, $event)\"\n (mousedown)=\"$event.stopPropagation()\"\n (pointerdown)=\"$event.stopPropagation()\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n a role=\"treeitem\" admits no focusable descendant, and the click must reach\n the row handler above. Selection is announced by the <li>'s aria-selected. -->\n <!-- --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. Set as a custom property rather than a utility\n class: the CVA already sets gap-(--spacing-xs), and two same-specificity\n classes would be resolved by stylesheet order, not by us. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"treeItem.selected() || node.$selectedVisually || false\"\n [disabled]=\"node.disabled\"\n [class]=\"\n cn(\n 'pointer-events-none',\n (treeItem.selected() || node.$selectedVisually) &&\n 'selected-icon'\n )\n \" />\n </span>\n }\n <!-- Depth indent sits on the chevron, after the checkbox: the checkboxes of every\n level stay flush left (aligned with a flat list's), and since this adds no extra\n flex child, the step between consecutive levels is exactly one indent. -->\n @let depthIndent =\n \"calc(var(--agg-tree-indent, 0.5rem) * \" +\n (treeItem.level() - 1) +\n \")\";\n @if (node.hasChildren) {\n <!-- Expand/collapse affordance for the mouse only, hence a <span> and not a <button>:\n the keyboard already expands with ArrowRight / ArrowLeft, handled by ngTreeItem, and\n a role=\"treeitem\" admits no focusable descendant — so this was a <button> nobody\n could reach, already carrying aria-hidden and tabindex=\"-1\" to say so. Every audit\n reads it as a button with no accessible name (RGAA 7.1, 11.9). Same shape as the leaf\n branch below. -->\n <span\n aria-hidden=\"true\"\n (click)=\"toggleTreeNode(treeItem, $event)\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex cursor-pointer items-center justify-center\">\n <ChevronRightIcon\n [class]=\"\n cn('expand-icon shrink-0', node.$opened && 'rotate-90')\n \" />\n <FolderIcon class=\"shrink-0\" />\n </span>\n } @else {\n <span\n aria-hidden=\"true\"\n [style.marginInlineStart]=\"depthIndent\"\n class=\"inline-flex items-center justify-center\">\n <!-- Reserves the chevron's own width (`.expand-icon` is a styling hook with\n no rule of its own), so a leaf lines up with its parent nodes. -->\n <span class=\"expand-icon w-4 shrink-0\"></span>\n <FileIcon class=\"size-4 shrink-0\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <span\n class=\"grow truncate select-none\"\n (click)=\"onTreeItemTextClick(node, $event)\">\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: node,\n field: aggregation()?.column,\n selected: treeItem.selected() ?? false,\n count: node.count,\n name: label,\n searchText: searchText(),\n level: node.$level ?? 0,\n expanded: node.$opened ?? false,\n hasChildren: node.hasChildren ?? false\n }\" />\n </span>\n } @else {\n <!-- ce span est essentiel pour le style -->\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'grow truncate select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isTreeItemFiltered(node)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n label\n : label\n \"\n (click)=\"onTreeItemTextClick(node, $event)\">\n @for (\n chunk of label | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && node.count > 0) {\n <span class=\"ml-auto px-1 text-xs\" aria-hidden=\"true\">{{\n node.count\n }}</span>\n }\n }\n @let description = nodeDescription(node);\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the treeitem's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(node.$path)\">{{ description }}</span>\n }\n </div>\n </li>\n @if (node.hasChildren) {\n <!-- No indentation here: it would shift the checkbox too. Depth is applied\n inside the row, after the checkbox, so every checkbox stays flush left. -->\n <!-- The <ul> sits INSIDE the deferred content, not around it: `ngTreeItemGroup` only\n instantiates its view when the node opens, so a <ul> declared outside stayed in the DOM,\n empty, for every collapsed node — a structural tag with nothing to structure (RGAA 8.9).\n The @if cannot move onto the <ul> either: the directive registers itself with its tree\n item on init and has to exist for as long as the item does. -->\n <ng-template\n ngTreeItemGroup\n [ownedBy]=\"treeItem\"\n #group=\"ngTreeItemGroup\">\n <ul role=\"group\">\n <ng-template\n [ngTemplateOutlet]=\"treeNodes\"\n [ngTemplateOutletContext]=\"{ nodes: node.items, parent: group }\" />\n </ul>\n </ng-template>\n }\n }\n</ng-template>\n"
|
|
25262
25423
|
}]
|
|
25263
25424
|
}],
|
|
25264
25425
|
ctorParameters: () => [],
|
|
@@ -25915,7 +26076,7 @@ var AggregationListComponent = class AggregationListComponent {
|
|
|
25915
26076
|
},
|
|
25916
26077
|
host: {
|
|
25917
26078
|
listeners: { "keydown.enter": "onHostEnterKey($event)" },
|
|
25918
|
-
properties: { "class": "cn(\"
|
|
26079
|
+
properties: { "class": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")" }
|
|
25919
26080
|
},
|
|
25920
26081
|
queries: [{
|
|
25921
26082
|
propertyName: "itemDirective",
|
|
@@ -25947,7 +26108,7 @@ var AggregationListComponent = class AggregationListComponent {
|
|
|
25947
26108
|
}
|
|
25948
26109
|
],
|
|
25949
26110
|
ngImport: i0,
|
|
25950
|
-
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"
|
|
26111
|
+
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isListItemFiltered(item, field)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n (listItemName(item) | syslang)\n : (listItemName(item) | syslang)\n \"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n",
|
|
25951
26112
|
dependencies: [
|
|
25952
26113
|
{
|
|
25953
26114
|
kind: "directive",
|
|
@@ -26088,10 +26249,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26088
26249
|
CheckboxComponent
|
|
26089
26250
|
],
|
|
26090
26251
|
host: {
|
|
26091
|
-
"[class]": "cn(\"
|
|
26252
|
+
"[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\", class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")",
|
|
26092
26253
|
"(keydown.enter)": "onHostEnterKey($event)"
|
|
26093
26254
|
},
|
|
26094
|
-
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"
|
|
26255
|
+
template: "@if (aggregation()?.isTree) {\n <div class=\"p-2 text-sm text-red-500\">\n <triangle-alert-icon class=\"mr-1 shrink-0\" />\n The aggregation component no longer supports tree aggregations. Please use\n the <AggregationTree /> component instead.\n </div>\n}\n<aggregation-panel\n [id]=\"id()\"\n [collapsible]=\"collapsible()\"\n [collapsed]=\"collapsed()\"\n [isEmpty]=\"isEmpty()\"\n [isDate]=\"isDate()\"\n [aggregation]=\"aggregation()\"\n [showFiltersCount]=\"showFiltersCount()\"\n [filtersCount]=\"filtersCount()\"\n [hasFilters]=\"hasFilters()\"\n [selection]=\"selection()\"\n [isAllSelected]=\"isAllSelected()\"\n [(searchText)]=\"searchText\"\n [itemsLength]=\"items().length\"\n [hasMore]=\"aggregation()?.$hasMore ?? false\"\n [searchedItemsLength]=\"searchedItems().length\"\n (cleared)=\"clear()\"\n (applied)=\"apply()\"\n (allSelected)=\"selectAll()\"\n (allUnselected)=\"unselectAll()\"\n (loadedMore)=\"loadMore()\">\n <!-- Relay the host's `label` projection into the panel's `label` slot. When the\n host projects nothing, the local <ng-content> falls back to the icon + display\n (mirrors AggregationPanel's own fallback), so the header is never empty. -->\n <ng-container ngProjectAs=\"label\">\n <ng-content select=\"label\">\n @let labelIcon = aggregation()?.icon;\n @if (labelIcon) {\n <FaIcon [faClass]=\"labelIcon\" class=\"mr-1 shrink-0\" />\n }\n <span class=\"grow truncate\">{{\n aggregation()?.display | syslang | transloco\n }}</span>\n </ng-content>\n </ng-container>\n <div\n #scrollElement\n class=\"min-h-0 w-full flex-1 scrollbar-thin overflow-auto px-1 pt-1 pb-3\">\n <div\n ngListbox\n multi\n selectionMode=\"explicit\"\n [value]=\"selectedValues()\"\n (valueChange)=\"syncListboxValue($event)\"\n [wrap]=\"false\"\n [softDisabled]=\"false\"\n class=\"relative w-full\"\n [style.height]=\"virtualizer.getTotalSize() + 'px'\"\n role=\"listbox\"\n aria-multiselectable=\"true\"\n [attr.aria-label]=\"aggregation()?.display | syslang | transloco\">\n @for (vItem of virtualizer.getVirtualItems(); track vItem.index) {\n @let item = items()[vItem.index];\n @let description = rowDescription(item);\n <div\n #row\n ngOption\n [attr.aria-describedby]=\"\n description ? descriptionId(vItem.index) : null\n \"\n [attr.data-index]=\"vItem.index\"\n [value]=\"item.value\"\n [disabled]=\"item.count === 0\"\n [class]=\"cn('absolute w-full', menuItemBaseClass)\"\n (keydown.enter)=\"onOptionEnterKey(item, $event)\"\n (keydown.space)=\"\n onListItemRowClick(item, $event); $event.preventDefault()\n \"\n [style.transform]=\"'translateY(' + vItem.start + 'px)'\"\n role=\"option\">\n <ng-container\n [ngTemplateOutlet]=\"listItemTpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: aggregation()?.column\n }\">\n </ng-container>\n @if (description) {\n <!-- `hidden`, not `sr-only`: read through aria-describedby only. A visually-hidden\n span would also be counted in the option's name (computed from its content). -->\n <span hidden [id]=\"descriptionId(vItem.index)\">{{\n description\n }}</span>\n }\n </div>\n }\n </div>\n </div>\n</aggregation-panel>\n\n<ng-template #listItemTpl let-item let-field=\"field\">\n <a\n [attr.aria-label]=\"listItemName(item) | syslang\"\n [class]=\"\n cn(\n '-m-1.5 flex min-w-0 grow items-center gap-2 leading-6',\n item.count === 0 && 'pointer-events-none text-(--font-neutral-muted)'\n )\n \"\n (click)=\"onListItemRowClick(item, $event)\">\n @if (showCheckbox()) {\n <!-- `inert` keeps the native input out of the tab order and out of hit-testing:\n keyboard navigation belongs to ngListbox/ngOption (roving tabindex), the\n selection is already exposed by role=\"option\" + aria-selected, and the click\n must reach the <a> above. Same treatment as the tree.\n --spacing-xs: 0 kills the gap the checkbox keeps between its box and its\n (here empty) text block. -->\n <span\n inert\n aria-hidden=\"true\"\n class=\"inline-flex shrink-0\"\n style=\"--spacing-xs: 0\">\n <checkbox\n [checked]=\"item.$selected ?? false\"\n [disabled]=\"item.count === 0\"\n [class]=\"'pointer-events-none'\" />\n </span>\n }\n @if (resolvedItemTemplate(); as tpl) {\n <ng-container\n [ngTemplateOutlet]=\"tpl\"\n [ngTemplateOutletContext]=\"{\n $implicit: item,\n field: field,\n selected: item.$selected ?? false,\n count: item.count,\n name: (listItemName(item) | syslang) ?? '',\n searchText: searchText()\n }\" />\n } @else {\n @let icon = $any(item).icon;\n @if (icon) {\n <FaIcon\n [faClass]=\"icon\"\n class=\"shrink-0 self-center justify-self-center\" />\n }\n @let appliesOnLabelClick = quickFilter() || applyOnSelect();\n <span\n [class]=\"\n cn(\n 'line-clamp-1 cursor-pointer break-all text-ellipsis select-none',\n appliesOnLabelClick && 'hover:underline'\n )\n \"\n [title]=\"\n appliesOnLabelClick\n ? ((isListItemFiltered(item, field)\n ? 'filters.removeFilter'\n : 'filters.addFilter'\n ) | transloco) +\n ': ' +\n (listItemName(item) | syslang)\n : (listItemName(item) | syslang)\n \"\n (click)=\"onListItemTextClick(item, $event)\">\n @for (\n chunk of (listItemName(item) | syslang) ?? \"\"\n | highlightWord: searchText() : 10;\n track $index\n ) {\n <span [class]=\"{ 'font-bold': chunk.match }\" aria-hidden=\"true\">{{\n chunk.text\n }}</span>\n }\n </span>\n @if (showCount() && item.count > 0) {\n <span class=\"ml-auto px-1 text-xs empty:hidden\" aria-hidden=\"true\">{{\n item.count\n }}</span>\n }\n }\n </a>\n</ng-template>\n"
|
|
26095
26256
|
}]
|
|
26096
26257
|
}],
|
|
26097
26258
|
ctorParameters: () => [],
|
|
@@ -26538,7 +26699,7 @@ var AggregationComponent = class AggregationComponent {
|
|
|
26538
26699
|
selectionChange: "selectionChange",
|
|
26539
26700
|
filtersChange: "filtersChange"
|
|
26540
26701
|
},
|
|
26541
|
-
host: { properties: { "class": "cn(\"
|
|
26702
|
+
host: { properties: { "class": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\",class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")" } },
|
|
26542
26703
|
queries: [{
|
|
26543
26704
|
propertyName: "itemDirective",
|
|
26544
26705
|
first: true,
|
|
@@ -26574,6 +26735,7 @@ var AggregationComponent = class AggregationComponent {
|
|
|
26574
26735
|
@switch (resolvedType()) {
|
|
26575
26736
|
@case ("date") {
|
|
26576
26737
|
<AggregationDate
|
|
26738
|
+
class="min-h-0 flex-1"
|
|
26577
26739
|
[id]="id()"
|
|
26578
26740
|
[name]="name()"
|
|
26579
26741
|
[column]="column()"
|
|
@@ -26595,6 +26757,7 @@ var AggregationComponent = class AggregationComponent {
|
|
|
26595
26757
|
}
|
|
26596
26758
|
@case ("tree") {
|
|
26597
26759
|
<AggregationTree
|
|
26760
|
+
class="min-h-0 w-auto flex-1"
|
|
26598
26761
|
[id]="id()"
|
|
26599
26762
|
[name]="name()"
|
|
26600
26763
|
[column]="column()"
|
|
@@ -26621,6 +26784,7 @@ var AggregationComponent = class AggregationComponent {
|
|
|
26621
26784
|
}
|
|
26622
26785
|
@default {
|
|
26623
26786
|
<AggregationList
|
|
26787
|
+
class="min-h-0 flex-1"
|
|
26624
26788
|
[id]="id()"
|
|
26625
26789
|
[name]="name()"
|
|
26626
26790
|
[column]="column()"
|
|
@@ -26720,6 +26884,7 @@ var AggregationComponent = class AggregationComponent {
|
|
|
26720
26884
|
type: AggregationDateComponent,
|
|
26721
26885
|
selector: "aggregation-date, AggregationDate, aggregationdate",
|
|
26722
26886
|
inputs: [
|
|
26887
|
+
"class",
|
|
26723
26888
|
"name",
|
|
26724
26889
|
"column",
|
|
26725
26890
|
"id",
|
|
@@ -26767,6 +26932,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26767
26932
|
@switch (resolvedType()) {
|
|
26768
26933
|
@case ("date") {
|
|
26769
26934
|
<AggregationDate
|
|
26935
|
+
class="min-h-0 flex-1"
|
|
26770
26936
|
[id]="id()"
|
|
26771
26937
|
[name]="name()"
|
|
26772
26938
|
[column]="column()"
|
|
@@ -26788,6 +26954,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26788
26954
|
}
|
|
26789
26955
|
@case ("tree") {
|
|
26790
26956
|
<AggregationTree
|
|
26957
|
+
class="min-h-0 w-auto flex-1"
|
|
26791
26958
|
[id]="id()"
|
|
26792
26959
|
[name]="name()"
|
|
26793
26960
|
[column]="column()"
|
|
@@ -26814,6 +26981,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26814
26981
|
}
|
|
26815
26982
|
@default {
|
|
26816
26983
|
<AggregationList
|
|
26984
|
+
class="min-h-0 flex-1"
|
|
26817
26985
|
[id]="id()"
|
|
26818
26986
|
[name]="name()"
|
|
26819
26987
|
[column]="column()"
|
|
@@ -26839,7 +27007,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
26839
27007
|
}
|
|
26840
27008
|
}
|
|
26841
27009
|
`,
|
|
26842
|
-
host: { "[class]": "cn(\"
|
|
27010
|
+
host: { "[class]": "cn(\"flex h-[inherit] max-h-[inherit] flex-col\",class(), (isMissing() || (hideWhenEmpty() && isEmpty())) && \"hidden\")" }
|
|
26843
27011
|
}]
|
|
26844
27012
|
}],
|
|
26845
27013
|
propDecorators: {
|
|
@@ -27699,6 +27867,10 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27699
27867
|
...ngDevMode ? { debugName: "expandedLevel" } : /* istanbul ignore next */ {},
|
|
27700
27868
|
transform: numberAttribute
|
|
27701
27869
|
});
|
|
27870
|
+
overflowed = input(false, {
|
|
27871
|
+
...ngDevMode ? { debugName: "overflowed" } : /* istanbul ignore next */ {},
|
|
27872
|
+
transform: booleanAttribute
|
|
27873
|
+
});
|
|
27702
27874
|
variant = signal("tertiary", ...ngDevMode ? [{ debugName: "variant" }] : /* istanbul ignore next */ []);
|
|
27703
27875
|
filter = signal({
|
|
27704
27876
|
name: "",
|
|
@@ -27713,6 +27885,7 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27713
27885
|
popoverId = `filter-popover-${popoverUid++}`;
|
|
27714
27886
|
popoverRef = viewChild(PopoverDirective, ...ngDevMode ? [{ debugName: "popoverRef" }] : /* istanbul ignore next */ []);
|
|
27715
27887
|
isOpen = computed(() => this.popoverRef()?.state() === "open", ...ngDevMode ? [{ debugName: "isOpen" }] : /* istanbul ignore next */ []);
|
|
27888
|
+
toolbar = inject(Toolbar, { optional: true });
|
|
27716
27889
|
nativeElement = inject(ElementRef).nativeElement;
|
|
27717
27890
|
aggregationsStore = inject(AggregationsStore);
|
|
27718
27891
|
queryParamsStore = inject(QueryParamsStore);
|
|
@@ -27825,6 +27998,13 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27825
27998
|
isSignal: true,
|
|
27826
27999
|
isRequired: false,
|
|
27827
28000
|
transformFunction: null
|
|
28001
|
+
},
|
|
28002
|
+
overflowed: {
|
|
28003
|
+
classPropertyName: "overflowed",
|
|
28004
|
+
publicName: "overflowed",
|
|
28005
|
+
isSignal: true,
|
|
28006
|
+
isRequired: false,
|
|
28007
|
+
transformFunction: null
|
|
27828
28008
|
}
|
|
27829
28009
|
},
|
|
27830
28010
|
host: {
|
|
@@ -27846,13 +28026,7 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27846
28026
|
}],
|
|
27847
28027
|
ngImport: i0,
|
|
27848
28028
|
template: `
|
|
27849
|
-
<
|
|
27850
|
-
[variant]="variant()"
|
|
27851
|
-
[attr.popovertarget]="popoverId"
|
|
27852
|
-
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
27853
|
-
[attr.data-disabled]="filter().disabled"
|
|
27854
|
-
[disabled]="filter().disabled || null"
|
|
27855
|
-
>
|
|
28029
|
+
<ng-template #label>
|
|
27856
28030
|
<!-- display the filter icon if set -->
|
|
27857
28031
|
@let icon = filter().icon;
|
|
27858
28032
|
@if (icon) {
|
|
@@ -27872,7 +28046,35 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27872
28046
|
@if (filter().count > 1) {
|
|
27873
28047
|
<Badge variant="number" fill="secondary" scheme="sage" size="xs">+{{ filter().count - 1 }}</Badge>
|
|
27874
28048
|
}
|
|
27875
|
-
</
|
|
28049
|
+
</ng-template>
|
|
28050
|
+
|
|
28051
|
+
@if (toolbar) {
|
|
28052
|
+
<!-- Inside a toolbar the BUTTON is the roving widget, not the host: the host is an element with
|
|
28053
|
+
no role of its own, so making it the widget gave two tab stops — an inert host that the
|
|
28054
|
+
roving focused, then this button with its own stop. A native disabled attribute would also make a
|
|
28055
|
+
widget unfocusable, so the toolbar's own disabled input (soft: still focusable, aria-disabled)
|
|
28056
|
+
takes over and the popover trigger is simply not wired. The same disabled state removes a filter
|
|
28057
|
+
hidden by the overflow manager (display: none) from the roving: arrow keys must not spend
|
|
28058
|
+
presses on filters nobody can see. -->
|
|
28059
|
+
<button
|
|
28060
|
+
ngToolbarWidget
|
|
28061
|
+
[variant]="variant()"
|
|
28062
|
+
[attr.popovertarget]="filter().disabled ? null : popoverId"
|
|
28063
|
+
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
28064
|
+
[attr.data-disabled]="filter().disabled"
|
|
28065
|
+
[disabled]="filter().disabled || overflowed()">
|
|
28066
|
+
<ng-container *ngTemplateOutlet="label" />
|
|
28067
|
+
</button>
|
|
28068
|
+
} @else {
|
|
28069
|
+
<button
|
|
28070
|
+
[variant]="variant()"
|
|
28071
|
+
[attr.popovertarget]="popoverId"
|
|
28072
|
+
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
28073
|
+
[attr.data-disabled]="filter().disabled"
|
|
28074
|
+
[disabled]="filter().disabled || null">
|
|
28075
|
+
<ng-container *ngTemplateOutlet="label" />
|
|
28076
|
+
</button>
|
|
28077
|
+
}
|
|
27876
28078
|
|
|
27877
28079
|
<!-- Native HTML Popover (top-layer). The popover content stays a DOM
|
|
27878
28080
|
descendant of the surrounding @angular/aria toolbar, so pointer/click
|
|
@@ -27886,8 +28088,10 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27886
28088
|
trigger-width matching so the panel sizes to its content. overflow-hidden
|
|
27887
28089
|
resets the browser's default overflow:auto on [popover] elements, which
|
|
27888
28090
|
would otherwise scroll on its own in addition to Aggregation's own
|
|
27889
|
-
|
|
27890
|
-
|
|
28091
|
+
facet list (a double scrollbar) once its fit-content height would
|
|
28092
|
+
otherwise exceed the viewport. The facet list is the only thing that
|
|
28093
|
+
scrolls: the facet itself is capped by the room the popover has (see
|
|
28094
|
+
below). -->
|
|
27891
28095
|
<div
|
|
27892
28096
|
popover
|
|
27893
28097
|
[id]="popoverId"
|
|
@@ -27905,21 +28109,23 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27905
28109
|
rings (2px outlines) of the facet header and its buttons sit flush against the wrapper's edges —
|
|
27906
28110
|
they were cut off on three sides. The 4px of padding gives them room, the matching negative
|
|
27907
28111
|
margin leaves the layout unchanged. Width and max-height grow by the 0.5rem of padding
|
|
27908
|
-
(15.5rem = the former w-60 + 0.5rem
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
1.5rem subtracted is this popover's own vertical padding (p-3), which
|
|
27916
|
-
same available space
|
|
27917
|
-
|
|
28112
|
+
(15.5rem = the former w-60 + 0.5rem) because box-sizing is border-box here: the facet keeps
|
|
28113
|
+
its 15rem and the popover its size. The wrapper no longer scrolls (and no longer clips the
|
|
28114
|
+
rings): the facet's own list is the one scrollbar. -->
|
|
28115
|
+
<!-- The facet is capped by the smaller of two heights: its comfortable size (19 lines of list +
|
|
28116
|
+
7rem of header, search box and paddings), and the room the popover really has.
|
|
28117
|
+
size() (added to injectPopover) exposes that room, in the chosen direction, as the
|
|
28118
|
+
--popover-available-height CSS var — flip()/shift() alone only reposition the popover, they
|
|
28119
|
+
never shrink it. The 1.5rem subtracted is this popover's own vertical padding (p-3), which
|
|
28120
|
+
eats into that same available space (this wrapper's p-1 / -m-1 cancel out). The facet is a
|
|
28121
|
+
flex column whose list is min-h-0 flex-1, so it is the list that shrinks and scrolls — a
|
|
28122
|
+
capped wrapper with overflow-y-auto around it would add a second, outer scrollbar. -->
|
|
28123
|
+
<div class="-m-1 w-[15.5rem] p-1">
|
|
27918
28124
|
@let name = filter().name;
|
|
27919
28125
|
@let column = filter().column;
|
|
27920
28126
|
@if (name) {
|
|
27921
28127
|
<Aggregation
|
|
27922
|
-
class="
|
|
28128
|
+
class="w-full max-h-[min(calc(19lh+7rem),calc(var(--popover-available-height,55vh)_-_1.5rem))]"
|
|
27923
28129
|
id="filter-{{ filter().column }}"
|
|
27924
28130
|
[name]="name"
|
|
27925
28131
|
[column]="column"
|
|
@@ -27938,6 +28144,23 @@ var FilterButtonComponent = class FilterButtonComponent {
|
|
|
27938
28144
|
`,
|
|
27939
28145
|
isInline: true,
|
|
27940
28146
|
dependencies: [
|
|
28147
|
+
{
|
|
28148
|
+
kind: "directive",
|
|
28149
|
+
type: NgTemplateOutlet,
|
|
28150
|
+
selector: "[ngTemplateOutlet]",
|
|
28151
|
+
inputs: [
|
|
28152
|
+
"ngTemplateOutletContext",
|
|
28153
|
+
"ngTemplateOutlet",
|
|
28154
|
+
"ngTemplateOutletInjector"
|
|
28155
|
+
]
|
|
28156
|
+
},
|
|
28157
|
+
{
|
|
28158
|
+
kind: "directive",
|
|
28159
|
+
type: ToolbarWidget,
|
|
28160
|
+
selector: "[ngToolbarWidget]",
|
|
28161
|
+
inputs: ["id", "disabled"],
|
|
28162
|
+
exportAs: ["ngToolbarWidget"]
|
|
28163
|
+
},
|
|
27941
28164
|
{
|
|
27942
28165
|
kind: "directive",
|
|
27943
28166
|
type: ButtonComponent,
|
|
@@ -28045,6 +28268,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28045
28268
|
args: [{
|
|
28046
28269
|
selector: "filter-button, FilterButton",
|
|
28047
28270
|
imports: [
|
|
28271
|
+
NgTemplateOutlet,
|
|
28272
|
+
ToolbarWidget,
|
|
28048
28273
|
ButtonComponent,
|
|
28049
28274
|
PopoverDirective,
|
|
28050
28275
|
OperatorPipe,
|
|
@@ -28055,13 +28280,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28055
28280
|
FaIconComponent
|
|
28056
28281
|
],
|
|
28057
28282
|
template: `
|
|
28058
|
-
<
|
|
28059
|
-
[variant]="variant()"
|
|
28060
|
-
[attr.popovertarget]="popoverId"
|
|
28061
|
-
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
28062
|
-
[attr.data-disabled]="filter().disabled"
|
|
28063
|
-
[disabled]="filter().disabled || null"
|
|
28064
|
-
>
|
|
28283
|
+
<ng-template #label>
|
|
28065
28284
|
<!-- display the filter icon if set -->
|
|
28066
28285
|
@let icon = filter().icon;
|
|
28067
28286
|
@if (icon) {
|
|
@@ -28081,7 +28300,35 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28081
28300
|
@if (filter().count > 1) {
|
|
28082
28301
|
<Badge variant="number" fill="secondary" scheme="sage" size="xs">+{{ filter().count - 1 }}</Badge>
|
|
28083
28302
|
}
|
|
28084
|
-
</
|
|
28303
|
+
</ng-template>
|
|
28304
|
+
|
|
28305
|
+
@if (toolbar) {
|
|
28306
|
+
<!-- Inside a toolbar the BUTTON is the roving widget, not the host: the host is an element with
|
|
28307
|
+
no role of its own, so making it the widget gave two tab stops — an inert host that the
|
|
28308
|
+
roving focused, then this button with its own stop. A native disabled attribute would also make a
|
|
28309
|
+
widget unfocusable, so the toolbar's own disabled input (soft: still focusable, aria-disabled)
|
|
28310
|
+
takes over and the popover trigger is simply not wired. The same disabled state removes a filter
|
|
28311
|
+
hidden by the overflow manager (display: none) from the roving: arrow keys must not spend
|
|
28312
|
+
presses on filters nobody can see. -->
|
|
28313
|
+
<button
|
|
28314
|
+
ngToolbarWidget
|
|
28315
|
+
[variant]="variant()"
|
|
28316
|
+
[attr.popovertarget]="filter().disabled ? null : popoverId"
|
|
28317
|
+
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
28318
|
+
[attr.data-disabled]="filter().disabled"
|
|
28319
|
+
[disabled]="filter().disabled || overflowed()">
|
|
28320
|
+
<ng-container *ngTemplateOutlet="label" />
|
|
28321
|
+
</button>
|
|
28322
|
+
} @else {
|
|
28323
|
+
<button
|
|
28324
|
+
[variant]="variant()"
|
|
28325
|
+
[attr.popovertarget]="popoverId"
|
|
28326
|
+
[class]="isOpen() ? 'border border-(--stroke-neutral-dark)/50' : ''"
|
|
28327
|
+
[attr.data-disabled]="filter().disabled"
|
|
28328
|
+
[disabled]="filter().disabled || null">
|
|
28329
|
+
<ng-container *ngTemplateOutlet="label" />
|
|
28330
|
+
</button>
|
|
28331
|
+
}
|
|
28085
28332
|
|
|
28086
28333
|
<!-- Native HTML Popover (top-layer). The popover content stays a DOM
|
|
28087
28334
|
descendant of the surrounding @angular/aria toolbar, so pointer/click
|
|
@@ -28095,8 +28342,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28095
28342
|
trigger-width matching so the panel sizes to its content. overflow-hidden
|
|
28096
28343
|
resets the browser's default overflow:auto on [popover] elements, which
|
|
28097
28344
|
would otherwise scroll on its own in addition to Aggregation's own
|
|
28098
|
-
|
|
28099
|
-
|
|
28345
|
+
facet list (a double scrollbar) once its fit-content height would
|
|
28346
|
+
otherwise exceed the viewport. The facet list is the only thing that
|
|
28347
|
+
scrolls: the facet itself is capped by the room the popover has (see
|
|
28348
|
+
below). -->
|
|
28100
28349
|
<div
|
|
28101
28350
|
popover
|
|
28102
28351
|
[id]="popoverId"
|
|
@@ -28114,21 +28363,23 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28114
28363
|
rings (2px outlines) of the facet header and its buttons sit flush against the wrapper's edges —
|
|
28115
28364
|
they were cut off on three sides. The 4px of padding gives them room, the matching negative
|
|
28116
28365
|
margin leaves the layout unchanged. Width and max-height grow by the 0.5rem of padding
|
|
28117
|
-
(15.5rem = the former w-60 + 0.5rem
|
|
28118
|
-
|
|
28119
|
-
|
|
28120
|
-
|
|
28121
|
-
|
|
28122
|
-
|
|
28123
|
-
|
|
28124
|
-
1.5rem subtracted is this popover's own vertical padding (p-3), which
|
|
28125
|
-
same available space
|
|
28126
|
-
|
|
28366
|
+
(15.5rem = the former w-60 + 0.5rem) because box-sizing is border-box here: the facet keeps
|
|
28367
|
+
its 15rem and the popover its size. The wrapper no longer scrolls (and no longer clips the
|
|
28368
|
+
rings): the facet's own list is the one scrollbar. -->
|
|
28369
|
+
<!-- The facet is capped by the smaller of two heights: its comfortable size (19 lines of list +
|
|
28370
|
+
7rem of header, search box and paddings), and the room the popover really has.
|
|
28371
|
+
size() (added to injectPopover) exposes that room, in the chosen direction, as the
|
|
28372
|
+
--popover-available-height CSS var — flip()/shift() alone only reposition the popover, they
|
|
28373
|
+
never shrink it. The 1.5rem subtracted is this popover's own vertical padding (p-3), which
|
|
28374
|
+
eats into that same available space (this wrapper's p-1 / -m-1 cancel out). The facet is a
|
|
28375
|
+
flex column whose list is min-h-0 flex-1, so it is the list that shrinks and scrolls — a
|
|
28376
|
+
capped wrapper with overflow-y-auto around it would add a second, outer scrollbar. -->
|
|
28377
|
+
<div class="-m-1 w-[15.5rem] p-1">
|
|
28127
28378
|
@let name = filter().name;
|
|
28128
28379
|
@let column = filter().column;
|
|
28129
28380
|
@if (name) {
|
|
28130
28381
|
<Aggregation
|
|
28131
|
-
class="
|
|
28382
|
+
class="w-full max-h-[min(calc(19lh+7rem),calc(var(--popover-available-height,55vh)_-_1.5rem))]"
|
|
28132
28383
|
id="filter-{{ filter().column }}"
|
|
28133
28384
|
[name]="name"
|
|
28134
28385
|
[column]="column"
|
|
@@ -28205,6 +28456,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28205
28456
|
required: false
|
|
28206
28457
|
}]
|
|
28207
28458
|
}],
|
|
28459
|
+
overflowed: [{
|
|
28460
|
+
type: i0.Input,
|
|
28461
|
+
args: [{
|
|
28462
|
+
isSignal: true,
|
|
28463
|
+
alias: "overflowed",
|
|
28464
|
+
required: false
|
|
28465
|
+
}]
|
|
28466
|
+
}],
|
|
28208
28467
|
popoverRef: [{
|
|
28209
28468
|
type: i0.ViewChild,
|
|
28210
28469
|
args: [i0.forwardRef(() => PopoverDirective), { isSignal: true }]
|
|
@@ -28328,7 +28587,7 @@ var MoreComponent = class MoreComponent {
|
|
|
28328
28587
|
<div role="group" [attr.aria-label]="'filters.moreFilters' | transloco" class="divide-y divide-(--font-neutral-muted)/18 pt-1 pb-1">
|
|
28329
28588
|
@for (filter of filteredFilters(); track filter.name) {
|
|
28330
28589
|
<Aggregation
|
|
28331
|
-
class="w-full px-1 [
|
|
28590
|
+
class="w-full px-1 max-h-[calc(15lh+7rem)]"
|
|
28332
28591
|
id="more-filters"
|
|
28333
28592
|
[attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
|
|
28334
28593
|
showFiltersCount
|
|
@@ -28431,7 +28690,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28431
28690
|
<div role="group" [attr.aria-label]="'filters.moreFilters' | transloco" class="divide-y divide-(--font-neutral-muted)/18 pt-1 pb-1">
|
|
28432
28691
|
@for (filter of filteredFilters(); track filter.name) {
|
|
28433
28692
|
<Aggregation
|
|
28434
|
-
class="w-full px-1 [
|
|
28693
|
+
class="w-full px-1 max-h-[calc(15lh+7rem)]"
|
|
28435
28694
|
id="more-filters"
|
|
28436
28695
|
[attr.title]="'filters.openFilter' | transloco: { filter: filter.name }"
|
|
28437
28696
|
showFiltersCount
|
|
@@ -28519,6 +28778,15 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28519
28778
|
nativeElement = inject(ElementRef).nativeElement;
|
|
28520
28779
|
breakpointObserver = inject(BreakpointObserverService);
|
|
28521
28780
|
aggregationsService = inject(AggregationsService);
|
|
28781
|
+
toolbar = inject(Toolbar, { optional: true });
|
|
28782
|
+
disabled = input(false, {
|
|
28783
|
+
...ngDevMode ? { debugName: "disabled" } : /* istanbul ignore next */ {},
|
|
28784
|
+
transform: booleanAttribute
|
|
28785
|
+
});
|
|
28786
|
+
triggerClass = "gap-1 truncate font-semibold";
|
|
28787
|
+
openTriggerClass = "gap-1 truncate font-semibold border border-(--stroke-neutral-dark)/18";
|
|
28788
|
+
openSheet = () => this.sheetRef()?.open();
|
|
28789
|
+
openDialog = () => this.dialogRef()?.showModal();
|
|
28522
28790
|
count = input(2, {
|
|
28523
28791
|
...ngDevMode ? { debugName: "count" } : /* istanbul ignore next */ {},
|
|
28524
28792
|
transform: numberAttribute
|
|
@@ -28572,6 +28840,13 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28572
28840
|
isStandalone: true,
|
|
28573
28841
|
selector: "more-button, MoreButton",
|
|
28574
28842
|
inputs: {
|
|
28843
|
+
disabled: {
|
|
28844
|
+
classPropertyName: "disabled",
|
|
28845
|
+
publicName: "disabled",
|
|
28846
|
+
isSignal: true,
|
|
28847
|
+
isRequired: false,
|
|
28848
|
+
transformFunction: null
|
|
28849
|
+
},
|
|
28575
28850
|
count: {
|
|
28576
28851
|
classPropertyName: "count",
|
|
28577
28852
|
publicName: "count",
|
|
@@ -28673,10 +28948,24 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28673
28948
|
}
|
|
28674
28949
|
</ng-template>
|
|
28675
28950
|
|
|
28951
|
+
<!-- The trigger button, shared by the three variants below. Inside a toolbar the BUTTON is the
|
|
28952
|
+
roving widget and not the host: the host is an element with no role of its own and no key
|
|
28953
|
+
handler, so an arrow key landed on a dead element, then the button was a second tab stop.
|
|
28954
|
+
Outside a toolbar it is a plain button. -->
|
|
28955
|
+
<ng-template #button let-onClick="onClick" let-popoverTarget="popoverTarget" let-cls="cls">
|
|
28956
|
+
@if (toolbar) {
|
|
28957
|
+
<button ngToolbarWidget variant="tertiary" [attr.popovertarget]="popoverTarget" [class]="cls" aria-label="more filters" [disabled]="disabled()" (click)="onClick?.()">
|
|
28958
|
+
<ng-container *ngTemplateOutlet="trigger" />
|
|
28959
|
+
</button>
|
|
28960
|
+
} @else {
|
|
28961
|
+
<button variant="tertiary" [attr.popovertarget]="popoverTarget" [class]="cls" aria-label="more filters" [disabled]="disabled() || null" (click)="onClick?.()">
|
|
28962
|
+
<ng-container *ngTemplateOutlet="trigger" />
|
|
28963
|
+
</button>
|
|
28964
|
+
}
|
|
28965
|
+
</ng-template>
|
|
28966
|
+
|
|
28676
28967
|
@if (breakpointObserver.isMobile()) {
|
|
28677
|
-
<
|
|
28678
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
28679
|
-
</button>
|
|
28968
|
+
<ng-container *ngTemplateOutlet="button; context: { onClick: openSheet, cls: triggerClass }" />
|
|
28680
28969
|
|
|
28681
28970
|
<Sheet side="bottom" size="lg">
|
|
28682
28971
|
<SheetHeader [closeLabel]="'filters.close' | transloco">
|
|
@@ -28696,9 +28985,7 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28696
28985
|
</SheetBody>
|
|
28697
28986
|
</Sheet>
|
|
28698
28987
|
} @else if (isEnriched()) {
|
|
28699
|
-
<
|
|
28700
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
28701
|
-
</button>
|
|
28988
|
+
<ng-container *ngTemplateOutlet="button; context: { onClick: openDialog, cls: triggerClass }" />
|
|
28702
28989
|
|
|
28703
28990
|
<dialog>
|
|
28704
28991
|
<DialogContent size="md">
|
|
@@ -28718,13 +29005,7 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28718
29005
|
</DialogContent>
|
|
28719
29006
|
</dialog>
|
|
28720
29007
|
} @else {
|
|
28721
|
-
<button
|
|
28722
|
-
variant="tertiary"
|
|
28723
|
-
[attr.popovertarget]="popoverId"
|
|
28724
|
-
[class]="isOpen() ? 'gap-1 truncate font-semibold border border-(--stroke-neutral-dark)/18' : 'gap-1 truncate font-semibold'"
|
|
28725
|
-
aria-label="more filters">
|
|
28726
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
28727
|
-
</button>
|
|
29008
|
+
<ng-container *ngTemplateOutlet="button; context: { popoverTarget: popoverId, cls: isOpen() ? openTriggerClass : triggerClass }" />
|
|
28728
29009
|
|
|
28729
29010
|
<!-- Native HTML Popover (top-layer). Fully isolate pointer/click events from
|
|
28730
29011
|
the surrounding @angular/aria toolbar (see FilterButton for the rationale:
|
|
@@ -28774,6 +29055,13 @@ var MoreButtonComponent = class MoreButtonComponent {
|
|
|
28774
29055
|
"ngTemplateOutletInjector"
|
|
28775
29056
|
]
|
|
28776
29057
|
},
|
|
29058
|
+
{
|
|
29059
|
+
kind: "directive",
|
|
29060
|
+
type: ToolbarWidget,
|
|
29061
|
+
selector: "[ngToolbarWidget]",
|
|
29062
|
+
inputs: ["id", "disabled"],
|
|
29063
|
+
exportAs: ["ngToolbarWidget"]
|
|
29064
|
+
},
|
|
28777
29065
|
{
|
|
28778
29066
|
kind: "directive",
|
|
28779
29067
|
type: ButtonComponent,
|
|
@@ -28919,6 +29207,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28919
29207
|
selector: "more-button, MoreButton",
|
|
28920
29208
|
imports: [
|
|
28921
29209
|
NgTemplateOutlet,
|
|
29210
|
+
ToolbarWidget,
|
|
28922
29211
|
ButtonComponent,
|
|
28923
29212
|
PopoverDirective,
|
|
28924
29213
|
TranslocoPipe,
|
|
@@ -28946,10 +29235,24 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28946
29235
|
}
|
|
28947
29236
|
</ng-template>
|
|
28948
29237
|
|
|
29238
|
+
<!-- The trigger button, shared by the three variants below. Inside a toolbar the BUTTON is the
|
|
29239
|
+
roving widget and not the host: the host is an element with no role of its own and no key
|
|
29240
|
+
handler, so an arrow key landed on a dead element, then the button was a second tab stop.
|
|
29241
|
+
Outside a toolbar it is a plain button. -->
|
|
29242
|
+
<ng-template #button let-onClick="onClick" let-popoverTarget="popoverTarget" let-cls="cls">
|
|
29243
|
+
@if (toolbar) {
|
|
29244
|
+
<button ngToolbarWidget variant="tertiary" [attr.popovertarget]="popoverTarget" [class]="cls" aria-label="more filters" [disabled]="disabled()" (click)="onClick?.()">
|
|
29245
|
+
<ng-container *ngTemplateOutlet="trigger" />
|
|
29246
|
+
</button>
|
|
29247
|
+
} @else {
|
|
29248
|
+
<button variant="tertiary" [attr.popovertarget]="popoverTarget" [class]="cls" aria-label="more filters" [disabled]="disabled() || null" (click)="onClick?.()">
|
|
29249
|
+
<ng-container *ngTemplateOutlet="trigger" />
|
|
29250
|
+
</button>
|
|
29251
|
+
}
|
|
29252
|
+
</ng-template>
|
|
29253
|
+
|
|
28949
29254
|
@if (breakpointObserver.isMobile()) {
|
|
28950
|
-
<
|
|
28951
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
28952
|
-
</button>
|
|
29255
|
+
<ng-container *ngTemplateOutlet="button; context: { onClick: openSheet, cls: triggerClass }" />
|
|
28953
29256
|
|
|
28954
29257
|
<Sheet side="bottom" size="lg">
|
|
28955
29258
|
<SheetHeader [closeLabel]="'filters.close' | transloco">
|
|
@@ -28969,9 +29272,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28969
29272
|
</SheetBody>
|
|
28970
29273
|
</Sheet>
|
|
28971
29274
|
} @else if (isEnriched()) {
|
|
28972
|
-
<
|
|
28973
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
28974
|
-
</button>
|
|
29275
|
+
<ng-container *ngTemplateOutlet="button; context: { onClick: openDialog, cls: triggerClass }" />
|
|
28975
29276
|
|
|
28976
29277
|
<dialog>
|
|
28977
29278
|
<DialogContent size="md">
|
|
@@ -28991,13 +29292,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
28991
29292
|
</DialogContent>
|
|
28992
29293
|
</dialog>
|
|
28993
29294
|
} @else {
|
|
28994
|
-
<button
|
|
28995
|
-
variant="tertiary"
|
|
28996
|
-
[attr.popovertarget]="popoverId"
|
|
28997
|
-
[class]="isOpen() ? 'gap-1 truncate font-semibold border border-(--stroke-neutral-dark)/18' : 'gap-1 truncate font-semibold'"
|
|
28998
|
-
aria-label="more filters">
|
|
28999
|
-
<ng-container *ngTemplateOutlet="trigger" />
|
|
29000
|
-
</button>
|
|
29295
|
+
<ng-container *ngTemplateOutlet="button; context: { popoverTarget: popoverId, cls: isOpen() ? openTriggerClass : triggerClass }" />
|
|
29001
29296
|
|
|
29002
29297
|
<!-- Native HTML Popover (top-layer). Fully isolate pointer/click events from
|
|
29003
29298
|
the surrounding @angular/aria toolbar (see FilterButton for the rationale:
|
|
@@ -29039,6 +29334,14 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
29039
29334
|
}]
|
|
29040
29335
|
}],
|
|
29041
29336
|
propDecorators: {
|
|
29337
|
+
disabled: [{
|
|
29338
|
+
type: i0.Input,
|
|
29339
|
+
args: [{
|
|
29340
|
+
isSignal: true,
|
|
29341
|
+
alias: "disabled",
|
|
29342
|
+
required: false
|
|
29343
|
+
}]
|
|
29344
|
+
}],
|
|
29042
29345
|
count: [{
|
|
29043
29346
|
type: i0.Input,
|
|
29044
29347
|
args: [{
|
|
@@ -29353,7 +29656,7 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
29353
29656
|
tabindex with it: the keyboard stopped on an invisible strip of zero height, announced
|
|
29354
29657
|
"Filter controls" and disabled at once. -->
|
|
29355
29658
|
@if (hasFilters() || currentBasket() || hasAggregations()) {
|
|
29356
|
-
<div ngToolbar [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
|
|
29659
|
+
<div ngToolbar [softDisabled]="false" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
|
|
29357
29660
|
@if (hasFilters()) {
|
|
29358
29661
|
<button
|
|
29359
29662
|
ngToolbarWidget
|
|
@@ -29391,10 +29694,13 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
29391
29694
|
: 'flex-row'
|
|
29392
29695
|
)">
|
|
29393
29696
|
|
|
29394
|
-
@for (filter of authorizedFilters(); track filter.name) {
|
|
29697
|
+
@for (filter of authorizedFilters(); track filter.name; let i = $index) {
|
|
29698
|
+
<!-- No ngToolbarWidget here: FilterButton makes its own inner button the roving widget, so the
|
|
29699
|
+
bar has one tab stop per filter and not two (host, then button). A filter hidden by the
|
|
29700
|
+
overflow manager is disabled, hence skipped by the roving (the toolbar is not soft-disabled). -->
|
|
29395
29701
|
<FilterButton
|
|
29396
|
-
ngToolbarWidget
|
|
29397
29702
|
overflowItem
|
|
29703
|
+
[overflowed]="i >= visibleFiltersCount()"
|
|
29398
29704
|
[name]="filter.name"
|
|
29399
29705
|
[column]="filter.column"
|
|
29400
29706
|
[position]="position()"
|
|
@@ -29410,8 +29716,8 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
29410
29716
|
forbids the attribute. The name belongs to the <button> inside MoreButton, which
|
|
29411
29717
|
already carries it. -->
|
|
29412
29718
|
<MoreButton
|
|
29413
|
-
ngToolbarWidget
|
|
29414
29719
|
overflowStop
|
|
29720
|
+
[disabled]="hasMoreFilters().length === 0"
|
|
29415
29721
|
[class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none')"
|
|
29416
29722
|
[count]="visibleFiltersCount()"
|
|
29417
29723
|
[position]="morePosition()"
|
|
@@ -29444,6 +29750,7 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
29444
29750
|
type: MoreButtonComponent,
|
|
29445
29751
|
selector: "more-button, MoreButton",
|
|
29446
29752
|
inputs: [
|
|
29753
|
+
"disabled",
|
|
29447
29754
|
"count",
|
|
29448
29755
|
"position",
|
|
29449
29756
|
"lockPosition",
|
|
@@ -29465,7 +29772,8 @@ var FiltersBarComponent = class FiltersBarComponent {
|
|
|
29465
29772
|
"position",
|
|
29466
29773
|
"offset",
|
|
29467
29774
|
"lockPosition",
|
|
29468
|
-
"expandedLevel"
|
|
29775
|
+
"expandedLevel",
|
|
29776
|
+
"overflowed"
|
|
29469
29777
|
]
|
|
29470
29778
|
},
|
|
29471
29779
|
{
|
|
@@ -29557,7 +29865,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
29557
29865
|
tabindex with it: the keyboard stopped on an invisible strip of zero height, announced
|
|
29558
29866
|
"Filter controls" and disabled at once. -->
|
|
29559
29867
|
@if (hasFilters() || currentBasket() || hasAggregations()) {
|
|
29560
|
-
<div ngToolbar [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
|
|
29868
|
+
<div ngToolbar [softDisabled]="false" [attr.aria-label]="ariaLabel() ?? ('filters.filterControls' | transloco)" overflowManager [direction]="direction()" [reserveStop]="hasCappedFilters()" (count)="adjustFiltersCount($event)" class="flex items-end gap-2 rounded-[inherit] bg-inherit">
|
|
29561
29869
|
@if (hasFilters()) {
|
|
29562
29870
|
<button
|
|
29563
29871
|
ngToolbarWidget
|
|
@@ -29595,10 +29903,13 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
29595
29903
|
: 'flex-row'
|
|
29596
29904
|
)">
|
|
29597
29905
|
|
|
29598
|
-
@for (filter of authorizedFilters(); track filter.name) {
|
|
29906
|
+
@for (filter of authorizedFilters(); track filter.name; let i = $index) {
|
|
29907
|
+
<!-- No ngToolbarWidget here: FilterButton makes its own inner button the roving widget, so the
|
|
29908
|
+
bar has one tab stop per filter and not two (host, then button). A filter hidden by the
|
|
29909
|
+
overflow manager is disabled, hence skipped by the roving (the toolbar is not soft-disabled). -->
|
|
29599
29910
|
<FilterButton
|
|
29600
|
-
ngToolbarWidget
|
|
29601
29911
|
overflowItem
|
|
29912
|
+
[overflowed]="i >= visibleFiltersCount()"
|
|
29602
29913
|
[name]="filter.name"
|
|
29603
29914
|
[column]="filter.column"
|
|
29604
29915
|
[position]="position()"
|
|
@@ -29614,8 +29925,8 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
29614
29925
|
forbids the attribute. The name belongs to the <button> inside MoreButton, which
|
|
29615
29926
|
already carries it. -->
|
|
29616
29927
|
<MoreButton
|
|
29617
|
-
ngToolbarWidget
|
|
29618
29928
|
overflowStop
|
|
29929
|
+
[disabled]="hasMoreFilters().length === 0"
|
|
29619
29930
|
[class]="cn('shrink-0', hasMoreFilters().length === 0 && 'invisible pointer-events-none')"
|
|
29620
29931
|
[count]="visibleFiltersCount()"
|
|
29621
29932
|
[position]="morePosition()"
|
|
@@ -31365,6 +31676,36 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31365
31676
|
}]
|
|
31366
31677
|
}] }
|
|
31367
31678
|
});
|
|
31679
|
+
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31680
|
+
const LEAF_SEPARATOR = " › ";
|
|
31681
|
+
function flattenLeaves(source, prefix = []) {
|
|
31682
|
+
return Object.entries(source).flatMap(([name, value]) => isPlainObject(value) ? flattenLeaves(value, [...prefix, name]) : [{
|
|
31683
|
+
path: [...prefix, name],
|
|
31684
|
+
value
|
|
31685
|
+
}]);
|
|
31686
|
+
}
|
|
31687
|
+
function valueAtPath(source, path) {
|
|
31688
|
+
return path.reduce((node, name) => isPlainObject(node) ? node[name] : void 0, source);
|
|
31689
|
+
}
|
|
31690
|
+
const toFieldText = (value) => {
|
|
31691
|
+
if (value === void 0 || value === null) return "";
|
|
31692
|
+
return Array.isArray(value) ? JSON.stringify(value) : String(value);
|
|
31693
|
+
};
|
|
31694
|
+
function normalizeCustomData(entries) {
|
|
31695
|
+
const properties = /* @__PURE__ */ new Map();
|
|
31696
|
+
const add = (key, value) => {
|
|
31697
|
+
if (!key || properties.has(key)) return;
|
|
31698
|
+
properties.set(key, value === void 0 || value === null ? { key } : {
|
|
31699
|
+
key,
|
|
31700
|
+
defaultValue: value
|
|
31701
|
+
});
|
|
31702
|
+
};
|
|
31703
|
+
for (const entry of Array.isArray(entries) ? entries : []) if (typeof entry === "string") add(entry);
|
|
31704
|
+
else if (entry && typeof entry === "object" && !Array.isArray(entry)) Object.entries(entry).forEach(([key, value]) => add(key, value));
|
|
31705
|
+
else console.warn("[user-profile] customData: entry ignored, expected a string or an object.", entry);
|
|
31706
|
+
return [...properties.values()];
|
|
31707
|
+
}
|
|
31708
|
+
const SAVED_FLASH_MS = 3e3;
|
|
31368
31709
|
const sameKeys = (a, b) => a.length === b.length && a.every((key, index) => key === b[index]);
|
|
31369
31710
|
const userProfileSchema = (editing) => schema((p) => {
|
|
31370
31711
|
applyEach(p.data, (field) => readonly(field, { when: ({ key }) => `data.${key()}` !== editing() }));
|
|
@@ -31378,9 +31719,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31378
31719
|
transloco = inject(TranslocoService);
|
|
31379
31720
|
injector = inject(Injector);
|
|
31380
31721
|
document = inject(DOCUMENT);
|
|
31722
|
+
destroyRef = inject(DestroyRef);
|
|
31381
31723
|
labelClass = FORM_LABEL_CLASS;
|
|
31382
31724
|
errorClass = FORM_ERROR_CLASS;
|
|
31725
|
+
chipClass = "inline-flex items-center rounded-(--radius-rounded) bg-(--bg-accent-base-alt) px-(--spacing-2xs) text-(length:--size-sm) font-medium text-(--font-accent-base)";
|
|
31383
31726
|
statusMessage = signal("", ...ngDevMode ? [{ debugName: "statusMessage" }] : /* istanbul ignore next */ []);
|
|
31727
|
+
savedProperty = signal(void 0, ...ngDevMode ? [{ debugName: "savedProperty" }] : /* istanbul ignore next */ []);
|
|
31728
|
+
savedTimer;
|
|
31384
31729
|
dataKeys = [
|
|
31385
31730
|
"fullName",
|
|
31386
31731
|
"mail",
|
|
@@ -31403,10 +31748,29 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31403
31748
|
equal: sameKeys
|
|
31404
31749
|
});
|
|
31405
31750
|
allowProfilePhoto = computed(() => this.keys().some((key) => key === "profilePhoto"), ...ngDevMode ? [{ debugName: "allowProfilePhoto" }] : /* istanbul ignore next */ []);
|
|
31406
|
-
|
|
31751
|
+
customDataConfig = computed(() => normalizeCustomData(this.appStore.general()?.features?.userProfile?.customData).filter(({ key }) => !this.keys().some((k) => k.toLowerCase() === key.toLowerCase())), ...ngDevMode ? [{ debugName: "customDataConfig" }] : /* istanbul ignore next */ []);
|
|
31752
|
+
customRows = computed(() => {
|
|
31753
|
+
const stored = this.user()?.customData;
|
|
31754
|
+
return this.customDataConfig().flatMap(({ key, defaultValue }) => {
|
|
31755
|
+
const shape = isPlainObject(defaultValue) ? defaultValue : isPlainObject(stored?.[key]) ? stored[key] : void 0;
|
|
31756
|
+
if (!shape) return [{
|
|
31757
|
+
key,
|
|
31758
|
+
root: key,
|
|
31759
|
+
defaultValue
|
|
31760
|
+
}];
|
|
31761
|
+
return flattenLeaves(shape).map(({ path, value }) => ({
|
|
31762
|
+
key: [key, ...path].join(LEAF_SEPARATOR),
|
|
31763
|
+
root: key,
|
|
31764
|
+
path: [key, ...path],
|
|
31765
|
+
defaultValue: isPlainObject(defaultValue) ? value : void 0
|
|
31766
|
+
}));
|
|
31767
|
+
});
|
|
31768
|
+
}, ...ngDevMode ? [{ debugName: "customRows" }] : /* istanbul ignore next */ []);
|
|
31769
|
+
customData = computed(() => this.customRows().map(({ key }) => key), {
|
|
31407
31770
|
...ngDevMode ? { debugName: "customData" } : /* istanbul ignore next */ {},
|
|
31408
31771
|
equal: sameKeys
|
|
31409
31772
|
});
|
|
31773
|
+
customRowIndex = computed(() => new Map(this.customRows().map((row) => [row.key, row])), ...ngDevMode ? [{ debugName: "customRowIndex" }] : /* istanbul ignore next */ []);
|
|
31410
31774
|
properties = computed(() => [...this.formKeys().map((key) => ({
|
|
31411
31775
|
category: "data",
|
|
31412
31776
|
key,
|
|
@@ -31416,6 +31780,13 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31416
31780
|
key,
|
|
31417
31781
|
translated: false
|
|
31418
31782
|
}))], ...ngDevMode ? [{ debugName: "properties" }] : /* istanbul ignore next */ []);
|
|
31783
|
+
sections = computed(() => {
|
|
31784
|
+
const rows = this.properties();
|
|
31785
|
+
return ["data", "customData"].map((category) => ({
|
|
31786
|
+
id: category === "data" ? "information" : "preferences",
|
|
31787
|
+
rows: rows.filter((row) => row.category === category)
|
|
31788
|
+
})).filter((section) => section.rows.length > 0);
|
|
31789
|
+
}, ...ngDevMode ? [{ debugName: "sections" }] : /* istanbul ignore next */ []);
|
|
31419
31790
|
initials = computed(() => this.principalStore.initials(), ...ngDevMode ? [{ debugName: "initials" }] : /* istanbul ignore next */ []);
|
|
31420
31791
|
username = computed(() => this.principalStore.name() || this.principalStore.email() || null, ...ngDevMode ? [{ debugName: "username" }] : /* istanbul ignore next */ []);
|
|
31421
31792
|
propertyToEdit = signal(void 0, ...ngDevMode ? [{ debugName: "propertyToEdit" }] : /* istanbul ignore next */ []);
|
|
@@ -31441,7 +31812,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31441
31812
|
const profile = this.user();
|
|
31442
31813
|
return {
|
|
31443
31814
|
data: toRecord(this.formKeys(), profile?.data),
|
|
31444
|
-
customData:
|
|
31815
|
+
customData: Object.fromEntries(this.customData().map((key) => [key, toFieldText(this.effectiveValue("customData", key))]))
|
|
31445
31816
|
};
|
|
31446
31817
|
}, ...ngDevMode ? [{ debugName: "profileModel" }] : /* istanbul ignore next */ []);
|
|
31447
31818
|
profileForm = form(this.profileModel, userProfileSchema(this.propertyToEdit), {
|
|
@@ -31452,6 +31823,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31452
31823
|
effect(() => {
|
|
31453
31824
|
if (this.userProfileResource.error()?.status === 404) this.createUserProfile();
|
|
31454
31825
|
});
|
|
31826
|
+
this.destroyRef.onDestroy(() => clearTimeout(this.savedTimer));
|
|
31455
31827
|
}
|
|
31456
31828
|
fieldId(category, key) {
|
|
31457
31829
|
return `user-profile-${category}-${key.replace(/[^\w-]+/g, "-")}`;
|
|
@@ -31480,6 +31852,33 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31480
31852
|
getDataValue(category, key) {
|
|
31481
31853
|
return this.user()?.[category]?.[key];
|
|
31482
31854
|
}
|
|
31855
|
+
defaultValue(category, key) {
|
|
31856
|
+
return category === "customData" ? this.customRowIndex().get(key)?.defaultValue : void 0;
|
|
31857
|
+
}
|
|
31858
|
+
isLeaf(category, key) {
|
|
31859
|
+
return category === "customData" && this.customRowIndex().get(key)?.path !== void 0;
|
|
31860
|
+
}
|
|
31861
|
+
storedValue(category, key) {
|
|
31862
|
+
const bucket = this.user()?.[category];
|
|
31863
|
+
const path = category === "customData" ? this.customRowIndex().get(key)?.path : void 0;
|
|
31864
|
+
return path ? valueAtPath(bucket, path) : bucket?.[key];
|
|
31865
|
+
}
|
|
31866
|
+
isBoolean(category, key) {
|
|
31867
|
+
return !this.isLeaf(category, key) && typeof this.effectiveValue(category, key) === "boolean";
|
|
31868
|
+
}
|
|
31869
|
+
booleanValue(category, key) {
|
|
31870
|
+
return this.effectiveValue(category, key) === true;
|
|
31871
|
+
}
|
|
31872
|
+
isReadOnlyValue(category, key) {
|
|
31873
|
+
return Array.isArray(this.effectiveValue(category, key));
|
|
31874
|
+
}
|
|
31875
|
+
arrayTags(category, key) {
|
|
31876
|
+
const value = this.effectiveValue(category, key);
|
|
31877
|
+
return [...new Set((Array.isArray(value) ? value : []).map((item) => typeof item === "string" ? item : JSON.stringify(item)))];
|
|
31878
|
+
}
|
|
31879
|
+
effectiveValue(category, key) {
|
|
31880
|
+
return this.storedValue(category, key) ?? this.defaultValue(category, key);
|
|
31881
|
+
}
|
|
31483
31882
|
onEdit(category, key) {
|
|
31484
31883
|
this.propertyToEdit.set(`${category}.${key}`);
|
|
31485
31884
|
this.fieldFor(category, key)?.().focusBoundControl();
|
|
@@ -31489,19 +31888,43 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31489
31888
|
const field = event.target;
|
|
31490
31889
|
field.setSelectionRange(field.value.length, field.value.length);
|
|
31491
31890
|
}
|
|
31891
|
+
onEscape(event, category, key) {
|
|
31892
|
+
if (!this.isEditing(category, key)) return;
|
|
31893
|
+
event.preventDefault();
|
|
31894
|
+
event.stopPropagation();
|
|
31895
|
+
this.onCancelEdit(category, key);
|
|
31896
|
+
}
|
|
31492
31897
|
onCancelEdit(category, key) {
|
|
31493
31898
|
if (!this.isEditing(category, key)) return;
|
|
31494
31899
|
const state = this.fieldFor(category, key)?.();
|
|
31495
|
-
state?.value.set(this.
|
|
31900
|
+
state?.value.set(toFieldText(this.effectiveValue(category, key)));
|
|
31496
31901
|
state?.reset();
|
|
31497
31902
|
this.leaveEditMode(category, key);
|
|
31498
31903
|
this.announce("editCancelled", category, key);
|
|
31499
31904
|
}
|
|
31500
|
-
|
|
31501
|
-
|
|
31905
|
+
labelFor(category, key) {
|
|
31906
|
+
if (category === "data") return this.transloco.translate(`userProfile.data.${key}`);
|
|
31907
|
+
const leafPath = this.customRowIndex().get(key)?.path;
|
|
31908
|
+
if (leafPath) return [this.labelFor(category, leafPath[0]), ...leafPath.slice(1)].join(LEAF_SEPARATOR);
|
|
31909
|
+
const path = `userProfile.customData.${key}`;
|
|
31910
|
+
const translated = this.transloco.translate(path);
|
|
31911
|
+
return translated && translated !== path ? translated : key;
|
|
31912
|
+
}
|
|
31913
|
+
isDefault(category, key) {
|
|
31914
|
+
const stored = this.storedValue(category, key);
|
|
31915
|
+
return (stored === void 0 || stored === null) && this.defaultValue(category, key) !== void 0;
|
|
31916
|
+
}
|
|
31917
|
+
isSaved(category, key) {
|
|
31918
|
+
return this.savedProperty() === `${category}.${key}`;
|
|
31919
|
+
}
|
|
31920
|
+
flashSaved(category, key) {
|
|
31921
|
+
this.savedProperty.set(`${category}.${key}`);
|
|
31922
|
+
clearTimeout(this.savedTimer);
|
|
31923
|
+
this.savedTimer = setTimeout(() => this.savedProperty.set(void 0), SAVED_FLASH_MS);
|
|
31502
31924
|
}
|
|
31503
31925
|
announce(kind, category, key) {
|
|
31504
|
-
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.
|
|
31926
|
+
this.statusMessage.set(this.transloco.translate(`userProfile.status.${kind}`, { property: this.labelFor(category, key) }));
|
|
31927
|
+
if (kind === "saved") this.flashSaved(category, key);
|
|
31505
31928
|
}
|
|
31506
31929
|
leaveEditMode(category, key) {
|
|
31507
31930
|
this.propertyToEdit.set(void 0);
|
|
@@ -31535,6 +31958,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31535
31958
|
}
|
|
31536
31959
|
}
|
|
31537
31960
|
async onDeleteData(category, key) {
|
|
31961
|
+
if (!this.hasStoredValue(category, key)) return;
|
|
31538
31962
|
try {
|
|
31539
31963
|
const response = await this.client.api.userProfile.deleteProperty(this.user(), category, key);
|
|
31540
31964
|
if (!response) {
|
|
@@ -31542,14 +31966,46 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31542
31966
|
return;
|
|
31543
31967
|
}
|
|
31544
31968
|
info("Deleted user profile property", response);
|
|
31545
|
-
|
|
31546
|
-
userProfile[category][key] = "";
|
|
31547
|
-
this.userProfileResource.set(userProfile);
|
|
31548
|
-
this.announce("deleted", category, key);
|
|
31969
|
+
this.forgetProperty(category, key);
|
|
31549
31970
|
} catch (err) {
|
|
31971
|
+
if (err?.status === 404) {
|
|
31972
|
+
info("User profile property already absent", {
|
|
31973
|
+
category,
|
|
31974
|
+
key
|
|
31975
|
+
});
|
|
31976
|
+
this.forgetProperty(category, key);
|
|
31977
|
+
return;
|
|
31978
|
+
}
|
|
31550
31979
|
notify.error(err.message);
|
|
31551
31980
|
}
|
|
31552
31981
|
}
|
|
31982
|
+
hasStoredValue(category, key) {
|
|
31983
|
+
const stored = this.storedValue(category, key);
|
|
31984
|
+
return stored !== void 0 && stored !== null;
|
|
31985
|
+
}
|
|
31986
|
+
forgetProperty(category, key) {
|
|
31987
|
+
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
31988
|
+
if (this.defaultValue(category, key) === void 0) userProfile[category][key] = "";
|
|
31989
|
+
else delete userProfile[category][key];
|
|
31990
|
+
this.userProfileResource.set(userProfile);
|
|
31991
|
+
this.announce("deleted", category, key);
|
|
31992
|
+
}
|
|
31993
|
+
async onToggleBoolean(category, key, checked, toggle) {
|
|
31994
|
+
const previous = this.user();
|
|
31995
|
+
if (!previous || this.booleanValue(category, key) === checked) return;
|
|
31996
|
+
this.userProfileResource.set({
|
|
31997
|
+
...previous,
|
|
31998
|
+
[category]: {
|
|
31999
|
+
...previous[category],
|
|
32000
|
+
[key]: checked
|
|
32001
|
+
}
|
|
32002
|
+
});
|
|
32003
|
+
if (await this.patchProperty(category, key, checked)) this.announce("saved", category, key);
|
|
32004
|
+
else {
|
|
32005
|
+
this.userProfileResource.set(previous);
|
|
32006
|
+
toggle.checked.set(!checked);
|
|
32007
|
+
}
|
|
32008
|
+
}
|
|
31553
32009
|
async patchProperty(category, key, value) {
|
|
31554
32010
|
const userProfile = JSON.parse(JSON.stringify(this.user()));
|
|
31555
32011
|
userProfile[category] = {
|
|
@@ -31603,7 +32059,7 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31603
32059
|
host: { classAttribute: "flex flex-col h-full overflow-hidden" },
|
|
31604
32060
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
31605
32061
|
ngImport: i0,
|
|
31606
|
-
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n",
|
|
32062
|
+
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n",
|
|
31607
32063
|
dependencies: [
|
|
31608
32064
|
{
|
|
31609
32065
|
kind: "directive",
|
|
@@ -31696,24 +32152,75 @@ var UserProfileFormComponent = class UserProfileFormComponent {
|
|
|
31696
32152
|
"size"
|
|
31697
32153
|
]
|
|
31698
32154
|
},
|
|
32155
|
+
{
|
|
32156
|
+
kind: "component",
|
|
32157
|
+
type: CircleCheckIcon,
|
|
32158
|
+
selector: "circle-check, circle-check-icon, CircleCheck, CircleCheckIcon",
|
|
32159
|
+
inputs: ["class"]
|
|
32160
|
+
},
|
|
32161
|
+
{
|
|
32162
|
+
kind: "component",
|
|
32163
|
+
type: LockIcon,
|
|
32164
|
+
selector: "lock, lock-icon, Lock, LockIcon",
|
|
32165
|
+
inputs: ["class"]
|
|
32166
|
+
},
|
|
32167
|
+
{
|
|
32168
|
+
kind: "component",
|
|
32169
|
+
type: SwitchComponent,
|
|
32170
|
+
selector: "switch, Switch",
|
|
32171
|
+
inputs: [
|
|
32172
|
+
"checked",
|
|
32173
|
+
"disabled",
|
|
32174
|
+
"size",
|
|
32175
|
+
"class",
|
|
32176
|
+
"aria-label",
|
|
32177
|
+
"aria-labelledby",
|
|
32178
|
+
"presentational",
|
|
32179
|
+
"compact",
|
|
32180
|
+
"tabIndex"
|
|
32181
|
+
],
|
|
32182
|
+
outputs: ["checkedChange"]
|
|
32183
|
+
},
|
|
32184
|
+
{
|
|
32185
|
+
kind: "component",
|
|
32186
|
+
type: TagInputComponent,
|
|
32187
|
+
selector: "TagInput, tag-input, taginput",
|
|
32188
|
+
inputs: [
|
|
32189
|
+
"value",
|
|
32190
|
+
"disabled",
|
|
32191
|
+
"readonly",
|
|
32192
|
+
"placeholder",
|
|
32193
|
+
"separator",
|
|
32194
|
+
"addOnBlur",
|
|
32195
|
+
"size",
|
|
32196
|
+
"scheme",
|
|
32197
|
+
"class",
|
|
32198
|
+
"aria-label",
|
|
32199
|
+
"addLabel",
|
|
32200
|
+
"removeLabel",
|
|
32201
|
+
"addedLabel",
|
|
32202
|
+
"removedLabel"
|
|
32203
|
+
],
|
|
32204
|
+
outputs: ["valueChange", "touch"]
|
|
32205
|
+
},
|
|
31699
32206
|
{
|
|
31700
32207
|
kind: "directive",
|
|
31701
|
-
type:
|
|
31702
|
-
selector: "
|
|
32208
|
+
type: InputComponent,
|
|
32209
|
+
selector: "input-group, inputgroup, InputGroup",
|
|
31703
32210
|
inputs: [
|
|
31704
32211
|
"class",
|
|
31705
32212
|
"size",
|
|
31706
32213
|
"scheme",
|
|
31707
|
-
"resize",
|
|
31708
32214
|
"filled",
|
|
31709
|
-
"
|
|
31710
|
-
"
|
|
32215
|
+
"numbers",
|
|
32216
|
+
"autoHeight",
|
|
32217
|
+
"textSize"
|
|
31711
32218
|
]
|
|
31712
32219
|
},
|
|
31713
32220
|
{
|
|
31714
32221
|
kind: "directive",
|
|
31715
|
-
type:
|
|
31716
|
-
selector: "
|
|
32222
|
+
type: InputControlDirective,
|
|
32223
|
+
selector: "input[input-control], input[inputControl]",
|
|
31717
32224
|
inputs: ["class"]
|
|
31718
32225
|
},
|
|
31719
32226
|
{
|
|
@@ -31765,18 +32272,22 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
31765
32272
|
UserIcon,
|
|
31766
32273
|
AvatarImageComponent,
|
|
31767
32274
|
ButtonComponent,
|
|
31768
|
-
|
|
31769
|
-
|
|
32275
|
+
CircleCheckIcon,
|
|
32276
|
+
LockIcon,
|
|
32277
|
+
SwitchComponent,
|
|
32278
|
+
TagInputComponent,
|
|
32279
|
+
InputComponent,
|
|
32280
|
+
InputControlDirective,
|
|
31770
32281
|
ChangePasswordComponent
|
|
31771
32282
|
],
|
|
31772
32283
|
providers: [provideTranslocoScope("user-profile", "login")],
|
|
31773
|
-
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (\n property of properties();\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label =\n property.translated\n ? (\"userProfile.data.\" + key | transloco)\n : key;\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n <!-- role=\"group\" named by the label: without it the four action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex flex-col gap-(--spacing-3xs)\">\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <div class=\"group flex flex-row\">\n <div class=\"min-w-0 grow\">\n <textarea-group\n resize=\"auto\"\n [maxRows]=\"3\"\n [scheme]=\"invalid ? 'error' : 'default'\">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <textarea\n textarea-control\n rows=\"1\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"'userProfile.notDefined' | transloco\"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onCancelEdit(category, key)\"></textarea>\n </textarea-group>\n </div>\n <!-- group-focus-within, and not only group-hover: `invisible` takes the buttons out of\n the tab order, so without it editing or deleting a property is unreachable with a\n keyboard. Focusing the field of the row reveals its actions. -->\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"visible mx-2 cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive visible cursor-pointer sm:invisible sm:group-focus-within:visible sm:group-hover:visible\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n }\n </div>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n"
|
|
32284
|
+
template: "@if (userProfileResource.hasValue()) {\n @let profile = user();\n <div class=\"flex min-h-0 flex-1 flex-col gap-2 p-4\">\n <div class=\"mb-4 flex\">\n @if (allowProfilePhoto()) {\n @let profilePhoto = profile?.data?.profilePhoto;\n <!-- AVATAR — a real <button>: the avatar opens the file picker, so it has to be reachable and\n activatable with the keyboard, which a click handler on a <div> never is. -->\n <button\n type=\"button\"\n variant=\"none\"\n class=\"cursor-pointer rounded-full\"\n [title]=\"'userProfile.changePhoto' | transloco\"\n [attr.aria-label]=\"'userProfile.changePhoto' | transloco\"\n (click)=\"avatarInput.click()\">\n <Avatar fill=\"secondary\" class=\"size-14\">\n @if (profilePhoto) {\n <!-- alt=\"\": the button already carries the accessible name, the image would repeat it. -->\n <AvatarImage [src]=\"profilePhoto\" width=\"44\" height=\"44\" alt=\"\" />\n }\n <AvatarFallback class=\"size-full text-lg\">\n @if (initials()) {\n <span>{{ initials() }}</span>\n } @else {\n <UserIcon class=\"size-7\" />\n }\n </AvatarFallback>\n </Avatar>\n </button>\n @if (profilePhoto) {\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco\n : { property: 'userProfile.data.profilePhoto' | transloco }\n \"\n (click)=\"onDeleteData('data', 'profilePhoto')\">\n <TrashIcon />\n </button>\n }\n }\n <div class=\"grow\"></div>\n <div class=\"flex flex-col gap-4\">\n <!-- CHANGE PASSWORD -->\n @if (allowChangePassword()) {\n <button\n type=\"button\"\n [disabled]=\"changingPassword()\"\n (click)=\"changingPassword.set(true)\">\n {{ \"login.changePassword\" | transloco }}\n </button>\n }\n </div>\n </div>\n @if (changingPassword()) {\n <!-- ChangePassword renders its own <form [formRoot]>: it stays in this branch, mutually\n exclusive with the profile form below, so the two never nest. -->\n <ChangePassword\n [username]=\"username()\"\n [redirectAfterSuccess]=\"false\"\n [redirectAfterCancel]=\"false\"\n (cancel)=\"changingPassword.set(false)\"\n (success)=\"changingPassword.set(false)\" />\n } @else {\n <!-- p: the focus outline of the fields is drawn *outside* their box, and this is the\n scrolling ancestor — without a gutter it clips the ring on every edge it touches\n (left for all of them, bottom for the last one, top for the first). -->\n <div class=\"min-h-0 flex-1 overflow-auto p-(--spacing-3xs)\">\n <!--\n [formRoot] pose novalidate, intercepte submit et appelle preventDefault() avant de déclencher\n submit() sur le field tree. Un seul bouton type=\"submit\" est rendu à la fois — celui de la\n ligne en cours d'édition — et l'action de soumission persiste cette seule propriété.\n -->\n <!-- aria-label: a <form> with no accessible name is not exposed as a form landmark, so the\n fields could not be reached as a whole. aria-busy holds the announcement back while a\n property is being persisted. -->\n <form\n class=\"@container\"\n [formRoot]=\"profileForm\"\n [attr.aria-label]=\"'userProfile.profileFields' | transloco\"\n [attr.aria-busy]=\"profileForm().submitting() ? 'true' : null\">\n @for (section of sections(); track section.id) {\n <section\n [attr.aria-labelledby]=\"'user-profile-section-' + section.id\"\n class=\"mb-(--spacing-md) flex flex-col gap-(--spacing-sm) rounded-(--radius-lg) border border-solid border-(--stroke-primary-lighter) p-(--spacing-md)\">\n <div>\n <h3\n [id]=\"'user-profile-section-' + section.id\"\n class=\"m-0 text-(length:--size-base) font-semibold\">\n {{ \"userProfile.sections.\" + section.id | transloco }}\n </h3>\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{\n \"userProfile.sections.\" + section.id + \"Hint\" | transloco\n }}\n </p>\n </div>\n <div\n class=\"grid grid-cols-1 gap-x-(--spacing-md) gap-y-(--spacing-sm) @lg:grid-cols-2\">\n @for (\n property of section.rows;\n track property.category + \".\" + property.key\n ) {\n @let category = property.category;\n @let key = property.key;\n @let field = profileForm[category][key];\n @let label = labelFor(category, key);\n @let editing = isEditing(category, key);\n @let invalid = showError(field);\n @let boolean = isBoolean(category, key);\n @let readOnlyValue = isReadOnlyValue(category, key);\n @let leaf = isLeaf(category, key);\n @let saved = isSaved(category, key);\n <!-- role=\"group\" named by the label: without it the action buttons of a row are\n announced out of context, as a flat run of \"Edit …\" / \"Delete …\" with nothing tying\n them to the property they belong to. A switch, and a value shown but not edited,\n take the whole width of the grid: they are not a field to line up with another. -->\n <div\n role=\"group\"\n [attr.aria-labelledby]=\"labelId(category, key)\"\n class=\"flex min-w-0 flex-col gap-(--spacing-3xs)\"\n [class.col-span-full]=\"boolean || readOnlyValue\">\n @if (boolean) {\n <!-- A switch is not a labelable text field: the name goes through aria-labelledby. The\n value is saved as soon as it is flipped, so there is no edit mode. -->\n <div\n class=\"flex flex-row items-center gap-(--spacing-sm) rounded-(--radius-md) border border-solid border-(--stroke-primary-lighter) px-(--spacing-md) py-(--spacing-xs)\">\n <span\n [id]=\"labelId(category, key)\"\n class=\"grow\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n @if (saved) {\n <span\n class=\"flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </span>\n } @else if (isDefault(category, key)) {\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n }\n <switch\n #toggle\n [checked]=\"booleanValue(category, key)\"\n [disabled]=\"profileForm().submitting()\"\n [aria-labelledby]=\"labelId(category, key)\"\n (checkedChange)=\"\n onToggleBoolean(category, key, $event, toggle)\n \" />\n <button\n type=\"button\"\n variant=\"tertiary\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"'userProfile.deleteProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n </div>\n } @else if (readOnlyValue) {\n <!-- An array is shown, never edited: its shape is the administrator's. A galactik\n TagInput in read-only mode, which is what it is for; its inner text field is named\n after the property, as there is no <label for> to reach it. -->\n <span\n [id]=\"labelId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</span\n >\n <tag-input\n class=\"[&_tag-remove]:hidden\"\n [value]=\"arrayTags(category, key)\"\n [readonly]=\"true\"\n [aria-label]=\"label\"\n [addLabel]=\"label\" />\n } @else {\n <label\n [id]=\"labelId(category, key)\"\n [for]=\"fieldId(category, key)\"\n [class]=\"labelClass\"\n >{{ label }}</label\n >\n <!-- The actions sit inside the box: galactik styles it from the control with has-[]\n selectors, which match descendants, so the focus ring and the read-only look\n keep working with the buttons next to the input. At rest the box is tinted,\n which is what tells a field from its label; it is white while edited. -->\n <!-- scheme: galactik colours the field itself for an error and for a save that just\n went through. The tint of a read-only field must not hide the success one, hence\n the data attribute and its more specific variant. -->\n <input-group\n class=\"has-[[readonly]]:bg-(--bg-primary-lighter) data-[state=success]:has-[[readonly]]:bg-(--bg-success-base-alt)\"\n [attr.data-state]=\"saved ? 'success' : null\"\n [scheme]=\"\n invalid ? 'error' : saved ? 'success' : 'default'\n \">\n <!-- No [attr.readonly] here: [formField] writes the readonly attribute itself from\n the field state, which the schema's readonly() rule drives. The native\n readonly attribute is what exposes aria-readonly — do not duplicate it. -->\n <input\n input-control\n type=\"text\"\n [id]=\"fieldId(category, key)\"\n [formField]=\"field\"\n [attr.placeholder]=\"\n 'userProfile.notDefined' | transloco\n \"\n [attr.aria-invalid]=\"invalid ? 'true' : null\"\n [attr.aria-describedby]=\"\n invalid ? errorId(category, key) : null\n \"\n [attr.aria-keyshortcuts]=\"editing ? 'Escape' : null\"\n (focus)=\"onFieldFocus($event)\"\n (keydown.escape)=\"onEscape($event, category, key)\" />\n <!-- Always visible: hiding them until hover made them unreachable on a touch screen\n and left nothing to discover. -->\n @if (!leaf) {\n <div class=\"flex shrink-0 flex-row items-center\">\n @if (editing) {\n <button\n type=\"submit\"\n variant=\"primary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\n [disabled]=\"profileForm().submitting()\"\n [title]=\"'userProfile.saveProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.savePropertyNamed'\n | transloco: { property: label }\n \">\n <save-icon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"cursor-pointer\"\n [title]=\"'userProfile.cancelEdit' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.cancelEditNamed'\n | transloco: { property: label }\n \"\n (click)=\"onCancelEdit(category, key)\">\n <UndoIcon />\n </button>\n } @else {\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n [id]=\"editButtonId(category, key)\"\n class=\"cursor-pointer\"\n [title]=\"'userProfile.editProperty' | transloco\"\n [attr.aria-label]=\"\n 'userProfile.editPropertyNamed'\n | transloco: { property: label }\n \"\n (click)=\"onEdit(category, key)\">\n <EditIcon />\n </button>\n <button\n type=\"button\"\n variant=\"tertiary\"\n size=\"sm\"\n [iconOnly]=\"true\"\n class=\"text-destructive cursor-pointer\"\n [title]=\"\n 'userProfile.deleteProperty' | transloco\n \"\n [attr.aria-label]=\"\n 'userProfile.deletePropertyNamed'\n | transloco: { property: label }\n \"\n [disabled]=\"!hasStoredValue(category, key)\"\n (click)=\"onDeleteData(category, key)\">\n <TrashIcon />\n </button>\n }\n </div>\n }\n </input-group>\n }\n <!-- Under the field, one message at a time: the error first, then the confirmation of a\n save, then what is worth knowing about the value. -->\n @if (invalid) {\n <p\n [id]=\"errorId(category, key)\"\n role=\"alert\"\n [class]=\"errorClass\">\n {{ errorKey(field) | transloco }}\n </p>\n } @else if (!boolean) {\n @if (saved) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-success-base)\">\n <CircleCheckIcon aria-hidden=\"true\" />\n {{ \"userProfile.saved\" | transloco }}\n </p>\n } @else if (readOnlyValue || leaf) {\n <p\n class=\"m-0 flex items-center gap-(--spacing-3xs) text-(length:--size-sm) text-(--font-neutral-muted)\">\n <LockIcon />\n {{ \"userProfile.lockedByAdmin\" | transloco }}\n </p>\n } @else if (editing) {\n <p\n class=\"m-0 text-(length:--size-sm) text-(--font-neutral-muted)\">\n {{ \"userProfile.editHint\" | transloco }}\n </p>\n } @else if (isDefault(category, key)) {\n <p class=\"m-0\">\n <span [class]=\"chipClass\">{{\n \"userProfile.defaultValue\" | transloco\n }}</span>\n </p>\n }\n }\n </div>\n }\n </div>\n </section>\n }\n </form>\n <!-- Always in the DOM, and only its text changes: a live region inserted at the same time as\n its content is not reliably announced. `sr-only` — the transitions it describes are\n already visible on screen. -->\n <p class=\"sr-only\" role=\"status\" aria-live=\"polite\">\n {{ statusMessage() }}\n </p>\n </div>\n }\n </div>\n} @else if (userProfileResource.isLoading()) {\n <p role=\"status\">{{ \"userProfile.loading\" | transloco }}</p>\n} @else if (userProfileResource.error()) {\n <p role=\"alert\">{{ \"userProfile.loadError\" | transloco }}</p>\n}\n\n<!-- AVATAR UPLOAD — `hidden` keeps it out of both the tab order and the accessibility tree, which is\n intended: it is opened by the avatar button above. It also sits outside the <form>. -->\n<input\n #avatarInput\n class=\"hidden\"\n type=\"file\"\n accept=\"image/*\"\n (change)=\"uploadAvatar($event)\" />\n"
|
|
31774
32285
|
}]
|
|
31775
32286
|
}],
|
|
31776
32287
|
ctorParameters: () => []
|
|
31777
32288
|
});
|
|
31778
32289
|
function toRecord(keys, source) {
|
|
31779
|
-
return Object.fromEntries(keys.map((key) => [key,
|
|
32290
|
+
return Object.fromEntries(keys.map((key) => [key, toFieldText(source?.[key])]));
|
|
31780
32291
|
}
|
|
31781
32292
|
function splitPropertyId(id) {
|
|
31782
32293
|
const separator = id.indexOf(".");
|
|
@@ -32418,8 +32929,10 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32418
32929
|
template: `
|
|
32419
32930
|
@if (!duplicate) {
|
|
32420
32931
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32421
|
-
|
|
32422
|
-
|
|
32932
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
32933
|
+
dialog itself and the panel is a white card set into it. -->
|
|
32934
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
32935
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32423
32936
|
<button
|
|
32424
32937
|
type="button"
|
|
32425
32938
|
variant="none"
|
|
@@ -32456,7 +32969,7 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32456
32969
|
</nav>
|
|
32457
32970
|
</div>
|
|
32458
32971
|
|
|
32459
|
-
<div class="
|
|
32972
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
32460
32973
|
@switch (panel()) {
|
|
32461
32974
|
@case ("account") {
|
|
32462
32975
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -32529,6 +33042,12 @@ var SettingsDialogComponent = class SettingsDialogComponent {
|
|
|
32529
33042
|
selector: "x-mark, x-mark-icon, XMark, XMarkIcon",
|
|
32530
33043
|
inputs: ["class"]
|
|
32531
33044
|
},
|
|
33045
|
+
{
|
|
33046
|
+
kind: "directive",
|
|
33047
|
+
type: SidebarInsetDirective,
|
|
33048
|
+
selector: "[sidebarInset]",
|
|
33049
|
+
inputs: ["class"]
|
|
33050
|
+
},
|
|
32532
33051
|
{
|
|
32533
33052
|
kind: "component",
|
|
32534
33053
|
type: SettingsAccountPanelComponent,
|
|
@@ -32571,6 +33090,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32571
33090
|
PaletteIcon,
|
|
32572
33091
|
KeyboardIcon,
|
|
32573
33092
|
XMarkIcon,
|
|
33093
|
+
SidebarInsetDirective,
|
|
32574
33094
|
SettingsAccountPanelComponent,
|
|
32575
33095
|
SettingsAppearancePanelComponent,
|
|
32576
33096
|
SettingsShortcutsPanelComponent
|
|
@@ -32579,8 +33099,10 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32579
33099
|
template: `
|
|
32580
33100
|
@if (!duplicate) {
|
|
32581
33101
|
<div dialog #dialog="dialog" [closeLabel]="'settings.close' | transloco" (closed)="call.end($event)">
|
|
32582
|
-
|
|
32583
|
-
|
|
33102
|
+
<!-- The inset layout of the sidebar, in a dialog: the navigation sits on the tinted surface of the
|
|
33103
|
+
dialog itself and the panel is a white card set into it. -->
|
|
33104
|
+
<DialogContent size="xl" class="h-[min(48rem,calc(100dvh-2rem))] flex-row! items-stretch! bg-(--bg-primary-lighter)">
|
|
33105
|
+
<div class="flex w-64 shrink-0 flex-col gap-(--spacing-3xs) p-(--spacing-sm)">
|
|
32584
33106
|
<button
|
|
32585
33107
|
type="button"
|
|
32586
33108
|
variant="none"
|
|
@@ -32617,7 +33139,7 @@ i0.ɵɵngDeclareClassMetadata({
|
|
|
32617
33139
|
</nav>
|
|
32618
33140
|
</div>
|
|
32619
33141
|
|
|
32620
|
-
<div class="
|
|
33142
|
+
<div sidebarInset class="my-(--spacing-xs) mr-(--spacing-xs) overflow-y-auto rounded-(--radius-2xl) bg-(--bg-neutral-white) p-(--spacing-xl) shadow-none">
|
|
32621
33143
|
@switch (panel()) {
|
|
32622
33144
|
@case ("account") {
|
|
32623
33145
|
<settings-account-panel class="flex min-h-0 flex-1 flex-col" [focusOnOpen]="!navigated()" />
|
|
@@ -32975,10 +33497,12 @@ const SCOPE = "session-expiry";
|
|
|
32975
33497
|
const SCOPE_ALIAS = "sessionExpiry";
|
|
32976
33498
|
const FALLBACK = {
|
|
32977
33499
|
expiringTitle: "Your session is about to expire",
|
|
32978
|
-
expiringDescription: "It expires in about {{minutes}} min.
|
|
33500
|
+
expiringDescription: "It expires in about {{minutes}} min. Extend your session to keep it open.",
|
|
32979
33501
|
expiredTitle: "Your session has expired",
|
|
32980
33502
|
expiredDescription: "Sign in again to continue.",
|
|
32981
|
-
reload: "Sign in again"
|
|
33503
|
+
reload: "Sign in again",
|
|
33504
|
+
extend: "Extend session",
|
|
33505
|
+
extendedTitle: "Your session has been extended"
|
|
32982
33506
|
};
|
|
32983
33507
|
function provideSessionExpiry(options = {}) {
|
|
32984
33508
|
const warnBeforeMs = options.warnBeforeMs ?? DEFAULT_WARN_BEFORE_MS;
|
|
@@ -33016,14 +33540,36 @@ function provideSessionExpiry(options = {}) {
|
|
|
33016
33540
|
await settled;
|
|
33017
33541
|
if (!stillDue()) return;
|
|
33018
33542
|
const minutes = Math.max(1, Math.ceil((expiresAt - Date.now()) / 6e4));
|
|
33019
|
-
const [title, description] = await Promise.all([
|
|
33543
|
+
const [title, description, extendLabel] = await Promise.all([
|
|
33544
|
+
translate("expiringTitle"),
|
|
33545
|
+
translate("expiringDescription", { minutes }),
|
|
33546
|
+
translate("extend")
|
|
33547
|
+
]);
|
|
33020
33548
|
if (!stillDue()) return;
|
|
33021
33549
|
notify.warning(title, {
|
|
33022
33550
|
description,
|
|
33023
33551
|
closeButton: true,
|
|
33024
|
-
important: true
|
|
33552
|
+
important: true,
|
|
33553
|
+
action: {
|
|
33554
|
+
label: extendLabel,
|
|
33555
|
+
onClick: () => void extend()
|
|
33556
|
+
}
|
|
33025
33557
|
});
|
|
33026
33558
|
};
|
|
33559
|
+
const extend = async () => {
|
|
33560
|
+
if (options.onExtend) return options.onExtend();
|
|
33561
|
+
const before = expiresAt;
|
|
33562
|
+
try {
|
|
33563
|
+
await client.api.principal.get();
|
|
33564
|
+
} catch {
|
|
33565
|
+
return;
|
|
33566
|
+
}
|
|
33567
|
+
if (before !== null && expiresAt !== null && expiresAt > before) {
|
|
33568
|
+
notify.success(await translate("extendedTitle"), { closeButton: true });
|
|
33569
|
+
return;
|
|
33570
|
+
}
|
|
33571
|
+
onReconnect();
|
|
33572
|
+
};
|
|
33027
33573
|
const check = () => {
|
|
33028
33574
|
if (expiresAt === null || warned) return;
|
|
33029
33575
|
const delay = expiresAt - warnBeforeMs - Date.now();
|
|
@@ -33093,6 +33639,6 @@ function provideSessionExpiry(options = {}) {
|
|
|
33093
33639
|
const queryNameResolver = () => {
|
|
33094
33640
|
return inject(AppStore).getDefaultQuery()?.name || "";
|
|
33095
33641
|
};
|
|
33096
|
-
export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, ATOMIC_CLIENT, ATOMIC_SCOPED_PROVIDERS, AddToCollection, AdvancedFiltersBodyComponent, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchTabs, AggregationComponent, AggregationDateComponent, AggregationDateRange, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertForm, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsListComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, Settings, SettingsAccountPanelComponent, SettingsAppearancePanelComponent, SettingsDialogComponent, SettingsShortcutsPanelComponent, ShortcutDirective, ShowBookmarkDirective, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TagCloudComponent, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserOverrideService, UserProfileFormComponent, UserProfileService, UserSettingsPersistence, UserSettingsStore, WEIGHT_STEPS, WordCloudComponent, ZoomControlsComponent, __resetSignInMutexForTests, activateShortcutTarget, applyThemeToNativeElement, atomicClientProvider, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildHeatmap, buildQuery, buildTagCloud, candidatesFor, canonicalize, debouncedSignal, errorInterceptorFn, fieldErrorKey, fieldShowsError, filterKey, filterLeaves, filterValues, findAggregation, flagsFromWeekdays, focusWhenReady, galactikPrimitive, galactikSemantic, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, quantileScale, queryNameResolver, rebuildWithout, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
|
|
33642
|
+
export { AGGREGATIONS_NAMES, AGGREGATIONS_NAMES_PRESET_DEFAULT, APP_FEATURES, ATOMIC_CLIENT, ATOMIC_SCOPED_PROVIDERS, AddToCollection, AdvancedFiltersBodyComponent, AdvancedFiltersComponent, AdvancedSearch, AdvancedSearchTabs, AggregationComponent, AggregationDateComponent, AggregationDateRange, AggregationItemDirective, AggregationListComponent, AggregationPanelComponent, AggregationTreeComponent, AggregationsService, AggregationsStore, Alert, AlertDialog, AlertForm, AlertsComponent, AppService, AppStore, ApplicationService, ApplicationStore, AppliedFiltersComponent, ArticleEntities, ArticleEntitiesTabPanel, ArticleExtracts, ArticleExtractsTabPanel, ArticleHighlights, ArticleLabels, ArticleLabelsTabPanel, ArticleMatchLocations, ArticleMatchLocationsTabPanel, ArticleSimilarDocuments, ArticleSimilarDocumentsTabPanel, AsideFiltersComponent, AuditFeedbackType, AuditService, AuthGuard, AuthPageComponent, AutocompleteService, BOOKMARKS_CONFIG, BOOKMARKS_OPTIONS, BackdropComponent, BackdropService, BookmarkButtonComponent, BookmarksComponent, COLLECTIONS_CONFIG, COLLECTIONS_OPTIONS, COMPONENTS_FOR_DOCUMENT_TYPE, ChangePasswordComponent, ChildMarkerDirective, CollectionsComponent, CollectionsDialog, ConverterSelectComponent, DeleteCollection, DeleteCollectionDialog, DidYouMeanComponent, DocumentLocatorComponent, EditLabels, ErrorComponent, Export, ExportDialog, ExportService, FILTERS_BREAKPOINT, FILTER_DATE_ALLOW_CUSTOM_RANGE, FORM_ALERT_CLASS, FORM_BARE_CLASS, FORM_ERROR_CLASS, FORM_FIELD_CLASS, FORM_FOOTER_CLASS, FORM_LABEL_CLASS, FORM_SURFACE_CLASS, FaIconComponent, FeatureFlags, FeatureFlagsDialogComponent, Feedback, FeedbackDialogComponent, FileSizePipe, FilterButtonComponent, FiltersBarComponent, FloatingToolbarComponent, ForgotPasswordComponent, HIGHLIGHTS, HeatmapComponent, HighlightWordPipe, InfinityScrollDirective, InlineWorker, JsonMethodPluginService, KeyboardShortcutsDialog, KeyboardShortcutsDialogComponent, KeyboardShortcutsListComponent, KeyboardShortcutsService, LabelService, LabelsEditDialog, ListPanelItemDirective, LoadingComponent, MarkdownPipe, MarkdownRendererService, MetadataComponent, MissingTermsComponent, MoreButtonComponent, MoreComponent, MultiSelectLabelsComponent, MultiSelectionToolbarComponent, NON_SEARCHABLE_COLUMNS, NON_SEARCHABLE_DEFAULTS, NavbarTabsComponent, NavigationService, NoResultComponent, OpenArticleOnCtrlEnterDirective, OperatorPipe, OverflowItemDirective, OverflowManagerDirective, OverflowStopDirective, OverrideUser, OverrideUserDialogComponent, PREVIEW_ASSISTANT_COMPONENT, PREVIEW_CONFIG, PagerComponent, PreviewActionsComponent, PreviewContentComponent, PreviewHeaderComponent, PreviewNavigator, PreviewService, PrincipalService, PrincipalStore, QueryParamsStore, QueryService, RECENT_SEARCHES_CONFIG, RECENT_SEARCHES_OPTIONS, ROUTE_COMPONENTS, RecentSearchesComponent, ResetUserSettings, ResetUserSettingsDialogComponent, SAVED_SEARCHES_CONFIG, SAVED_SEARCHES_OPTIONS, STARTUP_DATA_SOURCE, SavedSearch, SavedSearchDialog, SavedSearchPopover, SavedSearchesComponent, SavedSearchesService, SearchFeedbackComponent, SearchInputFooter, SearchService, SelectArticleDirective, SelectionService, SelectionStore, Settings, SettingsAccountPanelComponent, SettingsAppearancePanelComponent, SettingsDialogComponent, SettingsShortcutsPanelComponent, ShortcutDirective, ShowBookmarkDirective, SignInComponent, SignedOutComponent, SortSelectorComponent, SourceComponent, SourceIconPipe, SponsoredResultsComponent, SyslangPipe, THEMES, TagCloudComponent, TextChunkService, ThemeProviderDirective, ThemeSelectorComponent, ThemeStore, ThemeToggleComponent, TranslocoDateImpurePipe, UserOverrideService, UserProfileFormComponent, UserProfileService, UserSettingsPersistence, UserSettingsStore, WEIGHT_STEPS, WordCloudComponent, ZoomControlsComponent, __resetSignInMutexForTests, activateShortcutTarget, applyThemeToNativeElement, atomicClientProvider, auditInterceptorFn, authInterceptorFn, bodyInterceptorFn, bootstrapApp, buildHeatmap, buildQuery, buildTagCloud, candidatesFor, canonicalize, debouncedSignal, errorInterceptorFn, fieldErrorKey, fieldShowsError, filterKey, filterLeaves, filterValues, findAggregation, flagsFromWeekdays, focusWhenReady, galactikPrimitive, galactikSemantic, getCurrentPath, getCurrentQueryName, getQueryNameFromRoute, injectAdvancedFilters, injectAtomicClient, injectRouteNavigation, injectUserSettingsPatch, isAuthRoute, isSignInInFlight, itemLabel, layoutWordCloud, legacyFilterToFilter, makeWeightScale, multiSelectionToolbarVariants, paletteColor, processCssVars, provideAtomicClient, provideAtomicNotifications, provideAtomicScope, provideDocumentLang, provideKeyboardShortcuts, provideSessionExpiry, provideStartupAggregation, quantileScale, queryNameResolver, rebuildWithout, seriesPalette, signIn, themeColorNameToCssVariable, themeColorsToCssVariables, timelinePalette, toAriaKeyshortcuts, toDisplayParts, toPercentages, toSeries, toastInterceptorFn, weekdaysFromFlags, withAggregationsFeatures, withAlertsFeatures, withAlpha, withAppFeatures, withApplicationFeatures, withAssistantFeatures, withBasketsFeatures, withBookmarkFeatures, withBootstrapApp, withExtractsFeatures, withFetch, withMultiSelectionFeatures, withQueryParamsFeatures, withRecentSearchesFeatures, withSavedSearchesFeatures, withSelectionFeatures, withThemeBodyHook, withThemes, withThemesFeatures, withUserSettingsFeatures };
|
|
33097
33643
|
|
|
33098
33644
|
//# sourceMappingURL=sinequa-atomic-angular.mjs.map
|