@internetarchive/collection-browser 0.3.1-alpha.3 → 0.3.2-alpha.1

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.
Files changed (30) hide show
  1. package/dist/src/collection-browser.d.ts +6 -0
  2. package/dist/src/collection-browser.js +346 -341
  3. package/dist/src/collection-browser.js.map +1 -1
  4. package/dist/src/collection-facets/facets-template.js +150 -150
  5. package/dist/src/collection-facets/facets-template.js.map +1 -1
  6. package/dist/src/collection-facets/more-facets-content.js +134 -134
  7. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  8. package/dist/src/collection-facets.d.ts +2 -0
  9. package/dist/src/collection-facets.js +158 -147
  10. package/dist/src/collection-facets.js.map +1 -1
  11. package/dist/src/models.js.map +1 -1
  12. package/dist/src/restoration-state-handler.js.map +1 -1
  13. package/dist/src/tiles/list/tile-list.js +204 -204
  14. package/dist/src/tiles/list/tile-list.js.map +1 -1
  15. package/dist/src/utils/format-count.js.map +1 -1
  16. package/dist/test/collection-browser.test.js +26 -26
  17. package/dist/test/collection-browser.test.js.map +1 -1
  18. package/dist/test/collection-facets.test.js +2 -2
  19. package/dist/test/collection-facets.test.js.map +1 -1
  20. package/package.json +1 -1
  21. package/src/collection-browser.ts +1539 -1530
  22. package/src/collection-facets/facets-template.ts +294 -294
  23. package/src/collection-facets/more-facets-content.ts +518 -518
  24. package/src/collection-facets.ts +582 -569
  25. package/src/models.ts +216 -216
  26. package/src/restoration-state-handler.ts +302 -302
  27. package/src/tiles/list/tile-list.ts +509 -509
  28. package/src/utils/format-count.ts +96 -96
  29. package/test/collection-browser.test.ts +490 -490
  30. package/test/collection-facets.test.ts +510 -510
