@internetarchive/collection-browser 0.4.16-alpha.9 → 0.4.17

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 (74) hide show
  1. package/dist/src/app-root.js +12 -0
  2. package/dist/src/app-root.js.map +1 -1
  3. package/dist/src/collection-browser.d.ts +53 -14
  4. package/dist/src/collection-browser.js +279 -113
  5. package/dist/src/collection-browser.js.map +1 -1
  6. package/dist/src/collection-facets/facets-template.d.ts +3 -0
  7. package/dist/src/collection-facets/facets-template.js +20 -1
  8. package/dist/src/collection-facets/facets-template.js.map +1 -1
  9. package/dist/src/collection-facets/more-facets-content.js +7 -4
  10. package/dist/src/collection-facets/more-facets-content.js.map +1 -1
  11. package/dist/src/collection-facets.d.ts +0 -2
  12. package/dist/src/collection-facets.js +1 -4
  13. package/dist/src/collection-facets.js.map +1 -1
  14. package/dist/src/empty-placeholder.js +1 -0
  15. package/dist/src/empty-placeholder.js.map +1 -1
  16. package/dist/src/models.d.ts +5 -0
  17. package/dist/src/models.js.map +1 -1
  18. package/dist/src/tiles/grid/item-tile.js +10 -3
  19. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  20. package/dist/src/tiles/list/tile-list-compact.d.ts +1 -0
  21. package/dist/src/tiles/list/tile-list-compact.js +13 -1
  22. package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
  23. package/dist/src/tiles/list/tile-list.js +10 -1
  24. package/dist/src/tiles/list/tile-list.js.map +1 -1
  25. package/dist/src/utils/format-date.d.ts +1 -1
  26. package/dist/src/utils/format-date.js +3 -0
  27. package/dist/src/utils/format-date.js.map +1 -1
  28. package/dist/src/utils/local-date-from-utc.d.ts +9 -0
  29. package/dist/src/utils/local-date-from-utc.js +16 -0
  30. package/dist/src/utils/local-date-from-utc.js.map +1 -0
  31. package/dist/test/collection-browser.test.js +73 -10
  32. package/dist/test/collection-browser.test.js.map +1 -1
  33. package/dist/test/collection-facets/facets-template.test.js +80 -0
  34. package/dist/test/collection-facets/facets-template.test.js.map +1 -1
  35. package/dist/test/mocks/mock-collection-name-cache.d.ts +2 -0
  36. package/dist/test/mocks/mock-collection-name-cache.js +4 -0
  37. package/dist/test/mocks/mock-collection-name-cache.js.map +1 -1
  38. package/dist/test/mocks/mock-search-responses.d.ts +2 -0
  39. package/dist/test/mocks/mock-search-responses.js +81 -0
  40. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  41. package/dist/test/mocks/mock-search-service.js +3 -1
  42. package/dist/test/mocks/mock-search-service.js.map +1 -1
  43. package/dist/test/tiles/grid/item-tile.test.js +124 -3
  44. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  45. package/dist/test/tiles/list/tile-list-compact.test.js +65 -20
  46. package/dist/test/tiles/list/tile-list-compact.test.js.map +1 -1
  47. package/dist/test/tiles/list/tile-list.test.js +106 -4
  48. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  49. package/dist/test/utils/local-date-from-utc.test.d.ts +1 -0
  50. package/dist/test/utils/local-date-from-utc.test.js +27 -0
  51. package/dist/test/utils/local-date-from-utc.test.js.map +1 -0
  52. package/index.html +1 -0
  53. package/package.json +3 -3
  54. package/src/app-root.ts +12 -0
  55. package/src/collection-browser.ts +311 -114
  56. package/src/collection-facets/facets-template.ts +32 -1
  57. package/src/collection-facets/more-facets-content.ts +4 -1
  58. package/src/collection-facets.ts +1 -8
  59. package/src/empty-placeholder.ts +1 -0
  60. package/src/models.ts +6 -0
  61. package/src/tiles/grid/item-tile.ts +11 -4
  62. package/src/tiles/list/tile-list-compact.ts +16 -2
  63. package/src/tiles/list/tile-list.ts +12 -5
  64. package/src/utils/format-date.ts +4 -0
  65. package/src/utils/local-date-from-utc.ts +15 -0
  66. package/test/collection-browser.test.ts +101 -12
  67. package/test/collection-facets/facets-template.test.ts +98 -0
  68. package/test/mocks/mock-collection-name-cache.ts +8 -0
  69. package/test/mocks/mock-search-responses.ts +89 -0
  70. package/test/mocks/mock-search-service.ts +4 -0
  71. package/test/tiles/grid/item-tile.test.ts +145 -3
  72. package/test/tiles/list/tile-list-compact.test.ts +70 -19
  73. package/test/tiles/list/tile-list.test.ts +118 -4
  74. package/test/utils/local-date-from-utc.test.ts +37 -0
