@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,509 +1,509 @@
1
- import {
2
- css,
3
- html,
4
- LitElement,
5
- nothing,
6
- PropertyValues,
7
- TemplateResult,
8
- } from 'lit';
9
- import { ifDefined } from 'lit/directives/if-defined.js';
10
- import { join } from 'lit/directives/join.js';
11
- import { map } from 'lit/directives/map.js';
12
- import { customElement, property, state } from 'lit/decorators.js';
13
- import DOMPurify from 'dompurify';
14
-
15
- import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
16
- import type { SortParam } from '@internetarchive/search-service';
17
- import type { TileModel } from '../../models';
18
-
19
- import { dateLabel } from './date-label';
20
- import { accountLabel } from './account-label';
21
- import { formatCount, NumberFormat } from '../../utils/format-count';
22
- import { formatDate, DateFormat } from '../../utils/format-date';
23
-
24
- import '../image-block';
25
- import '../mediatype-icon';
26
-
27
- @customElement('tile-list')
28
- export class TileList extends LitElement {
29
- @property({ type: Object }) model?: TileModel;
30
-
31
- @property({ type: String }) baseNavigationUrl?: string;
32
-
33
- @property({ type: Object })
34
- collectionNameCache?: CollectionNameCacheInterface;
35
-
36
- @property({ type: Number }) currentWidth?: number;
37
-
38
- @property({ type: Number }) currentHeight?: number;
39
-
40
- @property({ type: Object }) sortParam: SortParam | null = null;
41
-
42
- @property({ type: Number }) mobileBreakpoint?: number;
43
-
44
- @state() private collectionLinks: TemplateResult[] = [];
45
-
46
- @property({ type: String }) baseImageUrl?: string;
47
-
48
- @property({ type: Boolean }) loggedIn = false;
49
-
50
- protected updated(changed: PropertyValues): void {
51
- if (changed.has('model')) {
52
- this.fetchCollectionNames();
53
- }
54
- }
55
-
56
- private async fetchCollectionNames() {
57
- if (
58
- !this.model?.collections ||
59
- this.model.collections.length === 0 ||
60
- !this.collectionNameCache
61
- ) {
62
- return;
63
- }
64
- // Note: quirk of Lit: need to replace collectionLinks array,
65
- // otherwise it will not re-render. Can't simply alter the array.
66
- this.collectionLinks = [];
67
- const newCollellectionLinks: TemplateResult[] = [];
68
- const promises: Promise<void>[] = [];
69
- for (const collection of this.model.collections) {
70
- promises.push(
71
- this.collectionNameCache?.collectionNameFor(collection).then(name => {
72
- newCollellectionLinks.push(
73
- this.detailsLink(collection, name ?? collection)
74
- );
75
- })
76
- );
77
- }
78
- await Promise.all(promises);
79
- this.collectionLinks = newCollellectionLinks;
80
- }
81
-
82
- render() {
83
- return html`
84
- <div id="list-line" class="${this.classSize}">
85
- ${this.classSize === 'mobile'
86
- ? this.mobileTemplate
87
- : this.desktopTemplate}
88
- </div>
89
- `;
90
- }
91
-
92
- private get mobileTemplate() {
93
- return html`
94
- <div id="list-line-top">
95
- <div id="list-line-left">${this.imageBlockTemplate}</div>
96
- <div id="list-line-right">
97
- <div id="title-line">
98
- <div id="title">${this.titleTemplate}</div>
99
- ${this.iconRightTemplate}
100
- </div>
101
- </div>
102
- </div>
103
- <div id="list-line-bottom">${this.detailsTemplate}</div>
104
- `;
105
- }
106
-
107
- private get desktopTemplate() {
108
- return html`
109
- <div id="list-line-left">${this.imageBlockTemplate}</div>
110
- <div id="list-line-right">
111
- <div id="title-line">
112
- <div id="title">${this.titleTemplate}</div>
113
- ${this.iconRightTemplate}
114
- </div>
115
- ${this.detailsTemplate}
116
- </div>
117
- `;
118
- }
119
-
120
- private get imageBlockTemplate() {
121
- return html`
122
- <image-block
123
- .model=${this.model}
124
- .baseImageUrl=${this.baseImageUrl}
125
- .isCompactTile=${false}
126
- .isListTile=${true}
127
- .viewSize=${this.classSize}
128
- >
129
- </image-block>
130
- `;
131
- }
132
-
133
- private get detailsTemplate() {
134
- return html`
135
- ${this.itemLineTemplate} ${this.creatorTemplate}
136
- <div id="dates-line">
137
- ${this.datePublishedTemplate} ${this.dateSortByTemplate}
138
- </div>
139
- <div id="views-line">
140
- ${this.viewsTemplate} ${this.ratingTemplate} ${this.reviewsTemplate}
141
- </div>
142
- ${this.topicsTemplate} ${this.collectionsTemplate}
143
- ${this.descriptionTemplate} ${this.textSnippetsTemplate}
144
- `;
145
- }
146
-
147
- // Data templates
148
- private get iconRightTemplate() {
149
- return html`
150
- <div id="icon-right">
151
- <mediatype-icon
152
- .mediatype=${this.model?.mediatype}
153
- .collections=${this.model?.collections}
154
- >
155
- </mediatype-icon>
156
- </div>
157
- `;
158
- }
159
-
160
- private get titleTemplate() {
161
- if (!this.model?.title) {
162
- return nothing;
163
- }
164
- return html` ${this.detailsLink(this.model.identifier, this.model.title)} `;
165
- }
166
-
167
- private get itemLineTemplate() {
168
- const source = this.sourceTemplate;
169
- const volume = this.volumeTemplate;
170
- const issue = this.issueTemplate;
171
- if (!source && !volume && !issue) {
172
- return nothing;
173
- }
174
- return html` <div id="item-line">${source} ${volume} ${issue}</div> `;
175
- }
176
-
177
- private get sourceTemplate() {
178
- if (!this.model?.source) {
179
- return nothing;
180
- }
181
- return html`
182
- <div id="source" class="metadata">
183
- ${this.labelTemplate('Source')}
184
- ${this.searchLink('source', this.model.source)}
185
- </div>
186
- `;
187
- }
188
-
189
- private get volumeTemplate() {
190
- return this.metadataTemplate(this.model?.volume, 'Volume');
191
- }
192
-
193
- private get issueTemplate() {
194
- return this.metadataTemplate(this.model?.issue, 'Issue');
195
- }
196
-
197
- private get creatorTemplate() {
198
- // "Achivist since" if account
199
- if (this.model?.mediatype === 'account') {
200
- return html`
201
- <div id="creator" class="metadata">
202
- <span class="label"> ${accountLabel(this.model?.dateAdded)} </span>
203
- </div>
204
- `;
205
- }
206
- // "Creator" if not account tile
207
- if (!this.model?.creators || this.model.creators.length === 0) {
208
- return nothing;
209
- }
210
- return html`
211
- <div id="creator" class="metadata">
212
- ${this.labelTemplate('By')}
213
- ${join(
214
- map(this.model.creators, id => this.searchLink('creator', id)),
215
- html`, `
216
- )}
217
- </div>
218
- `;
219
- }
220
-
221
- private get datePublishedTemplate() {
222
- return this.metadataTemplate(
223
- formatDate(this.model?.datePublished, 'long'),
224
- 'Published'
225
- );
226
- }
227
-
228
- // Show date label/value when sorted by date type
229
- // Except datePublished which is always shown
230
- private get dateSortByTemplate() {
231
- if (
232
- this.sortParam &&
233
- (this.sortParam.field === 'addeddate' ||
234
- this.sortParam.field === 'reviewdate' ||
235
- this.sortParam.field === 'publicdate')
236
- ) {
237
- return this.metadataTemplate(
238
- formatDate(this.date, 'long'),
239
- dateLabel(this.sortParam.field)
240
- );
241
- }
242
- return nothing;
243
- }
244
-
245
- private get viewsTemplate() {
246
- return this.metadataTemplate(
247
- `${formatCount(this.model?.viewCount ?? 0, this.formatSize)}`,
248
- 'Views'
249
- );
250
- }
251
-
252
- private get ratingTemplate() {
253
- return this.metadataTemplate(this.model?.averageRating, 'Avg Rating');
254
- }
255
-
256
- private get reviewsTemplate() {
257
- return this.metadataTemplate(this.model?.commentCount, 'Reviews');
258
- }
259
-
260
- private get topicsTemplate() {
261
- if (!this.model?.subjects || this.model.subjects.length === 0) {
262
- return nothing;
263
- }
264
- return html`
265
- <div id="topics" class="metadata">
266
- ${this.labelTemplate('Topics')}
267
- ${join(
268
- map(this.model.subjects, id => this.searchLink('subject', id)),
269
- html`, `
270
- )}
271
- </div>
272
- `;
273
- }
274
-
275
- private get collectionsTemplate() {
276
- if (!this.collectionLinks || this.collectionLinks.length === 0) {
277
- return nothing;
278
- }
279
- return html`
280
- <div id="collections" class="metadata">
281
- ${this.labelTemplate('Collections')}
282
- ${join(this.collectionLinks, html`, `)}
283
- </div>
284
- `;
285
- }
286
-
287
- private get descriptionTemplate() {
288
- return this.metadataTemplate(
289
- DOMPurify.sanitize(this.model?.description ?? ''),
290
- '',
291
- 'description'
292
- );
293
- }
294
-
295
- private get textSnippetsTemplate(): TemplateResult | typeof nothing {
296
- if (!this.hasSnippets) return nothing;
297
-
298
- return html`<text-snippet-block
299
- viewsize="list"
300
- .snippets=${this.model?.snippets}
301
- ></text-snippet-block>`;
302
- }
303
-
304
- private get hasSnippets(): boolean {
305
- return !!this.model?.snippets?.length;
306
- }
307
-
308
- // Utility functions
309
- // eslint-disable-next-line default-param-last
310
- private metadataTemplate(text: any, label = '', id?: string) {
311
- if (!text) return nothing;
312
- return html`
313
- <div id=${ifDefined(id)} class="metadata">
314
- ${this.labelTemplate(label)} ${text}
315
- </div>
316
- `;
317
- }
318
-
319
- private labelTemplate(label: string) {
320
- return html` ${label
321
- ? html`<span class="label">${label}: </span>`
322
- : nothing}`;
323
- }
324
-
325
- private searchLink(field: string, searchTerm: string) {
326
- if (!field || !searchTerm) {
327
- return nothing;
328
- }
329
- const query = encodeURIComponent(`${field}:"${searchTerm}"`);
330
- // No whitespace after closing tag
331
- // Note: single ' for href='' to wrap " in query var gets changed back by yarn format
332
-
333
- // eslint-disable-next-line lit/no-invalid-html
334
- return html`<a href="${this.baseNavigationUrl}/search.php?query=${query}">
335
- ${DOMPurify.sanitize(searchTerm)}</a
336
- >`;
337
- }
338
-
339
- private detailsLink(identifier: string, text?: string): TemplateResult {
340
- const linkText = text ?? identifier;
341
- // No whitespace after closing tag
342
- // identifiers (all ASCII in their creation) should be safe to use in href, but sanitize anyway
343
- return html`<a
344
- href="${this.baseNavigationUrl}/details/${encodeURI(identifier)}"
345
- >${DOMPurify.sanitize(linkText)}</a
346
- >`;
347
- }
348
-
349
- /*
350
- * TODO: fix field names to match model in src/collection-browser.ts
351
- * private get dateSortSelector()
352
- * @see src/models.ts
353
- */
354
- private get date(): Date | undefined {
355
- switch (this.sortParam?.field) {
356
- case 'date':
357
- return this.model?.datePublished;
358
- case 'reviewdate':
359
- return this.model?.dateReviewed;
360
- case 'addeddate':
361
- return this.model?.dateAdded;
362
- default:
363
- return this.model?.dateArchived; // publicdate
364
- }
365
- }
366
-
367
- private get classSize(): string {
368
- if (
369
- this.mobileBreakpoint &&
370
- this.currentWidth &&
371
- this.currentWidth < this.mobileBreakpoint
372
- ) {
373
- return 'mobile';
374
- }
375
- return 'desktop';
376
- }
377
-
378
- private get formatSize(): DateFormat | NumberFormat {
379
- if (
380
- this.mobileBreakpoint &&
381
- this.currentWidth &&
382
- this.currentWidth < this.mobileBreakpoint
383
- ) {
384
- return 'short';
385
- }
386
- return 'long';
387
- }
388
-
389
- static get styles() {
390
- return css`
391
- html {
392
- font-size: unset;
393
- }
394
-
395
- div {
396
- font-size: 14px;
397
- }
398
-
399
- div a {
400
- text-decoration: none;
401
- }
402
-
403
- .label {
404
- font-weight: bold;
405
- }
406
-
407
- #list-line.mobile {
408
- --infiniteScrollerRowGap: 20px;
409
- --infiniteScrollerRowHeight: auto;
410
- }
411
-
412
- #list-line.desktop {
413
- --infiniteScrollerRowGap: 30px;
414
- --infiniteScrollerRowHeight: auto;
415
- }
416
-
417
- /* fields */
418
- #icon-right {
419
- width: 20px;
420
- padding-top: 5px;
421
- --iconHeight: 20px;
422
- --iconWidth: 20px;
423
- --iconTextAlign: right;
424
- margin-top: -8px;
425
- text-align: right;
426
- }
427
-
428
- #title {
429
- color: #4b64ff;
430
- text-decoration: none;
431
- font-size: 22px;
432
- font-weight: bold;
433
- /* align top of text with image */
434
- line-height: 25px;
435
- margin-top: -4px;
436
- padding-bottom: 2px;
437
- flex-grow: 1;
438
- }
439
-
440
- .metadata {
441
- line-height: 20px;
442
- }
443
-
444
- #description,
445
- #creator,
446
- #topics,
447
- #source {
448
- text-align: left;
449
- overflow: hidden;
450
- text-overflow: ellipsis;
451
- -webkit-box-orient: vertical;
452
- display: -webkit-box;
453
- word-break: break-word;
454
- -webkit-line-clamp: 3; /* number of lines to show */
455
- line-clamp: 3;
456
- }
457
-
458
- #icon {
459
- padding-top: 5px;
460
- }
461
-
462
- #description {
463
- padding-top: 10px;
464
- }
465
-
466
- /* Top level container */
467
- #list-line {
468
- display: flex;
469
- }
470
-
471
- #list-line.mobile {
472
- flex-direction: column;
473
- }
474
-
475
- #list-line.desktop {
476
- column-gap: 10px;
477
- }
478
-
479
- #list-line-top {
480
- display: flex;
481
- column-gap: 7px;
482
- }
483
-
484
- #list-line-bottom {
485
- padding-top: 4px;
486
- }
487
-
488
- #list-line-right,
489
- #list-line-top,
490
- #list-line-bottom {
491
- width: 100%;
492
- }
493
-
494
- div a:hover {
495
- text-decoration: underline;
496
- }
497
-
498
- /* Lines containing multiple div as row */
499
- #item-line,
500
- #dates-line,
501
- #views-line,
502
- #title-line {
503
- display: flex;
504
- flex-direction: row;
505
- gap: 10px;
506
- }
507
- `;
508
- }
509
- }
1
+ import {
2
+ css,
3
+ html,
4
+ LitElement,
5
+ nothing,
6
+ PropertyValues,
7
+ TemplateResult,
8
+ } from 'lit';
9
+ import { ifDefined } from 'lit/directives/if-defined.js';
10
+ import { join } from 'lit/directives/join.js';
11
+ import { map } from 'lit/directives/map.js';
12
+ import { customElement, property, state } from 'lit/decorators.js';
13
+ import DOMPurify from 'dompurify';
14
+
15
+ import type { CollectionNameCacheInterface } from '@internetarchive/collection-name-cache';
16
+ import type { SortParam } from '@internetarchive/search-service';
17
+ import type { TileModel } from '../../models';
18
+
19
+ import { dateLabel } from './date-label';
20
+ import { accountLabel } from './account-label';
21
+ import { formatCount, NumberFormat } from '../../utils/format-count';
22
+ import { formatDate, DateFormat } from '../../utils/format-date';
23
+
24
+ import '../image-block';
25
+ import '../mediatype-icon';
26
+
27
+ @customElement('tile-list')
28
+ export class TileList extends LitElement {
29
+ @property({ type: Object }) model?: TileModel;
30
+
31
+ @property({ type: String }) baseNavigationUrl?: string;
32
+
33
+ @property({ type: Object })
34
+ collectionNameCache?: CollectionNameCacheInterface;
35
+
36
+ @property({ type: Number }) currentWidth?: number;
37
+
38
+ @property({ type: Number }) currentHeight?: number;
39
+
40
+ @property({ type: Object }) sortParam: SortParam | null = null;
41
+
42
+ @property({ type: Number }) mobileBreakpoint?: number;
43
+
44
+ @state() private collectionLinks: TemplateResult[] = [];
45
+
46
+ @property({ type: String }) baseImageUrl?: string;
47
+
48
+ @property({ type: Boolean }) loggedIn = false;
49
+
50
+ protected updated(changed: PropertyValues): void {
51
+ if (changed.has('model')) {
52
+ this.fetchCollectionNames();
53
+ }
54
+ }
55
+
56
+ private async fetchCollectionNames() {
57
+ if (
58
+ !this.model?.collections ||
59
+ this.model.collections.length === 0 ||
60
+ !this.collectionNameCache
61
+ ) {
62
+ return;
63
+ }
64
+ // Note: quirk of Lit: need to replace collectionLinks array,
65
+ // otherwise it will not re-render. Can't simply alter the array.
66
+ this.collectionLinks = [];
67
+ const newCollellectionLinks: TemplateResult[] = [];
68
+ const promises: Promise<void>[] = [];
69
+ for (const collection of this.model.collections) {
70
+ promises.push(
71
+ this.collectionNameCache?.collectionNameFor(collection).then(name => {
72
+ newCollellectionLinks.push(
73
+ this.detailsLink(collection, name ?? collection)
74
+ );
75
+ })
76
+ );
77
+ }
78
+ await Promise.all(promises);
79
+ this.collectionLinks = newCollellectionLinks;
80
+ }
81
+
82
+ render() {
83
+ return html`
84
+ <div id="list-line" class="${this.classSize}">
85
+ ${this.classSize === 'mobile'
86
+ ? this.mobileTemplate
87
+ : this.desktopTemplate}
88
+ </div>
89
+ `;
90
+ }
91
+
92
+ private get mobileTemplate() {
93
+ return html`
94
+ <div id="list-line-top">
95
+ <div id="list-line-left">${this.imageBlockTemplate}</div>
96
+ <div id="list-line-right">
97
+ <div id="title-line">
98
+ <div id="title">${this.titleTemplate}</div>
99
+ ${this.iconRightTemplate}
100
+ </div>
101
+ </div>
102
+ </div>
103
+ <div id="list-line-bottom">${this.detailsTemplate}</div>
104
+ `;
105
+ }
106
+
107
+ private get desktopTemplate() {
108
+ return html`
109
+ <div id="list-line-left">${this.imageBlockTemplate}</div>
110
+ <div id="list-line-right">
111
+ <div id="title-line">
112
+ <div id="title">${this.titleTemplate}</div>
113
+ ${this.iconRightTemplate}
114
+ </div>
115
+ ${this.detailsTemplate}
116
+ </div>
117
+ `;
118
+ }
119
+
120
+ private get imageBlockTemplate() {
121
+ return html`
122
+ <image-block
123
+ .model=${this.model}
124
+ .baseImageUrl=${this.baseImageUrl}
125
+ .isCompactTile=${false}
126
+ .isListTile=${true}
127
+ .viewSize=${this.classSize}
128
+ >
129
+ </image-block>
130
+ `;
131
+ }
132
+
133
+ private get detailsTemplate() {
134
+ return html`
135
+ ${this.itemLineTemplate} ${this.creatorTemplate}
136
+ <div id="dates-line">
137
+ ${this.datePublishedTemplate} ${this.dateSortByTemplate}
138
+ </div>
139
+ <div id="views-line">
140
+ ${this.viewsTemplate} ${this.ratingTemplate} ${this.reviewsTemplate}
141
+ </div>
142
+ ${this.topicsTemplate} ${this.collectionsTemplate}
143
+ ${this.descriptionTemplate} ${this.textSnippetsTemplate}
144
+ `;
145
+ }
146
+
147
+ // Data templates
148
+ private get iconRightTemplate() {
149
+ return html`
150
+ <div id="icon-right">
151
+ <mediatype-icon
152
+ .mediatype=${this.model?.mediatype}
153
+ .collections=${this.model?.collections}
154
+ >
155
+ </mediatype-icon>
156
+ </div>
157
+ `;
158
+ }
159
+
160
+ private get titleTemplate() {
161
+ if (!this.model?.title) {
162
+ return nothing;
163
+ }
164
+ return html` ${this.detailsLink(this.model.identifier, this.model.title)} `;
165
+ }
166
+
167
+ private get itemLineTemplate() {
168
+ const source = this.sourceTemplate;
169
+ const volume = this.volumeTemplate;
170
+ const issue = this.issueTemplate;
171
+ if (!source && !volume && !issue) {
172
+ return nothing;
173
+ }
174
+ return html` <div id="item-line">${source} ${volume} ${issue}</div> `;
175
+ }
176
+
177
+ private get sourceTemplate() {
178
+ if (!this.model?.source) {
179
+ return nothing;
180
+ }
181
+ return html`
182
+ <div id="source" class="metadata">
183
+ ${this.labelTemplate('Source')}
184
+ ${this.searchLink('source', this.model.source)}
185
+ </div>
186
+ `;
187
+ }
188
+
189
+ private get volumeTemplate() {
190
+ return this.metadataTemplate(this.model?.volume, 'Volume');
191
+ }
192
+
193
+ private get issueTemplate() {
194
+ return this.metadataTemplate(this.model?.issue, 'Issue');
195
+ }
196
+
197
+ private get creatorTemplate() {
198
+ // "Achivist since" if account
199
+ if (this.model?.mediatype === 'account') {
200
+ return html`
201
+ <div id="creator" class="metadata">
202
+ <span class="label"> ${accountLabel(this.model?.dateAdded)} </span>
203
+ </div>
204
+ `;
205
+ }
206
+ // "Creator" if not account tile
207
+ if (!this.model?.creators || this.model.creators.length === 0) {
208
+ return nothing;
209
+ }
210
+ return html`
211
+ <div id="creator" class="metadata">
212
+ ${this.labelTemplate('By')}
213
+ ${join(
214
+ map(this.model.creators, id => this.searchLink('creator', id)),
215
+ html`, `
216
+ )}
217
+ </div>
218
+ `;
219
+ }
220
+
221
+ private get datePublishedTemplate() {
222
+ return this.metadataTemplate(
223
+ formatDate(this.model?.datePublished, 'long'),
224
+ 'Published'
225
+ );
226
+ }
227
+
228
+ // Show date label/value when sorted by date type
229
+ // Except datePublished which is always shown
230
+ private get dateSortByTemplate() {
231
+ if (
232
+ this.sortParam &&
233
+ (this.sortParam.field === 'addeddate' ||
234
+ this.sortParam.field === 'reviewdate' ||
235
+ this.sortParam.field === 'publicdate')
236
+ ) {
237
+ return this.metadataTemplate(
238
+ formatDate(this.date, 'long'),
239
+ dateLabel(this.sortParam.field)
240
+ );
241
+ }
242
+ return nothing;
243
+ }
244
+
245
+ private get viewsTemplate() {
246
+ return this.metadataTemplate(
247
+ `${formatCount(this.model?.viewCount ?? 0, this.formatSize)}`,
248
+ 'Views'
249
+ );
250
+ }
251
+
252
+ private get ratingTemplate() {
253
+ return this.metadataTemplate(this.model?.averageRating, 'Avg Rating');
254
+ }
255
+
256
+ private get reviewsTemplate() {
257
+ return this.metadataTemplate(this.model?.commentCount, 'Reviews');
258
+ }
259
+
260
+ private get topicsTemplate() {
261
+ if (!this.model?.subjects || this.model.subjects.length === 0) {
262
+ return nothing;
263
+ }
264
+ return html`
265
+ <div id="topics" class="metadata">
266
+ ${this.labelTemplate('Topics')}
267
+ ${join(
268
+ map(this.model.subjects, id => this.searchLink('subject', id)),
269
+ html`, `
270
+ )}
271
+ </div>
272
+ `;
273
+ }
274
+
275
+ private get collectionsTemplate() {
276
+ if (!this.collectionLinks || this.collectionLinks.length === 0) {
277
+ return nothing;
278
+ }
279
+ return html`
280
+ <div id="collections" class="metadata">
281
+ ${this.labelTemplate('Collections')}
282
+ ${join(this.collectionLinks, html`, `)}
283
+ </div>
284
+ `;
285
+ }
286
+
287
+ private get descriptionTemplate() {
288
+ return this.metadataTemplate(
289
+ DOMPurify.sanitize(this.model?.description ?? ''),
290
+ '',
291
+ 'description'
292
+ );
293
+ }
294
+
295
+ private get textSnippetsTemplate(): TemplateResult | typeof nothing {
296
+ if (!this.hasSnippets) return nothing;
297
+
298
+ return html`<text-snippet-block
299
+ viewsize="list"
300
+ .snippets=${this.model?.snippets}
301
+ ></text-snippet-block>`;
302
+ }
303
+
304
+ private get hasSnippets(): boolean {
305
+ return !!this.model?.snippets?.length;
306
+ }
307
+
308
+ // Utility functions
309
+ // eslint-disable-next-line default-param-last
310
+ private metadataTemplate(text: any, label = '', id?: string) {
311
+ if (!text) return nothing;
312
+ return html`
313
+ <div id=${ifDefined(id)} class="metadata">
314
+ ${this.labelTemplate(label)} ${text}
315
+ </div>
316
+ `;
317
+ }
318
+
319
+ private labelTemplate(label: string) {
320
+ return html` ${label
321
+ ? html`<span class="label">${label}: </span>`
322
+ : nothing}`;
323
+ }
324
+
325
+ private searchLink(field: string, searchTerm: string) {
326
+ if (!field || !searchTerm) {
327
+ return nothing;
328
+ }
329
+ const query = encodeURIComponent(`${field}:"${searchTerm}"`);
330
+ // No whitespace after closing tag
331
+ // Note: single ' for href='' to wrap " in query var gets changed back by yarn format
332
+
333
+ // eslint-disable-next-line lit/no-invalid-html
334
+ return html`<a href="${this.baseNavigationUrl}/search.php?query=${query}">
335
+ ${DOMPurify.sanitize(searchTerm)}</a
336
+ >`;
337
+ }
338
+
339
+ private detailsLink(identifier: string, text?: string): TemplateResult {
340
+ const linkText = text ?? identifier;
341
+ // No whitespace after closing tag
342
+ // identifiers (all ASCII in their creation) should be safe to use in href, but sanitize anyway
343
+ return html`<a
344
+ href="${this.baseNavigationUrl}/details/${encodeURI(identifier)}"
345
+ >${DOMPurify.sanitize(linkText)}</a
346
+ >`;
347
+ }
348
+
349
+ /*
350
+ * TODO: fix field names to match model in src/collection-browser.ts
351
+ * private get dateSortSelector()
352
+ * @see src/models.ts
353
+ */
354
+ private get date(): Date | undefined {
355
+ switch (this.sortParam?.field) {
356
+ case 'date':
357
+ return this.model?.datePublished;
358
+ case 'reviewdate':
359
+ return this.model?.dateReviewed;
360
+ case 'addeddate':
361
+ return this.model?.dateAdded;
362
+ default:
363
+ return this.model?.dateArchived; // publicdate
364
+ }
365
+ }
366
+
367
+ private get classSize(): string {
368
+ if (
369
+ this.mobileBreakpoint &&
370
+ this.currentWidth &&
371
+ this.currentWidth < this.mobileBreakpoint
372
+ ) {
373
+ return 'mobile';
374
+ }
375
+ return 'desktop';
376
+ }
377
+
378
+ private get formatSize(): DateFormat | NumberFormat {
379
+ if (
380
+ this.mobileBreakpoint &&
381
+ this.currentWidth &&
382
+ this.currentWidth < this.mobileBreakpoint
383
+ ) {
384
+ return 'short';
385
+ }
386
+ return 'long';
387
+ }
388
+
389
+ static get styles() {
390
+ return css`
391
+ html {
392
+ font-size: unset;
393
+ }
394
+
395
+ div {
396
+ font-size: 14px;
397
+ }
398
+
399
+ div a {
400
+ text-decoration: none;
401
+ }
402
+
403
+ .label {
404
+ font-weight: bold;
405
+ }
406
+
407
+ #list-line.mobile {
408
+ --infiniteScrollerRowGap: 20px;
409
+ --infiniteScrollerRowHeight: auto;
410
+ }
411
+
412
+ #list-line.desktop {
413
+ --infiniteScrollerRowGap: 30px;
414
+ --infiniteScrollerRowHeight: auto;
415
+ }
416
+
417
+ /* fields */
418
+ #icon-right {
419
+ width: 20px;
420
+ padding-top: 5px;
421
+ --iconHeight: 20px;
422
+ --iconWidth: 20px;
423
+ --iconTextAlign: right;
424
+ margin-top: -8px;
425
+ text-align: right;
426
+ }
427
+
428
+ #title {
429
+ color: #4b64ff;
430
+ text-decoration: none;
431
+ font-size: 22px;
432
+ font-weight: bold;
433
+ /* align top of text with image */
434
+ line-height: 25px;
435
+ margin-top: -4px;
436
+ padding-bottom: 2px;
437
+ flex-grow: 1;
438
+ }
439
+
440
+ .metadata {
441
+ line-height: 20px;
442
+ }
443
+
444
+ #description,
445
+ #creator,
446
+ #topics,
447
+ #source {
448
+ text-align: left;
449
+ overflow: hidden;
450
+ text-overflow: ellipsis;
451
+ -webkit-box-orient: vertical;
452
+ display: -webkit-box;
453
+ word-break: break-word;
454
+ -webkit-line-clamp: 3; /* number of lines to show */
455
+ line-clamp: 3;
456
+ }
457
+
458
+ #icon {
459
+ padding-top: 5px;
460
+ }
461
+
462
+ #description {
463
+ padding-top: 10px;
464
+ }
465
+
466
+ /* Top level container */
467
+ #list-line {
468
+ display: flex;
469
+ }
470
+
471
+ #list-line.mobile {
472
+ flex-direction: column;
473
+ }
474
+
475
+ #list-line.desktop {
476
+ column-gap: 10px;
477
+ }
478
+
479
+ #list-line-top {
480
+ display: flex;
481
+ column-gap: 7px;
482
+ }
483
+
484
+ #list-line-bottom {
485
+ padding-top: 4px;
486
+ }
487
+
488
+ #list-line-right,
489
+ #list-line-top,
490
+ #list-line-bottom {
491
+ width: 100%;
492
+ }
493
+
494
+ div a:hover {
495
+ text-decoration: underline;
496
+ }
497
+
498
+ /* Lines containing multiple div as row */
499
+ #item-line,
500
+ #dates-line,
501
+ #views-line,
502
+ #title-line {
503
+ display: flex;
504
+ flex-direction: row;
505
+ gap: 10px;
506
+ }
507
+ `;
508
+ }
509
+ }