@internetarchive/collection-browser 4.3.2-rc-webdev-8334.3 → 4.3.2-rc-webdev-8334.4

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.
@@ -1,4 +1,11 @@
1
- import { css, CSSResultGroup, html, LitElement, nothing } from 'lit';
1
+ import {
2
+ css,
3
+ CSSResultGroup,
4
+ html,
5
+ LitElement,
6
+ nothing,
7
+ PropertyValues,
8
+ } from 'lit';
2
9
  import { customElement, property, query, state } from 'lit/decorators.js';
3
10
  import { ClassInfo, classMap } from 'lit/directives/class-map.js';
4
11
 
@@ -12,6 +19,14 @@ import { searchIcon } from '../assets/img/icons/mediatype/search';
12
19
 
13
20
  @customElement('item-image')
14
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
+
15
30
  @property({ type: Object }) model?: TileModel;
16
31
 
17
32
  @property({ type: String }) baseImageUrl?: string;
@@ -30,6 +45,15 @@ export class ItemImage extends LitElement {
30
45
 
31
46
  @query('img') private baseImage!: HTMLImageElement;
32
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
+
33
57
  render() {
34
58
  return html`
35
59
  <div class=${classMap(this.itemBaseClass)}>${this.imageTemplate}</div>
@@ -149,6 +173,9 @@ export class ItemImage extends LitElement {
149
173
  this.baseImage.naturalWidth / this.baseImage.naturalHeight === 4
150
174
  ) {
151
175
  this.isWaveform = true;
176
+ if (this.model?.identifier) {
177
+ ItemImage.waveformByIdentifier.set(this.model.identifier, true);
178
+ }
152
179
  }
153
180
  }
154
181
 
@@ -176,7 +176,26 @@ export class TileDispatcher
176
176
  // Use the server-specified href if available.
177
177
  // Otherwise, construct a details page URL from the item identifier.
178
178
  if (this.model.href) {
179
- return `${this.baseNavigationUrl}${this.model.href}`;
179
+ // Backstop for legacy Wayback Machine URLs where the target URL was
180
+ // erroneously percent-encoded by encodeURIComponent (the root cause in
181
+ // tile-display-value-provider is fixed, but old stored data may still
182
+ // carry encoded URLs). Decode only the target-URL segment after the
183
+ // /web/{timestamp}/ prefix — decoding the full href risks corrupting the
184
+ // Wayback prefix or intentional encoding elsewhere in the path.
185
+ // %3A%2F%2F (encoded "://") is an unambiguous indicator the entire URL
186
+ // was over-encoded; legitimate paths with %3A never appear next to %2F%2F.
187
+ const href = this.model.href.replace(
188
+ /(\/web\/\d+\/)(.+)/,
189
+ (_, prefix, target) => {
190
+ if (!/%3A%2F%2F/i.test(target)) return `${prefix}${target}`;
191
+ try {
192
+ return `${prefix}${decodeURIComponent(target)}`;
193
+ } catch {
194
+ return `${prefix}${target}`;
195
+ }
196
+ },
197
+ );
198
+ return `${this.baseNavigationUrl}${href}`;
180
199
  }
181
200
 
182
201
  return this.displayValueProvider.itemPageUrl(
@@ -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,65 @@ describe('Tile Dispatcher', () => {
110
110
  window.open = oldWindowOpen;
111
111
  });
112
112
 
113
+ it('should use model href as-is when not percent-encoded', 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
+
133
+ it('should decode percent-encoded model href before use', async () => {
134
+ const el = await fixture<TileDispatcher>(html`
135
+ <tile-dispatcher
136
+ .model=${{
137
+ identifier: 'foo',
138
+ href: 'https://web.archive.org/web/20180613065659/http%3A%2F%2Fwww.sankei.com%2F',
139
+ }}
140
+ .baseNavigationUrl=${''}
141
+ ></tile-dispatcher>
142
+ `);
143
+
144
+ const tileLink = el.shadowRoot?.querySelector(
145
+ 'a[href]',
146
+ ) as HTMLAnchorElement;
147
+ expect(tileLink).to.exist;
148
+ expect(tileLink.getAttribute('href')).to.equal(
149
+ 'https://web.archive.org/web/20180613065659/http://www.sankei.com/',
150
+ );
151
+ });
152
+
153
+ it('should not decode %3A in Wayback URL path when not an encoded scheme', async () => {
154
+ // %3A in the path (e.g. /path/%3Asomething) is not a sign of over-encoding;
155
+ // only %3A%2F%2F (encoded "://") is.
156
+ const href =
157
+ 'https://web.archive.org/web/20180613065659/https://example.com/path/%3Asomething';
158
+ const el = await fixture<TileDispatcher>(html`
159
+ <tile-dispatcher
160
+ .model=${{ identifier: 'foo', href }}
161
+ .baseNavigationUrl=${''}
162
+ ></tile-dispatcher>
163
+ `);
164
+
165
+ const tileLink = el.shadowRoot?.querySelector(
166
+ 'a[href]',
167
+ ) as HTMLAnchorElement;
168
+ expect(tileLink).to.exist;
169
+ expect(tileLink.getAttribute('href')).to.equal(href);
170
+ });
171
+
113
172
  it('should toggle model checked state when manage check clicked', async () => {
114
173
  const el = await fixture<TileDispatcher>(html`
115
174
  <tile-dispatcher