@internetarchive/collection-browser 2.18.2 → 2.18.3-alpha-webdev7768.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 (48) hide show
  1. package/.editorconfig +29 -29
  2. package/.github/workflows/ci.yml +27 -27
  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/.prettierignore +1 -1
  8. package/LICENSE +661 -661
  9. package/README.md +83 -83
  10. package/dist/src/collection-browser.js +682 -682
  11. package/dist/src/collection-browser.js.map +1 -1
  12. package/dist/src/collection-facets.js +291 -274
  13. package/dist/src/collection-facets.js.map +1 -1
  14. package/dist/src/expanded-date-picker.d.ts +6 -4
  15. package/dist/src/expanded-date-picker.js +64 -55
  16. package/dist/src/expanded-date-picker.js.map +1 -1
  17. package/dist/src/models.js.map +1 -1
  18. package/dist/src/sort-filter-bar/sort-filter-bar.js +382 -382
  19. package/dist/src/sort-filter-bar/sort-filter-bar.js.map +1 -1
  20. package/dist/src/tiles/grid/item-tile.js +139 -139
  21. package/dist/src/tiles/grid/item-tile.js.map +1 -1
  22. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js +118 -118
  23. package/dist/src/tiles/grid/styles/tile-grid-shared-styles.js.map +1 -1
  24. package/dist/src/tiles/list/tile-list.js +289 -289
  25. package/dist/src/tiles/list/tile-list.js.map +1 -1
  26. package/dist/src/tiles/tile-dispatcher.js +200 -200
  27. package/dist/src/tiles/tile-dispatcher.js.map +1 -1
  28. package/dist/test/sort-filter-bar/sort-filter-bar.test.js +37 -37
  29. package/dist/test/sort-filter-bar/sort-filter-bar.test.js.map +1 -1
  30. package/eslint.config.mjs +53 -53
  31. package/index.html +24 -24
  32. package/local.archive.org.cert +86 -86
  33. package/local.archive.org.key +27 -27
  34. package/package.json +117 -117
  35. package/renovate.json +6 -6
  36. package/src/collection-browser.ts +2712 -2712
  37. package/src/collection-facets.ts +990 -966
  38. package/src/expanded-date-picker.ts +191 -175
  39. package/src/models.ts +822 -822
  40. package/src/sort-filter-bar/sort-filter-bar.ts +1189 -1189
  41. package/src/tiles/grid/item-tile.ts +339 -339
  42. package/src/tiles/grid/styles/tile-grid-shared-styles.ts +129 -129
  43. package/src/tiles/list/tile-list.ts +688 -688
  44. package/src/tiles/tile-dispatcher.ts +486 -486
  45. package/test/sort-filter-bar/sort-filter-bar.test.ts +692 -692
  46. package/tsconfig.json +20 -20
  47. package/web-dev-server.config.mjs +30 -30
  48. package/web-test-runner.config.mjs +41 -41
