@internetarchive/collection-browser 0.2.21 → 0.2.22

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