@internetarchive/collection-browser 0.4.16-alpha.8 → 0.4.16

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 (63) 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 +262 -116
  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 +41 -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/tiles/grid/item-tile.test.js +124 -3
  36. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  37. package/dist/test/tiles/list/tile-list-compact.test.js +65 -20
  38. package/dist/test/tiles/list/tile-list-compact.test.js.map +1 -1
  39. package/dist/test/tiles/list/tile-list.test.js +106 -4
  40. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  41. package/dist/test/utils/local-date-from-utc.test.d.ts +1 -0
  42. package/dist/test/utils/local-date-from-utc.test.js +27 -0
  43. package/dist/test/utils/local-date-from-utc.test.js.map +1 -0
  44. package/index.html +1 -0
  45. package/package.json +1 -1
  46. package/src/app-root.ts +12 -0
  47. package/src/collection-browser.ts +300 -126
  48. package/src/collection-facets/facets-template.ts +32 -1
  49. package/src/collection-facets/more-facets-content.ts +4 -1
  50. package/src/collection-facets.ts +1 -8
  51. package/src/empty-placeholder.ts +1 -0
  52. package/src/models.ts +6 -0
  53. package/src/tiles/grid/item-tile.ts +11 -4
  54. package/src/tiles/list/tile-list-compact.ts +16 -2
  55. package/src/tiles/list/tile-list.ts +12 -5
  56. package/src/utils/format-date.ts +4 -0
  57. package/src/utils/local-date-from-utc.ts +15 -0
  58. package/test/collection-browser.test.ts +57 -12
  59. package/test/collection-facets/facets-template.test.ts +98 -0
  60. package/test/tiles/grid/item-tile.test.ts +145 -3
  61. package/test/tiles/list/tile-list-compact.test.ts +70 -19
  62. package/test/tiles/list/tile-list.test.ts +118 -4
  63. 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}
@@ -469,7 +470,6 @@ export class CollectionBrowser
469
470
  }
470
471
 
471
472
  private selectedSortChanged(): void {
472
- console.log('selectedSortChanged');
473
473
  if (this.selectedSort === 'relevance') {
474
474
  this.sortParam = null;
475
475
  return;
@@ -481,7 +481,6 @@ export class CollectionBrowser
481
481
  this.sortParam = { field: sortField, direction: this.sortDirection };
482
482
 
483
483
  // Lazy-load the alphabet counts for title/creator sort bar as needed
484
- console.log('will update prefix filters for current sort');
485
484
  this.updatePrefixFiltersForCurrentSort();
486
485
  }
487
486
 
@@ -499,9 +498,30 @@ export class CollectionBrowser
499
498
  }
500
499
  }
501
500
 
502
- /** 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
503
523
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
504
- * */
524
+ */
505
525
  private sendFilterByTitleAnalytics(prevSelectedLetter: string | null): void {
506
526
  if (!prevSelectedLetter && !this.selectedTitleFilter) {
507
527
  return;
@@ -517,15 +537,10 @@ export class CollectionBrowser
517
537
  });
518
538
  }
519
539
 
520
- private selectedTitleLetterChanged(): void {
521
- this.titleQuery = this.selectedTitleFilter
522
- ? `firstTitle:${this.selectedTitleFilter}`
523
- : undefined;
524
- }
525
-
526
- /** Send Analytics when filtering by creator's first letter
540
+ /**
541
+ * Send Analytics when filtering by creator's first letter
527
542
  * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
528
- * */
543
+ */
529
544
  private sendFilterByCreatorAnalytics(
530
545
  prevSelectedLetter: string | null
531
546
  ): void {
@@ -543,26 +558,24 @@ export class CollectionBrowser
543
558
  });
544
559
  }
545
560
 
546
- private selectedCreatorLetterChanged(): void {
547
- this.creatorQuery = this.selectedCreatorFilter
548
- ? `firstCreator:${this.selectedCreatorFilter}`
549
- : undefined;
550
- }
551
-
561
+ /**
562
+ * Handler for changes to which letter is selected in the title alphabet bar.
563
+ */
552
564
  private titleLetterSelected(
553
565
  e: CustomEvent<{ selectedLetter: string | null }>
554
566
  ): void {
555
567
  this.selectedCreatorFilter = null;
556
568
  this.selectedTitleFilter = e.detail.selectedLetter;
557
- this.selectedTitleLetterChanged();
558
569
  }