@@ -55,7 +55,7 @@ import {
55
55
  PrefixFilterType,
56
56
  PrefixFilterCounts,
57
57
  prefixFilterAggregationKeys,
58
- FacetOption,
58
+ FacetEventDetails,
59
59
  } from './models';
60
60
  import {
61
61
  RestorationStateHandlerInterface,
@@ -104,10 +104,6 @@ export class CollectionBrowser
104
104
 
105
105
  @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
106
106
 
107
- @property({ type: String }) titleQuery?: string;
108
-
109
- @property({ type: String }) creatorQuery?: string;
110
-
111
107
  @property({ type: Number }) currentPage?: number;
112
108
 
113
109
  @property({ type: String }) minSelectedDate?: string;
@@ -189,6 +185,8 @@ export class CollectionBrowser
189
185
 
190
186
  @query('#content-container') private contentContainer!: HTMLDivElement;
191
187
 
188
+ @query('#left-column') private leftColumn?: HTMLDivElement;
189
+
192
190
  @property({ type: Object, attribute: false })
193
191
  private analyticsHandler?: AnalyticsManagerInterface;
194
192
 
@@ -209,6 +207,10 @@ export class CollectionBrowser
209
207
  */
210
208
  private isResizeToMobile = false;
211
209
 
210
+ private leftColIntersectionObserver?: IntersectionObserver;
211
+
212
+ private facetsIntersectionObserver?: IntersectionObserver;
213
+
212
214
  private placeholderCellTemplate = html`<collection-browser-loading-tile></collection-browser-loading-tile>`;
213
215
 
214
216
  private tileModelAtCellIndex(index: number): TileModel | undefined {
@@ -294,8 +296,6 @@ export class CollectionBrowser
294
296
  if (letterFilters) {
295
297
  this.selectedTitleFilter = null;
296
298
  this.selectedCreatorFilter = null;
297
- this.titleQuery = undefined;
298
- this.creatorQuery = undefined;
299
299
  }
300
300
 
301
301
  if (sort) {
@@ -339,11 +339,9 @@ export class CollectionBrowser
339
339
  this.placeholderType = null;
340
340
  if (!this.baseQuery?.trim()) {
341
341
  this.placeholderType = 'empty-query';
342
- }
343
-
344
- if (
345
- (!this.searchResultsLoading && this.totalResults === 0) ||
346
- !this.searchService
342
+ } else if (
343
+ !this.searchResultsLoading &&
344
+ (this.totalResults === 0 || !this.searchService)
347
345
  ) {
348
346
  this.placeholderType = 'null-result';
349
347
  }
@@ -370,7 +368,8 @@ export class CollectionBrowser
370
368
  this.searchResultsLoading || this.totalResults === undefined;
371
369
  const resultsCount = this.totalResults?.toLocaleString();
372
370
  const resultsLabel = this.totalResults === 1 ? 'Result' : 'Results';
373
- return html`<div
371
+ return html` <div id="left-column-scroll-sentinel"></div>
372
+ <div
374
373
  id="left-column"
375
374
  class="column${this.isResizeToMobile ? ' preload' : ''}"
376
375
  >
@@ -392,7 +391,9 @@ export class CollectionBrowser
392
391
  : ''}
393
392
  >
394
393
  ${this.facetsTemplate}
394
+ <div id="facets-scroll-sentinel"></div>
395
395
  </div>
396
+ ${this.mobileView ? nothing : html`<div id="facets-bottom-fade"></div>`}
396
397
  </div>
397
398
  <div id="right-column" class="column">
398
399
  ${this.sortFilterBarTemplate}
@@ -497,9 +498,30 @@ export class CollectionBrowser
497
498
  }
498
499
  }
499
500
 
