@internetarchive/collection-browser 4.3.0 → 4.3.1-alpha-webdev8257.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/app-root.d.ts +11 -0
- package/dist/src/app-root.js +107 -0
- package/dist/src/app-root.js.map +1 -1
- package/dist/src/collection-browser.d.ts +8 -4
- package/dist/src/collection-browser.js +19 -20
- package/dist/src/collection-browser.js.map +1 -1
- package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
- package/dist/src/manage/manage-bar.d.ts +7 -2
- package/dist/src/manage/manage-bar.js +25 -3
- package/dist/src/manage/manage-bar.js.map +1 -1
- package/dist/src/styles/tile-action-styles.d.ts +14 -0
- package/dist/src/styles/tile-action-styles.js +52 -0
- package/dist/src/styles/tile-action-styles.js.map +1 -0
- package/dist/src/tiles/base-tile-component.d.ts +17 -1
- package/dist/src/tiles/base-tile-component.js +48 -1
- package/dist/src/tiles/base-tile-component.js.map +1 -1
- package/dist/src/tiles/grid/item-tile.js +1 -0
- package/dist/src/tiles/grid/item-tile.js.map +1 -1
- package/dist/src/tiles/list/tile-list-compact-header.js +66 -46
- package/dist/src/tiles/list/tile-list-compact-header.js.map +1 -1
- package/dist/src/tiles/list/tile-list-compact.d.ts +1 -1
- package/dist/src/tiles/list/tile-list-compact.js +132 -100
- package/dist/src/tiles/list/tile-list-compact.js.map +1 -1
- package/dist/src/tiles/list/tile-list.d.ts +1 -1
- package/dist/src/tiles/list/tile-list.js +316 -298
- package/dist/src/tiles/list/tile-list.js.map +1 -1
- package/dist/src/tiles/models.d.ts +14 -0
- package/dist/src/tiles/models.js.map +1 -1
- package/dist/src/tiles/tile-dispatcher.d.ts +14 -0
- package/dist/src/tiles/tile-dispatcher.js +107 -4
- package/dist/src/tiles/tile-dispatcher.js.map +1 -1
- package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
- package/dist/test/data-source/collection-browser-data-source.test.js +2 -2
- package/dist/test/data-source/collection-browser-data-source.test.js.map +1 -1
- package/dist/test/manage/manage-bar.test.d.ts +1 -0
- package/dist/test/manage/manage-bar.test.js +123 -1
- package/dist/test/manage/manage-bar.test.js.map +1 -1
- package/dist/test/tiles/list/tile-list-compact-header.test.js +12 -12
- package/dist/test/tiles/list/tile-list-compact-header.test.js.map +1 -1
- package/dist/test/tiles/list/tile-list.test.js +134 -134
- package/dist/test/tiles/list/tile-list.test.js.map +1 -1
- package/index.ts +1 -0
- package/package.json +1 -1
- package/src/app-root.ts +115 -0
- package/src/collection-browser.ts +16 -30
- package/src/data-source/collection-browser-data-source.ts +1465 -1465
- package/src/manage/manage-bar.ts +33 -4
- package/src/styles/tile-action-styles.ts +52 -0
- package/src/tiles/base-tile-component.ts +57 -1
- package/src/tiles/grid/item-tile.ts +1 -0
- package/src/tiles/list/tile-list-compact-header.ts +106 -86
- package/src/tiles/list/tile-list-compact.ts +273 -239
- package/src/tiles/list/tile-list.ts +718 -700
- package/src/tiles/models.ts +16 -0
- package/src/tiles/tile-dispatcher.ts +114 -4
- package/src/tiles/tile-display-value-provider.ts +134 -134
- package/test/data-source/collection-browser-data-source.test.ts +193 -193
- package/test/manage/manage-bar.test.ts +192 -2
- package/test/tiles/list/tile-list-compact-header.test.ts +43 -43
- package/test/tiles/list/tile-list.test.ts +576 -576
- package/.claude/settings.local.json +0 -11
package/src/tiles/models.ts
CHANGED
|
@@ -6,3 +6,19 @@
|
|
|
6
6
|
* - `minimal`: Show neither tile stats nor the text snippets.
|
|
7
7
|
*/
|
|
8
8
|
export type LayoutType = 'default' | 'stats-only' | 'snippets-only' | 'minimal';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Describes an action button to render at the bottom of a tile.
|
|
12
|
+
* Styling is controlled via CSS custom properties on the host:
|
|
13
|
+
* - `--tileActionColor` (default: #333)
|
|
14
|
+
* - `--tileActionBg` (default: #fff)
|
|
15
|
+
* - `--tileActionHoverBg` (default: #f0f0f0)
|
|
16
|
+
* - `--tileActionSeparatorColor` (default: #ddd)
|
|
17
|
+
*/
|
|
18
|
+
export interface TileAction {
|
|
19
|
+
/** Unique identifier for this action */
|
|
20
|
+
id: string;
|
|
21
|
+
|
|
22
|
+
/** Label text displayed on the button */
|
|
23
|
+
label: string;
|
|
24
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { css, html, nothing, PropertyValues } from 'lit';
|
|
2
2
|
import { customElement, property, query } from 'lit/decorators.js';
|
|
3
|
+
import { classMap } from 'lit/directives/class-map.js';
|
|
3
4
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
|
4
5
|
import { msg } from '@lit/localize';
|
|
5
6
|
import type {
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
HoverPaneProviderInterface,
|
|
27
28
|
} from './hover/hover-pane-controller';
|
|
28
29
|
import { srOnlyStyle } from '../styles/sr-only';
|
|
30
|
+
import { tileActionStyles } from '../styles/tile-action-styles';
|
|
29
31
|
|
|
30
32
|
@customElement('tile-dispatcher')
|
|
31
33
|
export class TileDispatcher
|
|
@@ -37,6 +39,7 @@ export class TileDispatcher
|
|
|
37
39
|
/*
|
|
38
40
|
* Reactive properties inherited from BaseTileComponent:
|
|
39
41
|
* - model?: TileModel;
|
|
42
|
+
* - tileActions: TileAction[] = [];
|
|
40
43
|
* - currentWidth?: number;
|
|
41
44
|
* - currentHeight?: number;
|
|
42
45
|
* - baseNavigationUrl?: string;
|
|
@@ -104,14 +107,20 @@ export class TileDispatcher
|
|
|
104
107
|
|
|
105
108
|
render() {
|
|
106
109
|
const isGridMode = this.tileDisplayMode === 'grid';
|
|
110
|
+
const hasTileActions = isGridMode && this.showGridTileActions;
|
|
107
111
|
const hoverPaneTemplate =
|
|
108
112
|
this.hoverPaneController?.getTemplate() ?? nothing;
|
|
113
|
+
const containerClasses = classMap({
|
|
114
|
+
hoverable: isGridMode,
|
|
115
|
+
'has-tile-actions': hasTileActions,
|
|
116
|
+
});
|
|
109
117
|
return html`
|
|
110
|
-
<div id="container" class=${
|
|
118
|
+
<div id="container" class=${containerClasses}>
|
|
111
119
|
${this.tileDisplayMode === 'list-header'
|
|
112
120
|
? this.headerTemplate
|
|
113
121
|
: this.tileTemplate}
|
|
114
|
-
${this.
|
|
122
|
+
${this.gridTileActionsTemplate} ${this.manageCheckTemplate}
|
|
123
|
+
${hoverPaneTemplate}
|
|
115
124
|
</div>
|
|
116
125
|
`;
|
|
117
126
|
}
|
|
@@ -134,6 +143,7 @@ export class TileDispatcher
|
|
|
134
143
|
.currentWidth=${currentWidth}
|
|
135
144
|
.sortParam=${sortParam ?? defaultSortParam}
|
|
136
145
|
.mobileBreakpoint=${mobileBreakpoint}
|
|
146
|
+
.tileActions=${this.tileActions}
|
|
137
147
|
>
|
|
138
148
|
</tile-list-compact-header>
|
|
139
149
|
`;
|
|
@@ -306,6 +316,52 @@ export class TileDispatcher
|
|
|
306
316
|
});
|
|
307
317
|
}
|
|
308
318
|
|
|
319
|
+
/** Whether tile action buttons should be rendered in grid mode */
|
|
320
|
+
private get showGridTileActions(): boolean {
|
|
321
|
+
return (
|
|
322
|
+
this.tileActions.length > 0 &&
|
|
323
|
+
!this.isManageView &&
|
|
324
|
+
this.tileDisplayMode === 'grid'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Template for the grid-mode action buttons. Rendered alongside the inner
|
|
330
|
+
* tile link inside the dispatcher's shadow root so the action buttons can
|
|
331
|
+
* suppress the hover pane on hover.
|
|
332
|
+
*/
|
|
333
|
+
private get gridTileActionsTemplate() {
|
|
334
|
+
if (!this.showGridTileActions) return nothing;
|
|
335
|
+
|
|
336
|
+
return html`
|
|
337
|
+
<div
|
|
338
|
+
class="tile-actions grid-tile-actions"
|
|
339
|
+
@mouseenter=${this.handleGridActionsMouseEnter}
|
|
340
|
+
@mousemove=${(e: Event) => e.stopPropagation()}
|
|
341
|
+
>
|
|
342
|
+
${this.tileActions.map(
|
|
343
|
+
action => html`
|
|
344
|
+
<button
|
|
345
|
+
class="tile-action-btn"
|
|
346
|
+
@click=${(e: Event) => this.handleTileActionClick(e, action)}
|
|
347
|
+
>
|
|
348
|
+
${action.label}
|
|
349
|
+
</button>
|
|
350
|
+
`,
|
|
351
|
+
)}
|
|
352
|
+
</div>
|
|
353
|
+
`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* When the mouse enters the grid-mode tile actions area, dispatch a
|
|
358
|
+
* synthetic mouseleave on the host to cancel the hover pane's show timer
|
|
359
|
+
* and hide any visible pane.
|
|
360
|
+
*/
|
|
361
|
+
private handleGridActionsMouseEnter = (): void => {
|
|
362
|
+
this.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
|
|
363
|
+
};
|
|
364
|
+
|
|
309
365
|
private get tile() {
|
|
310
366
|
const {
|
|
311
367
|
model,
|
|
@@ -402,6 +458,7 @@ export class TileDispatcher
|
|
|
402
458
|
.baseImageUrl=${this.baseImageUrl}
|
|
403
459
|
.loggedIn=${this.loggedIn}
|
|
404
460
|
.suppressBlurring=${this.suppressBlurring}
|
|
461
|
+
.tileActions=${this.isManageView ? [] : this.tileActions}
|
|
405
462
|
?useLocalTime=${this.useLocalTime}
|
|
406
463
|
>
|
|
407
464
|
</tile-list-compact>`;
|
|
@@ -420,6 +477,7 @@ export class TileDispatcher
|
|
|
420
477
|
.baseImageUrl=${this.baseImageUrl}
|
|
421
478
|
.loggedIn=${this.loggedIn}
|
|
422
479
|
.suppressBlurring=${this.suppressBlurring}
|
|
480
|
+
.tileActions=${this.isManageView ? [] : this.tileActions}
|
|
423
481
|
?useLocalTime=${this.useLocalTime}
|
|
424
482
|
>
|
|
425
483
|
</tile-list>`;
|
|
@@ -431,6 +489,7 @@ export class TileDispatcher
|
|
|
431
489
|
static get styles() {
|
|
432
490
|
return [
|
|
433
491
|
srOnlyStyle,
|
|
492
|
+
tileActionStyles,
|
|
434
493
|
css`
|
|
435
494
|
:host {
|
|
436
495
|
display: block;
|
|
@@ -466,8 +525,45 @@ export class TileDispatcher
|
|
|
466
525
|
border-radius: 4px;
|
|
467
526
|
}
|
|
468
527
|
|
|
469
|
-
|
|
470
|
-
|
|
528
|
+
/*
|
|
529
|
+
* When tile actions are present, the container takes on the role of
|
|
530
|
+
* the tile's visual card so the tile content and action row appear
|
|
531
|
+
* as a single unified element. The inner tile's own shadow/radius
|
|
532
|
+
* are disabled via CSS variable overrides to avoid visual
|
|
533
|
+
* duplication, and the action row sits as a footer inside the same
|
|
534
|
+
* card.
|
|
535
|
+
*/
|
|
536
|
+
#container.has-tile-actions {
|
|
537
|
+
display: flex;
|
|
538
|
+
flex-direction: column;
|
|
539
|
+
overflow: hidden;
|
|
540
|
+
box-shadow: var(--tileShadow, 1px 1px 2px 0);
|
|
541
|
+
--tileBoxShadow: none;
|
|
542
|
+
--tileCornerRadius: 0;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
#container.has-tile-actions .tile-link {
|
|
546
|
+
flex: 1;
|
|
547
|
+
min-height: 0;
|
|
548
|
+
overflow: hidden;
|
|
549
|
+
border-radius: 0;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/* Normal hover shadow lives on the inner anchor for plain tiles */
|
|
553
|
+
#container.hoverable:not(.has-tile-actions) a:focus,
|
|
554
|
+
#container.hoverable:not(.has-tile-actions) a:hover {
|
|
555
|
+
box-shadow: var(
|
|
556
|
+
--tileHoverBoxShadow,
|
|
557
|
+
0 0 6px 2px rgba(8, 8, 32, 0.8)
|
|
558
|
+
);
|
|
559
|
+
transition: box-shadow 0.1s ease;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/*
|
|
563
|
+
* When the container owns the card visuals, the hover shadow needs
|
|
564
|
+
* to move up to the container so it wraps the action row too.
|
|
565
|
+
*/
|
|
566
|
+
#container.hoverable.has-tile-actions:hover {
|
|
471
567
|
box-shadow: var(
|
|
472
568
|
--tileHoverBoxShadow,
|
|
473
569
|
0 0 6px 2px rgba(8, 8, 32, 0.8)
|
|
@@ -521,6 +617,20 @@ export class TileDispatcher
|
|
|
521
617
|
left: -9999px;
|
|
522
618
|
z-index: 2;
|
|
523
619
|
}
|
|
620
|
+
|
|
621
|
+
/*
|
|
622
|
+
* Grid-mode action row sits flush against the bottom of the card —
|
|
623
|
+
* the buttons' own borders form the visible bottom edge. The outer
|
|
624
|
+
* buttons get rounded bottom corners to match the container so the
|
|
625
|
+
* red border traces cleanly around the card's bottom corners.
|
|
626
|
+
*/
|
|
627
|
+
.grid-tile-actions .tile-action-btn:first-child {
|
|
628
|
+
border-bottom-left-radius: 4px;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
.grid-tile-actions .tile-action-btn:last-child {
|
|
632
|
+
border-bottom-right-radius: 4px;
|
|
633
|
+
}
|
|
524
634
|
`,
|
|
525
635
|
];
|
|
526
636
|
}
|
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { TemplateResult, html, nothing } from 'lit';
|
|
2
|
-
import { msg, str } from '@lit/localize';
|
|
3
|
-
import type { SortParam } from '@internetarchive/search-service';
|
|
4
|
-
import type { TileModel } from '../models';
|
|
5
|
-
import { formatDate } from '../utils/format-date';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* A class encapsulating shared logic for converting model values into display values
|
|
9
|
-
* across different types of tiles.
|
|
10
|
-
*/
|
|
11
|
-
export class TileDisplayValueProvider {
|
|
12
|
-
private model?: TileModel;
|
|
13
|
-
|
|
14
|
-
private baseNavigationUrl?: string;
|
|
15
|
-
|
|
16
|
-
private collectionPagePath?: string;
|
|
17
|
-
|
|
18
|
-
private sortParam?: SortParam;
|
|
19
|
-
|
|
20
|
-
private creatorFilter?: string;
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
options: {
|
|
24
|
-
model?: TileModel;
|
|
25
|
-
baseNavigationUrl?: string;
|
|
26
|
-
collectionPagePath?: string;
|
|
27
|
-
sortParam?: SortParam;
|
|
28
|
-
creatorFilter?: string;
|
|
29
|
-
} = {},
|
|
30
|
-
) {
|
|
31
|
-
this.model = options.model;
|
|
32
|
-
this.baseNavigationUrl = options.baseNavigationUrl;
|
|
33
|
-
this.collectionPagePath = options.collectionPagePath ?? '/details/';
|
|
34
|
-
this.sortParam = options.sortParam;
|
|
35
|
-
this.creatorFilter = options.creatorFilter;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Examines the creator(s) for the given tile model, returning
|
|
40
|
-
* the first creator whose name matches the provided filter
|
|
41
|
-
* (or simply the first creator overall if no filter is provided).
|
|
42
|
-
*/
|
|
43
|
-
get firstCreatorMatchingFilter(): string | undefined {
|
|
44
|
-
let matchingCreator;
|
|
45
|
-
|
|
46
|
-
// If we're filtering by creator initial and have multiple creators, we want
|
|
47
|
-
// to surface the first creator who matches the filter.
|
|
48
|
-
if (this.creatorFilter && this.model?.creators.length) {
|
|
49
|
-
const firstLetter = this.creatorFilter; // This is just to satisfy tsc
|
|
50
|
-
matchingCreator = this.model.creators.find(creator =>
|
|
51
|
-
// Decompose combining characters first, so that e.g., filtering on E matches É too.
|
|
52
|
-
// Then remove anything that isn't strictly alphabetic, since our filters currently
|
|
53
|
-
// only handle A-Z. The first such letter (if one exists) is what needs to match.
|
|
54
|
-
creator
|
|
55
|
-
.normalize('NFD')
|
|
56
|
-
.replace(/[^A-Z]+/gi, '')
|
|
57
|
-
.toUpperCase()
|
|
58
|
-
.startsWith(firstLetter),
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return matchingCreator ?? this.model?.creator;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The label indicating what year an account item was created.
|
|
67
|
-
* E.g., "Archivist since 2015"
|
|
68
|
-
*/
|
|
69
|
-
get accountLabel(): string {
|
|
70
|
-
return this.model?.dateAdded
|
|
71
|
-
? msg(str`Archivist since ${this.model.dateAdded.getFullYear()}`)
|
|
72
|
-
: '';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The readable label for the current sort if it is a type of date sort,
|
|
77
|
-
* or the empty string otherwise.
|
|
78
|
-
*/
|
|
79
|
-
get dateLabel(): string {
|
|
80
|
-
switch (this.sortParam?.field) {
|
|
81
|
-
case 'publicdate':
|
|
82
|
-
return msg('Archived');
|
|
83
|
-
case 'reviewdate':
|
|
84
|
-
return msg('Reviewed');
|
|
85
|
-
case 'addeddate':
|
|
86
|
-
return msg('Added');
|
|
87
|
-
case 'date':
|
|
88
|
-
return msg('Published');
|
|
89
|
-
default:
|
|
90
|
-
return '';
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* The readable label for the current views column, based on whether
|
|
96
|
-
* weekly or all-time views are being shown.
|
|
97
|
-
*/
|
|
98
|
-
get viewsLabel(): string {
|
|
99
|
-
return this.sortParam?.field === 'week'
|
|
100
|
-
? msg('Weekly views')
|
|
101
|
-
: msg('All-time views');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Produces a URL pointing at the item page for the given identifier,
|
|
106
|
-
* using the current base URL and the correct path based on whether the
|
|
107
|
-
* item is specified to be a collection (default false).
|
|
108
|
-
*/
|
|
109
|
-
itemPageUrl(
|
|
110
|
-
identifier?: string,
|
|
111
|
-
isCollection = false,
|
|
112
|
-
): string | typeof nothing {
|
|
113
|
-
if (!identifier || this.baseNavigationUrl == null) return nothing;
|
|
114
|
-
const basePath = isCollection ? this.collectionPagePath : '/details/';
|
|
115
|
-
return `${this.baseNavigationUrl}${basePath}${identifier}`;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Produces a template for a link to a single web capture of the given URL and date
|
|
120
|
-
*/
|
|
121
|
-
webArchivesCaptureLink(url: string, date: Date): TemplateResult {
|
|
122
|
-
// Convert the date into the format used to identify wayback captures (e.g., '20150102124550')
|
|
123
|
-
const captureDateStr = date
|
|
124
|
-
.toISOString()
|
|
125
|
-
.replace(/[TZ:-]/g, '')
|
|
126
|
-
.replace(/\..*/, '');
|
|
127
|
-
const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
|
|
128
|
-
url,
|
|
129
|
-
)}`;
|
|
130
|
-
const captureText = formatDate(date, 'long');
|
|
131
|
-
|
|
132
|
-
return html` <a href=${captureHref}> ${captureText} </a> `;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
1
|
+
import { TemplateResult, html, nothing } from 'lit';
|
|
2
|
+
import { msg, str } from '@lit/localize';
|
|
3
|
+
import type { SortParam } from '@internetarchive/search-service';
|
|
4
|
+
import type { TileModel } from '../models';
|
|
5
|
+
import { formatDate } from '../utils/format-date';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A class encapsulating shared logic for converting model values into display values
|
|
9
|
+
* across different types of tiles.
|
|
10
|
+
*/
|
|
11
|
+
export class TileDisplayValueProvider {
|
|
12
|
+
private model?: TileModel;
|
|
13
|
+
|
|
14
|
+
private baseNavigationUrl?: string;
|
|
15
|
+
|
|
16
|
+
private collectionPagePath?: string;
|
|
17
|
+
|
|
18
|
+
private sortParam?: SortParam;
|
|
19
|
+
|
|
20
|
+
private creatorFilter?: string;
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
options: {
|
|
24
|
+
model?: TileModel;
|
|
25
|
+
baseNavigationUrl?: string;
|
|
26
|
+
collectionPagePath?: string;
|
|
27
|
+
sortParam?: SortParam;
|
|
28
|
+
creatorFilter?: string;
|
|
29
|
+
} = {},
|
|
30
|
+
) {
|
|
31
|
+
this.model = options.model;
|
|
32
|
+
this.baseNavigationUrl = options.baseNavigationUrl;
|
|
33
|
+
this.collectionPagePath = options.collectionPagePath ?? '/details/';
|
|
34
|
+
this.sortParam = options.sortParam;
|
|
35
|
+
this.creatorFilter = options.creatorFilter;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Examines the creator(s) for the given tile model, returning
|
|
40
|
+
* the first creator whose name matches the provided filter
|
|
41
|
+
* (or simply the first creator overall if no filter is provided).
|
|
42
|
+
*/
|
|
43
|
+
get firstCreatorMatchingFilter(): string | undefined {
|
|
44
|
+
let matchingCreator;
|
|
45
|
+
|
|
46
|
+
// If we're filtering by creator initial and have multiple creators, we want
|
|
47
|
+
// to surface the first creator who matches the filter.
|
|
48
|
+
if (this.creatorFilter && this.model?.creators.length) {
|
|
49
|
+
const firstLetter = this.creatorFilter; // This is just to satisfy tsc
|
|
50
|
+
matchingCreator = this.model.creators.find(creator =>
|
|
51
|
+
// Decompose combining characters first, so that e.g., filtering on E matches É too.
|
|
52
|
+
// Then remove anything that isn't strictly alphabetic, since our filters currently
|
|
53
|
+
// only handle A-Z. The first such letter (if one exists) is what needs to match.
|
|
54
|
+
creator
|
|
55
|
+
.normalize('NFD')
|
|
56
|
+
.replace(/[^A-Z]+/gi, '')
|
|
57
|
+
.toUpperCase()
|
|
58
|
+
.startsWith(firstLetter),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return matchingCreator ?? this.model?.creator;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The label indicating what year an account item was created.
|
|
67
|
+
* E.g., "Archivist since 2015"
|
|
68
|
+
*/
|
|
69
|
+
get accountLabel(): string {
|
|
70
|
+
return this.model?.dateAdded
|
|
71
|
+
? msg(str`Archivist since ${this.model.dateAdded.getFullYear()}`)
|
|
72
|
+
: '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The readable label for the current sort if it is a type of date sort,
|
|
77
|
+
* or the empty string otherwise.
|
|
78
|
+
*/
|
|
79
|
+
get dateLabel(): string {
|
|
80
|
+
switch (this.sortParam?.field) {
|
|
81
|
+
case 'publicdate':
|
|
82
|
+
return msg('Archived');
|
|
83
|
+
case 'reviewdate':
|
|
84
|
+
return msg('Reviewed');
|
|
85
|
+
case 'addeddate':
|
|
86
|
+
return msg('Added');
|
|
87
|
+
case 'date':
|
|
88
|
+
return msg('Published');
|
|
89
|
+
default:
|
|
90
|
+
return '';
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* The readable label for the current views column, based on whether
|
|
96
|
+
* weekly or all-time views are being shown.
|
|
97
|
+
*/
|
|
98
|
+
get viewsLabel(): string {
|
|
99
|
+
return this.sortParam?.field === 'week'
|
|
100
|
+
? msg('Weekly views')
|
|
101
|
+
: msg('All-time views');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Produces a URL pointing at the item page for the given identifier,
|
|
106
|
+
* using the current base URL and the correct path based on whether the
|
|
107
|
+
* item is specified to be a collection (default false).
|
|
108
|
+
*/
|
|
109
|
+
itemPageUrl(
|
|
110
|
+
identifier?: string,
|
|
111
|
+
isCollection = false,
|
|
112
|
+
): string | typeof nothing {
|
|
113
|
+
if (!identifier || this.baseNavigationUrl == null) return nothing;
|
|
114
|
+
const basePath = isCollection ? this.collectionPagePath : '/details/';
|
|
115
|
+
return `${this.baseNavigationUrl}${basePath}${identifier}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Produces a template for a link to a single web capture of the given URL and date
|
|
120
|
+
*/
|
|
121
|
+
webArchivesCaptureLink(url: string, date: Date): TemplateResult {
|
|
122
|
+
// Convert the date into the format used to identify wayback captures (e.g., '20150102124550')
|
|
123
|
+
const captureDateStr = date
|
|
124
|
+
.toISOString()
|
|
125
|
+
.replace(/[TZ:-]/g, '')
|
|
126
|
+
.replace(/\..*/, '');
|
|
127
|
+
const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
|
|
128
|
+
url,
|
|
129
|
+
)}`;
|
|
130
|
+
const captureText = formatDate(date, 'long');
|
|
131
|
+
|
|
132
|
+
return html` <a href=${captureHref}> ${captureText} </a> `;
|
|
133
|
+
}
|
|
134
|
+
}
|