@internetarchive/collection-browser 1.13.0-alpha2 → 1.14.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 (287) 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 +64 -54
  12. package/dist/src/app-root.js +320 -293
  13. package/dist/src/app-root.js.map +1 -1
  14. package/dist/src/assets/img/icons/arrow-left.d.ts +2 -2
  15. package/dist/src/assets/img/icons/arrow-left.js +2 -2
  16. package/dist/src/assets/img/icons/arrow-right.d.ts +2 -2
  17. package/dist/src/assets/img/icons/arrow-right.js +2 -2
  18. package/dist/src/assets/img/icons/chevron.d.ts +2 -2
  19. package/dist/src/assets/img/icons/chevron.js +2 -2
  20. package/dist/src/assets/img/icons/contract.d.ts +2 -2
  21. package/dist/src/assets/img/icons/contract.js +2 -2
  22. package/dist/src/assets/img/icons/empty-query.d.ts +2 -2
  23. package/dist/src/assets/img/icons/empty-query.js +2 -2
  24. package/dist/src/assets/img/icons/expand.d.ts +2 -2
  25. package/dist/src/assets/img/icons/expand.js +2 -2
  26. package/dist/src/assets/img/icons/eye-closed.d.ts +2 -2
  27. package/dist/src/assets/img/icons/eye-closed.js +2 -2
  28. package/dist/src/assets/img/icons/eye.d.ts +2 -2
  29. package/dist/src/assets/img/icons/eye.js +2 -2
  30. package/dist/src/assets/img/icons/favorite-filled.d.ts +1 -1
  31. package/dist/src/assets/img/icons/favorite-filled.js +2 -2
  32. package/dist/src/assets/img/icons/login-required.d.ts +1 -1
  33. package/dist/src/assets/img/icons/login-required.js +2 -2
  34. package/dist/src/assets/img/icons/mediatype/account.d.ts +1 -1
  35. package/dist/src/assets/img/icons/mediatype/account.js +2 -2
  36. package/dist/src/assets/img/icons/mediatype/audio.d.ts +1 -1
  37. package/dist/src/assets/img/icons/mediatype/audio.js +2 -2
  38. package/dist/src/assets/img/icons/mediatype/collection.d.ts +1 -1
  39. package/dist/src/assets/img/icons/mediatype/collection.js +2 -2
  40. package/dist/src/assets/img/icons/mediatype/data.d.ts +1 -1
  41. package/dist/src/assets/img/icons/mediatype/data.js +2 -2
  42. package/dist/src/assets/img/icons/mediatype/etree.d.ts +1 -1
  43. package/dist/src/assets/img/icons/mediatype/etree.js +2 -2
  44. package/dist/src/assets/img/icons/mediatype/film.d.ts +1 -1
  45. package/dist/src/assets/img/icons/mediatype/film.js +2 -2
  46. package/dist/src/assets/img/icons/mediatype/images.d.ts +1 -1
  47. package/dist/src/assets/img/icons/mediatype/images.js +2 -2
  48. package/dist/src/assets/img/icons/mediatype/radio.d.ts +1 -1
  49. package/dist/src/assets/img/icons/mediatype/radio.js +2 -2
  50. package/dist/src/assets/img/icons/mediatype/software.d.ts +1 -1
  51. package/dist/src/assets/img/icons/mediatype/software.js +2 -2
  52. package/dist/src/assets/img/icons/mediatype/texts.d.ts +1 -1
  53. package/dist/src/assets/img/icons/mediatype/texts.js +2 -2
  54. package/dist/src/assets/img/icons/mediatype/tv.d.ts +1 -1
  55. package/dist/src/assets/img/icons/mediatype/tv.js +2 -2
  56. package/dist/src/assets/img/icons/mediatype/video.d.ts +1 -1
  57. package/dist/src/assets/img/icons/mediatype/video.js +2 -2
  58. package/dist/src/assets/img/icons/mediatype/web.d.ts +1 -1
  59. package/dist/src/assets/img/icons/mediatype/web.js +2 -2
  60. package/dist/src/assets/img/icons/null-result.d.ts +2 -2
  61. package/dist/src/assets/img/icons/null-result.js +2 -2
  62. package/dist/src/assets/img/icons/restricted.d.ts +1 -1
  63. package/dist/src/assets/img/icons/restricted.js +2 -2
  64. package/dist/src/assets/img/icons/reviews.d.ts +1 -1
  65. package/dist/src/assets/img/icons/reviews.js +2 -2
  66. package/dist/src/assets/img/icons/upload.d.ts +1 -1
  67. package/dist/src/assets/img/icons/upload.js +2 -2
  68. package/dist/src/assets/img/icons/views.d.ts +1 -1
  69. package/dist/src/assets/img/icons/views.js +2 -2
  70. package/dist/src/circular-activity-indicator.d.ts +5 -5
  71. package/dist/src/circular-activity-indicator.js +17 -17
  72. package/dist/src/collection-browser.d.ts +522 -473
  73. package/dist/src/collection-browser.js +1846 -1697
  74. package/dist/src/collection-browser.js.map +1 -1
  75. package/dist/src/collection-facets/facet-tombstone-row.d.ts +5 -5
  76. package/dist/src/collection-facets/facet-tombstone-row.js +15 -15
  77. package/dist/src/collection-facets/facets-template.d.ts +20 -20
  78. package/dist/src/collection-facets/facets-template.js +152 -152
  79. package/dist/src/collection-facets/more-facets-content.d.ts +77 -77
  80. package/dist/src/collection-facets/more-facets-content.js +359 -359
  81. package/dist/src/collection-facets/more-facets-pagination.d.ts +36 -36
  82. package/dist/src/collection-facets/more-facets-pagination.js +196 -196
  83. package/dist/src/collection-facets/toggle-switch.d.ts +41 -41
  84. package/dist/src/collection-facets/toggle-switch.js +94 -94
  85. package/dist/src/collection-facets.d.ts +104 -103
  86. package/dist/src/collection-facets.js +511 -498
  87. package/dist/src/collection-facets.js.map +1 -1
  88. package/dist/src/empty-placeholder.d.ts +23 -23
  89. package/dist/src/empty-placeholder.js +74 -74
  90. package/dist/src/expanded-date-picker.d.ts +43 -43
  91. package/dist/src/expanded-date-picker.js +109 -109
  92. package/dist/src/language-code-handler/language-code-handler.d.ts +37 -37
  93. package/dist/src/language-code-handler/language-code-handler.js +26 -26
  94. package/dist/src/language-code-handler/language-code-mapping.d.ts +1 -1
  95. package/dist/src/language-code-handler/language-code-mapping.js +562 -562
  96. package/dist/src/manage/manage-bar.d.ts +26 -0
  97. package/dist/src/manage/manage-bar.js +147 -0
  98. package/dist/src/manage/manage-bar.js.map +1 -0
  99. package/dist/src/mediatype/mediatype-config.d.ts +3 -3
  100. package/dist/src/mediatype/mediatype-config.js +85 -85
  101. package/dist/src/models.d.ts +164 -163
  102. package/dist/src/models.js +269 -269
  103. package/dist/src/models.js.map +1 -1
  104. package/dist/src/restoration-state-handler.d.ts +70 -70
  105. package/dist/src/restoration-state-handler.js +355 -355
  106. package/dist/src/sort-filter-bar/alpha-bar-tooltip.d.ts +6 -6
  107. package/dist/src/sort-filter-bar/alpha-bar-tooltip.js +24 -24
  108. package/dist/src/sort-filter-bar/alpha-bar.d.ts +21 -21
  109. package/dist/src/sort-filter-bar/alpha-bar.js +128 -128
  110. package/dist/src/sort-filter-bar/img/compact.d.ts +1 -1
  111. package/dist/src/sort-filter-bar/img/compact.js +2 -2
  112. package/dist/src/sort-filter-bar/img/list.d.ts +1 -1
  113. package/dist/src/sort-filter-bar/img/list.js +2 -2
  114. package/dist/src/sort-filter-bar/img/sort-toggle-disabled.d.ts +1 -1
  115. package/dist/src/sort-filter-bar/img/sort-toggle-disabled.js +2 -2
  116. package/dist/src/sort-filter-bar/img/sort-toggle-down.d.ts +1 -1
  117. package/dist/src/sort-filter-bar/img/sort-toggle-down.js +2 -2
  118. package/dist/src/sort-filter-bar/img/sort-toggle-up.d.ts +1 -1
  119. package/dist/src/sort-filter-bar/img/sort-toggle-up.js +2 -2
  120. package/dist/src/sort-filter-bar/img/sort-triangle.d.ts +1 -1
  121. package/dist/src/sort-filter-bar/img/sort-triangle.js +2 -2
  122. package/dist/src/sort-filter-bar/img/tile.d.ts +1 -1
  123. package/dist/src/sort-filter-bar/img/tile.js +2 -2
  124. package/dist/src/sort-filter-bar/sort-filter-bar.d.ts +208 -208
  125. package/dist/src/sort-filter-bar/sort-filter-bar.js +637 -637
  126. package/dist/src/styles/item-image-styles.d.ts +8 -8
  127. package/dist/src/styles/item-image-styles.js +9 -9
  128. package/dist/src/styles/sr-only.d.ts +1 -1
  129. package/dist/src/styles/sr-only.js +2 -2
  130. package/dist/src/tiles/base-tile-component.d.ts +18 -18
  131. package/dist/src/tiles/base-tile-component.js +59 -59
  132. package/dist/src/tiles/collection-browser-loading-tile.d.ts +5 -5
  133. package/dist/src/tiles/collection-browser-loading-tile.js +15 -15
  134. package/dist/src/tiles/grid/account-tile.d.ts +18 -18
  135. package/dist/src/tiles/grid/account-tile.js +72 -72
  136. package/dist/src/tiles/grid/collection-tile.d.ts +15 -15
  137. package/dist/src/tiles/grid/collection-tile.js +80 -80
  138. package/dist/src/tiles/grid/item-tile.d.ts +27 -27
  139. package/dist/src/tiles/grid/item-tile.js +134 -134
  140. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.d.ts +1 -1
  141. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js +8 -8
  142. package/dist/src/tiles/grid/tile-stats.d.ts +11 -11
  143. package/dist/src/tiles/grid/tile-stats.js +48 -48
  144. package/dist/src/tiles/hover/hover-pane-controller.d.ts +219 -219
  145. package/dist/src/tiles/hover/hover-pane-controller.js +352 -352
  146. package/dist/src/tiles/hover/tile-hover-pane.d.ts +15 -15
  147. package/dist/src/tiles/hover/tile-hover-pane.js +38 -38
  148. package/dist/src/tiles/image-block.d.ts +17 -17
  149. package/dist/src/tiles/image-block.js +72 -72
  150. package/dist/src/tiles/item-image.d.ts +35 -35
  151. package/dist/src/tiles/item-image.js +117 -117
  152. package/dist/src/tiles/list/tile-list-compact-header.d.ts +6 -6
  153. package/dist/src/tiles/list/tile-list-compact-header.js +38 -38
  154. package/dist/src/tiles/list/tile-list-compact.d.ts +15 -15
  155. package/dist/src/tiles/list/tile-list-compact.js +114 -114
  156. package/dist/src/tiles/list/tile-list.d.ts +46 -46
  157. package/dist/src/tiles/list/tile-list.js +298 -298
  158. package/dist/src/tiles/mediatype-icon.d.ts +9 -9
  159. package/dist/src/tiles/mediatype-icon.js +47 -47
  160. package/dist/src/tiles/overlay/icon-overlay.d.ts +10 -10
  161. package/dist/src/tiles/overlay/icon-overlay.js +40 -40
  162. package/dist/src/tiles/overlay/icon-text-overlay.d.ts +9 -9
  163. package/dist/src/tiles/overlay/icon-text-overlay.js +38 -38
  164. package/dist/src/tiles/overlay/text-overlay.d.ts +10 -10
  165. package/dist/src/tiles/overlay/text-overlay.js +42 -42
  166. package/dist/src/tiles/text-snippet-block.d.ts +27 -27
  167. package/dist/src/tiles/text-snippet-block.js +73 -73
  168. package/dist/src/tiles/tile-dispatcher.d.ts +63 -50
  169. package/dist/src/tiles/tile-dispatcher.js +255 -187
  170. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  171. package/dist/src/tiles/tile-display-value-provider.d.ts +43 -43
  172. package/dist/src/tiles/tile-display-value-provider.js +80 -80
  173. package/dist/src/utils/analytics-events.d.ts +25 -25
  174. package/dist/src/utils/analytics-events.js +27 -27
  175. package/dist/src/utils/array-equals.d.ts +4 -4
  176. package/dist/src/utils/array-equals.js +10 -10
  177. package/dist/src/utils/format-count.d.ts +7 -7
  178. package/dist/src/utils/format-count.js +76 -76
  179. package/dist/src/utils/format-date.d.ts +2 -2
  180. package/dist/src/utils/format-date.js +25 -25
  181. package/dist/src/utils/format-unit-size.d.ts +2 -2
  182. package/dist/src/utils/format-unit-size.js +33 -33
  183. package/dist/src/utils/local-date-from-utc.d.ts +9 -9
  184. package/dist/src/utils/local-date-from-utc.js +15 -15
  185. package/dist/src/utils/sha1.d.ts +2 -2
  186. package/dist/src/utils/sha1.js +8 -8
  187. package/dist/test/collection-browser.test.d.ts +1 -1
  188. package/dist/test/collection-browser.test.js +1122 -979
  189. package/dist/test/collection-browser.test.js.map +1 -1
  190. package/dist/test/collection-facets/facets-template.test.d.ts +1 -1
  191. package/dist/test/collection-facets/facets-template.test.js +134 -134
  192. package/dist/test/collection-facets/more-facets-content.test.d.ts +1 -1
  193. package/dist/test/collection-facets/more-facets-content.test.js +133 -133
  194. package/dist/test/collection-facets/more-facets-pagination.test.d.ts +1 -1
  195. package/dist/test/collection-facets/more-facets-pagination.test.js +117 -117
  196. package/dist/test/collection-facets/toggle-switch.test.d.ts +1 -1
  197. package/dist/test/collection-facets/toggle-switch.test.js +73 -73
  198. package/dist/test/collection-facets.test.d.ts +2 -2
  199. package/dist/test/collection-facets.test.js +682 -682
  200. package/dist/test/empty-placeholder.test.d.ts +1 -1
  201. package/dist/test/empty-placeholder.test.js +63 -63
  202. package/dist/test/expanded-date-picker.test.d.ts +1 -1
  203. package/dist/test/expanded-date-picker.test.js +95 -95
  204. package/dist/test/icon-overlay.test.d.ts +1 -1
  205. package/dist/test/icon-overlay.test.js +24 -24
  206. package/dist/test/image-block.test.d.ts +1 -1
  207. package/dist/test/image-block.test.js +48 -48
  208. package/dist/test/item-image.test.d.ts +1 -1
  209. package/dist/test/item-image.test.js +86 -84
  210. package/dist/test/item-image.test.js.map +1 -1
  211. package/dist/test/manage/manage-bar.test.d.ts +1 -0
  212. package/dist/test/manage/manage-bar.test.js +73 -0
  213. package/dist/test/manage/manage-bar.test.js.map +1 -0
  214. package/dist/test/mediatype-config.test.d.ts +1 -1
  215. package/dist/test/mediatype-config.test.js +16 -16
  216. package/dist/test/mocks/mock-analytics-handler.d.ts +10 -10
  217. package/dist/test/mocks/mock-analytics-handler.js +15 -15
  218. package/dist/test/mocks/mock-collection-name-cache.d.ts +9 -9
  219. package/dist/test/mocks/mock-collection-name-cache.js +17 -17
  220. package/dist/test/mocks/mock-search-responses.d.ts +21 -21
  221. package/dist/test/mocks/mock-search-responses.js +709 -707
  222. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  223. package/dist/test/mocks/mock-search-service.d.ts +15 -15
  224. package/dist/test/mocks/mock-search-service.js +50 -50
  225. package/dist/test/restoration-state-handler.test.d.ts +1 -1
  226. package/dist/test/restoration-state-handler.test.js +270 -270
  227. package/dist/test/sort-filter-bar/alpha-bar-tooltip.test.d.ts +1 -1
  228. package/dist/test/sort-filter-bar/alpha-bar-tooltip.test.js +12 -12
  229. package/dist/test/sort-filter-bar/alpha-bar.test.d.ts +1 -1
  230. package/dist/test/sort-filter-bar/alpha-bar.test.js +73 -73
  231. package/dist/test/sort-filter-bar/sort-filter-bar.test.d.ts +1 -1
  232. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +378 -378
  233. package/dist/test/text-overlay.test.d.ts +1 -1
  234. package/dist/test/text-overlay.test.js +48 -48
  235. package/dist/test/text-snippet-block.test.d.ts +1 -1
  236. package/dist/test/text-snippet-block.test.js +57 -57
  237. package/dist/test/tile-stats.test.d.ts +1 -1
  238. package/dist/test/tile-stats.test.js +33 -33
  239. package/dist/test/tiles/grid/account-tile.test.d.ts +1 -1
  240. package/dist/test/tiles/grid/account-tile.test.js +76 -76
  241. package/dist/test/tiles/grid/collection-tile.test.d.ts +1 -1
  242. package/dist/test/tiles/grid/collection-tile.test.js +73 -73
  243. package/dist/test/tiles/grid/item-tile.test.d.ts +1 -1
  244. package/dist/test/tiles/grid/item-tile.test.js +254 -254
  245. package/dist/test/tiles/hover/hover-pane-controller.test.d.ts +1 -1
  246. package/dist/test/tiles/hover/hover-pane-controller.test.js +258 -257
  247. package/dist/test/tiles/hover/hover-pane-controller.test.js.map +1 -1
  248. package/dist/test/tiles/hover/tile-hover-pane.test.d.ts +1 -1
  249. package/dist/test/tiles/hover/tile-hover-pane.test.js +13 -13
  250. package/dist/test/tiles/list/tile-list-compact.test.d.ts +1 -1
  251. package/dist/test/tiles/list/tile-list-compact.test.js +143 -143
  252. package/dist/test/tiles/list/tile-list.test.d.ts +1 -1
  253. package/dist/test/tiles/list/tile-list.test.js +242 -242
  254. package/dist/test/tiles/tile-dispatcher.test.d.ts +1 -1
  255. package/dist/test/tiles/tile-dispatcher.test.js +107 -67
  256. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -1
  257. package/dist/test/tiles/tile-display-value-provider.test.d.ts +1 -1
  258. package/dist/test/tiles/tile-display-value-provider.test.js +141 -141
  259. package/dist/test/utils/array-equals.test.d.ts +1 -1
  260. package/dist/test/utils/array-equals.test.js +26 -26
  261. package/dist/test/utils/format-count.test.d.ts +1 -1
  262. package/dist/test/utils/format-count.test.js +23 -23
  263. package/dist/test/utils/format-date.test.d.ts +1 -1
  264. package/dist/test/utils/format-date.test.js +17 -17
  265. package/dist/test/utils/format-unit-size.test.d.ts +1 -1
  266. package/dist/test/utils/format-unit-size.test.js +17 -17
  267. package/dist/test/utils/local-date-from-utc.test.d.ts +1 -1
  268. package/dist/test/utils/local-date-from-utc.test.js +26 -26
  269. package/local.archive.org.cert +86 -86
  270. package/local.archive.org.key +27 -27
  271. package/package.json +2 -2
  272. package/renovate.json +6 -6
  273. package/src/app-root.ts +29 -0
  274. package/src/collection-browser.ts +197 -23
  275. package/src/collection-facets.ts +13 -1
  276. package/src/manage/manage-bar.ts +151 -0
  277. package/src/models.ts +1 -0
  278. package/src/tiles/tile-dispatcher.ts +71 -5
  279. package/test/collection-browser.test.ts +198 -1
  280. package/test/item-image.test.ts +2 -0
  281. package/test/manage/manage-bar.test.ts +107 -0
  282. package/test/mocks/mock-search-responses.ts +2 -0
  283. package/test/tiles/hover/hover-pane-controller.test.ts +1 -0
  284. package/test/tiles/tile-dispatcher.test.ts +52 -0
  285. package/tsconfig.json +21 -21
  286. package/web-dev-server.config.mjs +30 -30
  287. package/web-test-runner.config.mjs +41 -41