500
- /** Send Analytics when sorting by title's first letter
501
+ /**
502
+ * Returns a query clause identifying the currently selected title filter,
503
+ * e.g., `firstTitle:X`.
504
+ */
505
+ private get titleQuery(): string | undefined {
506
+ return this.selectedTitleFilter
507
+ ? `firstTitle:${this.selectedTitleFilter}`
508
+ : undefined;
509
+ }
510
+
511
+ /**
512
+ * Returns a query clause identifying the currently selected creator filter,
513
+ * e.g., `firstCreator:X`.
514
+ */
515
+ private get creatorQuery(): string | undefined {
516
+ return this.selectedCreatorFilter
517
+ ? `firstCreator:${this.selectedCreatorFilter}`
518
+ : undefined;
519
+ }
520
+
521
+ /**
522
+ * Send Analytics when sorting by title's first letter
501
523
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
502
- * */
524
+ */
503
525
  private sendFilterByTitleAnalytics(prevSelectedLetter: string | null): void {
504
526
  if (!prevSelectedLetter && !this.selectedTitleFilter) {
505
527
  return;
@@ -515,15 +537,10 @@ export class CollectionBrowser
515
537
  });
516
538
  }
517
539
 
518
- private selectedTitleLetterChanged(): void {
519
- this.titleQuery = this.selectedTitleFilter
520
- ? `firstTitle:${this.selectedTitleFilter}`
521
- : undefined;
522
- }
523
-
524
- /** Send Analytics when filtering by creator's first letter
540
+ /**
541
+ * Send Analytics when filtering by creator's first letter
525
542
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
526
- * */
543
+ */
527
544
  private sendFilterByCreatorAnalytics(
528
545
  prevSelectedLetter: string | null
529
546
  ): void {
@@ -541,26 +558,24 @@ export class CollectionBrowser
541
558
  });
542
559
  }
543
560
 
544
- private selectedCreatorLetterChanged(): void {
545
- this.creatorQuery = this.selectedCreatorFilter
546
- ? `firstCreator:${this.selectedCreatorFilter}`
547
- : undefined;
548
- }
549
-
561
+ /**
562
+ * Handler for changes to which letter is selected in the title alphabet bar.
563
+ */
550
564
  private titleLetterSelected(
551
565
  e: CustomEvent<{ selectedLetter: string | null }>
552
566
  ): void {
553
567
  this.selectedCreatorFilter = null;
554
568
  this.selectedTitleFilter = e.detail.selectedLetter;
555
- this.selectedTitleLetterChanged();
556
569
  }
557
570
 
571
+ /**
572
+ * Handler for changes to which letter is selected in the creator alphabet bar.
573
+ */
558
574
  private creatorLetterSelected(
559
575
  e: CustomEvent<{ selectedLetter: string | null }>
560
576
  ): void {
561
577
  this.selectedTitleFilter = null;
562
578
  this.selectedCreatorFilter = e.detail.selectedLetter;
563
- this.selectedCreatorLetterChanged();
564
579
  }
565
580
 
566
581
  private get mobileFacetsTemplate() {
@@ -603,13 +618,13 @@ export class CollectionBrowser
603
618
  .selectedFacets=${this.selectedFacets}
604
619
  .collectionNameCache=${this.collectionNameCache}
605
620
  .showHistogramDatePicker=${this.showHistogramDatePicker}
606
- .query=${this.filteredQuery}
621
+ .query=${this.baseQuery}
607
622
  .filterMap=${this.filterMap}
608
623
  .modalManager=${this.modalManager}
609
624
  ?collapsableFacets=${this.mobileView}
610
625
  ?facetsLoading=${this.facetsLoading}
611
626
  ?fullYearAggregationLoading=${this.facetsLoading}
612
- .onFacetClick=${this.facetClickHandler}
627
+ @facetClick=${this.facetClickHandler}
613
628
  .analyticsHandler=${this.analyticsHandler}
614
629
  >
615
630
  </collection-facets>
@@ -669,6 +684,16 @@ export class CollectionBrowser
669
684
  }
670
685
 
