@internetarchive/collection-browser 4.4.0 → 4.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) 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 +1 -1
  7. package/.prettierignore +1 -1
  8. package/LICENSE +661 -661
  9. package/README.md +83 -83
  10. package/dist/src/app-root.js +672 -672
  11. package/dist/src/app-root.js.map +1 -1
  12. package/dist/src/collection-browser.js +763 -763
  13. package/dist/src/collection-browser.js.map +1 -1
  14. package/dist/src/data-source/collection-browser-data-source.js.map +1 -1
  15. package/dist/src/tiles/item-image.js +28 -28
  16. package/dist/src/tiles/item-image.js.map +1 -1
  17. package/dist/src/tiles/tile-display-value-provider.js +2 -1
  18. package/dist/src/tiles/tile-display-value-provider.js.map +1 -1
  19. package/dist/test/tiles/grid/item-tile.test.js +2 -2
  20. package/dist/test/tiles/grid/item-tile.test.js.map +1 -1
  21. package/dist/test/tiles/list/tile-list.test.js +2 -2
  22. package/dist/test/tiles/list/tile-list.test.js.map +1 -1
  23. package/dist/test/tiles/tile-dispatcher.test.js +14 -0
  24. package/dist/test/tiles/tile-dispatcher.test.js.map +1 -1
  25. package/eslint.config.mjs +53 -53
  26. package/index.html +24 -24
  27. package/local.archive.org.cert +86 -86
  28. package/local.archive.org.key +27 -27
  29. package/package.json +120 -120
  30. package/renovate.json +6 -6
  31. package/src/app-root.ts +1254 -1254
  32. package/src/collection-browser.ts +3161 -3161
  33. package/src/data-source/collection-browser-data-source.ts +1465 -1465
  34. package/src/tiles/item-image.ts +214 -214
  35. package/src/tiles/tile-display-value-provider.ts +2 -3
  36. package/test/tiles/grid/item-tile.test.ts +2 -2
  37. package/test/tiles/list/tile-list.test.ts +2 -2
  38. package/test/tiles/tile-dispatcher.test.ts +20 -0
  39. package/tsconfig.json +25 -25
  40. package/web-dev-server.config.mjs +30 -30
  41. package/web-test-runner.config.mjs +52 -52
