@internetarchive/collection-browser 0.3.1-alpha.3 → 0.3.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 (214) 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 +236 -236
  68. package/dist/src/collection-browser.js +1406 -1406
  69. package/dist/src/collection-browser.js.map +1 -1
  70. package/dist/src/collection-facets/facets-template.d.ts +16 -16
  71. package/dist/src/collection-facets/facets-template.js +275 -275
  72. package/dist/src/collection-facets/facets-template.js.map +1 -1
  73. package/dist/src/collection-facets/more-facets-content.d.ts +74 -74
  74. package/dist/src/collection-facets/more-facets-content.js +464 -464
  75. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  76. package/dist/src/collection-facets/more-facets-pagination.d.ts +36 -36
  77. package/dist/src/collection-facets/more-facets-pagination.js +192 -192
  78. package/dist/src/collection-facets.d.ts +71 -71
  79. package/dist/src/collection-facets.js +496 -496
  80. package/dist/src/collection-facets.js.map +1 -1
  81. package/dist/src/empty-placeholder.d.ts +11 -11
  82. package/dist/src/empty-placeholder.js +42 -42
  83. package/dist/src/language-code-handler/language-code-handler.d.ts +37 -37
  84. package/dist/src/language-code-handler/language-code-handler.js +26 -26
  85. package/dist/src/language-code-handler/language-code-mapping.d.ts +1 -1
  86. package/dist/src/language-code-handler/language-code-mapping.js +562 -562
  87. package/dist/src/mediatype/mediatype-config.d.ts +3 -3
  88. package/dist/src/mediatype/mediatype-config.js +85 -85
  89. package/dist/src/models.d.ts +97 -97
  90. package/dist/src/models.js +100 -100
  91. package/dist/src/models.js.map +1 -1
  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/restoration-state-handler.js.map +1 -1
  95. package/dist/src/sort-filter-bar/alpha-bar.d.ts +9 -9
  96. package/dist/src/sort-filter-bar/alpha-bar.js +41 -41
  97. package/dist/src/sort-filter-bar/img/compact.d.ts +1 -1
  98. package/dist/src/sort-filter-bar/img/compact.js +2 -2
  99. package/dist/src/sort-filter-bar/img/list.d.ts +1 -1
  100. package/dist/src/sort-filter-bar/img/list.js +2 -2
  101. package/dist/src/sort-filter-bar/img/sort-triangle.d.ts +1 -1
  102. package/dist/src/sort-filter-bar/img/sort-triangle.js +2 -2
  103. package/dist/src/sort-filter-bar/img/tile.d.ts +1 -1
  104. package/dist/src/sort-filter-bar/img/tile.js +2 -2
  105. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +107 -107
  106. package/dist/src/sort-filter-bar/sort-filter-bar.js +423 -423
  107. package/dist/src/styles/item-image-styles.d.ts +8 -8
  108. package/dist/src/styles/item-image-styles.js +9 -9
  109. package/dist/src/tiles/collection-browser-loading-tile.d.ts +5 -5
  110. package/dist/src/tiles/collection-browser-loading-tile.js +15 -15
  111. package/dist/src/tiles/grid/account-tile.d.ts +8 -8
  112. package/dist/src/tiles/grid/account-tile.js +20 -20
  113. package/dist/src/tiles/grid/collection-tile.d.ts +7 -7
  114. package/dist/src/tiles/grid/collection-tile.js +23 -23
  115. package/dist/src/tiles/grid/item-tile.d.ts +24 -24
  116. package/dist/src/tiles/grid/item-tile.js +87 -87
  117. package/dist/src/tiles/grid/tile-stats.d.ts +10 -10
  118. package/dist/src/tiles/grid/tile-stats.js +35 -35
  119. package/dist/src/tiles/image-block.d.ts +17 -17
  120. package/dist/src/tiles/image-block.js +73 -73
  121. package/dist/src/tiles/item-image.d.ts +31 -31
  122. package/dist/src/tiles/item-image.js +103 -103
  123. package/dist/src/tiles/list/account-label.d.ts +1 -1
  124. package/dist/src/tiles/list/account-label.js +6 -6
  125. package/dist/src/tiles/list/date-label.d.ts +1 -1
  126. package/dist/src/tiles/list/date-label.js +12 -12
  127. package/dist/src/tiles/list/tile-list-compact-header.d.ts +12 -12
  128. package/dist/src/tiles/list/tile-list-compact-header.js +41 -41
  129. package/dist/src/tiles/list/tile-list-compact.d.ts +20 -20
  130. package/dist/src/tiles/list/tile-list-compact.js +87 -87
  131. package/dist/src/tiles/list/tile-list.d.ts +50 -50
  132. package/dist/src/tiles/list/tile-list.js +468 -468
  133. package/dist/src/tiles/list/tile-list.js.map +1 -1
  134. package/dist/src/tiles/mediatype-icon.d.ts +9 -9
  135. package/dist/src/tiles/mediatype-icon.js +47 -47
  136. package/dist/src/tiles/overlay/icon-overlay.d.ts +7 -7
  137. package/dist/src/tiles/overlay/icon-overlay.js +31 -31
  138. package/dist/src/tiles/overlay/text-overlay.d.ts +8 -8
  139. package/dist/src/tiles/overlay/text-overlay.js +31 -31
  140. package/dist/src/tiles/text-snippet-block.d.ts +29 -29
  141. package/dist/src/tiles/text-snippet-block.js +81 -81
  142. package/dist/src/tiles/tile-dispatcher.d.ts +36 -36
  143. package/dist/src/tiles/tile-dispatcher.js +128 -128
  144. package/dist/src/utils/analytics-events.d.ts +18 -18
  145. package/dist/src/utils/analytics-events.js +20 -20
  146. package/dist/src/utils/format-count.d.ts +7 -7
  147. package/dist/src/utils/format-count.js +76 -76
  148. package/dist/src/utils/format-count.js.map +1 -1
  149. package/dist/src/utils/format-date.d.ts +2 -2
  150. package/dist/src/utils/format-date.js +23 -23
  151. package/dist/test/collection-browser.test.d.ts +1 -1
  152. package/dist/test/collection-browser.test.js +344 -344
  153. package/dist/test/collection-browser.test.js.map +1 -1
  154. package/dist/test/collection-facets/facets-template.test.d.ts +1 -1
  155. package/dist/test/collection-facets/facets-template.test.js +62 -62
  156. package/dist/test/collection-facets/more-facets-content.test.d.ts +1 -1
  157. package/dist/test/collection-facets/more-facets-content.test.js +91 -91
  158. package/dist/test/collection-facets/more-facets-pagination.test.d.ts +1 -1
  159. package/dist/test/collection-facets/more-facets-pagination.test.js +117 -117
  160. package/dist/test/collection-facets.test.d.ts +2 -2
  161. package/dist/test/collection-facets.test.js +387 -387
  162. package/dist/test/collection-facets.test.js.map +1 -1
  163. package/dist/test/empty-placeholder.test.d.ts +1 -1
  164. package/dist/test/empty-placeholder.test.js +33 -33
  165. package/dist/test/icon-overlay.test.d.ts +1 -1
  166. package/dist/test/icon-overlay.test.js +24 -24
  167. package/dist/test/item-image.test.d.ts +1 -1
  168. package/dist/test/item-image.test.js +56 -56
  169. package/dist/test/mediatype-config.test.d.ts +1 -1
  170. package/dist/test/mediatype-config.test.js +16 -16
  171. package/dist/test/mocks/mock-analytics-handler.d.ts +10 -10
  172. package/dist/test/mocks/mock-analytics-handler.js +15 -15
  173. package/dist/test/mocks/mock-collection-name-cache.d.ts +7 -7
  174. package/dist/test/mocks/mock-collection-name-cache.js +13 -13
  175. package/dist/test/mocks/mock-search-responses.d.ts +5 -5
  176. package/dist/test/mocks/mock-search-responses.js +103 -103
  177. package/dist/test/mocks/mock-search-service.d.ts +13 -13
  178. package/dist/test/mocks/mock-search-service.js +25 -25
  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.test.d.ts +1 -1
  192. package/dist/test/tiles/list/tile-list.test.js +36 -36
  193. package/dist/test/utils/format-count.test.d.ts +1 -1
  194. package/dist/test/utils/format-count.test.js +23 -23
  195. package/dist/test/utils/format-date.test.d.ts +1 -1
  196. package/dist/test/utils/format-date.test.js +17 -17
  197. package/index.html +24 -24
  198. package/local.archive.org.cert +86 -86
  199. package/local.archive.org.key +27 -27
  200. package/package.json +115 -115
  201. package/renovate.json +6 -6
  202. package/src/collection-browser.ts +1530 -1530
  203. package/src/collection-facets/facets-template.ts +294 -294
  204. package/src/collection-facets/more-facets-content.ts +518 -518
  205. package/src/collection-facets.ts +569 -569
  206. package/src/models.ts +216 -216
  207. package/src/restoration-state-handler.ts +302 -302
  208. package/src/tiles/list/tile-list.ts +509 -509
  209. package/src/utils/format-count.ts +96 -96
  210. package/test/collection-browser.test.ts +490 -490
  211. package/test/collection-facets.test.ts +510 -510
  212. package/tsconfig.json +21 -21
  213. package/web-dev-server.config.mjs +30 -30
  214. package/web-test-runner.config.mjs +41 -41
