@internetarchive/collection-browser 0.3.4 → 0.3.5-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 (212) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +26 -26
  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/LICENSE +661 -661
  8. package/README.md +83 -83
  9. package/dist/index.d.ts +9 -9
  10. package/dist/index.js +9 -9
  11. package/dist/src/app-root.d.ts +43 -43
  12. package/dist/src/app-root.js +233 -233
  13. package/dist/src/assets/img/icons/arrow-left.d.ts +2 -2
  14. package/dist/src/assets/img/icons/arrow-left.js +2 -2
  15. package/dist/src/assets/img/icons/arrow-right.d.ts +2 -2
  16. package/dist/src/assets/img/icons/arrow-right.js +2 -2
  17. package/dist/src/assets/img/icons/chevron.d.ts +2 -2
  18. package/dist/src/assets/img/icons/chevron.js +2 -2
  19. package/dist/src/assets/img/icons/empty-query.d.ts +2 -2
  20. package/dist/src/assets/img/icons/empty-query.js +2 -2
  21. package/dist/src/assets/img/icons/eye-closed.d.ts +2 -2
  22. package/dist/src/assets/img/icons/eye-closed.js +2 -2
  23. package/dist/src/assets/img/icons/eye.d.ts +2 -2
  24. package/dist/src/assets/img/icons/eye.js +2 -2
  25. package/dist/src/assets/img/icons/favorite-filled.d.ts +1 -1
  26. package/dist/src/assets/img/icons/favorite-filled.js +2 -2
  27. package/dist/src/assets/img/icons/login-required.d.ts +1 -1
  28. package/dist/src/assets/img/icons/login-required.js +2 -2
  29. package/dist/src/assets/img/icons/mediatype/account.d.ts +1 -1
  30. package/dist/src/assets/img/icons/mediatype/account.js +2 -2
  31. package/dist/src/assets/img/icons/mediatype/audio.d.ts +1 -1
  32. package/dist/src/assets/img/icons/mediatype/audio.js +2 -2
  33. package/dist/src/assets/img/icons/mediatype/collection.d.ts +1 -1
  34. package/dist/src/assets/img/icons/mediatype/collection.js +2 -2
  35. package/dist/src/assets/img/icons/mediatype/data.d.ts +1 -1
  36. package/dist/src/assets/img/icons/mediatype/data.js +2 -2
  37. package/dist/src/assets/img/icons/mediatype/etree.d.ts +1 -1
  38. package/dist/src/assets/img/icons/mediatype/etree.js +2 -2
  39. package/dist/src/assets/img/icons/mediatype/film.d.ts +1 -1
  40. package/dist/src/assets/img/icons/mediatype/film.js +2 -2
  41. package/dist/src/assets/img/icons/mediatype/images.d.ts +1 -1
  42. package/dist/src/assets/img/icons/mediatype/images.js +2 -2
  43. package/dist/src/assets/img/icons/mediatype/radio.d.ts +1 -1
  44. package/dist/src/assets/img/icons/mediatype/radio.js +2 -2
  45. package/dist/src/assets/img/icons/mediatype/software.d.ts +1 -1
  46. package/dist/src/assets/img/icons/mediatype/software.js +2 -2
  47. package/dist/src/assets/img/icons/mediatype/texts.d.ts +1 -1
  48. package/dist/src/assets/img/icons/mediatype/texts.js +2 -2
  49. package/dist/src/assets/img/icons/mediatype/tv.d.ts +1 -1
  50. package/dist/src/assets/img/icons/mediatype/tv.js +2 -2
  51. package/dist/src/assets/img/icons/mediatype/video.d.ts +1 -1
  52. package/dist/src/assets/img/icons/mediatype/video.js +2 -2
  53. package/dist/src/assets/img/icons/mediatype/web.d.ts +1 -1
  54. package/dist/src/assets/img/icons/mediatype/web.js +2 -2
  55. package/dist/src/assets/img/icons/null-result.d.ts +2 -2
  56. package/dist/src/assets/img/icons/null-result.js +2 -2
  57. package/dist/src/assets/img/icons/restricted.d.ts +1 -1
  58. package/dist/src/assets/img/icons/restricted.js +2 -2
  59. package/dist/src/assets/img/icons/reviews.d.ts +1 -1
  60. package/dist/src/assets/img/icons/reviews.js +2 -2
  61. package/dist/src/assets/img/icons/upload.d.ts +1 -1
  62. package/dist/src/assets/img/icons/upload.js +2 -2
  63. package/dist/src/assets/img/icons/views.d.ts +1 -1
  64. package/dist/src/assets/img/icons/views.js +2 -2
  65. package/dist/src/circular-activity-indicator.d.ts +5 -5
  66. package/dist/src/circular-activity-indicator.js +17 -17
  67. package/dist/src/collection-browser.d.ts +245 -247
  68. package/dist/src/collection-browser.js +1359 -1398
  69. package/dist/src/collection-browser.js.map +1 -1
  70. package/dist/src/collection-facets/facet-tombstone-row.d.ts +5 -0
  71. package/dist/src/collection-facets/facet-tombstone-row.js +43 -0
  72. package/dist/src/collection-facets/facet-tombstone-row.js.map +1 -0
  73. package/dist/src/collection-facets/facets-template.d.ts +16 -16
  74. package/dist/src/collection-facets/facets-template.js +125 -125
  75. package/dist/src/collection-facets/more-facets-content.d.ts +76 -76
  76. package/dist/src/collection-facets/more-facets-content.js +353 -353
  77. package/dist/src/collection-facets/more-facets-pagination.d.ts +36 -36
  78. package/dist/src/collection-facets/more-facets-pagination.js +192 -192
  79. package/dist/src/collection-facets.d.ts +77 -75
  80. package/dist/src/collection-facets.js +551 -525
  81. package/dist/src/collection-facets.js.map +1 -1
  82. package/dist/src/empty-placeholder.d.ts +11 -11
  83. package/dist/src/empty-placeholder.js +42 -42
  84. package/dist/src/language-code-handler/language-code-handler.d.ts +37 -37
  85. package/dist/src/language-code-handler/language-code-handler.js +26 -26
  86. package/dist/src/language-code-handler/language-code-mapping.d.ts +1 -1
  87. package/dist/src/language-code-handler/language-code-mapping.js +562 -562
  88. package/dist/src/mediatype/mediatype-config.d.ts +3 -3
  89. package/dist/src/mediatype/mediatype-config.js +85 -85
  90. package/dist/src/models.d.ts +102 -102
  91. package/dist/src/models.js +117 -117
  92. package/dist/src/restoration-state-handler.d.ts +45 -45
  93. package/dist/src/restoration-state-handler.js +220 -220
  94. package/dist/src/sort-filter-bar/alpha-bar.d.ts +9 -9
  95. package/dist/src/sort-filter-bar/alpha-bar.js +41 -41
  96. package/dist/src/sort-filter-bar/img/compact.d.ts +1 -1
  97. package/dist/src/sort-filter-bar/img/compact.js +2 -2
  98. package/dist/src/sort-filter-bar/img/list.d.ts +1 -1
  99. package/dist/src/sort-filter-bar/img/list.js +2 -2
  100. package/dist/src/sort-filter-bar/img/sort-triangle.d.ts +1 -1
  101. package/dist/src/sort-filter-bar/img/sort-triangle.js +2 -2
  102. package/dist/src/sort-filter-bar/img/tile.d.ts +1 -1
  103. package/dist/src/sort-filter-bar/img/tile.js +2 -2
  104. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +107 -107
  105. package/dist/src/sort-filter-bar/sort-filter-bar.js +423 -423
  106. package/dist/src/styles/item-image-styles.d.ts +8 -8
  107. package/dist/src/styles/item-image-styles.js +9 -9
  108. package/dist/src/tiles/collection-browser-loading-tile.d.ts +5 -5
  109. package/dist/src/tiles/collection-browser-loading-tile.js +15 -15
  110. package/dist/src/tiles/grid/account-tile.d.ts +8 -8
  111. package/dist/src/tiles/grid/account-tile.js +20 -20
  112. package/dist/src/tiles/grid/collection-tile.d.ts +7 -7
  113. package/dist/src/tiles/grid/collection-tile.js +23 -23
  114. package/dist/src/tiles/grid/item-tile.d.ts +24 -24
  115. package/dist/src/tiles/grid/item-tile.js +87 -87
  116. package/dist/src/tiles/grid/tile-stats.d.ts +10 -10
  117. package/dist/src/tiles/grid/tile-stats.js +35 -35
  118. package/dist/src/tiles/image-block.d.ts +17 -17
  119. package/dist/src/tiles/image-block.js +69 -69
  120. package/dist/src/tiles/item-image.d.ts +31 -31
  121. package/dist/src/tiles/item-image.js +103 -103
  122. package/dist/src/tiles/list/account-label.d.ts +1 -1
  123. package/dist/src/tiles/list/account-label.js +6 -6
  124. package/dist/src/tiles/list/date-label.d.ts +1 -1
  125. package/dist/src/tiles/list/date-label.js +12 -12
  126. package/dist/src/tiles/list/tile-list-compact-header.d.ts +12 -12
  127. package/dist/src/tiles/list/tile-list-compact-header.js +41 -41
  128. package/dist/src/tiles/list/tile-list-compact.d.ts +20 -20
  129. package/dist/src/tiles/list/tile-list-compact.js +87 -87
  130. package/dist/src/tiles/list/tile-list.d.ts +50 -50
  131. package/dist/src/tiles/list/tile-list.js +268 -268
  132. package/dist/src/tiles/mediatype-icon.d.ts +9 -9
  133. package/dist/src/tiles/mediatype-icon.js +47 -47
  134. package/dist/src/tiles/overlay/icon-overlay.d.ts +7 -7
  135. package/dist/src/tiles/overlay/icon-overlay.js +30 -30
  136. package/dist/src/tiles/overlay/text-overlay.d.ts +8 -8
  137. package/dist/src/tiles/overlay/text-overlay.js +31 -31
  138. package/dist/src/tiles/text-snippet-block.d.ts +29 -29
  139. package/dist/src/tiles/text-snippet-block.js +81 -81
  140. package/dist/src/tiles/tile-dispatcher.d.ts +36 -36
  141. package/dist/src/tiles/tile-dispatcher.js +128 -128
  142. package/dist/src/utils/analytics-events.d.ts +22 -22
  143. package/dist/src/utils/analytics-events.js +24 -24
  144. package/dist/src/utils/format-count.d.ts +7 -7
  145. package/dist/src/utils/format-count.js +76 -76
  146. package/dist/src/utils/format-date.d.ts +2 -2
  147. package/dist/src/utils/format-date.js +23 -23
  148. package/dist/test/collection-browser.test.d.ts +1 -1
  149. package/dist/test/collection-browser.test.js +415 -359
  150. package/dist/test/collection-browser.test.js.map +1 -1
  151. package/dist/test/collection-facets/facets-template.test.d.ts +1 -1
  152. package/dist/test/collection-facets/facets-template.test.js +62 -62
  153. package/dist/test/collection-facets/more-facets-content.test.d.ts +1 -1
  154. package/dist/test/collection-facets/more-facets-content.test.js +114 -114
  155. package/dist/test/collection-facets/more-facets-pagination.test.d.ts +1 -1
  156. package/dist/test/collection-facets/more-facets-pagination.test.js +117 -117
  157. package/dist/test/collection-facets.test.d.ts +2 -2
  158. package/dist/test/collection-facets.test.js +498 -498
  159. package/dist/test/empty-placeholder.test.d.ts +1 -1
  160. package/dist/test/empty-placeholder.test.js +33 -33
  161. package/dist/test/icon-overlay.test.d.ts +1 -1
  162. package/dist/test/icon-overlay.test.js +24 -24
  163. package/dist/test/image-block.test.d.ts +1 -1
  164. package/dist/test/image-block.test.js +48 -48
  165. package/dist/test/item-image.test.d.ts +1 -1
  166. package/dist/test/item-image.test.js +56 -56
  167. package/dist/test/mediatype-config.test.d.ts +1 -1
  168. package/dist/test/mediatype-config.test.js +16 -16
  169. package/dist/test/mocks/mock-analytics-handler.d.ts +10 -10
  170. package/dist/test/mocks/mock-analytics-handler.js +15 -15
  171. package/dist/test/mocks/mock-collection-name-cache.d.ts +7 -7
  172. package/dist/test/mocks/mock-collection-name-cache.js +13 -13
  173. package/dist/test/mocks/mock-search-responses.d.ts +8 -5
  174. package/dist/test/mocks/mock-search-responses.js +198 -103
  175. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  176. package/dist/test/mocks/mock-search-service.d.ts +13 -13
  177. package/dist/test/mocks/mock-search-service.js +32 -25
  178. package/dist/test/mocks/mock-search-service.js.map +1 -1
  179. package/dist/test/restoration-state-handler.test.d.ts +1 -1
  180. package/dist/test/restoration-state-handler.test.js +117 -117
  181. package/dist/test/sort-filter-bar/sort-filter-bar.test.d.ts +1 -1
  182. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +113 -113
  183. package/dist/test/text-overlay.test.d.ts +1 -1
  184. package/dist/test/text-overlay.test.js +41 -41
  185. package/dist/test/text-snippet-block.test.d.ts +1 -1
  186. package/dist/test/text-snippet-block.test.js +57 -57
  187. package/dist/test/tile-stats.test.d.ts +1 -1
  188. package/dist/test/tile-stats.test.js +33 -33
  189. package/dist/test/tiles/grid/item-tile.test.d.ts +1 -1
  190. package/dist/test/tiles/grid/item-tile.test.js +107 -107
  191. package/dist/test/tiles/list/tile-list-compact.test.d.ts +1 -1
  192. package/dist/test/tiles/list/tile-list-compact.test.js +26 -26
  193. package/dist/test/tiles/list/tile-list.test.d.ts +1 -1
  194. package/dist/test/tiles/list/tile-list.test.js +47 -47
  195. package/dist/test/utils/format-count.test.d.ts +1 -1
  196. package/dist/test/utils/format-count.test.js +23 -23
  197. package/dist/test/utils/format-date.test.d.ts +1 -1
  198. package/dist/test/utils/format-date.test.js +17 -17
  199. package/index.html +24 -24
  200. package/local.archive.org.cert +86 -86
  201. package/local.archive.org.key +27 -27
  202. package/package.json +115 -115
  203. package/renovate.json +6 -6
  204. package/src/collection-browser.ts +1488 -1525
  205. package/src/collection-facets/facet-tombstone-row.ts +40 -0
  206. package/src/collection-facets.ts +635 -604
  207. package/test/collection-browser.test.ts +599 -513
  208. package/test/mocks/mock-search-responses.ts +226 -119
  209. package/test/mocks/mock-search-service.ts +61 -53
  210. package/tsconfig.json +21 -21
  211. package/web-dev-server.config.mjs +30 -30
  212. package/web-test-runner.config.mjs +41 -41