@@ -1,214 +1,214 @@
1
- import {
2
- css,
3
- CSSResultGroup,
4
- html,
5
- LitElement,
6
- nothing,
7
- PropertyValues,
8
- } from 'lit';
9
- import { customElement, property, query, state } from 'lit/decorators.js';
10
- import { ClassInfo, classMap } from 'lit/directives/class-map.js';
11
-
12
- import type { TileModel } from '../models';
13
-
14
- import {
15
- baseItemImageStyles,
16
- waveformGradientStyles,
17
- } from '../styles/item-image-styles';
18
- import { searchIcon } from '../assets/img/icons/mediatype/search';
19
-
20
- @customElement('item-image')
21
- export class ItemImage extends LitElement {
22
- /**
23
- * Map to cache which identifiers have waveform-style thumbnails, so that
24
- * they can have their waveform styling applied immediately, rather than
25
- * waiting for the image content to load before applying it (which can
26
- * cause noticeable flicker when such tiles refresh).
27
- */
28
- private static readonly waveformByIdentifier = new Map<string, boolean>();
29
-
30
- @property({ type: Object }) model?: TileModel;
31
-
32
- @property({ type: String }) baseImageUrl?: string;
33
-
34
- @property({ type: Boolean }) isListTile = false;
35
-
36
- @property({ type: Boolean }) isCompactTile = false;
37
-
38
- @property({ type: Boolean }) loggedIn = false;
39
-
40
- @property({ type: Boolean }) suppressBlurring = false;
41
-
42
- @state() private isWaveform = false;
43
-
44
- @state() private isNotFound = false;
45
-
46
- @query('img') private baseImage!: HTMLImageElement;
47
-
48
- protected willUpdate(changed: PropertyValues): void {
49
- if (changed.has('model')) {
50
- // If this identifier is known to have a waveform image, then set isWaveform upfront
51
- const identifier = this.model?.identifier;
52
- this.isWaveform =
53
- ItemImage.waveformByIdentifier.get(identifier as string) === true;
54
- }
55
- }
56
-
57
- render() {
58
- return html`
59
- <div class=${classMap(this.itemBaseClass)}>${this.imageTemplate}</div>
60
- `;
61
- }
62
-
63
- private get imageTemplate() {
64
- if (this.model?.mediatype === 'search') {
65
- return html`${searchIcon}`;
66
- }
67
-
68
- return html`
69
- <img
70
- class=${classMap(this.itemImageClass)}
71
- src="${this.imageSrc}"
72
- alt=""
73
- @load=${this.onLoad}
74
- @error=${this.onError}
75
- />
76
- `;
77
- }
78
-
79
- /**
80
- * Helpers
81
- */
82
- private get imageSrc() {
83
- if (this.isNotFound) return this.notFoundSrc;
84
-
85
- // Use the correct image for web capture tiles, if possible
86
- if (this.model?.captureDates && this.model.identifier) {
87
- try {
88
- const url = new URL(this.model.identifier);
89
- const domain = encodeURIComponent(url.hostname);
90
- return this.baseImageUrl
91
- ? `https://web.archive.org/thumb/${domain}?generate=1`
92
- : nothing;
93
- } catch {
94
- return `${this.baseImageUrl}/images/notfound.png`;
95
- }
96
- }
97
-
98
- // Use the thumbnail URL specified in the model if it exists
99
- if (this.model?.thumbnailUrl) return this.model.thumbnailUrl;
100
-
101
- // Don't try to load invalid image URLs
102
- return this.baseImageUrl && this.model?.identifier
103
- ? `${this.baseImageUrl}/services/img/${this.model.identifier}`
104
- : nothing;
105
- }
106
-
107
- private get notFoundSrc() {
108
- return this.baseImageUrl
109
- ? `${this.baseImageUrl}/images/notfound.png`
110
- : nothing;
111
- }
112
-
113
- private get hashBasedGradient() {
114
- if (!this.model?.identifier) {
115
- return 'waveform-grad0';
116
- }
117
- const gradient = this.hashStrToInt(this.model.identifier) % 6; // returns 0-5
118
- return `waveform-grad${gradient}`;
119
- }
120
-
121
- private hashStrToInt(str: string): number {
122
- return str
123
- .split('')
124
- .reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0);
125
- }
126
-
127
- /**
128
- * Classes
129
- */
130
- private get itemBaseClass(): ClassInfo {
131
- return {
132
- 'drop-shadow': true,
133
- 'list-box': this.isListTile,
134
- 'search-image': this.model?.mediatype === 'search',
135
- [this.hashBasedGradient]: this.isWaveform,
136
- };
137
- }
138
-
139
- private get itemImageClass(): ClassInfo {
140
- const hasSensitiveContent = !!(
141
- this.model?.contentWarning || this.model?.loginRequired
142
- );
143
- const shouldBlur = hasSensitiveContent && !this.suppressBlurring;
144
-
145
- return {
146
- contain: !this.isCompactTile && !this.isWaveform,
147
- cover: this.isCompactTile,
148
- blur: shouldBlur,
149
- waveform: this.isWaveform,
150
- 'account-image': this.isAccountImage, // for account tile image
151
- 'collection-image': this.model?.mediatype === 'collection', // for collection tile image
152
- };
153
- }
154
-
155
- /**
156
- * Helper function to determine if account tile image
157
- */
158
- private get isAccountImage() {
159
- return (
160
- this.model?.mediatype === 'account' &&
161
- !this.isCompactTile &&
162
- !this.isListTile
163
- );
164
- }
165
-
166
- /**
167
- * Event listener sets isWaveform true if image is waveform
168
- */
169
- private onLoad() {
170
- if (
171
- (this.model?.mediatype === 'audio' ||
172
- this.model?.mediatype === 'etree') &&
173
- this.baseImage.naturalWidth / this.baseImage.naturalHeight === 4
174
- ) {
175
- this.isWaveform = true;
176
- if (this.model?.identifier) {
177
- ItemImage.waveformByIdentifier.set(this.model.identifier, true);
178
- }
179
- }
180
- }
181
-
182
- private onError() {
183
- this.isNotFound = true;
184
- }
185
-
186
- /**
187
- * CSS
188
- */
189
- static get styles(): CSSResultGroup {
190
- return [
191
- baseItemImageStyles,
192
- waveformGradientStyles,
193
- css`
194
- img {
195
- height: var(--imgHeight, 16rem);
196
- width: var(--imgWidth, 16rem);
197
- }
198
-
199
- .search-image {
200
- display: flex;
201
- align-items: center;
202
- justify-content: center;
203
- background: rgb(245, 245, 247);
204
- border-radius: 4px;
205
- }
206
-
207
- svg {
208
- height: 10rem;
209
- width: 10rem;
210
- }
211
- `,
212
- ];
213
- }
214
- }
1
+ import {
2
+ css,
3
+ CSSResultGroup,
4
+ html,
5
+ LitElement,
6
+ nothing,
7
+ PropertyValues,
8
+ } from 'lit';
9
+ import { customElement, property, query, state } from 'lit/decorators.js';
10
+ import { ClassInfo, classMap } from 'lit/directives/class-map.js';
11
+
12
+ import type { TileModel } from '../models';
13
+
14
+ import {
15
+ baseItemImageStyles,
16
+ waveformGradientStyles,
17
+ } from '../styles/item-image-styles';
18
+ import { searchIcon } from '../assets/img/icons/mediatype/search';
19
+
20
+ @customElement('item-image')
21
+ export class ItemImage extends LitElement {
22
+ /**
23
+ * Map to cache which identifiers have waveform-style thumbnails, so that
24
+ * they can have their waveform styling applied immediately, rather than
25
+ * waiting for the image content to load before applying it (which can
26
+ * cause noticeable flicker when such tiles refresh).
27
+ */
28
+ private static readonly waveformByIdentifier = new Map<string, boolean>();
29
+
30
+ @property({ type: Object }) model?: TileModel;
31
+
32
+ @property({ type: String }) baseImageUrl?: string;
33
+
34
+ @property({ type: Boolean }) isListTile = false;
35
+
36
+ @property({ type: Boolean }) isCompactTile = false;
37
+
38
+ @property({ type: Boolean }) loggedIn = false;
39
+
40
+ @property({ type: Boolean }) suppressBlurring = false;
41
+
42
+ @state() private isWaveform = false;
43
+
44
+ @state() private isNotFound = false;
45
+
46
+ @query('img') private baseImage!: HTMLImageElement;
47
+
48
+ protected willUpdate(changed: PropertyValues): void {
49
+ if (changed.has('model')) {
50
+ // If this identifier is known to have a waveform image, then set isWaveform upfront
51
+ const identifier = this.model?.identifier;
52
+ this.isWaveform =
53
+ ItemImage.waveformByIdentifier.get(identifier as string) === true;
54
+ }
55
+ }
56
+
57
+ render() {
58
+ return html`
59
+ <div class=${classMap(this.itemBaseClass)}>${this.imageTemplate}</div>
60
+ `;
61
+ }
62
+
63
+ private get imageTemplate() {
64
+ if (this.model?.mediatype === 'search') {
65
+ return html`${searchIcon}`;
66
+ }
67
+
68
+ return html`
69
+ <img
70
+ class=${classMap(this.itemImageClass)}
71
+ src="${this.imageSrc}"
72
+ alt=""
73
+ @load=${this.onLoad}
74
+ @error=${this.onError}
75
+ />
76
+ `;
77
+ }
78
+
79
+ /**
80
+ * Helpers
81
+ */
82
+ private get imageSrc() {
83
+ if (this.isNotFound) return this.notFoundSrc;
84
+
85
+ // Use the correct image for web capture tiles, if possible
86
+ if (this.model?.captureDates && this.model.identifier) {
87
+ try {
88
+ const url = new URL(this.model.identifier);
89
+ const domain = encodeURIComponent(url.hostname);
90
+ return this.baseImageUrl
91
+ ? `https://web.archive.org/thumb/${domain}?generate=1`
92
+ : nothing;
93
+ } catch {
94
+ return `${this.baseImageUrl}/images/notfound.png`;
95
+ }
96
+ }
97
+
98
+ // Use the thumbnail URL specified in the model if it exists
99
+ if (this.model?.thumbnailUrl) return this.model.thumbnailUrl;
100
+
101
+ // Don't try to load invalid image URLs
102
+ return this.baseImageUrl && this.model?.identifier
103
+ ? `${this.baseImageUrl}/services/img/${this.model.identifier}`
104
+ : nothing;
105
+ }
106
+
107
+ private get notFoundSrc() {
108
+ return this.baseImageUrl
109
+ ? `${this.baseImageUrl}/images/notfound.png`
110
+ : nothing;
111
+ }
112
+
113
+ private get hashBasedGradient() {
114
+ if (!this.model?.identifier) {
115
+ return 'waveform-grad0';
116
+ }
117
+ const gradient = this.hashStrToInt(this.model.identifier) % 6; // returns 0-5
118
+ return `waveform-grad${gradient}`;
119
+ }
120
+
121
+ private hashStrToInt(str: string): number {
122
+ return str
123
+ .split('')
124
+ .reduce((acc: number, char: string) => acc + char.charCodeAt(0), 0);
125
+ }
126
+
127
+ /**
128
+ * Classes
129
+ */
130
+ private get itemBaseClass(): ClassInfo {
131
+ return {
132
+ 'drop-shadow': true,
133
+ 'list-box': this.isListTile,
134
+ 'search-image': this.model?.mediatype === 'search',
135
+ [this.hashBasedGradient]: this.isWaveform,
136
+ };
137
+ }
138
+
139
+ private get itemImageClass(): ClassInfo {
140
+ const hasSensitiveContent = !!(
141
+ this.model?.contentWarning || this.model?.loginRequired
142
+ );
143
+ const shouldBlur = hasSensitiveContent && !this.suppressBlurring;
144
+
145
+ return {
146
+ contain: !this.isCompactTile && !this.isWaveform,
147
+ cover: this.isCompactTile,
148
+ blur: shouldBlur,
149
+ waveform: this.isWaveform,
150
+ 'account-image': this.isAccountImage, // for account tile image
151
+ 'collection-image': this.model?.mediatype === 'collection', // for collection tile image
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Helper function to determine if account tile image
157
+ */
158
+ private get isAccountImage() {
159
+ return (
160
+ this.model?.mediatype === 'account' &&
161
+ !this.isCompactTile &&
162
+ !this.isListTile
163
+ );
164
+ }
165
+
166
+ /**
167
+ * Event listener sets isWaveform true if image is waveform
168
+ */
169
+ private onLoad() {
170
+ if (
171
+ (this.model?.mediatype === 'audio' ||
172
+ this.model?.mediatype === 'etree') &&
173
+ this.baseImage.naturalWidth / this.baseImage.naturalHeight === 4
174
+ ) {
175
+ this.isWaveform = true;
176
+ if (this.model?.identifier) {
177
+ ItemImage.waveformByIdentifier.set(this.model.identifier, true);
178
+ }
179
+ }
180
+ }
181
+
182
+ private onError() {
183
+ this.isNotFound = true;
184
+ }
185
+
186
+ /**
187
+ * CSS
188
+ */
189
+ static get styles(): CSSResultGroup {
190
+ return [
191
+ baseItemImageStyles,
192
+ waveformGradientStyles,
193
+ css`
194
+ img {
195
+ height: var(--imgHeight, 16rem);
196
+ width: var(--imgWidth, 16rem);
197
+ }
198
+
199
+ .search-image {
200
+ display: flex;
201
+ align-items: center;
202
+ justify-content: center;
203
+ background: rgb(245, 245, 247);
204
+ border-radius: 4px;
205
+ }
206
+
207
+ svg {
208
+ height: 10rem;
209
+ width: 10rem;
210
+ }
211
+ `,
212
+ ];
213
+ }
214
+ }
@@ -124,9 +124,8 @@ export class TileDisplayValueProvider {
124
124
  .toISOString()
125
125
  .replace(/[TZ:-]/g, '')
126
126
  .replace(/\..*/, '');
127
- const captureHref = `https://web.archive.org/web/${captureDateStr}/${encodeURIComponent(
128
- url,
129
- )}`;
127
+ // url must not be percent-encoded — Wayback Machine matches on the raw URL
128
+ const captureHref = `https://web.archive.org/web/${captureDateStr}/${url}`;
130
129
  const captureText = formatDate(date, 'long');
131
130
 
132
131
  return html` <a href=${captureHref}> ${captureText} </a> `;
@@ -453,7 +453,7 @@ describe('Item Tile', () => {
453
453
  const firstDateLink = captureDatesUl?.children[0]?.querySelector('a[href]');
454
454
  expect(firstDateLink, 'first date link').to.exist;
455
455
  expect(firstDateLink?.getAttribute('href')).to.equal(
456
- 'https://web.archive.org/web/20100102123456/https%3A%2F%2Fexample.com%2F',
456
+ 'https://web.archive.org/web/20100102123456/https://example.com/',
457
457
  );
458
458
  expect(firstDateLink?.textContent?.trim()).to.equal('Jan 02, 2010');
459
459
 
@@ -461,7 +461,7 @@ describe('Item Tile', () => {
461
461
  captureDatesUl?.children[1]?.querySelector('a[href]');
462
462
  expect(secondDateLink, 'second date link').to.exist;
463
463
  expect(secondDateLink?.getAttribute('href')).to.equal(
464
- 'https://web.archive.org/web/20110203124321/https%3A%2F%2Fexample.com%2F',
464
+ 'https://web.archive.org/web/20110203124321/https://example.com/',
465
465
  );
466
466
  expect(secondDateLink?.textContent?.trim()).to.equal('Feb 03, 2011');
467
467
  });
@@ -509,7 +509,7 @@ describe('List Tile', () => {
509
509
  const firstDateLink = captureDatesUl?.children[0]?.querySelector('a[href]');
510
510
  expect(firstDateLink, 'first date link').to.exist;
511
511
  expect(firstDateLink?.getAttribute('href')).to.equal(
512
- 'https://web.archive.org/web/20100102123456/https%3A%2F%2Fexample.com%2F',
512
+ 'https://web.archive.org/web/20100102123456/https://example.com/',
513
513
  );
514
514
  expect(firstDateLink?.textContent?.trim()).to.equal('Jan 02, 2010');
515
515
 
@@ -517,7 +517,7 @@ describe('List Tile', () => {
517
517
  captureDatesUl?.children[1]?.querySelector('a[href]');
518
518
  expect(secondDateLink, 'second date link').to.exist;
519
519
  expect(secondDateLink?.getAttribute('href')).to.equal(
520
- 'https://web.archive.org/web/20110203124321/https%3A%2F%2Fexample.com%2F',
520
+ 'https://web.archive.org/web/20110203124321/https://example.com/',
521
521
  );
522
522
  expect(secondDateLink?.textContent?.trim()).to.equal('Feb 03, 2011');
523
523
  });
@@ -110,6 +110,26 @@ describe('Tile Dispatcher', () => {
110
110
  window.open = oldWindowOpen;
111
111
  });
112
112
 
113
+ it('should use model href as link href', async () => {
114
+ const el = await fixture<TileDispatcher>(html`
115
+ <tile-dispatcher
116
+ .model=${{
117
+ identifier: 'foo',
118
+ href: 'https://web.archive.org/web/20180613065659/http://www.sankei.com/',
119
+ }}
120
+ .baseNavigationUrl=${''}
121
+ ></tile-dispatcher>
122
+ `);
123
+
124
+ const tileLink = el.shadowRoot?.querySelector(
125
+ 'a[href]',
126
+ ) as HTMLAnchorElement;
127
+ expect(tileLink).to.exist;
128
+ expect(tileLink.getAttribute('href')).to.equal(
129
+ 'https://web.archive.org/web/20180613065659/http://www.sankei.com/',
130
+ );
131
+ });
132
+
113
133
  it('should toggle model checked state when manage check clicked', async () => {
114
134
  const el = await fixture<TileDispatcher>(html`
115
135
  <tile-dispatcher
package/tsconfig.json CHANGED
@@ -1,25 +1,25 @@
1
- {
2
- "compilerOptions": {
3
- "target": "esnext",
4
- "module": "esnext",
5
- "moduleResolution": "bundler",
6
- "noEmitOnError": true,
7
- "lib": [
8
- "ESNext",
9
- "dom",
10
- "dom.iterable"
11
- ],
12
- "strict": true,
13
- "esModuleInterop": false,
14
- "allowSyntheticDefaultImports": true,
15
- "experimentalDecorators": true,
16
- "importHelpers": true,
17
- "outDir": "dist",
18
- "sourceMap": true,
19
- "inlineSources": true,
20
- "rootDir": "./",
21
- "declaration": true,
22
- "useDefineForClassFields": false,
23
- },
24
- "include": ["src", "test", "index.ts", "types"],
25
- }
1
+ {
2
+ "compilerOptions": {
3
+ "target": "esnext",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "noEmitOnError": true,
7
+ "lib": [
8
+ "ESNext",
9
+ "dom",
10
+ "dom.iterable"
11
+ ],
12
+ "strict": true,
13
+ "esModuleInterop": false,
14
+ "allowSyntheticDefaultImports": true,
15
+ "experimentalDecorators": true,
16
+ "importHelpers": true,
17
+ "outDir": "dist",
18
+ "sourceMap": true,
19
+ "inlineSources": true,
20
+ "rootDir": "./",
21
+ "declaration": true,
22
+ "useDefineForClassFields": false,
23
+ },
24
+ "include": ["src", "test", "index.ts", "types"],
25
+ }
@@ -1,30 +1,30 @@
1
- // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2
-
3
- /** Use Hot Module replacement by adding --hmr to the start command */
4
- const hmr = process.argv.includes('--hmr');
5
-
6
- export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7
- nodeResolve: true,
8
- open: '/',
9
- watch: !hmr,
10
-
11
- /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
12
- // esbuildTarget: 'auto'
13
-
14
- /** Set appIndex to enable SPA routing */
15
- // appIndex: 'demo/index.html',
16
-
17
- /** Confgure bare import resolve plugin */
18
- // nodeResolve: {
19
- // exportConditions: ['browser', 'development']
20
- // },
21
-
22
- plugins: [
23
- /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
24
- // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
25
- ],
26
-
27
- http2: true,
28
- sslCert: './local.archive.org.cert',
29
- sslKey: './local.archive.org.key',
30
- });
1
+ // import { hmrPlugin, presets } from '@open-wc/dev-server-hmr';
2
+
3
+ /** Use Hot Module replacement by adding --hmr to the start command */
4
+ const hmr = process.argv.includes('--hmr');
5
+
6
+ export default /** @type {import('@web/dev-server').DevServerConfig} */ ({
7
+ nodeResolve: true,
8
+ open: '/',
9
+ watch: !hmr,
10
+
11
+ /** Compile JS for older browsers. Requires @web/dev-server-esbuild plugin */
12
+ // esbuildTarget: 'auto'
13
+
14
+ /** Set appIndex to enable SPA routing */
15
+ // appIndex: 'demo/index.html',
16
+
17
+ /** Confgure bare import resolve plugin */
18
+ // nodeResolve: {
19
+ // exportConditions: ['browser', 'development']
20
+ // },
21
+
22
+ plugins: [
23
+ /** Use Hot Module Replacement by uncommenting. Requires @open-wc/dev-server-hmr plugin */
24
+ // hmr && hmrPlugin({ exclude: ['**/*/node_modules/**/*'], presets: [presets.litElement] }),
25
+ ],
26
+
27
+ http2: true,
28
+ sslCert: './local.archive.org.cert',
29
+ sslKey: './local.archive.org.key',
30
+ });