@@ -1,1189 +1,1189 @@
1
- import {
2
- LitElement,
3
- html,
4
- css,
5
- nothing,
6
- PropertyValues,
7
- TemplateResult,
8
- } from 'lit';
9
- import { customElement, property, query, state } from 'lit/decorators.js';
10
- import { msg } from '@lit/localize';
11
- import type {
12
- SharedResizeObserverInterface,
13
- SharedResizeObserverResizeHandlerInterface,
14
- } from '@internetarchive/shared-resize-observer';
15
- import '@internetarchive/ia-dropdown';
16
- import type { IaDropdown, optionInterface } from '@internetarchive/ia-dropdown';
17
- import type { SortDirection } from '@internetarchive/search-service';
18
- import {
19
- CollectionDisplayMode,
20
- PrefixFilterCounts,
21
- PrefixFilterType,
22
- SORT_OPTIONS,
23
- SortField,
24
- } from '../models';
25
- import './alpha-bar';
26
-
27
- import { sortUpIcon } from './img/sort-toggle-up';
28
- import { sortDownIcon } from './img/sort-toggle-down';
29
- import { sortDisabledIcon } from './img/sort-toggle-disabled';
30
- import { tileIcon } from './img/tile';
31
- import { listIcon } from './img/list';
32
- import { compactIcon } from './img/compact';
33
- import { srOnlyStyle } from '../styles/sr-only';
34
-
35
- type AlphaSelector = 'creator' | 'title';
36
-
37
- @customElement('sort-filter-bar')
38
- export class SortFilterBar
39
- extends LitElement
40
- implements SharedResizeObserverResizeHandlerInterface
41
- {
42
- /** Which display mode the tiles are being rendered with (grid/list-detail/list-compact) */
43
- @property({ type: String }) displayMode?: CollectionDisplayMode;
44
-
45
- /** The default sort direction to use if none is set */
46
- @property({ type: String }) defaultSortDirection: SortDirection | null = null;
47
-
48
- /** The default sort field to use if none is set */
49
- @property({ type: String }) defaultSortField: Exclude<
50
- SortField,
51
- SortField.default
52
- > = SortField.relevance;
53
-
54
- /** The current sort direction (asc/desc), or null if none is set */
55
- @property({ type: String }) sortDirection: SortDirection | null = null;
56
-
57
- /** The field currently being sorted on (e.g., 'title'). Defaults to relevance. */
58
- @property({ type: String }) selectedSort: SortField = SortField.default;
59
-
60
- /** The currently selected title letter filter, or null if none is set */
61
- @property({ type: String }) selectedTitleFilter: string | null = null;
62
-
63
- /** The currently selected creator letter filter, or null if none is set */
64
- @property({ type: String }) selectedCreatorFilter: string | null = null;
65
-
66
- /** Whether to show the Relevance sort option (default `true`) */
67
- @property({ type: Boolean }) showRelevance: boolean = true;
68
-
69
- /** Whether to show the Date Favorited sort option instead of Date Published/Archived/Reviewed (default `false`) */
70
- @property({ type: Boolean }) showDateFavorited: boolean = false;
71
-
72
- /** Whether to replace the default sort options with a slot for customization (default `false`) */
73
- @property({ type: Boolean, reflect: true }) enableSortOptionsSlot: boolean =
74
- false;
75
-
76
- /** Whether to suppress showing the three display mode options on the right of the bar (default `false`) */
77
- @property({ type: Boolean, reflect: true })
78
- suppressDisplayModes: boolean = false;
79
-
80
- /** Maps of result counts for letters on the alphabet bar, for each letter filter type */
81
- @property({ type: Object }) prefixFilterCountMap?: Record<
82
- PrefixFilterType,
83
- PrefixFilterCounts
84
- >;
85
-
86
- @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
87
-
88
- /**
89
- * The Views sort option that was most recently selected (or the default, if none has been selected yet)
90
- */
91
- @state() private lastSelectedViewSort = SortField.weeklyview;
92
-
93
- /**
94
- * The Date sort option that was most recently selected (or the default, if none has been selected yet)
95
- */
96
- @state() private lastSelectedDateSort = this.defaultDateSortField;
97
-
98
- /**
99
- * Which of the alphabet bars (title/creator) should be shown, or null if one
100
- * should not currently be rendered.
101
- */
102
- @state() alphaSelectorVisible: AlphaSelector | null = null;
103
-
104
- /**
105
- * Whether the transparent backdrop to catch clicks outside the dropdown menu
106
- * should be rendered.
107
- */
108
- @state() dropdownBackdropVisible = false;
109
-
110
- /**
111
- * The width of the desktop view sort option container, updated upon each resize.
112
- * Used for dynamically determining whether to use desktop or mobile view.
113
- */
114
- @state() desktopSortContainerWidth = 0;
115
-
116
- /**
117
- * The width of the full sort bar, updated upon each resize.
118
- * Used for dynamically determining whether to use desktop or mobile view.
119
- */
120
- @state() selectorBarContainerWidth = 0;
121
-
122
- /**
123
- * The container for all the desktop view's sort options.
124
- * Used for dynamically determining whether to use desktop or mobile view.
125
- */
126
- @query('#desktop-sort-container')
127
- private desktopSortContainer!: HTMLUListElement;
128
-
129
- /**
130
- * The container for the full sort bar.
131
- * Used for dynamically determining whether to use desktop or mobile view.
132
- */
133
- @query('#sort-selector-container')
134
- private sortSelectorContainer!: HTMLDivElement;
135
-
136
- /** The dropdown component containing options for weekly and all-time views */
137
- @query('#views-dropdown')
138
- private viewsDropdown!: IaDropdown;
139
-
140
- /** The dropdown component containing the four date options */
141
- @query('#date-dropdown')
142
- private dateDropdown!: IaDropdown;
143
-
144
- /** The single, consolidated dropdown component shown in mobile view */
145
- @query('#mobile-dropdown')
146
- private mobileDropdown!: IaDropdown;
147
-
148
- render() {
149
- return html`
150
- <div id="container">
151
- <section id="sort-bar" aria-label="Sorting options">
152
- <slot name="sort-options-left"></slot>
153
- <div id="sort-options">
154
- ${!this.enableSortOptionsSlot
155
- ? html`
156
- <div class="sort-direction-container">
157
- ${this.sortDirectionSelectorTemplate}
158
- </div>
159
- <span class="sort-by-text">${msg('Sort by:')}</span>
160
- <div id="sort-selector-container">
161
- ${this.mobileSortSelectorTemplate}
162
- ${this.desktopSortSelectorTemplate}
163
- </div>
164
- `
165
- : html`<slot name="sort-options"></slot>`}
166
- </div>
167
- <slot name="sort-options-right"></slot>
168
-
169
- ${this.suppressDisplayModes
170
- ? nothing
171
- : html`<div id="display-style-selector">
172
- ${this.displayOptionTemplate}
173
- </div>`}
174
- </section>
175
-
176
- ${this.dropdownBackdropVisible ? this.dropdownBackdrop : nothing}
177
- ${this.alphaBarTemplate}
178
- </div>
179
- `;
180
- }
181
-
182
- willUpdate(changed: PropertyValues) {
183
- if (changed.has('selectedSort') || changed.has('defaultSortField')) {
184
- // If the sort is changed from its default without a direction set,
185
- // we adopt the default sort direction for that sort type.
186
- if (
187
- this.selectedSort &&
188
- this.selectedSort !== SortField.default &&
189
- this.sortDirection === null
190
- ) {
191
- const sortOption = SORT_OPTIONS[this.finalizedSortField];
192
- this.sortDirection = sortOption.defaultSortDirection;
193
- }
194
-
195
- if (this.viewOptionSelected) {
196
- this.lastSelectedViewSort = this.finalizedSortField;
197
- } else if (this.dateOptionSelected) {
198
- this.lastSelectedDateSort = this.finalizedSortField;
199
- }
200
- }
201
-
202
- // If we change which dropdown options are available, ensure the correct default becomes selected.
203
- // Currently, Date Favorited is the only dropdown option whose presence/absence can change.
204
- if (
205
- changed.has('showDateFavorited') &&
206
- changed.get('showDateFavorited') !== this.showDateFavorited
207
- ) {
208
- this.lastSelectedDateSort = this.defaultDateSortField;
209
- }
210
- }
211
-
212
- updated(changed: PropertyValues) {
213
- if (changed.has('displayMode')) {
214
- this.displayModeChanged();
215
- }
216
-
217
- if (changed.has('selectedTitleFilter') && this.selectedTitleFilter) {
218
- this.alphaSelectorVisible = 'title';
219
- }
220
-
221
- if (changed.has('selectedCreatorFilter') && this.selectedCreatorFilter) {
222
- this.alphaSelectorVisible = 'creator';
223
- }
224
-
225
- if (changed.has('dropdownBackdropVisible')) {
226
- this.setupEscapeListeners();
227
- }
228
-
229
- if (changed.has('resizeObserver') || changed.has('enableSortOptionsSlot')) {
230
- const oldObserver = changed.get(
231
- 'resizeObserver',
232
- ) as SharedResizeObserverInterface;
233
- if (oldObserver) this.disconnectResizeObserver(oldObserver);
234
- this.setupResizeObserver();
235
- }
236
- }
237
-
238
- private setupEscapeListeners() {
239
- if (this.dropdownBackdropVisible) {
240
- document.addEventListener(
241
- 'keydown',
242
- this.boundSortBarSelectorEscapeListener,
243
- );
244
- } else {
245
- document.removeEventListener(
246
- 'keydown',
247
- this.boundSortBarSelectorEscapeListener,
248
- );
249
- }
250
- }
251
-
252
- private boundSortBarSelectorEscapeListener = (e: KeyboardEvent) => {
253
- if (e.key === 'Escape') {
254
- this.closeDropdowns();
255
- }
256
- };
257
-
258
- connectedCallback(): void {
259
- super.connectedCallback?.();
260
- this.setupResizeObserver();
261
- }
262
-
263
- disconnectedCallback(): void {
264
- if (this.resizeObserver) {
265
- this.disconnectResizeObserver(this.resizeObserver);
266
- }
267
- }
268
-
269
- private disconnectResizeObserver(
270
- resizeObserver: SharedResizeObserverInterface,
271
- ) {
272
- if (this.sortSelectorContainer) {
273
- resizeObserver.removeObserver({
274
- target: this.sortSelectorContainer,
275
- handler: this,
276
- });
277
- }
278
-
279
- if (this.desktopSortContainer) {
280
- resizeObserver.removeObserver({
281
- target: this.desktopSortContainer,
282
- handler: this,
283
- });
284
- }
285
- }
286
-
287
- private setupResizeObserver() {
288
- if (!this.resizeObserver) return;
289
-
290
- if (this.sortSelectorContainer) {
291
- this.resizeObserver.addObserver({
292
- target: this.sortSelectorContainer,
293
- handler: this,
294
- });
295
- }
296
-
297
- if (this.desktopSortContainer) {
298
- this.resizeObserver.addObserver({
299
- target: this.desktopSortContainer,
300
- handler: this,
301
- });
302
- }
303
- }
304
-
305
- handleResize(entry: ResizeObserverEntry): void {
306
- if (entry.target === this.desktopSortContainer) {
307
- this.desktopSortContainerWidth = entry.contentRect.width;
308
- } else if (entry.target === this.sortSelectorContainer) {
309
- this.selectorBarContainerWidth = entry.contentRect.width;
310
- }
311
- }
312
-
313
- /**
314
- * Whether to show the mobile sort bar because there is not enough space
315
- * for the desktop sort bar.
316
- */
317
- private get mobileSelectorVisible() {
318
- return this.selectorBarContainerWidth - 10 < this.desktopSortContainerWidth;
319
- }
320
-
321
- /**
322
- * Template to render the alphabet bar, or `nothing` if it should not be rendered
323
- * for the current sort
324
- */
325
- private get alphaBarTemplate(): TemplateResult | typeof nothing {
326
- if (!['title', 'creator'].includes(this.selectedSort)) return nothing;
327
-
328
- if (this.alphaSelectorVisible === null) {
329
- if (this.selectedSort === 'creator') return this.creatorSelectorBar;
330
- if (this.selectedSort === 'title') return this.titleSelectorBar;
331
- } else {
332
- return this.alphaSelectorVisible === 'creator'
333
- ? this.creatorSelectorBar
334
- : this.titleSelectorBar;
335
- }
336
-
337
- return nothing;
338
- }
339
-
340
- /** Template to render the sort direction toggle button */
341
- private get sortDirectionSelectorTemplate(): TemplateResult {
342
- const oppositeSortDirectionReadable =
343
- this.sortDirection === 'asc' ? 'descending' : 'ascending';
344
- const srLabel = `Change to ${oppositeSortDirectionReadable} sort`;
345
-
346
- return html`
347
- <button
348
- class="sort-direction-selector"
349
- ?disabled=${!this.canChangeSortDirection}
350
- @click=${this.handleSortDirectionClicked}
351
- >
352
- <span class="sr-only">${srLabel}</span>
353
- ${this.sortDirectionIcon}
354
- </button>
355
- `;
356
- }
357
-
358
- /** Template to render the sort direction button's icon in the correct current state */
359
- private get sortDirectionIcon(): TemplateResult {
360
- // Show a fully disabled icon for sort options without direction support
361
- if (!this.canChangeSortDirection) {
362
- return html`<div class="sort-direction-icon">${sortDisabledIcon}</div>`;
363
- }
364
-
365
- // For all other sorts, show the ascending/descending direction
366
- return html`
367
- <div class="sort-direction-icon">
368
- ${this.finalizedSortDirection === 'asc' ? sortUpIcon : sortDownIcon}
369
- </div>
370
- `;
371
- }
372
-
373
- /** The template to render all the sort options in desktop view */
374
- private get desktopSortSelectorTemplate() {
375
- return html`
376
- <div
377
- id="desktop-sort-container"
378
- class=${this.mobileSelectorVisible ? 'hidden' : 'visible'}
379
- >
380
- <ul id="desktop-sort-selector">
381
- ${this.showRelevance
382
- ? html`<li>
383
- ${this.getSortDisplayOption(SortField.relevance, {
384
- onClick: () => {
385
- this.dropdownBackdropVisible = false;
386
- if (this.finalizedSortField !== SortField.relevance) {
387
- this.clearAlphaBarFilters();
388
- this.setSelectedSort(SortField.relevance);
389
- }
390
- },
391
- })}
392
- </li>`
393
- : nothing}
394
- <li>${this.viewsDropdownTemplate}</li>
395
- <li>
396
- ${this.getSortDisplayOption(SortField.title, {
397
- onClick: () => {
398
- this.dropdownBackdropVisible = false;
399
- if (this.finalizedSortField !== SortField.title) {
400
- this.alphaSelectorVisible = 'title';
401
- this.selectedCreatorFilter = null;
402
- this.setSelectedSort(SortField.title);
403
- this.emitCreatorLetterChangedEvent();
404
- }
405
- },
406
- })}
407
- </li>
408
- <li>${this.dateDropdownTemplate}</li>
409
- <li>
410
- ${this.getSortDisplayOption(SortField.creator, {
411
- onClick: () => {
412
- this.dropdownBackdropVisible = false;
413
- if (this.finalizedSortField !== SortField.creator) {
414
- this.alphaSelectorVisible = 'creator';
415
- this.selectedTitleFilter = null;
416
- this.setSelectedSort(SortField.creator);
417
- this.emitTitleLetterChangedEvent();
418
- }
419
- },
420
- })}
421
- </li>
422
- </ul>
423
- </div>
424
- `;
425
- }
426
-
427
- /** The template to render all the sort options in mobile view */
428
- private get mobileSortSelectorTemplate() {
429
- const displayedOptions = Object.values(SORT_OPTIONS)
430
- .filter(opt => opt.shownInSortBar)
431
- .filter(opt => this.showRelevance || opt.field !== SortField.relevance)
432
- .filter(
433
- opt => this.showDateFavorited || opt.field !== SortField.datefavorited,
434
- );
435
-
436
- return html`
437
- <div
438
- id="mobile-sort-container"
439
- class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
440
- >
441
- ${this.getSortDropdown({
442
- displayName: html`${SORT_OPTIONS[this.finalizedSortField].displayName}`,
443
- id: 'mobile-dropdown',
444
- selected: true,
445
- dropdownOptions: displayedOptions.map(opt =>
446
- this.getDropdownOption(opt.field),
447
- ),
448
- selectedOption: this.finalizedSortField,
449
- onOptionSelected: this.mobileSortChanged,
450
- onDropdownClick: () => {
451
- this.dropdownBackdropVisible = this.mobileDropdown.open;
452
- this.mobileDropdown.classList.toggle(
453
- 'open',
454
- this.mobileDropdown.open,
455
- );
456
- },
457
- })}
458
- </div>
459
- `;
460
- }
461
-
462
- /**
463
- * This generates each of the non-dropdown sort option links.
464
- *
465
- * It manages the display value and the selected state of the option.
466
- *
467
- * @param sortField
468
- * @param options {
469
- * onClick?: (e: Event) => void; If this is provided, it will also be called when the option is clicked.
470
- * displayName?: TemplateResult; The name to display for the option. Defaults to the sortField display name.
471
- * selected?: boolean; true if the option is selected. Defaults to the selectedSort === sortField.
472
- * }
473
- * @returns
474
- */
475
- private getSortDisplayOption(
476
- sortField: SortField,
477
- options?: {
478
- displayName?: TemplateResult;
479
- selected?: boolean;
480
- onClick?: (e: Event) => void;
481
- },
482
- ): TemplateResult {
483
- const isSelected =
484
- options?.selected ?? this.finalizedSortField === sortField;
485
- const displayName =
486
- options?.displayName ?? SORT_OPTIONS[sortField].displayName;
487
- return html`
488
- <button
489
- class=${isSelected ? 'selected' : nothing}
490
- data-title="${displayName}"
491
- @click=${(e: Event) => {
492
- e.preventDefault();
493
- options?.onClick?.(e);
494
- }}
495
- >
496
- ${displayName}
497
- </button>
498
- `;
499
- }
500
-
501
- /**
502
- * Generates a dropdown component containing multiple grouped sort options.
503
- *
504
- * @param options.displayName The name to use for the dropdown's visible label
505
- * @param options.id The id to apply to the dropdown element
506
- * @param options.dropdownOptions An array of option objects used to populate the dropdown
507
- * @param options.selectedOption The id of the option that should be initially selected
508
- * @param options.selected A boolean indicating whether this dropdown should use its
509
- * selected appearance
510
- * @param options.onOptionSelected A handler for optionSelected events coming from the dropdown
511
- * @param options.onDropdownClick A handler for click events on the dropdown
512
- * @param options.onLabelInteraction A handler for click events and Enter/Space keydown events
513
- * on the dropdown's label
514
- */
515
- private getSortDropdown(options: {
516
- displayName: TemplateResult;
517
- id?: string;
518
- dropdownOptions: optionInterface[];
519
- selectedOption?: string;
520
- selected: boolean;
521
- onOptionSelected?: (e: CustomEvent<{ option: optionInterface }>) => void;
522
- onDropdownClick?: (e: PointerEvent) => void;
523
- onLabelInteraction?: (e: Event) => void;
524
- }): TemplateResult {
525
- return html`
526
- <ia-dropdown
527
- id=${options.id ?? nothing}
528
- class=${options.selected ? 'selected' : nothing}
529
- displayCaret
530
- closeOnSelect
531
- includeSelectedOption
532
- .openViaButton=${options.selected}
533
- .options=${options.dropdownOptions}
534
- .selectedOption=${options.selectedOption ?? ''}
535
- @optionSelected=${options.onOptionSelected ?? nothing}
536
- @click=${options.onDropdownClick ?? nothing}
537
- >
538
- <span
539
- class="dropdown-label"
540
- slot="dropdown-label"
541
- data-title="${options.displayName.values}"
542
- @click=${options.onLabelInteraction ?? nothing}
543
- @keydown=${options.onLabelInteraction
544
- ? (e: KeyboardEvent) => {
545
- if (e.key === 'Enter' || e.key === ' ') {
546
- options.onLabelInteraction?.(e);
547
- }
548
- }
549
- : nothing}
550
- >
551
- ${options.displayName}
552
- </span>
553
- </ia-dropdown>
554
- `;
555
- }
556
-
557
- /** Generates a single dropdown option object for the given sort field */
558
- private getDropdownOption(sortField: SortField): optionInterface {
559
- return {
560
- id: sortField,
561
- selectedHandler: () => {
562
- this.selectDropdownSortField(sortField);
563
- },
564
- label: html`
565
- <span class="dropdown-option-label">
566
- ${SORT_OPTIONS[sortField].displayName}
567
- </span>
568
- `,
569
- };
570
- }
571
-
572
- /** Handler for when any sort dropdown option is selected */
573
- private dropdownOptionSelected(e: CustomEvent<{ option: optionInterface }>) {
574
- this.dropdownBackdropVisible = false;
575
- this.clearAlphaBarFilters();
576
-
577
- const sortField = e.detail.option.id as SortField;
578
- this.setSelectedSort(sortField);
579
- if (this.viewOptionSelected) {
580
- this.lastSelectedViewSort = sortField;
581
- } else if (this.dateOptionSelected) {
582
- this.lastSelectedDateSort = sortField;
583
- }
584
- }
585
-
586
- /** The template to render for the views dropdown */
587
- private get viewsDropdownTemplate(): TemplateResult {
588
- return this.getSortDropdown({
589
- displayName: html`${this.viewSortDisplayName}`,
590
- id: 'views-dropdown',
591
- selected: this.viewOptionSelected,
592
- dropdownOptions: [
593
- this.getDropdownOption(SortField.weeklyview),
594
- this.getDropdownOption(SortField.alltimeview),
595
- ],
596
- selectedOption: this.viewOptionSelected ? this.finalizedSortField : '',
597
- onOptionSelected: this.dropdownOptionSelected,
598
- onDropdownClick: () => {
599
- this.dateDropdown.open = false;
600
- this.dropdownBackdropVisible = this.viewsDropdown.open;
601
- this.viewsDropdown.classList.toggle('open', this.viewsDropdown.open);
602
- },
603
- onLabelInteraction: (e: Event) => {
604
- if (!this.viewsDropdown.open && !this.viewOptionSelected) {
605
- e.stopPropagation();
606
- this.clearAlphaBarFilters();
607
- this.setSelectedSort(this.lastSelectedViewSort);
608
- }
609
- },
610
- });
611
- }
612
-
613
- /** The template to render for the date dropdown */
614
- private get dateDropdownTemplate(): TemplateResult {
615
- return this.getSortDropdown({
616
- displayName: html`${this.dateSortDisplayName}`,
617
- id: 'date-dropdown',
618
- selected: this.dateOptionSelected,
619
- dropdownOptions: [
620
- ...(this.showDateFavorited
621
- ? [this.getDropdownOption(SortField.datefavorited)]
622
- : []),
623
- this.getDropdownOption(SortField.date),
624
- this.getDropdownOption(SortField.datearchived),
625
- this.getDropdownOption(SortField.datereviewed),
626
- this.getDropdownOption(SortField.dateadded),
627
- ],
628
- selectedOption: this.dateOptionSelected ? this.finalizedSortField : '',
629
- onOptionSelected: this.dropdownOptionSelected,
630
- onDropdownClick: () => {
631
- this.viewsDropdown.open = false;
632
- this.dropdownBackdropVisible = this.dateDropdown.open;
633
- this.dateDropdown.classList.toggle('open', this.dateDropdown.open);
634
- },
635
- onLabelInteraction: (e: Event) => {
636
- if (!this.dateDropdown.open && !this.dateOptionSelected) {
637
- e.stopPropagation();
638
- this.clearAlphaBarFilters();
639
- this.setSelectedSort(this.lastSelectedDateSort);
640
- }
641
- },
642
- });
643
- }
644
-
645
- /** Handler for when a new mobile sort dropdown option is selected */
646
- private mobileSortChanged(e: CustomEvent<{ option: optionInterface }>) {
647
- this.dropdownBackdropVisible = false;
648
-
649
- const sortField = e.detail.option.id as SortField;
650
- this.setSelectedSort(sortField);
651
-
652
- this.alphaSelectorVisible = null;
653
- if (sortField !== 'title' && this.selectedTitleFilter) {
654
- this.selectedTitleFilter = null;
655
- this.emitTitleLetterChangedEvent();
656
- }
657
- if (sortField !== 'creator' && this.selectedCreatorFilter) {
658
- this.selectedCreatorFilter = null;
659
- this.emitCreatorLetterChangedEvent();
660
- }
661
- }
662
-
663
- /** Template for rendering the three display mode options */
664
- /** Added data-testid for Playwright testing * */
665
- private get displayOptionTemplate() {
666
- return html`
667
- <ul>
668
- <li>
669
- <button
670
- id="grid-button"
671
- @click=${() => {
672
- this.displayMode = 'grid';
673
- }}
674
- class=${this.displayMode === 'grid' ? 'active' : ''}
675
- title="Tile view"
676
- data-testid="grid-button"
677
- >
678
- ${tileIcon}
679
- </button>
680
- </li>
681
- <li>
682
- <button
683
- id="list-detail-button"
684
- @click=${() => {
685
- this.displayMode = 'list-detail';
686
- }}
687
- class=${this.displayMode === 'list-detail' ? 'active' : ''}
688
- title="List view"
689
- data-testid="list-detail-button"
690
- >
691
- ${listIcon}
692
- </button>
693
- </li>
694
- <li>
695
- <button
696
- id="list-compact-button"
697
- @click=${() => {
698
- this.displayMode = 'list-compact';
699
- }}
700
- class=${this.displayMode === 'list-compact' ? 'active' : ''}
701
- title="Compact list view"
702
- data-testid="list-compact-button"
703
- >
704
- ${compactIcon}
705
- </button>
706
- </li>
707
- </ul>
708
- `;
709
- }
710
-
711
- /**
712
- * Template for rendering the transparent backdrop to capture clicks outside the
713
- * dropdown menu while it is open.
714
- */
715
- private get dropdownBackdrop() {
716
- return html`
717
- <div
718
- id="sort-selector-backdrop"
719
- @keyup=${this.closeDropdowns}
720
- @click=${this.closeDropdowns}
721
- ></div>
722
- `;
723
- }
724
-
725
- /** Closes all of the sorting dropdown components' menus */
726
- private closeDropdowns() {
727
- this.dropdownBackdropVisible = false;
728
- const allDropdowns = [
729
- this.viewsDropdown,
730
- this.dateDropdown,
731
- this.mobileDropdown,
732
- ];
733
- for (const dropdown of allDropdowns) {
734
- dropdown.open = false;
735
- dropdown.classList.remove('open');
736
- }
737
- }
738
-
739
- private selectDropdownSortField(sortField: SortField) {
740
- // When a dropdown sort option is selected, we additionally need to clear the backdrop
741
- this.dropdownBackdropVisible = false;
742
- this.setSelectedSort(sortField);
743
- }
744
-
745
- private clearAlphaBarFilters() {
746
- this.alphaSelectorVisible = null;
747
- this.selectedTitleFilter = null;
748
- this.selectedCreatorFilter = null;
749
- this.emitTitleLetterChangedEvent();
750
- this.emitCreatorLetterChangedEvent();
751
- }
752
-
753
- private setSortDirection(sortDirection: SortDirection) {
754
- this.sortDirection = sortDirection;
755
- this.emitSortChangedEvent();
756
- }
757
-
758
- /** Toggles the current sort direction between 'asc' and 'desc' */
759
- private toggleSortDirection() {
760
- this.setSortDirection(
761
- this.finalizedSortDirection === 'desc' ? 'asc' : 'desc',
762
- );
763
- }
764
-
765
- private handleSortDirectionClicked(): void {
766
- if (
767
- !this.sortDirection &&
768
- this.defaultSortField &&
769
- this.defaultSortDirection
770
- ) {
771
- // When the sort direction is merely defaulted (not set by the user), clicking
772
- // the toggled button should "promote" the default sort to an explicitly-set one
773
- // and then toggle it as usual.
774
- this.selectedSort = this.defaultSortField;
775
- this.sortDirection = this.defaultSortDirection;
776
- }
777
-
778
- this.toggleSortDirection();
779
- }
780
-
781
- private setSelectedSort(sort: SortField) {
782
- this.selectedSort = sort;
783
- // Apply this field's default sort direction
784
- const sortOption = SORT_OPTIONS[sort];
785
- this.sortDirection = sortOption.defaultSortDirection;
786
- this.emitSortChangedEvent();
787
- }
788
-
789
- /** The current sort field, or the default one if no explicit sort is set */
790
- private get finalizedSortField(): SortField {
791
- return this.selectedSort === SortField.default
792
- ? this.defaultSortField
793
- : this.selectedSort;
794
- }
795
-
796
- /** The current sort direction, or the default one if no explicit direction is set */
797
- private get finalizedSortDirection(): SortDirection | null {
798
- return this.sortDirection === null
799
- ? this.defaultSortDirection
800
- : this.sortDirection;
801
- }
802
-
803
- /** Whether the sort direction button should be enabled for the current sort */
804
- private get canChangeSortDirection(): boolean {
805
- return SORT_OPTIONS[this.finalizedSortField].canSetDirection;
806
- }
807
-
808
- /**
809
- * There are four date sort options.
810
- *
811
- * This checks to see if the current sort is one of them.
812
- *
813
- * @readonly
814
- * @private
815
- * @type {boolean}
816
- * @memberof SortFilterBar
817
- */
818
- private get dateOptionSelected(): boolean {
819
- const dateSortFields: SortField[] = [
820
- SortField.datefavorited,
821
- SortField.datearchived,
822
- SortField.date,
823
- SortField.datereviewed,
824
- SortField.dateadded,
825
- ];
826
- return dateSortFields.includes(this.finalizedSortField);
827
- }
828
-
829
- /**
830
- * There are two view sort options.
831
- *
832
- * This checks to see if the current sort is one of them.
833
- *
834
- * @readonly
835
- * @private
836
- * @type {boolean}
837
- * @memberof SortFilterBar
838
- */
839
- private get viewOptionSelected(): boolean {
840
- const viewSortFields: SortField[] = [
841
- SortField.alltimeview,
842
- SortField.weeklyview,
843
- ];
844
- return viewSortFields.includes(this.finalizedSortField);
845
- }
846
-
847
- /**
848
- * The default field for the date sort dropdown.
849
- * This is Date Favorited when that option is available, or Date Published otherwise.
850
- */
851
- private get defaultDateSortField(): SortField {
852
- return this.showDateFavorited ? SortField.datefavorited : SortField.date;
853
- }
854
-
855
- /**
856
- * The display name of the last selected date field
857
- *
858
- * @readonly
859
- * @private
860
- * @type {string}
861
- * @memberof SortFilterBar
862
- */
863
- private get dateSortDisplayName(): string {
864
- return SORT_OPTIONS[this.lastSelectedDateSort].displayName;
865
- }
866
-
867
- /**
868
- * The display name of the last selected view field
869
- *
870
- * @readonly
871
- * @private
872
- * @type {string}
873
- * @memberof SortFilterBar
874
- */
875
- private get viewSortDisplayName(): string {
876
- return SORT_OPTIONS[this.lastSelectedViewSort].displayName;
877
- }
878
-
879
- private get titleSelectorBar() {
880
- return html` <alpha-bar
881
- .selectedLetter=${this.selectedTitleFilter}
882
- .letterCounts=${this.prefixFilterCountMap?.title}
883
- ariaLandmarkLabel="Filter by title letter"
884
- @letterChanged=${this.titleLetterChanged}
885
- ></alpha-bar>`;
886
- }
887
-
888
- private get creatorSelectorBar() {
889
- return html` <alpha-bar
890
- .selectedLetter=${this.selectedCreatorFilter}
891
- .letterCounts=${this.prefixFilterCountMap?.creator}
892
- ariaLandmarkLabel="Filter by creator letter"
893
- @letterChanged=${this.creatorLetterChanged}
894
- ></alpha-bar>`;
895
- }
896
-
897
- private titleLetterChanged(
898
- e: CustomEvent<{ selectedLetter: string | undefined }>,
899
- ) {
900
- this.selectedTitleFilter = e.detail.selectedLetter ?? null;
901
- this.emitTitleLetterChangedEvent();
902
- }
903
-
904
- private creatorLetterChanged(
905
- e: CustomEvent<{ selectedLetter: string | undefined }>,
906
- ) {
907
- this.selectedCreatorFilter = e.detail.selectedLetter ?? null;
908
- this.emitCreatorLetterChangedEvent();
909
- }
910
-
911
- private emitTitleLetterChangedEvent() {
912
- const event = new CustomEvent<{ selectedLetter: string | null }>(
913
- 'titleLetterChanged',
914
- {
915
- detail: { selectedLetter: this.selectedTitleFilter },
916
- },
917
- );
918
- this.dispatchEvent(event);
919
- }
920
-
921
- private emitCreatorLetterChangedEvent() {
922
- const event = new CustomEvent<{ selectedLetter: string | null }>(
923
- 'creatorLetterChanged',
924
- {
925
- detail: { selectedLetter: this.selectedCreatorFilter },
926
- },
927
- );
928
- this.dispatchEvent(event);
929
- }
930
-
931
- private displayModeChanged() {
932
- const event = new CustomEvent<{
933
- displayMode?: CollectionDisplayMode;
934
- }>('displayModeChanged', {
935
- detail: { displayMode: this.displayMode },
936
- });
937
- this.dispatchEvent(event);
938
- }
939
-
940
- private emitSortChangedEvent() {
941
- const event = new CustomEvent<{
942
- selectedSort: SortField;
943
- sortDirection: SortDirection | null;
944
- }>('sortChanged', {
945
- detail: {
946
- selectedSort: this.selectedSort,
947
- sortDirection: this.sortDirection,
948
- },
949
- });
950
- this.dispatchEvent(event);
951
- }
952
-
953
- static get styles() {
954
- return [
955
- srOnlyStyle,
956
- css`
957
- #container {
958
- position: relative;
959
- }
960
-
961
- #sort-bar {
962
- display: flex;
963
- justify-content: flex-start;
964
- align-items: center;
965
- border-bottom: 1px solid #2c2c2c;
966
- font-size: 1.4rem;
967
- }
968
-
969
- #sort-options {
970
- display: flex;
971
- align-items: center;
972
- flex-grow: 1;
973
- }
974
-
975
- ul {
976
- list-style: none;
977
- display: flex;
978
- align-items: center;
979
- margin: 0;
980
- padding: 0;
981
- }
982
-
983
- li {
984
- padding: 0;
985
- }
986
-
987
- .sort-by-text {
988
- margin-right: 5px;
989
- font-weight: bold;
990
- white-space: nowrap;
991
- }
992
-
993
- .sort-direction-container {
994
- display: flex;
995
- align-self: stretch;
996
- flex: 0;
997
- margin: 0 5px;
998
- }
999
-
1000
- .sort-direction-selector {
1001
- padding: 0;
1002
- border: none;
1003
- appearance: none;
1004
- background: transparent;
1005
- cursor: pointer;
1006
- }
1007
-
1008
- .sort-direction-selector:disabled {
1009
- cursor: default;
1010
- }
1011
-
1012
- .sort-direction-icon {
1013
- display: flex;
1014
- align-items: center;
1015
- background: none;
1016
- color: inherit;
1017
- border: none;
1018
- padding: 0;
1019
- outline: inherit;
1020
- width: 14px;
1021
- height: 14px;
1022
- }
1023
-
1024
- .sort-direction-icon > svg {
1025
- flex: 1;
1026
- }
1027
-
1028
- #date-sort-selector,
1029
- #view-sort-selector {
1030
- position: absolute;
1031
- left: 150px;
1032
- top: 45px;
1033
-
1034
- z-index: 1;
1035
- padding: 1rem;
1036
- background-color: white;
1037
- border-radius: 2.5rem;
1038
- border: 1px solid #404142;
1039
- }
1040
-
1041
- #sort-selector-container {
1042
- flex: 1;
1043
- display: flex;
1044
- justify-content: flex-start;
1045
- align-items: center;
1046
- }
1047
-
1048
- #desktop-sort-container,
1049
- #mobile-sort-container {
1050
- display: flex;
1051
- justify-content: flex-start;
1052
- align-items: center;
1053
- }
1054
-
1055
- /*
1056
- we move the desktop sort selector offscreen instead of display: none
1057
- because we need to observe the width of it vs its container to determine
1058
- if it's wide enough to display the desktop version and if you display: none,
1059
- the width becomes 0
1060
- */
1061
- #desktop-sort-container.hidden {
1062
- position: absolute;
1063
- top: -9999px;
1064
- left: -9999px;
1065
- visibility: hidden;
1066
- }
1067
-
1068
- #mobile-sort-container.hidden {
1069
- display: none;
1070
- }
1071
-
1072
- #sort-selector-backdrop {
1073
- position: fixed;
1074
- top: 0;
1075
- left: 0;
1076
- width: 100vw;
1077
- height: 100vh;
1078
- z-index: 1;
1079
- background-color: transparent;
1080
- }
1081
-
1082
- #desktop-sort-selector {
1083
- display: inline-flex;
1084
- }
1085
-
1086
- #desktop-sort-selector li {
1087
- display: flex;
1088
- align-items: center;
1089
- padding-left: 5px;
1090
- padding-right: 5px;
1091
- }
1092
-
1093
- #desktop-sort-selector li a {
1094
- padding: 0 5px;
1095
- text-decoration: none;
1096
- color: #333;
1097
- line-height: 2;
1098
- }
1099
-
1100
- #desktop-sort-selector li button {
1101
- padding: 0px 5px;
1102
- border: none;
1103
- background: none;
1104
- font-family: inherit;
1105
- font-size: inherit;
1106
- color: #333;
1107
- line-height: 2;
1108
- cursor: pointer;
1109
- appearance: none;
1110
- }
1111
-
1112
- #desktop-sort-selector li button.selected {
1113
- font-weight: bold;
1114
- }
1115
-
1116
- /**
1117
- * Fix to not shift the sort-bar options when get selected
1118
- */
1119
- #desktop-sort-selector li button::before,
1120
- #desktop-sort-selector .dropdown-label::before {
1121
- display: block;
1122
- content: attr(data-title);
1123
- font-weight: bold;
1124
- height: 0;
1125
- overflow: hidden;
1126
- visibility: hidden;
1127
- }
1128
-
1129
- #display-style-selector {
1130
- flex: 0;
1131
- }
1132
-
1133
- #display-style-selector button {
1134
- background: none;
1135
- color: inherit;
1136
- border: none;
1137
- appearance: none;
1138
- cursor: pointer;
1139
- -webkit-appearance: none;
1140
- fill: #bbbbbb;
1141
- }
1142
-
1143
- #display-style-selector button.active {
1144
- fill: var(--ia-theme-primary-text-color, #2c2c2c);
1145
- }
1146
-
1147
- #display-style-selector button svg {
1148
- width: 24px;
1149
- height: 24px;
1150
- }
1151
-
1152
- ia-dropdown {
1153
- --dropdownTextColor: white;
1154
- --dropdownOffsetTop: 0;
1155
- --dropdownBorderTopWidth: 0;
1156
- --dropdownBorderTopLeftRadius: 0;
1157
- --dropdownBorderTopRightRadius: 0;
1158
- --dropdownWhiteSpace: nowrap;
1159
- --dropdownListZIndex: 2;
1160
- --dropdownCaretColor: var(--ia-theme-primary-text-color, #2c2c2c);
1161
- --dropdownSelectedTextColor: white;
1162
- --dropdownSelectedBgColor: rgba(255, 255, 255, 0.3);
1163
- --dropdownHoverBgColor: rgba(255, 255, 255, 0.3);
1164
- --caretHeight: 9px;
1165
- --caretWidth: 12px;
1166
- --caretPadding: 0 5px 0 0;
1167
- }
1168
- ia-dropdown.selected .dropdown-label {
1169
- font-weight: bold;
1170
- }
1171
- ia-dropdown.open {
1172
- z-index: 2;
1173
- }
1174
-
1175
- .dropdown-label {
1176
- display: inline-block;
1177
- height: 100%;
1178
- padding-left: 5px;
1179
- font-size: 1.4rem;
1180
- font-family: var(--ia-theme-base-font-family);
1181
- line-height: 2;
1182
- color: var(--ia-theme-primary-text-color, #2c2c2c);
1183
- white-space: nowrap;
1184
- user-select: none;
1185
- }
1186
- `,
1187
- ];
1188
- }
1189
- }
1
+ import {
2
+ LitElement,
3
+ html,
4
+ css,
5
+ nothing,
6
+ PropertyValues,
7
+ TemplateResult,
8
+ } from 'lit';
9
+ import { customElement, property, query, state } from 'lit/decorators.js';
10
+ import { msg } from '@lit/localize';
11
+ import type {
12
+ SharedResizeObserverInterface,
13
+ SharedResizeObserverResizeHandlerInterface,
14
+ } from '@internetarchive/shared-resize-observer';
15
+ import '@internetarchive/ia-dropdown';
16
+ import type { IaDropdown, optionInterface } from '@internetarchive/ia-dropdown';
17
+ import type { SortDirection } from '@internetarchive/search-service';
18
+ import {
19
+ CollectionDisplayMode,
20
+ PrefixFilterCounts,
21
+ PrefixFilterType,
22
+ SORT_OPTIONS,
23
+ SortField,
24
+ } from '../models';
25
+ import './alpha-bar';
26
+
27
+ import { sortUpIcon } from './img/sort-toggle-up';
28
+ import { sortDownIcon } from './img/sort-toggle-down';
29
+ import { sortDisabledIcon } from './img/sort-toggle-disabled';
30
+ import { tileIcon } from './img/tile';
31
+ import { listIcon } from './img/list';
32
+ import { compactIcon } from './img/compact';
33
+ import { srOnlyStyle } from '../styles/sr-only';
34
+
35
+ type AlphaSelector = 'creator' | 'title';
36
+
37
+ @customElement('sort-filter-bar')
38
+ export class SortFilterBar
39
+ extends LitElement
40
+ implements SharedResizeObserverResizeHandlerInterface
41
+ {
42
+ /** Which display mode the tiles are being rendered with (grid/list-detail/list-compact) */
43
+ @property({ type: String }) displayMode?: CollectionDisplayMode;
44
+
45
+ /** The default sort direction to use if none is set */
46
+ @property({ type: String }) defaultSortDirection: SortDirection | null = null;
47
+
48
+ /** The default sort field to use if none is set */
49
+ @property({ type: String }) defaultSortField: Exclude<
50
+ SortField,
51
+ SortField.default
52
+ > = SortField.relevance;
53
+
54
+ /** The current sort direction (asc/desc), or null if none is set */
55
+ @property({ type: String }) sortDirection: SortDirection | null = null;
56
+
57
+ /** The field currently being sorted on (e.g., 'title'). Defaults to relevance. */
58
+ @property({ type: String }) selectedSort: SortField = SortField.default;
59
+
60
+ /** The currently selected title letter filter, or null if none is set */
61
+ @property({ type: String }) selectedTitleFilter: string | null = null;
62
+
63
+ /** The currently selected creator letter filter, or null if none is set */
64
+ @property({ type: String }) selectedCreatorFilter: string | null = null;
65
+
66
+ /** Whether to show the Relevance sort option (default `true`) */
67
+ @property({ type: Boolean }) showRelevance: boolean = true;
68
+
69
+ /** Whether to show the Date Favorited sort option instead of Date Published/Archived/Reviewed (default `false`) */
70
+ @property({ type: Boolean }) showDateFavorited: boolean = false;
71
+
72
+ /** Whether to replace the default sort options with a slot for customization (default `false`) */
73
+ @property({ type: Boolean, reflect: true }) enableSortOptionsSlot: boolean =
74
+ false;
75
+
76
+ /** Whether to suppress showing the three display mode options on the right of the bar (default `false`) */
77
+ @property({ type: Boolean, reflect: true })
78
+ suppressDisplayModes: boolean = false;
79
+
80
+ /** Maps of result counts for letters on the alphabet bar, for each letter filter type */
81
+ @property({ type: Object }) prefixFilterCountMap?: Record<
82
+ PrefixFilterType,
83
+ PrefixFilterCounts
84
+ >;
85
+
86
+ @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;
87
+
88
+ /**
89
+ * The Views sort option that was most recently selected (or the default, if none has been selected yet)
90
+ */
91
+ @state() private lastSelectedViewSort = SortField.weeklyview;
92
+
93
+ /**
94
+ * The Date sort option that was most recently selected (or the default, if none has been selected yet)
95
+ */
96
+ @state() private lastSelectedDateSort = this.defaultDateSortField;
97
+
98
+ /**
99
+ * Which of the alphabet bars (title/creator) should be shown, or null if one
100
+ * should not currently be rendered.
101
+ */
102
+ @state() alphaSelectorVisible: AlphaSelector | null = null;
103
+
104
+ /**
105
+ * Whether the transparent backdrop to catch clicks outside the dropdown menu
106
+ * should be rendered.
107
+ */
108
+ @state() dropdownBackdropVisible = false;
109
+
110
+ /**
111
+ * The width of the desktop view sort option container, updated upon each resize.
112
+ * Used for dynamically determining whether to use desktop or mobile view.
113
+ */
114
+ @state() desktopSortContainerWidth = 0;
115
+
116
+ /**
117
+ * The width of the full sort bar, updated upon each resize.
118
+ * Used for dynamically determining whether to use desktop or mobile view.
119
+ */
120
+ @state() selectorBarContainerWidth = 0;
121
+
122
+ /**
123
+ * The container for all the desktop view's sort options.
124
+ * Used for dynamically determining whether to use desktop or mobile view.
125
+ */
126
+ @query('#desktop-sort-container')
127
+ private desktopSortContainer!: HTMLUListElement;
128
+
129
+ /**
130
+ * The container for the full sort bar.
131
+ * Used for dynamically determining whether to use desktop or mobile view.
132
+ */
133
+ @query('#sort-selector-container')
134
+ private sortSelectorContainer!: HTMLDivElement;
135
+
136
+ /** The dropdown component containing options for weekly and all-time views */
137
+ @query('#views-dropdown')
138
+ private viewsDropdown!: IaDropdown;
139
+
140
+ /** The dropdown component containing the four date options */
141
+ @query('#date-dropdown')
142
+ private dateDropdown!: IaDropdown;
143
+
144
+ /** The single, consolidated dropdown component shown in mobile view */
145
+ @query('#mobile-dropdown')
146
+ private mobileDropdown!: IaDropdown;
147
+
148
+ render() {
149
+ return html`
150
+ <div id="container">
151
+ <section id="sort-bar" aria-label="Sorting options">
152
+ <slot name="sort-options-left"></slot>
153
+ <div id="sort-options">
154
+ ${!this.enableSortOptionsSlot
155
+ ? html`
156
+ <div class="sort-direction-container">
157
+ ${this.sortDirectionSelectorTemplate}
158
+ </div>
159
+ <span class="sort-by-text">${msg('Sort by:')}</span>
160
+ <div id="sort-selector-container">
161
+ ${this.mobileSortSelectorTemplate}
162
+ ${this.desktopSortSelectorTemplate}
163
+ </div>
164
+ `
165
+ : html`<slot name="sort-options"></slot>`}
166
+ </div>
167
+ <slot name="sort-options-right"></slot>
168
+
169
+ ${this.suppressDisplayModes
170
+ ? nothing
171
+ : html`<div id="display-style-selector">
172
+ ${this.displayOptionTemplate}
173
+ </div>`}
174
+ </section>
175
+
176
+ ${this.dropdownBackdropVisible ? this.dropdownBackdrop : nothing}
177
+ ${this.alphaBarTemplate}
178
+ </div>
179
+ `;
180
+ }
181
+
182
+ willUpdate(changed: PropertyValues) {
183
+ if (changed.has('selectedSort') || changed.has('defaultSortField')) {
184
+ // If the sort is changed from its default without a direction set,
185
+ // we adopt the default sort direction for that sort type.
186
+ if (
187
+ this.selectedSort &&
188
+ this.selectedSort !== SortField.default &&
189
+ this.sortDirection === null
190
+ ) {
191
+ const sortOption = SORT_OPTIONS[this.finalizedSortField];
192
+ this.sortDirection = sortOption.defaultSortDirection;
193
+ }
194
+
195
+ if (this.viewOptionSelected) {
196
+ this.lastSelectedViewSort = this.finalizedSortField;
197
+ } else if (this.dateOptionSelected) {
198
+ this.lastSelectedDateSort = this.finalizedSortField;
199
+ }
200
+ }
201
+
202
+ // If we change which dropdown options are available, ensure the correct default becomes selected.
203
+ // Currently, Date Favorited is the only dropdown option whose presence/absence can change.
204
+ if (
205
+ changed.has('showDateFavorited') &&
206
+ changed.get('showDateFavorited') !== this.showDateFavorited
207
+ ) {
208
+ this.lastSelectedDateSort = this.defaultDateSortField;
209
+ }
210
+ }
211
+
212
+ updated(changed: PropertyValues) {
213
+ if (changed.has('displayMode')) {
214
+ this.displayModeChanged();
215
+ }
216
+
217
+ if (changed.has('selectedTitleFilter') && this.selectedTitleFilter) {
218
+ this.alphaSelectorVisible = 'title';
219
+ }
220
+
221
+ if (changed.has('selectedCreatorFilter') && this.selectedCreatorFilter) {
222
+ this.alphaSelectorVisible = 'creator';
223
+ }
224
+
225
+ if (changed.has('dropdownBackdropVisible')) {
226
+ this.setupEscapeListeners();
227
+ }
228
+
229
+ if (changed.has('resizeObserver') || changed.has('enableSortOptionsSlot')) {
230
+ const oldObserver = changed.get(
231
+ 'resizeObserver',
232
+ ) as SharedResizeObserverInterface;
233
+ if (oldObserver) this.disconnectResizeObserver(oldObserver);
234
+ this.setupResizeObserver();
235
+ }
236
+ }
237
+
238
+ private setupEscapeListeners() {
239
+ if (this.dropdownBackdropVisible) {
240
+ document.addEventListener(
241
+ 'keydown',
242
+ this.boundSortBarSelectorEscapeListener,
243
+ );
244
+ } else {
245
+ document.removeEventListener(
246
+ 'keydown',
247
+ this.boundSortBarSelectorEscapeListener,
248
+ );
249
+ }
250
+ }
251
+
252
+ private boundSortBarSelectorEscapeListener = (e: KeyboardEvent) => {
253
+ if (e.key === 'Escape') {
254
+ this.closeDropdowns();
255
+ }
256
+ };
257
+
258
+ connectedCallback(): void {
259
+ super.connectedCallback?.();
260
+ this.setupResizeObserver();
261
+ }
262
+
263
+ disconnectedCallback(): void {
264
+ if (this.resizeObserver) {
265
+ this.disconnectResizeObserver(this.resizeObserver);
266
+ }
267
+ }
268
+
269
+ private disconnectResizeObserver(
270
+ resizeObserver: SharedResizeObserverInterface,
271
+ ) {
272
+ if (this.sortSelectorContainer) {
273
+ resizeObserver.removeObserver({
274
+ target: this.sortSelectorContainer,
275
+ handler: this,
276
+ });
277
+ }
278
+
279
+ if (this.desktopSortContainer) {
280
+ resizeObserver.removeObserver({
281
+ target: this.desktopSortContainer,
282
+ handler: this,
283
+ });
284
+ }
285
+ }
286
+
287
+ private setupResizeObserver() {
288
+ if (!this.resizeObserver) return;
289
+
290
+ if (this.sortSelectorContainer) {
291
+ this.resizeObserver.addObserver({
292
+ target: this.sortSelectorContainer,
293
+ handler: this,
294
+ });
295
+ }
296
+
297
+ if (this.desktopSortContainer) {
298
+ this.resizeObserver.addObserver({
299
+ target: this.desktopSortContainer,
300
+ handler: this,
301
+ });
302
+ }
303
+ }
304
+
305
+ handleResize(entry: ResizeObserverEntry): void {
306
+ if (entry.target === this.desktopSortContainer) {
307
+ this.desktopSortContainerWidth = entry.contentRect.width;
308
+ } else if (entry.target === this.sortSelectorContainer) {
309
+ this.selectorBarContainerWidth = entry.contentRect.width;
310
+ }
311
+ }
312
+
313
+ /**
314
+ * Whether to show the mobile sort bar because there is not enough space
315
+ * for the desktop sort bar.
316
+ */
317
+ private get mobileSelectorVisible() {
318
+ return this.selectorBarContainerWidth - 10 < this.desktopSortContainerWidth;
319
+ }
320
+
321
+ /**
322
+ * Template to render the alphabet bar, or `nothing` if it should not be rendered
323
+ * for the current sort
324
+ */
325
+ private get alphaBarTemplate(): TemplateResult | typeof nothing {
326
+ if (!['title', 'creator'].includes(this.selectedSort)) return nothing;
327
+
328
+ if (this.alphaSelectorVisible === null) {
329
+ if (this.selectedSort === 'creator') return this.creatorSelectorBar;
330
+ if (this.selectedSort === 'title') return this.titleSelectorBar;
331
+ } else {
332
+ return this.alphaSelectorVisible === 'creator'
333
+ ? this.creatorSelectorBar
334
+ : this.titleSelectorBar;
335
+ }
336
+
337
+ return nothing;
338
+ }
339
+
340
+ /** Template to render the sort direction toggle button */
341
+ private get sortDirectionSelectorTemplate(): TemplateResult {
342
+ const oppositeSortDirectionReadable =
343
+ this.sortDirection === 'asc' ? 'descending' : 'ascending';
344
+ const srLabel = `Change to ${oppositeSortDirectionReadable} sort`;
345
+
346
+ return html`
347
+ <button
348
+ class="sort-direction-selector"
349
+ ?disabled=${!this.canChangeSortDirection}
350
+ @click=${this.handleSortDirectionClicked}
351
+ >
352
+ <span class="sr-only">${srLabel}</span>
353
+ ${this.sortDirectionIcon}
354
+ </button>
355
+ `;
356
+ }
357
+
358
+ /** Template to render the sort direction button's icon in the correct current state */
359
+ private get sortDirectionIcon(): TemplateResult {
360
+ // Show a fully disabled icon for sort options without direction support
361
+ if (!this.canChangeSortDirection) {
362
+ return html`<div class="sort-direction-icon">${sortDisabledIcon}</div>`;
363
+ }
364
+
365
+ // For all other sorts, show the ascending/descending direction
366
+ return html`
367
+ <div class="sort-direction-icon">
368
+ ${this.finalizedSortDirection === 'asc' ? sortUpIcon : sortDownIcon}
369
+ </div>
370
+ `;
371
+ }
372
+
373
+ /** The template to render all the sort options in desktop view */
374
+ private get desktopSortSelectorTemplate() {
375
+ return html`
376
+ <div
377
+ id="desktop-sort-container"
378
+ class=${this.mobileSelectorVisible ? 'hidden' : 'visible'}
379
+ >
380
+ <ul id="desktop-sort-selector">
381
+ ${this.showRelevance
382
+ ? html`<li>
383
+ ${this.getSortDisplayOption(SortField.relevance, {
384
+ onClick: () => {
385
+ this.dropdownBackdropVisible = false;
386
+ if (this.finalizedSortField !== SortField.relevance) {
387
+ this.clearAlphaBarFilters();
388
+ this.setSelectedSort(SortField.relevance);
389
+ }
390
+ },
391
+ })}
392
+ </li>`
393
+ : nothing}
394
+ <li>${this.viewsDropdownTemplate}</li>
395
+ <li>
396
+ ${this.getSortDisplayOption(SortField.title, {
397
+ onClick: () => {
398
+ this.dropdownBackdropVisible = false;
399
+ if (this.finalizedSortField !== SortField.title) {
400
+ this.alphaSelectorVisible = 'title';
401
+ this.selectedCreatorFilter = null;
402
+ this.setSelectedSort(SortField.title);
403
+ this.emitCreatorLetterChangedEvent();
404
+ }
405
+ },
406
+ })}
407
+ </li>
408
+ <li>${this.dateDropdownTemplate}</li>
409
+ <li>
410
+ ${this.getSortDisplayOption(SortField.creator, {
411
+ onClick: () => {
412
+ this.dropdownBackdropVisible = false;
413
+ if (this.finalizedSortField !== SortField.creator) {
414
+ this.alphaSelectorVisible = 'creator';
415
+ this.selectedTitleFilter = null;
416
+ this.setSelectedSort(SortField.creator);
417
+ this.emitTitleLetterChangedEvent();
418
+ }
419
+ },
420
+ })}
421
+ </li>
422
+ </ul>
423
+ </div>
424
+ `;
425
+ }
426
+
427
+ /** The template to render all the sort options in mobile view */
428
+ private get mobileSortSelectorTemplate() {
429
+ const displayedOptions = Object.values(SORT_OPTIONS)
430
+ .filter(opt => opt.shownInSortBar)
431
+ .filter(opt => this.showRelevance || opt.field !== SortField.relevance)
432
+ .filter(
433
+ opt => this.showDateFavorited || opt.field !== SortField.datefavorited,
434
+ );
435
+
436
+ return html`
437
+ <div
438
+ id="mobile-sort-container"
439
+ class=${this.mobileSelectorVisible ? 'visible' : 'hidden'}
440
+ >
441
+ ${this.getSortDropdown({
442
+ displayName: html`${SORT_OPTIONS[this.finalizedSortField].displayName}`,
443
+ id: 'mobile-dropdown',
444
+ selected: true,
445
+ dropdownOptions: displayedOptions.map(opt =>
446
+ this.getDropdownOption(opt.field),
447
+ ),
448
+ selectedOption: this.finalizedSortField,
449
+ onOptionSelected: this.mobileSortChanged,
450
+ onDropdownClick: () => {
451
+ this.dropdownBackdropVisible = this.mobileDropdown.open;
452
+ this.mobileDropdown.classList.toggle(
453
+ 'open',
454
+ this.mobileDropdown.open,
455
+ );
456
+ },
457
+ })}
458
+ </div>
459
+ `;
460
+ }
461
+
462
+ /**
463
+ * This generates each of the non-dropdown sort option links.
464
+ *
465
+ * It manages the display value and the selected state of the option.
466
+ *
467
+ * @param sortField
468
+ * @param options {
469
+ * onClick?: (e: Event) => void; If this is provided, it will also be called when the option is clicked.
470
+ * displayName?: TemplateResult; The name to display for the option. Defaults to the sortField display name.
471
+ * selected?: boolean; true if the option is selected. Defaults to the selectedSort === sortField.
472
+ * }
473
+ * @returns
474
+ */
475
+ private getSortDisplayOption(
476
+ sortField: SortField,
477
+ options?: {
478
+ displayName?: TemplateResult;
479
+ selected?: boolean;
480
+ onClick?: (e: Event) => void;
481
+ },
482
+ ): TemplateResult {
483
+ const isSelected =
484
+ options?.selected ?? this.finalizedSortField === sortField;
485
+ const displayName =
486
+ options?.displayName ?? SORT_OPTIONS[sortField].displayName;
487
+ return html`
488
+ <button
489
+ class=${isSelected ? 'selected' : nothing}
490
+ data-title="${displayName}"
491
+ @click=${(e: Event) => {
492
+ e.preventDefault();
493
+ options?.onClick?.(e);
494
+ }}
495
+ >
496
+ ${displayName}
497
+ </button>
498
+ `;
499
+ }
500
+
501
+ /**
502
+ * Generates a dropdown component containing multiple grouped sort options.
503
+ *
504
+ * @param options.displayName The name to use for the dropdown's visible label
505
+ * @param options.id The id to apply to the dropdown element
506
+ * @param options.dropdownOptions An array of option objects used to populate the dropdown
507
+ * @param options.selectedOption The id of the option that should be initially selected
508
+ * @param options.selected A boolean indicating whether this dropdown should use its
509
+ * selected appearance
510
+ * @param options.onOptionSelected A handler for optionSelected events coming from the dropdown
511
+ * @param options.onDropdownClick A handler for click events on the dropdown
512
+ * @param options.onLabelInteraction A handler for click events and Enter/Space keydown events
513
+ * on the dropdown's label
514
+ */
515
+ private getSortDropdown(options: {
516
+ displayName: TemplateResult;
517
+ id?: string;
518
+ dropdownOptions: optionInterface[];
519
+ selectedOption?: string;
520
+ selected: boolean;
521
+ onOptionSelected?: (e: CustomEvent<{ option: optionInterface }>) => void;
522
+ onDropdownClick?: (e: PointerEvent) => void;
523
+ onLabelInteraction?: (e: Event) => void;
524
+ }): TemplateResult {
525
+ return html`
526
+ <ia-dropdown
527
+ id=${options.id ?? nothing}
528
+ class=${options.selected ? 'selected' : nothing}
529
+ displayCaret
530
+ closeOnSelect
531
+ includeSelectedOption
532
+ .openViaButton=${options.selected}
533
+ .options=${options.dropdownOptions}
534
+ .selectedOption=${options.selectedOption ?? ''}
535
+ @optionSelected=${options.onOptionSelected ?? nothing}
536
+ @click=${options.onDropdownClick ?? nothing}
537
+ >
538
+ <span
539
+ class="dropdown-label"
540
+ slot="dropdown-label"
541
+ data-title="${options.displayName.values}"
542
+ @click=${options.onLabelInteraction ?? nothing}
543
+ @keydown=${options.onLabelInteraction
544
+ ? (e: KeyboardEvent) => {
545
+ if (e.key === 'Enter' || e.key === ' ') {
546
+ options.onLabelInteraction?.(e);
547
+ }
548
+ }
549
+ : nothing}
550
+ >
551
+ ${options.displayName}
552
+ </span>
553
+ </ia-dropdown>
554
+ `;
555
+ }
556
+
557
+ /** Generates a single dropdown option object for the given sort field */
558
+ private getDropdownOption(sortField: SortField): optionInterface {
559
+ return {
560
+ id: sortField,
561
+ selectedHandler: () => {
562
+ this.selectDropdownSortField(sortField);
563
+ },
564
+ label: html`
565
+ <span class="dropdown-option-label">
566
+ ${SORT_OPTIONS[sortField].displayName}
567
+ </span>
568
+ `,
569
+ };
570
+ }
571
+
572
+ /** Handler for when any sort dropdown option is selected */
573
+ private dropdownOptionSelected(e: CustomEvent<{ option: optionInterface }>) {
574
+ this.dropdownBackdropVisible = false;
575
+ this.clearAlphaBarFilters();
576
+
577
+ const sortField = e.detail.option.id as SortField;
578
+ this.setSelectedSort(sortField);
579
+ if (this.viewOptionSelected) {
580
+ this.lastSelectedViewSort = sortField;
581
+ } else if (this.dateOptionSelected) {
582
+ this.lastSelectedDateSort = sortField;
583
+ }
584
+ }
585
+
586
+ /** The template to render for the views dropdown */
587
+ private get viewsDropdownTemplate(): TemplateResult {
588
+ return this.getSortDropdown({
589
+ displayName: html`${this.viewSortDisplayName}`,
590
+ id: 'views-dropdown',
591
+ selected: this.viewOptionSelected,
592
+ dropdownOptions: [
593
+ this.getDropdownOption(SortField.weeklyview),
594
+ this.getDropdownOption(SortField.alltimeview),
595
+ ],
596
+ selectedOption: this.viewOptionSelected ? this.finalizedSortField : '',
597
+ onOptionSelected: this.dropdownOptionSelected,
598
+ onDropdownClick: () => {
599
+ this.dateDropdown.open = false;
600
+ this.dropdownBackdropVisible = this.viewsDropdown.open;
601
+ this.viewsDropdown.classList.toggle('open', this.viewsDropdown.open);
602
+ },
603
+ onLabelInteraction: (e: Event) => {
604
+ if (!this.viewsDropdown.open && !this.viewOptionSelected) {
605
+ e.stopPropagation();
606
+ this.clearAlphaBarFilters();
607
+ this.setSelectedSort(this.lastSelectedViewSort);
608
+ }
609
+ },
610
+ });
611
+ }
612
+
613
+ /** The template to render for the date dropdown */
614
+ private get dateDropdownTemplate(): TemplateResult {
615
+ return this.getSortDropdown({
616
+ displayName: html`${this.dateSortDisplayName}`,
617
+ id: 'date-dropdown',
618
+ selected: this.dateOptionSelected,
619
+ dropdownOptions: [
620
+ ...(this.showDateFavorited
621
+ ? [this.getDropdownOption(SortField.datefavorited)]
622
+ : []),
623
+ this.getDropdownOption(SortField.date),
624
+ this.getDropdownOption(SortField.datearchived),
625
+ this.getDropdownOption(SortField.datereviewed),
626
+ this.getDropdownOption(SortField.dateadded),
627
+ ],
628
+ selectedOption: this.dateOptionSelected ? this.finalizedSortField : '',
629
+ onOptionSelected: this.dropdownOptionSelected,
630
+ onDropdownClick: () => {
631
+ this.viewsDropdown.open = false;
632
+ this.dropdownBackdropVisible = this.dateDropdown.open;
633
+ this.dateDropdown.classList.toggle('open', this.dateDropdown.open);
634
+ },
635
+ onLabelInteraction: (e: Event) => {
636
+ if (!this.dateDropdown.open && !this.dateOptionSelected) {
637
+ e.stopPropagation();
638
+ this.clearAlphaBarFilters();
639
+ this.setSelectedSort(this.lastSelectedDateSort);
640
+ }
641
+ },
642
+ });
643
+ }
644
+
645
+ /** Handler for when a new mobile sort dropdown option is selected */
646
+ private mobileSortChanged(e: CustomEvent<{ option: optionInterface }>) {
647
+ this.dropdownBackdropVisible = false;
648
+
649
+ const sortField = e.detail.option.id as SortField;
650
+ this.setSelectedSort(sortField);
651
+
652
+ this.alphaSelectorVisible = null;
653
+ if (sortField !== 'title' && this.selectedTitleFilter) {
654
+ this.selectedTitleFilter = null;
655
+ this.emitTitleLetterChangedEvent();
656
+ }
657
+ if (sortField !== 'creator' && this.selectedCreatorFilter) {
658
+ this.selectedCreatorFilter = null;
659
+ this.emitCreatorLetterChangedEvent();
660
+ }
661
+ }
662
+
663
+ /** Template for rendering the three display mode options */
664
+ /** Added data-testid for Playwright testing * */
665
+ private get displayOptionTemplate() {
666
+ return html`
667
+ <ul>
668
+ <li>
669
+ <button
670
+ id="grid-button"
671
+ @click=${() => {
672
+ this.displayMode = 'grid';
673
+ }}
674
+ class=${this.displayMode === 'grid' ? 'active' : ''}
675
+ title="Tile view"
676
+ data-testid="grid-button"
677
+ >
678
+ ${tileIcon}
679
+ </button>
680
+ </li>
681
+ <li>
682
+ <button
683
+ id="list-detail-button"
684
+ @click=${() => {
685
+ this.displayMode = 'list-detail';
686
+ }}
687
+ class=${this.displayMode === 'list-detail' ? 'active' : ''}
688
+ title="List view"
689
+ data-testid="list-detail-button"
690
+ >
691
+ ${listIcon}
692
+ </button>
693
+ </li>
694
+ <li>
695
+ <button
696
+ id="list-compact-button"
697
+ @click=${() => {
698
+ this.displayMode = 'list-compact';
699
+ }}
700
+ class=${this.displayMode === 'list-compact' ? 'active' : ''}
701
+ title="Compact list view"
702
+ data-testid="list-compact-button"
703
+ >
704
+ ${compactIcon}
705
+ </button>
706
+ </li>
707
+ </ul>
708
+ `;
709
+ }
710
+
711
+ /**
712
+ * Template for rendering the transparent backdrop to capture clicks outside the
713
+ * dropdown menu while it is open.
714
+ */
715
+ private get dropdownBackdrop() {
716
+ return html`
717
+ <div
718
+ id="sort-selector-backdrop"
719
+ @keyup=${this.closeDropdowns}
720
+ @click=${this.closeDropdowns}
721
+ ></div>
722
+ `;
723
+ }
724
+
725
+ /** Closes all of the sorting dropdown components' menus */
726
+ private closeDropdowns() {
727
+ this.dropdownBackdropVisible = false;
728
+ const allDropdowns = [
729
+ this.viewsDropdown,
730
+ this.dateDropdown,
731
+ this.mobileDropdown,
732
+ ];
733
+ for (const dropdown of allDropdowns) {
734
+ dropdown.open = false;
735
+ dropdown.classList.remove('open');
736
+ }
737
+ }
738
+
739
+ private selectDropdownSortField(sortField: SortField) {
740
+ // When a dropdown sort option is selected, we additionally need to clear the backdrop
741
+ this.dropdownBackdropVisible = false;
742
+ this.setSelectedSort(sortField);
743
+ }
744
+
745
+ private clearAlphaBarFilters() {
746
+ this.alphaSelectorVisible = null;
747
+ this.selectedTitleFilter = null;
748
+ this.selectedCreatorFilter = null;
749
+ this.emitTitleLetterChangedEvent();
750
+ this.emitCreatorLetterChangedEvent();
751
+ }
752
+
753
+ private setSortDirection(sortDirection: SortDirection) {
754
+ this.sortDirection = sortDirection;
755
+ this.emitSortChangedEvent();
756
+ }
757
+
758
+ /** Toggles the current sort direction between 'asc' and 'desc' */
759
+ private toggleSortDirection() {
760
+ this.setSortDirection(
761
+ this.finalizedSortDirection === 'desc' ? 'asc' : 'desc',
762
+ );
763
+ }
764
+
765
+ private handleSortDirectionClicked(): void {
766
+ if (
767
+ !this.sortDirection &&
768
+ this.defaultSortField &&
769
+ this.defaultSortDirection
770
+ ) {
771
+ // When the sort direction is merely defaulted (not set by the user), clicking
772
+ // the toggled button should "promote" the default sort to an explicitly-set one
773
+ // and then toggle it as usual.
774
+ this.selectedSort = this.defaultSortField;
775
+ this.sortDirection = this.defaultSortDirection;
776
+ }
777
+
778
+ this.toggleSortDirection();
779
+ }
780
+
781
+ private setSelectedSort(sort: SortField) {
782
+ this.selectedSort = sort;
783
+ // Apply this field's default sort direction
784
+ const sortOption = SORT_OPTIONS[sort];
785
+ this.sortDirection = sortOption.defaultSortDirection;
786
+ this.emitSortChangedEvent();
787
+ }
788
+
789
+ /** The current sort field, or the default one if no explicit sort is set */
790
+ private get finalizedSortField(): SortField {
791
+ return this.selectedSort === SortField.default
792
+ ? this.defaultSortField
793
+ : this.selectedSort;
794
+ }
795
+
796
+ /** The current sort direction, or the default one if no explicit direction is set */
797
+ private get finalizedSortDirection(): SortDirection | null {
798
+ return this.sortDirection === null
799
+ ? this.defaultSortDirection
800
+ : this.sortDirection;
801
+ }
802
+
803
+ /** Whether the sort direction button should be enabled for the current sort */
804
+ private get canChangeSortDirection(): boolean {
805
+ return SORT_OPTIONS[this.finalizedSortField].canSetDirection;
806
+ }
807
+
808
+ /**
809
+ * There are four date sort options.
810
+ *
811
+ * This checks to see if the current sort is one of them.
812
+ *
813
+ * @readonly
814
+ * @private
815
+ * @type {boolean}
816
+ * @memberof SortFilterBar
817
+ */
818
+ private get dateOptionSelected(): boolean {
819
+ const dateSortFields: SortField[] = [
820
+ SortField.datefavorited,
821
+ SortField.datearchived,
822
+ SortField.date,
823
+ SortField.datereviewed,
824
+ SortField.dateadded,
825
+ ];
826
+ return dateSortFields.includes(this.finalizedSortField);
827
+ }
828
+
829
+ /**
830
+ * There are two view sort options.
831
+ *
832
+ * This checks to see if the current sort is one of them.
833
+ *
834
+ * @readonly
835
+ * @private
836
+ * @type {boolean}
837
+ * @memberof SortFilterBar
838
+ */
839
+ private get viewOptionSelected(): boolean {
840
+ const viewSortFields: SortField[] = [
841
+ SortField.alltimeview,
842
+ SortField.weeklyview,
843
+ ];
844
+ return viewSortFields.includes(this.finalizedSortField);
845
+ }
846
+
847
+ /**
848
+ * The default field for the date sort dropdown.
849
+ * This is Date Favorited when that option is available, or Date Published otherwise.
850
+ */
851
+ private get defaultDateSortField(): SortField {
852
+ return this.showDateFavorited ? SortField.datefavorited : SortField.date;
853
+ }
854
+
855
+ /**
856
+ * The display name of the last selected date field
857
+ *
858
+ * @readonly
859
+ * @private
860
+ * @type {string}
861
+ * @memberof SortFilterBar
862
+ */
863
+ private get dateSortDisplayName(): string {
864
+ return SORT_OPTIONS[this.lastSelectedDateSort].displayName;
865
+ }
866
+
867
+ /**
868
+ * The display name of the last selected view field
869
+ *
870
+ * @readonly
871
+ * @private
872
+ * @type {string}
873
+ * @memberof SortFilterBar
874
+ */
875
+ private get viewSortDisplayName(): string {
876
+ return SORT_OPTIONS[this.lastSelectedViewSort].displayName;
877
+ }
878
+
879
+ private get titleSelectorBar() {
880
+ return html` <alpha-bar
881
+ .selectedLetter=${this.selectedTitleFilter}
882
+ .letterCounts=${this.prefixFilterCountMap?.title}
883
+ ariaLandmarkLabel="Filter by title letter"
884
+ @letterChanged=${this.titleLetterChanged}
885
+ ></alpha-bar>`;
886
+ }
887
+
888
+ private get creatorSelectorBar() {
889
+ return html` <alpha-bar
890
+ .selectedLetter=${this.selectedCreatorFilter}
891
+ .letterCounts=${this.prefixFilterCountMap?.creator}
892
+ ariaLandmarkLabel="Filter by creator letter"
893
+ @letterChanged=${this.creatorLetterChanged}
894
+ ></alpha-bar>`;
895
+ }
896
+
897
+ private titleLetterChanged(
898
+ e: CustomEvent<{ selectedLetter: string | undefined }>,
899
+ ) {
900
+ this.selectedTitleFilter = e.detail.selectedLetter ?? null;
901
+ this.emitTitleLetterChangedEvent();
902
+ }
903
+
904
+ private creatorLetterChanged(
905
+ e: CustomEvent<{ selectedLetter: string | undefined }>,
906
+ ) {
907
+ this.selectedCreatorFilter = e.detail.selectedLetter ?? null;
908
+ this.emitCreatorLetterChangedEvent();
909
+ }
910
+
911
+ private emitTitleLetterChangedEvent() {
912
+ const event = new CustomEvent<{ selectedLetter: string | null }>(
913
+ 'titleLetterChanged',
914
+ {
915
+ detail: { selectedLetter: this.selectedTitleFilter },
916
+ },
917
+ );
918
+ this.dispatchEvent(event);
919
+ }
920
+
921
+ private emitCreatorLetterChangedEvent() {
922
+ const event = new CustomEvent<{ selectedLetter: string | null }>(
923
+ 'creatorLetterChanged',
924
+ {
925
+ detail: { selectedLetter: this.selectedCreatorFilter },
926
+ },
927
+ );
928
+ this.dispatchEvent(event);
929
+ }
930
+
931
+ private displayModeChanged() {
932
+ const event = new CustomEvent<{
933
+ displayMode?: CollectionDisplayMode;
934
+ }>('displayModeChanged', {
935
+ detail: { displayMode: this.displayMode },
936
+ });
937
+ this.dispatchEvent(event);
938
+ }
939
+
940
+ private emitSortChangedEvent() {
941
+ const event = new CustomEvent<{
942
+ selectedSort: SortField;
943
+ sortDirection: SortDirection | null;
944
+ }>('sortChanged', {
945
+ detail: {
946
+ selectedSort: this.selectedSort,
947
+ sortDirection: this.sortDirection,
948
+ },
949
+ });
950
+ this.dispatchEvent(event);
951
+ }
952
+
953
+ static get styles() {
954
+ return [
955
+ srOnlyStyle,
956
+ css`
957
+ #container {
958
+ position: relative;
959
+ }
960
+
961
+ #sort-bar {
962
+ display: flex;
963
+ justify-content: flex-start;
964
+ align-items: center;
965
+ border-bottom: 1px solid #2c2c2c;
966
+ font-size: 1.4rem;
967
+ }
968
+
969
+ #sort-options {
970
+ display: flex;
971
+ align-items: center;
972
+ flex-grow: 1;
973
+ }
974
+
975
+ ul {
976
+ list-style: none;
977
+ display: flex;
978
+ align-items: center;
979
+ margin: 0;
980
+ padding: 0;
981
+ }
982
+
983
+ li {
984
+ padding: 0;
985
+ }
986
+
987
+ .sort-by-text {
988
+ margin-right: 5px;
989
+ font-weight: bold;
990
+ white-space: nowrap;
991
+ }
992
+
993
+ .sort-direction-container {
994
+ display: flex;
995
+ align-self: stretch;
996
+ flex: 0;
997
+ margin: 0 5px;
998
+ }
999
+
1000
+ .sort-direction-selector {
1001
+ padding: 0;
1002
+ border: none;
1003
+ appearance: none;
1004
+ background: transparent;
1005
+ cursor: pointer;
1006
+ }
1007
+
1008
+ .sort-direction-selector:disabled {
1009
+ cursor: default;
1010
+ }
1011
+
1012
+ .sort-direction-icon {
1013
+ display: flex;
1014
+ align-items: center;
1015
+ background: none;
1016
+ color: inherit;
1017
+ border: none;
1018
+ padding: 0;
1019
+ outline: inherit;
1020
+ width: 14px;
1021
+ height: 14px;
1022
+ }
1023
+
1024
+ .sort-direction-icon > svg {
1025
+ flex: 1;
1026
+ }
1027
+
1028
+ #date-sort-selector,
1029
+ #view-sort-selector {
1030
+ position: absolute;
1031
+ left: 150px;
1032
+ top: 45px;
1033
+
1034
+ z-index: 1;
1035
+ padding: 1rem;
1036
+ background-color: white;
1037
+ border-radius: 2.5rem;
1038
+ border: 1px solid #404142;
1039
+ }
1040
+
1041
+ #sort-selector-container {
1042
+ flex: 1;
1043
+ display: flex;
1044
+ justify-content: flex-start;
1045
+ align-items: center;
1046
+ }
1047
+
1048
+ #desktop-sort-container,
1049
+ #mobile-sort-container {
1050
+ display: flex;
1051
+ justify-content: flex-start;
1052
+ align-items: center;
1053
+ }
1054
+
1055
+ /*
1056
+ we move the desktop sort selector offscreen instead of display: none
1057
+ because we need to observe the width of it vs its container to determine
1058
+ if it's wide enough to display the desktop version and if you display: none,
1059
+ the width becomes 0
1060
+ */
1061
+ #desktop-sort-container.hidden {
1062
+ position: absolute;
1063
+ top: -9999px;
1064
+ left: -9999px;
1065
+ visibility: hidden;
1066
+ }
1067
+
1068
+ #mobile-sort-container.hidden {
1069
+ display: none;
1070
+ }
1071
+
1072
+ #sort-selector-backdrop {
1073
+ position: fixed;
1074
+ top: 0;
1075
+ left: 0;
1076
+ width: 100vw;
1077
+ height: 100vh;
1078
+ z-index: 1;
1079
+ background-color: transparent;
1080
+ }
1081
+
1082
+ #desktop-sort-selector {
1083
+ display: inline-flex;
1084
+ }
1085
+
1086
+ #desktop-sort-selector li {
1087
+ display: flex;
1088
+ align-items: center;
1089
+ padding-left: 5px;
1090
+ padding-right: 5px;
1091
+ }
1092
+
1093
+ #desktop-sort-selector li a {
1094
+ padding: 0 5px;
1095
+ text-decoration: none;
1096
+ color: #333;
1097
+ line-height: 2;
1098
+ }
1099
+
1100
+ #desktop-sort-selector li button {
1101
+ padding: 0px 5px;
1102
+ border: none;
1103
+ background: none;
1104
+ font-family: inherit;
1105
+ font-size: inherit;
1106
+ color: #333;
1107
+ line-height: 2;
1108
+ cursor: pointer;
1109
+ appearance: none;
1110
+ }
1111
+
1112
+ #desktop-sort-selector li button.selected {
1113
+ font-weight: bold;
1114
+ }
1115
+
1116
+ /**
1117
+ * Fix to not shift the sort-bar options when get selected
1118
+ */
1119
+ #desktop-sort-selector li button::before,
1120
+ #desktop-sort-selector .dropdown-label::before {
1121
+ display: block;
1122
+ content: attr(data-title);
1123
+ font-weight: bold;
1124
+ height: 0;
1125
+ overflow: hidden;
1126
+ visibility: hidden;
1127
+ }
1128
+
1129
+ #display-style-selector {
1130
+ flex: 0;
1131
+ }
1132
+
1133
+ #display-style-selector button {
1134
+ background: none;
1135
+ color: inherit;
1136
+ border: none;
1137
+ appearance: none;
1138
+ cursor: pointer;
1139
+ -webkit-appearance: none;
1140
+ fill: #bbbbbb;
1141
+ }
1142
+
1143
+ #display-style-selector button.active {
1144
+ fill: var(--ia-theme-primary-text-color, #2c2c2c);
1145
+ }
1146
+
1147
+ #display-style-selector button svg {
1148
+ width: 24px;
1149
+ height: 24px;
1150
+ }
1151
+
1152
+ ia-dropdown {
1153
+ --dropdownTextColor: white;
1154
+ --dropdownOffsetTop: 0;
1155
+ --dropdownBorderTopWidth: 0;
1156
+ --dropdownBorderTopLeftRadius: 0;
1157
+ --dropdownBorderTopRightRadius: 0;
1158
+ --dropdownWhiteSpace: nowrap;
1159
+ --dropdownListZIndex: 2;
1160
+ --dropdownCaretColor: var(--ia-theme-primary-text-color, #2c2c2c);
1161
+ --dropdownSelectedTextColor: white;
1162
+ --dropdownSelectedBgColor: rgba(255, 255, 255, 0.3);
1163
+ --dropdownHoverBgColor: rgba(255, 255, 255, 0.3);
1164
+ --caretHeight: 9px;
1165
+ --caretWidth: 12px;
1166
+ --caretPadding: 0 5px 0 0;
1167
+ }
1168
+ ia-dropdown.selected .dropdown-label {
1169
+ font-weight: bold;
1170
+ }
1171
+ ia-dropdown.open {
1172
+ z-index: 2;
1173
+ }
1174
+
1175
+ .dropdown-label {
1176
+ display: inline-block;
1177
+ height: 100%;
1178
+ padding-left: 5px;
1179
+ font-size: 1.4rem;
1180
+ font-family: var(--ia-theme-base-font-family);
1181
+ line-height: 2;
1182
+ color: var(--ia-theme-primary-text-color, #2c2c2c);
1183
+ white-space: nowrap;
1184
+ user-select: none;
1185
+ }
1186
+ `,
1187
+ ];
1188
+ }
1189
+ }