@internetarchive/collection-browser 0.0.1-alpha.7 → 0.1.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 (238) hide show
  1. package/README.md +8 -11
  2. package/demo/app-root.ts +30 -96
  3. package/dist/demo/app-root.d.ts +3 -5
  4. package/dist/demo/app-root.js +28 -87
  5. package/dist/demo/app-root.js.map +1 -1
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/src/assets/img/icons/chevron.d.ts +2 -0
  10. package/dist/src/assets/img/icons/chevron.js +4 -0
  11. package/dist/src/assets/img/icons/chevron.js.map +1 -0
  12. package/dist/src/assets/img/icons/eye-closed.d.ts +2 -0
  13. package/dist/src/assets/img/icons/eye-closed.js +5 -0
  14. package/dist/src/assets/img/icons/eye-closed.js.map +1 -0
  15. package/dist/src/assets/img/icons/eye.d.ts +2 -0
  16. package/dist/src/assets/img/icons/eye.js +5 -0
  17. package/dist/src/assets/img/icons/eye.js.map +1 -0
  18. package/dist/src/assets/img/icons/mediatype/account.d.ts +1 -2
  19. package/dist/src/assets/img/icons/mediatype/account.js +6 -4
  20. package/dist/src/assets/img/icons/mediatype/account.js.map +1 -1
  21. package/dist/src/assets/img/icons/mediatype/audio.js +7 -4
  22. package/dist/src/assets/img/icons/mediatype/audio.js.map +1 -1
  23. package/dist/src/assets/img/icons/mediatype/collection.js +7 -4
  24. package/dist/src/assets/img/icons/mediatype/collection.js.map +1 -1
  25. package/dist/src/assets/img/icons/mediatype/data.d.ts +1 -0
  26. package/dist/src/assets/img/icons/mediatype/data.js +15 -0
  27. package/dist/src/assets/img/icons/mediatype/data.js.map +1 -0
  28. package/dist/src/assets/img/icons/mediatype/etree.js +10 -5
  29. package/dist/src/assets/img/icons/mediatype/etree.js.map +1 -1
  30. package/dist/src/assets/img/icons/mediatype/film.js +2 -1
  31. package/dist/src/assets/img/icons/mediatype/film.js.map +1 -1
  32. package/dist/src/assets/img/icons/mediatype/images.js +9 -6
  33. package/dist/src/assets/img/icons/mediatype/images.js.map +1 -1
  34. package/dist/src/assets/img/icons/mediatype/radio.d.ts +1 -0
  35. package/dist/src/assets/img/icons/mediatype/radio.js +15 -0
  36. package/dist/src/assets/img/icons/mediatype/radio.js.map +1 -0
  37. package/dist/src/assets/img/icons/mediatype/software.js +9 -6
  38. package/dist/src/assets/img/icons/mediatype/software.js.map +1 -1
  39. package/dist/src/assets/img/icons/mediatype/texts.js +9 -6
  40. package/dist/src/assets/img/icons/mediatype/texts.js.map +1 -1
  41. package/dist/src/assets/img/icons/mediatype/tv.js +10 -5
  42. package/dist/src/assets/img/icons/mediatype/tv.js.map +1 -1
  43. package/dist/src/assets/img/icons/mediatype/video.js +10 -6
  44. package/dist/src/assets/img/icons/mediatype/video.js.map +1 -1
  45. package/dist/src/assets/img/icons/mediatype/web.js +9 -6
  46. package/dist/src/assets/img/icons/mediatype/web.js.map +1 -1
  47. package/dist/src/collection-browser.d.ts +57 -20
  48. package/dist/src/collection-browser.js +511 -128
  49. package/dist/src/collection-browser.js.map +1 -1
  50. package/dist/src/collection-facets.d.ts +27 -6
  51. package/dist/src/collection-facets.js +316 -100
  52. package/dist/src/collection-facets.js.map +1 -1
  53. package/dist/src/language-code-handler/language-code-handler.d.ts +37 -0
  54. package/dist/src/language-code-handler/language-code-handler.js +27 -0
  55. package/dist/src/language-code-handler/language-code-handler.js.map +1 -0
  56. package/dist/src/language-code-handler/language-code-mapping.d.ts +1 -0
  57. package/dist/src/language-code-handler/language-code-mapping.js +563 -0
  58. package/dist/src/language-code-handler/language-code-mapping.js.map +1 -0
  59. package/dist/src/mediatype/mediatype-config.d.ts +3 -0
  60. package/dist/src/mediatype/mediatype-config.js +86 -0
  61. package/dist/src/mediatype/mediatype-config.js.map +1 -0
  62. package/dist/src/models.d.ts +72 -13
  63. package/dist/src/models.js +57 -1
  64. package/dist/src/models.js.map +1 -1
  65. package/dist/src/restoration-state-handler.d.ts +38 -0
  66. package/dist/src/restoration-state-handler.js +204 -0
  67. package/dist/src/restoration-state-handler.js.map +1 -0
  68. package/dist/src/sort-filter-bar/alpha-bar.d.ts +1 -2
  69. package/dist/src/sort-filter-bar/alpha-bar.js +12 -8
  70. package/dist/src/sort-filter-bar/alpha-bar.js.map +1 -1
  71. package/dist/src/sort-filter-bar/img/compact.d.ts +1 -0
  72. package/dist/src/sort-filter-bar/img/compact.js +5 -0
  73. package/dist/src/sort-filter-bar/img/compact.js.map +1 -0
  74. package/dist/src/sort-filter-bar/img/list.d.ts +1 -0
  75. package/dist/src/sort-filter-bar/img/list.js +5 -0
  76. package/dist/src/sort-filter-bar/img/list.js.map +1 -0
  77. package/dist/src/sort-filter-bar/img/sort-triangle.d.ts +1 -0
  78. package/dist/src/sort-filter-bar/img/sort-triangle.js +5 -0
  79. package/dist/src/sort-filter-bar/img/sort-triangle.js.map +1 -0
  80. package/dist/src/sort-filter-bar/img/tile.d.ts +1 -0
  81. package/dist/src/sort-filter-bar/img/tile.js +5 -0
  82. package/dist/src/sort-filter-bar/img/tile.js.map +1 -0
  83. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +74 -13
  84. package/dist/src/sort-filter-bar/sort-filter-bar.js +547 -172
  85. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  86. package/dist/src/tiles/{loading-tile.d.ts → collection-browser-loading-tile.d.ts} +1 -1
  87. package/dist/src/tiles/collection-browser-loading-tile.js +32 -0
  88. package/dist/src/tiles/collection-browser-loading-tile.js.map +1 -0
  89. package/dist/src/tiles/grid/account-tile.d.ts +1 -1
  90. package/dist/src/tiles/grid/account-tile.js +5 -5
  91. package/dist/src/tiles/grid/account-tile.js.map +1 -1
  92. package/dist/src/tiles/grid/collection-tile.js +1 -2
  93. package/dist/src/tiles/grid/collection-tile.js.map +1 -1
  94. package/dist/src/tiles/grid/icons/views.d.ts +1 -1
  95. package/dist/src/tiles/grid/icons/views.js +2 -2
  96. package/dist/src/tiles/grid/icons/views.js.map +1 -1
  97. package/dist/src/tiles/grid/item-tile.d.ts +2 -2
  98. package/dist/src/tiles/grid/item-tile.js +58 -150
  99. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  100. package/dist/src/tiles/item-image.d.ts +19 -0
  101. package/dist/src/tiles/item-image.js +204 -0
  102. package/dist/src/tiles/item-image.js.map +1 -0
  103. package/dist/src/tiles/list/account-label.d.ts +1 -0
  104. package/dist/src/tiles/list/account-label.js +7 -0
  105. package/dist/src/tiles/list/account-label.js.map +1 -0
  106. package/dist/src/tiles/list/date-label.d.ts +1 -0
  107. package/dist/src/tiles/list/date-label.js +13 -0
  108. package/dist/src/tiles/list/date-label.js.map +1 -0
  109. package/dist/src/tiles/list/tile-list-compact-header.d.ts +12 -0
  110. package/dist/src/tiles/list/tile-list-compact-header.js +84 -0
  111. package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -0
  112. package/dist/src/tiles/list/tile-list-compact.d.ts +12 -0
  113. package/dist/src/tiles/list/tile-list-compact.js +203 -6
  114. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  115. package/dist/src/tiles/list/tile-list.d.ts +35 -10
  116. package/dist/src/tiles/list/tile-list.js +368 -104
  117. package/dist/src/tiles/list/tile-list.js.map +1 -1
  118. package/dist/src/{mediatype-icon.d.ts → tiles/mediatype-icon.d.ts} +2 -2
  119. package/dist/src/tiles/mediatype-icon.js +78 -0
  120. package/dist/src/tiles/mediatype-icon.js.map +1 -0
  121. package/dist/src/tiles/tile-dispatcher.d.ts +11 -4
  122. package/dist/src/tiles/tile-dispatcher.js +56 -19
  123. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  124. package/dist/src/utils/format-date.js +2 -2
  125. package/dist/src/utils/format-date.js.map +1 -1
  126. package/dist/test/collection-browser.test.d.ts +1 -0
  127. package/dist/test/collection-browser.test.js +16 -2
  128. package/dist/test/collection-browser.test.js.map +1 -1
  129. package/dist/test/{utils/format-string.test.d.ts → mediatype-config.test.d.ts} +0 -0
  130. package/dist/test/mediatype-config.test.js +17 -0
  131. package/dist/test/mediatype-config.test.js.map +1 -0
  132. package/dist/test/utils/format-date.test.js +1 -1
  133. package/dist/test/utils/format-date.test.js.map +1 -1
  134. package/index.ts +6 -0
  135. package/local.archive.org.cert +86 -0
  136. package/local.archive.org.key +27 -0
  137. package/package.json +9 -5
  138. package/src/assets/img/icons/chevron.ts +4 -0
  139. package/src/assets/img/icons/eye-closed.ts +5 -0
  140. package/src/assets/img/icons/eye.ts +5 -0
  141. package/src/assets/img/icons/mediatype/account.ts +6 -4
  142. package/src/assets/img/icons/mediatype/audio.ts +7 -4
  143. package/src/assets/img/icons/mediatype/collection.ts +7 -4
  144. package/src/assets/img/icons/mediatype/data.ts +15 -0
  145. package/src/assets/img/icons/mediatype/etree.ts +10 -5
  146. package/src/assets/img/icons/mediatype/film.ts +2 -1
  147. package/src/assets/img/icons/mediatype/images.ts +9 -6
  148. package/src/assets/img/icons/mediatype/radio.ts +15 -0
  149. package/src/assets/img/icons/mediatype/software.ts +9 -6
  150. package/src/assets/img/icons/mediatype/texts.ts +9 -6
  151. package/src/assets/img/icons/mediatype/tv.ts +10 -5
  152. package/src/assets/img/icons/mediatype/video.ts +10 -6
  153. package/src/assets/img/icons/mediatype/web.ts +9 -6
  154. package/src/collection-browser.ts +537 -123
  155. package/src/collection-facets.ts +352 -132
  156. package/src/language-code-handler/language-code-handler.ts +64 -0
  157. package/src/language-code-handler/language-code-mapping.ts +564 -0
  158. package/src/mediatype/mediatype-config.ts +86 -0
  159. package/src/models.ts +141 -13
  160. package/src/restoration-state-handler.ts +266 -0
  161. package/src/sort-filter-bar/alpha-bar.ts +12 -8
  162. package/src/sort-filter-bar/img/compact.ts +5 -0
  163. package/src/sort-filter-bar/img/list.ts +5 -0
  164. package/src/sort-filter-bar/img/sort-triangle.ts +5 -0
  165. package/src/sort-filter-bar/img/tile.ts +5 -0
  166. package/src/sort-filter-bar/sort-filter-bar.ts +604 -176
  167. package/src/tiles/collection-browser-loading-tile.ts +29 -0
  168. package/src/tiles/grid/account-tile.ts +1 -1
  169. package/src/tiles/grid/collection-tile.ts +1 -2
  170. package/src/tiles/grid/icons/views.ts +2 -2
  171. package/src/tiles/grid/item-tile.ts +57 -162
  172. package/src/tiles/item-image.ts +206 -0
  173. package/src/tiles/list/account-label.ts +6 -0
  174. package/src/tiles/list/date-label.ts +12 -0
  175. package/src/tiles/list/tile-list-compact-header.ts +77 -0
  176. package/src/tiles/list/tile-list-compact.ts +218 -0
  177. package/src/tiles/list/tile-list.ts +412 -107
  178. package/src/tiles/mediatype-icon.ts +75 -0
  179. package/src/tiles/tile-dispatcher.ts +66 -18
  180. package/src/utils/format-date.ts +2 -2
  181. package/test/collection-browser.test.ts +20 -1
  182. package/test/mediatype-config.test.ts +18 -0
  183. package/test/utils/format-date.test.ts +1 -1
  184. package/web-dev-server.config.mjs +3 -1
  185. package/dist/src/assets/img/icons/audio.d.ts +0 -1
  186. package/dist/src/assets/img/icons/audio.js +0 -9
  187. package/dist/src/assets/img/icons/audio.js.map +0 -1
  188. package/dist/src/assets/img/icons/collection.d.ts +0 -1
  189. package/dist/src/assets/img/icons/collection.js +0 -9
  190. package/dist/src/assets/img/icons/collection.js.map +0 -1
  191. package/dist/src/assets/img/icons/etree.d.ts +0 -1
  192. package/dist/src/assets/img/icons/etree.js +0 -9
  193. package/dist/src/assets/img/icons/etree.js.map +0 -1
  194. package/dist/src/assets/img/icons/images.d.ts +0 -1
  195. package/dist/src/assets/img/icons/images.js +0 -10
  196. package/dist/src/assets/img/icons/images.js.map +0 -1
  197. package/dist/src/assets/img/icons/mediatype/etree copy.d.ts +0 -1
  198. package/dist/src/assets/img/icons/mediatype/etree copy.js +0 -9
  199. package/dist/src/assets/img/icons/mediatype/etree copy.js.map +0 -1
  200. package/dist/src/assets/img/icons/mediatype/livemusic.d.ts +0 -1
  201. package/dist/src/assets/img/icons/mediatype/livemusic.js +0 -7
  202. package/dist/src/assets/img/icons/mediatype/livemusic.js.map +0 -1
  203. package/dist/src/assets/img/icons/mediatype/photos.d.ts +0 -1
  204. package/dist/src/assets/img/icons/mediatype/photos.js +0 -7
  205. package/dist/src/assets/img/icons/mediatype/photos.js.map +0 -1
  206. package/dist/src/assets/img/icons/software.d.ts +0 -1
  207. package/dist/src/assets/img/icons/software.js +0 -10
  208. package/dist/src/assets/img/icons/software.js.map +0 -1
  209. package/dist/src/assets/img/icons/texts.d.ts +0 -1
  210. package/dist/src/assets/img/icons/texts.js +0 -10
  211. package/dist/src/assets/img/icons/texts.js.map +0 -1
  212. package/dist/src/assets/img/icons/tv.d.ts +0 -1
  213. package/dist/src/assets/img/icons/tv.js +0 -9
  214. package/dist/src/assets/img/icons/tv.js.map +0 -1
  215. package/dist/src/assets/img/icons/video.d.ts +0 -1
  216. package/dist/src/assets/img/icons/video.js +0 -10
  217. package/dist/src/assets/img/icons/video.js.map +0 -1
  218. package/dist/src/assets/img/icons/web.d.ts +0 -1
  219. package/dist/src/assets/img/icons/web.js +0 -10
  220. package/dist/src/assets/img/icons/web.js.map +0 -1
  221. package/dist/src/mediatype-icon.js +0 -89
  222. package/dist/src/mediatype-icon.js.map +0 -1
  223. package/dist/src/search-handler.d.ts +0 -11
  224. package/dist/src/search-handler.js +0 -34
  225. package/dist/src/search-handler.js.map +0 -1
  226. package/dist/src/tiles/list/tile-list-detail.d.ts +0 -7
  227. package/dist/src/tiles/list/tile-list-detail.js +0 -28
  228. package/dist/src/tiles/list/tile-list-detail.js.map +0 -1
  229. package/dist/src/tiles/loading-tile.js +0 -73
  230. package/dist/src/tiles/loading-tile.js.map +0 -1
  231. package/dist/src/utils/format-string.d.ts +0 -2
  232. package/dist/src/utils/format-string.js +0 -7
  233. package/dist/src/utils/format-string.js.map +0 -1
  234. package/dist/test/utils/format-string.test.js +0 -17
  235. package/dist/test/utils/format-string.test.js.map +0 -1
  236. package/src/assets/img/icons/mediatype/foo.svg +0 -5
  237. package/src/mediatype-icon.ts +0 -83
  238. package/src/tiles/loading-tile.ts +0 -70