559
570
 
571
+ /**
572
+ * Handler for changes to which letter is selected in the creator alphabet bar.
573
+ */
560
574
  private creatorLetterSelected(
561
575
  e: CustomEvent<{ selectedLetter: string | null }>
562
576
  ): void {
563
577
  this.selectedTitleFilter = null;
564
578
  this.selectedCreatorFilter = e.detail.selectedLetter;
565
- this.selectedCreatorLetterChanged();
566
579
  }
567
580
 
568
581
  private get mobileFacetsTemplate() {
@@ -605,13 +618,13 @@ export class CollectionBrowser
605
618
  .selectedFacets=${this.selectedFacets}
606
619
  .collectionNameCache=${this.collectionNameCache}
607
620
  .showHistogramDatePicker=${this.showHistogramDatePicker}
608
- .query=${this.filteredQuery}
621
+ .query=${this.baseQuery}
609
622
  .filterMap=${this.filterMap}
610
623
  .modalManager=${this.modalManager}
611
624
  ?collapsableFacets=${this.mobileView}
612
625
  ?facetsLoading=${this.facetsLoading}
613
626
  ?fullYearAggregationLoading=${this.facetsLoading}
614
- .onFacetClick=${this.facetClickHandler}
627
+ @facetClick=${this.facetClickHandler}
615
628
  .analyticsHandler=${this.analyticsHandler}
616
629
  >
617
630
  </collection-facets>
@@ -671,7 +684,16 @@ export class CollectionBrowser
671
684
  }
672
685
 
673
686
  updated(changed: PropertyValues) {
674
- console.log('updated', changed, this.baseQuery, JSON.stringify(this.selectedFacets));
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
+
675
697
  if (
676
698
  changed.has('displayMode') ||
677
699
  changed.has('baseNavigationUrl') ||
@@ -732,13 +754,11 @@ export class CollectionBrowser
732
754
  this.sendFilterByTitleAnalytics(
733
755
  changed.get('selectedTitleFilter') as string
734
756
  );
735
- this.selectedTitleLetterChanged();
736
757
  }
737
758
  if (changed.has('selectedCreatorFilter')) {
738
759
  this.sendFilterByCreatorAnalytics(
739
760
  changed.get('selectedCreatorFilter') as string
740
761
  );
741
- this.selectedCreatorLetterChanged();
742
762
  }
743
763
 
744
764
  if (
@@ -777,6 +797,10 @@ export class CollectionBrowser
777
797
  if (this.boundNavigationHandler) {
778
798
  window.removeEventListener('popstate', this.boundNavigationHandler);
779
799
  }
800
+
801
+ this.leftColIntersectionObserver?.disconnect();
802
+ this.facetsIntersectionObserver?.disconnect();
803
+ window.removeEventListener('resize', this.updateLeftColumnHeight);
780
804
  }
781
805
 
782
806
  handleResize(entry: ResizeObserverEntry): void {
@@ -788,8 +812,80 @@ export class CollectionBrowser
788
812
  this.isResizeToMobile = true;
789
813
  }
790
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);
791
844
  }
792
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
+ }
861
+ }
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
+
793
889
  private emitBaseQueryChanged() {
794
890
  this.dispatchEvent(
795
891
  new CustomEvent<{ baseQuery?: string }>('baseQueryChanged', {
@@ -955,6 +1051,11 @@ export class CollectionBrowser
955
1051
  this.searchResultsLoading = false;
956
1052
  }
957
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
+ */
958
1059
  private get filterMap(): FilterMap {
959
1060
  const builder = new FilterMapBuilder();
960
1061
 
@@ -998,21 +1099,24 @@ export class CollectionBrowser
998
1099
  }
999
1100
  }
1000
1101
 
1001
- const filterMap = builder.build();
1002
- return filterMap;
1003
- }
1004
-
1005
- /** The base query joined with any title/creator letter filters */
1006
- private get filteredQuery(): string | undefined {
1007
- if (!this.baseQuery) return undefined;
1008
- let filteredQuery = this.baseQuery.trim();
1009
-
1010
- const { sortFilterQueries } = this;
1011
- if (sortFilterQueries) {
1012
- 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
+ );
1013
1116
  }
1014
1117
 
1015
- return filteredQuery.trim();
1118
+ const filterMap = builder.build();
1119
+ return filterMap;
1016
1120
  }
