@internetarchive/collection-browser 2.18.3-alpha-webdev7768.2 → 2.18.3-alpha-webdev7768.3

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