@internetarchive/collection-browser 0.2.18 → 0.2.20-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/src/app-root.d.ts +6 -0
  2. package/dist/src/app-root.js +94 -1
  3. package/dist/src/app-root.js.map +1 -1
  4. package/dist/src/collection-browser.d.ts +14 -1
  5. package/dist/src/collection-browser.js +133 -20
  6. package/dist/src/collection-browser.js.map +1 -1
  7. package/dist/src/collection-facets.d.ts +2 -0
  8. package/dist/src/collection-facets.js +9 -0
  9. package/dist/src/collection-facets.js.map +1 -1
  10. package/dist/src/models.d.ts +2 -0
  11. package/dist/src/models.js +8 -0
  12. package/dist/src/models.js.map +1 -1
  13. package/dist/src/tiles/grid/item-tile.d.ts +5 -2
  14. package/dist/src/tiles/grid/item-tile.js +28 -2
  15. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  16. package/dist/src/tiles/image-block.js +1 -1
  17. package/dist/src/tiles/image-block.js.map +1 -1
  18. package/dist/src/tiles/list/tile-list.d.ts +2 -0
  19. package/dist/src/tiles/list/tile-list.js +14 -1
  20. package/dist/src/tiles/list/tile-list.js.map +1 -1
  21. package/dist/src/tiles/text-snippet-block.d.ts +29 -0
  22. package/dist/src/tiles/text-snippet-block.js +128 -0
  23. package/dist/src/tiles/text-snippet-block.js.map +1 -0
  24. package/dist/src/tiles/tile-dispatcher.js +1 -0
  25. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  26. package/dist/src/utils/analytics-events.d.ts +18 -0
  27. package/dist/src/utils/analytics-events.js +21 -0
  28. package/dist/src/utils/analytics-events.js.map +1 -0
  29. package/dist/test/collection-browser.test.js +91 -5
  30. package/dist/test/collection-browser.test.js.map +1 -1
  31. package/dist/test/mocks/mock-analytics-handler.d.ts +10 -0
  32. package/dist/test/mocks/mock-analytics-handler.js +16 -0
  33. package/dist/test/mocks/mock-analytics-handler.js.map +1 -0
  34. package/dist/test/mocks/mock-search-responses.d.ts +2 -1
  35. package/dist/test/mocks/mock-search-responses.js +27 -1
  36. package/dist/test/mocks/mock-search-responses.js.map +1 -1
  37. package/dist/test/mocks/mock-search-service.js +6 -2
  38. package/dist/test/mocks/mock-search-service.js.map +1 -1
  39. package/dist/test/text-snippet-block.test.d.ts +1 -0
  40. package/dist/test/text-snippet-block.test.js +52 -0
  41. package/dist/test/text-snippet-block.test.js.map +1 -0
  42. package/dist/test/tiles/grid/item-tile.test.js +15 -0
  43. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  44. package/dist/test/tiles/list/tile-list.test.d.ts +1 -0
  45. package/dist/test/tiles/list/tile-list.test.js +42 -0
  46. package/dist/test/tiles/list/tile-list.test.js.map +1 -0
  47. package/package.json +4 -3
  48. package/src/app-root.ts +103 -2
  49. package/src/collection-browser.ts +153 -17
  50. package/src/collection-facets.ts +13 -1
  51. package/src/models.ts +10 -0
  52. package/src/tiles/grid/item-tile.ts +35 -2
  53. package/src/tiles/image-block.ts +1 -1
  54. package/src/tiles/list/tile-list.ts +14 -1
  55. package/src/tiles/text-snippet-block.ts +130 -0
  56. package/src/tiles/tile-dispatcher.ts +4 -0
  57. package/src/utils/analytics-events.ts +19 -0
  58. package/test/collection-browser.test.ts +131 -4
  59. package/test/mocks/mock-analytics-handler.ts +30 -0
  60. package/test/mocks/mock-search-responses.ts +34 -1
  61. package/test/mocks/mock-search-service.ts +10 -2
  62. package/test/text-snippet-block.test.ts +69 -0
  63. package/test/tiles/grid/item-tile.test.ts +19 -0
  64. package/test/tiles/list/tile-list.test.ts +51 -0