1017
1121
 
1018
1122
  /** The full query, including year facets and date range clauses */
@@ -1034,22 +1138,6 @@ export class CollectionBrowser
1034
1138
  return fullQuery.trim();
1035
1139
  }
1036
1140
 
1037
- /** The full query without any title/creator letter filters */
1038
- private get fullQueryWithoutAlphaFilters(): string | undefined {
1039
- if (!this.baseQuery) return undefined;
1040
- let fullQuery = this.baseQuery.trim();
1041
-
1042
- const { facetQuery, dateRangeQueryClause } = this;
1043
-
1044
- if (facetQuery) {
1045
- fullQuery += ` AND ${facetQuery}`;
1046
- }
1047
- if (dateRangeQueryClause) {
1048
- fullQuery += ` AND ${dateRangeQueryClause}`;
1049
- }
1050
- return fullQuery.trim();
1051
- }
1052
-
1053
1141
  /**
1054
1142
  * Generates a query string for the given facets
1055
1143
  *
@@ -1140,35 +1228,39 @@ export class CollectionBrowser
1140
1228
  this.selectedFacets = e.detail;
1141
1229
  }
1142
1230
 
1143
- facetClickHandler(
1144
- name: FacetOption,
1145
- facetSelected: boolean,
1146
- negative: boolean
1147
- ): void {
1231
+ facetClickHandler({
1232
+ detail: { key, state: facetState, negative },
1233
+ }: CustomEvent<FacetEventDetails>): void {
1148
1234
  if (negative) {
1149
1235
  this.analyticsHandler?.sendEvent({
1150
1236
  category: this.searchContext,
1151
- action: facetSelected
1152
- ? analyticsActions.facetNegativeSelected
1153
- : analyticsActions.facetNegativeDeselected,
1154
- label: name,
1237
+ action:
1238
+ facetState !== 'none'
1239
+ ? analyticsActions.facetNegativeSelected
1240
+ : analyticsActions.facetNegativeDeselected,
1241
+ label: key,
1155
1242
  });
1156
1243
  } else {
1157
1244
  this.analyticsHandler?.sendEvent({
1158
1245
  category: this.searchContext,
1159
- action: facetSelected
1160
- ? analyticsActions.facetSelected
1161
- : analyticsActions.facetDeselected,
1162
- label: name,
1246
+ action:
1247
+ facetState !== 'none'
1248
+ ? analyticsActions.facetSelected
1249
+ : analyticsActions.facetDeselected,
1250
+ label: key,
1163
1251
  });
1164
1252
  }
1165
1253
  }
1166
1254
 
1167
1255
  private async fetchFacets() {
1168
- if (!this.filteredQuery) return;
1256
+ const trimmedQuery = this.baseQuery?.trim();
1257
+ if (!trimmedQuery) return;
1258
+ if (!this.searchService) return;
1259
+
1260
+ const { facetFetchQueryKey } = this;
1169
1261
 
1170
1262
  const params: SearchParams = {
1171
- query: this.filteredQuery,
1263
+ query: trimmedQuery,
1172
1264
  rows: 0,
1173
1265
  filters: this.filterMap,
1174
1266
  // Fetch a few extra buckets beyond the 6 we show, in case some get suppressed
@@ -1179,12 +1271,18 @@ export class CollectionBrowser
1179
1271
  };
1180
1272
 
1181
1273
  this.facetsLoading = true;
1182
- const searchResponse = await this.searchService?.search(
1274
+ const searchResponse = await this.searchService.search(
1183
1275
  params,
1184
1276
  this.searchType
1185
1277
  );
1186
1278
  const success = searchResponse?.success;
1187
- 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;
1188
1286
 
1189
1287
  if (!success) {
1190
1288
  const errorMsg = searchResponse?.error?.message;
@@ -1201,17 +1299,12 @@ export class CollectionBrowser
1201
1299
  return;
1202
1300
  }
1203
1301
 
1204
- // This is checking to see if the query has changed since the data was fetched.
1205
- // If so, we just want to discard this set of aggregations because they are
1206
- // likely no longer valid for the newer query.
1207
- const returnedUid = (success.request.clientParameters as any).uid;
1208
- const queryChangedSinceFetch = returnedUid !== this.facetFetchQueryKey;
1209
- if (queryChangedSinceFetch) return;
1210
-
1211
1302
  this.aggregations = success?.response.aggregations;
1212
1303
 
1213
1304
  this.fullYearsHistogramAggregation =
1214
1305
  success?.response?.aggregations?.year_histogram;
1306
+
1307
+ this.facetsLoading = false;
1215
1308
  }
1216
1309
 
1217
1310
  private scrollToPage(pageNumber: number): Promise<void> {
@@ -1266,7 +1359,9 @@ export class CollectionBrowser
1266
1359
  private pageFetchesInProgress: Record<string, Set<number>> = {};
1267
1360
 
1268
1361
  async fetchPage(pageNumber: number) {
1269
- if (!this.filteredQuery) return;
1362
+ const trimmedQuery = this.baseQuery?.trim();
1363
+ if (!trimmedQuery) return;
1364
+ if (!this.searchService) return;
1270
1365
 
1271
1366
  // if we already have data, don't fetch again
1272
1367
  if (this.dataSource[pageNumber]) return;
@@ -1283,7 +1378,7 @@ export class CollectionBrowser
1283
1378
 
1284
1379
  const sortParams = this.sortParam ? [this.sortParam] : [];
1285
1380
  const params: SearchParams = {
1286
- query: this.filteredQuery,
1381
+ query: trimmedQuery,
1287
1382
  page: pageNumber,
1288
1383
  rows: this.pageSize,
1289
1384
  sort: sortParams,
@@ -1291,12 +1386,18 @@ export class CollectionBrowser
1291
1386
  aggregations: { omit: true },
1292
1387
  uid: this.pageFetchQueryKey,
1293
1388
  };
1294
- const searchResponse = await this.searchService?.search(
1389
+ const searchResponse = await this.searchService.search(
1295
1390
  params,
1296
1391
  this.searchType
1297
1392
  );
1298
1393
  const success = searchResponse?.success;
1299
1394
 
1395
+ // This is checking to see if the query has changed since the data was fetched.
1396
+ // If so, we just want to discard the data since there should be a new query
1397
+ // right behind it.
1398
+ const queryChangedSinceFetch = pageFetchQueryKey !== this.pageFetchQueryKey;
1399
+ if (queryChangedSinceFetch) return;
1400
+
1300
1401
  if (!success) {
1301
1402
  const errorMsg = searchResponse?.error?.message;
1302
1403
  const detailMsg = searchResponse?.error?.details?.message;
@@ -1311,16 +1412,11 @@ export class CollectionBrowser
1311
1412
  window?.Sentry?.captureMessage?.(this.queryErrorMessage, 'error');
1312
1413
  }
1313
1414
 
1415
+ this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1416
+ this.searchResultsLoading = false;
1314
1417
  return;
1315
1418
  }
1316
1419
 
1317
- // This is checking to see if the query has changed since the data was fetched.
1318
- // If so, we just want to discard the data since there should be a new query
1319
- // right behind it.
1320
- const returnedUid = (success.request.clientParameters as any).uid;
1321
- const queryChangedSinceFetch = returnedUid !== this.pageFetchQueryKey;
1322
- if (queryChangedSinceFetch) return;
1323
-
1324
1420
  this.totalResults = success.response.totalResults;
1325
1421
 
1326
1422
  const { results } = success.response;
@@ -1338,6 +1434,7 @@ export class CollectionBrowser
1338
1434
  this.infiniteScroller.itemCount = this.totalResults;
1339
1435
  }
1340
1436
  }
1437
+
1341
1438
  this.pageFetchesInProgress[pageFetchQueryKey]?.delete(pageNumber);
1342
1439
  this.searchResultsLoading = false;
1343
1440
  }
@@ -1443,24 +1540,25 @@ export class CollectionBrowser
1443
1540
  private async fetchPrefixFilterBuckets(
1444
1541
  filterType: PrefixFilterType
1445
1542
  ): Promise<Bucket[]> {
1446
- console.log('fetchPrefixFilterBuckets', this.fullQueryWithoutAlphaFilters);
1447
- if (!this.fullQueryWithoutAlphaFilters) return [];
1543
+ const trimmedQuery = this.baseQuery?.trim();
1544
+ if (!trimmedQuery) return [];
1448
1545
 
1449
1546
  const filterAggregationKey = prefixFilterAggregationKeys[filterType];
1450
1547
  const params: SearchParams = {
1451
- query: this.fullQueryWithoutAlphaFilters,
1548
+ query: trimmedQuery,
1452
1549
  rows: 0,
1550
+ filters: this.filterMap,
1453
1551
  // Only fetch the firstTitle or firstCreator aggregation
1454
1552
  aggregations: { simpleParams: [filterAggregationKey] },
1455
1553
  // Fetch all 26 letter buckets
1456
1554
  aggregationsSize: 26,
1457
1555
  };
1458
- console.log('sending prefix filter search request');
1556
+
1459
1557
  const searchResponse = await this.searchService?.search(
1460
1558
  params,
1461
1559
  this.searchType
1462
1560
  );
1463
- console.log('response', searchResponse);
1561
+
1464
1562
  return (searchResponse?.success?.response?.aggregations?.[
1465
1563
  filterAggregationKey
1466
1564
  ]?.buckets ?? []) as Bucket[];
@@ -1470,9 +1568,15 @@ export class CollectionBrowser
1470
1568
  private async updatePrefixFilterCounts(
1471
1569
  filterType: PrefixFilterType
1472
1570
  ): Promise<void> {
1473
- console.log('updatePrefixFilterCounts');
1571
+ const { facetFetchQueryKey } = this;
1474
1572
  const buckets = await this.fetchPrefixFilterBuckets(filterType);
1475
- console.log('buckets', buckets);
1573
+
1574
+ // Don't update the filter counts for an outdated query (if it has been changed
1575
+ // since we sent the request)
1576
+ const queryChangedSinceFetch =
1577
+ facetFetchQueryKey !== this.facetFetchQueryKey;
1578
+ if (queryChangedSinceFetch) return;
1579
+
1476
1580
  // Unpack the aggregation buckets into a simple map like { 'A': 50, 'B': 25, ... }
1477
1581
  this.prefixFilterCountMap = { ...this.prefixFilterCountMap }; // Clone the object to trigger an update
1478
1582
  this.prefixFilterCountMap[filterType] = buckets.reduce(
@@ -1489,12 +1593,9 @@ export class CollectionBrowser
1489
1593
  * provided it is one that permits prefix filtering. (If not, this does nothing).
1490
1594
  */