@@ -1,17 +1,30 @@
1
- import { css, html, LitElement } from 'lit';
2
- import { customElement, property } from 'lit/decorators.js';
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';
3
11
  import { repeat } from 'lit/directives/repeat.js';
4
- import { Aggregation } from '@internetarchive/search-service';
12
+ import { Aggregation, Bucket } from '@internetarchive/search-service';
5
13
  import '@internetarchive/histogram-date-range';
6
14
  import '@internetarchive/feature-feedback';
7
-
8
- type FacetOption =
9
- | 'subject'
10
- | 'mediatype'
11
- | 'language'
12
- | 'creator'
13
- | 'collection'
14
- | 'year';
15
+ import '@internetarchive/collection-name-cache';
16
+ import { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
17
+ import eyeIcon from './assets/img/icons/eye';
18
+ import eyeClosedIcon from './assets/img/icons/eye-closed';
19
+ import chevronIcon from './assets/img/icons/chevron';
20
+ import {
21
+ FacetOption,
22
+ SelectedFacets,
23
+ FacetGroup,
24
+ FacetBucket,
25
+ defaultSelectedFacets,
26
+ } from './models';
27
+ import { LanguageCodeHandlerInterface } from './language-code-handler/language-code-handler';
15
28
 
16
29
  const facetDisplayOrder: FacetOption[] = [
17
30
  'mediatype',
@@ -40,82 +53,88 @@ const facetTitles: Record<FacetOption, string> = {
40
53
  year: 'Year',
41
54
  };
42
55
 
43
- interface FacetBucket {
44
- // for some facets, we augment the key with a display value
45
- displayText?: string;
46
- key: string;
47
- count: number;
48
- selected: boolean;
49
- }
50
-
51
- interface FacetGroup {
52
- title: string;
53
- key: string;
54
- buckets: FacetBucket[];
55
- }
56
-
57
56
  @customElement('collection-facets')
58
57
  export class CollectionFacets extends LitElement {
59
58
  @property({ type: Object }) aggregations?: Record<string, Aggregation>;
60
59
 
61
60
  @property({ type: Object }) fullYearsHistogramAggregation?: Aggregation;
62
61
 
63
- @property({ type: Object }) selectedFacets: Record<string, string[]> = {};
62
+ @property({ type: String }) minSelectedDate?: string;
63
+
64
+ @property({ type: String }) maxSelectedDate?: string;
64
65
 
65
66
  @property({ type: Boolean }) facetsLoading = false;
66
67
 
67
68
  @property({ type: Boolean }) fullYearAggregationLoading = false;
68
69
 
69
- private get hydratedSelectedFacets(): Record<string, string[]> {
70
- const { selectedFacets } = this;
71
- const hydratedSelectedFacets: Record<string, string[]> = {};
72
- Object.entries(selectedFacets).forEach(([key]) => {
73
- const values = hydratedSelectedFacets[key];
74
- const title = facetTitles[key as FacetOption];
75
- hydratedSelectedFacets[title] = values || [];
76
- delete hydratedSelectedFacets[key];
77
- });
78
- return hydratedSelectedFacets;
79
- }
70
+ @property({ type: Object }) selectedFacets?: SelectedFacets;
71
+
72
+ @property({ type: Boolean }) collapsableFacets = false;
73
+
74
+ @property({ type: Boolean }) showHistogramDatePicker = false;
75
+
76
+ @property({ type: Object })
77
+ languageCodeHandler?: LanguageCodeHandlerInterface;
78
+
79
+ @property({ type: Object })
80
+ collectionNameCache?: CollectionNameCacheInterface;
81
+
82
+ @state() openFacets: Record<FacetOption, boolean> = {
83
+ subject: false,
84
+ mediatype: false,
85
+ language: false,
86
+ creator: false,
87
+ collection: false,
88
+ year: false,
89
+ };
80
90
 
81
91
  render() {
82
92
  return html`
83
93
  <div id="container" class="${this.facetsLoading ? 'loading' : ''}">
84
- <div class="facet-group">
85
- <h1>Year Published <feature-feedback></feature-feedback></h1>
86
- ${this.histogramTemplate}
87
- </div>
88
-
89
- ${this.mergedFacets.map(
90
- facetGroup =>
91
- html`
94
+ ${this.showHistogramDatePicker && this.fullYearsHistogramAggregation
95
+ ? html`
92
96
  <div class="facet-group">
93
- <h1>${facetGroup.title}</h1>
94
- ${this.getFacetTemplate(facetGroup)}
97
+ <h1>Year Published <feature-feedback></feature-feedback></h1>
98
+ ${this.histogramTemplate}
95
99
  </div>
96
100
  `
101
+ : nothing}
102
+ ${this.mergedFacets.map(facetGroup =>
103
+ this.getFacetGroupTemplate(facetGroup)
97
104
  )}
98
105
  </div>
99
106
  `;
100
107
  }
101
108
 
109
+ updated(changed: PropertyValues) {
110
+ if (changed.has('selectedFacets')) {
111
+ this.dispatchFacetsChangedEvent();
112
+ }
113
+ }
114
+
115
+ private dispatchFacetsChangedEvent() {
116
+ const event = new CustomEvent<SelectedFacets>('facetsChanged', {
117
+ detail: this.selectedFacets,
118
+ });
119
+ this.dispatchEvent(event);
120
+ }
121
+
102
122
  private get currentYearsHistogramAggregation(): Aggregation | undefined {
103
123
  return this.aggregations?.year_histogram;
104
124
  }
105
125
 
106
126
  private get histogramTemplate() {
107
- const { currentYearsHistogramAggregation, fullYearsHistogramAggregation } =
108
- this;
127
+ const { fullYearsHistogramAggregation } = this;
109
128
  return html`
110
129
  <histogram-date-range
111
130
  .minDate=${fullYearsHistogramAggregation?.first_bucket_key}
112
131
  .maxDate=${fullYearsHistogramAggregation?.last_bucket_key}
113
- .minSelectedDate=${currentYearsHistogramAggregation?.first_bucket_key}
114
- .maxSelectedDate=${currentYearsHistogramAggregation?.last_bucket_key}
132
+ .minSelectedDate=${this.minSelectedDate}
133
+ .maxSelectedDate=${this.maxSelectedDate}
115
134
  .updateDelay=${100}
116
135
  missingDataMessage="..."
117
136
  .width=${180}
118
- .bins=${fullYearsHistogramAggregation?.buckets}
137
+ .bins=${fullYearsHistogramAggregation?.buckets as number[]}
119
138
  @histogramDateRangeUpdated=${this.histogramDateRangeUpdated}
120
139
  ></histogram-date-range>
121
140
  `;
@@ -166,13 +185,12 @@ export class CollectionFacets extends LitElement {
166
185
  const selectedBucket = aggregateFacetGroup.buckets.find(
167
186
  b => b.key === bucket.key
168
187
  );
169
- if (selectedBucket) {
170
- return {
171
- ...bucket,
172
- count: selectedBucket.count,
173
- };
174
- }
175
- return bucket;
188
+ return selectedBucket
189
+ ? {
190
+ ...bucket,
191
+ count: selectedBucket.count,
192
+ }
193
+ : bucket;
176
194
  }) ?? [];
177
195
 
178
196
  // append any additional buckets that were not selected
@@ -183,8 +201,6 @@ export class CollectionFacets extends LitElement {
183
201
  });
184
202
  facetGroup.buckets = bucketsWithCount.splice(0, 5);
185
203
 
186
- if (facetGroup.buckets.length === 0) return;
187
-
188
204
  facetGroups.push(facetGroup);
189
205
  });
190
206
 
@@ -192,24 +208,46 @@ export class CollectionFacets extends LitElement {
192
208
  }
193
209
 
194
210
  /**
195
- * Converts the raw `selectedFacets` to `FacetGroups`, which are easier to use
211
+ * Converts the selected facets to a `FacetGroup` array,
212
+ * which is easier to work with
196
213
  */
197
214
  private get selectedFacetGroups(): FacetGroup[] {
198
- const selectedFacetGroups: FacetGroup[] = [];
199
- Object.entries(this.selectedFacets).forEach(([key, buckets]) => {
200
- const title = facetTitles[key as FacetOption];
201
- const group = {
202
- title,
203
- key,
204
- buckets: buckets.map(bucket => ({
205
- key: bucket,
206
- count: 0,
207
- selected: true,
208
- })),
209
- };
210
- selectedFacetGroups.push(group);
211
- });
212
- return selectedFacetGroups;
215
+ if (!this.selectedFacets) return [];
216
+
217
+ const facetGroups: FacetGroup[] = Object.entries(this.selectedFacets).map(
218
+ ([key, selectedFacets]) => {
219
+ const option = key as FacetOption;
220
+ const title = facetTitles[option];
221
+
222
+ const buckets: FacetBucket[] = Object.entries(selectedFacets).map(
223
+ ([value, facetState]) => {
224
+ let displayText = value;
225
+ // for selected languages, we store the language code instead of the
226
+ // display name, so look up the name from the mapping
227
+ if (option === 'language') {
228
+ displayText =
229
+ this.languageCodeHandler?.getLanguageNameFromCodeString(
230
+ value
231
+ ) ?? value;
232
+ }
233
+ return {
234
+ displayText,
235
+ key: value,
236
+ count: 0,
237
+ state: facetState,
238
+ };
239
+ }
240
+ );
241
+
242
+ return {
243
+ title,
244
+ key: option,
245
+ buckets,
246
+ };
247
+ }
248
+ );
249
+
250
+ return facetGroups;
213
251
  }
214
252
 
215
253
  /**
@@ -218,14 +256,30 @@ export class CollectionFacets extends LitElement {
218
256
  private get aggregationFacetGroups(): FacetGroup[] {
219
257
  const facetGroups: FacetGroup[] = [];
220
258
  Object.entries(this.aggregations ?? []).forEach(([key, buckets]) => {
259
+ // the year_histogram data is in a different format so can't be handled here
221
260
  if (key === 'year_histogram') return;
222
261
  const option = this.getFacetOptionFromKey(key);
223
262
  const title = facetTitles[option];
224
- const facetBuckets: FacetBucket[] = buckets.buckets.map(bucket => ({
225
- key: `${bucket.key}`,
226
- count: bucket.doc_count,
227
- selected: false,
228
- }));
263
+ const castedBuckets = buckets.buckets as Bucket[];
264
+ const facetBuckets: FacetBucket[] = castedBuckets.map(bucket => {
265
+ let bucketKey = bucket.key;
266
+ // for languages, we need to search by language code instead of the
267
+ // display name, which is what we get from the search engine result
268
+ if (option === 'language') {
269
+ // const languageCodeKey = languageToCodeMap[bucket.key];
270
+ bucketKey =
271
+ this.languageCodeHandler?.getCodeStringFromLanguageName(
272
+ `${bucket.key}`
273
+ ) ?? bucket.key;
274
+ // bucketKey = languageCodeKey ?? bucket.key;
275
+ }
276
+ return {
277
+ displayText: `${bucket.key}`,
278
+ key: `${bucketKey}`,
279
+ count: bucket.doc_count,
280
+ state: 'none',
281
+ };
282
+ });
229
283
  const group: FacetGroup = {
230
284
  title,
231
285
  key: option,
@@ -236,74 +290,177 @@ export class CollectionFacets extends LitElement {
236
290
  return facetGroups;
237
291
  }
238
292
 
239
- private getFacetTemplate(facetGroup: FacetGroup) {
293
+ /**
294
+ * Generate the template for a facet group with a header and the collapsible
295
+ * chevron for the mobile view
296
+ */
297
+ private getFacetGroupTemplate(
298
+ facetGroup: FacetGroup
299
+ ): TemplateResult | typeof nothing {
300
+ if (facetGroup.buckets.length === 0) return nothing;
301
+ const { key } = facetGroup;
302
+ const isOpen = this.openFacets[key];
303
+ const collapser = html`
304
+ <span class="collapser ${isOpen ? 'open' : ''}"> ${chevronIcon} </span>
305
+ `;
306
+
307
+ return html`
308
+ <div class="facet-group ${this.collapsableFacets ? 'mobile' : ''}">
309
+ <h1
310
+ @click=${() => {
311
+ const newOpenFacets = { ...this.openFacets };
312
+ newOpenFacets[key] = !isOpen;
313
+ this.openFacets = newOpenFacets;
314
+ }}
315
+ @keyup=${() => {
316
+ const newOpenFacets = { ...this.openFacets };
317
+ newOpenFacets[key] = !isOpen;
318
+ this.openFacets = newOpenFacets;
319
+ }}
320
+ >
321
+ ${this.collapsableFacets ? collapser : nothing} ${facetGroup.title}
322
+ </h1>
323
+ <div class="facet-group-content ${isOpen ? 'open' : ''}">
324
+ ${this.getFacetTemplate(facetGroup)}
325
+ </div>
326
+ </div>
327
+ `;
328
+ }
329
+
330
+ /**
331
+ * Generate the list template for each bucket in a facet group
332
+ */
333
+ private getFacetTemplate(facetGroup: FacetGroup): TemplateResult {
334
+ const bucketsNoFavorites = facetGroup.buckets.filter(
335
+ bucket => bucket.key.startsWith('fav-') === false
336
+ );
337
+ const bucketsMaxSix = bucketsNoFavorites.slice(0, 6);
338
+
240
339
  return html`
241
340
  <ul class="facet-list">
242
341
  ${repeat(
243
- facetGroup.buckets,
342
+ bucketsMaxSix,
244
343
  bucket => `${facetGroup.key}:${bucket.key}`,
245
- bucket => html`
246
- <li>
247
- <label class="facet-row">
248
- <div class="facet-checkbox">
249
- <input
250
- type="checkbox"
251
- .name=${facetGroup.key}
252
- .value=${bucket.key}
253
- @click=${this.facetToggled}
254
- ?checked=${bucket.selected}
255
- />
344
+ bucket => {
345
+ const showOnlyCheckboxId = `${facetGroup.key}:${bucket.key}-show-only`;
346
+ const negativeCheckboxId = `${facetGroup.key}:${bucket.key}-negative`;
347
+
348
+ // for collections, we need to asynchronously load the collection name
349
+ // so we use the `async-collection-name` widget and for the rest, we have
350
+ // a static value to use
351
+ const bucketTextDisplay =
352
+ facetGroup.key !== 'collection'
353
+ ? html`${bucket.displayText ?? bucket.key}`
354
+ : html`
355
+ <async-collection-name
356
+ .collectionNameCache=${this.collectionNameCache}
357
+ .identifier=${bucket.key}
358
+ placeholder="-"
359
+ ></async-collection-name>
360
+ `;
361
+
362
+ const facetHidden = bucket.state === 'hidden';
363
+ const facetSelected = bucket.state === 'selected';
364
+
365
+ const titleText = `${facetGroup.key}: ${
366
+ bucket.displayText ?? bucket.key
367
+ }`;
368
+ const onlyShowText = facetSelected
369
+ ? `Show all ${facetGroup.key}s`
370
+ : `Only show ${titleText}`;
371
+ const hideText = `Hide ${titleText}`;
372
+ const unhideText = `Unhide ${titleText}`;
373
+ const showHideText = facetHidden ? unhideText : hideText;
374
+
375
+ return html`
376
+ <li>
377
+ <div class="facet-row">
378
+ <div class="facet-checkbox">
379
+ <input
380
+ type="checkbox"
381
+ .name=${facetGroup.key}
382
+ .value=${bucket.key}
383
+ @click=${(e: Event) => {
384
+ this.facetClicked(e, bucket, false);
385
+ }}
386
+ .checked=${facetSelected}
387
+ class="select-facet-checkbox"
388
+ title=${onlyShowText}
389
+ id=${showOnlyCheckboxId}
390
+ />
391
+ <input
392
+ type="checkbox"
393
+ id=${negativeCheckboxId}
394
+ .name=${facetGroup.key}
395
+ .value=${bucket.key}
396
+ @click=${(e: Event) => {
397
+ this.facetClicked(e, bucket, true);
398
+ }}
399
+ .checked=${facetHidden}
400
+ class="hide-facet-checkbox"
401
+ />
402
+ <label
403
+ for=${negativeCheckboxId}
404
+ class="hide-facet-icon"
405
+ title=${showHideText}
406
+ >
407
+ ${facetHidden ? eyeClosedIcon : eyeIcon}
408
+ </label>
409
+ </div>
410
+
411
+ <label
412
+ for=${showOnlyCheckboxId}
413
+ class="facet-info-display"
414
+ title=${onlyShowText}
415
+ >
416
+ <div class="facet-title">${bucketTextDisplay}</div>
417
+ <div class="facet-count">${bucket.count}</div>
418
+ </label>
256
419
  </div>
257
- <div class="facet-title">${bucket.key}</div>
258
- <div class="facet-count">${bucket.count}</div>
259
- </label>
260
- </li>
261
- `
420
+ </li>
421
+ `;
422
+ }
262
423
  )}
263
424
  </ul>
264
425
  `;
265
426
  }
266
427
 
267
- private facetChecked(name: string, value: string) {
268
- const { selectedFacets } = this;
269
- const facetClone = { ...selectedFacets };
270
- const currentFacetValues = facetClone[name];
271
- if (currentFacetValues) {
272
- currentFacetValues.push(value);
273
- facetClone[name] = currentFacetValues;
428
+ private facetClicked(e: Event, bucket: FacetBucket, negative: boolean) {
429
+ const target = e.target as HTMLInputElement;
430
+ const { checked, name, value } = target;
431
+ if (checked) {
432
+ this.facetChecked(name as FacetOption, value, negative);
274
433
  } else {
275
- facetClone[name] = [value];
434
+ this.facetUnchecked(name as FacetOption, value);
276
435
  }
277
- this.selectedFacets = facetClone;
278
436
  }
279
437
 
280
- private facetUnchecked(name: string, value: string) {
438
+ private facetChecked(key: FacetOption, value: string, negative: boolean) {
281
439
  const { selectedFacets } = this;
282
- const facetClone = { ...selectedFacets };
283
- let currentFacetValues = selectedFacets[name];
284
- if (currentFacetValues) {
285
- currentFacetValues = currentFacetValues.filter(el => el !== value);
286
- facetClone[name] = currentFacetValues;
287
- if (currentFacetValues.length === 0) {
288
- delete facetClone[name];
289
- }
440
+ let newFacets: SelectedFacets;
441
+ if (selectedFacets) {
442
+ newFacets = {
443
+ ...selectedFacets,
444
+ };
445
+ } else {
446
+ newFacets = defaultSelectedFacets;
290
447
  }
291
- this.selectedFacets = facetClone;
448
+ newFacets[key][value] = negative ? 'hidden' : 'selected';
449
+ this.selectedFacets = newFacets;
292
450
  }
293
451
 
294
- private facetToggled(e: Event) {
295
- const target = e.target as HTMLInputElement;
296
- const { checked, name, value } = target;
297
- if (checked) {
298
- this.facetChecked(name, value);
452
+ private facetUnchecked(key: FacetOption, value: string) {
453
+ const { selectedFacets } = this;
454
+ let newFacets: SelectedFacets;
455
+ if (selectedFacets) {
456
+ newFacets = {
457
+ ...selectedFacets,
458
+ };
299
459
  } else {
300
- this.facetUnchecked(name, value);
460
+ newFacets = defaultSelectedFacets;
301
461
  }
302
-
303
- const event = new CustomEvent<Record<string, string[]>>('facetsChanged', {
304
- detail: this.selectedFacets,
305
- });
306
- this.dispatchEvent(event);
462
+ delete newFacets[key][value];
463
+ this.selectedFacets = newFacets;
307
464
  }
308
465
 
309
466
  /**
@@ -332,12 +489,52 @@ export class CollectionFacets extends LitElement {
332
489
  opacity: 0.5;
333
490
  }
334
491
 
492
+ .collapser {
493
+ display: inline-block;
494
+ cursor: pointer;
495
+ width: 10px;
496
+ height: 10px;
497
+ }
498
+
499
+ .collapser svg {
500
+ transition: transform 0.2s ease-in-out;
501
+ }
502
+
503
+ .collapser.open svg {
504
+ transform: rotate(90deg);
505
+ }
506
+
507
+ .facet-group {
508
+ margin-bottom: 2rem;
509
+ }
510
+
511
+ .facet-group h1 {
512
+ margin-bottom: 0.7rem;
513
+ }
514
+
515
+ .facet-group.mobile h1 {
516
+ cursor: pointer;
517
+ }
518
+
519
+ .facet-group-content {
520
+ transition: max-height 0.2s ease-in-out;
521
+ }
522
+
523
+ .facet-group.mobile .facet-group-content {
524
+ max-height: 0;
525
+ overflow: hidden;
526
+ }
527
+
528
+ .facet-group.mobile .facet-group-content.open {
529
+ max-height: 2000px;
530
+ }
531
+
335
532
  h1 {
336
533
  font-size: 1.4rem;
337
534
  font-weight: 200;
338
535
  border-bottom: 1px solid rgb(232, 232, 232);
339
536
  padding-bottom: 3px;
340
- margin: 24px 0 14px 0;
537
+ margin: 0;
341
538
  }
342
539
 
343
540
  ul.facet-list {
@@ -352,15 +549,23 @@ export class CollectionFacets extends LitElement {
352
549
 
353
550
  .facet-checkbox {
354
551
  margin-right: 0.5rem;
552
+ display: flex;
553
+ align-items: center;
355
554
  }
356
555
 
357
556
  .facet-row {
358
557
  display: flex;
359
- align-items: center;
558
+ align-items: start;
360
559
  font-weight: 500;
361
560
  font-size: 1.2rem;
362
561
  }
363
562
 
563
+ .facet-info-display {
564
+ display: flex;
565
+ flex: 1;
566
+ cursor: pointer;
567
+ }
568
+
364
569
  .facet-title {
365
570
  flex: 1;
366
571
  }
@@ -368,6 +573,21 @@ export class CollectionFacets extends LitElement {
368
573
  .facet-count {
369
574
  margin-left: 0.5rem;
370
575
  }
576
+
577
+ .select-facet-checkbox {
578
+ cursor: pointer;
579
+ margin-right: 5px;
580
+ }
581
+
582
+ .hide-facet-checkbox {
583
+ display: none;
584
+ }
585
+
586
+ .hide-facet-icon {
587
+ width: 15px;
588
+ height: 15px;
589
+ cursor: pointer;
590
+ }
371
591
  `;
372
592
  }
373
593
  }
@@ -0,0 +1,64 @@
1
+ import { codeToLanguageMap } from './language-code-mapping';
2
+
3
+ // To serialize the list of potential language codes, we store
4
+ // the string in the format `en-us|en|en-gb` with `|` being the separator.
5
+ // This allows us to generate a query of `language:(en-us OR en OR en-gb)`
6
+ // when we deserialize the string.
7
+ export interface LanguageCodeHandlerInterface {
8
+ /**
9
+ * This takes a list of codes in the format `en-us|en|en-gb` and
10
+ * returns the first language code in the list.
11
+ *
12
+ * The codes in the string should always represent the same language name.
13
+ * eg `en-us`, `en-gb`, and `en` should always be "English"
14
+ *
15
+ * @param languageCodes
16
+ */
17
+ getLanguageNameFromCodeString(languageCodes: string): string;
18
+
19
+ /**
20
+ * This creates a serialized string of language codes for the given language.
21
+ *
22
+ * eg. `English` becomes `en-us|en|en-gb|.....`
23
+ *
24
+ * @param languageName
25
+ */
26
+ getCodeStringFromLanguageName(languageName: string): string;
27
+
28
+ /**
29
+ * Get an array of langauge codes from a serialized string.
30
+ *
31
+ * eg. `en-us|en|en-gb` becomes `['en-us', 'en', 'en-gb']`
32
+ *
33
+ * @param languageCodes
34
+ */
35
+ getCodeArrayFromCodeString(languageCodes: string): string[];
36
+ }
37
+
38
+ export class LanguageCodeHandler implements LanguageCodeHandlerInterface {
39
+ private delimeter = '|';
40
+
41
+ /** @inheritdoc */
42
+ getLanguageNameFromCodeString(languageCodes: string): string {
43
+ const split = this.getCodeArrayFromCodeString(languageCodes);
44
+ if (split.length === 0) return '';
45
+ const languageCode = split[0];
46
+ const languageName = codeToLanguageMap[languageCode];
47
+ return languageName ?? languageCodes;
48
+ }
49
+
50
+ /** @inheritdoc */
51
+ getCodeStringFromLanguageName(languageName: string): string {
52
+ const languageCodes = Object.keys(codeToLanguageMap).filter(
53
+ code => codeToLanguageMap[code] === languageName
54
+ );
55
+ const stringifiedCodes = languageCodes?.join(this.delimeter);
56
+ return stringifiedCodes;
57
+ }
58
+
59
+ /** @inheritdoc */
60
+ getCodeArrayFromCodeString(languageCodes: string): string[] {
61
+ const split = languageCodes.split(this.delimeter);
62
+ return split;
63
+ }
64
+ }