@@ -1,518 +1,518 @@
1
- /* eslint-disable dot-notation */
2
- /* eslint-disable lit-a11y/click-events-have-key-events */
3
- import {
4
- css,
5
- CSSResultGroup,
6
- html,
7
- LitElement,
8
- nothing,
9
- PropertyValues,
10
- TemplateResult,
11
- } from 'lit';
12
- import { customElement, property, state } from 'lit/decorators.js';
13
- import {
14
- Aggregation,
15
- Bucket,
16
- SearchServiceInterface,
17
- SearchParams,
18
- SearchType,
19
- AggregationSortType,
20
- } from '@internetarchive/search-service';
21
- import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
22
- import type { ModalManagerInterface } from '@internetarchive/modal-manager';
23
- import {
24
- SelectedFacets,
25
- FacetGroup,
26
- FacetBucket,
27
- FacetOption,
28
- facetTitles,
29
- } from '../models';
30
- import type { LanguageCodeHandlerInterface } from '../language-code-handler/language-code-handler';
31
- import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
32
- import './more-facets-pagination';
33
- import './facets-template';
34
-
35
- @customElement('more-facets-content')
36
- export class MoreFacetsContent extends LitElement {
37
- @property({ type: String }) facetKey?: string;
38
-
39
- @property({ type: String }) facetAggregationKey?: string;
40
-
41
- @property({ type: String }) fullQuery?: string;
42
-
43
- @property({ type: Object }) modalManager?: ModalManagerInterface;
44
-
45
- @property({ type: Object }) searchService?: SearchServiceInterface;
46
-
47
- @property({ type: String }) searchType?: SearchType;
48
-
49
- @property({ type: Object })
50
- collectionNameCache?: CollectionNameCacheInterface;
51
-
52
- @property({ type: Object })
53
- languageCodeHandler?: LanguageCodeHandlerInterface;
54
-
55
- @property({ type: Object }) selectedFacets?: SelectedFacets;
56
-
57
- @property({ type: String }) sortedBy = 'count'; // count | alpha
58
-
59
- @state() aggregations?: Record<string, Aggregation>;
60
-
61
- @state() facetGroup?: FacetGroup[] = [];
62
-
63
- @state() facetGroupTitle?: String = '';
64
-
65
- @state() pageNumber = 1;
66
-
67
- /**
68
- * Facets are loading on popup
69
- */
70
- @state() facetsLoading = true;
71
-
72
- @state() paginationSize = 0;
73
-
74
- @state() facetsType = 'modal';
75
-
76
- private facetsPerPage = 35;
77
-
78
- updated(changed: PropertyValues) {
79
- if (changed.has('facetKey')) {
80
- this.facetsLoading = true;
81
- this.pageNumber = 1;
82
-
83
- this.updateSpecificFacets();
84
- }
85
-
86
- if (changed.has('pageNumber')) {
87
- this.facetGroup = this.aggregationFacetGroups;
88
- }
89
- }
90
-
91
- firstUpdated() {
92
- this.setupEscapeListeners();
93
- }
94
-
95
- /**
96
- * Close more facets modal on Escape click
97
- */
98
- private setupEscapeListeners() {
99
- if (this.modalManager) {
100
- document.addEventListener('keydown', (e: KeyboardEvent) => {
101
- if (e.key === 'Escape') {
102
- this.modalManager?.closeModal();
103
- }
104
- });
105
- } else {
106
- document.removeEventListener('keydown', () => {});
107
- }
108
- }
109
-
110
- /**
111
- * Get specific facets data from search-service API based of currently query params
112
- * - this.aggregations - hold result of search service and being used for further processing.
113
- */
114
- async updateSpecificFacets(): Promise<void> {
115
- const aggregations = {
116
- simpleParams: [this.facetAggregationKey as string],
117
- };
118
- const aggregationsSize = 65535; // todo - do we want to have all the records at once?
119
-
120
- const params: SearchParams = {
121
- query: this.fullQuery as string,
122
- aggregations,
123
- aggregationsSize,
124
- rows: 0, // todo - do we want server-side pagination with offset/page/limit flag?
125
- };
126
-
127
- const results = await this.searchService?.search(params, this.searchType);
128
- this.aggregations = results?.success?.response.aggregations;
129
-
130
- this.facetGroup = this.aggregationFacetGroups;
131
- this.facetsLoading = false;
132
- }
133
-
134
- private pageNumberClicked(e: CustomEvent<{ page: number }>) {
135
- const page = e?.detail?.page;
136
- if (page) {
137
- this.pageNumber = Number(page);
138
- }
139
- }
140
-
141
- /**
142
- * Combines the selected facets with the aggregations to create a single list of facets
143
- */
144
- private get mergedFacets(): FacetGroup[] | void {
145
- const facetGroups: FacetGroup[] = [];
146
-
147
- const selectedFacetGroup = this.selectedFacetGroups.find(
148
- group => group.key === this.facetKey
149
- );
150
- const aggregateFacetGroup = this.aggregationFacetGroups.find(
151
- group => group.key === this.facetKey
152
- );
153
-
154
- // if the user selected a facet, but it's not in the aggregation, we add it as-is
155
- if (selectedFacetGroup && !aggregateFacetGroup) {
156
- facetGroups.push(selectedFacetGroup);
157
- return facetGroups;
158
- }
159
-
160
- // if we don't have an aggregate facet group, don't add this to the list
161
- if (!aggregateFacetGroup) return facetGroups;
162
-
163
- // start with either the selected group if we have one, or the aggregate group
164
- const facetGroup = selectedFacetGroup ?? aggregateFacetGroup;
165
-
166
- // attach the counts to the selected buckets
167
- const bucketsWithCount =
168
- selectedFacetGroup?.buckets.map(bucket => {
169
- const selectedBucket = aggregateFacetGroup.buckets.find(
170
- b => b.key === bucket.key
171
- );
172
- return selectedBucket
173
- ? {
174
- ...bucket,
175
- count: selectedBucket.count,
176
- }
177
- : bucket;
178
- }) ?? [];
179
-
180
- // append any additional buckets that were not selected
181
- aggregateFacetGroup.buckets.forEach(bucket => {
182
- const existingBucket = bucketsWithCount.find(b => b.key === bucket.key);
183
- if (existingBucket) return;
184
- bucketsWithCount.push(bucket);
185
- });
186
- facetGroup.buckets = bucketsWithCount;
187
-
188
- facetGroups.push(facetGroup);
189
- return facetGroups;
190
- }
191
-
192
- /**
193
- * Converts the selected facets to a `FacetGroup` array,
194
- * which is easier to work with
195
- */
196
- private get selectedFacetGroups(): FacetGroup[] {
197
- if (!this.selectedFacets) return [];
198
-
199
- const facetGroups: FacetGroup[] = Object.entries(this.selectedFacets).map(
200
- ([key, selectedFacets]) => {
201
- const option = key as FacetOption;
202
- const title = facetTitles[option];
203
-
204
- const buckets: FacetBucket[] = Object.entries(selectedFacets).map(
205
- ([value, data]) => {
206
- let displayText: string = value;
207
- // for selected languages, we store the language code instead of the
208
- // display name, so look up the name from the mapping
209
- if (option === 'language') {
210
- displayText =
211
- this.languageCodeHandler?.getLanguageNameFromCodeString(
212
- value
213
- ) ?? value;
214
- }
215
- return {
216
- displayText,
217
- key: value,
218
- count: data?.count,
219
- state: data?.state,
220
- };
221
- }
222
- );
223
-
224
- return {
225
- title,
226
- key: option,
227
- buckets,
228
- };
229
- }
230
- );
231
-
232
- return facetGroups;
233
- }
234
-
235
- /**
236
- * Converts the raw `aggregations` to `FacetGroups`, which are easier to use
237
- */
238
- private get aggregationFacetGroups(): FacetGroup[] {
239
- const facetGroups: FacetGroup[] = [];
240
- Object.entries(this.aggregations ?? []).forEach(([key, aggregation]) => {
241
- // the year_histogram data is in a different format so can't be handled here
242
- if (key === 'year_histogram') return;
243
-
244
- const option = key as FacetOption;
245
- this.facetGroupTitle = facetTitles[option];
246
-
247
- // sort facets in specific order
248
- let castedBuckets = aggregation.getSortedBuckets(
249
- this.sortedBy === 'alpha'
250
- ? AggregationSortType.ALPHABETICAL
251
- : AggregationSortType.COUNT
252
- ) as Bucket[];
253
-
254
- if (option === 'collection') {
255
- // we are not showing fav- collection items in facets
256
- castedBuckets = castedBuckets?.filter(
257
- bucket => bucket?.key?.toString().startsWith('fav-') === false
258
- );
259
-
260
- // asynchronously load the collection name
261
- this.preloadCollectionNames(castedBuckets);
262
- }
263
-
264
- // find length and pagination size for modal pagination
265
- const { length } = Object.keys(castedBuckets as []);
266
- this.paginationSize = Math.ceil(length / this.facetsPerPage);
267
-
268
- // render only items which will be visible as per this.facetsPerPage
269
- const bucketsMaxSix = castedBuckets?.slice(
270
- (this.pageNumber - 1) * this.facetsPerPage,
271
- this.pageNumber * this.facetsPerPage
272
- );
273
-
274
- const facetBucket: FacetBucket[] = bucketsMaxSix.map(bucket => {
275
- let bucketKey = bucket.key;
276
- // for languages, we need to search by language code instead of the
277
- // display name, which is what we get from the search engine result
278
- if (option === 'language') {
279
- bucketKey =
280
- this.languageCodeHandler?.getCodeStringFromLanguageName(
281
- `${bucket.key}`
282
- ) ?? bucket.key;
283
- }
284
- return {
285
- displayText: `${bucket.key}`,
286
- key: `${bucketKey}`,
287
- count: bucket.doc_count,
288
- state: 'none',
289
- };
290
- });
291
- const group: FacetGroup = {
292
- title: this.facetGroupTitle as string,
293
- key: option,
294
- buckets: facetBucket,
295
- };
296
- facetGroups.push(group);
297
- });
298
-
299
- return facetGroups;
300
- }
301
-
302
- /**
303
- * for collections, we need to asynchronously load the collection name
304
- * so we use the `async-collection-name` widget and for the rest, we have a static value to use
305
- *
306
- * @param castedBuckets
307
- */
308
- private preloadCollectionNames(castedBuckets: any[]) {
309
- const collectionIds = castedBuckets?.map(option => option.key);
310
- const collectionIdsArray = Array.from(new Set(collectionIds)) as string[];
311
-
312
- this.collectionNameCache?.preloadIdentifiers(collectionIdsArray);
313
- }
314
-
315
- private get getMoreFacetsTemplate(): TemplateResult {
316
- return html`
317
- <facets-template
318
- .facetGroup=${this.mergedFacets?.shift()}
319
- .selectedFacets=${this.selectedFacets}
320
- .renderOn=${'modal'}
321
- .collectionNameCache=${this.collectionNameCache}
322
- @selectedFacetsChanged=${(e: CustomEvent) => {
323
- this.selectedFacets = e.detail;
324
- }}
325
- ></facets-template>
326
- `;
327
- }
328
-
329
- private get loaderTemplate(): TemplateResult {
330
- return html`<div class="facets-loader">
331
- <ia-activity-indicator .mode=${'processing'}></ia-activity-indicator>
332
- </div> `;
333
- }
334
-
335
- // render pagination if more then 1 page
336
- private get facetsPaginationTemplate() {
337
- return this.paginationSize > 1
338
- ? html`<more-facets-pagination
339
- .size=${this.paginationSize}
340
- .currentPage=${1}
341
- @pageNumberClicked=${this.pageNumberClicked}
342
- ></more-facets-pagination>`
343
- : nothing;
344
- }
345
-
346
- private get footerTemplate() {
347
- if (this.paginationSize > 0) {
348
- return html`${this.facetsPaginationTemplate}
349
- <div class="footer">
350
- <button
351
- class="btn btn-cancel"
352
- type="button"
353
- @click=${this.cancelClick}
354
- >
355
- Cancel
356
- </button>
357
- <button
358
- class="btn btn-submit"
359
- type="button"
360
- @click=${this.applySearchFacetsClicked}
361
- >
362
- Apply filters
363
- </button>
364
- </div> `;
365
- }
366
-
367
- return nothing;
368
- }
369
-
370
- private sortFacetAggregation() {
371
- this.sortedBy = this.sortedBy === 'count' ? 'alpha' : 'count';
372
- this.dispatchEvent(
373
- new CustomEvent('sortedFacets', { detail: this.sortedBy })
374
- );
375
- }
376
-
377
- private get getModalHeaderTemplate(): TemplateResult {
378
- const title =
379
- this.sortedBy === 'alpha' ? 'Sort by count' : 'Sort by alphabetically';
380
-
381
- const image =
382
- this.sortedBy === 'alpha'
383
- ? 'https://archive.org/images/filter-alpha.png'
384
- : 'https://archive.org/images/filter-count.png';
385
-
386
- return html`<span class="sr-only">More facets for:</span>
387
- <span class="title">
388
- ${this.facetGroupTitle}
389
- <input
390
- class="sorting-icon"
391
- type="image"
392
- @click=${() => this.sortFacetAggregation()}
393
- src="${image}"
394
- title=${title}
395
- alt="sort facets"
396
- />
397
- </span> `;
398
- }
399
-
400
- render() {
401
- return html`
402
- ${this.facetsLoading
403
- ? this.loaderTemplate
404
- : html`
405
- <section id="more-facets">
406
- <div class="header-content">${this.getModalHeaderTemplate}</div>
407
- <div class="facets-content">${this.getMoreFacetsTemplate}</div>
408
- ${this.footerTemplate}
409
- </section>
410
- `}
411
- `;
412
- }
413
-
414
- private applySearchFacetsClicked() {
415
- const event = new CustomEvent<SelectedFacets>('facetsChanged', {
416
- detail: this.selectedFacets,
417
- bubbles: true,
418
- composed: true,
419
- });
420
- this.dispatchEvent(event);
421
- this.modalManager?.closeModal();
422
- }
423
-
424
- private cancelClick() {
425
- this.modalManager?.closeModal();
426
- }
427
-
428
- static get styles(): CSSResultGroup {
429
- const modalSubmitButton = css`var(--primaryButtonBGColor, #194880)`;
430
-
431
- return css`
432
- @media (max-width: 560px) {
433
- section#more-facets {
434
- max-height: 450px;
435
- }
436
- .facets-content {
437
- overflow-y: auto;
438
- height: 300px;
439
- }
440
- }
441
- section#more-facets {
442
- overflow: auto;
443
- padding: 10px; // leaves room for scroll bar to appear without overlaying on content
444
- }
445
- .header-content .title {
446
- display: block;
447
- text-align: left;
448
- font-size: 1.8rem;
449
- padding: 0 10px;
450
- font-weight: bold;
451
- }
452
- .facets-content {
453
- font-size: 1.2rem;
454
- max-height: 300px;
455
- overflow: auto;
456
- padding: 10px;
457
- }
458
- .page-number {
459
- background: none;
460
- border: 0;
461
- cursor: pointer;
462
- border-radius: 100%;
463
- width: 25px;
464
- height: 25px;
465
- margin: 10px;
466
- font-size: 1.4rem;
467
- vertical-align: middle;
468
- }
469
- .current-page {
470
- background: black;
471
- color: white;
472
- }
473
- .facets-loader {
474
- margin-bottom: 20px;
475
- width: 70px;
476
- display: block;
477
- margin-left: auto;
478
- margin-right: auto;
479
- }
480
- .btn {
481
- border: none;
482
- padding: 10px;
483
- margin-bottom: 10px;
484
- width: auto;
485
- border-radius: 4px;
486
- cursor: pointer;
487
- }
488
- .btn-cancel {
489
- background-color: #000;
490
- color: white;
491
- }
492
- .btn-submit {
493
- background-color: ${modalSubmitButton};
494
- color: white;
495
- }
496
- .footer {
497
- text-align: center;
498
- margin-top: 10px;
499
- }
500
-
501
- .sr-only {
502
- position: absolute;
503
- width: 1px;
504
- height: 1px;
505
- padding: 0;
506
- margin: -1px;
507
- overflow: hidden;
508
- clip: rect(0, 0, 0, 0);
509
- border: 0;
510
- }
511
- .sorting-icon {
512
- height: 15px;
513
- vertical-align: baseline;
514
- cursor: pointer;
515
- }
516
- `;
517
- }
518
- }
1
+ /* eslint-disable dot-notation */
2
+ /* eslint-disable lit-a11y/click-events-have-key-events */
3
+ import {
4
+ css,
5
+ CSSResultGroup,
6
+ html,
7
+ LitElement,
8
+ nothing,
9
+ PropertyValues,
10
+ TemplateResult,
11
+ } from 'lit';
12
+ import { customElement, property, state } from 'lit/decorators.js';
13
+ import {
14
+ Aggregation,
15
+ Bucket,
16
+ SearchServiceInterface,
17
+ SearchParams,
18
+ SearchType,
19
+ AggregationSortType,
20
+ } from '@internetarchive/search-service';
21
+ import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
22
+ import type { ModalManagerInterface } from '@internetarchive/modal-manager';
23
+ import {
24
+ SelectedFacets,
25
+ FacetGroup,
26
+ FacetBucket,
27
+ FacetOption,
28
+ facetTitles,
29
+ } from '../models';
30
+ import type { LanguageCodeHandlerInterface } from '../language-code-handler/language-code-handler';
31
+ import '@internetarchive/ia-activity-indicator/ia-activity-indicator';
32
+ import './more-facets-pagination';
33
+ import './facets-template';
34
+
35
+ @customElement('more-facets-content')
36
+ export class MoreFacetsContent extends LitElement {
37
+ @property({ type: String }) facetKey?: string;
38
+
39
+ @property({ type: String }) facetAggregationKey?: string;
40
+
41
+ @property({ type: String }) fullQuery?: string;
42
+
43
+ @property({ type: Object }) modalManager?: ModalManagerInterface;
44
+
45
+ @property({ type: Object }) searchService?: SearchServiceInterface;
46
+
47
+ @property({ type: String }) searchType?: SearchType;
48
+
49
+ @property({ type: Object })
50
+ collectionNameCache?: CollectionNameCacheInterface;
51
+
52
+ @property({ type: Object })
53
+ languageCodeHandler?: LanguageCodeHandlerInterface;
54
+
55
+ @property({ type: Object }) selectedFacets?: SelectedFacets;
56
+
57
+ @property({ type: String }) sortedBy = 'count'; // count | alpha
58
+
59
+ @state() aggregations?: Record<string, Aggregation>;
60
+
61
+ @state() facetGroup?: FacetGroup[] = [];
62
+
63
+ @state() facetGroupTitle?: String = '';
64
+
65
+ @state() pageNumber = 1;
66
+
67
+ /**
68
+ * Facets are loading on popup
69
+ */
70
+ @state() facetsLoading = true;
71
+
72
+ @state() paginationSize = 0;
73
+
74
+ @state() facetsType = 'modal';
75
+
76
+ private facetsPerPage = 35;
77
+
78
+ updated(changed: PropertyValues) {
79
+ if (changed.has('facetKey')) {
80
+ this.facetsLoading = true;
81
+ this.pageNumber = 1;
82
+
83
+ this.updateSpecificFacets();
84
+ }
85
+
86
+ if (changed.has('pageNumber')) {
87
+ this.facetGroup = this.aggregationFacetGroups;
88
+ }
89
+ }
90
+
91
+ firstUpdated() {
92
+ this.setupEscapeListeners();
93
+ }
94
+
95
+ /**
96
+ * Close more facets modal on Escape click
97
+ */
98
+ private setupEscapeListeners() {
99
+ if (this.modalManager) {
100
+ document.addEventListener('keydown', (e: KeyboardEvent) => {
101
+ if (e.key === 'Escape') {
102
+ this.modalManager?.closeModal();
103
+ }
104
+ });
105
+ } else {
106
+ document.removeEventListener('keydown', () => {});
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Get specific facets data from search-service API based of currently query params
112
+ * - this.aggregations - hold result of search service and being used for further processing.
113
+ */
114
+ async updateSpecificFacets(): Promise<void> {
115
+ const aggregations = {
116
+ simpleParams: [this.facetAggregationKey as string],
117
+ };
118
+ const aggregationsSize = 65535; // todo - do we want to have all the records at once?
119
+
120
+ const params: SearchParams = {
121
+ query: this.fullQuery as string,
122
+ aggregations,
123
+ aggregationsSize,
124
+ rows: 0, // todo - do we want server-side pagination with offset/page/limit flag?
125
+ };
126
+
127
+ const results = await this.searchService?.search(params, this.searchType);
128
+ this.aggregations = results?.success?.response.aggregations;
129
+
130
+ this.facetGroup = this.aggregationFacetGroups;
131
+ this.facetsLoading = false;
132
+ }
133
+
134
+ private pageNumberClicked(e: CustomEvent<{ page: number }>) {
135
+ const page = e?.detail?.page;
136
+ if (page) {
137
+ this.pageNumber = Number(page);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Combines the selected facets with the aggregations to create a single list of facets
143
+ */
144
+ private get mergedFacets(): FacetGroup[] | void {
145
+ const facetGroups: FacetGroup[] = [];
146
+
147
+ const selectedFacetGroup = this.selectedFacetGroups.find(
148
+ group => group.key === this.facetKey
149
+ );
150
+ const aggregateFacetGroup = this.aggregationFacetGroups.find(
151
+ group => group.key === this.facetKey
152
+ );
153
+
154
+ // if the user selected a facet, but it's not in the aggregation, we add it as-is
155
+ if (selectedFacetGroup && !aggregateFacetGroup) {
156
+ facetGroups.push(selectedFacetGroup);
157
+ return facetGroups;
158
+ }
159
+
160
+ // if we don't have an aggregate facet group, don't add this to the list
161
+ if (!aggregateFacetGroup) return facetGroups;
162
+
163
+ // start with either the selected group if we have one, or the aggregate group
164
+ const facetGroup = selectedFacetGroup ?? aggregateFacetGroup;
165
+
166
+ // attach the counts to the selected buckets
167
+ const bucketsWithCount =
168
+ selectedFacetGroup?.buckets.map(bucket => {
169
+ const selectedBucket = aggregateFacetGroup.buckets.find(
170
+ b => b.key === bucket.key
171
+ );
172
+ return selectedBucket
173
+ ? {
174
+ ...bucket,
175
+ count: selectedBucket.count,
176
+ }
177
+ : bucket;
178
+ }) ?? [];
179
+
180
+ // append any additional buckets that were not selected
181
+ aggregateFacetGroup.buckets.forEach(bucket => {
182
+ const existingBucket = bucketsWithCount.find(b => b.key === bucket.key);
183
+ if (existingBucket) return;
184
+ bucketsWithCount.push(bucket);
185
+ });
186
+ facetGroup.buckets = bucketsWithCount;
187
+
188
+ facetGroups.push(facetGroup);
189
+ return facetGroups;
190
+ }
191
+
192
+ /**
193
+ * Converts the selected facets to a `FacetGroup` array,
194
+ * which is easier to work with
195
+ */
196
+ private get selectedFacetGroups(): FacetGroup[] {
197
+ if (!this.selectedFacets) return [];
198
+
199
+ const facetGroups: FacetGroup[] = Object.entries(this.selectedFacets).map(
200
+ ([key, selectedFacets]) => {
201
+ const option = key as FacetOption;
202
+ const title = facetTitles[option];
203
+
204
+ const buckets: FacetBucket[] = Object.entries(selectedFacets).map(
205
+ ([value, data]) => {
206
+ let displayText: string = value;
207
+ // for selected languages, we store the language code instead of the
208
+ // display name, so look up the name from the mapping
209
+ if (option === 'language') {
210
+ displayText =
211
+ this.languageCodeHandler?.getLanguageNameFromCodeString(
212
+ value
213
+ ) ?? value;
214
+ }
215
+ return {
216
+ displayText,
217
+ key: value,
218
+ count: data?.count,
219
+ state: data?.state,
220
+ };
221
+ }
222
+ );
223
+
224
+ return {
225
+ title,
226
+ key: option,
227
+ buckets,
228
+ };
229
+ }
230
+ );
231
+
232
+ return facetGroups;
233
+ }
234
+
235
+ /**
236
+ * Converts the raw `aggregations` to `FacetGroups`, which are easier to use
237
+ */
238
+ private get aggregationFacetGroups(): FacetGroup[] {
239
+ const facetGroups: FacetGroup[] = [];
240
+ Object.entries(this.aggregations ?? []).forEach(([key, aggregation]) => {
241
+ // the year_histogram data is in a different format so can't be handled here
242
+ if (key === 'year_histogram') return;
243
+
244
+ const option = key as FacetOption;
245
+ this.facetGroupTitle = facetTitles[option];
246
+
247
+ // sort facets in specific order
248
+ let castedBuckets = aggregation.getSortedBuckets(
249
+ this.sortedBy === 'alpha'
250
+ ? AggregationSortType.ALPHABETICAL
251
+ : AggregationSortType.COUNT
252
+ ) as Bucket[];
253
+
254
+ if (option === 'collection') {
255
+ // we are not showing fav- collection items in facets
256
+ castedBuckets = castedBuckets?.filter(
257
+ bucket => bucket?.key?.toString().startsWith('fav-') === false
258
+ );
259
+
260
+ // asynchronously load the collection name
261
+ this.preloadCollectionNames(castedBuckets);
262
+ }
263
+
264
+ // find length and pagination size for modal pagination
265
+ const { length } = Object.keys(castedBuckets as []);
266
+ this.paginationSize = Math.ceil(length / this.facetsPerPage);
267
+
268
+ // render only items which will be visible as per this.facetsPerPage
269
+ const bucketsMaxSix = castedBuckets?.slice(
270
+ (this.pageNumber - 1) * this.facetsPerPage,
271
+ this.pageNumber * this.facetsPerPage
272
+ );
273
+
274
+ const facetBucket: FacetBucket[] = bucketsMaxSix.map(bucket => {
275
+ let bucketKey = bucket.key;
276
+ // for languages, we need to search by language code instead of the
277
+ // display name, which is what we get from the search engine result
278
+ if (option === 'language') {
279
+ bucketKey =
280
+ this.languageCodeHandler?.getCodeStringFromLanguageName(
281
+ `${bucket.key}`
282
+ ) ?? bucket.key;
283
+ }
284
+ return {
285
+ displayText: `${bucket.key}`,
286
+ key: `${bucketKey}`,
287
+ count: bucket.doc_count,
288
+ state: 'none',
289
+ };
290
+ });
291
+ const group: FacetGroup = {
292
+ title: this.facetGroupTitle as string,
293
+ key: option,
294
+ buckets: facetBucket,
295
+ };
296
+ facetGroups.push(group);
297
+ });
298
+
299
+ return facetGroups;
300
+ }
301
+
302
+ /**
303
+ * for collections, we need to asynchronously load the collection name
304
+ * so we use the `async-collection-name` widget and for the rest, we have a static value to use
305
+ *
306
+ * @param castedBuckets
307
+ */
308
+ private preloadCollectionNames(castedBuckets: any[]) {
309
+ const collectionIds = castedBuckets?.map(option => option.key);
310
+ const collectionIdsArray = Array.from(new Set(collectionIds)) as string[];
311
+
312
+ this.collectionNameCache?.preloadIdentifiers(collectionIdsArray);
313
+ }
314
+
315
+ private get getMoreFacetsTemplate(): TemplateResult {
316
+ return html`
317
+ <facets-template
318
+ .facetGroup=${this.mergedFacets?.shift()}
319
+ .selectedFacets=${this.selectedFacets}
320
+ .renderOn=${'modal'}
321
+ .collectionNameCache=${this.collectionNameCache}
322
+ @selectedFacetsChanged=${(e: CustomEvent) => {
323
+ this.selectedFacets = e.detail;
324
+ }}
325
+ ></facets-template>
326
+ `;
327
+ }
328
+
329
+ private get loaderTemplate(): TemplateResult {
330
+ return html`<div class="facets-loader">
331
+ <ia-activity-indicator .mode=${'processing'}></ia-activity-indicator>
332
+ </div> `;
333
+ }
334
+
335
+ // render pagination if more then 1 page
336
+ private get facetsPaginationTemplate() {
337
+ return this.paginationSize > 1
338
+ ? html`<more-facets-pagination
339
+ .size=${this.paginationSize}
340
+ .currentPage=${1}
341
+ @pageNumberClicked=${this.pageNumberClicked}
342
+ ></more-facets-pagination>`
343
+ : nothing;
344
+ }
345
+
346
+ private get footerTemplate() {
347
+ if (this.paginationSize > 0) {
348
+ return html`${this.facetsPaginationTemplate}
349
+ <div class="footer">
350
+ <button
351
+ class="btn btn-cancel"
352
+ type="button"
353
+ @click=${this.cancelClick}
354
+ >
355
+ Cancel
356
+ </button>
357
+ <button
358
+ class="btn btn-submit"
359
+ type="button"
360
+ @click=${this.applySearchFacetsClicked}
361
+ >
362
+ Apply filters
363
+ </button>
364
+ </div> `;
365
+ }
366
+
367
+ return nothing;
368
+ }
369
+
370
+ private sortFacetAggregation() {
371
+ this.sortedBy = this.sortedBy === 'count' ? 'alpha' : 'count';
372
+ this.dispatchEvent(
373
+ new CustomEvent('sortedFacets', { detail: this.sortedBy })
374
+ );
375
+ }
376
+
377
+ private get getModalHeaderTemplate(): TemplateResult {
378
+ const title =
379
+ this.sortedBy === 'alpha' ? 'Sort by count' : 'Sort by alphabetically';
380
+
381
+ const image =
382
+ this.sortedBy === 'alpha'
383
+ ? 'https://archive.org/images/filter-alpha.png'
384
+ : 'https://archive.org/images/filter-count.png';
385
+
386
+ return html`<span class="sr-only">More facets for:</span>
387
+ <span class="title">
388
+ ${this.facetGroupTitle}
389
+ <input
390
+ class="sorting-icon"
391
+ type="image"
392
+ @click=${() => this.sortFacetAggregation()}
393
+ src="${image}"
394
+ title=${title}
395
+ alt="sort facets"
396
+ />
397
+ </span> `;
398
+ }
399
+
400
+ render() {
401
+ return html`
402
+ ${this.facetsLoading
403
+ ? this.loaderTemplate
404
+ : html`
405
+ <section id="more-facets">
406
+ <div class="header-content">${this.getModalHeaderTemplate}</div>
407
+ <div class="facets-content">${this.getMoreFacetsTemplate}</div>
408
+ ${this.footerTemplate}
409
+ </section>
410
+ `}
411
+ `;
412
+ }
413
+
414
+ private applySearchFacetsClicked() {
415
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
416
+ detail: this.selectedFacets,
417
+ bubbles: true,
418
+ composed: true,
419
+ });
420
+ this.dispatchEvent(event);
421
+ this.modalManager?.closeModal();
422
+ }
423
+
424
+ private cancelClick() {
425
+ this.modalManager?.closeModal();
426
+ }
427
+
428
+ static get styles(): CSSResultGroup {
429
+ const modalSubmitButton = css`var(--primaryButtonBGColor, #194880)`;
430
+
431
+ return css`
432
+ @media (max-width: 560px) {
433
+ section#more-facets {
434
+ max-height: 450px;
435
+ }
436
+ .facets-content {
437
+ overflow-y: auto;
438
+ height: 300px;
439
+ }
440
+ }
441
+ section#more-facets {
442
+ overflow: auto;
443
+ padding: 10px; // leaves room for scroll bar to appear without overlaying on content
444
+ }
445
+ .header-content .title {
446
+ display: block;
447
+ text-align: left;
448
+ font-size: 1.8rem;
449
+ padding: 0 10px;
450
+ font-weight: bold;
451
+ }
452
+ .facets-content {
453
+ font-size: 1.2rem;
454
+ max-height: 300px;
455
+ overflow: auto;
456
+ padding: 10px;
457
+ }
458
+ .page-number {
459
+ background: none;
460
+ border: 0;
461
+ cursor: pointer;
462
+ border-radius: 100%;
463
+ width: 25px;
464
+ height: 25px;
465
+ margin: 10px;
466
+ font-size: 1.4rem;
467
+ vertical-align: middle;
468
+ }
469
+ .current-page {
470
+ background: black;
471
+ color: white;
472
+ }
473
+ .facets-loader {
474
+ margin-bottom: 20px;
475
+ width: 70px;
476
+ display: block;
477
+ margin-left: auto;
478
+ margin-right: auto;
479
+ }
480
+ .btn {
481
+ border: none;
482
+ padding: 10px;
483
+ margin-bottom: 10px;
484
+ width: auto;
485
+ border-radius: 4px;
486
+ cursor: pointer;
487
+ }
488
+ .btn-cancel {
489
+ background-color: #000;
490
+ color: white;
491
+ }
492
+ .btn-submit {
493
+ background-color: ${modalSubmitButton};
494
+ color: white;
495
+ }
496
+ .footer {
497
+ text-align: center;
498
+ margin-top: 10px;
499
+ }
500
+
501
+ .sr-only {
502
+ position: absolute;
503
+ width: 1px;
504
+ height: 1px;
505
+ padding: 0;
506
+ margin: -1px;
507
+ overflow: hidden;
508
+ clip: rect(0, 0, 0, 0);
509
+ border: 0;
510
+ }
511
+ .sorting-icon {
512
+ height: 15px;
513
+ vertical-align: baseline;
514
+ cursor: pointer;
515
+ }
516
+ `;
517
+ }
518
+ }