@internetarchive/collection-browser 3.0.0 → 3.0.1-webdev-7936.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.
Files changed (85) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +27 -27
  3. package/.github/workflows/gh-pages-main.yml +39 -39
  4. package/.github/workflows/npm-publish.yml +39 -39
  5. package/.github/workflows/pr-preview.yml +38 -38
  6. package/.husky/pre-commit +4 -4
  7. package/.prettierignore +1 -1
  8. package/LICENSE +661 -661
  9. package/README.md +83 -83
  10. package/dist/src/collection-browser.js +681 -680
  11. package/dist/src/collection-browser.js.map +1 -1
  12. package/dist/src/collection-facets/facet-row.js +131 -130
  13. package/dist/src/collection-facets/facet-row.js.map +1 -1
  14. package/dist/src/collection-facets/smart-facets/smart-facet-bar.js +75 -75
  15. package/dist/src/collection-facets/smart-facets/smart-facet-bar.js.map +1 -1
  16. package/dist/src/collection-facets.js +264 -263
  17. package/dist/src/collection-facets.js.map +1 -1
  18. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  19. package/dist/src/data-source/collection-browser-query-state.js.map +1 -1
  20. package/dist/src/data-source/models.js.map +1 -1
  21. package/dist/src/models.js.map +1 -1
  22. package/dist/src/restoration-state-handler.js.map +1 -1
  23. package/dist/src/sort-filter-bar/sort-filter-bar.js +1 -1
  24. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  25. package/dist/src/tiles/base-tile-component.js.map +1 -1
  26. package/dist/src/tiles/grid/collection-tile.js +3 -2
  27. package/dist/src/tiles/grid/collection-tile.js.map +1 -1
  28. package/dist/src/tiles/grid/item-tile.js +139 -139
  29. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  30. package/dist/src/tiles/grid/search-tile.js +3 -2
  31. package/dist/src/tiles/grid/search-tile.js.map +1 -1
  32. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js +2 -1
  33. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js.map +1 -1
  34. package/dist/src/tiles/hover/hover-pane-controller.js +21 -21
  35. package/dist/src/tiles/hover/hover-pane-controller.js.map +1 -1
  36. package/dist/src/tiles/hover/tile-hover-pane.js +108 -108
  37. package/dist/src/tiles/hover/tile-hover-pane.js.map +1 -1
  38. package/dist/src/tiles/list/tile-list-compact-header.js +45 -45
  39. package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -1
  40. package/dist/src/tiles/list/tile-list-compact.js +97 -97
  41. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  42. package/dist/src/tiles/list/tile-list.js +300 -290
  43. package/dist/src/tiles/list/tile-list.js.map +1 -1
  44. package/dist/src/tiles/tile-dispatcher.js +200 -200
  45. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  46. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  47. package/dist/test/collection-browser.test.js +183 -183
  48. package/dist/test/collection-browser.test.js.map +1 -1
  49. package/dist/test/restoration-state-handler.test.js.map +1 -1
  50. package/dist/test/tiles/list/tile-list.test.js +13 -0
  51. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  52. package/eslint.config.mjs +53 -53
  53. package/index.html +24 -24
  54. package/local.archive.org.cert +86 -86
  55. package/local.archive.org.key +27 -27
  56. package/package.json +117 -117
  57. package/renovate.json +6 -6
  58. package/src/collection-browser.ts +2776 -2775
  59. package/src/collection-facets/facet-row.ts +283 -282
  60. package/src/collection-facets/smart-facets/smart-facet-bar.ts +437 -437
  61. package/src/collection-facets.ts +991 -990
  62. package/src/data-source/collection-browser-data-source.ts +1390 -1390
  63. package/src/data-source/collection-browser-query-state.ts +63 -63
  64. package/src/data-source/models.ts +43 -43
  65. package/src/models.ts +870 -870
  66. package/src/restoration-state-handler.ts +544 -544
  67. package/src/sort-filter-bar/sort-filter-bar.ts +1 -1
  68. package/src/tiles/base-tile-component.ts +53 -53
  69. package/src/tiles/grid/collection-tile.ts +3 -2
  70. package/src/tiles/grid/item-tile.ts +339 -339
  71. package/src/tiles/grid/search-tile.ts +3 -2
  72. package/src/tiles/grid/styles/tile-grid-shared-styles.ts +2 -1
  73. package/src/tiles/hover/hover-pane-controller.ts +517 -517
  74. package/src/tiles/hover/tile-hover-pane.ts +180 -180
  75. package/src/tiles/list/tile-list-compact-header.ts +86 -86
  76. package/src/tiles/list/tile-list-compact.ts +236 -236
  77. package/src/tiles/list/tile-list.ts +696 -688
  78. package/src/tiles/tile-dispatcher.ts +486 -486
  79. package/src/tiles/tile-display-value-provider.ts +124 -124
  80. package/test/collection-browser.test.ts +2340 -2340
  81. package/test/restoration-state-handler.test.ts +510 -510
  82. package/test/tiles/list/tile-list.test.ts +15 -0
  83. package/tsconfig.json +20 -20
  84. package/web-dev-server.config.mjs +30 -30
  85. package/web-test-runner.config.mjs +41 -41