671
686
  updated(changed: PropertyValues) {
687
+ if (changed.has('placeholderType') && this.placeholderType === null) {
688
+ if (!this.leftColIntersectionObserver) {
689
+ this.setupLeftColumnScrollListeners();
690
+ }
691
+ if (!this.facetsIntersectionObserver) {
692
+ this.setupFacetsScrollListeners();
693
+ }
694
+ this.updateLeftColumnHeight();
695
+ }
696
+
672
697
  if (
673
698
  changed.has('displayMode') ||
674
699
  changed.has('baseNavigationUrl') ||
@@ -729,13 +754,11 @@ export class CollectionBrowser
729
754
  this.sendFilterByTitleAnalytics(
730
755
  changed.get('selectedTitleFilter') as string
731
756
  );
732
- this.selectedTitleLetterChanged();
733
757
  }
734
758
  if (changed.has('selectedCreatorFilter')) {
735
759
  this.sendFilterByCreatorAnalytics(
736
760
  changed.get('selectedCreatorFilter') as string
737
761
  );
738
- this.selectedCreatorLetterChanged();
739
762
  }
740
763
 
741
764
  if (
@@ -774,6 +797,10 @@ export class CollectionBrowser
774
797
  if (this.boundNavigationHandler) {
775
798
  window.removeEventListener('popstate', this.boundNavigationHandler);
776
799
  }
800
+
801
+ this.leftColIntersectionObserver?.disconnect();
802
+ this.facetsIntersectionObserver?.disconnect();
803
+ window.removeEventListener('resize', this.updateLeftColumnHeight);
777
804
  }
778
805
 
779
806
  handleResize(entry: ResizeObserverEntry): void {
@@ -785,8 +812,80 @@ export class CollectionBrowser
785
812
  this.isResizeToMobile = true;
786
813
  }
787
814
  }
815
+
816
+ // Ensure the facet sidebar remains sized correctly
817
+ this.updateLeftColumnHeight();
818
+ }
819
+
820
+ /**
821
+ * Sets up listeners for events that may require updating the left column height.
822
+ */
823
+ private setupLeftColumnScrollListeners(): void {
824
+ // We observe intersections between the left column's scroll sentinel and
825
+ // the viewport, so that we can ensure the left column is always sized to
826
+ // match the _available_ viewport height. This should generally be more
827
+ // performant than listening to scroll events on the page or column.
828
+ const leftColumnSentinel = this.shadowRoot?.querySelector(
829
+ '#left-column-scroll-sentinel'
830
+ );
831
+ if (leftColumnSentinel) {
832
+ this.leftColIntersectionObserver = new IntersectionObserver(
833
+ this.updateLeftColumnHeight,
834
+ {
835
+ threshold: [...Array(101).keys()].map(n => n / 100), // Threshold every 1%
836
+ }
837
+ );
838
+ this.leftColIntersectionObserver.observe(leftColumnSentinel);
839
+ }
840
+
841
+ // We also listen for window resize events, as they are not always captured
842
+ // by the resize observer and can affect the desired height of the left column.
843
+ window.addEventListener('resize', this.updateLeftColumnHeight);
844
+ }
845
+
846
+ /**
847
+ * Sets up listeners to control whether the facet sidebar shows its bottom fade-out.
848
+ * Note this uses a separate IntersectionObserver from the left column, because we
849
+ * don't need granular intersection thresholds for this.
850
+ */
851
+ private setupFacetsScrollListeners(): void {
852
+ const facetsSentinel = this.shadowRoot?.querySelector(
853
+ '#facets-scroll-sentinel'
854
+ );
855
+ if (facetsSentinel) {
856
+ this.facetsIntersectionObserver = new IntersectionObserver(
857
+ this.updateFacetFadeOut
858
+ );
859
+ this.facetsIntersectionObserver.observe(facetsSentinel);
860
+ }
788
861
  }
789
862
 