package/src/app-root.ts CHANGED
@@ -1,10 +1,17 @@
1
+ import {
2
+ AnalyticsEvent,
3
+ AnalyticsManager,
4
+ } from '@internetarchive/analytics-manager';
1
5
  import { SearchService } from '@internetarchive/search-service';
2
6
  import { LocalCache } from '@internetarchive/local-cache';
3
7
  import { html, css, LitElement, PropertyValues } from 'lit';
4
- import { customElement, query, state } from 'lit/decorators.js';
8
+ import { customElement, property, query, state } from 'lit/decorators.js';
5
9
  import { SharedResizeObserver } from '@internetarchive/shared-resize-observer';
6
10
  import { CollectionNameCache } from '@internetarchive/collection-name-cache';
11
+
12
+ import type { AnalyticsManagerInterface } from '@internetarchive/analytics-manager';
7
13
  import type { CollectionBrowser } from '../src/collection-browser';
14
+
8
15
  import '../src/collection-browser';
9
16
 
10
17
  @customElement('app-root')
@@ -34,12 +41,28 @@ export class AppRoot extends LitElement {
34
41
 
35
42
  @state() private loggedIn: boolean = false;
36
43
 
44
+ @property({ type: Object, reflect: false }) latestAction?: AnalyticsEvent;
45
+
37
46
  @query('#base-query-field') private baseQueryField!: HTMLInputElement;
38
47
 
39
48
  @query('#page-number-input') private pageNumberInput!: HTMLInputElement;
40
49
 
41
50
  @query('collection-browser') private collectionBrowser!: CollectionBrowser;
42
51
 
52
+ private analyticsManager = new AnalyticsManager();
53
+
54
+ private analyticsHandler: AnalyticsManagerInterface = {
55
+ sendPing: this.sendAnalytics.bind(this),
56
+ sendEvent: this.sendAnalytics.bind(this),
57
+ sendEventNoSampling: this.sendAnalytics.bind(this),
58
+ };
59
+
60
+ private sendAnalytics(ae: AnalyticsEvent) {
61
+ console.log('Analytics Received ----', ae);
62
+ this.latestAction = ae;
63
+ this.analyticsManager?.sendEventNoSampling(ae);
64
+ }
65
+
43
66
  private searchPressed(e: Event) {
44
67
  e.preventDefault();
45
68
  this.searchQuery = this.baseQueryField.value;
@@ -54,7 +77,7 @@ export class AppRoot extends LitElement {
54
77
  this.collectionBrowser.goToPage(this.currentPage);
55
78
  }
56
79
 
57
- protected updated(changed: PropertyValues): void {
80
+ protected override updated(changed: PropertyValues): void {
58
81
  if (changed.has('currentPage') && this.currentPage) {
59
82
  this.pageNumberInput.value = this.currentPage.toString();
60
83
  }
@@ -86,6 +109,23 @@ export class AppRoot extends LitElement {
86
109
  <input type="submit" value="Go" />
87
110
  </form>
88
111
 
112
+ <div id="last-event">
113
+ <button
114
+ @click=${() => {
115
+ const details = this.shadowRoot?.getElementById(
116
+ 'latest-event-details'
117
+ );
118
+ details?.classList.toggle('hidden');
119
+ }}
120
+ >
121
+ Last Event Captured
122
+ </button>
123
+ <pre id="latest-event-details">
124
+ ${JSON.stringify(this.latestAction, null, 2)}
125
+ </pre
126
+ >
127
+ </div>
128
+
89
129
  <div id="cell-controls">
90
130
  <div id="cell-size-control">
91
131
  <div>
@@ -130,6 +170,14 @@ export class AppRoot extends LitElement {
130
170
  @click=${this.loginChanged}
131
171
  />
132
172
  </div>
173
+ <div>
174
+ <label for="show-dummy-snippets">Show dummy snippets:</label>
175
+ <input
176
+ type="checkbox"
177
+ id="show-dummy-snippets"
178
+ @click=${this.snippetsChanged}
179
+ />
180
+ </div>
133
181
  </div>
134
182
  <div id="cell-gap-control">
135
183
  <div>
@@ -171,6 +219,7 @@ export class AppRoot extends LitElement {
171
219
  .collectionNameCache=${this.collectionNameCache}
172
220
  .showHistogramDatePicker=${true}
173
221
  .loggedIn=${this.loggedIn}
222
+ .analyticsHandler=${this.analyticsHandler}
174
223
  @visiblePageChanged=${this.visiblePageChanged}
175
224
  @baseQueryChanged=${this.baseQueryChanged}
176
225
  >
@@ -206,6 +255,48 @@ export class AppRoot extends LitElement {
206
255
  }
207
256
  }
208
257
 
258
+ private async snippetsChanged(e: Event) {
259
+ const target = e.target as HTMLInputElement;
260
+ if (target.checked) {
261
+ // Decorate the default search service with a wrapper that adds
262
+ // dummy snippets to any successful searches
263
+ this.searchService = {
264
+ ...SearchService.default,
265
+ async search(params) {
266
+ const result = await SearchService.default.search(params);
267
+ result.success?.response.docs.forEach(doc => {
268
+ const metadata = doc.rawMetadata;
269
+ if (metadata) {
270
+ metadata.snippets = [
271
+ 'this is a text {{{snippet}}} block with potentially',
272
+ 'multiple {{{snippets}}} and such',
273
+ 'but the {{{snippet}}} block may be quite long perhaps',
274
+ 'depending on how many {{{snippet}}} matches there are',
275
+ 'there may be multiple lines of {{{snippets}}} to show',
276
+ 'but each {{{snippet}}} should be relatively short',
277
+ 'and {{{snippets}}} are each a {{{snippet}}} of text',
278
+ 'but every {{{snippet}}} might have multiple matches',
279
+ 'the {{{snippets}}} should be separated and surrounded by ellipses',
280
+ ];
281
+ }
282
+ });
283
+ return result;
284
+ },
285
+ };
286
+ } else {
287
+ // Restore the default seach service
288
+ this.searchService = SearchService.default;
289
+ }
290
+
291
+ // Re-perform the current search to show/hide the snippets immediately
292
+ const oldQuery = this.searchQuery;
293
+ this.searchQuery = ''; // Should just reset to the placeholder
294
+ await this.updateComplete;
295
+ // For unclear reasons, Safari refuses to re-apply the old query until the next tick, hence:
296
+ await new Promise(res => setTimeout(res, 0));
297
+ this.searchQuery = oldQuery; // Re-apply the original query
298
+ }
299
+
209
300
  private rowGapChanged(e: Event) {
210
301
  const input = e.target as HTMLInputElement;
211
302
  this.rowGap = parseFloat(input.value);
@@ -293,5 +384,15 @@ export class AppRoot extends LitElement {
293
384
  #cell-gap-control {
294
385
  margin-left: 1rem;
295
386
  }
387
+
388
+ #last-event {
389
+ background-color: aliceblue;
390
+ padding: 5px;
391
+ margin: 5px auto;
392
+ }
393
+
394
+ .hidden {
395
+ display: none;
396
+ }
296
397
  `;
297
398
  }
@@ -9,6 +9,8 @@ import {
9
9
  } from 'lit';
10
10
  import { customElement, property, query, state } from 'lit/decorators.js';
11
11
  import { ifDefined } from 'lit/directives/if-defined.js';
12
+
13
+ import type { AnalyticsManagerInterface } from '@internetarchive/analytics-manager';
12
14
  import type {
13
15
  InfiniteScroller,
14
16
  InfiniteScrollerCellProviderInterface,
@@ -41,6 +43,7 @@ import {
41
43
  defaultSelectedFacets,
42
44
  TileModel,
43
45
  CollectionDisplayMode,
46
+ FacetOption,
44
47
  } from './models';
45
48
  import {
46
49
  RestorationStateHandlerInterface,
@@ -52,6 +55,11 @@ import { LanguageCodeHandler } from './language-code-handler/language-code-handl
52
55
  import type { PlaceholderType } from './empty-placeholder';
53
56
  import './empty-placeholder';
54
57
 
58
+ import {
59
+ analyticsActions,
60
+ analyticsCategories,
61
+ } from './utils/analytics-events';
62
+
55
63
  @customElement('collection-browser')
56
64
  export class CollectionBrowser
57
65
  extends LitElement
@@ -155,6 +163,9 @@ export class CollectionBrowser
155
163
 
156
164
  private languageCodeHandler = new LanguageCodeHandler();
157
165
 
166
+ @property({ type: Object, attribute: false })
167
+ private analyticsHandler?: AnalyticsManagerInterface;
168
+
158
169
  /**
159
170
  * When we're animated scrolling to the page, we don't want to fetch
160
171
  * all of the pages as it scrolls so this lets us know if we're scrolling
@@ -295,7 +306,9 @@ export class CollectionBrowser
295
306
  ? this.totalResults.toLocaleString()
296
307
  : '-'}
297
308
  </span>
298
- <span id="big-results-label">Results</span>
309
+ <span id="big-results-label">
310
+ ${this.totalResults === 1 ? 'Result' : 'Results'}
311
+ </span>
299
312
  </div>
300
313
  </div>
301
314
  <div
@@ -328,18 +341,21 @@ export class CollectionBrowser
328
341
  }
329
342
 
330
343
  private get sortFilterBarTemplate() {
331
- return html`<sort-filter-bar
332
- .selectedSort=${this.selectedSort}
333
- .sortDirection=${this.sortDirection}
334
- .displayMode=${this.displayMode}
335
- .selectedTitleFilter=${this.selectedTitleFilter}
336
- .selectedCreatorFilter=${this.selectedCreatorFilter}
337
- .resizeObserver=${this.resizeObserver}
338
- @sortChanged=${this.userChangedSort}
339
- @displayModeChanged=${this.displayModeChanged}
340
- @titleLetterChanged=${this.titleLetterSelected}
341
- @creatorLetterChanged=${this.creatorLetterSelected}
342
- ></sort-filter-bar>`;
344
+ return html`
345
+ <sort-filter-bar
346
+ .selectedSort=${this.selectedSort}
347
+ .sortDirection=${this.sortDirection}
348
+ .displayMode=${this.displayMode}
349
+ .selectedTitleFilter=${this.selectedTitleFilter}
350
+ .selectedCreatorFilter=${this.selectedCreatorFilter}
351
+ .resizeObserver=${this.resizeObserver}
352
+ @sortChanged=${this.userChangedSort}
353
+ @displayModeChanged=${this.displayModeChanged}
354
+ @titleLetterChanged=${this.titleLetterSelected}
355
+ @creatorLetterChanged=${this.creatorLetterSelected}
356
+ >
357
+ </sort-filter-bar>
358
+ `;
343
359
  }
344
360
 
345
361
  private userChangedSort(
@@ -358,12 +374,25 @@ export class CollectionBrowser
358
374
  this.currentPage = 1;
359
375
  }
360
376
 
361
- private selectedSortChanged() {
377
+ private sendSortByAnaltyics(prevSortDirection: SortDirection | null): void {
378
+ const directionCleared = prevSortDirection && !this.sortDirection;
379
+
380
+ this.analyticsHandler?.sendEventNoSampling({
381
+ category: analyticsCategories.default,
382
+ action: analyticsActions.sortBy,
383
+ label: `${this.selectedSort}${
384
+ this.sortDirection || directionCleared ? `-${this.sortDirection}` : ''
385
+ }`,
386
+ });
387
+ }
388
+
389
+ private selectedSortChanged(): void {
362
390
  if (this.selectedSort === 'relevance' || this.sortDirection === null) {
363
391
  this.sortParam = null;
364
392
  return;
365
393
  }
366
394
  const sortField = SortFieldToMetadataField[this.selectedSort];
395
+
367
396
  if (!sortField) return;
368
397
  this.sortParam = { field: sortField, direction: this.sortDirection };
369
398
  }
@@ -372,15 +401,61 @@ export class CollectionBrowser
372
401
  e: CustomEvent<{ displayMode: CollectionDisplayMode }>
373
402
  ) {
374
403
  this.displayMode = e.detail.displayMode;
404
+
405
+ if (this.displayMode) {
406
+ this.analyticsHandler?.sendEventNoSampling({
407
+ category: analyticsCategories.default,
408
+ action: analyticsActions.displayMode,
409
+ label: this.displayMode,
410
+ });
411
+ }
412
+ }
413
+
414
+ /** Send Analytics when sorting by title's first letter
415
+ * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
416
+ * */
417
+ private sendFilterByTitleAnalytics(prevSelectedLetter: string | null): void {
418
+ if (!prevSelectedLetter && !this.selectedTitleFilter) {
419
+ return;
420
+ }
421
+ const cleared = prevSelectedLetter && this.selectedTitleFilter === null;
422
+
423
+ this.analyticsHandler?.sendEventNoSampling({
424
+ category: analyticsCategories.default,
425
+ action: analyticsActions.filterByTitle,
426
+ label: cleared
427
+ ? `clear-${prevSelectedLetter}`
428
+ : `${prevSelectedLetter || 'start'}-${this.selectedTitleFilter}`,
429
+ });
375
430
  }
376
431
 
377
- private selectedTitleLetterChanged() {
432
+ private selectedTitleLetterChanged(): void {
378
433
  this.titleQuery = this.selectedTitleFilter
379
434
  ? `firstTitle:${this.selectedTitleFilter}`
380
435
  : undefined;
381
436
  }
382
437
 
383
- private selectedCreatorLetterChanged() {
438
+ /** Send Analytics when filtering by creator's first letter
439
+ * labels: 'start-<ToLetter>' | 'clear-<FromLetter>' | '<FromLetter>-<ToLetter>'
440
+ * */
441
+ private sendFilterByCreatorAnalytics(
442
+ prevSelectedLetter: string | null
443
+ ): void {
444
+ if (!prevSelectedLetter && !this.selectedCreatorFilter) {
445
+ return;
446
+ }
447
+ const cleared = prevSelectedLetter && this.selectedCreatorFilter === null;
448
+
449
+ this.analyticsHandler?.sendEventNoSampling({
450
+ category: analyticsCategories.default,
451
+ action: analyticsActions.filterByCreator,
452
+ label: cleared
453
+ ? `clear-${prevSelectedLetter}`
454
+ : `${prevSelectedLetter || 'start'}-${this.selectedCreatorFilter}`,
455
+ });
456
+ }
457
+
458
+ private selectedCreatorLetterChanged(): void {
384
459
  this.creatorQuery = this.selectedCreatorFilter
385
460
  ? `firstCreator:${this.selectedCreatorFilter}`
386
461
  : undefined;
@@ -439,7 +514,10 @@ export class CollectionBrowser
439
514
  ?collapsableFacets=${this.mobileView}
440
515
  ?facetsLoading=${this.facetDataLoading}
441
516
  ?fullYearAggregationLoading=${this.fullYearAggregationLoading}
442
- ></collection-facets>
517
+ .onFacetClick=${this.facetClickHandler}
518
+ .analyticsHandler=${this.analyticsHandler}
519
+ >
520
+ </collection-facets>
443
521
  `;
444
522
  }
445
523
 
@@ -489,6 +567,14 @@ export class CollectionBrowser
489
567
  ) {
490
568
  const { minDate, maxDate } = e.detail;
491
569
  this.dateRangeQueryClause = `year:[${minDate} TO ${maxDate}]`;
570
+
571
+ if (this.dateRangeQueryClause) {
572
+ this.analyticsHandler?.sendEventNoSampling({
573
+ category: analyticsCategories.default,
574
+ action: analyticsActions.histogramChanged,
575
+ label: this.dateRangeQueryClause,
576
+ });
577
+ }
492
578
  }
493
579
 
494
580
  firstUpdated(): void {
@@ -523,12 +609,20 @@ export class CollectionBrowser
523
609
  this.handleQueryChange();
524
610
  }
525
611
  if (changed.has('selectedSort') || changed.has('sortDirection')) {
612
+ const prevSortDirection = changed.get('sortDirection') as SortDirection;
613
+ this.sendSortByAnaltyics(prevSortDirection);
526
614
  this.selectedSortChanged();
527
615
  }
528
616
  if (changed.has('selectedTitleFilter')) {
617
+ this.sendFilterByTitleAnalytics(
618
+ changed.get('selectedTitleFilter') as string
619
+ );
529
620
  this.selectedTitleLetterChanged();
530
621
  }
531
622
  if (changed.has('selectedCreatorFilter')) {
623
+ this.sendFilterByCreatorAnalytics(
624
+ changed.get('selectedCreatorFilter') as string
625
+ );
532
626
  this.selectedCreatorLetterChanged();
533
627
  }
534
628
  if (changed.has('pagesToRender')) {
@@ -748,6 +842,7 @@ export class CollectionBrowser
748
842
  private get facetQuery(): string | undefined {
749
843
  if (!this.selectedFacets) return undefined;
750
844
  const facetQuery = [];
845
+ // console.log('selectedFacets: ', this.selectedFacets);
751
846
  for (const [facetName, facetValues] of Object.entries(
752
847
  this.selectedFacets
753
848
  )) {
@@ -778,6 +873,30 @@ export class CollectionBrowser
778
873
  this.selectedFacets = e.detail;
779
874
  }
780
875
 
876
+ facetClickHandler(
877
+ name: FacetOption,
878
+ facetSelected: boolean,
879
+ negative: boolean
880
+ ): void {
881
+ if (negative) {
882
+ this.analyticsHandler?.sendEventNoSampling({
883
+ category: analyticsCategories.default,
884
+ action: facetSelected
885
+ ? analyticsActions.facetNegativeSelected
886
+ : analyticsActions.facetNegativeDeselected,
887
+ label: name,
888
+ });
889
+ } else {
890
+ this.analyticsHandler?.sendEventNoSampling({
891
+ category: analyticsCategories.default,
892
+ action: facetSelected
893
+ ? analyticsActions.facetSelected
894
+ : analyticsActions.facetDeselected,
895
+ label: name,
896
+ });
897
+ }
898
+ }
899
+
781
900
  private async fetchFacets() {
782
901
  if (!this.fullQuery) return;
783
902
 
@@ -1076,6 +1195,7 @@ export class CollectionBrowser
1076
1195
  issue: doc.issue?.value,
1077
1196
  itemCount: doc.item_count?.value ?? 0,
1078
1197
  mediatype: doc.mediatype?.value ?? 'data',
1198
+ snippets: doc.snippets?.values ?? [],
1079
1199
  source: doc.source?.value,
1080
1200
  subjects: doc.subject?.values ?? [],
1081
1201
  title: this.etreeTitle(
@@ -1120,6 +1240,21 @@ export class CollectionBrowser
1120
1240
  return title ?? '';
1121
1241
  }
1122
1242
 
1243
+ /** callback when a result is selected */
1244
+ resultSelected(event: CustomEvent<TileModel>): void {
1245
+ this.analyticsHandler?.sendEventNoSampling({
1246
+ category: analyticsCategories.default,
1247
+ action: analyticsActions.resultSelected,
1248
+ label: event.detail.mediatype === 'collection' ? 'collection' : 'item',
1249
+ });
1250
+
1251
+ this.analyticsHandler?.sendEventNoSampling({
1252
+ category: analyticsCategories.default,
1253
+ action: analyticsActions.resultSelected,
1254
+ label: `page-${this.currentPage}`,
1255
+ });
1256
+ }
1257
+
1123
1258
  cellForIndex(index: number): TemplateResult | undefined {
1124
1259
  const model = this.tileModelAtCellIndex(index);
1125
1260
  if (!model) return undefined;
@@ -1135,6 +1270,7 @@ export class CollectionBrowser
1135
1270
  .sortParam=${this.sortParam}
1136
1271
  .mobileBreakpoint=${this.mobileBreakpoint}
1137
1272
  .loggedIn=${this.loggedIn}
1273
+ @resultSelected=${(e: CustomEvent) => this.resultSelected(e)}
1138
1274
  >
1139
1275
  </tile-dispatcher>
1140
1276
  `;
@@ -79,6 +79,13 @@ export class CollectionFacets extends LitElement {
79
79
  @property({ type: Object })
80
80
  collectionNameCache?: CollectionNameCacheInterface;
81
81
 
82
+ /** Fires when a facet is clicked */
83
+ @property({ type: Function }) onFacetClick?: (
84
+ name: FacetOption,
85
+ facetChecked: boolean,
86
+ negative: boolean
87
+ ) => void;
88
+
82
89
  @state() openFacets: Record<FacetOption, boolean> = {
83
90
  subject: false,
84
91
  mediatype: false,
@@ -88,6 +95,7 @@ export class CollectionFacets extends LitElement {
88
95
  year: false,
89
96
  };
90
97
 
98
+ @property({ type: Object, attribute: false })
91
99
  render() {
92
100
  return html`
93
101
  <div id="container" class="${this.facetsLoading ? 'loading' : ''}">
@@ -426,7 +434,7 @@ export class CollectionFacets extends LitElement {
426
434
  `;
427
435
  }
428
436
 
429
- private facetClicked(e: Event, bucket: FacetBucket, negative: boolean) {
437
+ private facetClicked(e: Event, bucket: FacetBucket, negative: boolean): void {
430
438
  const target = e.target as HTMLInputElement;
431
439
  const { checked, name, value } = target;
432
440
  if (checked) {
@@ -434,6 +442,10 @@ export class CollectionFacets extends LitElement {
434
442
  } else {
435
443
  this.facetUnchecked(name as FacetOption, value);
436
444
  }
445
+
446
+ if (this.onFacetClick) {
447
+ this.onFacetClick(name as FacetOption, checked, negative);
448
+ }
437
449
  }
438
450
 
439
451
  private facetChecked(key: FacetOption, value: string, negative: boolean) {
package/src/models.ts CHANGED
@@ -19,6 +19,7 @@ export interface TileModel {
19
19
  itemCount: number;
20
20
  mediatype: MediaType;
21
21
  source?: string;
22
+ snippets?: string[];
22
23
  subjects: string[];
23
24
  title: string;
24
25
  viewCount: number;
@@ -158,3 +159,12 @@ export const defaultSelectedFacets: SelectedFacets = {
158
159
  collection: {},
159
160
  year: {},
160
161
  };
162
+
163
+ export const mockedSelectedFacets: SelectedFacets = {
164
+ subject: {},
165
+ mediatype: { data: 'selected' },
166
+ language: {},
167
+ creator: {},
168
+ collection: {},
169
+ year: {},
170
+ };
@@ -1,5 +1,12 @@
1
1
  /* eslint-disable import/no-duplicates */
2
- import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
2
+ import {
3
+ css,
4
+ CSSResultGroup,
5
+ html,
6
+ LitElement,
7
+ nothing,
8
+ TemplateResult,
9
+ } from 'lit';
3
10
  import { customElement, property } from 'lit/decorators.js';
4
11
  import { ifDefined } from 'lit/directives/if-defined.js';
5
12
  import type { SortParam } from '@internetarchive/search-service';
@@ -8,6 +15,7 @@ import { formatDate } from '../../utils/format-date';
8
15
  import type { TileModel } from '../../models';
9
16
 
10
17
  import '../image-block';
18
+ import '../text-snippet-block';
11
19
  import '../item-image';
12
20
  import '../mediatype-icon';
13
21
  import './tile-stats';
@@ -35,6 +43,7 @@ export class ItemTile extends LitElement {
35
43
  </div>
36
44
 
37
45
  <image-block
46
+ class=${this.hasSnippets ? 'has-snippets' : nothing}
38
47
  .model=${this.model}
39
48
  .baseImageUrl=${this.baseImageUrl}
40
49
  .loggedIn=${this.loggedIn}
@@ -43,6 +52,8 @@ export class ItemTile extends LitElement {
43
52
  .viewSize=${'grid'}>
44
53
  </image-block>
45
54
 
55
+ ${this.textSnippetsTemplate}
56
+
46
57
  ${
47
58
  this.doesSortedByDate
48
59
  ? this.sortedDateInfoTemplate
@@ -109,6 +120,19 @@ export class ItemTile extends LitElement {
109
120
  `;
110
121
  }
111
122
 
123
+ private get textSnippetsTemplate(): TemplateResult | typeof nothing {
124
+ if (!this.hasSnippets) return nothing;
125
+
126
+ return html`<text-snippet-block
127
+ viewsize="grid"
128
+ .snippets=${this.model?.snippets}
129
+ ></text-snippet-block>`;
130
+ }
131
+
132
+ private get hasSnippets(): boolean {
133
+ return !!this.model?.snippets?.length;
134
+ }
135
+
112
136
  static get styles(): CSSResultGroup {
113
137
  return css`
114
138
  .container {
@@ -142,6 +166,16 @@ export class ItemTile extends LitElement {
142
166
  text-decoration: underline;
143
167
  }
144
168
 
169
+ image-block {
170
+ display: block;
171
+ margin-bottom: 5px;
172
+ }
173
+
174
+ image-block.has-snippets {
175
+ /* If there is a text snippet block present, the image block needs to shrink */
176
+ --imgBlockHeight: 11rem;
177
+ }
178
+
145
179
  .created-by,
146
180
  .date-sorted-by {
147
181
  display: flex;
@@ -149,7 +183,6 @@ export class ItemTile extends LitElement {
149
183
  align-items: flex-end; /* Important to start text from bottom */
150
184
  height: 3rem;
151
185
  padding-top: 1rem;
152
- margin-top: 5px;
153
186
  }
154
187
 
155
188
  .truncated {
@@ -90,7 +90,7 @@ export class ImageBlock extends LitElement {
90
90
  }
91
91
 
92
92
  .grid {
93
- height: 16rem;
93
+ height: var(--imgBlockHeight, 16rem);
94
94
  flex: 1;
95
95
  }
96
96
 
@@ -140,7 +140,7 @@ export class TileList extends LitElement {
140
140
  ${this.viewsTemplate} ${this.ratingTemplate} ${this.reviewsTemplate}
141
141
  </div>
142
142
  ${this.topicsTemplate} ${this.collectionsTemplate}
143
- ${this.descriptionTemplate}
143
+ ${this.descriptionTemplate} ${this.textSnippetsTemplate}
144
144
  `;
145
145
  }
146
146
 
@@ -292,6 +292,19 @@ export class TileList extends LitElement {
292
292
  );
293
293
  }
294
294
 
295
+ private get textSnippetsTemplate(): TemplateResult | typeof nothing {
296
+ if (!this.hasSnippets) return nothing;
297
+
298
+ return html`<text-snippet-block
299
+ viewsize="list"
300
+ .snippets=${this.model?.snippets}
301
+ ></text-snippet-block>`;
302
+ }
303
+
304
+ private get hasSnippets(): boolean {
305
+ return !!this.model?.snippets?.length;
306
+ }
307
+
295
308
  // Utility functions
296
309
  private metadataTemplate(text: any, label = '', id?: string) {
297
310
  if (!text) return nothing;