@@ -1,437 +1,437 @@
1
- import {
2
- css,
3
- html,
4
- LitElement,
5
- TemplateResult,
6
- CSSResultGroup,
7
- nothing,
8
- PropertyValues,
9
- } from 'lit';
10
- import { repeat } from 'lit/directives/repeat.js';
11
- import { customElement, property, state } from 'lit/decorators.js';
12
- import type { Aggregation, Bucket } from '@internetarchive/search-service';
13
- import type { CollectionTitles } from '../../data-source/models';
14
- import type {
15
- FacetEventDetails,
16
- FacetOption,
17
- FacetState,
18
- SelectedFacets,
19
- } from '../../models';
20
- import { updateSelectedFacetBucket } from '../../utils/facet-utils';
21
- import { SmartQueryHeuristicGroup } from './smart-facet-heuristics';
22
- import type { SmartFacetDropdown } from './smart-facet-dropdown';
23
- import type { SmartFacet, SmartFacetEvent } from './models';
24
- import { smartFacetEquals } from './smart-facet-equals';
25
- import { dedupe } from './dedupe';
26
- import { log } from '../../utils/log';
27
- import filterIcon from '../../assets/img/icons/filter';
28
-
29
- import './smart-facet-button';
30
- import './smart-facet-dropdown';
31
-
32
- const fieldPrefixes: Partial<Record<FacetOption, string>> = {
33
- collection: 'Collection: ',
34
- creator: 'By: ',
35
- subject: 'About: ',
36
- };
37
-
38
- function capitalize(str: string) {
39
- return str.charAt(0).toUpperCase() + str.slice(1);
40
- }
41
-
42
- @customElement('smart-facet-bar')
43
- export class SmartFacetBar extends LitElement {
44
- @property({ type: String }) query?: string;
45
-
46
- @property({ type: Object }) aggregations?: Record<string, Aggregation>;
47
-
48
- @property({ type: Object }) selectedFacets?: SelectedFacets;
49
-
50
- /** The map from collection identifiers to their titles */
51
- @property({ type: Object })
52
- collectionTitles?: CollectionTitles;
53
-
54
- @property({ type: Boolean }) filterToggleShown = false;
55
-
56
- @property({ type: Boolean }) filterToggleActive = false;
57
-
58
- @property({ type: String }) label?: string;
59
-
60
- @state() private heuristicRecs: SmartFacet[] = [];
61
-
62
- @state() private smartFacets: SmartFacet[][] = [];
63
-
64
- @state() private lastAggregations?: Record<string, Aggregation>;
65
-
66
- //
67
- // COMPONENT LIFECYCLE METHODS
68
- //
69
-
70
- render() {
71
- if (!this.query) return nothing;
72
-
73
- const shouldShowLabel = !!this.label && this.smartFacets.length > 0;
74
- return html`
75
- <div id="smart-facets-container">
76
- ${this.filtersToggleTemplate}
77
- ${shouldShowLabel
78
- ? html`<p id="filters-label">${this.label}</p>`
79
- : nothing}
80
- ${repeat(
81
- this.smartFacets,
82
- f =>
83
- `${f[0].label}|${f[0].facets[0].facetType}|${f[0].facets[0].bucketKey}`,
84
- facet => this.makeSmartFacet(facet),
85
- )}
86
- </div>
87
- `;
88
- }
89
-
90
- protected willUpdate(changed: PropertyValues): void {
91
- let shouldUpdateSmartFacets = false;
92
-
93
- if (changed.has('query')) {
94
- log('query change', changed.get('query'), this.query);
95
- this.lastAggregations = undefined;
96
- shouldUpdateSmartFacets = true;
97
- }
98
-
99
- if (
100
- changed.has('aggregations') &&
101
- !this.lastAggregations &&
102
- this.aggregations &&
103
- Object.keys(this.aggregations).length > 0
104
- ) {
105
- log('aggs change', changed.get('aggregations'), this.aggregations);
106
- this.lastAggregations = this.aggregations;
107
- shouldUpdateSmartFacets = true;
108
- }
109
-
110
- if (shouldUpdateSmartFacets) {
111
- log('should update smart facets, doing so...');
112
- this.updateSmartFacets();
113
- }
114
- }
115
-
116
- refresh(): void {
117
- this.lastAggregations = this.aggregations;
118
- this.updateSmartFacets();
119
- }
120
-
121
- deselectAll(): void {
122
- for (const sf of this.smartFacets) {
123
- for (const facet of sf) {
124
- facet.selected = false;
125
- }
126
- }
127
- this.requestUpdate();
128
- }
129
-
130
- private async updateSmartFacets(): Promise<void> {
131
- log('updating smart facets');
132
- if (this.query) {
133
- this.heuristicRecs =
134
- await new SmartQueryHeuristicGroup().getRecommendedFacets(this.query);
135
- log('heuristic recs are', this.heuristicRecs);
136
- this.smartFacets = dedupe(this.facetsToDisplay);
137
- log('smart facets are', this.smartFacets);
138
- }
139
- }
140
-
141
- //
142
- // OTHER METHODS
143
- //
144
-
145
- private makeSmartFacet(facets: SmartFacet[]) {
146
- if (facets.length === 0) {
147
- return nothing;
148
- }
149
- if (facets.length === 1) {
150
- return this.smartFacetButton(facets[0]);
151
- }
152
- return this.smartFacetDropdown(facets);
153
- }
154
-
155
- private smartFacetButton(facet: SmartFacet) {
156
- return html`
157
- <smart-facet-button
158
- .facetInfo=${facet}
159
- .labelPrefix=${fieldPrefixes[facet.facets[0].facetType]}
160
- .selected=${facet.selected ?? false}
161
- @facetClick=${this.facetClicked}
162
- ></smart-facet-button>
163
- `;
164
- }
165
-
166
- private smartFacetDropdown(facets: SmartFacet[]) {
167
- return html`
168
- <smart-facet-dropdown
169
- .facetInfo=${facets}
170
- .labelPrefix=${fieldPrefixes[facets[0].facets[0].facetType]}
171
- .activeFacetRef=${facets[0].facets[0]}
172
- @facetClick=${this.dropdownOptionClicked}
173
- @dropdownClick=${this.dropdownClicked}
174
- ></smart-facet-dropdown>
175
- `;
176
- }
177
-
178
- private get filtersToggleTemplate(): TemplateResult | typeof nothing {
179
- if (!this.filterToggleShown) return nothing;
180
-
181
- return html`
182
- <button
183
- id="filters-toggle"
184
- class=${this.filterToggleActive ? 'active' : ''}
185
- title="${this.filterToggleActive ? 'Hide' : 'Show'} filters pane"
186
- @click=${this.filterToggleClicked}
187
- >
188
- ${filterIcon}
189
- </button>
190
- `;
191
- }
192
-
193
- private get facetsToDisplay(): SmartFacet[][] {
194
- const facets: SmartFacet[][] = [];
195
-
196
- if (this.heuristicRecs.length > 0) {
197
- for (const rec of this.heuristicRecs) {
198
- // Suppress mediatype-only facets for now.
199
- if (rec.facets.length === 1 && rec.facets[0].facetType === 'mediatype')
200
- continue;
201
-
202
- facets.push([rec]);
203
- }
204
- }
205
-
206
- if (this.lastAggregations) {
207
- const keys = [
208
- 'mediatype',
209
- 'year',
210
- 'language',
211
- 'creator',
212
- 'subject',
213
- 'collection',
214
- ];
215
- for (const key of keys) {
216
- const agg = this.lastAggregations[key];
217
- if (!agg) continue;
218
- if (agg.buckets.length === 0) continue;
219
- if (['lending', 'year_histogram', 'date_histogram'].includes(key))
220
- continue;
221
- if (typeof agg.buckets[0] === 'number') continue;
222
-
223
- if (
224
- key === 'mediatype' &&
225
- this.selectedFacets &&
226
- Object.values(this.selectedFacets.mediatype ?? {}).some(
227
- bucket => bucket.state !== 'none',
228
- )
229
- ) {
230
- continue;
231
- }
232
-
233
- const facetType = key as FacetOption;
234
- const buckets = agg.buckets as Bucket[];
235
-
236
- const unusedBuckets = buckets.filter(b => {
237
- const selectedFacetBucket = this.selectedFacets?.[facetType]?.[b.key];
238
- if (selectedFacetBucket && selectedFacetBucket.state !== 'none') {
239
- return false;
240
- }
241
- return true;
242
- });
243
-
244
- if (facetType === 'mediatype') {
245
- continue;
246
- // Don't include mediatype bubbles
247
- } else if (facetType === 'collection' || facetType === 'subject') {
248
- const topBuckets = unusedBuckets.slice(0, 5);
249
- facets.push(topBuckets.map(b => this.toSmartFacet(facetType, [b])));
250
- } else {
251
- facets.push([this.toSmartFacet(facetType, [unusedBuckets[0]])]);
252
- }
253
- }
254
- }
255
-
256
- return facets;
257
- }
258
-
259
- private toSmartFacet(
260
- facetType: FacetOption,
261
- buckets: Bucket[],
262
- // prefix = true
263
- ): SmartFacet {
264
- return {
265
- facets: buckets.map(bucket => {
266
- let displayText = capitalize(bucket.key.toString());
267
- if (facetType === 'collection') {
268
- const title = this.collectionTitles?.get(bucket.key.toString());
269
- if (title) displayText = title;
270
- }
271
-
272
- return {
273
- facetType,
274
- bucketKey: bucket.key.toString(),
275
- displayText,
276
- };
277
- }),
278
- } as SmartFacet;
279
- }
280
-
281
- /**
282
- * Toggles the state of the given smart facet, and updates the selected facets accordingly.
283
- */
284
- private toggleSmartFacet(
285
- facet: SmartFacet,
286
- details: FacetEventDetails[],
287
- ): void {
288
- let newState: FacetState;
289
- if (facet.selected) {
290
- // When deselected, leave the smart facet where it is
291
- newState = 'none';
292
- this.smartFacets = this.smartFacets.map(f => {
293
- if (f[0] === facet) return [{ ...facet, selected: false }];
294
- return f;
295
- });
296
- } else {
297
- // When selected, move the toggled smart facet to the front of the list
298
- newState = 'selected';
299
- this.smartFacets = [
300
- [{ ...facet, selected: true }],
301
- ...this.smartFacets.filter(f => f[0] !== facet),
302
- ];
303
- }
304
-
305
- this.updateSelectedFacets(
306
- details.map(facet => ({
307
- ...facet,
308
- bucket: {
309
- ...facet.bucket,
310
- state: newState,
311
- },
312
- })),
313
- );
314
- }
315
-
316
- /**
317
- * Updates the selected facet buckets for each of the given facets,
318
- * and emits a `facetsChanged` event to notify parent components of
319
- * the new state.
320
- */
321
- private updateSelectedFacets(facets: FacetEventDetails[]): void {
322
- for (const facet of facets) {
323
- this.selectedFacets = updateSelectedFacetBucket(
324
- this.selectedFacets,
325
- facet.facetType,
326
- facet.bucket,
327
- true,
328
- );
329
- }
330
-
331
- const event = new CustomEvent<SelectedFacets>('facetsChanged', {
332
- detail: this.selectedFacets,
333
- });
334
- this.dispatchEvent(event);
335
- }
336
-
337
- /**
338
- * Handler for when a smart facet button is clicked
339
- */
340
- private facetClicked(e: CustomEvent<SmartFacetEvent>): void {
341
- this.toggleSmartFacet(e.detail.smartFacet, e.detail.details);
342
- }
343
-
344
- /**
345
- * Handler for when an option in a smart facet dropdown menu is selected
346
- */
347
- private dropdownOptionClicked(e: CustomEvent<SmartFacetEvent>): void {
348
- const existingFacet = this.smartFacets.find(
349
- sf => sf.length === 1 && smartFacetEquals(sf[0], e.detail.smartFacet),
350
- );
351
- if (existingFacet) {
352
- // The facet already exists outside the dropdown, so just select it there
353
- this.toggleSmartFacet(existingFacet[0], e.detail.details);
354
- return;
355
- }
356
-
357
- // Otherwise, prepend a new smart facet for the selected option
358
- this.smartFacets = [
359
- [{ ...e.detail.smartFacet, selected: true }],
360
- ...this.smartFacets,
361
- ];
362
-
363
- this.updateSelectedFacets(e.detail.details);
364
- }
365
-
366
- /**
367
- * Handler for when any dropdown is clicked (whether button, caret, or menu item)
368
- */
369
- private dropdownClicked(e: CustomEvent<SmartFacetDropdown>): void {
370
- log('smart bar: onDropdownClick', e.detail);
371
- this.shadowRoot
372
- ?.querySelectorAll('smart-facet-dropdown')
373
- .forEach(dropdown => {
374
- if (dropdown !== e.detail) {
375
- log('closing', dropdown);
376
- (dropdown as SmartFacetDropdown).close();
377
- }
378
- });
379
- }
380
-
381
- private filterToggleClicked(): void {
382
- this.dispatchEvent(new CustomEvent('filtersToggled'));
383
- }
384
-
385
- //
386
- // STYLES
387
- //
388
-
389
- static get styles(): CSSResultGroup {
390
- return css`
391
- #smart-facets-container {
392
- display: flex;
393
- align-items: center;
394
- gap: 5px 10px;
395
- padding: 10px 0;
396
- white-space: nowrap;
397
- overflow: scroll hidden;
398
- scrollbar-width: none;
399
- }
400
-
401
- #filters-toggle {
402
- margin: 0;
403
- border: 0;
404
- padding: 5px 8px;
405
- border-radius: 5px;
406
- background: white;
407
- color: #2c2c2c;
408
- border: 1px solid #194880;
409
- font-size: 1.4rem;
410
- font-family: inherit;
411
- text-decoration: none;
412
- cursor: pointer;
413
- }
414
-
415
- #filters-toggle.active {
416
- background: #194880;
417
- color: white;
418
- }
419
-
420
- #filters-toggle > svg {
421
- width: 12px;
422
- filter: invert(0.16667);
423
- vertical-align: -1px;
424
- }
425
-
426
- #filters-toggle.active > svg {
427
- filter: invert(1);
428
- }
429
-
430
- #filters-label {
431
- font-size: 1.4rem;
432
- font-weight: var(--smartFacetLabelFontWeight, normal);
433
- margin: 0 -5px 0 0;
434
- }
435
- `;
436
- }
437
- }
1
+ import {
2
+ css,
3
+ html,
4
+ LitElement,
5
+ TemplateResult,
6
+ CSSResultGroup,
7
+ nothing,
8
+ PropertyValues,
9
+ } from 'lit';
10
+ import { repeat } from 'lit/directives/repeat.js';
11
+ import { customElement, property, state } from 'lit/decorators.js';
12
+ import type { Aggregation, Bucket } from '@internetarchive/search-service';
13
+ import type { CollectionTitles } from '../../data-source/models';
14
+ import type {
15
+ FacetEventDetails,
16
+ FacetOption,
17
+ FacetState,
18
+ SelectedFacets,
19
+ } from '../../models';
20
+ import { updateSelectedFacetBucket } from '../../utils/facet-utils';
21
+ import { SmartQueryHeuristicGroup } from './smart-facet-heuristics';
22
+ import type { SmartFacetDropdown } from './smart-facet-dropdown';
23
+ import type { SmartFacet, SmartFacetEvent } from './models';
24
+ import { smartFacetEquals } from './smart-facet-equals';
25
+ import { dedupe } from './dedupe';
26
+ import { log } from '../../utils/log';
27
+ import filterIcon from '../../assets/img/icons/filter';
28
+
29
+ import './smart-facet-button';
30
+ import './smart-facet-dropdown';
31
+
32
+ const fieldPrefixes: Partial<Record<FacetOption, string>> = {
33
+ collection: 'Collection: ',
34
+ creator: 'By: ',
35
+ subject: 'About: ',
36
+ };
37
+
38
+ function capitalize(str: string) {
39
+ return str.charAt(0).toUpperCase() + str.slice(1);
40
+ }
41
+
42
+ @customElement('smart-facet-bar')
43
+ export class SmartFacetBar extends LitElement {
44
+ @property({ type: String }) query?: string;
45
+
46
+ @property({ type: Object }) aggregations?: Record<string, Aggregation>;
47
+
48
+ @property({ type: Object }) selectedFacets?: SelectedFacets;
49
+
50
+ /** The map from collection identifiers to their titles */
51
+ @property({ type: Object })
52
+ collectionTitles?: CollectionTitles;
53
+
54
+ @property({ type: Boolean }) filterToggleShown = false;
55
+
56
+ @property({ type: Boolean }) filterToggleActive = false;
57
+
58
+ @property({ type: String }) label?: string;
59
+
60
+ @state() private heuristicRecs: SmartFacet[] = [];
61
+
62
+ @state() private smartFacets: SmartFacet[][] = [];
63
+
64
+ @state() private lastAggregations?: Record<string, Aggregation>;
65
+
66
+ //
67
+ // COMPONENT LIFECYCLE METHODS
68
+ //
69
+
70
+ render() {
71
+ if (!this.query) return nothing;
72
+
73
+ const shouldShowLabel = !!this.label && this.smartFacets.length > 0;
74
+ return html`
75
+ <div id="smart-facets-container">
76
+ ${this.filtersToggleTemplate}
77
+ ${shouldShowLabel
78
+ ? html`<p id="filters-label">${this.label}</p>`
79
+ : nothing}
80
+ ${repeat(
81
+ this.smartFacets,
82
+ f =>
83
+ `${f[0].label}|${f[0].facets[0].facetType}|${f[0].facets[0].bucketKey}`,
84
+ facet => this.makeSmartFacet(facet),
85
+ )}
86
+ </div>
87
+ `;
88
+ }
89
+
90
+ protected willUpdate(changed: PropertyValues): void {
91
+ let shouldUpdateSmartFacets = false;
92
+
93
+ if (changed.has('query')) {
94
+ log('query change', changed.get('query'), this.query);
95
+ this.lastAggregations = undefined;
96
+ shouldUpdateSmartFacets = true;
97
+ }
98
+
99
+ if (
100
+ changed.has('aggregations') &&
101
+ !this.lastAggregations &&
102
+ this.aggregations &&
103
+ Object.keys(this.aggregations).length > 0
104
+ ) {
105
+ log('aggs change', changed.get('aggregations'), this.aggregations);
106
+ this.lastAggregations = this.aggregations;
107
+ shouldUpdateSmartFacets = true;
108
+ }
109
+
110
+ if (shouldUpdateSmartFacets) {
111
+ log('should update smart facets, doing so...');
112
+ this.updateSmartFacets();
113
+ }
114
+ }
115
+
116
+ refresh(): void {
117
+ this.lastAggregations = this.aggregations;
118
+ this.updateSmartFacets();
119
+ }
120
+
121
+ deselectAll(): void {
122
+ for (const sf of this.smartFacets) {
123
+ for (const facet of sf) {
124
+ facet.selected = false;
125
+ }
126
+ }
127
+ this.requestUpdate();
128
+ }
129
+
130
+ private async updateSmartFacets(): Promise<void> {
131
+ log('updating smart facets');
132
+ if (this.query) {
133
+ this.heuristicRecs =
134
+ await new SmartQueryHeuristicGroup().getRecommendedFacets(this.query);
135
+ log('heuristic recs are', this.heuristicRecs);
136
+ this.smartFacets = dedupe(this.facetsToDisplay);
137
+ log('smart facets are', this.smartFacets);
138
+ }
139
+ }
140
+
141
+ //
142
+ // OTHER METHODS
143
+ //
144
+
145
+ private makeSmartFacet(facets: SmartFacet[]) {
146
+ if (facets.length === 0) {
147
+ return nothing;
148
+ }
149
+ if (facets.length === 1) {
150
+ return this.smartFacetButton(facets[0]);
151
+ }
152
+ return this.smartFacetDropdown(facets);
153
+ }
154
+
155
+ private smartFacetButton(facet: SmartFacet) {
156
+ return html`
157
+ <smart-facet-button
158
+ .facetInfo=${facet}
159
+ .labelPrefix=${fieldPrefixes[facet.facets[0].facetType]}
160
+ .selected=${facet.selected ?? false}
161
+ @facetClick=${this.facetClicked}
162
+ ></smart-facet-button>
163
+ `;
164
+ }
165
+
166
+ private smartFacetDropdown(facets: SmartFacet[]) {
167
+ return html`
168
+ <smart-facet-dropdown
169
+ .facetInfo=${facets}
170
+ .labelPrefix=${fieldPrefixes[facets[0].facets[0].facetType]}
171
+ .activeFacetRef=${facets[0].facets[0]}
172
+ @facetClick=${this.dropdownOptionClicked}
173
+ @dropdownClick=${this.dropdownClicked}
174
+ ></smart-facet-dropdown>
175
+ `;
176
+ }
177
+
178
+ private get filtersToggleTemplate(): TemplateResult | typeof nothing {
179
+ if (!this.filterToggleShown) return nothing;
180
+
181
+ return html`
182
+ <button
183
+ id="filters-toggle"
184
+ class=${this.filterToggleActive ? 'active' : ''}
185
+ title="${this.filterToggleActive ? 'Hide' : 'Show'} filters pane"
186
+ @click=${this.filterToggleClicked}
187
+ >
188
+ ${filterIcon}
189
+ </button>
190
+ `;
191
+ }
192
+
193
+ private get facetsToDisplay(): SmartFacet[][] {
194
+ const facets: SmartFacet[][] = [];
195
+
196
+ if (this.heuristicRecs.length > 0) {
197
+ for (const rec of this.heuristicRecs) {
198
+ // Suppress mediatype-only facets for now.
199
+ if (rec.facets.length === 1 && rec.facets[0].facetType === 'mediatype')
200
+ continue;
201
+
202
+ facets.push([rec]);
203
+ }
204
+ }
205
+
206
+ if (this.lastAggregations) {
207
+ const keys = [
208
+ 'mediatype',
209
+ 'year',
210
+ 'language',
211
+ 'creator',
212
+ 'subject',
213
+ 'collection',
214
+ ];
215
+ for (const key of keys) {
216
+ const agg = this.lastAggregations[key];
217
+ if (!agg) continue;
218
+ if (agg.buckets.length === 0) continue;
219
+ if (['lending', 'year_histogram', 'date_histogram'].includes(key))
220
+ continue;
221
+ if (typeof agg.buckets[0] === 'number') continue;
222
+
223
+ if (
224
+ key === 'mediatype' &&
225
+ this.selectedFacets &&
226
+ Object.values(this.selectedFacets.mediatype ?? {}).some(
227
+ bucket => bucket.state !== 'none',
228
+ )
229
+ ) {
230
+ continue;
231
+ }
232
+
233
+ const facetType = key as FacetOption;
234
+ const buckets = agg.buckets as Bucket[];
235
+
236
+ const unusedBuckets = buckets.filter(b => {
237
+ const selectedFacetBucket = this.selectedFacets?.[facetType]?.[b.key];
238
+ if (selectedFacetBucket && selectedFacetBucket.state !== 'none') {
239
+ return false;
240
+ }
241
+ return true;
242
+ });
243
+
244
+ if (facetType === 'mediatype') {
245
+ continue;
246
+ // Don't include mediatype bubbles
247
+ } else if (facetType === 'collection' || facetType === 'subject') {
248
+ const topBuckets = unusedBuckets.slice(0, 5);
249
+ facets.push(topBuckets.map(b => this.toSmartFacet(facetType, [b])));
250
+ } else {
251
+ facets.push([this.toSmartFacet(facetType, [unusedBuckets[0]])]);
252
+ }
253
+ }
254
+ }
255
+
256
+ return facets;
257
+ }
258
+
259
+ private toSmartFacet(
260
+ facetType: FacetOption,
261
+ buckets: Bucket[],
262
+ // prefix = true
263
+ ): SmartFacet {
264
+ return {
265
+ facets: buckets.map(bucket => {
266
+ let displayText = capitalize(bucket.key.toString());
267
+ if (facetType === 'collection') {
268
+ const title = this.collectionTitles?.get(bucket.key.toString());
269
+ if (title) displayText = title;
270
+ }
271
+
272
+ return {
273
+ facetType,
274
+ bucketKey: bucket.key.toString(),
275
+ displayText,
276
+ };
277
+ }),
278
+ } as SmartFacet;
279
+ }
280
+
281
+ /**
282
+ * Toggles the state of the given smart facet, and updates the selected facets accordingly.
283
+ */
284
+ private toggleSmartFacet(
285
+ facet: SmartFacet,
286
+ details: FacetEventDetails[],
287
+ ): void {
288
+ let newState: FacetState;
289
+ if (facet.selected) {
290
+ // When deselected, leave the smart facet where it is
291
+ newState = 'none';
292
+ this.smartFacets = this.smartFacets.map(f => {
293
+ if (f[0] === facet) return [{ ...facet, selected: false }];
294
+ return f;
295
+ });
296
+ } else {
297
+ // When selected, move the toggled smart facet to the front of the list
298
+ newState = 'selected';
299
+ this.smartFacets = [
300
+ [{ ...facet, selected: true }],
301
+ ...this.smartFacets.filter(f => f[0] !== facet),
302
+ ];
303
+ }
304
+
305
+ this.updateSelectedFacets(
306
+ details.map(facet => ({
307
+ ...facet,
308
+ bucket: {
309
+ ...facet.bucket,
310
+ state: newState,
311
+ },
312
+ })),
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Updates the selected facet buckets for each of the given facets,
318
+ * and emits a `facetsChanged` event to notify parent components of
319
+ * the new state.
320
+ */
321
+ private updateSelectedFacets(facets: FacetEventDetails[]): void {
322
+ for (const facet of facets) {
323
+ this.selectedFacets = updateSelectedFacetBucket(
324
+ this.selectedFacets,
325
+ facet.facetType,
326
+ facet.bucket,
327
+ true,
328
+ );
329
+ }
330
+
331
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
332
+ detail: this.selectedFacets,
333
+ });
334
+ this.dispatchEvent(event);
335
+ }
336
+
337
+ /**
338
+ * Handler for when a smart facet button is clicked
339
+ */
340
+ private facetClicked(e: CustomEvent<SmartFacetEvent>): void {
341
+ this.toggleSmartFacet(e.detail.smartFacet, e.detail.details);
342
+ }
343
+
344
+ /**
345
+ * Handler for when an option in a smart facet dropdown menu is selected
346
+ */
347
+ private dropdownOptionClicked(e: CustomEvent<SmartFacetEvent>): void {
348
+ const existingFacet = this.smartFacets.find(
349
+ sf => sf.length === 1 && smartFacetEquals(sf[0], e.detail.smartFacet),
350
+ );
351
+ if (existingFacet) {
352
+ // The facet already exists outside the dropdown, so just select it there
353
+ this.toggleSmartFacet(existingFacet[0], e.detail.details);
354
+ return;
355
+ }
356
+
357
+ // Otherwise, prepend a new smart facet for the selected option
358
+ this.smartFacets = [
359
+ [{ ...e.detail.smartFacet, selected: true }],
360
+ ...this.smartFacets,
361
+ ];
362
+
363
+ this.updateSelectedFacets(e.detail.details);
364
+ }
365
+
366
+ /**
367
+ * Handler for when any dropdown is clicked (whether button, caret, or menu item)
368
+ */
369
+ private dropdownClicked(e: CustomEvent<SmartFacetDropdown>): void {
370
+ log('smart bar: onDropdownClick', e.detail);
371
+ this.shadowRoot
372
+ ?.querySelectorAll('smart-facet-dropdown')
373
+ .forEach(dropdown => {
374
+ if (dropdown !== e.detail) {
375
+ log('closing', dropdown);
376
+ (dropdown as SmartFacetDropdown).close();
377
+ }
378
+ });
379
+ }
380
+
381
+ private filterToggleClicked(): void {
382
+ this.dispatchEvent(new CustomEvent('filtersToggled'));
383
+ }
384
+
385
+ //
386
+ // STYLES
387
+ //
388
+
389
+ static get styles(): CSSResultGroup {
390
+ return css`
391
+ #smart-facets-container {
392
+ display: flex;
393
+ align-items: center;
394
+ gap: 5px 10px;
395
+ padding: 10px 0;
396
+ white-space: nowrap;
397
+ overflow: scroll hidden;
398
+ scrollbar-width: none;
399
+ }
400
+
401
+ #filters-toggle {
402
+ margin: 0;
403
+ border: 0;
404
+ padding: 5px 8px;
405
+ border-radius: 5px;
406
+ background: white;
407
+ color: #2c2c2c;
408
+ border: 1px solid #194880;
409
+ font-size: 1.4rem;
410
+ font-family: inherit;
411
+ text-decoration: none;
412
+ cursor: pointer;
413
+ }
414
+
415
+ #filters-toggle.active {
416
+ background: #194880;
417
+ color: white;
418
+ }
419
+
420
+ #filters-toggle > svg {
421
+ width: 12px;
422
+ filter: invert(0.16667);
423
+ vertical-align: -1px;
424
+ }
425
+
426
+ #filters-toggle.active > svg {
427
+ filter: invert(1);
428
+ }
429
+
430
+ #filters-label {
431
+ font-size: 1.4rem;
432
+ font-weight: var(--smartFacetLabelFontWeight, normal);
433
+ margin: 0 -5px 0 0;
434
+ }
435
+ `;
436
+ }
437
+ }