863
+ /**
864
+ * Updates the height of the left column according to its position on the page.
865
+ * Arrow function ensures proper `this` binding.
866
+ */
867
+ private updateLeftColumnHeight = (): void => {
868
+ if (this.mobileView) {
869
+ this.leftColumn?.style?.removeProperty('height');
870
+ } else {
871
+ const clientTop = this.leftColumn?.getBoundingClientRect().top;
872
+ this.leftColumn?.style?.setProperty(
873
+ 'height',
874
+ `${window.innerHeight - (clientTop ?? 0) - 3}px`
875
+ );
876
+ }
877
+ };
878
+
879
+ /**
880
+ * Toggles whether the fade-out is visible at the bottom of the facets.
881
+ * It should only be visible if the facets are not scrolled to the bottom.
882
+ * Arrow function ensures proper `this` binding.
883
+ */
884
+ private updateFacetFadeOut = (entries: IntersectionObserverEntry[]): void => {
885
+ const fadeElmt = this.shadowRoot?.getElementById('facets-bottom-fade');
886
+ fadeElmt?.classList.toggle('hidden', entries?.[0]?.isIntersecting);
887
+ };
888
+
790
889
  private emitBaseQueryChanged() {
791
890
  this.dispatchEvent(
792
891
  new CustomEvent<{ baseQuery?: string }>('baseQueryChanged', {
@@ -952,6 +1051,11 @@ export class CollectionBrowser
952
1051
  this.searchResultsLoading = false;
953
1052
  }
954
1053
 
1054
+ /**
1055
+ * Constructs a search service FilterMap object from the combination of
1056
+ * all the currently-applied filters. This includes any facets, letter
1057
+ * filters, and date range.
1058
+ */
955
1059
  private get filterMap(): FilterMap {
956
1060
  const builder = new FilterMapBuilder();
957
1061
 
@@ -995,21 +1099,24 @@ export class CollectionBrowser
995
1099
  }
996
1100
  }
997
1101
 
998
- const filterMap = builder.build();
999
- return filterMap;
1000
- }
1001
-
1002
- /** The base query joined with any title/creator letter filters */
1003
- private get filteredQuery(): string | undefined {
1004
- if (!this.baseQuery) return undefined;
1005
- let filteredQuery = this.baseQuery.trim();
1006
-
1007
- const { sortFilterQueries } = this;
1008
- if (sortFilterQueries) {
1009
- filteredQuery += ` AND ${sortFilterQueries}`;
1102
+ // Add any letter filters
1103
+ if (this.selectedTitleFilter) {
1104
+ builder.addFilter(
1105
+ 'firstTitle',
1106
+ this.selectedTitleFilter,
1107
+ FilterConstraint.INCLUDE
1108
+ );
1109
+ }
1110
+ if (this.selectedCreatorFilter) {
1111
+ builder.addFilter(
1112
+ 'firstCreator',
1113
+ this.selectedCreatorFilter,
1114
+ FilterConstraint.INCLUDE
1115
+ );
1010
1116
  }
1011
1117
 
1012
- return filteredQuery.trim();
1118
+ const filterMap = builder.build();
1119
+ return filterMap;
1013
1120
  }
1014
1121
 
1015
1122
  /** The full query, including year facets and date range clauses */
@@ -1031,22 +1138,6 @@ export class CollectionBrowser
1031
1138
  return fullQuery.trim();
1032
1139
  }
1033
1140
 
1034
- /** The full query without any title/creator letter filters */
1035
- private get fullQueryWithoutAlphaFilters(): string | undefined {
1036
- if (!this.baseQuery) return undefined;
1037
- let fullQuery = this.baseQuery.trim();
1038
-
1039
- const { facetQuery, dateRangeQueryClause } = this;
1040
-
1041
- if (facetQuery) {
1042
- fullQuery += ` AND ${facetQuery}`;
1043
- }
1044
- if (dateRangeQueryClause) {
1045
- fullQuery += ` AND ${dateRangeQueryClause}`;
1046
- }
1047
- return fullQuery.trim();
1048
- }
1049
-
1050
1141
  /**
1051
1142
  * Generates a query string for the given facets
1052
1143
  *
@@ -1137,36 +1228,39 @@ export class CollectionBrowser
1137
1228
  this.selectedFacets = e.detail;
1138
1229
  }
1139
1230
 
1140
- facetClickHandler(
1141
- name: FacetOption,
1142
- facetSelected: boolean,
1143
- negative: boolean
1144
- ): void {
1231
+ facetClickHandler({
1232
+ detail: { key, state: facetState, negative },
1233
+ }: CustomEvent<FacetEventDetails>): void {
1145
1234
  if (negative) {
1146
1235
  this.analyticsHandler?.sendEvent({
1147
1236
  category: this.searchContext,
1148
- action: facetSelected
1149
- ? analyticsActions.facetNegativeSelected
1150
- : analyticsActions.facetNegativeDeselected,
1151
- label: name,
1237
+ action:
1238
+ facetState !== 'none'
1239
+ ? analyticsActions.facetNegativeSelected
1240
+ : analyticsActions.facetNegativeDeselected,
1241
+ label: key,
1152
1242
  });
1153
1243
  } else {
1154
1244
  this.analyticsHandler?.sendEvent({
1155
1245
  category: this.searchContext,
1156
- action: facetSelected
1157
- ? analyticsActions.facetSelected
1158
- : analyticsActions.facetDeselected,
1159
- label: name,
1246
+ action:
1247
+ facetState !== 'none'
1248
+ ? analyticsActions.facetSelected
1249
+ : analyticsActions.facetDeselected,
1250
+ label: key,
1160
1251
  });
1161
1252
  }
1162
1253
  }
1163
1254
 
1164
1255
  private async fetchFacets() {
1165
- if (!this.filteredQuery) return;
1256
+ const trimmedQuery = this.baseQuery?.trim();
1257
+ if (!trimmedQuery) return;
1166
1258
  if (!this.searchService) return;
1167
1259
 
1260
+ const { facetFetchQueryKey } = this;
1261
+
1168
1262
  const params: SearchParams = {
1169
- query: this.filteredQuery,
1263
+ query: trimmedQuery,
1170
1264
  rows: 0,
1171
1265
  filters: this.filterMap,
1172
1266
  // Fetch a few extra buckets beyond the 6 we show, in case some get suppressed
@@ -1182,7 +1276,13 @@ export class CollectionBrowser
1182
1276
  this.searchType
1183
1277
  );
1184
1278
  const success = searchResponse?.success;
1185
- this.facetsLoading = false;
1279
+
1280
+ // This is checking to see if the query has changed since the data was fetched.
1281
+ // If so, we just want to discard this set of aggregations because they are
1282
+ // likely no longer valid for the newer query.
1283
+ const queryChangedSinceFetch =
1284
+ facetFetchQueryKey !== this.facetFetchQueryKey;
1285
+ if (queryChangedSinceFetch) return;
1186
1286
 
1187
1287
  if (!success) {
1188
1288
  const errorMsg = searchResponse?.error?.message;
@@ -1199,17 +1299,23 @@ export class CollectionBrowser
1199
1299
  return;
1200
1300
  }
1201
1301
 
1202
- // This is checking to see if the query has changed since the data was fetched.
1203
- // If so, we just want to discard this set of aggregations because they are
1204
- // likely no longer valid for the newer query.
1205
- const returnedUid = (success.request.clientParameters as any).uid;
1206
- const queryChangedSinceFetch = returnedUid !== this.facetFetchQueryKey;
1207
- if (queryChangedSinceFetch) return;
1302
+ const { aggregations, collectionTitles } = success.response;
1303
+ this.aggregations = aggregations;
1208
1304
 
1209
- this.aggregations = success?.response.aggregations;
1305
+ if (collectionTitles) {
1306
+ this.collectionNameCache?.addKnownTitles(collectionTitles);
1307
+ } else if (this.aggregations?.collection) {
1308
+ this.collectionNameCache?.preloadIdentifiers(
1309
+ (this.aggregations.collection.buckets as Bucket[]).map(bucket =>
1310
+ bucket.key?.toString()
1311
+ )
1312
+ );
1313
+ }
1210
1314
 
1211
1315
  this.fullYearsHistogramAggregation =
1212
1316
  success?.response?.aggregations?.year_histogram;
1317
+
1318
+ this.facetsLoading = false;
1213
1319
  }
1214
1320
 
1215
1321
  private scrollToPage(pageNumber: number): Promise<void> {
@@ -1264,7 +1370,8 @@ export class CollectionBrowser
1264
1370
  private pageFetchesInProgress: Record<string, Set<number>> = {};
1265
1371
 
1266
1372
  async fetchPage(pageNumber: number) {
1267
- if (!this.filteredQuery) return;
1373
+ const trimmedQuery = this.baseQuery?.trim();
1374
+ if (!trimmedQuery) return;
1268
1375
  if (!this.searchService) return;
1269
1376
 
1270
1377
  // if we already have data, don't fetch again
@@ -1282,7 +1389,7 @@ export class CollectionBrowser
1282
1389
 
1283
1390
  const sortParams = this.sortParam ? [this.sortParam] : [];
1284
1391
  const params: SearchParams = {
1285
- query: this.filteredQuery,
1392
+ query: trimmedQuery,
1286
1393
  page: pageNumber,
1287
1394
  rows: this.pageSize,
1288
1395
  sort: sortParams,
@@ -1296,6 +1403,12 @@ export class CollectionBrowser
1296
1403
  );
1297
1404
  const success = searchResponse?.success;
1298
1405
 
1406
+ // This is checking to see if the query has changed since the data was fetched.
1407
+ // If so, we just want to discard the data since there should be a new query
1408
+ // right behind it.
1409
+ const queryChangedSinceFetch = pageFetchQueryKey !== this.pageFetchQueryKey;
1410
+ if (queryChangedSinceFetch) return;
1411
+
1299
1412
  if (!success) {
1300
1413
  const errorMsg = searchResponse?.error?.message;
1301
1414
  const detailMsg = searchResponse?.error?.details?.message;
@@ -1310,21 +1423,20 @@ export class CollectionBrowser
1310
1423
  window?.Sentry?.captureMessage?.(this.queryErrorMessage, 'error');
1311
1424
  }
1312
1425
 
1426
+ this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1427
+ this.searchResultsLoading = false;
1313
1428
  return;
1314
1429
  }
1315
1430
 
1316
- // This is checking to see if the query has changed since the data was fetched.
1317
- // If so, we just want to discard the data since there should be a new query
1318
- // right behind it.
1319
- const returnedUid = (success.request.clientParameters as any).uid;
1320
- const queryChangedSinceFetch = returnedUid !== this.pageFetchQueryKey;
1321
- if (queryChangedSinceFetch) return;
1322
-
1323
1431
  this.totalResults = success.response.totalResults;
1324
1432
 
1325
- const { results } = success.response;
1433
+ const { results, collectionTitles } = success.response;
1326
1434
  if (results && results.length > 0) {
1327
- this.preloadCollectionNames(results);
1435
+ if (collectionTitles) {
1436
+ this.collectionNameCache?.addKnownTitles(collectionTitles);
1437
+ } else {
1438
+ this.preloadCollectionNames(results);
1439
+ }
1328
1440
  this.updateDataSource(pageNumber, results);
1329
1441
  }
1330
1442
 
@@ -1337,6 +1449,7 @@ export class CollectionBrowser
1337
1449
  this.infiniteScroller.itemCount = this.totalResults;
1338
1450
  }
1339
1451
  }
1452
+
1340
1453
  this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1341
1454
  this.searchResultsLoading = false;
1342
1455
  }
@@ -1442,12 +1555,14 @@ export class CollectionBrowser
1442
1555
  private async fetchPrefixFilterBuckets(
1443
1556
  filterType: PrefixFilterType
1444
1557
  ): Promise<Bucket[]> {
1445
- if (!this.fullQueryWithoutAlphaFilters) return [];
1558
+ const trimmedQuery = this.baseQuery?.trim();
1559
+ if (!trimmedQuery) return [];
1446
1560
 
1447
1561
  const filterAggregationKey = prefixFilterAggregationKeys[filterType];
1448
1562
  const params: SearchParams = {
1449
- query: this.fullQueryWithoutAlphaFilters,
1563
+ query: trimmedQuery,
1450
1564
  rows: 0,
1565
+ filters: this.filterMap,
1451
1566
  // Only fetch the firstTitle or firstCreator aggregation
1452
1567
  aggregations: { simpleParams: [filterAggregationKey] },
1453
1568
  // Fetch all 26 letter buckets
@@ -1468,7 +1583,15 @@ export class CollectionBrowser
1468
1583
  private async updatePrefixFilterCounts(
1469
1584
  filterType: PrefixFilterType
1470
1585
  ): Promise<void> {
1586
+ const { facetFetchQueryKey } = this;
1471
1587
  const buckets = await this.fetchPrefixFilterBuckets(filterType);
1588
+
1589
+ // Don't update the filter counts for an outdated query (if it has been changed
1590
+ // since we sent the request)
1591
+ const queryChangedSinceFetch =
1592
+ facetFetchQueryKey !== this.facetFetchQueryKey;
1593
+ if (queryChangedSinceFetch) return;
1594
+
1472
1595
  // Unpack the aggregation buckets into a simple map like { 'A': 50, 'B': 25, ... }
1473
1596
  this.prefixFilterCountMap = { ...this.prefixFilterCountMap }; // Clone the object to trigger an update
1474
1597
  this.prefixFilterCountMap[filterType] = buckets.reduce(
@@ -1560,6 +1683,9 @@ export class CollectionBrowser
1560
1683
  static styles = css`
1561
1684
  :host {
1562
1685
  display: block;
1686
+
1687
+ --leftColumnWidth: 18rem;
1688
+ --leftColumnPaddingRight: 2.5rem;
1563
1689
  }
1564
1690
 
1565
1691
  /**
@@ -1620,15 +1746,91 @@ export class CollectionBrowser
1620
1746
  }
1621
1747
 
1622
1748
  #left-column {
1623
- width: 18rem;
1624
- min-width: 18rem; /* Prevents Safari from shrinking col at first draw */
1625
- padding-right: 12px;
1626
- padding-right: 2.5rem;
1749
+ width: var(--leftColumnWidth, 18rem);
1750
+ /* Prevents Safari from shrinking col at first draw */
1751
+ min-width: var(--leftColumnWidth, 18rem);
1752
+ padding-top: 0;
1753
+ /* Reduced padding by 0.2rem to add the invisible border in the rule below */
1754
+ padding-right: calc(var(--leftColumnPaddingRight, 2.5rem) - 0.2rem);
1755
+ border-right: 0.2rem solid transparent; /* Pads to the right of the scrollbar a bit */
1627
1756
  z-index: 1;
1628
1757
  }