@@ -1,604 +1,635 @@
1
- /* eslint-disable import/no-duplicates */
2
- import {
3
- css,
4
- html,
5
- LitElement,
6
- PropertyValues,
7
- nothing,
8
- TemplateResult,
9
- } from 'lit';
10
- import { customElement, property, state } from 'lit/decorators.js';
11
- import type {
12
- Aggregation,
13
- Bucket,
14
- SearchServiceInterface,
15
- SearchType,
16
- } from '@internetarchive/search-service';
17
- import '@internetarchive/histogram-date-range';
18
- import '@internetarchive/feature-feedback';
19
- import '@internetarchive/collection-name-cache';
20
- import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
21
- import {
22
- ModalConfig,
23
- ModalManagerInterface,
24
- } from '@internetarchive/modal-manager';
25
- import type { AnalyticsManagerInterface } from '@internetarchive/analytics-manager';
26
- import chevronIcon from './assets/img/icons/chevron';
27
- import {
28
- FacetOption,
29
- SelectedFacets,
30
- FacetGroup,
31
- FacetBucket,
32
- facetDisplayOrder,
33
- facetTitles,
34
- lendingFacetDisplayNames,
35
- lendingFacetKeysVisibility,
36
- LendingFacetKey,
37
- suppressedCollections,
38
- } from './models';
39
- import type { LanguageCodeHandlerInterface } from './language-code-handler/language-code-handler';
40
- import './collection-facets/more-facets-content';
41
- import './collection-facets/facets-template';
42
- import {
43
- analyticsActions,
44
- analyticsCategories,
45
- } from './utils/analytics-events';
46
-
47
- @customElement('collection-facets')
48
- export class CollectionFacets extends LitElement {
49
- @property({ type: Object }) searchService?: SearchServiceInterface;
50
-
51
- @property({ type: String }) searchType?: SearchType;
52
-
53
- @property({ type: Object }) aggregations?: Record<string, Aggregation>;
54
-
55
- @property({ type: Object }) fullYearsHistogramAggregation?: Aggregation;
56
-
57
- @property({ type: String }) minSelectedDate?: string;
58
-
59
- @property({ type: String }) maxSelectedDate?: string;
60
-
61
- @property({ type: Boolean }) moreLinksVisible = true;
62
-
63
- @property({ type: Boolean }) facetsLoading = false;
64
-
65
- @property({ type: Boolean }) fullYearAggregationLoading = false;
66
-
67
- @property({ type: Object }) selectedFacets?: SelectedFacets;
68
-
69
- @property({ type: Boolean }) collapsableFacets = false;
70
-
71
- @property({ type: Boolean }) showHistogramDatePicker = false;
72
-
73
- @property({ type: String }) fullQuery?: string;
74
-
75
- @property({ type: Object, attribute: false })
76
- modalManager?: ModalManagerInterface;
77
-
78
- @property({ type: Object, attribute: false })
79
- analyticsHandler?: AnalyticsManagerInterface;
80
-
81
- @property({ type: Object })
82
- languageCodeHandler?: LanguageCodeHandlerInterface;
83
-
84
- @property({ type: Object })
85
- collectionNameCache?: CollectionNameCacheInterface;
86
-
87
- /** Fires when a facet is clicked */
88
- @property({ type: Function }) onFacetClick?: (
89
- name: FacetOption,
90
- facetChecked: boolean,
91
- negative: boolean
92
- ) => void;
93
-
94
- @state() openFacets: Record<FacetOption, boolean> = {
95
- subject: false,
96
- lending: false,
97
- mediatype: false,
98
- language: false,
99
- creator: false,
100
- collection: false,
101
- year: false,
102
- };
103
-
104
- @property({ type: Object, attribute: false })
105
-
106
- /**
107
- * render number of facet items
108
- */
109
- private allowedFacetCount = 6;
110
-
111
- render() {
112
- return html`
113
- <div id="container" class="${this.facetsLoading ? 'loading' : ''}">
114
- ${this.showHistogramDatePicker && this.fullYearsHistogramAggregation
115
- ? html`
116
- <div class="facet-group">
117
- <h1>Year Published <feature-feedback></feature-feedback></h1>
118
- ${this.histogramTemplate}
119
- </div>
120
- `
121
- : nothing}
122
- ${this.mergedFacets.map(facetGroup =>
123
- this.getFacetGroupTemplate(facetGroup)
124
- )}
125
- </div>
126
- `;
127
- }
128
-
129
- updated(changed: PropertyValues) {
130
- if (changed.has('selectedFacets')) {
131
- this.dispatchFacetsChangedEvent();
132
- }
133
- }
134
-
135
- // TODO: want to fire analytics?
136
- private dispatchFacetsChangedEvent() {
137
- const event = new CustomEvent<SelectedFacets>('facetsChanged', {
138
- detail: this.selectedFacets,
139
- });
140
- this.dispatchEvent(event);
141
- }
142
-
143
- private get currentYearsHistogramAggregation(): Aggregation | undefined {
144
- return this.aggregations?.year_histogram;
145
- }
146
-
147
- private get histogramTemplate() {
148
- const { fullYearsHistogramAggregation } = this;
149
- return html`
150
- <histogram-date-range
151
- .minDate=${fullYearsHistogramAggregation?.first_bucket_key}
152
- .maxDate=${fullYearsHistogramAggregation?.last_bucket_key}
153
- .minSelectedDate=${this.minSelectedDate}
154
- .maxSelectedDate=${this.maxSelectedDate}
155
- .updateDelay=${100}
156
- missingDataMessage="..."
157
- .width=${180}
158
- .bins=${fullYearsHistogramAggregation?.buckets as number[]}
159
- @histogramDateRangeUpdated=${this.histogramDateRangeUpdated}
160
- ></histogram-date-range>
161
- `;
162
- }
163
-
164
- private histogramDateRangeUpdated(
165
- e: CustomEvent<{
166
- minDate: string;
167
- maxDate: string;
168
- }>
169
- ) {
170
- const { minDate, maxDate } = e.detail;
171
- const event = new CustomEvent('histogramDateRangeUpdated', {
172
- detail: { minDate, maxDate },
173
- });
174
- this.dispatchEvent(event);
175
- }
176
-
177
- /**
178
- * Combines the selected facets with the aggregations to create a single list of facets
179
- */
180
- private get mergedFacets(): FacetGroup[] {
181
- const facetGroups: FacetGroup[] = [];
182
-
183
- facetDisplayOrder.forEach(facetKey => {
184
- const selectedFacetGroup = this.selectedFacetGroups.find(
185
- group => group.key === facetKey
186
- );
187
- const aggregateFacetGroup = this.aggregationFacetGroups.find(
188
- group => group.key === facetKey
189
- );
190
-
191
- // if the user selected a facet, but it's not in the aggregation, we add it as-is
192
- if (selectedFacetGroup && !aggregateFacetGroup) {
193
- facetGroups.push(selectedFacetGroup);
194
- return;
195
- }
196
-
197
- // if we don't have an aggregate facet group, don't add this to the list
198
- if (!aggregateFacetGroup) return;
199
-
200
- // start with either the selected group if we have one, or the aggregate group
201
- const facetGroup = selectedFacetGroup ?? aggregateFacetGroup;
202
-
203
- // attach the counts to the selected buckets
204
- let bucketsWithCount =
205
- selectedFacetGroup?.buckets.map(bucket => {
206
- const selectedBucket = aggregateFacetGroup.buckets.find(
207
- b => b.key === bucket.key
208
- );
209
- return selectedBucket
210
- ? {
211
- ...bucket,
212
- count: selectedBucket.count,
213
- }
214
- : bucket;
215
- }) ?? [];
216
-
217
- // append any additional buckets that were not selected
218
- aggregateFacetGroup.buckets.forEach(bucket => {
219
- const existingBucket = bucketsWithCount.find(b => b.key === bucket.key);
220
- if (existingBucket) return;
221
- bucketsWithCount.push(bucket);
222
- });
223
-
224
- // For lending facets, only include a specific subset of buckets
225
- if (facetKey === 'lending') {
226
- bucketsWithCount = bucketsWithCount.filter(
227
- bucket => lendingFacetKeysVisibility[bucket.key as LendingFacetKey]
228
- );
229
- }
230
-
231
- /**
232
- * render limited facet items on page facet area
233
- *
234
- * - by-default we are showing 6 items
235
- * - additionally want to show all items (selected/suppressed) in page facet area
236
- */
237
- let allowedFacetCount = Object.keys(
238
- (selectedFacetGroup?.buckets as []) || []
239
- )?.length;
240
- if (allowedFacetCount < this.allowedFacetCount) {
241
- allowedFacetCount = this.allowedFacetCount; // splice start index from 0th
242
- }
243
-
244
- // splice how many items we want to show in page facet area
245
- facetGroup.buckets = bucketsWithCount.splice(0, allowedFacetCount);
246
-
247
- facetGroups.push(facetGroup);
248
- });
249
-
250
- return facetGroups;
251
- }
252
-
253
- /**
254
- * Converts the selected facets to a `FacetGroup` array,
255
- * which is easier to work with
256
- */
257
- private get selectedFacetGroups(): FacetGroup[] {
258
- if (!this.selectedFacets) return [];
259
-
260
- const facetGroups: FacetGroup[] = Object.entries(this.selectedFacets).map(
261
- ([key, selectedFacets]) => {
262
- const option = key as FacetOption;
263
- const title = facetTitles[option];
264
-
265
- const buckets: FacetBucket[] = Object.entries(selectedFacets).map(
266
- ([value, facetData]) => {
267
- let displayText: string = value;
268
- // for selected languages, we store the language code instead of the
269
- // display name, so look up the name from the mapping
270
- if (option === 'language') {
271
- displayText =
272
- this.languageCodeHandler?.getLanguageNameFromCodeString(
273
- value
274
- ) ?? value;
275
- }
276
- // for lending facets, convert the key to a readable format
277
- if (option === 'lending') {
278
- displayText =
279
- lendingFacetDisplayNames[value as LendingFacetKey] ?? value;
280
- }
281
- return {
282
- displayText,
283
- key: value,
284
- count: facetData.count,
285
- state: facetData.state,
286
- };
287
- }
288
- );
289
-
290
- return {
291
- title,
292
- key: option,
293
- buckets,
294
- };
295
- }
296
- );
297
-
298
- return facetGroups;
299
- }
300
-
301
- /**
302
- * Converts the raw `aggregations` to `FacetGroups`, which are easier to use
303
- */
304
- private get aggregationFacetGroups(): FacetGroup[] {
305
- const facetGroups: FacetGroup[] = [];
306
- Object.entries(this.aggregations ?? []).forEach(([key, buckets]) => {
307
- // the year_histogram data is in a different format so can't be handled here
308
- if (key === 'year_histogram') return;
309
-
310
- const option = key as FacetOption;
311
- const title = facetTitles[option];
312
- if (!title) return;
313
-
314
- let castedBuckets = buckets.buckets as Bucket[];
315
-
316
- if (option === 'collection') {
317
- // we are not showing fav- collections or certain deemphasized collections in facets
318
- castedBuckets = castedBuckets?.filter(bucket => {
319
- const bucketKey = bucket?.key?.toString();
320
- return (
321
- !suppressedCollections[bucketKey] && !bucketKey?.startsWith('fav-')
322
- );
323
- });
324
- }
325
-
326
- const facetBuckets: FacetBucket[] = castedBuckets.map(bucket => {
327
- let bucketKey = bucket.key;
328
- let displayText = `${bucket.key}`;
329
- // for languages, we need to search by language code instead of the
330
- // display name, which is what we get from the search engine result
331
- if (option === 'language') {
332
- // const languageCodeKey = languageToCodeMap[bucket.key];
333
- bucketKey =
334
- this.languageCodeHandler?.getCodeStringFromLanguageName(
335
- `${bucket.key}`
336
- ) ?? bucket.key;
337
- // bucketKey = languageCodeKey ?? bucket.key;
338
- }
339
- // for lending facets, convert the bucket key to a readable format
340
- if (option === 'lending') {
341
- displayText =
342
- lendingFacetDisplayNames[bucket.key as LendingFacetKey] ??
343
- `${bucket.key}`;
344
- }
345
- return {
346
- displayText,
347
- key: `${bucketKey}`,
348
- count: bucket.doc_count,
349
- state: 'none',
350
- };
351
- });
352
- const group: FacetGroup = {
353
- title,
354
- key: option,
355
- buckets: facetBuckets,
356
- };
357
- facetGroups.push(group);
358
- });
359
- return facetGroups;
360
- }
361
-
362
- /**
363
- * Generate the template for a facet group with a header and the collapsible
364
- * chevron for the mobile view
365
- */
366
- private getFacetGroupTemplate(
367
- facetGroup: FacetGroup
368
- ): TemplateResult | typeof nothing {
369
- if (facetGroup.buckets.length === 0) return nothing;
370
- const { key } = facetGroup;
371
- const isOpen = this.openFacets[key];
372
- const collapser = html`
373
- <span class="collapser ${isOpen ? 'open' : ''}"> ${chevronIcon} </span>
374
- `;
375
-
376
- return html`
377
- <div class="facet-group ${this.collapsableFacets ? 'mobile' : ''}">
378
- <div class="facet-group-header">
379
- <h1
380
- @click=${() => {
381
- const newOpenFacets = { ...this.openFacets };
382
- newOpenFacets[key] = !isOpen;
383
- this.openFacets = newOpenFacets;
384
- }}
385
- @keyup=${() => {
386
- const newOpenFacets = { ...this.openFacets };
387
- newOpenFacets[key] = !isOpen;
388
- this.openFacets = newOpenFacets;
389
- }}
390
- >
391
- ${this.collapsableFacets ? collapser : nothing} ${facetGroup.title}
392
- </h1>
393
- ${this.moreFacetsSortingIcon(facetGroup)}
394
- </div>
395
- <div class="facet-group-content ${isOpen ? 'open' : ''}">
396
- ${this.getFacetTemplate(facetGroup)}
397
- ${this.searchMoreFacetsLink(facetGroup)}
398
- </div>
399
- </div>
400
- `;
401
- }
402
-
403
- private moreFacetsSortingIcon(
404
- facetGroup: FacetGroup
405
- ): TemplateResult | typeof nothing {
406
- // Display the sorting icon for every facet group except lending
407
- return facetGroup.key === 'lending'
408
- ? nothing
409
- : html`
410
- <input
411
- class="sorting-icon"
412
- type="image"
413
- @click=${() => this.showMoreFacetsModal(facetGroup, 'alpha')}
414
- src="https://archive.org/images/filter-count.png"
415
- alt="Sort alphabetically"
416
- />
417
- `;
418
- }
419
-
420
- /**
421
- * Generate the More... link button just below the facets group
422
- *
423
- * TODO: want to fire analytics?
424
- */
425
- private searchMoreFacetsLink(
426
- facetGroup: FacetGroup
427
- ): TemplateResult | typeof nothing {
428
- // Don't render More... links for FTS searches
429
- if (!this.moreLinksVisible) {
430
- return nothing;
431
- }
432
-
433
- // Don't render More... links for lending facets
434
- if (facetGroup.key === 'lending') {
435
- return nothing;
436
- }
437
-
438
- // Don't render More... link if the number of facets < this.allowedFacetCount
439
- if (Object.keys(facetGroup.buckets).length < this.allowedFacetCount) {
440
- return nothing;
441
- }
442
-
443
- return html`<button
444
- class="more-link"
445
- @click=${() => {
446
- this.showMoreFacetsModal(facetGroup, 'count');
447
- this.analyticsHandler?.sendEventNoSampling({
448
- category: analyticsCategories.default,
449
- action: analyticsActions.showMoreFacetsModal,
450
- label: facetGroup.key,
451
- });
452
- this.dispatchEvent(
453
- new CustomEvent('showMoreFacets', { detail: facetGroup.key })
454
- );
455
- }}
456
- >
457
- More...
458
- </button>`;
459
- }
460
-
461
- async showMoreFacetsModal(
462
- facetGroup: FacetGroup,
463
- sortedBy: string
464
- ): Promise<void> {
465
- const facetAggrKey = facetGroup.key;
466
-
467
- const customModalContent = html`
468
- <more-facets-content
469
- .analyticsHandler=${this.analyticsHandler}
470
- .facetKey=${facetGroup.key}
471
- .facetAggregationKey=${facetAggrKey}
472
- .fullQuery=${this.fullQuery}
473
- .modalManager=${this.modalManager}
474
- .searchService=${this.searchService}
475
- .searchType=${this.searchType}
476
- .collectionNameCache=${this.collectionNameCache}
477
- .languageCodeHandler=${this.languageCodeHandler}
478
- .selectedFacets=${this.selectedFacets}
479
- .sortedBy=${sortedBy}
480
- @facetsChanged=${(e: CustomEvent) => {
481
- const event = new CustomEvent<SelectedFacets>('facetsChanged', {
482
- detail: e.detail,
483
- bubbles: true,
484
- composed: true,
485
- });
486
- this.dispatchEvent(event);
487
- }}
488
- >
489
- </more-facets-content>
490
- `;
491
-
492
- const config = new ModalConfig({
493
- bodyColor: '#fff',
494
- headerColor: '#194880',
495
- showHeaderLogo: false,
496
- closeOnBackdropClick: true, // TODO: want to fire analytics
497
- title: html`Select filters`,
498
- });
499
- this.modalManager?.classList.add('more-search-facets');
500
- this.modalManager?.showModal({
501
- config,
502
- customModalContent,
503
- });
504
- }
505
-
506
- /**
507
- * Generate the list template for each bucket in a facet group
508
- */
509
- private getFacetTemplate(facetGroup: FacetGroup): TemplateResult {
510
- return html`
511
- <facets-template
512
- .facetGroup=${facetGroup}
513
- .selectedFacets=${this.selectedFacets}
514
- .renderOn=${'page'}
515
- .collectionNameCache=${this.collectionNameCache}
516
- @selectedFacetsChanged=${(e: CustomEvent) => {
517
- const event = new CustomEvent<SelectedFacets>('facetsChanged', {
518
- detail: e.detail,
519
- bubbles: true,
520
- composed: true,
521
- });
522
- this.dispatchEvent(event);
523
- }}
524
- ></facets-template>
525
- `;
526
- }
527
-
528
- static get styles() {
529
- return css`
530
- #container.loading {
531
- opacity: 0.5;
532
- }
533
-
534
- .collapser {
535
- display: inline-block;
536
- cursor: pointer;
537
- width: 10px;
538
- height: 10px;
539
- }
540
-
541
- .collapser svg {
542
- transition: transform 0.2s ease-in-out;
543
- }
544
-
545
- .collapser.open svg {
546
- transform: rotate(90deg);
547
- }
548
-
549
- .facet-group {
550
- margin-bottom: 2rem;
551
- }
552
-
553
- .facet-group h1 {
554
- margin-bottom: 0.7rem;
555
- }
556
-
557
- .facet-group.mobile h1 {
558
- cursor: pointer;
559
- }
560
-
561
- .facet-group-header {
562
- display: flex;
563
- margin-bottom: 0.7rem;
564
- justify-content: space-between;
565
- border-bottom: 1px solid rgb(232, 232, 232);
566
- }
567
-
568
- .facet-group-content {
569
- transition: max-height 0.2s ease-in-out;
570
- }
571
-
572
- .facet-group.mobile .facet-group-content {
573
- max-height: 0;
574
- overflow: hidden;
575
- }
576
-
577
- .facet-group.mobile .facet-group-content.open {
578
- max-height: 2000px;
579
- }
580
-
581
- h1 {
582
- font-size: 1.4rem;
583
- font-weight: 200
584
- padding-bottom: 3px;
585
- margin: 0;
586
- }
587
-
588
- .more-link {
589
- font-size: 1.2rem;
590
- text-decoration: none;
591
- padding: 0;
592
- background: inherit;
593
- border: 0;
594
- color: var(--ia-theme-link-color, #4b64ff);
595
- cursor: pointer;
596
- }
597
-
598
- .sorting-icon {
599
- height: 15px;
600
- cursor: pointer;
601
- }
602
- `;
603
- }
604
- }
1
+ /* eslint-disable import/no-duplicates */
2
+ import {
3
+ css,
4
+ html,
5
+ LitElement,
6
+ PropertyValues,
7
+ nothing,
8
+ TemplateResult,
9
+ } from 'lit';
10
+ import { customElement, property, state } from 'lit/decorators.js';
11
+ import { map } from 'lit/directives/map.js';
12
+ import type {
13
+ Aggregation,
14
+ Bucket,
15
+ SearchServiceInterface,
16
+ SearchType,
17
+ } from '@internetarchive/search-service';
18
+ import '@internetarchive/histogram-date-range';
19
+ import '@internetarchive/feature-feedback';
20
+ import '@internetarchive/collection-name-cache';
21
+ import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
22
+ import {
23
+ ModalConfig,
24
+ ModalManagerInterface,
25
+ } from '@internetarchive/modal-manager';
26
+ import type { AnalyticsManagerInterface } from '@internetarchive/analytics-manager';
27
+ import chevronIcon from './assets/img/icons/chevron';
28
+ import {
29
+ FacetOption,
30
+ SelectedFacets,
31
+ FacetGroup,
32
+ FacetBucket,
33
+ facetDisplayOrder,
34
+ facetTitles,
35
+ lendingFacetDisplayNames,
36
+ lendingFacetKeysVisibility,
37
+ LendingFacetKey,
38
+ suppressedCollections,
39
+ } from './models';
40
+ import type { LanguageCodeHandlerInterface } from './language-code-handler/language-code-handler';
41
+ import './collection-facets/more-facets-content';
42
+ import './collection-facets/facets-template';
43
+ import './collection-facets/facet-tombstone-row';
44
+ import {
45
+ analyticsActions,
46
+ analyticsCategories,
47
+ } from './utils/analytics-events';
48
+
49
+ @customElement('collection-facets')
50
+ export class CollectionFacets extends LitElement {
51
+ @property({ type: Object }) searchService?: SearchServiceInterface;
52
+
53
+ @property({ type: String }) searchType?: SearchType;
54
+
55
+ @property({ type: Object }) aggregations?: Record<string, Aggregation>;
56
+
57
+ @property({ type: Object }) fullYearsHistogramAggregation?: Aggregation;
58
+
59
+ @property({ type: String }) minSelectedDate?: string;
60
+
61
+ @property({ type: String }) maxSelectedDate?: string;
62
+
63
+ @property({ type: Boolean }) moreLinksVisible = true;
64
+
65
+ @property({ type: Boolean }) facetsLoading = false;
66
+
67
+ @property({ type: Boolean }) fullYearAggregationLoading = false;
68
+
69
+ @property({ type: Object }) selectedFacets?: SelectedFacets;
70
+
71
+ @property({ type: Boolean }) collapsableFacets = false;
72
+
73
+ @property({ type: Boolean }) showHistogramDatePicker = false;
74
+
75
+ @property({ type: String }) fullQuery?: string;
76
+
77
+ @property({ type: Object, attribute: false })
78
+ modalManager?: ModalManagerInterface;
79
+
80
+ @property({ type: Object, attribute: false })
81
+ analyticsHandler?: AnalyticsManagerInterface;
82
+
83
+ @property({ type: Object })
84
+ languageCodeHandler?: LanguageCodeHandlerInterface;
85
+
86
+ @property({ type: Object })
87
+ collectionNameCache?: CollectionNameCacheInterface;
88
+
89
+ /** Fires when a facet is clicked */
90
+ @property({ type: Function }) onFacetClick?: (
91
+ name: FacetOption,
92
+ facetChecked: boolean,
93
+ negative: boolean
94
+ ) => void;
95
+
96
+ @state() openFacets: Record<FacetOption, boolean> = {
97
+ subject: false,
98
+ lending: false,
99
+ mediatype: false,
100
+ language: false,
101
+ creator: false,
102
+ collection: false,
103
+ year: false,
104
+ };
105
+
106
+ @property({ type: Object, attribute: false })
107
+
108
+ /**
109
+ * render number of facet items
110
+ */
111
+ private allowedFacetCount = 6;
112
+
113
+ render() {
114
+ return html`
115
+ <div id="container" class="${this.facetsLoading ? 'loading' : ''}">
116
+ ${(this.showHistogramDatePicker &&
117
+ this.fullYearsHistogramAggregation) ||
118
+ this.fullYearAggregationLoading
119
+ ? html`
120
+ <div class="facet-group">
121
+ <h1>Year Published <feature-feedback></feature-feedback></h1>
122
+ ${this.histogramTemplate}
123
+ </div>
124
+ `
125
+ : nothing}
126
+ ${this.mergedFacets.map(facetGroup =>
127
+ this.getFacetGroupTemplate(facetGroup)
128
+ )}
129
+ </div>
130
+ `;
131
+ }
132
+
133
+ updated(changed: PropertyValues) {
134
+ if (changed.has('selectedFacets')) {
135
+ this.dispatchFacetsChangedEvent();
136
+ }
137
+ }
138
+
139
+ // TODO: want to fire analytics?
140
+ private dispatchFacetsChangedEvent() {
141
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
142
+ detail: this.selectedFacets,
143
+ });
144
+ this.dispatchEvent(event);
145
+ }
146
+
147
+ private get currentYearsHistogramAggregation(): Aggregation | undefined {
148
+ return this.aggregations?.year_histogram;
149
+ }
150
+
151
+ private get histogramTemplate() {
152
+ const { fullYearsHistogramAggregation } = this;
153
+ return this.fullYearAggregationLoading
154
+ ? html`<div class="histogram-loading-indicator">&hellip;</div>` // Ellipsis block
155
+ : html`
156
+ <histogram-date-range
157
+ .minDate=${fullYearsHistogramAggregation?.first_bucket_key}
158
+ .maxDate=${fullYearsHistogramAggregation?.last_bucket_key}
159
+ .minSelectedDate=${this.minSelectedDate}
160
+ .maxSelectedDate=${this.maxSelectedDate}
161
+ .updateDelay=${100}
162
+ missingDataMessage="..."
163
+ .width=${180}
164
+ .bins=${fullYearsHistogramAggregation?.buckets as number[]}
165
+ @histogramDateRangeUpdated=${this.histogramDateRangeUpdated}
166
+ ></histogram-date-range>
167
+ `;
168
+ }
169
+
170
+ private histogramDateRangeUpdated(
171
+ e: CustomEvent<{
172
+ minDate: string;
173
+ maxDate: string;
174
+ }>
175
+ ) {
176
+ const { minDate, maxDate } = e.detail;
177
+ const event = new CustomEvent('histogramDateRangeUpdated', {
178
+ detail: { minDate, maxDate },
179
+ });
180
+ this.dispatchEvent(event);
181
+ }
182
+
183
+ /**
184
+ * Combines the selected facets with the aggregations to create a single list of facets
185
+ */
186
+ private get mergedFacets(): FacetGroup[] {
187
+ const facetGroups: FacetGroup[] = [];
188
+
189
+ facetDisplayOrder.forEach(facetKey => {
190
+ const selectedFacetGroup = this.selectedFacetGroups.find(
191
+ group => group.key === facetKey
192
+ );
193
+ const aggregateFacetGroup = this.aggregationFacetGroups.find(
194
+ group => group.key === facetKey
195
+ );
196
+
197
+ // if the user selected a facet, but it's not in the aggregation, we add it as-is
198
+ if (selectedFacetGroup && !aggregateFacetGroup) {
199
+ facetGroups.push(selectedFacetGroup);
200
+ return;
201
+ }
202
+
203
+ // if we don't have an aggregate facet group, don't add this to the list
204
+ if (!aggregateFacetGroup) return;
205
+
206
+ // start with either the selected group if we have one, or the aggregate group
207
+ const facetGroup = selectedFacetGroup ?? aggregateFacetGroup;
208
+
209
+ // attach the counts to the selected buckets
210
+ let bucketsWithCount =
211
+ selectedFacetGroup?.buckets.map(bucket => {
212
+ const selectedBucket = aggregateFacetGroup.buckets.find(
213
+ b => b.key === bucket.key
214
+ );
215
+ return selectedBucket
216
+ ? {
217
+ ...bucket,
218
+ count: selectedBucket.count,
219
+ }
220
+ : bucket;
221
+ }) ?? [];
222
+
223
+ // append any additional buckets that were not selected
224
+ aggregateFacetGroup.buckets.forEach(bucket => {
225
+ const existingBucket = bucketsWithCount.find(b => b.key === bucket.key);
226
+ if (existingBucket) return;
227
+ bucketsWithCount.push(bucket);
228
+ });
229
+
230
+ // For lending facets, only include a specific subset of buckets
231
+ if (facetKey === 'lending') {
232
+ bucketsWithCount = bucketsWithCount.filter(
233
+ bucket => lendingFacetKeysVisibility[bucket.key as LendingFacetKey]
234
+ );
235
+ }
236
+
237
+ /**
238
+ * render limited facet items on page facet area
239
+ *
240
+ * - by-default we are showing 6 items
241
+ * - additionally want to show all items (selected/suppressed) in page facet area
242
+ */
243
+ let allowedFacetCount = Object.keys(
244
+ (selectedFacetGroup?.buckets as []) || []
245
+ )?.length;
246
+ if (allowedFacetCount < this.allowedFacetCount) {
247
+ allowedFacetCount = this.allowedFacetCount; // splice start index from 0th
248
+ }
249
+
250
+ // splice how many items we want to show in page facet area
251
+ facetGroup.buckets = bucketsWithCount.splice(0, allowedFacetCount);
252
+
253
+ facetGroups.push(facetGroup);
254
+ });
255
+
256
+ return facetGroups;
257
+ }
258
+
259
+ /**
260
+ * Converts the selected facets to a `FacetGroup` array,
261
+ * which is easier to work with
262
+ */
263
+ private get selectedFacetGroups(): FacetGroup[] {
264
+ if (!this.selectedFacets) return [];
265
+
266
+ const facetGroups: FacetGroup[] = Object.entries(this.selectedFacets).map(
267
+ ([key, selectedFacets]) => {
268
+ const option = key as FacetOption;
269
+ const title = facetTitles[option];
270
+
271
+ const buckets: FacetBucket[] = Object.entries(selectedFacets).map(
272
+ ([value, facetData]) => {
273
+ let displayText: string = value;
274
+ // for selected languages, we store the language code instead of the
275
+ // display name, so look up the name from the mapping
276
+ if (option === 'language') {
277
+ displayText =
278
+ this.languageCodeHandler?.getLanguageNameFromCodeString(
279
+ value
280
+ ) ?? value;
281
+ }
282
+ // for lending facets, convert the key to a readable format
283
+ if (option === 'lending') {
284
+ displayText =
285
+ lendingFacetDisplayNames[value as LendingFacetKey] ?? value;
286
+ }
287
+ return {
288
+ displayText,
289
+ key: value,
290
+ count: facetData.count,
291
+ state: facetData.state,
292
+ };
293
+ }
294
+ );
295
+
296
+ return {
297
+ title,
298
+ key: option,
299
+ buckets,
300
+ };
301
+ }
302
+ );
303
+
304
+ return facetGroups;
305
+ }
306
+
307
+ /**
308
+ * Converts the raw `aggregations` to `FacetGroups`, which are easier to use
309
+ */
310
+ private get aggregationFacetGroups(): FacetGroup[] {
311
+ const facetGroups: FacetGroup[] = [];
312
+ Object.entries(this.aggregations ?? []).forEach(([key, buckets]) => {
313
+ // the year_histogram data is in a different format so can't be handled here
314
+ if (key === 'year_histogram') return;
315
+
316
+ const option = key as FacetOption;
317
+ const title = facetTitles[option];
318
+ if (!title) return;
319
+
320
+ let castedBuckets = buckets.buckets as Bucket[];
321
+
322
+ if (option === 'collection') {
323
+ // we are not showing fav- collections or certain deemphasized collections in facets
324
+ castedBuckets = castedBuckets?.filter(bucket => {
325
+ const bucketKey = bucket?.key?.toString();
326
+ return (
327
+ !suppressedCollections[bucketKey] && !bucketKey?.startsWith('fav-')
328
+ );
329
+ });
330
+ }
331
+
332
+ const facetBuckets: FacetBucket[] = castedBuckets.map(bucket => {
333
+ let bucketKey = bucket.key;
334
+ let displayText = `${bucket.key}`;
335
+ // for languages, we need to search by language code instead of the
336
+ // display name, which is what we get from the search engine result
337
+ if (option === 'language') {
338
+ // const languageCodeKey = languageToCodeMap[bucket.key];
339
+ bucketKey =
340
+ this.languageCodeHandler?.getCodeStringFromLanguageName(
341
+ `${bucket.key}`
342
+ ) ?? bucket.key;
343
+ // bucketKey = languageCodeKey ?? bucket.key;
344
+ }
345
+ // for lending facets, convert the bucket key to a readable format
346
+ if (option === 'lending') {
347
+ displayText =
348
+ lendingFacetDisplayNames[bucket.key as LendingFacetKey] ??
349
+ `${bucket.key}`;
350
+ }
351
+ return {
352
+ displayText,
353
+ key: `${bucketKey}`,
354
+ count: bucket.doc_count,
355
+ state: 'none',
356
+ };
357
+ });
358
+ const group: FacetGroup = {
359
+ title,
360
+ key: option,
361
+ buckets: facetBuckets,
362
+ };
363
+ facetGroups.push(group);
364
+ });
365
+ return facetGroups;
366
+ }
367
+
368
+ /**
369
+ * Generate the template for a facet group with a header and the collapsible
370
+ * chevron for the mobile view
371
+ */
372
+ private getFacetGroupTemplate(
373
+ facetGroup: FacetGroup
374
+ ): TemplateResult | typeof nothing {
375
+ if (!this.facetsLoading && facetGroup.buckets.length === 0) return nothing;
376
+
377
+ const { key } = facetGroup;
378
+ const isOpen = this.openFacets[key];
379
+ const collapser = html`
380
+ <span class="collapser ${isOpen ? 'open' : ''}"> ${chevronIcon} </span>
381
+ `;
382
+
383
+ return html`
384
+ <div class="facet-group ${this.collapsableFacets ? 'mobile' : ''}">
385
+ <div class="facet-group-header">
386
+ <h1
387
+ @click=${() => {
388
+ const newOpenFacets = { ...this.openFacets };
389
+ newOpenFacets[key] = !isOpen;
390
+ this.openFacets = newOpenFacets;
391
+ }}
392
+ @keyup=${() => {
393
+ const newOpenFacets = { ...this.openFacets };
394
+ newOpenFacets[key] = !isOpen;
395
+ this.openFacets = newOpenFacets;
396
+ }}
397
+ >
398
+ ${this.collapsableFacets ? collapser : nothing} ${facetGroup.title}
399
+ </h1>
400
+ ${this.facetsLoading
401
+ ? nothing
402
+ : this.moreFacetsSortingIcon(facetGroup)}
403
+ </div>
404
+ <div class="facet-group-content ${isOpen ? 'open' : ''}">
405
+ ${this.facetsLoading
406
+ ? this.getTombstoneFacetGroupTemplate()
407
+ : html`
408
+ ${this.getFacetTemplate(facetGroup)}
409
+ ${this.searchMoreFacetsLink(facetGroup)}
410
+ `}
411
+ </div>
412
+ </div>
413
+ `;
414
+ }
415
+
416
+ private getTombstoneFacetGroupTemplate(): TemplateResult {
417
+ // Render five tombstone rows
418
+ return html`
419
+ ${map(
420
+ Array(5).fill(null),
421
+ () => html`<facet-tombstone-row></facet-tombstone-row>`
422
+ )}
423
+ `;
424
+ }
425
+
426
+ private moreFacetsSortingIcon(
427
+ facetGroup: FacetGroup
428
+ ): TemplateResult | typeof nothing {
429
+ // Display the sorting icon for every facet group except lending
430
+ return facetGroup.key === 'lending'
431
+ ? nothing
432
+ : html`
433
+ <input
434
+ class="sorting-icon"
435
+ type="image"
436
+ @click=${() => this.showMoreFacetsModal(facetGroup, 'alpha')}
437
+ src="https://archive.org/images/filter-count.png"
438
+ alt="Sort alphabetically"
439
+ />
440
+ `;
441
+ }
442
+
443
+ /**
444
+ * Generate the More... link button just below the facets group
445
+ *
446
+ * TODO: want to fire analytics?
447
+ */
448
+ private searchMoreFacetsLink(
449
+ facetGroup: FacetGroup
450
+ ): TemplateResult | typeof nothing {
451
+ // Don't render More... links for FTS searches
452
+ if (!this.moreLinksVisible) {
453
+ return nothing;
454
+ }
455
+
456
+ // Don't render More... links for lending facets
457
+ if (facetGroup.key === 'lending') {
458
+ return nothing;
459
+ }
460
+
461
+ // Don't render More... link if the number of facets < this.allowedFacetCount
462
+ if (Object.keys(facetGroup.buckets).length < this.allowedFacetCount) {
463
+ return nothing;
464
+ }
465
+
466
+ return html`<button
467
+ class="more-link"
468
+ @click=${() => {
469
+ this.showMoreFacetsModal(facetGroup, 'count');
470
+ this.analyticsHandler?.sendEventNoSampling({
471
+ category: analyticsCategories.default,
472
+ action: analyticsActions.showMoreFacetsModal,
473
+ label: facetGroup.key,
474
+ });
475
+ this.dispatchEvent(
476
+ new CustomEvent('showMoreFacets', { detail: facetGroup.key })
477
+ );
478
+ }}
479
+ >
480
+ More...
481
+ </button>`;
482
+ }
483
+
484
+ async showMoreFacetsModal(
485
+ facetGroup: FacetGroup,
486
+ sortedBy: string
487
+ ): Promise<void> {
488
+ const facetAggrKey = facetGroup.key;
489
+
490
+ const customModalContent = html`
491
+ <more-facets-content
492
+ .analyticsHandler=${this.analyticsHandler}
493
+ .facetKey=${facetGroup.key}
494
+ .facetAggregationKey=${facetAggrKey}
495
+ .fullQuery=${this.fullQuery}
496
+ .modalManager=${this.modalManager}
497
+ .searchService=${this.searchService}
498
+ .searchType=${this.searchType}
499
+ .collectionNameCache=${this.collectionNameCache}
500
+ .languageCodeHandler=${this.languageCodeHandler}
501
+ .selectedFacets=${this.selectedFacets}
502
+ .sortedBy=${sortedBy}
503
+ @facetsChanged=${(e: CustomEvent) => {
504
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
505
+ detail: e.detail,
506
+ bubbles: true,
507
+ composed: true,
508
+ });
509
+ this.dispatchEvent(event);
510
+ }}
511
+ >
512
+ </more-facets-content>
513
+ `;
514
+
515
+ const config = new ModalConfig({
516
+ bodyColor: '#fff',
517
+ headerColor: '#194880',
518
+ showHeaderLogo: false,
519
+ closeOnBackdropClick: true, // TODO: want to fire analytics
520
+ title: html`Select filters`,
521
+ });
522
+ this.modalManager?.classList.add('more-search-facets');
523
+ this.modalManager?.showModal({
524
+ config,
525
+ customModalContent,
526
+ });
527
+ }
528
+
529
+ /**
530
+ * Generate the list template for each bucket in a facet group
531
+ */
532
+ private getFacetTemplate(facetGroup: FacetGroup): TemplateResult {
533
+ return html`
534
+ <facets-template
535
+ .facetGroup=${facetGroup}
536
+ .selectedFacets=${this.selectedFacets}
537
+ .renderOn=${'page'}
538
+ .collectionNameCache=${this.collectionNameCache}
539
+ @selectedFacetsChanged=${(e: CustomEvent) => {
540
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
541
+ detail: e.detail,
542
+ bubbles: true,
543
+ composed: true,
544
+ });
545
+ this.dispatchEvent(event);
546
+ }}
547
+ ></facets-template>
548
+ `;
549
+ }
550
+
551
+ static get styles() {
552
+ return css`
553
+ #container.loading {
554
+ opacity: 0.5;
555
+ }
556
+
557
+ .histogram-loading-indicator {
558
+ width: 100%;
559
+ height: 2.25rem;
560
+ margin-top: 1.75rem;
561
+ font-size: 1.4rem;
562
+ text-align: center;
563
+ }
564
+
565
+ .collapser {
566
+ display: inline-block;
567
+ cursor: pointer;
568
+ width: 10px;
569
+ height: 10px;
570
+ }
571
+
572
+ .collapser svg {
573
+ transition: transform 0.2s ease-in-out;
574
+ }
575
+
576
+ .collapser.open svg {
577
+ transform: rotate(90deg);
578
+ }
579
+
580
+ .facet-group {
581
+ margin-bottom: 2rem;
582
+ }
583
+
584
+ .facet-group h1 {
585
+ margin-bottom: 0.7rem;
586
+ }
587
+
588
+ .facet-group.mobile h1 {
589
+ cursor: pointer;
590
+ }
591
+
592
+ .facet-group-header {
593
+ display: flex;
594
+ margin-bottom: 0.7rem;
595
+ justify-content: space-between;
596
+ border-bottom: 1px solid rgb(232, 232, 232);
597
+ }
598
+
599
+ .facet-group-content {
600
+ transition: max-height 0.2s ease-in-out;
601
+ }
602
+
603
+ .facet-group.mobile .facet-group-content {
604
+ max-height: 0;
605
+ overflow: hidden;
606
+ }
607
+
608
+ .facet-group.mobile .facet-group-content.open {
609
+ max-height: 2000px;
610
+ }
611
+
612
+ h1 {
613
+ font-size: 1.4rem;
614
+ font-weight: 200
615
+ padding-bottom: 3px;
616
+ margin: 0;
617
+ }
618
+
619
+ .more-link {
620
+ font-size: 1.2rem;
621
+ text-decoration: none;
622
+ padding: 0;
623
+ background: inherit;
624
+ border: 0;
625
+ color: var(--ia-theme-link-color, #4b64ff);
626
+ cursor: pointer;
627
+ }
628
+
629
+ .sorting-icon {
630
+ height: 15px;
631
+ cursor: pointer;
632
+ }
633
+ `;
634
+ }
635
+ }