@@ -1,302 +1,302 @@
1
- import type { SortDirection, SortParam } from '@internetarchive/search-service';
2
- import { getCookie, setCookie } from 'typescript-cookie';
3
- import {
4
- MetadataFieldToSortField,
5
- MetadataSortField,
6
- FacetOption,
7
- CollectionBrowserContext,
8
- CollectionDisplayMode,
9
- SelectedFacets,
10
- SortField,
11
- FacetBucket,
12
- FacetState,
13
- } from './models';
14
-
15
- export interface RestorationState {
16
- displayMode?: CollectionDisplayMode;
17
- sortParam?: SortParam;
18
- selectedSort?: SortField;
19
- sortDirection?: SortDirection;
20
- selectedFacets: SelectedFacets;
21
- baseQuery?: string;
22
- currentPage?: number;
23
- dateRangeQueryClause?: string;
24
- titleQuery?: string;
25
- creatorQuery?: string;
26
- minSelectedDate?: string;
27
- maxSelectedDate?: string;
28
- selectedTitleFilter?: string;
29
- selectedCreatorFilter?: string;
30
- }
31
-
32
- export interface RestorationStateHandlerInterface {
33
- persistState(state: RestorationState): void;
34
- getRestorationState(): RestorationState;
35
- }
36
-
37
- export class RestorationStateHandler
38
- implements RestorationStateHandlerInterface
39
- {
40
- private context: CollectionBrowserContext;
41
-
42
- private cookieDomain = '.archive.org';
43
-
44
- private cookieExpiration = 30;
45
-
46
- private cookiePath = '/';
47
-
48
- constructor(options: { context: CollectionBrowserContext }) {
49
- this.context = options.context;
50
- }
51
-
52
- persistState(state: RestorationState): void {
53
- if (state.displayMode) this.persistViewStateToCookies(state.displayMode);
54
- this.persistQueryStateToUrl(state);
55
- }
56
-
57
- getRestorationState(): RestorationState {
58
- const restorationState = this.loadQueryStateFromUrl();
59
- const displayMode = this.loadTileViewStateFromCookies();
60
- restorationState.displayMode = displayMode;
61
- return restorationState;
62
- }
63
-
64
- private persistViewStateToCookies(displayMode: CollectionDisplayMode) {
65
- const gridState = displayMode === 'grid' ? 'tiles' : 'lists';
66
- setCookie(`view-${this.context}`, gridState, {
67
- domain: this.cookieDomain,
68
- expires: this.cookieExpiration,
69
- path: this.cookiePath,
70
- });
71
- const detailsState = displayMode === 'list-detail' ? 'showdetails' : '';
72
- setCookie(`showdetails-${this.context}`, detailsState, {
73
- domain: this.cookieDomain,
74
- expires: this.cookieExpiration,
75
- path: this.cookiePath,
76
- });
77
- }
78
-
79
- private loadTileViewStateFromCookies(): CollectionDisplayMode {
80
- const viewState = getCookie(`view-${this.context}`);
81
- const detailsState = getCookie(`showdetails-${this.context}`);
82
- if (viewState === 'tiles' || viewState === undefined) return 'grid';
83
- if (detailsState === 'showdetails') return 'list-detail';
84
- return 'list-compact';
85
- }
86
-
87
- private persistQueryStateToUrl(state: RestorationState) {
88
- const url = new URL(window.location.href);
89
- const { searchParams } = url;
90
- searchParams.delete('sort');
91
- searchParams.delete('query');
92
- searchParams.delete('page');
93
- searchParams.delete('and[]');
94
- searchParams.delete('not[]');
95
-
96
- if (state.sortParam) {
97
- const prefix = state.sortParam.direction === 'desc' ? '-' : '';
98
- searchParams.set('sort', `${prefix}${state.sortParam.field}`);
99
- }
100
-
101
- if (state.baseQuery) {
102
- searchParams.set('query', state.baseQuery);
103
- }
104
-
105
- if (state.currentPage) {
106
- if (state.currentPage > 1) {
107
- searchParams.set('page', state.currentPage.toString());
108
- } else {
109
- searchParams.delete('page');
110
- }
111
- }
112
-
113
- if (state.selectedFacets) {
114
- for (const [facetName, facetValues] of Object.entries(
115
- state.selectedFacets
116
- )) {
117
- const facetEntries = Object.entries(facetValues);
118
- // eslint-disable-next-line no-continue
119
- if (facetEntries.length === 0) continue;
120
- for (const [key, data] of facetEntries) {
121
- const notValue = data.state === 'hidden';
122
- const paramValue = `${facetName}:"${key}"`;
123
- if (notValue) {
124
- searchParams.append('not[]', paramValue);
125
- } else {
126
- searchParams.append('and[]', paramValue);
127
- }
128
- }
129
- }
130
- }
131
-
132
- if (state.dateRangeQueryClause) {
133
- searchParams.append('and[]', state.dateRangeQueryClause);
134
- }
135
- if (state.titleQuery) {
136
- searchParams.append('and[]', state.titleQuery);
137
- }
138
- if (state.creatorQuery) {
139
- searchParams.append('and[]', state.creatorQuery);
140
- }
141
-
142
- window.history.pushState(
143
- {
144
- sort: state.sortParam,
145
- query: state.baseQuery,
146
- page: state.currentPage,
147
- and: state.selectedFacets,
148
- not: state.selectedFacets,
149
- dateRange: state.dateRangeQueryClause,
150
- },
151
- '',
152
- url
153
- );
154
- }
155
-
156
- private loadQueryStateFromUrl(): RestorationState {
157
- const url = new URL(window.location.href);
158
- const pageNumber = url.searchParams.get('page');
159
- const searchQuery = url.searchParams.get('query');
160
- const sortQuery = url.searchParams.get('sort');
161
- const facetAnds = url.searchParams.getAll('and[]');
162
- const facetNots = url.searchParams.getAll('not[]');
163
-
164
- const restorationState: RestorationState = {
165
- selectedFacets: {
166
- subject: {},
167
- lending: {},
168
- creator: {},
169
- mediatype: {},
170
- language: {},
171
- collection: {},
172
- year: {},
173
- },
174
- };
175
-
176
- if (pageNumber) {
177
- const parsed = parseInt(pageNumber, 10);
178
- restorationState.currentPage = parsed;
179
- } else {
180
- restorationState.currentPage = 1;
181
- }
182
- if (searchQuery) {
183
- restorationState.baseQuery = searchQuery;
184
- }
185
- if (sortQuery) {
186
- // check for two different sort formats: `date desc` and `-date`
187
- const hasSpace = sortQuery.indexOf(' ') > -1;
188
- if (hasSpace) {
189
- const [field, direction] = sortQuery.split(' ');
190
- const metadataField =
191
- MetadataFieldToSortField[field as MetadataSortField];
192
- if (metadataField) {
193
- restorationState.selectedSort = metadataField;
194
- }
195
- if (direction === 'desc' || direction === 'asc') {
196
- restorationState.sortDirection = direction as SortDirection;
197
- }
198
- } else {
199
- const sortDirection = sortQuery.startsWith('-') ? 'desc' : 'asc';
200
- const sortField = sortQuery.startsWith('-')
201
- ? sortQuery.slice(1)
202
- : sortQuery;
203
- const metadataField =
204
- MetadataFieldToSortField[sortField as MetadataSortField];
205
- if (metadataField) restorationState.selectedSort = metadataField;
206
- restorationState.sortDirection = sortDirection as SortDirection;
207
- }
208
- }
209
- if (facetAnds) {
210
- facetAnds.forEach(and => {
211
- const [field, value] = and.split(':');
212
-
213
- switch (field) {
214
- case 'year': {
215
- const [minDate, maxDate] = value.split(' TO ');
216
- // we have two potential ways of filtering by date:
217
- // the range with "date TO date" or the single date with "date"
218
- // this is checking for the range case and if we don't have those, fall
219
- // back to the single date case
220
- if (minDate && maxDate) {
221
- restorationState.minSelectedDate = minDate.substring(
222
- 1,
223
- minDate.length
224
- );
225
- restorationState.maxSelectedDate = maxDate.substring(
226
- 0,
227
- maxDate.length - 1
228
- );
229
- restorationState.dateRangeQueryClause = `year:${value}`;
230
- } else {
231
- this.setSelectedFacetState(
232
- restorationState.selectedFacets,
233
- field as FacetOption,
234
- value,
235
- 'selected'
236
- );
237
- }
238
- break;
239
- }
240
- case 'firstTitle':
241
- restorationState.selectedTitleFilter = value;
242
- break;
243
- case 'firstCreator':
244
- restorationState.selectedCreatorFilter = value;
245
- break;
246
- default:
247
- this.setSelectedFacetState(
248
- restorationState.selectedFacets,
249
- field as FacetOption,
250
- value,
251
- 'selected'
252
- );
253
- }
254
- });
255
- }
256
- if (facetNots) {
257
- facetNots.forEach(not => {
258
- const [field, value] = not.split(':');
259
- this.setSelectedFacetState(
260
- restorationState.selectedFacets,
261
- field as FacetOption,
262
- value,
263
- 'hidden'
264
- );
265
- });
266
- }
267
- return restorationState;
268
- }
269
-
270
- // remove optional opening and closing quotes from a string
271
- private stripQuotes(value: string): string {
272
- if (value.startsWith('"') && value.endsWith('"')) {
273
- return value.substring(1, value.length - 1);
274
- }
275
- return value;
276
- }
277
-
278
- /**
279
- * Sets the facet state for the given field & value to the given state,
280
- * creating any previously-undefined buckets as needed.
281
- */
282
- private setSelectedFacetState(
283
- selectedFacets: SelectedFacets,
284
- field: FacetOption,
285
- value: string,
286
- state: FacetState
287
- ): void {
288
- const facet = selectedFacets[field];
289
- const unQuotedValue = this.stripQuotes(value);
290
- facet[unQuotedValue] ??= this.getDefaultBucket(value);
291
- facet[unQuotedValue].state = state;
292
- }
293
-
294
- /** Returns a default bucket with the given key, count of 0, and state 'none'. */
295
- private getDefaultBucket(key: string): FacetBucket {
296
- return {
297
- key,
298
- count: 0,
299
- state: 'none',
300
- };
301
- }
302
- }
1
+ import type { SortDirection, SortParam } from '@internetarchive/search-service';
2
+ import { getCookie, setCookie } from 'typescript-cookie';
3
+ import {
4
+ MetadataFieldToSortField,
5
+ MetadataSortField,
6
+ FacetOption,
7
+ CollectionBrowserContext,
8
+ CollectionDisplayMode,
9
+ SelectedFacets,
10
+ SortField,
11
+ FacetBucket,
12
+ FacetState,
13
+ } from './models';
14
+
15
+ export interface RestorationState {
16
+ displayMode?: CollectionDisplayMode;
17
+ sortParam?: SortParam;
18
+ selectedSort?: SortField;
19
+ sortDirection?: SortDirection;
20
+ selectedFacets: SelectedFacets;
21
+ baseQuery?: string;
22
+ currentPage?: number;
23
+ dateRangeQueryClause?: string;
24
+ titleQuery?: string;
25
+ creatorQuery?: string;
26
+ minSelectedDate?: string;
27
+ maxSelectedDate?: string;
28
+ selectedTitleFilter?: string;
29
+ selectedCreatorFilter?: string;
30
+ }
31
+
32
+ export interface RestorationStateHandlerInterface {
33
+ persistState(state: RestorationState): void;
34
+ getRestorationState(): RestorationState;
35
+ }
36
+
37
+ export class RestorationStateHandler
38
+ implements RestorationStateHandlerInterface
39
+ {
40
+ private context: CollectionBrowserContext;
41
+
42
+ private cookieDomain = '.archive.org';
43
+
44
+ private cookieExpiration = 30;
45
+
46
+ private cookiePath = '/';
47
+
48
+ constructor(options: { context: CollectionBrowserContext }) {
49
+ this.context = options.context;
50
+ }
51
+
52
+ persistState(state: RestorationState): void {
53
+ if (state.displayMode) this.persistViewStateToCookies(state.displayMode);
54
+ this.persistQueryStateToUrl(state);
55
+ }
56
+
57
+ getRestorationState(): RestorationState {
58
+ const restorationState = this.loadQueryStateFromUrl();
59
+ const displayMode = this.loadTileViewStateFromCookies();
60
+ restorationState.displayMode = displayMode;
61
+ return restorationState;
62
+ }
63
+
64
+ private persistViewStateToCookies(displayMode: CollectionDisplayMode) {
65
+ const gridState = displayMode === 'grid' ? 'tiles' : 'lists';
66
+ setCookie(`view-${this.context}`, gridState, {
67
+ domain: this.cookieDomain,
68
+ expires: this.cookieExpiration,
69
+ path: this.cookiePath,
70
+ });
71
+ const detailsState = displayMode === 'list-detail' ? 'showdetails' : '';
72
+ setCookie(`showdetails-${this.context}`, detailsState, {
73
+ domain: this.cookieDomain,
74
+ expires: this.cookieExpiration,
75
+ path: this.cookiePath,
76
+ });
77
+ }
78
+
79
+ private loadTileViewStateFromCookies(): CollectionDisplayMode {
80
+ const viewState = getCookie(`view-${this.context}`);
81
+ const detailsState = getCookie(`showdetails-${this.context}`);
82
+ if (viewState === 'tiles' || viewState === undefined) return 'grid';
83
+ if (detailsState === 'showdetails') return 'list-detail';
84
+ return 'list-compact';
85
+ }
86
+
87
+ private persistQueryStateToUrl(state: RestorationState) {
88
+ const url = new URL(window.location.href);
89
+ const { searchParams } = url;
90
+ searchParams.delete('sort');
91
+ searchParams.delete('query');
92
+ searchParams.delete('page');
93
+ searchParams.delete('and[]');
94
+ searchParams.delete('not[]');
95
+
96
+ if (state.sortParam) {
97
+ const prefix = state.sortParam.direction === 'desc' ? '-' : '';
98
+ searchParams.set('sort', `${prefix}${state.sortParam.field}`);
99
+ }
100
+
101
+ if (state.baseQuery) {
102
+ searchParams.set('query', state.baseQuery);
103
+ }
104
+
105
+ if (state.currentPage) {
106
+ if (state.currentPage > 1) {
107
+ searchParams.set('page', state.currentPage.toString());
108
+ } else {
109
+ searchParams.delete('page');
110
+ }
111
+ }
112
+
113
+ if (state.selectedFacets) {
114
+ for (const [facetName, facetValues] of Object.entries(
115
+ state.selectedFacets
116
+ )) {
117
+ const facetEntries = Object.entries(facetValues);
118
+ // eslint-disable-next-line no-continue
119
+ if (facetEntries.length === 0) continue;
120
+ for (const [key, data] of facetEntries) {
121
+ const notValue = data.state === 'hidden';
122
+ const paramValue = `${facetName}:"${key}"`;
123
+ if (notValue) {
124
+ searchParams.append('not[]', paramValue);
125
+ } else {
126
+ searchParams.append('and[]', paramValue);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ if (state.dateRangeQueryClause) {
133
+ searchParams.append('and[]', state.dateRangeQueryClause);
134
+ }
135
+ if (state.titleQuery) {
136
+ searchParams.append('and[]', state.titleQuery);
137
+ }
138
+ if (state.creatorQuery) {
139
+ searchParams.append('and[]', state.creatorQuery);
140
+ }
141
+
142
+ window.history.pushState(
143
+ {
144
+ sort: state.sortParam,
145
+ query: state.baseQuery,
146
+ page: state.currentPage,
147
+ and: state.selectedFacets,
148
+ not: state.selectedFacets,
149
+ dateRange: state.dateRangeQueryClause,
150
+ },
151
+ '',
152
+ url
153
+ );
154
+ }
155
+
156
+ private loadQueryStateFromUrl(): RestorationState {
157
+ const url = new URL(window.location.href);
158
+ const pageNumber = url.searchParams.get('page');
159
+ const searchQuery = url.searchParams.get('query');
160
+ const sortQuery = url.searchParams.get('sort');
161
+ const facetAnds = url.searchParams.getAll('and[]');
162
+ const facetNots = url.searchParams.getAll('not[]');
163
+
164
+ const restorationState: RestorationState = {
165
+ selectedFacets: {
166
+ subject: {},
167
+ lending: {},
168
+ creator: {},
169
+ mediatype: {},
170
+ language: {},
171
+ collection: {},
172
+ year: {},
173
+ },
174
+ };
175
+
176
+ if (pageNumber) {
177
+ const parsed = parseInt(pageNumber, 10);
178
+ restorationState.currentPage = parsed;
179
+ } else {
180
+ restorationState.currentPage = 1;
181
+ }
182
+ if (searchQuery) {
183
+ restorationState.baseQuery = searchQuery;
184
+ }
185
+ if (sortQuery) {
186
+ // check for two different sort formats: `date desc` and `-date`
187
+ const hasSpace = sortQuery.indexOf(' ') > -1;
188
+ if (hasSpace) {
189
+ const [field, direction] = sortQuery.split(' ');
190
+ const metadataField =
191
+ MetadataFieldToSortField[field as MetadataSortField];
192
+ if (metadataField) {
193
+ restorationState.selectedSort = metadataField;
194
+ }
195
+ if (direction === 'desc' || direction === 'asc') {
196
+ restorationState.sortDirection = direction as SortDirection;
197
+ }
198
+ } else {
199
+ const sortDirection = sortQuery.startsWith('-') ? 'desc' : 'asc';
200
+ const sortField = sortQuery.startsWith('-')
201
+ ? sortQuery.slice(1)
202
+ : sortQuery;
203
+ const metadataField =
204
+ MetadataFieldToSortField[sortField as MetadataSortField];
205
+ if (metadataField) restorationState.selectedSort = metadataField;
206
+ restorationState.sortDirection = sortDirection as SortDirection;
207
+ }
208
+ }
209
+ if (facetAnds) {
210
+ facetAnds.forEach(and => {
211
+ const [field, value] = and.split(':');
212
+
213
+ switch (field) {
214
+ case 'year': {
215
+ const [minDate, maxDate] = value.split(' TO ');
216
+ // we have two potential ways of filtering by date:
217
+ // the range with "date TO date" or the single date with "date"
218
+ // this is checking for the range case and if we don't have those, fall
219
+ // back to the single date case
220
+ if (minDate && maxDate) {
221
+ restorationState.minSelectedDate = minDate.substring(
222
+ 1,
223
+ minDate.length
224
+ );
225
+ restorationState.maxSelectedDate = maxDate.substring(
226
+ 0,
227
+ maxDate.length - 1
228
+ );
229
+ restorationState.dateRangeQueryClause = `year:${value}`;
230
+ } else {
231
+ this.setSelectedFacetState(
232
+ restorationState.selectedFacets,
233
+ field as FacetOption,
234
+ value,
235
+ 'selected'
236
+ );
237
+ }
238
+ break;
239
+ }
240
+ case 'firstTitle':
241
+ restorationState.selectedTitleFilter = value;
242
+ break;
243
+ case 'firstCreator':
244
+ restorationState.selectedCreatorFilter = value;
245
+ break;
246
+ default:
247
+ this.setSelectedFacetState(
248
+ restorationState.selectedFacets,
249
+ field as FacetOption,
250
+ value,
251
+ 'selected'
252
+ );
253
+ }
254
+ });
255
+ }
256
+ if (facetNots) {
257
+ facetNots.forEach(not => {
258
+ const [field, value] = not.split(':');
259
+ this.setSelectedFacetState(
260
+ restorationState.selectedFacets,
261
+ field as FacetOption,
262
+ value,
263
+ 'hidden'
264
+ );
265
+ });
266
+ }
267
+ return restorationState;
268
+ }
269
+
270
+ // remove optional opening and closing quotes from a string
271
+ private stripQuotes(value: string): string {
272
+ if (value.startsWith('"') && value.endsWith('"')) {
273
+ return value.substring(1, value.length - 1);
274
+ }
275
+ return value;
276
+ }
277
+
278
+ /**
279
+ * Sets the facet state for the given field & value to the given state,
280
+ * creating any previously-undefined buckets as needed.
281
+ */
282
+ private setSelectedFacetState(
283
+ selectedFacets: SelectedFacets,
284
+ field: FacetOption,
285
+ value: string,
286
+ state: FacetState
287
+ ): void {
288
+ const facet = selectedFacets[field];
289
+ const unQuotedValue = this.stripQuotes(value);
290
+ facet[unQuotedValue] ??= this.getDefaultBucket(value);
291
+ facet[unQuotedValue].state = state;
292
+ }
293
+
294
+ /** Returns a default bucket with the given key, count of 0, and state 'none'. */
295
+ private getDefaultBucket(key: string): FacetBucket {
296
+ return {
297
+ key,
298
+ count: 0,
299
+ state: 'none',
300
+ };
301
+ }
302
+ }