1629
1758
 
1759
+ .desktop #left-column {
1760
+ top: 0;
1761
+ position: sticky;
1762
+ height: calc(100vh - 2rem);
1763
+ max-height: calc(100vh - 2rem);
1764
+ overflow-x: hidden;
1765
+ overflow-y: scroll;
1766
+
1767
+ /*
1768
+ * Firefox doesn't support any of the -webkit-scrollbar stuff below, but
1769
+ * does at least give us a tiny bit of control over width & color.
1770
+ */
1771
+ scrollbar-width: thin;
1772
+ scrollbar-color: transparent transparent;
1773
+ }
1774
+ .desktop #left-column:hover {
1775
+ scrollbar-color: auto;
1776
+ }
1630
1777
  .desktop #left-column::-webkit-scrollbar {
1631
- display: none;
1778
+ appearance: none;
1779
+ width: 6px;
1780
+ }
1781
+ .desktop #left-column::-webkit-scrollbar-button {
1782
+ height: 3px;
1783
+ background: transparent;
1784
+ }
1785
+ .desktop #left-column::-webkit-scrollbar-corner {
1786
+ background: transparent;
1787
+ }
1788
+ .desktop #left-column::-webkit-scrollbar-thumb {
1789
+ border-radius: 4px;
1790
+ }
1791
+ .desktop #left-column:hover::-webkit-scrollbar-thumb {
1792
+ background: rgba(0, 0, 0, 0.15);
1793
+ }
1794
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:hover {
1795
+ background: rgba(0, 0, 0, 0.2);
1796
+ }
1797
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:active {
1798
+ background: rgba(0, 0, 0, 0.3);
1799
+ }
1800
+
1801
+ #facets-bottom-fade {
1802
+ background: linear-gradient(
1803
+ to bottom,
1804
+ #f5f5f700 0%,
1805
+ #f5f5f7c0 50%,
1806
+ #f5f5f7 80%,
1807
+ #f5f5f7 100%
1808
+ );
1809
+ position: fixed;
1810
+ bottom: 0;
1811
+ height: 50px;
1812
+ /* Wide enough to cover the content, but leave the scrollbar uncovered */
1813
+ width: calc(
1814
+ var(--leftColumnWidth) + var(--leftColumnPaddingRight) - 10px
1815
+ );
1816
+ z-index: 2;
1817
+ pointer-events: none;
1818
+ transition: height 0.1s ease;
1819
+ }
1820
+ #facets-bottom-fade.hidden {
1821
+ height: 0;
1822
+ }
1823
+
1824
+ .desktop #left-column-scroll-sentinel {
1825
+ width: 1px;
1826
+ height: 100vh;
1827
+ background: transparent;
1828
+ }
1829
+
1830
+ .desktop #facets-scroll-sentinel {
1831
+ width: 1px;
1832
+ height: 1px;
1833
+ background: transparent;
1632
1834
  }
1633
1835
 
1634
1836
  .mobile #left-column {
@@ -1636,21 +1838,16 @@ export class CollectionBrowser
1636
1838
  padding: 0;
1637
1839
  }
1638
1840
 
1639
- .desktop #left-column {
1640
- top: 0;
1641
- position: sticky;
1642
- max-height: 100vh;
1643
- overflow: scroll;
1644
- -ms-overflow-style: none; /* hide scrollbar IE and Edge */
1645
- scrollbar-width: none; /* hide scrollbar Firefox */
1646
- }
1647
-
1648
1841
  #mobile-header-container {
1649
1842
  display: flex;
1650
1843
  justify-content: space-between;
1651
1844
  align-items: center;
1652
1845
  }
1653
1846
 
1847
+ .desktop #mobile-header-container {
1848
+ padding-top: 2rem;
1849
+ }
1850
+
1654
1851
  #facets-container {
1655
1852
  position: relative;
1656
1853
  max-height: 0;