@@ -41,9 +41,9 @@ import type { RecaptchaManagerInterface } from '@internetarchive/recaptcha-manag
41
41
  import './tiles/tile-dispatcher';
42
42
  import './tiles/collection-browser-loading-tile';
43
43
  import './sort-filter-bar/sort-filter-bar';
44
+ import './manage/manage-bar';
44
45
  import './collection-facets';
45
46
  import './circular-activity-indicator';
46
- import './sort-filter-bar/sort-filter-bar';
47
47
  import {
48
48
  SelectedFacets,
49
49
  SortField,
@@ -75,6 +75,8 @@ import {
75
75
  import { srOnlyStyle } from './styles/sr-only';
76
76
  import { sha1 } from './utils/sha1';
77
77
  import type { CollectionFacets } from './collection-facets';
78
+ import type { ManageableItem } from './manage/manage-bar';
79
+ import { formatDate } from './utils/format-date';
78
80
 
79
81
  type RequestKind = 'full' | 'hits' | 'aggregations';
80
82
 
@@ -241,8 +243,9 @@ export class CollectionBrowser
241
243
  private placeholderCellTemplate = html`<collection-browser-loading-tile></collection-browser-loading-tile>`;
242
244
 
243
245
  private tileModelAtCellIndex(index: number): TileModel | undefined {
244
- const pageNumber = Math.floor(index / this.pageSize) + 1;
245
- const itemIndex = index % this.pageSize;
246
+ const offsetIndex = index + this.tileModelOffset;
247
+ const pageNumber = Math.floor(offsetIndex / this.pageSize) + 1;
248
+ const itemIndex = offsetIndex % this.pageSize;
246
249
  const model = this.dataSource[pageNumber]?.[itemIndex];
247
250
  /**
248
251
  * If we encounter a model we don't have yet and we're not in the middle of an
@@ -279,6 +282,11 @@ export class CollectionBrowser
279
282
  */
280
283
  private dataSource: Record<string, TileModel[]> = {};
281
284
 
285
+ /**
286
+ * How many tiles to offset the data source by, to account for any removed tiles.
287
+ */
288
+ private tileModelOffset = 0;
289
+
282
290
  @query('infinite-scroller')
283
291
  private infiniteScroller!: InfiniteScroller;
284
292
 
@@ -531,7 +539,9 @@ export class CollectionBrowser
531
539
  private get rightColumnTemplate(): TemplateResult {
532
540
  return html`
533
541
  <div id="right-column" class="column">
534
- ${this.sortFilterBarTemplate}
542
+ ${this.isManageView
543
+ ? this.manageBarTemplate
544
+ : this.sortFilterBarTemplate}
535
545
  ${this.displayMode === `list-compact`
536
546
  ? this.listHeaderTemplate
537
547
  : nothing}
@@ -582,6 +592,153 @@ export class CollectionBrowser
582
592
  `;
583
593
  }
584
594
 
595
+ private get manageBarTemplate(): TemplateResult {
596
+ return html`
597
+ <manage-bar
598
+ showSelectAll
599
+ showUnselectAll
600
+ @removeItems=${this.handleRemoveItems}
601
+ @selectAll=${this.checkAllTiles}
602
+ @unselectAll=${this.uncheckAllTiles}
603
+ @cancel=${() => {
604
+ this.isManageView = false;
605
+ this.uncheckAllTiles();
606
+ }}
607
+ ></manage-bar>
608
+ `;
609
+ }
610
+
611
+ /**
612
+ * Handler for when the user requests to remove all checked items via the manage bar.
613
+ * Emits an `itemRemovalRequested` event with all checked tile models.
614
+ */
615
+ private handleRemoveItems(): void {
616
+ this.dispatchEvent(
617
+ new CustomEvent<{ items: ManageableItem[] }>('itemRemovalRequested', {
618
+ detail: {
619
+ items: this.checkedTileModels.map(model => ({
620
+ ...model,
621
+ date: formatDate(model.datePublished, 'long'),
622
+ })),
623
+ },
624
+ })
625
+ );
626
+ }
627
+
628
+ /**
629
+ * Removes all tile models that are currently checked & adjusts the paging
630
+ * of the data source to account for any new gaps in the data.
631
+ */
632
+ removeCheckedTiles(): void {
633
+ // To make sure our data source remains page-aligned, we will offset our data source by
634
+ // the number of removed tiles, so that we can just add the offset when the infinite
635
+ // scroller queries for cell contents.
636
+ // This only matters while we're still viewing the same set of results. If the user changes
637
+ // their query/filters/sort, then the data source is overwritten and the offset cleared.
638
+ const { checkedTileModels, uncheckedTileModels } = this;
639
+ const numChecked = checkedTileModels.length;
640
+ if (numChecked === 0) return;
641
+ this.tileModelOffset += numChecked;
642
+
643
+ const newDataSource: typeof this.dataSource = {};
644
+
645
+ // Which page the remaining tile models start on, post-offset
646
+ let offsetPageNumber = Math.floor(this.tileModelOffset / this.pageSize) + 1;
647
+ let indexOnPage = this.tileModelOffset % this.pageSize;
648
+
649
+ // Fill the pages up to that point with empty models
650
+ for (let page = 1; page <= offsetPageNumber; page += 1) {
651
+ const remainingHidden = this.tileModelOffset - this.pageSize * (page - 1);
652
+ const offsetCellsOnPage = Math.min(this.pageSize, remainingHidden);
653
+ newDataSource[page] = Array(offsetCellsOnPage).fill(undefined);
654
+ }
655
+
656
+ // Shift all the remaining tiles into their new positions in the data source
657
+ for (const model of uncheckedTileModels) {
658
+ if (!newDataSource[offsetPageNumber])
659
+ newDataSource[offsetPageNumber] = [];
660
+ newDataSource[offsetPageNumber].push(model);
661
+ indexOnPage += 1;
662
+ if (indexOnPage >= this.pageSize) {
663
+ offsetPageNumber += 1;
664
+ indexOnPage = 0;
665
+ }
666
+ }
667
+
668
+ // Swap in the new data source and update the infinite scroller
669
+ this.dataSource = newDataSource;
670
+ if (this.totalResults) this.totalResults -= numChecked;
671
+ if (this.infiniteScroller) {
672
+ this.infiniteScroller.itemCount -= numChecked;
673
+ this.infiniteScroller.refreshAllVisibleCells();
674
+ }
675
+ }
676
+
677
+ /**
678
+ * Checks every tile's management checkbox
679
+ */
680
+ checkAllTiles(): void {
681
+ this.mapDataSource(model => ({ ...model, checked: true }));
682
+ }
683
+
684
+ /**
685
+ * Unchecks every tile's management checkbox
686
+ */
687
+ uncheckAllTiles(): void {
688
+ this.mapDataSource(model => ({ ...model, checked: false }));
689
+ }
690
+
691
+ /**
692
+ * Applies the given map function to all of the tile models in every page of the data
693
+ * source. This method updates the data source object in immutable fashion.
694
+ *
695
+ * @param mapFn A callback function to apply on each tile model, as with Array.map
696
+ */
697
+ private mapDataSource(
698
+ mapFn: (model: TileModel, index: number, array: TileModel[]) => TileModel
699
+ ): void {
700
+ this.dataSource = Object.fromEntries(
701
+ Object.entries(this.dataSource).map(([page, tileModels]) => [
702
+ page,
703
+ tileModels.map((model, index, array) =>
704
+ model ? mapFn(model, index, array) : model
705
+ ),
706
+ ])
707
+ );
708
+ this.infiniteScroller?.refreshAllVisibleCells();
709
+ }
710
+
711
+ /**
712
+ * An array of all the tile models whose management checkboxes are checked
713
+ */
714
+ get checkedTileModels(): TileModel[] {
715
+ return this.getFilteredTileModels(model => model.checked);
716
+ }
717
+
718
+ /**
719
+ * An array of all the tile models whose management checkboxes are unchecked
720
+ */
721
+ get uncheckedTileModels(): TileModel[] {
722
+ return this.getFilteredTileModels(model => !model.checked);
723
+ }
724
+
725
+ /**
726
+ * Returns a flattened, filtered array of all the tile models in the data source
727
+ * for which the given predicate returns a truthy value.
728
+ *
729
+ * @param predicate A callback function to apply on each tile model, as with Array.filter
730
+ * @returns A filtered array of tile models satisfying the predicate
731
+ */
732
+ private getFilteredTileModels(
733
+ predicate: (model: TileModel, index: number, array: TileModel[]) => unknown
734
+ ): TileModel[] {
735
+ return Object.values(this.dataSource)
736
+ .flat()
737
+ .filter((model, index, array) =>
738
+ model ? predicate(model, index, array) : false
739
+ );
740
+ }
741
+
585
742
  private userChangedSort(
586
743
  e: CustomEvent<{
587
744
  selectedSort: SortField;
@@ -781,6 +938,7 @@ export class CollectionBrowser
781
938
  .contentWidth=${this.contentWidth}
782
939
  .query=${this.baseQuery}
783
940
  .filterMap=${this.filterMap}
941
+ .isManageView=${this.isManageView}
784
942
  .modalManager=${this.modalManager}
785
943
  ?collapsableFacets=${this.mobileView}
786
944
  ?facetsLoading=${this.facetsLoading}
@@ -869,6 +1027,17 @@ export class CollectionBrowser
869
1027
  return `year:[${this.minSelectedDate} TO ${this.maxSelectedDate}]`;
870
1028
  }
871
1029
 
1030
+ /**
1031
+ * Emits an event indicating a change in whether the manage mode is shown.
1032
+ */
1033
+ private emitManageModeChangedEvent(): void {
1034
+ this.dispatchEvent(
1035
+ new CustomEvent<boolean>('manageModeChanged', {
1036
+ detail: this.isManageView,
1037
+ })
1038
+ );
1039
+ }
1040
+
872
1041
  firstUpdated(): void {
873
1042
  this.setupStateRestorationObserver();
874
1043
  this.restoreState();
@@ -891,7 +1060,7 @@ export class CollectionBrowser
891
1060
  changed.has('baseImageUrl') ||
892
1061
  changed.has('loggedIn')
893
1062
  ) {
894
- this.infiniteScroller?.reload();
1063
+ this.infiniteScroller?.refreshAllVisibleCells();
895
1064
  }
896
1065
 
897
1066
  if (
@@ -1000,6 +1169,12 @@ export class CollectionBrowser
1000
1169
  }
1001
1170
  }
1002
1171
 
1172
+ if (changed.has('isManageView')) {
1173
+ if (this.isManageView) this.displayMode = 'grid';
1174
+ this.infiniteScroller?.refreshAllVisibleCells();
1175
+ this.emitManageModeChangedEvent();
1176
+ }
1177
+
1003
1178
  if (changed.has('resizeObserver')) {
1004
1179
  const oldObserver = changed.get(
1005
1180
  'resizeObserver'
@@ -1125,14 +1300,6 @@ export class CollectionBrowser
1125
1300
  );
1126
1301
  }
1127
1302
 
1128
- private emitTabChanged(tabId: string) {
1129
- this.dispatchEvent(
1130
- new CustomEvent<String>('tabChanged', {
1131
- detail: tabId,
1132
- })
1133
- );
1134
- }
1135
-
1136
1303
  private disconnectResizeObserver(
1137
1304
  resizeObserver: SharedResizeObserverInterface
1138
1305
  ) {
@@ -1222,6 +1389,7 @@ export class CollectionBrowser
1222
1389
  this.previousQueryKey = this.pageFetchQueryKey;
1223
1390
 
1224
1391
  this.dataSource = {};
1392
+ this.tileModelOffset = 0;
1225
1393
  this.totalResults = undefined;
1226
1394
  this.aggregations = undefined;
1227
1395
  this.fullYearsHistogramAggregation = undefined;
@@ -1654,7 +1822,7 @@ export class CollectionBrowser
1654
1822
  // giving it 0.5s to finish.
1655
1823
  setTimeout(() => {
1656
1824
  this.isScrollingToCell = false;
1657
- this.infiniteScroller?.reload();
1825
+ this.infiniteScroller?.refreshAllVisibleCells();
1658
1826
  resolve();
1659
1827
  }, 500);
1660
1828
  }, 0);
@@ -1810,12 +1978,7 @@ export class CollectionBrowser
1810
1978
  return;
1811
1979
  }
1812
1980
 
1813
- this.totalResults = success.response.totalResults;
1814
-
1815
- // switch tab to ?tab=about if this.totalResults is 0
1816
- if (this.totalResults === 0) {
1817
- this.emitTabChanged('about');
1818
- }
1981
+ this.totalResults = success.response.totalResults - this.tileModelOffset;
1819
1982
 
1820
1983
  if (this.withinCollection) {
1821
1984
  this.collectionInfo = success.response.collectionExtraInfo;
@@ -1858,8 +2021,7 @@ export class CollectionBrowser
1858
2021
  if (resultCountDiscrepancy > 0) {
1859
2022
  this.endOfDataReached = true;
1860
2023
  if (this.infiniteScroller) {
1861
- this.infiniteScroller.itemCount =
1862
- this.estimatedTileCount - resultCountDiscrepancy;
2024
+ this.infiniteScroller.itemCount = this.totalResults;
1863
2025
  }
1864
2026
  }
1865
2027
 
@@ -1975,6 +2137,7 @@ export class CollectionBrowser
1975
2137
 
1976
2138
  tiles.push({
1977
2139
  averageRating: result.avg_rating?.value,
2140
+ checked: false,
1978
2141
  collections: result.collection?.values ?? [],
1979
2142
  collectionFilesCount: result.collection_files_count?.value ?? 0,
1980
2143
  collectionSize: result.collection_size?.value ?? 0,
@@ -2008,7 +2171,7 @@ export class CollectionBrowser
2008
2171
  const visiblePages = this.currentVisiblePageNumbers;
2009
2172
  const needsReload = visiblePages.includes(pageNumber);
2010
2173
  if (needsReload) {
2011
- this.infiniteScroller?.reload();
2174
+ this.infiniteScroller?.refreshAllVisibleCells();
2012
2175
  }
2013
2176
  }
2014
2177
 
@@ -2116,6 +2279,16 @@ export class CollectionBrowser
2116
2279
  * Callback when a result is selected
2117
2280
  */
2118
2281
  resultSelected(event: CustomEvent<TileModel>): void {
2282
+ if (this.isManageView) {
2283
+ // Checked/unchecked state change -- rerender to ensure it propagates
2284
+ // this.mapDataSource(model => ({ ...model }));
2285
+ const cellIndex = Object.values(this.dataSource)
2286
+ .flat()
2287
+ .indexOf(event.detail);
2288
+ if (cellIndex >= 0)
2289
+ this.infiniteScroller?.refreshCell(cellIndex - this.tileModelOffset);
2290
+ }
2291
+
2119
2292
  this.analyticsHandler?.sendEvent({
2120
2293
  category: this.searchContext,
2121
2294
  action: analyticsActions.resultSelected,
@@ -2146,6 +2319,7 @@ export class CollectionBrowser
2146
2319
  .creatorFilter=${this.selectedCreatorFilter}
2147
2320
  .mobileBreakpoint=${this.mobileBreakpoint}
2148
2321
  .loggedIn=${this.loggedIn}
2322
+ .isManageView=${this.isManageView}
2149
2323
  ?enableHoverPane=${true}
2150
2324
  @resultSelected=${(e: CustomEvent) => this.resultSelected(e)}
2151
2325
  >
@@ -11,6 +11,7 @@ import { customElement, property, state } from 'lit/decorators.js';
11
11
  import { map } from 'lit/directives/map.js';
12
12
  import { ref } from 'lit/directives/ref.js';
13
13
  import { msg } from '@lit/localize';
14
+ import { classMap } from 'lit/directives/class-map.js';
14
15
  import type {
15
16
  Aggregation,
16
17
  AggregationSortType,
@@ -99,6 +100,8 @@ export class CollectionFacets extends LitElement {
99
100
 
100
101
  @property({ type: String }) collectionPagePath: string = '/details/';
101
102
 
103
+ @property({ type: Boolean }) isManageView = false;
104
+
102
105
  @property({ type: Object, attribute: false })
103
106
  modalManager?: ModalManagerInterface;
104
107
 
@@ -133,9 +136,14 @@ export class CollectionFacets extends LitElement {
133
136
  private allowedFacetCount = 6;
134
137
 
135
138
  render() {
139
+ const containerClasses = classMap({
140
+ loading: this.facetsLoading,
141
+ managing: this.isManageView,
142
+ });
143
+
136
144
  const datePickerLabelId = 'date-picker-label';
137
145
  return html`
138
- <div id="container" class="${this.facetsLoading ? 'loading' : ''}">
146
+ <div id="container" class=${containerClasses}>
139
147
  ${this.showHistogramDatePicker &&
140
148
  (this.fullYearsHistogramAggregation || this.fullYearAggregationLoading)
141
149
  ? html`
@@ -715,6 +723,10 @@ export class CollectionFacets extends LitElement {
715
723
  opacity: 0.5;
716
724
  }
717
725
 
726
+ #container.managing {
727
+ opacity: 0.3;
728
+ }
729
+
718
730
  .histogram-loading-indicator {
719
731
  width: 100%;
720
732
  height: 2.25rem;
@@ -0,0 +1,151 @@
1
+ import { msg } from '@lit/localize';
2
+ import { LitElement, html, css, TemplateResult, CSSResultGroup } from 'lit';
3
+ import { customElement, property } from 'lit/decorators.js';
4
+ import { when } from 'lit/directives/when.js';
5
+
6
+ export interface ManageableItem {
7
+ identifier: string;
8
+ title?: string;
9
+ date?: string;
10
+ }
11
+
12
+ @customElement('manage-bar')
13
+ export class ManageBar extends LitElement {
14
+ /**
15
+ * The label displayed in front of the management buttons
16
+ */
17
+ @property({ type: String }) label = msg('Select items to un-favorite');
18
+
19
+ /**
20
+ * Whether to show the "Select All" button (default false)
21
+ */
22
+ @property({ type: Boolean }) showSelectAll = false;
23
+
24
+ /**
25
+ * Whether to show the "Unselect All" button (default false)
26
+ */
27
+ @property({ type: Boolean }) showUnselectAll = false;
28
+
29
+ render(): TemplateResult {
30
+ return html`
31
+ <div class="manage-container">
32
+ <span class="manage-label">${this.label}</span>
33
+ <div class="manage-buttons">
34
+ <button class="cancel-btn" @click=${this.cancelClicked}>
35
+ ${msg('Cancel')}
36
+ </button>
37
+ <button class="remove-btn" @click=${this.removeClicked}>
38
+ ${msg('Remove selected items')}
39
+ </button>
40
+ <div class="selection-buttons">
41
+ ${when(
42
+ this.showSelectAll,
43
+ () => html` <button
44
+ class="link-styled select-all-btn"
45
+ @click=${this.selectAllClicked}
46
+ >
47
+ ${msg('Select all')}
48
+ </button>`
49
+ )}
50
+ ${when(
51
+ this.showUnselectAll,
52
+ () => html` <button
53
+ class="link-styled unselect-all-btn"
54
+ @click=${this.unselectAllClicked}
55
+ >
56
+ ${msg('Unselect all')}
57
+ </button>`
58
+ )}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ `;
63
+ }
64
+
65
+ private cancelClicked(): void {
66
+ this.dispatchEvent(new CustomEvent('cancel'));
67
+ }
68
+
69
+ private removeClicked(): void {
70
+ this.dispatchEvent(new CustomEvent('removeItems'));
71
+ }
72
+
73
+ private selectAllClicked(): void {
74
+ this.dispatchEvent(new CustomEvent('selectAll'));
75
+ }
76
+
77
+ private unselectAllClicked(): void {
78
+ this.dispatchEvent(new CustomEvent('unselectAll'));
79
+ }
80
+
81
+ static get styles(): CSSResultGroup {
82
+ return css`
83
+ .manage-container {
84
+ display: flex;
85
+ align-items: center;
86
+ column-gap: 5px;
87
+ padding: 10px 0 20px;
88
+ flex-wrap: wrap;
89
+ }
90
+
91
+ .manage-label {
92
+ display: inline-block;
93
+ font-weight: bold;
94
+ font-size: 1.8rem;
95
+ padding-right: 10px;
96
+ }
97
+
98
+ .manage-buttons {
99
+ display: flex;
100
+ align-items: center;
101
+ column-gap: 5px;
102
+ }
103
+
104
+ button {
105
+ display: inline-block;
106
+ font-size: 1.4rem;
107
+ cursor: pointer;
108
+ white-space: nowrap;
109
+ }
110
+
111
+ button.link-styled {
112
+ margin: 0;
113
+ padding: 6px;
114
+ border: 0;
115
+ appearance: none;
116
+ background: none;
117
+ color: var(--ia-theme-link-color, #4b64ff);
118
+ text-decoration: none;
119
+ }
120
+ button.link-styled:hover {
121
+ text-decoration: underline;
122
+ }
123
+
124
+ button:not(.link-styled) {
125
+ margin: 0;
126
+ padding: 6px 12px;
127
+ border-radius: 4px;
128
+ color: white;
129
+ }
130
+
131
+ /* Button styles derived from legacy version */
132
+ .cancel-btn {
133
+ background: #777777;
134
+ border: 1px solid #666666;
135
+ }
136
+ .cancel-btn:hover {
137
+ background: #6b6b6b;
138
+ border: 1px solid #505050;
139
+ }
140
+
141
+ .remove-btn {
142
+ background: #d9534f;
143
+ border: 1px solid #d43f3a;
144
+ }
145
+ .remove-btn:hover {
146
+ background: #d2322d;
147
+ border: 1px solid #ac2925;
148
+ }
149
+ `;
150
+ }
151
+ }
package/src/models.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
 
7
7
  export interface TileModel {
8
8
  averageRating?: number;
9
+ checked: boolean; // Whether this tile is currently checked for item management functions
9
10
  collectionIdentifier?: string;
10
11
  collectionName?: string;
11
12
  collectionFilesCount: number;
@@ -1,6 +1,7 @@
1
1
  import { css, html, nothing, PropertyValues } from 'lit';
2
2
  import { customElement, property, query } from 'lit/decorators.js';
3
3
  import { ifDefined } from 'lit/directives/if-defined.js';
4
+ import { msg } from '@lit/localize';
4
5
  import type {
5
6
  SharedResizeObserverInterface,
6
7
  SharedResizeObserverResizeHandlerInterface,
@@ -46,6 +47,8 @@ export class TileDispatcher
46
47
 
47
48
  @property({ type: String }) tileDisplayMode?: TileDisplayMode;
48
49
 
50
+ @property({ type: Boolean }) isManageView = false;
51
+
49
52
  @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
50
53
 
51
54
  @property({ type: Object })
@@ -54,6 +57,10 @@ export class TileDispatcher
54
57
  /** Whether this tile should include a hover pane at all (for applicable tile modes) */
55
58
  @property({ type: Boolean }) enableHoverPane = false;
56
59
 
60
+ @property({ type: String }) manageCheckTitle = msg(
61
+ 'Remove this item from the list'
62
+ );
63
+
57
64
  private hoverPaneController?: HoverPaneControllerInterface;
58
65
 
59
66
  @query('#container')
@@ -82,7 +89,7 @@ export class TileDispatcher
82
89
  ${this.tileDisplayMode === 'list-header'
83
90
  ? this.headerTemplate
84
91
  : this.tileTemplate}
85
- ${hoverPaneTemplate}
92
+ ${this.manageCheckTemplate} ${hoverPaneTemplate}
86
93
  </div>
87
94
  `;
88
95
  }
@@ -125,10 +132,8 @@ export class TileDispatcher
125
132
  title=${this.shouldPrepareHoverPane
126
133
  ? nothing // Don't show title tooltips when we have the tile info popups
127
134
  : ifDefined(this.model?.title)}
128
- @click=${() =>
129
- this.dispatchEvent(
130
- new CustomEvent('resultSelected', { detail: this.model })
131
- )}
135
+ @click=${this.handleLinkClicked}
136
+ @contextmenu=${this.handleLinkContextMenu}
132
137
  >
133
138
  ${this.tile}
134
139
  </a>
@@ -151,6 +156,23 @@ export class TileDispatcher
151
156
  );
152
157
  }
153
158
 
159
+ private get manageCheckTemplate() {
160
+ if (!this.isManageView || this.tileDisplayMode !== 'grid') return nothing;
161
+
162
+ return html`
163
+ <div class="manage-check">
164
+ <input
165
+ type="checkbox"
166
+ title=${this.manageCheckTitle}
167
+ .checked=${this.model?.checked}
168
+ @change=${() => {
169
+ if (this.model) this.model.checked = !this.model.checked;
170
+ }}
171
+ />
172
+ </div>
173
+ `;
174
+ }
175
+
154
176
  /**
155
177
  * Whether hover pane behavior should be prepared for this tile
156
178
  * (e.g., whether mouse listeners should be attached, etc.)
@@ -211,6 +233,32 @@ export class TileDispatcher
211
233
  }
212
234
  }
213
235
 
236
+ /**
237
+ * Handler for when the tile link is left-clicked. Emits the `resultSelected` event.
238
+ * In manage view, it also checks/unchecks the tile.
239
+ */
240
+ private handleLinkClicked(e: Event): void {
241
+ if (this.isManageView) {
242
+ e.preventDefault();
243
+ if (this.model) this.model.checked = !this.model.checked;
244
+ }
245
+
246
+ this.dispatchEvent(
247
+ new CustomEvent('resultSelected', { detail: this.model })
248
+ );
249
+ }
250
+
251
+ /**
252
+ * Handler for when the tile link is right-clicked.
253
+ * In manage view, it opens the item in a new tab. Otherwise, does nothing.
254
+ */
255
+ private handleLinkContextMenu(e: Event): void {
256
+ if (this.isManageView && this.linkTileHref !== nothing) {
257
+ e.preventDefault();
258
+ window.open(this.linkTileHref, '_blank');
259
+ }
260
+ }
261
+
214
262
  private tileInfoButtonPressed(
215
263
  e: CustomEvent<{ x: number; y: number }>
216
264
  ): void {
@@ -245,6 +293,7 @@ export class TileDispatcher
245
293
  .currentWidth=${currentWidth}
246
294
  .currentHeight=${currentHeight}
247
295
  .creatorFilter=${creatorFilter}
296
+ .isManageView=${this.isManageView}
248
297
  ?showInfoButton=${!this.isHoverEnabled}
249
298
  @infoButtonPressed=${this.tileInfoButtonPressed}
250
299
  >
@@ -257,6 +306,7 @@ export class TileDispatcher
257
306
  .currentWidth=${currentWidth}
258
307
  .currentHeight=${currentHeight}
259
308
  .creatorFilter=${creatorFilter}
309
+ .isManageView=${this.isManageView}
260
310
  ?showInfoButton=${!this.isHoverEnabled}
261
311
  @infoButtonPressed=${this.tileInfoButtonPressed}
262
312
  >
@@ -272,6 +322,7 @@ export class TileDispatcher
272
322
  .sortParam=${sortParam}
273
323
  .creatorFilter=${creatorFilter}
274
324
  .loggedIn=${this.loggedIn}
325
+ .isManageView=${this.isManageView}
275
326
  ?showInfoButton=${!this.isHoverEnabled}
276
327
  @infoButtonPressed=${this.tileInfoButtonPressed}
277
328
  >
@@ -358,6 +409,21 @@ export class TileDispatcher
358
409
  height: 100%;
359
410
  }
360
411
 
412
+ .manage-check {
413
+ position: absolute;
414
+ right: 0;
415
+ top: 0;
416
+ border: 5px solid #2c2c2c;
417
+ border-radius: 3px;
418
+ background-color: #2c2c2c;
419
+ z-index: 1;
420
+ }
421
+
422
+ .manage-check > input[type='checkbox'] {
423
+ display: block;
424
+ margin: 0;
425
+ }
426
+
361
427
  #touch-backdrop {
362
428
  position: fixed;
363
429
  width: 100vw;