1491
1595
  private async updatePrefixFiltersForCurrentSort(): Promise<void> {
1492
- console.log('updatePrefixFiltersForCurrentSort');
1493
1596
  if (['title', 'creator'].includes(this.selectedSort)) {
1494
- console.log('sort is title or creator - will update')
1495
1597
  const filterType = this.selectedSort as PrefixFilterType;
1496
1598
  if (!this.prefixFilterCountMap[filterType]) {
1497
- console.log('need new filters, updating');
1498
1599
  this.updatePrefixFilterCounts(filterType);
1499
1600
  }
1500
1601
  }
@@ -1508,7 +1609,6 @@ export class CollectionBrowser
1508
1609
  * Call this whenever the counts are invalidated (e.g., by a query change).
1509
1610
  */
1510
1611
  private refreshLetterCounts(): void {
1511
- console.log('refreshLetterCounts');
1512
1612
  if (Object.keys(this.prefixFilterCountMap).length > 0) {
1513
1613
  this.prefixFilterCountMap = {};
1514
1614
  }
@@ -1568,6 +1668,9 @@ export class CollectionBrowser
1568
1668
  static styles = css`
1569
1669
  :host {
1570
1670
  display: block;
1671
+
1672
+ --leftColumnWidth: 18rem;
1673
+ --leftColumnPaddingRight: 2.5rem;
1571
1674
  }
1572
1675
 
1573
1676
  /**
@@ -1628,15 +1731,91 @@ export class CollectionBrowser
1628
1731
  }
1629
1732
 
1630
1733
  #left-column {
1631
- width: 18rem;
1632
- min-width: 18rem; /* Prevents Safari from shrinking col at first draw */
1633
- padding-right: 12px;
1634
- padding-right: 2.5rem;
1734
+ width: var(--leftColumnWidth, 18rem);
1735
+ /* Prevents Safari from shrinking col at first draw */
1736
+ min-width: var(--leftColumnWidth, 18rem);
1737
+ padding-top: 0;
1738
+ /* Reduced padding by 0.2rem to add the invisible border in the rule below */
1739
+ padding-right: calc(var(--leftColumnPaddingRight, 2.5rem) - 0.2rem);
1740
+ border-right: 0.2rem solid transparent; /* Pads to the right of the scrollbar a bit */
1635
1741
  z-index: 1;
1636
1742
  }
1637
1743
 
1744
+ .desktop #left-column {
1745
+ top: 0;
1746
+ position: sticky;
1747
+ height: calc(100vh - 2rem);
1748
+ max-height: calc(100vh - 2rem);
1749
+ overflow-x: hidden;
1750
+ overflow-y: scroll;
1751
+
1752
+ /*
1753
+ * Firefox doesn't support any of the -webkit-scrollbar stuff below, but
1754
+ * does at least give us a tiny bit of control over width & color.
1755
+ */
1756
+ scrollbar-width: thin;
1757
+ scrollbar-color: transparent transparent;
1758
+ }
1759
+ .desktop #left-column:hover {
1760
+ scrollbar-color: auto;
1761
+ }
1638
1762
  .desktop #left-column::-webkit-scrollbar {
1639
- display: none;
1763
+ appearance: none;
1764
+ width: 6px;
1765
+ }
1766
+ .desktop #left-column::-webkit-scrollbar-button {
1767
+ height: 3px;
1768
+ background: transparent;
1769
+ }
1770
+ .desktop #left-column::-webkit-scrollbar-corner {
1771
+ background: transparent;
1772
+ }
1773
+ .desktop #left-column::-webkit-scrollbar-thumb {
1774
+ border-radius: 4px;
1775
+ }
1776
+ .desktop #left-column:hover::-webkit-scrollbar-thumb {
1777
+ background: rgba(0, 0, 0, 0.15);
1778
+ }
1779
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:hover {
1780
+ background: rgba(0, 0, 0, 0.2);
1781
+ }
1782
+ .desktop #left-column:hover::-webkit-scrollbar-thumb:active {
1783
+ background: rgba(0, 0, 0, 0.3);
1784
+ }
1785
+
1786
+ #facets-bottom-fade {
1787
+ background: linear-gradient(
1788
+ to bottom,
1789
+ #f5f5f700 0%,
1790
+ #f5f5f7c0 50%,
1791
+ #f5f5f7 80%,
1792
+ #f5f5f7 100%
1793
+ );
1794
+ position: fixed;
1795
+ bottom: 0;
1796
+ height: 50px;
1797
+ /* Wide enough to cover the content, but leave the scrollbar uncovered */
1798
+ width: calc(
1799
+ var(--leftColumnWidth) + var(--leftColumnPaddingRight) - 10px
1800
+ );
1801
+ z-index: 2;
1802
+ pointer-events: none;
1803
+ transition: height 0.1s ease;
1804
+ }
1805
+ #facets-bottom-fade.hidden {
1806
+ height: 0;
1807
+ }
1808
+
1809
+ .desktop #left-column-scroll-sentinel {
1810
+ width: 1px;
1811
+ height: 100vh;
1812
+ background: transparent;
1813
+ }
1814
+
1815
+ .desktop #facets-scroll-sentinel {
1816
+ width: 1px;
1817
+ height: 1px;
1818
+ background: transparent;
1640
1819
  }
1641
1820
 
1642
1821
  .mobile #left-column {
@@ -1644,21 +1823,16 @@ export class CollectionBrowser
1644
1823
  padding: 0;
1645
1824
  }
1646
1825
 
1647
- .desktop #left-column {
1648
- top: 0;
1649
- position: sticky;
1650
- max-height: 100vh;
1651
- overflow: scroll;
1652
- -ms-overflow-style: none; /* hide scrollbar IE and Edge */
1653
- scrollbar-width: none; /* hide scrollbar Firefox */
1654
- }
1655
-
1656
1826
  #mobile-header-container {
1657
1827
  display: flex;
1658
1828
  justify-content: space-between;
1659
1829
  align-items: center;
1660
1830
  }
1661
1831
 
1832
+ .desktop #mobile-header-container {
1833
+ padding-top: 2rem;
1834
+ }
1835
+
1662
1836
  #facets-container {
1663
1837
  position: relative;
1664
1838
  max-height: 0;