@smartsoft001-mobilems/creators-shell-angular 0.0.0-stage → 2.122.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.
@@ -0,0 +1,2136 @@
1
+ import { HttpClient, HttpErrorResponse } from '@angular/common/http';
2
+ import * as i0 from '@angular/core';
3
+ import { inject, Injectable, signal, DestroyRef, computed, effect, untracked, Injector, PLATFORM_ID, viewChild, afterNextRender, Directive, input, output, linkedSignal } from '@angular/core';
4
+ import { map, switchMap, of, forkJoin, catchError, throwError, shareReplay, Subject, distinctUntilChanged, combineLatest, startWith, scan, firstValueFrom } from 'rxjs';
5
+ import { yearRangeValue, SharedConfig, decodeHtmlEntities, stripHtml, serializeCatalogFilter, yearRangeLabel, offsetToPage, createCatalog, MuseumSeoService, catalogListQueryParams, FileUrlService, catalogEntryFilterParams } from '@smartsoft001-mobilems/angular';
6
+ import { SlugService, capitalize } from '@smartsoft001/utils';
7
+ import { takeUntilDestroyed, toSignal, toObservable } from '@angular/core/rxjs-interop';
8
+ import { Location, DOCUMENT, ViewportScroller, isPlatformBrowser } from '@angular/common';
9
+ import { ActivatedRoute, Router } from '@angular/router';
10
+ import { TranslateService } from '@ngx-translate/core';
11
+
12
+ // prettier-ignore
13
+ const CREATORS_ALPHABET = [
14
+ 'A', 'Ą', 'B', 'C', 'Ć', 'D', 'E', 'Ę', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
15
+ 'Ł', 'M', 'N', 'Ń', 'O', 'Ó', 'P', 'Q', 'R', 'S', 'Ś', 'T', 'U', 'V', 'W',
16
+ 'X', 'Y', 'Z', 'Ź', 'Ż', '#',
17
+ ];
18
+ /**
19
+ * `sw=` prefixes queried for the `#` bucket (the API has no "not a letter" filter).
20
+ *
21
+ * `"` and `„` are deliberately absent: the API treats them as a wildcard and
22
+ * returns every creator (all 8 pages). The foreign initials (`Č`, `Š`, `Ž`) are
23
+ * the ones present in the data today; `sw` is diacritic-sensitive, so a new one
24
+ * only needs to be appended here. Prefixes without matches answer 404 and give `[]`.
25
+ */
26
+ // prettier-ignore
27
+ const CREATORS_HASH_PREFIXES = [
28
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
29
+ '[', '(', "'", '-',
30
+ 'Č', 'Š', 'Ž',
31
+ ];
32
+ function isCreatorsLetter(value) {
33
+ return CREATORS_ALPHABET.includes(value);
34
+ }
35
+ /** Index letter of a name; anything outside the polish alphabet lands in `#`. */
36
+ function creatorsLetterOf(name) {
37
+ const [first = ''] = Array.from(name.trim());
38
+ const upper = first.toLocaleUpperCase('pl');
39
+ return isCreatorsLetter(upper) ? upper : '#';
40
+ }
41
+ const CREATORS_POLISH_LETTERS = new Set(CREATORS_ALPHABET.slice(0, -1));
42
+ /** Polish letters stay as they are; foreign diacritics fold to the base letter (é to e). */
43
+ function creatorsFoldForeign(char) {
44
+ return CREATORS_POLISH_LETTERS.has(char.toLocaleUpperCase('pl'))
45
+ ? char
46
+ : char.normalize('NFD').replace(/\p{M}/gu, '');
47
+ }
48
+ /**
49
+ * Two-character group prefix ("Ab"); a single character when the second one is
50
+ * not a letter. A foreign second letter joins its base group ("Béraud" is "Be").
51
+ */
52
+ function creatorsPrefixOf(name) {
53
+ const [first = '', second = ''] = Array.from(name.trim());
54
+ const head = first.toLocaleUpperCase('pl');
55
+ return /^\p{L}$/u.test(second)
56
+ ? head + creatorsFoldForeign(second).toLocaleLowerCase('pl')
57
+ : head;
58
+ }
59
+ const creatorsCollator = new Intl.Collator('pl', { sensitivity: 'base' });
60
+ /** Creators grouped by `creatorsPrefixOf`, groups and items in polish alphabetical order. */
61
+ function groupCreatorsByPrefix(items) {
62
+ const groups = new Map();
63
+ const nameOf = (item) => item.name ?? '';
64
+ const sorted = [...items].sort((a, b) => creatorsCollator.compare(nameOf(a), nameOf(b)));
65
+ for (const item of sorted) {
66
+ const prefix = creatorsPrefixOf(nameOf(item));
67
+ const group = groups.get(prefix);
68
+ if (group)
69
+ group.push(item);
70
+ else
71
+ groups.set(prefix, [item]);
72
+ }
73
+ return Array.from(groups, ([prefix, groupItems]) => ({
74
+ prefix,
75
+ items: groupItems,
76
+ })).sort((a, b) => creatorsCollator.compare(a.prefix, b.prefix));
77
+ }
78
+ /**
79
+ * API prefix (`GET dictionary/Creator/prefixes/{letter}` gives `AB`, `BÉ`, `A.`)
80
+ * in the form the index shows it: `Ab`.
81
+ */
82
+ function creatorsDisplayPrefix(raw) {
83
+ const [head = '', ...rest] = Array.from(raw);
84
+ return head + rest.join('').toLocaleLowerCase('pl');
85
+ }
86
+ /**
87
+ * Index group of a creator, for links back to the index: the first two
88
+ * characters as the API lists them (`Ab`, `Bé`, `A.`), or the local `#` group.
89
+ */
90
+ function creatorsIndexPrefixOf(name) {
91
+ const trimmed = name.trim();
92
+ if (creatorsLetterOf(trimmed) === '#')
93
+ return creatorsPrefixOf(trimmed);
94
+ return creatorsDisplayPrefix(Array.from(trimmed).slice(0, 2).join('').toLocaleUpperCase('pl'));
95
+ }
96
+ /**
97
+ * The prefix of `prefixes` the address names (`?prefiks=`, `#Ab`), case
98
+ * ignored. Diacritics count: the API keeps `Be` and `Bé` apart.
99
+ */
100
+ function creatorsFindPrefix(raw, prefixes) {
101
+ if (!raw)
102
+ return null;
103
+ const upper = raw.toLocaleUpperCase('pl');
104
+ return (prefixes.find((prefix) => prefix.toLocaleUpperCase('pl') === upper) ?? null);
105
+ }
106
+ /** `?litera=` query param to index letter; invalid or missing falls back to `A`. */
107
+ function parseCreatorsLetter(raw) {
108
+ const upper = (raw ?? '').toLocaleUpperCase('pl');
109
+ return isCreatorsLetter(upper) ? upper : 'A';
110
+ }
111
+ /**
112
+ * DOM id of a prefix group. Hex code points keep `[`, `'` etc. selector-safe.
113
+ * `base` is the museum's own anchor prefix (sztuki uses `artysci`).
114
+ */
115
+ function creatorsPrefixAnchor(prefix, base = 'tworcy') {
116
+ const codePoints = Array.from(prefix, (char) => (char.codePointAt(0) ?? 0).toString(16));
117
+ return [base, ...codePoints].join('-');
118
+ }
119
+ /**
120
+ * `#fragment` from the address to where the index should go: a group anchor
121
+ * (as written), a group prefix (`#Ba`), or a letter (`#B`), which switches
122
+ * `?litera=` first and then lands on that letter's first group.
123
+ */
124
+ function resolveCreatorsFragment(fragment, letter, groups) {
125
+ let raw = fragment;
126
+ try {
127
+ raw = decodeURIComponent(fragment);
128
+ }
129
+ catch {
130
+ // Malformed escape: match the fragment as written.
131
+ }
132
+ const byAnchor = groups.find((group) => group.anchor === raw);
133
+ if (byAnchor)
134
+ return { anchor: byAnchor.anchor };
135
+ const exact = creatorsFindPrefix(raw, groups.map((group) => group.prefix));
136
+ const byPrefix = groups.find((group) => group.prefix === exact) ??
137
+ groups.find((group) => creatorsCollator.compare(group.prefix, raw) === 0);
138
+ if (byPrefix)
139
+ return { anchor: byPrefix.anchor };
140
+ const upper = raw.toLocaleUpperCase('pl');
141
+ if (isCreatorsLetter(upper)) {
142
+ if (upper !== letter)
143
+ return { letter: upper };
144
+ return groups.length ? { anchor: groups[0].anchor } : null;
145
+ }
146
+ // A prefix of another letter (`#Cz` on A) or of the `#` bucket (`#1`,
147
+ // `#Ša`): switch letters first, then the prefix resolves.
148
+ const chars = Array.from(raw);
149
+ if (chars.length <= 2 && !/\s/u.test(raw)) {
150
+ const prefixLetter = creatorsLetterOf(raw);
151
+ if (prefixLetter !== letter)
152
+ return { letter: prefixLetter };
153
+ }
154
+ return null;
155
+ }
156
+
157
+ const FULL_DATE = /^\d{4}-\d{2}-\d{2}$/;
158
+ function creatorsUrlBirth(value, bound) {
159
+ const trimmed = value.trim();
160
+ return FULL_DATE.test(trimmed) ? trimmed : yearRangeValue(trimmed, bound);
161
+ }
162
+ /**
163
+ * List/search url of the Creator dictionary. An empty `q=` or `sw=` empties the list (404), so blank
164
+ * values are omitted. The birth range is the only working date filter and needs `YYYY-MM-DD`.
165
+ */
166
+ function creatorsListUrl(apiUrl, query) {
167
+ let url = `${apiUrl}dictionary/Creator/search/page/${query.page ?? 1}?maxPerPage=${query.maxPerPage ?? 24}`;
168
+ if (query.extraData !== false)
169
+ url += '&extraData=true';
170
+ const q = query.q?.trim();
171
+ const sw = query.sw?.trim();
172
+ if (q)
173
+ url += `&q=${encodeURIComponent(q)}`;
174
+ const lastNameSw = query.lastNameSw?.trim();
175
+ if (sw)
176
+ url += `&sw=${encodeURIComponent(sw)}`;
177
+ if (lastNameSw)
178
+ url += `&lastNameSw=${encodeURIComponent(lastNameSw)}`;
179
+ if (query.sort)
180
+ url += `&sort=${query.sort}`;
181
+ if (query.birthFrom?.trim()) {
182
+ url += `&filter[dateFrom][from]=${creatorsUrlBirth(query.birthFrom, 'from')}`;
183
+ }
184
+ if (query.birthTo?.trim()) {
185
+ url += `&filter[dateFrom][to]=${creatorsUrlBirth(query.birthTo, 'to')}`;
186
+ }
187
+ return url;
188
+ }
189
+ /** Works (objects) of one creator; `authors[].id` of an object is the Creator id. */
190
+ function creatorWorksUrl(apiUrl, id, page = 1, limit = 12) {
191
+ return `${apiUrl}search/Object/page/${page}?maxPerPage=${limit}&filter[creators][]=${encodeURIComponent(String(id))}`;
192
+ }
193
+ /** Two-letter groups of an index letter (lower-case, encoded), e.g. `prefixes/b`. */
194
+ function creatorsPrefixesUrl(apiUrl, letter) {
195
+ return `${apiUrl}dictionary/Creator/prefixes/${encodeURIComponent(letter.toLocaleLowerCase('pl'))}`;
196
+ }
197
+ /** The creator profile (MOB-1972). Only some API hosts have it; the others answer 404. */
198
+ function creatorProfileUrl(apiUrl, id) {
199
+ return `${apiUrl}creator/${encodeURIComponent(String(id))}`;
200
+ }
201
+ /** The Creator dictionary entry: every API host has it. */
202
+ function creatorDictionaryUrl(apiUrl, id) {
203
+ return `${apiUrl}dictionary/Creator/${encodeURIComponent(String(id))}`;
204
+ }
205
+ /**
206
+ * First page of a related list of `creator/{id}` (`/api/search/Object/page/{page}?filter[…]`) with `maxPerPage`;
207
+ * `null` for a list without a path or results (no request needed).
208
+ */
209
+ function creatorRelatedUrl(apiUrl, related, limit) {
210
+ if (!related?.path || !related.count)
211
+ return null;
212
+ const path = related.path.replace(/^\/?api\//, '').replace('{page}', '1');
213
+ const separator = path.includes('?') ? '&' : '?';
214
+ return `${apiUrl}${path}${separator}maxPerPage=${limit}`;
215
+ }
216
+
217
+ const CREATORS_ALL_PAGE_SIZE = 500;
218
+ const creatorsServiceCollator = new Intl.Collator('pl', {
219
+ sensitivity: 'base',
220
+ });
221
+ class CreatorsService {
222
+ constructor() {
223
+ this.creatorsServiceHttp = inject(HttpClient);
224
+ this.creatorsServiceConfig = inject(SharedConfig);
225
+ /** The `#` bucket, loaded once per `apiUrl` for its prefixes and its groups. */
226
+ this.creatorsServiceHashCache = null;
227
+ }
228
+ getPage(query) {
229
+ return this.creatorsServiceGetRaw(query).pipe(map(({ data }) => ({
230
+ items: this.creatorsServiceNormalise(data?.items),
231
+ total: data?.paginatorDetails?.totalItemsCount ?? 0,
232
+ pages: data?.paginatorDetails?.totalPagesCount ?? 0,
233
+ })), this.creatorsServiceOnNotFound({
234
+ items: [],
235
+ total: 0,
236
+ pages: 0,
237
+ }));
238
+ }
239
+ /** Every page at `maxPerPage=500`, in page order. */
240
+ getAll(query = {}) {
241
+ const pageQuery = (page) => ({
242
+ ...query,
243
+ page,
244
+ maxPerPage: CREATORS_ALL_PAGE_SIZE,
245
+ });
246
+ return this.creatorsServiceGetRaw(pageQuery(1)).pipe(switchMap(({ data }) => {
247
+ const first = this.creatorsServiceNormalise(data?.items);
248
+ const total = data?.paginatorDetails?.totalPagesCount ?? 1;
249
+ const rest = Array.from({ length: Math.max(total - 1, 0) }, (_, i) => this.creatorsServiceGetRaw(pageQuery(i + 2)));
250
+ if (!rest.length)
251
+ return of(first);
252
+ return forkJoin(rest).pipe(map((pages) => first.concat(...pages.map((page) => this.creatorsServiceNormalise(page.data?.items)))));
253
+ }), this.creatorsServiceOnNotFound([]));
254
+ }
255
+ /**
256
+ * Creators on one index letter. `#` has no API filter, so it merges the
257
+ * `CREATORS_HASH_PREFIXES` searches (deduped by id). The result is re-checked with
258
+ * `creatorsLetterOf`, because `sw` also matches beyond the letter. `q` and the birth
259
+ * range go to every call.
260
+ */
261
+ getByLetter(letter, query) {
262
+ const items$ = letter === '#'
263
+ ? forkJoin(CREATORS_HASH_PREFIXES.map((prefix) => this.getAll({ ...query, sw: prefix }))).pipe(map((lists) => this.creatorsServiceUniqueById(lists.flat())))
264
+ : this.getAll({ ...query, sw: letter });
265
+ return items$.pipe(map((items) => items.filter((item) => creatorsLetterOf(item.name ?? '') === letter)));
266
+ }
267
+ /**
268
+ * Two-letter groups of an index letter (MOB-1959): the API lists them (`AB`,
269
+ * `BÉ`, `A.`), the index shows them as `Ab`. The `#` bucket has no API prefixes,
270
+ * so its groups come from the loaded entries. The prefixes endpoint and
271
+ * `lastNameSw` exist on the staging API; check the museum's API before use.
272
+ */
273
+ getPrefixes(letter) {
274
+ if (letter === '#') {
275
+ return this.creatorsServiceGetHashCreators().pipe(map((items) => groupCreatorsByPrefix(items).map((group) => group.prefix)));
276
+ }
277
+ return this.creatorsServiceHttp
278
+ .get(creatorsPrefixesUrl(this.creatorsServiceConfig.apiUrl, letter))
279
+ .pipe(map(({ data }) => [
280
+ ...new Set((data?.items ?? [])
281
+ .map(creatorsDisplayPrefix)
282
+ .filter((prefix) => creatorsLetterOf(prefix) === letter)),
283
+ ]), this.creatorsServiceOnNotFound([]));
284
+ }
285
+ /**
286
+ * Creators of one index group: every page of `lastNameSw`, in polish order.
287
+ * `#` filters its loaded entries.
288
+ */
289
+ getByIndexPrefix(letter, prefix) {
290
+ if (letter === '#') {
291
+ return this.creatorsServiceGetHashCreators().pipe(map((items) => items.filter((item) => creatorsPrefixOf(item.name ?? '') === prefix)));
292
+ }
293
+ return this.getAll({ lastNameSw: prefix, sort: 'default' }).pipe(map((items) => items
294
+ .filter((item) => creatorsLetterOf(item.name ?? '') === letter)
295
+ .sort((a, b) => creatorsServiceCollator.compare(a.name ?? '', b.name ?? ''))));
296
+ }
297
+ /**
298
+ * The creator profile `creator/{id}` (MOB-1972: life years, editorial biography, portrait, lead artwork, tags and
299
+ * related lists). An API host without it answers 404, so the dictionary entry `dictionary/Creator/{id}` is read
300
+ * then. `null` when both answer 404.
301
+ */
302
+ getById(id) {
303
+ const apiUrl = this.creatorsServiceConfig.apiUrl;
304
+ return this.creatorsServiceGetOne(creatorProfileUrl(apiUrl, id)).pipe(catchError((error) => error instanceof HttpErrorResponse && error.status === 404
305
+ ? this.creatorsServiceGetOne(creatorDictionaryUrl(apiUrl, id)).pipe(this.creatorsServiceOnNotFound(null))
306
+ : throwError(() => error)));
307
+ }
308
+ /** First works of a creator (`filter[creators][]`), `limit` per page; page 1 by default. */
309
+ getWorks(id, limit, page) {
310
+ return this.creatorsServiceGetObjects(creatorWorksUrl(this.creatorsServiceConfig.apiUrl, id, page, limit));
311
+ }
312
+ /** First page of a related list of `creator/{id}` (MOB-1963); no request for an empty one. */
313
+ getRelated(related, limit) {
314
+ const url = creatorRelatedUrl(this.creatorsServiceConfig.apiUrl, related, limit);
315
+ return url
316
+ ? this.creatorsServiceGetObjects(url)
317
+ : of({ items: [], total: 0 });
318
+ }
319
+ /**
320
+ * `getByLetter('#')` shared by the prefixes and the groups. It is kept per `apiUrl`
321
+ * (another API loads it again); a failure is not kept.
322
+ */
323
+ creatorsServiceGetHashCreators() {
324
+ const apiUrl = this.creatorsServiceConfig.apiUrl;
325
+ if (this.creatorsServiceHashCache?.apiUrl !== apiUrl) {
326
+ const cache = {
327
+ apiUrl,
328
+ items$: this.getByLetter('#').pipe(catchError((error) => {
329
+ if (this.creatorsServiceHashCache === cache) {
330
+ this.creatorsServiceHashCache = null;
331
+ }
332
+ return throwError(() => error);
333
+ }), shareReplay({ bufferSize: 1, refCount: false })),
334
+ };
335
+ this.creatorsServiceHashCache = cache;
336
+ }
337
+ return this.creatorsServiceHashCache.items$;
338
+ }
339
+ creatorsServiceGetOne(url) {
340
+ return this.creatorsServiceHttp
341
+ .get(url)
342
+ .pipe(map(({ data }) => data ? this.creatorsServiceNormaliseOne(data) : null));
343
+ }
344
+ creatorsServiceGetObjects(url) {
345
+ return this.creatorsServiceHttp
346
+ .get(url)
347
+ .pipe(map(({ data }) => ({
348
+ items: data?.items ?? [],
349
+ total: data?.paginatorDetails?.totalItemsCount ?? 0,
350
+ })), this.creatorsServiceOnNotFound({
351
+ items: [],
352
+ total: 0,
353
+ }));
354
+ }
355
+ creatorsServiceGetRaw(query) {
356
+ return this.creatorsServiceHttp.get(creatorsListUrl(this.creatorsServiceConfig.apiUrl, query));
357
+ }
358
+ creatorsServiceNormalise(items) {
359
+ return (items ?? []).map((item) => this.creatorsServiceNormaliseOne(item));
360
+ }
361
+ /** First occurrence wins: the `#` prefixes can return the same creator twice. */
362
+ creatorsServiceUniqueById(items) {
363
+ const seen = new Set();
364
+ return items.filter(({ id }) => !seen.has(id) && !!seen.add(id));
365
+ }
366
+ /** The API sends `id` as a JSON number in the list and the detail; the entity keys on a string. */
367
+ creatorsServiceNormaliseOne(item) {
368
+ return { ...item, id: String(item.id) };
369
+ }
370
+ /** The API answers 404 (`NO_RESULTS_FOR_GIVEN_QUERY`, `RECORD_NOT_FOUND`) for no data. */
371
+ creatorsServiceOnNotFound(fallback) {
372
+ return catchError((error) => error instanceof HttpErrorResponse && error.status === 404
373
+ ? of(fallback)
374
+ : throwError(() => error));
375
+ }
376
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
377
+ /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsService, providedIn: 'root' }); }
378
+ }
379
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsService, decorators: [{
380
+ type: Injectable,
381
+ args: [{ providedIn: 'root' }]
382
+ }] });
383
+
384
+ /** Label of a dictionary value: an entry gives its name, text and numbers are shown as they are. */
385
+ function creatorDictLabel(value) {
386
+ if (value === null || value === undefined)
387
+ return '';
388
+ return typeof value === 'object' ? (value.name ?? '') : String(value);
389
+ }
390
+ /**
391
+ * "Kobro, Katarzyna" gives "Katarzyna Kobro". Only with exactly one comma:
392
+ * other forms (groups, pseudonyms) stay as they are.
393
+ */
394
+ function creatorDisplayName(name) {
395
+ const text = name ?? '';
396
+ const parts = text.split(',');
397
+ if (parts.length !== 2)
398
+ return text.trim();
399
+ const [surname, first] = parts.map((part) => part.trim());
400
+ return [first, surname].filter(Boolean).join(' ');
401
+ }
402
+ const CREATOR_YEAR = /\d{4}/g;
403
+ /**
404
+ * Life years only: "1898.1.26 - 1951.2.26" gives "1898–1951". Without a period
405
+ * they come from `dateFrom`/`dateTo`. A period given as a dictionary id has no dates.
406
+ */
407
+ function creatorLifeYears(item) {
408
+ const { period } = item;
409
+ const periodName = typeof period === 'string'
410
+ ? period
411
+ : typeof period === 'object' && period !== null
412
+ ? period.name
413
+ : undefined;
414
+ const source = periodName?.trim() || `${item.dateFrom ?? ''} ${item.dateTo ?? ''}`;
415
+ return source.match(CREATOR_YEAR)?.join('–');
416
+ }
417
+ /** A name from the CMS can be HTML with entities: plain text, spaces collapsed. */
418
+ function creatorPlainName(name) {
419
+ return decodeHtmlEntities(stripHtml(name ?? ''))
420
+ .replace(/\s+/g, ' ')
421
+ .trim();
422
+ }
423
+ /** Ends of blocks and line breaks: words on both sides must stay apart once the tags go. */
424
+ const CREATOR_BLOCK_BREAK = /<\/(?:p|div|li|h[1-6]|blockquote)>|<br\s*\/?>/gi;
425
+ /** Comments, MSO conditional blocks (`<!--[if gte mso 9]>...<![endif]-->`) included. */
426
+ const CREATOR_HTML_COMMENT = /<!--[\s\S]*?-->/g;
427
+ /** A biography as one line of text. `MuseumSeoService` decodes entities and truncates. */
428
+ function creatorPlainText(html) {
429
+ return stripHtml((html ?? '')
430
+ .replace(CREATOR_HTML_COMMENT, '')
431
+ .replace(CREATOR_BLOCK_BREAK, '$& '))
432
+ .replace(/\s+/g, ' ')
433
+ .trim();
434
+ }
435
+ /**
436
+ * Roles to show: `roleName`, else the name of a `role` object or a textual
437
+ * `role` string, then the additional roles. A numeric or id-like `role` (`366`,
438
+ * `'366'`) is never shown.
439
+ * Trimmed, empty ones dropped, deduped case-insensitively.
440
+ */
441
+ function creatorRoles(creator) {
442
+ if (!creator)
443
+ return [];
444
+ const { role } = creator;
445
+ const roleName = typeof role === 'object' && role !== null
446
+ ? role.name
447
+ : typeof role === 'string' && !/^\d+$/.test(role.trim())
448
+ ? role
449
+ : undefined;
450
+ const names = [
451
+ creator.roleName?.trim() || roleName,
452
+ ...(creator.additionalRoles ?? []).map((entry) => entry?.name),
453
+ ];
454
+ const seen = new Set();
455
+ const result = [];
456
+ for (const name of names) {
457
+ const text = name?.trim();
458
+ const key = text?.toLocaleLowerCase('pl');
459
+ if (!text || !key || seen.has(key))
460
+ continue;
461
+ seen.add(key);
462
+ result.push(text);
463
+ }
464
+ return result;
465
+ }
466
+ /** URL slug of the creator: the display name without markup. */
467
+ function creatorSlug(creator) {
468
+ return SlugService.create(creatorDisplayName(creatorPlainName(creator.name)));
469
+ }
470
+ /** Router commands to the details page: `[path, id, slug]`, or `[path, id]` for an empty slug. */
471
+ function creatorDetailsCommands(path, creator) {
472
+ const slug = creatorSlug(creator);
473
+ return slug ? [path, creator.id, slug] : [path, creator.id];
474
+ }
475
+ const CREATOR_DATE_YEAR = /^(\d{4})-\d{2}-\d{2}/;
476
+ /**
477
+ * Years under the name (museum-artists `artistYears`, MOB-1972): "1898 – 1951". The years of the full dates
478
+ * (`birthDate`/`deathDate`) first, then the ready `lifeYears`, then `birthYear`/`deathYear`, then the years in the
479
+ * period; `''` without any.
480
+ */
481
+ function creatorYears(creator) {
482
+ const born = CREATOR_DATE_YEAR.exec(creator.birthDate ?? '')?.[1];
483
+ const died = CREATOR_DATE_YEAR.exec(creator.deathDate ?? '')?.[1];
484
+ if (born || died)
485
+ return [born ?? '?', died ?? ''].join(' – ').trim();
486
+ const ready = creator.lifeYears?.trim();
487
+ if (ready)
488
+ return ready;
489
+ const bornYear = creator.birthYear ?? '';
490
+ const diedYear = creator.deathYear ?? '';
491
+ if (bornYear || diedYear) {
492
+ return [bornYear, diedYear].filter(Boolean).join(' – ');
493
+ }
494
+ const { period } = creator;
495
+ const text = typeof period === 'string'
496
+ ? period
497
+ : typeof period === 'object' && period !== null
498
+ ? (period.name ?? '')
499
+ : '';
500
+ return (text.match(CREATOR_YEAR) ?? []).slice(0, 2).join(' – ');
501
+ }
502
+ /** HTML whose text, without comments (MSO blocks), tags and entities (`&nbsp;`), is empty. */
503
+ function creatorLooksEmpty(html) {
504
+ return !decodeHtmlEntities(stripHtml(html.replace(CREATOR_HTML_COMMENT, ''))).trim();
505
+ }
506
+ /**
507
+ * The biography html to show (museum-artists `artistBiography`): `editorialBiography`, then `biography`, then
508
+ * `description`, trimmed. A field that only looks filled (`<p>&nbsp;</p>`, Word's `<!--[if gte mso 9]>` blocks) is
509
+ * skipped; `''` without any.
510
+ */
511
+ function creatorBiography(creator) {
512
+ if (!creator)
513
+ return '';
514
+ for (const html of [
515
+ creator.editorialBiography,
516
+ creator.biography,
517
+ creator.description,
518
+ ]) {
519
+ const value = html?.trim() ?? '';
520
+ if (value && !creatorLooksEmpty(value))
521
+ return value;
522
+ }
523
+ return '';
524
+ }
525
+ /** About six lines of the content column: a longer biography folds ("rozwiń tekst"). */
526
+ const CREATOR_FOLD_CHARS = 600;
527
+ function creatorBiographyText(html) {
528
+ return decodeHtmlEntities(creatorPlainText(html)).replace(/\s+/g, ' ').trim();
529
+ }
530
+ /** Whether the biography is longer than the folded view (600 characters of text). */
531
+ function creatorIsLongBiography(html) {
532
+ return creatorBiographyText(html).length > CREATOR_FOLD_CHARS;
533
+ }
534
+ /** The start of the biography for the folded view: plain text, cut at a word, with an ellipsis. */
535
+ function creatorBiographyExcerpt(html) {
536
+ const text = creatorBiographyText(html);
537
+ if (text.length <= CREATOR_FOLD_CHARS)
538
+ return text;
539
+ const cut = text.slice(0, CREATOR_FOLD_CHARS);
540
+ const space = cut.lastIndexOf(' ');
541
+ return `${(space > 0 ? cut.slice(0, space) : cut).replace(/[\s,.;:–-]+$/, '')}…`;
542
+ }
543
+
544
+ /**
545
+ * Group letter of a name: diacritics folded (NFD), only letters and digits
546
+ * kept, the first one upper-cased. `Ł` has no decomposition, so it stays its
547
+ * own letter. No letter or digit gives `#`.
548
+ */
549
+ function creatorNfdLetter(name) {
550
+ const normalized = (name ?? '')
551
+ .normalize('NFD')
552
+ .replace(/\p{Diacritic}/gu, '')
553
+ .replace(/[^\p{L}\p{N}]/gu, '');
554
+ return normalized.charAt(0).toUpperCase() || '#';
555
+ }
556
+ const creatorsCompare = (a, b) => a.localeCompare(b, 'pl');
557
+ /**
558
+ * Groups A-Z (reversed for `desc`), items A-Z inside a group either way. Both
559
+ * sorts use the Polish locale, so `Ł` follows `L` (Ossolineum's code-point sort
560
+ * puts it after `Z`).
561
+ */
562
+ function groupCreatorsByLetter(items, desc = false) {
563
+ const groups = new Map();
564
+ for (const item of items) {
565
+ const letter = creatorNfdLetter(item.name);
566
+ const group = groups.get(letter);
567
+ if (group)
568
+ group.push(item);
569
+ else
570
+ groups.set(letter, [item]);
571
+ }
572
+ const result = Array.from(groups, ([letter, groupItems]) => ({
573
+ letter,
574
+ items: groupItems.sort((a, b) => creatorsCompare(a.name ?? '', b.name ?? '')),
575
+ })).sort((a, b) => creatorsCompare(a.letter, b.letter));
576
+ return desc ? result.reverse() : result;
577
+ }
578
+
579
+ const EMPTY_CREATORS_FILTER = {
580
+ phrase: '',
581
+ birthFrom: '',
582
+ birthTo: '',
583
+ };
584
+ const CREATORS_BIRTH_KEY = 'dateFrom';
585
+ const CREATORS_YEAR = /^\d{1,4}$/;
586
+ /** `?filter=` of the creators page: the phrase and the birth range as full dates. */
587
+ function creatorsFilterQueryParams(value) {
588
+ const from = value.birthFrom.trim();
589
+ const to = value.birthTo.trim();
590
+ const query = [];
591
+ if (from) {
592
+ query.push({
593
+ key: CREATORS_BIRTH_KEY,
594
+ type: '>=',
595
+ value: yearRangeValue(from, 'from'),
596
+ });
597
+ }
598
+ if (to) {
599
+ query.push({
600
+ key: CREATORS_BIRTH_KEY,
601
+ type: '<=',
602
+ value: yearRangeValue(to, 'to'),
603
+ });
604
+ }
605
+ const filter = serializeCatalogFilter({
606
+ query: query,
607
+ searchText: value.phrase,
608
+ });
609
+ return filter ? { filter } : {};
610
+ }
611
+ /** Reads a parsed `?filter=` back into the form value. */
612
+ function creatorsFilterValueOf(filter) {
613
+ const birth = (type) => {
614
+ const item = filter?.query?.find((entry) => entry.key === CREATORS_BIRTH_KEY && entry.type === type);
615
+ return item ? yearRangeLabel(item.value) : '';
616
+ };
617
+ return {
618
+ phrase: filter?.searchText?.trim() ?? '',
619
+ birthFrom: birth('>='),
620
+ birthTo: birth('<='),
621
+ };
622
+ }
623
+ function hasCreatorsFilter(value) {
624
+ return !!(value.phrase.trim() ||
625
+ value.birthFrom.trim() ||
626
+ value.birthTo.trim());
627
+ }
628
+ /** A birth year as the API takes it: 1-4 digits, from 1 to 9999. */
629
+ function isCreatorsBirthYear(year) {
630
+ const trimmed = year.trim();
631
+ return CREATORS_YEAR.test(trimmed) && Number(trimmed) >= 1;
632
+ }
633
+ /** False for a year that is not `isCreatorsBirthYear`, or a range with `from` after `to`. */
634
+ function isCreatorsBirthRangeValid(value) {
635
+ const from = value.birthFrom.trim();
636
+ const to = value.birthTo.trim();
637
+ if ((from && !isCreatorsBirthYear(from)) ||
638
+ (to && !isCreatorsBirthYear(to))) {
639
+ return false;
640
+ }
641
+ return !(from && to && Number(from) > Number(to));
642
+ }
643
+
644
+ /**
645
+ * List meta of a creators page, one format for the index and the filtered view of the same page:
646
+ * `capitalize(t(titleKey)) - capitalize(t(siteNameKey))`, `t(descriptionKey)`, `type: 'website'`
647
+ * and `url` only when it is set.
648
+ */
649
+ function creatorsListSeoMetadata(seo, t) {
650
+ const metadata = {
651
+ title: `${capitalize(t(seo.titleKey))} - ${capitalize(t(seo.siteNameKey ?? 'MODEL.mainTitle'))}`,
652
+ description: t(seo.descriptionKey),
653
+ type: 'website',
654
+ };
655
+ if (seo.url)
656
+ metadata.url = seo.url;
657
+ return metadata;
658
+ }
659
+
660
+ /**
661
+ * The shown order of the last creators list (an index group or a catalog page). The details page
662
+ * reads prev/next from it; a deep link or SSR has no context and gets none.
663
+ */
664
+ class CreatorsListContext {
665
+ constructor() {
666
+ this.creatorsListContextItems = signal([], /* @ts-ignore */
667
+ ...(ngDevMode ? [{ debugName: "creatorsListContextItems" }] : /* istanbul ignore next */ []));
668
+ this.items = this.creatorsListContextItems.asReadonly();
669
+ }
670
+ /** The shown order of a list; ids are stringified (the list API sends numbers). */
671
+ set(items) {
672
+ this.creatorsListContextItems.set(items.map((item) => ({ id: String(item.id), name: item.name ?? null })));
673
+ }
674
+ clear() {
675
+ this.creatorsListContextItems.set([]);
676
+ }
677
+ /** Prev/next of a creator, compared as strings; none for an id outside the list. */
678
+ neighbours(id) {
679
+ const items = this.creatorsListContextItems();
680
+ const index = id === null || id === undefined
681
+ ? -1
682
+ : items.findIndex((item) => item.id === String(id));
683
+ if (index < 0)
684
+ return { prev: null, next: null };
685
+ return {
686
+ prev: items[index - 1] ?? null,
687
+ next: items[index + 1] ?? null,
688
+ };
689
+ }
690
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsListContext, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
691
+ /** @nocollapse */ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsListContext, providedIn: 'root' }); }
692
+ }
693
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsListContext, decorators: [{
694
+ type: Injectable,
695
+ args: [{ providedIn: 'root' }]
696
+ }] });
697
+
698
+ /** Page size 24, sorted by name A-Z, no letter and no filter. */
699
+ const CREATORS_CATALOG_DEFAULTS = {
700
+ offset: 0,
701
+ limit: 24,
702
+ sortBy: 'name',
703
+ sortDesc: false,
704
+ query: [],
705
+ };
706
+ const CREATORS_CATALOG_LETTER_KEY = 'sw';
707
+ const CREATORS_CATALOG_BIRTH_KEY = 'dateFrom';
708
+ const CREATORS_CATALOG_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
709
+ /** Days of a month, February by the leap-year rule. */
710
+ function creatorsCatalogDaysInMonth(year, month) {
711
+ if (month === 2) {
712
+ const leap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
713
+ return leap ? 29 : 28;
714
+ }
715
+ return [4, 6, 9, 11].indexOf(month) >= 0 ? 30 : 31;
716
+ }
717
+ /**
718
+ * A birth bound as the API takes it (`YYYY-MM-DD`): an `isCreatorsBirthYear` year is widened with
719
+ * `yearRangeValue`, a full date must be a real calendar date whose year passes `isCreatorsBirthYear`. Anything else is `null` and is not sent:
720
+ * the API answers 400 `SEARCH_ERROR` to it.
721
+ */
722
+ function creatorsCatalogBirthDate(value, bound) {
723
+ if (typeof value !== 'string' && typeof value !== 'number')
724
+ return null;
725
+ const trimmed = String(value).trim();
726
+ const date = isCreatorsBirthYear(trimmed)
727
+ ? yearRangeValue(trimmed, bound)
728
+ : trimmed;
729
+ const match = CREATORS_CATALOG_DATE.exec(date);
730
+ if (!match)
731
+ return null;
732
+ const year = Number(match[1]);
733
+ const month = Number(match[2]);
734
+ const day = Number(match[3]);
735
+ const valid = isCreatorsBirthYear(match[1]) &&
736
+ month >= 1 &&
737
+ month <= 12 &&
738
+ day >= 1 &&
739
+ day <= creatorsCatalogDaysInMonth(year, month);
740
+ return valid ? date : null;
741
+ }
742
+ function creatorsCatalogQueryItem(filter, key, type = '=') {
743
+ return filter?.query?.find((item) => item.key === key && (item.type ?? '=') === type);
744
+ }
745
+ function creatorsCatalogLetter(filter) {
746
+ const value = creatorsCatalogQueryItem(filter, CREATORS_CATALOG_LETTER_KEY)?.value;
747
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
748
+ }
749
+ /**
750
+ * The catalog filter as a `CreatorsService.getPage` query: page and size, the phrase (`q`), the
751
+ * letter (query key `sw`), the birth range (query key `dateFrom`, `>=`/`<=`) and the name sort.
752
+ * A blank phrase or letter and an invalid birth bound are left out.
753
+ */
754
+ function creatorsFilterToQuery(filter) {
755
+ const limit = filter?.limit || CREATORS_CATALOG_DEFAULTS.limit || 24;
756
+ const query = {
757
+ page: offsetToPage(filter?.offset ?? 0, limit),
758
+ maxPerPage: limit,
759
+ sort: filter?.sortBy === 'name'
760
+ ? filter.sortDesc
761
+ ? 'name-desc'
762
+ : 'name-asc'
763
+ : null,
764
+ };
765
+ const q = filter?.searchText?.trim();
766
+ const sw = creatorsCatalogLetter(filter);
767
+ const birthFrom = creatorsCatalogBirthDate(creatorsCatalogQueryItem(filter, CREATORS_CATALOG_BIRTH_KEY, '>=')?.value, 'from');
768
+ const birthTo = creatorsCatalogBirthDate(creatorsCatalogQueryItem(filter, CREATORS_CATALOG_BIRTH_KEY, '<=')?.value, 'to');
769
+ if (q)
770
+ query.q = q;
771
+ if (sw)
772
+ query.sw = sw;
773
+ if (birthFrom)
774
+ query.birthFrom = birthFrom;
775
+ if (birthTo)
776
+ query.birthTo = birthTo;
777
+ return query;
778
+ }
779
+ /**
780
+ * A signal facade over `CreatorsService.getPage` for `createCatalog` (not an NgRx CRUD slice).
781
+ * A newer read cancels the older one. Call in an injection context.
782
+ */
783
+ function createCreatorsFacade() {
784
+ const service = inject(CreatorsService);
785
+ const filter = signal(null, /* @ts-ignore */
786
+ ...(ngDevMode ? [{ debugName: "filter" }] : /* istanbul ignore next */ []));
787
+ const loading = signal(false, /* @ts-ignore */
788
+ ...(ngDevMode ? [{ debugName: "loading" }] : /* istanbul ignore next */ []));
789
+ const error = signal(null, /* @ts-ignore */
790
+ ...(ngDevMode ? [{ debugName: "error" }] : /* istanbul ignore next */ []));
791
+ const totalCount = signal(0, /* @ts-ignore */
792
+ ...(ngDevMode ? [{ debugName: "totalCount" }] : /* istanbul ignore next */ []));
793
+ const list = signal([], /* @ts-ignore */
794
+ ...(ngDevMode ? [{ debugName: "list" }] : /* istanbul ignore next */ []));
795
+ const loadedFilter = signal(null, /* @ts-ignore */
796
+ ...(ngDevMode ? [{ debugName: "loadedFilter" }] : /* istanbul ignore next */ []));
797
+ const reads = new Subject();
798
+ reads
799
+ .pipe(switchMap((next) => service.getPage(creatorsFilterToQuery(next)).pipe(map((page) => ({
800
+ page,
801
+ filter: next,
802
+ })), catchError((failure) => of({ error: failure })))), takeUntilDestroyed(inject(DestroyRef)))
803
+ .subscribe((result) => {
804
+ if ('page' in result) {
805
+ list.set(result.page.items);
806
+ loadedFilter.set(result.filter);
807
+ totalCount.set(result.page.total);
808
+ error.set(null);
809
+ }
810
+ else {
811
+ error.set(result.error);
812
+ list.set([]);
813
+ totalCount.set(0);
814
+ }
815
+ loading.set(false);
816
+ });
817
+ return {
818
+ filter: filter.asReadonly(),
819
+ loading: loading.asReadonly(),
820
+ error: error.asReadonly(),
821
+ totalCount: totalCount.asReadonly(),
822
+ list: list.asReadonly(),
823
+ loadedFilter: loadedFilter.asReadonly(),
824
+ read: (next) => {
825
+ filter.set(next);
826
+ loading.set(true);
827
+ reads.next(next);
828
+ },
829
+ };
830
+ }
831
+ /**
832
+ * The filtered creators list (`/artysci?filter=…`) on `createCatalog`, with the creators facade and
833
+ * no read guard (a foreign `CrudConfig` in the injector cannot stall it). After each load it writes
834
+ * the shown order into `CreatorsListContext`. Call in an injection context.
835
+ */
836
+ function createCreatorsCatalog(options) {
837
+ const facade = createCreatorsFacade();
838
+ const listContext = inject(CreatorsListContext);
839
+ const catalog = createCatalog({
840
+ ...options,
841
+ defaults: options.defaults ?? CREATORS_CATALOG_DEFAULTS,
842
+ facade,
843
+ readGuard: false,
844
+ });
845
+ const sortDesc = computed(() => !!catalog.filter()?.sortDesc, /* @ts-ignore */
846
+ ...(ngDevMode ? [{ debugName: "sortDesc" }] : /* istanbul ignore next */ []));
847
+ // The shown page keeps the direction it was loaded with until the next one arrives.
848
+ const groups = computed(() => groupCreatorsByLetter(facade.list(), !!facade.loadedFilter()?.sortDesc), /* @ts-ignore */
849
+ ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
850
+ effect(() => {
851
+ const shown = groups();
852
+ if (catalog.hasError()) {
853
+ untracked(() => listContext.clear());
854
+ return;
855
+ }
856
+ if (catalog.isLoading() || !catalog.filter())
857
+ return;
858
+ untracked(() => listContext.set(shown.flatMap((group) => group.items)));
859
+ });
860
+ const birthItem = (year, bound) => {
861
+ const value = year.trim()
862
+ ? creatorsCatalogBirthDate(year.trim(), bound)
863
+ : null;
864
+ return value
865
+ ? [
866
+ {
867
+ key: CREATORS_CATALOG_BIRTH_KEY,
868
+ value,
869
+ type: bound === 'from' ? '>=' : '<=',
870
+ hidden: true,
871
+ },
872
+ ]
873
+ : [];
874
+ };
875
+ const filterFor = (value) => {
876
+ const current = catalog.filter() ?? CREATORS_CATALOG_DEFAULTS;
877
+ const query = (current.query ?? []).filter((item) => !(item.key === CREATORS_CATALOG_BIRTH_KEY &&
878
+ (item.type === '>=' || item.type === '<=')));
879
+ return {
880
+ ...current,
881
+ offset: 0,
882
+ searchText: value.phrase.trim(),
883
+ query: [
884
+ ...query,
885
+ ...birthItem(value.birthFrom, 'from'),
886
+ ...birthItem(value.birthTo, 'to'),
887
+ ],
888
+ };
889
+ };
890
+ return {
891
+ ...catalog,
892
+ list: facade.list,
893
+ groups,
894
+ sortDesc,
895
+ startsWith: computed(() => creatorsCatalogLetter(catalog.filter())),
896
+ setStartsWith: (letter) => {
897
+ const trimmed = letter?.trim();
898
+ if (trimmed) {
899
+ catalog.setQueryItem(CREATORS_CATALOG_LETTER_KEY, trimmed);
900
+ }
901
+ else {
902
+ catalog.removeQueryKey(CREATORS_CATALOG_LETTER_KEY);
903
+ }
904
+ },
905
+ filterValue: computed(() => creatorsFilterValueOf(catalog.filter())),
906
+ filterFor,
907
+ applyFilter: (value) => {
908
+ const { searchText, query } = filterFor(value);
909
+ catalog.update({ searchText, query });
910
+ },
911
+ };
912
+ }
913
+
914
+ /** `?litera=` of the index. A constant: the letter stream is a field-initialized `toSignal`. */
915
+ const CREATORS_INDEX_LETTER_PARAM = 'litera';
916
+ /** `?prefiks=`: the two-letter group of the letter the index shows (MOB-1959). */
917
+ const CREATORS_INDEX_PREFIX_PARAM = 'prefiks';
918
+ /** `?filter=` belongs to the catalog results view; the index stays idle while it is there. */
919
+ const CREATORS_INDEX_FILTER_PARAM = 'filter';
920
+ const CREATORS_INDEX_LOADING = {
921
+ status: 'loading',
922
+ prefixes: [],
923
+ prefix: null,
924
+ items: [],
925
+ };
926
+ const CREATORS_INDEX_IDLE = {
927
+ status: 'success',
928
+ prefixes: [],
929
+ prefix: null,
930
+ items: [],
931
+ };
932
+ /**
933
+ * Logic of the creators letter index (museum-artists' `/artysci` at MOB-1959): `?litera=` picks the
934
+ * letter, `?prefiks=` its group, the address `#fragment` (`#B`, `#Ba`, an anchor) is followed once
935
+ * the group has loaded, and the mobile jump-to panel's choice is scrolled to and focused.
936
+ *
937
+ * The museum writes the HTML: a `#filtersButton` element (the focus goes back to it when the
938
+ * panel closes), a `[id]="group.anchor"` `tabindex="-1"` section for the shown group, and the
939
+ * jump-to panel bound to `changeLetter`, `selectPrefix` and `closeFilters`.
940
+ */
941
+ class CreatorsIndexBaseComponent {
942
+ constructor() {
943
+ /** Anchor prefix of the groups (`tworcy-42-61`); sztuki uses `'artysci'`. Read lazily. */
944
+ this.creatorsIndexAnchorBase = 'tworcy';
945
+ /** SEO keys (Ossolineum's by default). Read once, after the subclass initialisers. */
946
+ this.creatorsIndexSeo = {
947
+ titleKey: 'creatorsPage',
948
+ descriptionKey: 'CREATORS.metaDescription',
949
+ siteNameKey: 'museumName',
950
+ };
951
+ this.creatorsIndexRoute = inject(ActivatedRoute);
952
+ this.creatorsIndexRouter = inject(Router);
953
+ this.creatorsIndexInjector = inject(Injector);
954
+ this.creatorsIndexLocation = inject(Location);
955
+ this.creatorsIndexDocument = inject(DOCUMENT);
956
+ this.creatorsIndexScroller = inject(ViewportScroller);
957
+ this.creatorsIndexIsBrowser = isPlatformBrowser(inject(PLATFORM_ID));
958
+ this.creatorsIndexService = inject(CreatorsService);
959
+ this.creatorsIndexListContext = inject(CreatorsListContext);
960
+ this.creatorsIndexSeoService = inject(MuseumSeoService);
961
+ this.creatorsIndexTranslate = inject(TranslateService);
962
+ this.creatorsIndexRetry$ = new Subject();
963
+ /** Group picked in the jump-to panel, scrolled to once it has loaded. */
964
+ this.creatorsIndexPendingAnchor = signal(null, /* @ts-ignore */
965
+ ...(ngDevMode ? [{ debugName: "creatorsIndexPendingAnchor" }] : /* istanbul ignore next */ []));
966
+ /** Last `#fragment` already scrolled to, so data reloads do not jump again. */
967
+ this.creatorsIndexHandledFragment = null;
968
+ this.alphabet = CREATORS_ALPHABET;
969
+ this.creatorsIndexRouteState$ = this.creatorsIndexRoute.queryParamMap.pipe(map((params) => ({
970
+ letter: parseCreatorsLetter(params.get(CREATORS_INDEX_LETTER_PARAM)),
971
+ filtered: params.has(CREATORS_INDEX_FILTER_PARAM),
972
+ })), distinctUntilChanged((a, b) => a.letter === b.letter && a.filtered === b.filtered));
973
+ this.letter = toSignal(this.creatorsIndexRouteState$.pipe(map(({ letter }) => letter), distinctUntilChanged()), { initialValue: 'A' });
974
+ /**
975
+ * `?filter=` is present: the catalog results view owns the page and the index stays idle. The template switches
976
+ * between the index and the results view on it.
977
+ */
978
+ this.filtered = toSignal(this.creatorsIndexRouteState$.pipe(map(({ filtered }) => filtered)), { initialValue: false });
979
+ this.creatorsIndexPrefixParam$ = this.creatorsIndexRoute.queryParamMap.pipe(map((params) => params.get(CREATORS_INDEX_PREFIX_PARAM)), distinctUntilChanged());
980
+ this.creatorsIndexPrefixes$ = combineLatest([
981
+ this.creatorsIndexRouteState$,
982
+ this.creatorsIndexRetry$.pipe(startWith(undefined)),
983
+ ]).pipe(switchMap(([route]) => route.filtered
984
+ ? of({
985
+ ...route,
986
+ status: 'success',
987
+ prefixes: [],
988
+ })
989
+ : this.creatorsIndexService.getPrefixes(route.letter).pipe(map((prefixes) => ({ status: 'success', prefixes })), catchError(() => of({ status: 'error', prefixes: [] })), startWith({
990
+ status: 'loading',
991
+ prefixes: [],
992
+ }), map((state) => ({
993
+ ...route,
994
+ ...state,
995
+ })))), shareReplay({ bufferSize: 1, refCount: true }));
996
+ this.state = toSignal(combineLatest([
997
+ this.creatorsIndexPrefixes$,
998
+ this.creatorsIndexPrefixParam$,
999
+ ]).pipe(switchMap(([{ letter, filtered, status, prefixes }, raw]) => {
1000
+ if (filtered)
1001
+ return of(CREATORS_INDEX_IDLE);
1002
+ return status === 'success'
1003
+ ? this.creatorsIndexLoad(letter, prefixes, raw)
1004
+ : of({ ...CREATORS_INDEX_LOADING, status });
1005
+ })), { initialValue: CREATORS_INDEX_LOADING });
1006
+ this.groups = computed(() => {
1007
+ const { prefixes, prefix: shown, items } = this.state();
1008
+ const base = this.creatorsIndexAnchorBase;
1009
+ return prefixes.map((prefix) => ({
1010
+ prefix,
1011
+ anchor: creatorsPrefixAnchor(prefix, base),
1012
+ items: prefix === shown ? items : [],
1013
+ }));
1014
+ }, /* @ts-ignore */
1015
+ ...(ngDevMode ? [{ debugName: "groups" }] : /* istanbul ignore next */ []));
1016
+ this.prefixes = computed(() => this.state().prefixes, /* @ts-ignore */
1017
+ ...(ngDevMode ? [{ debugName: "prefixes" }] : /* istanbul ignore next */ []));
1018
+ this.activePrefix = computed(() => this.state().prefix, /* @ts-ignore */
1019
+ ...(ngDevMode ? [{ debugName: "activePrefix" }] : /* istanbul ignore next */ []));
1020
+ this.activeAnchor = computed(() => {
1021
+ const prefix = this.activePrefix();
1022
+ return prefix
1023
+ ? creatorsPrefixAnchor(prefix, this.creatorsIndexAnchorBase)
1024
+ : null;
1025
+ }, /* @ts-ignore */
1026
+ ...(ngDevMode ? [{ debugName: "activeAnchor" }] : /* istanbul ignore next */ []));
1027
+ this.creatorsIndexFragment = toSignal(this.creatorsIndexRoute.fragment, { initialValue: null });
1028
+ this.filtersOpen = signal(false, /* @ts-ignore */
1029
+ ...(ngDevMode ? [{ debugName: "filtersOpen" }] : /* istanbul ignore next */ []));
1030
+ /** The museum's `#filtersButton` (template ref contract). */
1031
+ this.creatorsIndexFiltersButton = viewChild('filtersButton', /* @ts-ignore */
1032
+ ...(ngDevMode ? [{ debugName: "creatorsIndexFiltersButton" }] : /* istanbul ignore next */ []));
1033
+ // Once, after the subclass initialisers, so an overridden `creatorsIndexSeo` counts.
1034
+ effect(() => untracked(() => this.creatorsIndexApplySeo()));
1035
+ // As the catalog: a failed load leaves no stale prev/next behind.
1036
+ effect(() => {
1037
+ const state = this.state();
1038
+ if (this.filtered() || state.status === 'loading')
1039
+ return;
1040
+ if (state.status === 'error') {
1041
+ untracked(() => this.creatorsIndexListContext.clear());
1042
+ return;
1043
+ }
1044
+ const shown = this.groups().flatMap((group) => group.items);
1045
+ untracked(() => this.creatorsIndexListContext.set(shown));
1046
+ });
1047
+ // The router's anchorScrolling fires on NavigationEnd, before the group's
1048
+ // creators arrive, so an address like `/artysci#B` would never scroll.
1049
+ effect(() => {
1050
+ if (!this.creatorsIndexIsBrowser)
1051
+ return;
1052
+ if (this.filtered()) {
1053
+ // The filter view owns the page; coming back to the same fragment scrolls again.
1054
+ this.creatorsIndexHandledFragment = null;
1055
+ return;
1056
+ }
1057
+ const fragment = this.creatorsIndexFragment();
1058
+ const state = this.state();
1059
+ if (!fragment) {
1060
+ // Leaving the fragment (e.g. another letter) re-arms it for Back.
1061
+ this.creatorsIndexHandledFragment = null;
1062
+ return;
1063
+ }
1064
+ if (state.status === 'loading' && !state.prefixes.length)
1065
+ return;
1066
+ if (fragment === this.creatorsIndexHandledFragment)
1067
+ return;
1068
+ const groups = this.groups();
1069
+ const target = resolveCreatorsFragment(fragment, this.letter(), groups);
1070
+ if (!target)
1071
+ return;
1072
+ // A letter switch does not need this letter's list (it may have failed).
1073
+ if ('letter' in target) {
1074
+ this.creatorsIndexRouter.navigate([], {
1075
+ relativeTo: this.creatorsIndexRoute,
1076
+ queryParams: { [CREATORS_INDEX_LETTER_PARAM]: target.letter },
1077
+ fragment,
1078
+ replaceUrl: true,
1079
+ });
1080
+ return;
1081
+ }
1082
+ // Another group of this letter is loaded first (`?prefiks=`).
1083
+ const group = groups.find((g) => g.anchor === target.anchor);
1084
+ if (group && group.prefix !== state.prefix) {
1085
+ this.creatorsIndexRouter.navigate([], {
1086
+ relativeTo: this.creatorsIndexRoute,
1087
+ queryParams: {
1088
+ [CREATORS_INDEX_LETTER_PARAM]: this.letter(),
1089
+ [CREATORS_INDEX_PREFIX_PARAM]: group.prefix,
1090
+ },
1091
+ fragment,
1092
+ replaceUrl: true,
1093
+ });
1094
+ return;
1095
+ }
1096
+ if (state.status !== 'success')
1097
+ return;
1098
+ this.creatorsIndexHandledFragment = fragment;
1099
+ afterNextRender(() => this.jumpTo(target.anchor), {
1100
+ injector: this.creatorsIndexInjector,
1101
+ });
1102
+ });
1103
+ // The group picked in the jump-to panel is reached once it has loaded; when
1104
+ // it fails, the focus goes back to the filters button instead.
1105
+ effect(() => {
1106
+ const state = this.state();
1107
+ const anchor = this.creatorsIndexPendingAnchor();
1108
+ if (!anchor || this.filtersOpen() || state.status === 'loading')
1109
+ return;
1110
+ if (anchor !== this.activeAnchor())
1111
+ return;
1112
+ this.creatorsIndexPendingAnchor.set(null);
1113
+ afterNextRender(() => state.status === 'success'
1114
+ ? this.jumpTo(anchor)
1115
+ : this.creatorsIndexFocusFiltersButton(), { injector: this.creatorsIndexInjector });
1116
+ });
1117
+ }
1118
+ back() {
1119
+ this.creatorsIndexLocation.back();
1120
+ }
1121
+ /** Scrolls to a group and moves focus to it (the router only scrolls). Browser only. */
1122
+ jumpTo(anchor) {
1123
+ if (!this.creatorsIndexIsBrowser)
1124
+ return;
1125
+ this.creatorsIndexScroller.scrollToAnchor(anchor);
1126
+ this.creatorsIndexDocument
1127
+ .getElementById(anchor)
1128
+ ?.focus({ preventScroll: true });
1129
+ }
1130
+ openFilters() {
1131
+ this.creatorsIndexPendingAnchor.set(null);
1132
+ this.filtersOpen.set(true);
1133
+ }
1134
+ changeLetter(letter) {
1135
+ this.creatorsIndexPendingAnchor.set(null);
1136
+ this.creatorsIndexRouter.navigate([], {
1137
+ relativeTo: this.creatorsIndexRoute,
1138
+ queryParams: { [CREATORS_INDEX_LETTER_PARAM]: letter },
1139
+ });
1140
+ }
1141
+ /** The panel's group: loaded through `?prefiks=`, then scrolled to. */
1142
+ selectPrefix(prefix) {
1143
+ const anchor = creatorsPrefixAnchor(prefix, this.creatorsIndexAnchorBase);
1144
+ this.creatorsIndexPendingAnchor.set(anchor);
1145
+ this.creatorsIndexRouter
1146
+ .navigate([], {
1147
+ relativeTo: this.creatorsIndexRoute,
1148
+ queryParams: {
1149
+ [CREATORS_INDEX_LETTER_PARAM]: this.letter(),
1150
+ [CREATORS_INDEX_PREFIX_PARAM]: prefix,
1151
+ },
1152
+ })
1153
+ .then((navigated) => {
1154
+ // A cancelled navigation never loads the group; the same address
1155
+ // (the active group applied again) is reached by the effect.
1156
+ if (navigated || this.creatorsIndexPendingAnchor() !== anchor)
1157
+ return;
1158
+ if (anchor === this.activeAnchor())
1159
+ return;
1160
+ this.creatorsIndexPendingAnchor.set(null);
1161
+ this.creatorsIndexFocusFiltersButton();
1162
+ });
1163
+ }
1164
+ /**
1165
+ * Focus moves after the next render, once the panel is gone: to the picked group once it has
1166
+ * loaded, or back to the filters button (Safari does not focus a button on click).
1167
+ */
1168
+ closeFilters() {
1169
+ this.filtersOpen.set(false);
1170
+ if (this.creatorsIndexPendingAnchor())
1171
+ return;
1172
+ afterNextRender(() => this.creatorsIndexFocusFiltersButton(), {
1173
+ injector: this.creatorsIndexInjector,
1174
+ });
1175
+ }
1176
+ retry() {
1177
+ this.creatorsIndexRetry$.next();
1178
+ }
1179
+ creatorsIndexFocusFiltersButton() {
1180
+ if (!this.creatorsIndexIsBrowser)
1181
+ return;
1182
+ this.creatorsIndexFiltersButton()?.nativeElement.focus();
1183
+ }
1184
+ creatorsIndexApplySeo() {
1185
+ this.creatorsIndexSeoService.apply(creatorsListSeoMetadata(this.creatorsIndexSeo, (key) => String(this.creatorsIndexTranslate.instant(key) ?? '')));
1186
+ }
1187
+ creatorsIndexLoad(letter, prefixes, raw) {
1188
+ const prefix = creatorsFindPrefix(raw, prefixes) ?? prefixes[0] ?? null;
1189
+ const base = { prefixes, prefix };
1190
+ if (!prefix)
1191
+ return of({ ...base, status: 'success', items: [] });
1192
+ return this.creatorsIndexService.getByIndexPrefix(letter, prefix).pipe(map((items) => ({
1193
+ ...base,
1194
+ status: 'success',
1195
+ items,
1196
+ })), catchError(() => of({ ...base, status: 'error', items: [] })), startWith({ ...base, status: 'loading', items: [] }));
1197
+ }
1198
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsIndexBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1199
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: CreatorsIndexBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "creatorsIndexFiltersButton", first: true, predicate: ["filtersButton"], descendants: true, isSignal: true }], ngImport: i0 }); }
1200
+ }
1201
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsIndexBaseComponent, decorators: [{
1202
+ type: Directive
1203
+ }], ctorParameters: () => [], propDecorators: { creatorsIndexFiltersButton: [{ type: i0.ViewChild, args: ['filtersButton', { isSignal: true }] }] } });
1204
+
1205
+ /**
1206
+ * Logic of the mobile "go to" panel of the creators index (museum-artists'
1207
+ * `index-filters-mobile`, MOB-1949/1959). The choice is a draft until `apply()`:
1208
+ * another letter emits `letterChange`, a prefix of the current letter emits
1209
+ * `prefixSelect`. Every way out (close, backdrop, Escape, apply) emits `closed`;
1210
+ * the host removes the panel and moves the focus on.
1211
+ *
1212
+ * Escape is bound here (`host`), so a subclass must not bind it again.
1213
+ */
1214
+ class CreatorsJumpToBaseComponent {
1215
+ constructor() {
1216
+ /**
1217
+ * The header the card opens under; the first element found wins. Read after the
1218
+ * first render, so an override counts.
1219
+ */
1220
+ this.creatorsJumpToHeaderSelectors = [
1221
+ 'app-header',
1222
+ 'app-nav-bar',
1223
+ ];
1224
+ this.creatorsJumpToDocument = inject(DOCUMENT);
1225
+ this.letters = input.required(/* @ts-ignore */
1226
+ ...(ngDevMode ? [{ debugName: "letters" }] : /* istanbul ignore next */ []));
1227
+ this.activeLetter = input.required(/* @ts-ignore */
1228
+ ...(ngDevMode ? [{ debugName: "activeLetter" }] : /* istanbul ignore next */ []));
1229
+ /** Prefix groups of the loaded (active) letter. */
1230
+ this.prefixes = input([], /* @ts-ignore */
1231
+ ...(ngDevMode ? [{ debugName: "prefixes" }] : /* istanbul ignore next */ []));
1232
+ /** Prefix the index currently shows, pre-selected. */
1233
+ this.activePrefix = input(null, /* @ts-ignore */
1234
+ ...(ngDevMode ? [{ debugName: "activePrefix" }] : /* istanbul ignore next */ []));
1235
+ this.letterChange = output();
1236
+ this.prefixSelect = output();
1237
+ this.closed = output();
1238
+ this.selectedLetter = linkedSignal(() => this.activeLetter(), /* @ts-ignore */
1239
+ ...(ngDevMode ? [{ debugName: "selectedLetter" }] : /* istanbul ignore next */ []));
1240
+ this.selectedPrefix = linkedSignal(() => this.activePrefix(), /* @ts-ignore */
1241
+ ...(ngDevMode ? [{ debugName: "selectedPrefix" }] : /* istanbul ignore next */ []));
1242
+ /** The card's top edge: the header's bottom edge, measured once the panel is rendered. */
1243
+ this.topOffset = signal(0, /* @ts-ignore */
1244
+ ...(ngDevMode ? [{ debugName: "topOffset" }] : /* istanbul ignore next */ []));
1245
+ afterNextRender(() => this.topOffset.set(this.creatorsJumpToMeasureHeader()));
1246
+ }
1247
+ selectLetter(letter) {
1248
+ this.selectedLetter.set(letter);
1249
+ this.selectedPrefix.set(null);
1250
+ }
1251
+ apply() {
1252
+ const letter = this.selectedLetter();
1253
+ const prefix = this.selectedPrefix();
1254
+ if (letter !== this.activeLetter())
1255
+ this.letterChange.emit(letter);
1256
+ else if (prefix)
1257
+ this.prefixSelect.emit(prefix);
1258
+ this.closed.emit();
1259
+ }
1260
+ close() {
1261
+ this.closed.emit();
1262
+ }
1263
+ creatorsJumpToMeasureHeader() {
1264
+ let header = null;
1265
+ for (const selector of this.creatorsJumpToHeaderSelectors) {
1266
+ header = this.creatorsJumpToDocument.querySelector(selector);
1267
+ if (header)
1268
+ break;
1269
+ }
1270
+ return Math.max(0, Math.round(header?.getBoundingClientRect().bottom ?? 0));
1271
+ }
1272
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsJumpToBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1273
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: CreatorsJumpToBaseComponent, isStandalone: true, inputs: { letters: { classPropertyName: "letters", publicName: "letters", isSignal: true, isRequired: true, transformFunction: null }, activeLetter: { classPropertyName: "activeLetter", publicName: "activeLetter", isSignal: true, isRequired: true, transformFunction: null }, prefixes: { classPropertyName: "prefixes", publicName: "prefixes", isSignal: true, isRequired: false, transformFunction: null }, activePrefix: { classPropertyName: "activePrefix", publicName: "activePrefix", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { letterChange: "letterChange", prefixSelect: "prefixSelect", closed: "closed" }, host: { listeners: { "keydown.escape": "close()" } }, ngImport: i0 }); }
1274
+ }
1275
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsJumpToBaseComponent, decorators: [{
1276
+ type: Directive,
1277
+ args: [{ host: { '(keydown.escape)': 'close()' } }]
1278
+ }], ctorParameters: () => [], propDecorators: { letters: [{ type: i0.Input, args: [{ isSignal: true, alias: "letters", required: true }] }], activeLetter: [{ type: i0.Input, args: [{ isSignal: true, alias: "activeLetter", required: true }] }], prefixes: [{ type: i0.Input, args: [{ isSignal: true, alias: "prefixes", required: false }] }], activePrefix: [{ type: i0.Input, args: [{ isSignal: true, alias: "activePrefix", required: false }] }], letterChange: [{ type: i0.Output, args: ["letterChange"] }], prefixSelect: [{ type: i0.Output, args: ["prefixSelect"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1279
+
1280
+ /**
1281
+ * Base of the filtered creators list (`/artysci?filter=…`) on `createCreatorsCatalog`: first read,
1282
+ * persistence, sort/size/page, the letter, the phrase and the birth range come from `catalog`;
1283
+ * the page keeps its template. The url is the source of truth for the filter: `applyFilter` navigates
1284
+ * to `?filter=` (the catalog reads it), `clearAllFilters` goes back to the index (`listPath`).
1285
+ *
1286
+ * ```ts
1287
+ * export class ResultsComponent extends CreatorsCatalogBaseComponent {
1288
+ * constructor() {
1289
+ * super({ persistKey: 'ARTISTS', listPath: '/artysci', seo: { titleKey: 'PAGE_TITLES.artists', descriptionKey: 'ARTISTS.metaDescription' } });
1290
+ * }
1291
+ * }
1292
+ * ```
1293
+ *
1294
+ * A `#listContainer` element in the template is scrolled to after a page change from the bottom
1295
+ * pagination (`bottomPageChange`), in the browser only.
1296
+ */
1297
+ class CreatorsCatalogBaseComponent {
1298
+ constructor(options) {
1299
+ this.creatorsCatalogPageTranslate = inject(TranslateService);
1300
+ this.creatorsCatalogPageRouter = inject(Router);
1301
+ this.creatorsCatalogPageSeoService = inject(MuseumSeoService);
1302
+ this.creatorsCatalogPageIsBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1303
+ this.creatorsCatalogPageListContainer = viewChild('listContainer', /* @ts-ignore */
1304
+ ...(ngDevMode ? [{ debugName: "creatorsCatalogPageListContainer" }] : /* istanbul ignore next */ []));
1305
+ this.creatorsCatalogPageScrollAfterLoad = false;
1306
+ /** The phrase or a birth bound is set. */
1307
+ this.hasActiveFilters = computed(() => hasCreatorsFilter(this.catalog.filterValue()), /* @ts-ignore */
1308
+ ...(ngDevMode ? [{ debugName: "hasActiveFilters" }] : /* istanbul ignore next */ []));
1309
+ const { seo, ...catalogOptions } = options;
1310
+ this.catalog = createCreatorsCatalog(catalogOptions);
1311
+ this.creatorsCatalogPageListPath = options.listPath;
1312
+ // After a page change from the bottom pagination scroll to the list top
1313
+ // once the data has loaded.
1314
+ effect(() => {
1315
+ const loading = this.catalog.isLoading();
1316
+ if (loading ||
1317
+ !this.creatorsCatalogPageScrollAfterLoad ||
1318
+ !this.creatorsCatalogPageIsBrowser) {
1319
+ return;
1320
+ }
1321
+ this.creatorsCatalogPageScrollAfterLoad = false;
1322
+ setTimeout(() => this.creatorsCatalogPageListContainer()?.nativeElement.scrollIntoView({
1323
+ behavior: 'smooth',
1324
+ block: 'start',
1325
+ }));
1326
+ });
1327
+ if (seo)
1328
+ this.creatorsCatalogPageApplySeo(seo);
1329
+ }
1330
+ pageChange(page) {
1331
+ this.catalog.setPage(page);
1332
+ }
1333
+ bottomPageChange(page) {
1334
+ this.creatorsCatalogPageScrollAfterLoad = true;
1335
+ this.pageChange(page);
1336
+ }
1337
+ onLimitChange(limit) {
1338
+ this.catalog.setLimit(limit);
1339
+ }
1340
+ sortChange(key) {
1341
+ this.catalog.setSortKey(key);
1342
+ }
1343
+ onSearch(phrase) {
1344
+ this.catalog.search(phrase);
1345
+ }
1346
+ /** The letter (`sw`); `null` or blank drops it. */
1347
+ setStartsWith(letter) {
1348
+ this.catalog.setStartsWith(letter);
1349
+ }
1350
+ /**
1351
+ * Navigates to `listPath?filter=…` with the phrase and the birth range (the letter, the sort and the size kept);
1352
+ * the catalog reads the new `?filter=`. Without any filter the url has no `?filter=`, so the index shows.
1353
+ */
1354
+ applyFilter(value) {
1355
+ void this.creatorsCatalogPageRouter.navigate([this.creatorsCatalogPageListPath], { queryParams: catalogListQueryParams(this.catalog.filterFor(value)) });
1356
+ }
1357
+ retryLoad() {
1358
+ this.catalog.retry();
1359
+ }
1360
+ /** Back to the index: `listPath` without parameters. */
1361
+ clearAllFilters() {
1362
+ void this.creatorsCatalogPageRouter.navigate([
1363
+ this.creatorsCatalogPageListPath,
1364
+ ]);
1365
+ }
1366
+ /**
1367
+ * The index's list meta (`creatorsListSeoMetadata`), not in a search tab: the canonical url of
1368
+ * the list must not point at a phrase url.
1369
+ */
1370
+ creatorsCatalogPageApplySeo(seo) {
1371
+ if (this.catalog.searchEnabled())
1372
+ return;
1373
+ this.creatorsCatalogPageSeoService.apply(creatorsListSeoMetadata(seo, (key) => String(this.creatorsCatalogPageTranslate.instant(key) ?? '')));
1374
+ }
1375
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsCatalogBaseComponent, deps: "invalid", target: i0.ɵɵFactoryTarget.Directive }); }
1376
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: CreatorsCatalogBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "creatorsCatalogPageListContainer", first: true, predicate: ["listContainer"], descendants: true, isSignal: true }], ngImport: i0 }); }
1377
+ }
1378
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsCatalogBaseComponent, decorators: [{
1379
+ type: Directive
1380
+ }], ctorParameters: () => [{ type: undefined }], propDecorators: { creatorsCatalogPageListContainer: [{ type: i0.ViewChild, args: ['listContainer', { isSignal: true }] }] } });
1381
+
1382
+ /**
1383
+ * Logic of the creators filters modal: a draft of the phrase and the birth range, started from
1384
+ * `value` (and again on each new `value`), applied with `apply()`. Escape closes it (bound by the
1385
+ * base; a subclass must not bind it again). The museum writes the HTML.
1386
+ */
1387
+ class CreatorsFiltersBaseComponent {
1388
+ constructor() {
1389
+ /** The applied filter (`catalog.filterValue()`). */
1390
+ this.value = input(EMPTY_CREATORS_FILTER, /* @ts-ignore */
1391
+ ...(ngDevMode ? [{ debugName: "value" }] : /* istanbul ignore next */ []));
1392
+ this.phrase = linkedSignal(() => this.value().phrase, /* @ts-ignore */
1393
+ ...(ngDevMode ? [{ debugName: "phrase" }] : /* istanbul ignore next */ []));
1394
+ this.birthFrom = linkedSignal(() => this.value().birthFrom, /* @ts-ignore */
1395
+ ...(ngDevMode ? [{ debugName: "birthFrom" }] : /* istanbul ignore next */ []));
1396
+ this.birthTo = linkedSignal(() => this.value().birthTo, /* @ts-ignore */
1397
+ ...(ngDevMode ? [{ debugName: "birthTo" }] : /* istanbul ignore next */ []));
1398
+ /** A year that the API does not take, or `from` after `to`. */
1399
+ this.rangeInvalid = computed(() => !isCreatorsBirthRangeValid(this.creatorsFiltersDraft()), /* @ts-ignore */
1400
+ ...(ngDevMode ? [{ debugName: "rangeInvalid" }] : /* istanbul ignore next */ []));
1401
+ this.applied = output();
1402
+ this.closed = output();
1403
+ }
1404
+ /** Emits the trimmed draft, then `closed`; nothing while `rangeInvalid`. */
1405
+ apply() {
1406
+ if (this.rangeInvalid())
1407
+ return;
1408
+ this.applied.emit(this.creatorsFiltersDraft());
1409
+ this.closed.emit();
1410
+ }
1411
+ /** Clears the draft; the applied filter stays until `apply()`. */
1412
+ reset() {
1413
+ this.phrase.set('');
1414
+ this.birthFrom.set('');
1415
+ this.birthTo.set('');
1416
+ }
1417
+ close() {
1418
+ this.closed.emit();
1419
+ }
1420
+ creatorsFiltersDraft() {
1421
+ return {
1422
+ phrase: this.phrase().trim(),
1423
+ birthFrom: this.birthFrom().trim(),
1424
+ birthTo: this.birthTo().trim(),
1425
+ };
1426
+ }
1427
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsFiltersBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1428
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: CreatorsFiltersBaseComponent, isStandalone: true, inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { applied: "applied", closed: "closed" }, host: { listeners: { "keydown.escape": "close()" } }, ngImport: i0 }); }
1429
+ }
1430
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsFiltersBaseComponent, decorators: [{
1431
+ type: Directive,
1432
+ args: [{ host: { '(keydown.escape)': 'close()' } }]
1433
+ }], propDecorators: { value: [{ type: i0.Input, args: [{ isSignal: true, alias: "value", required: false }] }], applied: [{ type: i0.Output, args: ["applied"] }], closed: [{ type: i0.Output, args: ["closed"] }] } });
1434
+
1435
+ /**
1436
+ * Logic of a creator list card: display name, life years, roles, image with the placeholder and
1437
+ * the details link. The museum writes the HTML.
1438
+ */
1439
+ class CreatorCardBaseComponent {
1440
+ constructor() {
1441
+ /** Details path (`/tworcy/:id/:slug`); sztuki uses `'/artysci'`. Read lazily. */
1442
+ this.creatorCardPath = '/tworcy';
1443
+ /** Image when the creator has none. Read lazily. */
1444
+ this.creatorCardPlaceholder = '/gfx/no-image.png';
1445
+ this.creatorCardFileUrl = inject(FileUrlService);
1446
+ this.item = input(undefined, /* @ts-ignore */
1447
+ ...(ngDevMode ? [{ debugName: "item" }] : /* istanbul ignore next */ []));
1448
+ /** "Kobro, Katarzyna" gives "Katarzyna Kobro" (markup dropped). */
1449
+ this.displayName = computed(() => creatorDisplayName(creatorPlainName(this.item()?.name)), /* @ts-ignore */
1450
+ ...(ngDevMode ? [{ debugName: "displayName" }] : /* istanbul ignore next */ []));
1451
+ /** "1898–1951"; `''` without an item or years. */
1452
+ this.lifeYears = computed(() => {
1453
+ const item = this.item();
1454
+ return (item && creatorLifeYears(item)) || '';
1455
+ }, /* @ts-ignore */
1456
+ ...(ngDevMode ? [{ debugName: "lifeYears" }] : /* istanbul ignore next */ []));
1457
+ /** Roles, comma separated. */
1458
+ this.roles = computed(() => creatorRoles(this.item()).join(', '), /* @ts-ignore */
1459
+ ...(ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []));
1460
+ /** A string `image` as is, an `MsFile` through `FileUrlService`, else `extraData.image`, else the placeholder. */
1461
+ this.imageSrc = computed(() => {
1462
+ const item = this.item();
1463
+ if (!item)
1464
+ return '';
1465
+ const placeholder = this.creatorCardPlaceholder;
1466
+ const { image } = item;
1467
+ if (typeof image === 'string' && image.trim())
1468
+ return image;
1469
+ if (image && typeof image === 'object') {
1470
+ return this.creatorCardFileUrl.get(image, 'cache', placeholder);
1471
+ }
1472
+ return item.extraData?.image?.trim() || placeholder;
1473
+ }, /* @ts-ignore */
1474
+ ...(ngDevMode ? [{ debugName: "imageSrc" }] : /* istanbul ignore next */ []));
1475
+ /** Router commands to the details page; `[]` without an item. */
1476
+ this.link = computed(() => {
1477
+ const item = this.item();
1478
+ return item ? creatorDetailsCommands(this.creatorCardPath, item) : [];
1479
+ }, /* @ts-ignore */
1480
+ ...(ngDevMode ? [{ debugName: "link" }] : /* istanbul ignore next */ []));
1481
+ }
1482
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorCardBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1483
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.3", type: CreatorCardBaseComponent, isStandalone: true, inputs: { item: { classPropertyName: "item", publicName: "item", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
1484
+ }
1485
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorCardBaseComponent, decorators: [{
1486
+ type: Directive
1487
+ }], propDecorators: { item: [{ type: i0.Input, args: [{ isSignal: true, alias: "item", required: false }] }] } });
1488
+
1489
+ /** Four rows of three, like the works category view. */
1490
+ const CREATORS_SEARCH_LIMIT = 12;
1491
+ const CREATORS_SEARCH_PAGE_SIZES = [12, 24, 48];
1492
+ const CREATORS_SEARCH_LOADING = {
1493
+ status: 'loading',
1494
+ items: [],
1495
+ total: 0,
1496
+ };
1497
+ /**
1498
+ * Logic of the "creators" category of the site search (`/wyszukaj/:text/artysci`): the phrase of
1499
+ * the route, the page and the page size, the loaded page and the list context. The view (layout,
1500
+ * toolbar, cards, pagination) stays in the museum.
1501
+ *
1502
+ * The request depends on the phrase, the page and the limit only. A `#results` template ref is
1503
+ * scrolled to after a page change from the bottom pagination (`changePage(page, true)`), in the
1504
+ * browser only.
1505
+ */
1506
+ class CreatorsSearchListBaseComponent {
1507
+ constructor() {
1508
+ this.creatorsSearchListRoute = inject(ActivatedRoute);
1509
+ this.creatorsSearchListService = inject(CreatorsService);
1510
+ this.creatorsSearchListContext = inject(CreatorsListContext);
1511
+ this.creatorsSearchListIsBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1512
+ this.creatorsSearchListResults = viewChild('results', /* @ts-ignore */
1513
+ ...(ngDevMode ? [{ debugName: "creatorsSearchListResults" }] : /* istanbul ignore next */ []));
1514
+ this.creatorsSearchListRetry$ = new Subject();
1515
+ this.creatorsSearchListScrollAfterLoad = false;
1516
+ this.pageSizes = [...CREATORS_SEARCH_PAGE_SIZES];
1517
+ /**
1518
+ * The phrase from `wyszukaj/:text`. The category route is lazy and has no component, so `:text`
1519
+ * is not on `route.parent`: the params of the whole path are merged.
1520
+ */
1521
+ this.text = toSignal(combineLatest(this.creatorsSearchListRoute.pathFromRoot.map((route) => route.params)).pipe(map((all) => {
1522
+ const params = Object.assign({}, ...all);
1523
+ return String(params['text'] ?? '');
1524
+ }), distinctUntilChanged()), { requireSync: true });
1525
+ this.limit = signal(CREATORS_SEARCH_LIMIT, /* @ts-ignore */
1526
+ ...(ngDevMode ? [{ debugName: "limit" }] : /* istanbul ignore next */ []));
1527
+ /** 1-based; a new phrase or page size goes back to the first page. */
1528
+ this.page = linkedSignal({ ...(ngDevMode ? { debugName: "page" } : /* istanbul ignore next */ {}), source: () => ({ text: this.text(), limit: this.limit() }),
1529
+ computation: () => 1 });
1530
+ this.creatorsSearchListRequest = computed(() => ({
1531
+ text: this.text(),
1532
+ page: this.page(),
1533
+ limit: this.limit(),
1534
+ }), /* @ts-ignore */
1535
+ ...(ngDevMode ? [{ debugName: "creatorsSearchListRequest" }] : /* istanbul ignore next */ []));
1536
+ this.state = toSignal(combineLatest([
1537
+ toObservable(this.creatorsSearchListRequest),
1538
+ this.creatorsSearchListRetry$.pipe(startWith(undefined)),
1539
+ ]).pipe(switchMap(([request]) => this.creatorsSearchListLoad(request)),
1540
+ // The total stays while the next page loads, so the pagination does not vanish.
1541
+ scan((previous, next) => next.status === 'loading' ? { ...next, total: previous.total } : next, CREATORS_SEARCH_LOADING)), { initialValue: CREATORS_SEARCH_LOADING });
1542
+ this.totalCount = computed(() => this.state().total, /* @ts-ignore */
1543
+ ...(ngDevMode ? [{ debugName: "totalCount" }] : /* istanbul ignore next */ []));
1544
+ this.items = computed(() => this.state().items, /* @ts-ignore */
1545
+ ...(ngDevMode ? [{ debugName: "items" }] : /* istanbul ignore next */ []));
1546
+ // The details page reads prev/next from the shown results; a failed load clears them, as the catalog does.
1547
+ effect(() => {
1548
+ const state = this.state();
1549
+ if (state.status === 'success') {
1550
+ this.creatorsSearchListContext.set(state.items);
1551
+ }
1552
+ else if (state.status === 'error') {
1553
+ this.creatorsSearchListContext.clear();
1554
+ }
1555
+ });
1556
+ // After a page change from the bottom pagination scroll to the list top once it has loaded.
1557
+ effect(() => {
1558
+ if (this.state().status === 'loading' ||
1559
+ !this.creatorsSearchListScrollAfterLoad ||
1560
+ !this.creatorsSearchListIsBrowser) {
1561
+ return;
1562
+ }
1563
+ this.creatorsSearchListScrollAfterLoad = false;
1564
+ setTimeout(() => this.creatorsSearchListResults()?.nativeElement.scrollIntoView({
1565
+ behavior: 'smooth',
1566
+ block: 'start',
1567
+ }));
1568
+ });
1569
+ }
1570
+ /** `scrollToResults`: the change came from the bottom pagination, below the list. */
1571
+ changePage(page, scrollToResults = false) {
1572
+ this.creatorsSearchListScrollAfterLoad = scrollToResults;
1573
+ this.page.set(page);
1574
+ }
1575
+ changeLimit(limit) {
1576
+ this.limit.set(limit);
1577
+ }
1578
+ retry() {
1579
+ this.creatorsSearchListRetry$.next();
1580
+ }
1581
+ creatorsSearchListLoad({ text, page, limit, }) {
1582
+ return this.creatorsSearchListService
1583
+ .getPage({ q: text, page, maxPerPage: limit, extraData: true })
1584
+ .pipe(map(({ items, total }) => ({
1585
+ status: 'success',
1586
+ items,
1587
+ total,
1588
+ })), catchError(() => of({
1589
+ ...CREATORS_SEARCH_LOADING,
1590
+ status: 'error',
1591
+ })), startWith(CREATORS_SEARCH_LOADING));
1592
+ }
1593
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsSearchListBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
1594
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.2.0", version: "22.1.3", type: CreatorsSearchListBaseComponent, isStandalone: true, viewQueries: [{ propertyName: "creatorsSearchListResults", first: true, predicate: ["results"], descendants: true, isSignal: true }], ngImport: i0 }); }
1595
+ }
1596
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorsSearchListBaseComponent, decorators: [{
1597
+ type: Directive
1598
+ }], ctorParameters: () => [], propDecorators: { creatorsSearchListResults: [{ type: i0.ViewChild, args: ['results', { isSignal: true }] }] } });
1599
+
1600
+ /** Ossolineum's keys for the rows and the title; museum-artists' for the contents and the share (Ossolineum has neither). */
1601
+ const CREATOR_DETAILS_KEYS = {
1602
+ period: 'DETAILS_PAGE.activityPeriod',
1603
+ birth: 'DETAILS_PAGE.placeDateOfBirth',
1604
+ death: 'DETAILS_PAGE.placeDateOfDeath',
1605
+ sectionKey: 'creatorsPage',
1606
+ siteNameKey: 'museumName',
1607
+ notFoundKey: 'NOT_FOUND.title',
1608
+ biographyToc: 'ARTISTS.toc.description',
1609
+ shareCopied: 'ARTISTS.shareCopied',
1610
+ };
1611
+ /** museum-artists' strips (MOB-1963), in the order of the UX board. */
1612
+ const CREATOR_RELATED_STRIPS = [
1613
+ {
1614
+ key: 'works',
1615
+ anchor: 'dziela',
1616
+ labelKey: 'ARTISTS.related.works',
1617
+ pageSize: 6,
1618
+ limit: 18,
1619
+ },
1620
+ {
1621
+ key: 'exhibitions',
1622
+ anchor: 'wystawy',
1623
+ labelKey: 'ARTISTS.related.exhibitions',
1624
+ pageSize: 3,
1625
+ limit: 9,
1626
+ },
1627
+ {
1628
+ key: 'archives',
1629
+ anchor: 'archiwum',
1630
+ labelKey: 'ARTISTS.related.archives',
1631
+ pageSize: 3,
1632
+ limit: 9,
1633
+ },
1634
+ {
1635
+ key: 'msBooks',
1636
+ anchor: 'ksiazki-ms',
1637
+ labelKey: 'ARTISTS.related.msBooks',
1638
+ pageSize: 3,
1639
+ limit: 9,
1640
+ },
1641
+ ];
1642
+ const CREATOR_DETAILS_LOADING = {
1643
+ status: 'loading',
1644
+ creator: null,
1645
+ };
1646
+ const CREATOR_DETAILS_NOT_FOUND = {
1647
+ status: 'not-found',
1648
+ creator: null,
1649
+ };
1650
+ const CREATOR_DETAILS_NO_RELATED = {
1651
+ loading: false,
1652
+ lists: {},
1653
+ };
1654
+ const CREATOR_DETAILS_NO_OBJECTS = { items: [], total: 0 };
1655
+ const CREATOR_DETAILS_HTTP_URL = /^https?:\/\//i;
1656
+ const CREATOR_DETAILS_NUMERIC_ID = /^\d+$/;
1657
+ /** How long the address fragment keeps its position while the layout above it changes. */
1658
+ const CREATOR_DETAILS_FRAGMENT_SETTLE_MS = 2000;
1659
+ const CREATOR_DETAILS_USER_SCROLL_EVENTS = [
1660
+ 'wheel',
1661
+ 'touchstart',
1662
+ 'keydown',
1663
+ 'mousedown',
1664
+ ];
1665
+ /**
1666
+ * Logic of a creator details page (museum-artists' `ItemComponent`, MOB-1972/1963, and Ossolineum's sections): the
1667
+ * creator of the `:id` param (`creator/{id}`, else the dictionary entry), its detail rows, years, biography (folded
1668
+ * when long), portrait, lead artwork, tags, activity places, bibliography, authority links, gallery, the related
1669
+ * strips (works, exhibitions, archives, MS books) and the contents, prev/next from `CreatorsListContext`, the way back
1670
+ * to the index, share, the scroll to the address fragment and the SEO (also during SSR, via
1671
+ * `MuseumSeoService.applyForSsr`). The museum writes the HTML.
1672
+ */
1673
+ class CreatorDetailsBaseComponent {
1674
+ constructor() {
1675
+ /** Details path (`/tworcy/:id/:slug`); sztuki uses `'/artysci'`. Starts with `/`. Read lazily. */
1676
+ this.creatorDetailsPath = '/tworcy';
1677
+ /** Row labels, SEO title keys and the page texts. Read lazily. */
1678
+ this.creatorDetailsKeys = CREATOR_DETAILS_KEYS;
1679
+ /** The related strips and their limits; the `works` entry also sets `works()`. Read lazily. */
1680
+ this.creatorDetailsRelatedStrips = CREATOR_RELATED_STRIPS;
1681
+ /** `#anchor` of the biography in the contents. Read lazily. */
1682
+ this.creatorDetailsBiographyAnchor = 'opis';
1683
+ /** The objects catalog that "show all" of the works opens. Read lazily. */
1684
+ this.creatorDetailsObjectsPath = '/obiekty';
1685
+ this.creatorDetailsRoute = inject(ActivatedRoute);
1686
+ this.creatorDetailsService = inject(CreatorsService);
1687
+ this.creatorDetailsContext = inject(CreatorsListContext);
1688
+ this.creatorDetailsFileUrl = inject(FileUrlService);
1689
+ this.creatorDetailsSeo = inject(MuseumSeoService);
1690
+ this.creatorDetailsTranslate = inject(TranslateService);
1691
+ this.creatorDetailsDocument = inject(DOCUMENT);
1692
+ this.creatorDetailsScroller = inject(ViewportScroller);
1693
+ this.creatorDetailsInjector = inject(Injector);
1694
+ this.creatorDetailsIsBrowser = isPlatformBrowser(inject(PLATFORM_ID));
1695
+ this.creatorDetailsRetry$ = new Subject();
1696
+ this.creatorDetailsRelatedRequest$ = new Subject();
1697
+ this.creatorDetailsRelated = signal(CREATOR_DETAILS_NO_RELATED, /* @ts-ignore */
1698
+ ...(ngDevMode ? [{ debugName: "creatorDetailsRelated" }] : /* istanbul ignore next */ []));
1699
+ this.creatorDetailsFragment = toSignal(this.creatorDetailsRoute.fragment, { initialValue: null });
1700
+ /** `id#fragment` already scrolled to: later data does not jump again. */
1701
+ this.creatorDetailsHandledFragment = null;
1702
+ this.creatorDetailsStopFragmentScroll = () => undefined;
1703
+ this.state = toSignal(combineLatest([
1704
+ this.creatorDetailsRoute.paramMap.pipe(map((params) => params.get('id') ?? ''), distinctUntilChanged()),
1705
+ this.creatorDetailsRetry$.pipe(startWith(undefined)),
1706
+ ]).pipe(switchMap(([id]) => this.creatorDetailsLoad(id))), { initialValue: CREATOR_DETAILS_LOADING });
1707
+ this.creator = computed(() => this.state().creator, /* @ts-ignore */
1708
+ ...(ngDevMode ? [{ debugName: "creator" }] : /* istanbul ignore next */ []));
1709
+ /** museum-artists' name of `creator`. */
1710
+ this.artist = this.creator;
1711
+ /** The name without markup. */
1712
+ this.plainName = computed(() => creatorPlainName(this.creator()?.name), /* @ts-ignore */
1713
+ ...(ngDevMode ? [{ debugName: "plainName" }] : /* istanbul ignore next */ []));
1714
+ /** "Kobro, Katarzyna" gives "Katarzyna Kobro", as in the page heading. */
1715
+ this.name = computed(() => creatorDisplayName(this.plainName()), /* @ts-ignore */
1716
+ ...(ngDevMode ? [{ debugName: "name" }] : /* istanbul ignore next */ []));
1717
+ /** Years under the name (`creatorYears`), `''` without dates. */
1718
+ this.years = computed(() => {
1719
+ const creator = this.creator();
1720
+ return creator ? creatorYears(creator) : '';
1721
+ }, /* @ts-ignore */
1722
+ ...(ngDevMode ? [{ debugName: "years" }] : /* istanbul ignore next */ []));
1723
+ /** Roles, comma separated; a numeric role id is never shown. */
1724
+ this.roles = computed(() => creatorRoles(this.creator()).join(', '), /* @ts-ignore */
1725
+ ...(ngDevMode ? [{ debugName: "roles" }] : /* istanbul ignore next */ []));
1726
+ /** Period, birth and death (museum-artists' order), empty values dropped. */
1727
+ this.details = computed(() => {
1728
+ const creator = this.creator();
1729
+ const keys = this.creatorDetailsKeys;
1730
+ const rows = [
1731
+ { labelKey: keys.period, value: creatorDictLabel(creator?.period) },
1732
+ { labelKey: keys.birth, value: creator?.placeDateOfBirth ?? '' },
1733
+ { labelKey: keys.death, value: creator?.placeDateOfDeath ?? '' },
1734
+ ];
1735
+ return rows.filter((row) => !!row.value);
1736
+ }, /* @ts-ignore */
1737
+ ...(ngDevMode ? [{ debugName: "details" }] : /* istanbul ignore next */ []));
1738
+ /**
1739
+ * The editorial biography, else the biography, else the description html, trimmed (`creatorBiography`); a field
1740
+ * that only looks filled (`<p>&nbsp;</p>`, Word's MSO blocks) is skipped; `''` without any.
1741
+ */
1742
+ this.biography = computed(() => creatorBiography(this.creator()), /* @ts-ignore */
1743
+ ...(ngDevMode ? [{ debugName: "biography" }] : /* istanbul ignore next */ []));
1744
+ /** A biography longer than the folded view (about six lines). */
1745
+ this.foldable = computed(() => creatorIsLongBiography(this.biography()), /* @ts-ignore */
1746
+ ...(ngDevMode ? [{ debugName: "foldable" }] : /* istanbul ignore next */ []));
1747
+ /** Plain-text start of the biography for the folded view. */
1748
+ this.excerpt = computed(() => creatorBiographyExcerpt(this.biography()), /* @ts-ignore */
1749
+ ...(ngDevMode ? [{ debugName: "excerpt" }] : /* istanbul ignore next */ []));
1750
+ /** The folded biography is unfolded; folded again for another biography. */
1751
+ this.expanded = linkedSignal({ ...(ngDevMode ? { debugName: "expanded" } : /* istanbul ignore next */ {}), source: this.biography,
1752
+ computation: () => false });
1753
+ /** Portrait (`photo`): a ready `url`, else a MUZA file; `null` without one. */
1754
+ this.portrait = computed(() => this.creatorDetailsImageOf(this.creator()?.photo), /* @ts-ignore */
1755
+ ...(ngDevMode ? [{ debugName: "portrait" }] : /* istanbul ignore next */ []));
1756
+ /** Lead artwork (`artworkFrame`), as `portrait`. */
1757
+ this.leadImage = computed(() => this.creatorDetailsImageOf(this.creator()?.artworkFrame), /* @ts-ignore */
1758
+ ...(ngDevMode ? [{ debugName: "leadImage" }] : /* istanbul ignore next */ []));
1759
+ /** Tags with a name. */
1760
+ this.tags = computed(() => (this.creator()?.tags ?? []).filter((tag) => !!tag?.name?.trim()), /* @ts-ignore */
1761
+ ...(ngDevMode ? [{ debugName: "tags" }] : /* istanbul ignore next */ []));
1762
+ /**
1763
+ * Per activity period: `from-to` years and the location names, entries with neither dropped. As in
1764
+ * Ossolineum, `[]` when no period has locations; otherwise every period, years-only ones too.
1765
+ */
1766
+ this.activityPlaces = computed(() => {
1767
+ const places = (this.creator()?.activityPeriod ?? [])
1768
+ .map((period) => ({
1769
+ years: [period?.fromYear, period?.toYear]
1770
+ // The API may send numbers, as it does for `id` and `role`.
1771
+ .map((year) => String(year ?? '').trim())
1772
+ .filter(Boolean)
1773
+ .join('-'),
1774
+ places: (period?.locations ?? [])
1775
+ .map((location) => creatorPlainName(location?.name))
1776
+ .filter(Boolean)
1777
+ .join(', '),
1778
+ }))
1779
+ .filter((place) => place.years || place.places);
1780
+ return places.some((place) => place.places) ? places : [];
1781
+ }, /* @ts-ignore */
1782
+ ...(ngDevMode ? [{ debugName: "activityPlaces" }] : /* istanbul ignore next */ []));
1783
+ /** Bibliography entries as plain text (`text`, else `html`), empty ones dropped. */
1784
+ this.bibliography = computed(() => (this.creator()?.bibliographies ?? [])
1785
+ .map((entry) => creatorPlainName(entry?.text || entry?.html))
1786
+ .filter(Boolean), /* @ts-ignore */
1787
+ ...(ngDevMode ? [{ debugName: "bibliography" }] : /* istanbul ignore next */ []));
1788
+ /** `identifier [base]` links of the authority files; only `http(s)://` URLs. */
1789
+ this.authorityLinks = computed(() => (this.creator()?.baseConnectionObjects ?? []).reduce((links, entry) => {
1790
+ const url = entry?.bcUrl?.trim() ?? '';
1791
+ if (!CREATOR_DETAILS_HTTP_URL.test(url))
1792
+ return links;
1793
+ const base = creatorPlainName(entry?.bcBase?.name);
1794
+ const label = [
1795
+ creatorPlainName(entry?.bcIdentifier),
1796
+ base ? `[${base}]` : '',
1797
+ ]
1798
+ .filter(Boolean)
1799
+ .join(' ');
1800
+ links.push({ label: label || url, url });
1801
+ return links;
1802
+ }, []), /* @ts-ignore */
1803
+ ...(ngDevMode ? [{ debugName: "authorityLinks" }] : /* istanbul ignore next */ []));
1804
+ /** Signature objects, else one image of the creator, else `[]`. */
1805
+ this.gallery = computed(() => this.creatorDetailsGalleryOf(this.creator()), /* @ts-ignore */
1806
+ ...(ngDevMode ? [{ debugName: "gallery" }] : /* istanbul ignore next */ []));
1807
+ /** The first works of the creator (its id only, no list filter), from the `works` strip entry. */
1808
+ this.works = computed(() => this.creatorDetailsRelated().lists.works?.items ?? [], /* @ts-ignore */
1809
+ ...(ngDevMode ? [{ debugName: "works" }] : /* istanbul ignore next */ []));
1810
+ this.worksTotal = computed(() => this.creatorDetailsRelated().lists.works?.total ?? 0, /* @ts-ignore */
1811
+ ...(ngDevMode ? [{ debugName: "worksTotal" }] : /* istanbul ignore next */ []));
1812
+ /** Also `true` while the creator loads: the related lists are requested once it has loaded. */
1813
+ this.worksLoading = computed(() => this.state().status === 'loading' || this.creatorDetailsRelated().loading, /* @ts-ignore */
1814
+ ...(ngDevMode ? [{ debugName: "worksLoading" }] : /* istanbul ignore next */ []));
1815
+ /** `?filter=` of the objects list with this creator, for the "all works" link. */
1816
+ this.worksListQueryParams = computed(() => {
1817
+ const creator = this.creator();
1818
+ if (!creator)
1819
+ return null;
1820
+ // Dictionary ids of the objects catalog are numbers; the service keys creators on strings.
1821
+ const id = CREATOR_DETAILS_NUMERIC_ID.test(creator.id)
1822
+ ? Number(creator.id)
1823
+ : creator.id;
1824
+ return catalogEntryFilterParams('creators', id, this.plainName());
1825
+ }, /* @ts-ignore */
1826
+ ...(ngDevMode ? [{ debugName: "worksListQueryParams" }] : /* istanbul ignore next */ []));
1827
+ /**
1828
+ * The related strips with results, in the configured order (an empty strip is left out, with its contents
1829
+ * entry); `[]` while they load. Each item goes through `creatorDetailsRelatedItem`.
1830
+ */
1831
+ this.strips = computed(() => {
1832
+ const { lists } = this.creatorDetailsRelated();
1833
+ return this.creatorDetailsRelatedStrips.reduce((strips, { key, anchor, labelKey, pageSize }) => {
1834
+ const list = lists[key];
1835
+ if (!list?.items.length)
1836
+ return strips;
1837
+ const query = key === 'works' ? this.worksListQueryParams() : null;
1838
+ strips.push({
1839
+ key,
1840
+ anchor,
1841
+ labelKey,
1842
+ pageSize,
1843
+ count: list.total,
1844
+ items: list.items.map((item) => this.creatorDetailsRelatedItem(item, key)),
1845
+ showAll: query
1846
+ ? { commands: [this.creatorDetailsObjectsPath], queryParams: query }
1847
+ : null,
1848
+ });
1849
+ return strips;
1850
+ }, []);
1851
+ }, /* @ts-ignore */
1852
+ ...(ngDevMode ? [{ debugName: "strips" }] : /* istanbul ignore next */ []));
1853
+ /** The contents: the biography and the strips the page has. */
1854
+ this.toc = computed(() => [
1855
+ ...(this.biography()
1856
+ ? [
1857
+ {
1858
+ anchor: this.creatorDetailsBiographyAnchor,
1859
+ labelKey: this.creatorDetailsKeys.biographyToc,
1860
+ },
1861
+ ]
1862
+ : []),
1863
+ ...this.strips().map(({ anchor, labelKey }) => ({ anchor, labelKey })),
1864
+ ], /* @ts-ignore */
1865
+ ...(ngDevMode ? [{ debugName: "toc" }] : /* istanbul ignore next */ []));
1866
+ /** Read out after `share()` copied the link. */
1867
+ this.shareStatus = signal('', /* @ts-ignore */
1868
+ ...(ngDevMode ? [{ debugName: "shareStatus" }] : /* istanbul ignore next */ []));
1869
+ /** Prev/next from the last shown list; none for a deep link, SSR or a creator outside it. */
1870
+ this.neighbours = computed(() => this.creatorDetailsContext.neighbours(this.creator()?.id), /* @ts-ignore */
1871
+ ...(ngDevMode ? [{ debugName: "neighbours" }] : /* istanbul ignore next */ []));
1872
+ this.prevCommands = computed(() => {
1873
+ const prev = this.neighbours().prev;
1874
+ return prev ? creatorDetailsCommands(this.creatorDetailsPath, prev) : null;
1875
+ }, /* @ts-ignore */
1876
+ ...(ngDevMode ? [{ debugName: "prevCommands" }] : /* istanbul ignore next */ []));
1877
+ this.nextCommands = computed(() => {
1878
+ const next = this.neighbours().next;
1879
+ return next ? creatorDetailsCommands(this.creatorDetailsPath, next) : null;
1880
+ }, /* @ts-ignore */
1881
+ ...(ngDevMode ? [{ debugName: "nextCommands" }] : /* istanbul ignore next */ []));
1882
+ /** `[path, id, slug]` of the loaded creator; `[]` without one. */
1883
+ this.canonicalCommands = computed(() => {
1884
+ const creator = this.creator();
1885
+ return creator
1886
+ ? creatorDetailsCommands(this.creatorDetailsPath, creator)
1887
+ : [];
1888
+ }, /* @ts-ignore */
1889
+ ...(ngDevMode ? [{ debugName: "canonicalCommands" }] : /* istanbul ignore next */ []));
1890
+ /** The index letter of the creator; `''` without one. */
1891
+ this.backLetter = computed(() => {
1892
+ const creator = this.creator();
1893
+ return creator ? creatorsLetterOf(creator.name ?? '') : '';
1894
+ }, /* @ts-ignore */
1895
+ ...(ngDevMode ? [{ debugName: "backLetter" }] : /* istanbul ignore next */ []));
1896
+ /** The two-letter group of the creator in the index (MOB-1959); `''` without one. */
1897
+ this.backPrefix = computed(() => creatorsIndexPrefixOf(this.creator()?.name ?? ''), /* @ts-ignore */
1898
+ ...(ngDevMode ? [{ debugName: "backPrefix" }] : /* istanbul ignore next */ []));
1899
+ /** The index opens on the creator's letter and two-letter group (MOB-1959). */
1900
+ this.backQueryParams = computed(() => {
1901
+ if (!this.creator())
1902
+ return {};
1903
+ const params = {
1904
+ [CREATORS_INDEX_LETTER_PARAM]: this.backLetter(),
1905
+ };
1906
+ const prefix = this.backPrefix();
1907
+ // Like sztuki (`backPrefix() || null`): no empty `prefiks=`.
1908
+ if (prefix)
1909
+ params[CREATORS_INDEX_PREFIX_PARAM] = prefix;
1910
+ return params;
1911
+ }, /* @ts-ignore */
1912
+ ...(ngDevMode ? [{ debugName: "backQueryParams" }] : /* istanbul ignore next */ []));
1913
+ /** Ossolineum's section accordions; both open at first. */
1914
+ this.biographyOpen = signal(true, /* @ts-ignore */
1915
+ ...(ngDevMode ? [{ debugName: "biographyOpen" }] : /* istanbul ignore next */ []));
1916
+ this.bibliographyOpen = signal(true, /* @ts-ignore */
1917
+ ...(ngDevMode ? [{ debugName: "bibliographyOpen" }] : /* istanbul ignore next */ []));
1918
+ this.creatorDetailsSeo.applyForSsr(() => this.creatorDetailsFetchSsrSeo());
1919
+ const destroyRef = inject(DestroyRef);
1920
+ destroyRef.onDestroy(() => this.creatorDetailsStopFragmentScroll());
1921
+ this.creatorDetailsRelatedRequest$
1922
+ .pipe(distinctUntilChanged((a, b) => a?.id === b?.id), switchMap((creator) => this.creatorDetailsLoadRelated(creator)), takeUntilDestroyed(destroyRef))
1923
+ .subscribe((related) => this.creatorDetailsRelated.set(related));
1924
+ effect(() => {
1925
+ const creator = this.creator();
1926
+ untracked(() => this.creatorDetailsRelatedRequest$.next(creator));
1927
+ });
1928
+ // The router scrolls to the fragment on NavigationEnd, before the biography and the strips load:
1929
+ // `/artysci/:id#dziela` (back, an outside link) is scrolled here once its part is in the contents.
1930
+ effect(() => {
1931
+ const fragment = this.creatorDetailsFragment();
1932
+ const id = this.creator()?.id;
1933
+ if (!fragment)
1934
+ this.creatorDetailsHandledFragment = null;
1935
+ if (!fragment || !id)
1936
+ return;
1937
+ const key = `${id}#${fragment}`;
1938
+ if (key === this.creatorDetailsHandledFragment)
1939
+ return;
1940
+ if (!this.toc().some(({ anchor }) => anchor === fragment))
1941
+ return;
1942
+ this.creatorDetailsHandledFragment = key;
1943
+ afterNextRender({ read: () => this.creatorDetailsScrollToFragment(fragment) }, { injector: this.creatorDetailsInjector });
1944
+ });
1945
+ effect(() => {
1946
+ const { status, creator } = this.state();
1947
+ untracked(() => {
1948
+ if (creator) {
1949
+ this.creatorDetailsSeo.apply(this.creatorDetailsSeoOf(creator));
1950
+ }
1951
+ else if (status === 'not-found') {
1952
+ const keys = this.creatorDetailsKeys;
1953
+ this.creatorDetailsSeo.apply({
1954
+ title: `${this.creatorDetailsT(keys.notFoundKey)} - ${this.creatorDetailsT(keys.siteNameKey)}`,
1955
+ });
1956
+ }
1957
+ });
1958
+ });
1959
+ }
1960
+ retry() {
1961
+ this.creatorDetailsRetry$.next();
1962
+ }
1963
+ /** Unfolds or folds the long biography (museum-artists' "rozwiń tekst"). */
1964
+ toggleBiography() {
1965
+ this.expanded.update((value) => !value);
1966
+ }
1967
+ /** Opens or closes Ossolineum's biography accordion. */
1968
+ toggleBiographyOpen() {
1969
+ this.biographyOpen.update((open) => !open);
1970
+ }
1971
+ toggleBibliography() {
1972
+ this.bibliographyOpen.update((open) => !open);
1973
+ }
1974
+ /** The system share, else the link to the clipboard (announced in `shareStatus`). Browser only. */
1975
+ async share() {
1976
+ const view = this.creatorDetailsDocument.defaultView;
1977
+ if (!this.creatorDetailsIsBrowser || !view)
1978
+ return;
1979
+ const url = view.location.href;
1980
+ const navigator = view.navigator;
1981
+ try {
1982
+ if (typeof navigator.share === 'function') {
1983
+ await navigator.share({ title: this.name(), url });
1984
+ return;
1985
+ }
1986
+ await navigator.clipboard.writeText(url);
1987
+ this.shareStatus.set(this.creatorDetailsT(this.creatorDetailsKeys.shareCopied));
1988
+ }
1989
+ catch {
1990
+ // A cancelled share sheet or no clipboard access: no message.
1991
+ }
1992
+ }
1993
+ /** A strip item as the museum shows it (sztuki: a result card); the object itself by default. */
1994
+ creatorDetailsRelatedItem(item, key) {
1995
+ void key;
1996
+ return item;
1997
+ }
1998
+ /**
1999
+ * The `<title>` of a creator: `plainName - t(sectionKey) - t(siteNameKey)` (the section left out for a `null`
2000
+ * `sectionKey`). Override to keep another format; sztuki keeps `name() - t('museumName')`. Called lazily, for the
2001
+ * page and on the server.
2002
+ */
2003
+ creatorDetailsSeoTitle(creator) {
2004
+ const keys = this.creatorDetailsKeys;
2005
+ return [
2006
+ creatorPlainName(creator.name),
2007
+ keys.sectionKey ? this.creatorDetailsT(keys.sectionKey) : '',
2008
+ this.creatorDetailsT(keys.siteNameKey),
2009
+ ]
2010
+ .filter(Boolean)
2011
+ .join(' - ');
2012
+ }
2013
+ creatorDetailsLoad(id) {
2014
+ return this.creatorDetailsService.getById(id).pipe(map((creator) => creator ? { status: 'success', creator } : CREATOR_DETAILS_NOT_FOUND), catchError(() => of({ status: 'error', creator: null })), startWith(CREATOR_DETAILS_LOADING));
2015
+ }
2016
+ /**
2017
+ * Runs from the effect, after construction, so the strip overrides are in place. A list that fails is empty;
2018
+ * the rest of the page stays.
2019
+ */
2020
+ creatorDetailsLoadRelated(creator) {
2021
+ const strips = this.creatorDetailsRelatedStrips;
2022
+ if (!creator || !strips.length)
2023
+ return of(CREATOR_DETAILS_NO_RELATED);
2024
+ return combineLatest(strips.map(({ key, limit }) => (key === 'works'
2025
+ ? this.creatorDetailsService.getWorks(creator.id, limit)
2026
+ : this.creatorDetailsService.getRelated(creator[key], limit)).pipe(catchError(() => of(CREATOR_DETAILS_NO_OBJECTS)), map((list) => [key, list])))).pipe(map((entries) => ({
2027
+ loading: false,
2028
+ lists: entries.reduce((lists, [key, list]) => ({ ...lists, [key]: list }), {}),
2029
+ })), startWith({
2030
+ ...CREATOR_DETAILS_NO_RELATED,
2031
+ loading: true,
2032
+ }));
2033
+ }
2034
+ /**
2035
+ * Scrolls to the anchor and holds it there for a moment: the lead artwork and the strips may still change the
2036
+ * height above it. The user's own scroll ends the corrections. Runs after render, in the browser only.
2037
+ */
2038
+ creatorDetailsScrollToFragment(fragment) {
2039
+ this.creatorDetailsStopFragmentScroll();
2040
+ this.creatorDetailsScroller.scrollToAnchor(fragment);
2041
+ const view = this.creatorDetailsDocument.defaultView;
2042
+ if (!view || typeof view.ResizeObserver === 'undefined')
2043
+ return;
2044
+ const observer = new view.ResizeObserver(() => this.creatorDetailsScroller.scrollToAnchor(fragment));
2045
+ const stop = () => {
2046
+ observer.disconnect();
2047
+ view.clearTimeout(timer);
2048
+ CREATOR_DETAILS_USER_SCROLL_EVENTS.forEach((name) => view.removeEventListener(name, stop));
2049
+ this.creatorDetailsStopFragmentScroll = () => undefined;
2050
+ };
2051
+ const timer = view.setTimeout(stop, CREATOR_DETAILS_FRAGMENT_SETTLE_MS);
2052
+ observer.observe(this.creatorDetailsDocument.body);
2053
+ CREATOR_DETAILS_USER_SCROLL_EVENTS.forEach((name) => view.addEventListener(name, stop, { passive: true }));
2054
+ this.creatorDetailsStopFragmentScroll = stop;
2055
+ }
2056
+ /** `creator/{id}` gives a ready `url`; a MUZA file (`filePath`) goes through `FileUrlService`. */
2057
+ creatorDetailsImageOf(file) {
2058
+ if (!file)
2059
+ return null;
2060
+ const url = 'url' in file ? (file.url?.trim() ?? '') : '';
2061
+ const src = url ||
2062
+ ('filePath' in file && file.filePath
2063
+ ? this.creatorDetailsFileUrl.get(file, 'cache')
2064
+ : '');
2065
+ return src ? { src, alt: file.altText || '' } : null;
2066
+ }
2067
+ creatorDetailsGalleryOf(creator) {
2068
+ if (!creator)
2069
+ return [];
2070
+ const name = creatorPlainName(creator.name);
2071
+ const signatures = (creator.signatureObjects ?? []).reduce((images, entry) => {
2072
+ const file = entry?.image ?? entry?.file;
2073
+ if (!file)
2074
+ return images;
2075
+ const title = creatorPlainName(entry?.title);
2076
+ images.push({
2077
+ src: this.creatorDetailsFileUrl.get(file, 'cache'),
2078
+ alt: title || name,
2079
+ ...(title ? { title } : {}),
2080
+ });
2081
+ return images;
2082
+ }, []);
2083
+ if (signatures.length)
2084
+ return signatures;
2085
+ const extra = creator.extraData?.image?.trim();
2086
+ if (extra)
2087
+ return [{ src: extra, alt: name }];
2088
+ const { image } = creator;
2089
+ if (typeof image === 'string' && image.trim()) {
2090
+ return [{ src: image.trim(), alt: name }];
2091
+ }
2092
+ if (image && typeof image === 'object') {
2093
+ return [
2094
+ { src: this.creatorDetailsFileUrl.get(image, 'cache'), alt: name },
2095
+ ];
2096
+ }
2097
+ return [];
2098
+ }
2099
+ /** `creatorDetailsSeoTitle`, the plain biography (`creatorBiography`), the first image, the canonical. */
2100
+ creatorDetailsSeoOf(creator) {
2101
+ const title = this.creatorDetailsSeoTitle(creator);
2102
+ const url = creatorDetailsCommands(this.creatorDetailsPath, creator).join('/');
2103
+ const metadata = {
2104
+ title,
2105
+ description: creatorPlainText(creatorBiography(creator)) || undefined,
2106
+ // A path override without the leading slash still gives an absolute canonical.
2107
+ url: url.startsWith('/') ? url : `/${url}`,
2108
+ type: 'article',
2109
+ };
2110
+ const image = this.creatorDetailsGalleryOf(creator)[0]?.src;
2111
+ if (image)
2112
+ metadata.image = image;
2113
+ return metadata;
2114
+ }
2115
+ creatorDetailsFetchSsrSeo() {
2116
+ const id = this.creatorDetailsRoute.snapshot.paramMap.get('id') ?? '';
2117
+ return firstValueFrom(this.creatorDetailsService.getById(id))
2118
+ .then((creator) => (creator ? this.creatorDetailsSeoOf(creator) : null))
2119
+ .catch(() => null);
2120
+ }
2121
+ creatorDetailsT(key) {
2122
+ return String(this.creatorDetailsTranslate.instant(key) ?? '');
2123
+ }
2124
+ /** @nocollapse */ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorDetailsBaseComponent, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
2125
+ /** @nocollapse */ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "22.1.3", type: CreatorDetailsBaseComponent, isStandalone: true, ngImport: i0 }); }
2126
+ }
2127
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.3", ngImport: i0, type: CreatorDetailsBaseComponent, decorators: [{
2128
+ type: Directive
2129
+ }], ctorParameters: () => [] });
2130
+
2131
+ /**
2132
+ * Generated bundle index. Do not edit.
2133
+ */
2134
+
2135
+ export { CREATORS_ALPHABET, CREATORS_CATALOG_DEFAULTS, CREATORS_HASH_PREFIXES, CREATORS_INDEX_LETTER_PARAM, CREATORS_INDEX_PREFIX_PARAM, CREATORS_SEARCH_LIMIT, CREATORS_SEARCH_PAGE_SIZES, CREATOR_DETAILS_KEYS, CREATOR_RELATED_STRIPS, CreatorCardBaseComponent, CreatorDetailsBaseComponent, CreatorsCatalogBaseComponent, CreatorsFiltersBaseComponent, CreatorsIndexBaseComponent, CreatorsJumpToBaseComponent, CreatorsListContext, CreatorsSearchListBaseComponent, CreatorsService, EMPTY_CREATORS_FILTER, createCreatorsCatalog, createCreatorsFacade, creatorBiography, creatorBiographyExcerpt, creatorDetailsCommands, creatorDictLabel, creatorDictionaryUrl, creatorDisplayName, creatorIsLongBiography, creatorLifeYears, creatorNfdLetter, creatorPlainName, creatorPlainText, creatorProfileUrl, creatorRelatedUrl, creatorRoles, creatorSlug, creatorWorksUrl, creatorYears, creatorsDisplayPrefix, creatorsFilterQueryParams, creatorsFilterToQuery, creatorsFilterValueOf, creatorsFindPrefix, creatorsIndexPrefixOf, creatorsLetterOf, creatorsListSeoMetadata, creatorsListUrl, creatorsPrefixAnchor, creatorsPrefixOf, creatorsPrefixesUrl, groupCreatorsByLetter, groupCreatorsByPrefix, hasCreatorsFilter, isCreatorsBirthRangeValid, isCreatorsBirthYear, parseCreatorsLetter, resolveCreatorsFragment };
2136
+ //# sourceMappingURL=smartsoft001-mobilems-creators-shell-angular.mjs.map