@internetarchive/collection-browser 4.9.1-webdev-9670.0 → 4.9.1-webdev-9670.2

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.
@@ -16,6 +16,12 @@ export type TilePreviewMedia = {
16
16
  } | {
17
17
  kind: 'zoom';
18
18
  };
19
+ interface ItemFile {
20
+ name: string;
21
+ format?: string;
22
+ length?: string;
23
+ size?: string;
24
+ }
19
25
  /** Clip and snippet length, in seconds. */
20
26
  export declare const PREVIEW_CLIP_SECONDS = 4;
21
27
  /** What the previews are allowed to fetch. */
@@ -27,6 +33,12 @@ export interface TilePreviewOptions {
27
33
  * 35 KB). Movies with no playable derivative always use the frames.
28
34
  */
29
35
  clips?: boolean;
36
+ /**
37
+ * The most a clip may be expected to download, in bytes (default 8 MB).
38
+ * Movies whose cheapest playable derivative is estimated to cost more get the
39
+ * frame thumbnails instead. See `estimateClipBytes`.
40
+ */
41
+ clipBudgetBytes?: number;
30
42
  /**
31
43
  * Play video clips and audio snippets with sound. Only takes effect once the
32
44
  * page has been activated by a click or key press; otherwise the browser
@@ -34,6 +46,7 @@ export interface TilePreviewOptions {
34
46
  */
35
47
  sound?: boolean;
36
48
  }
49
+ export declare const DEFAULT_CLIP_BUDGET_BYTES = 8000000;
37
50
  /**
38
51
  * Resolves the preview media for an item. One request at most per item per
39
52
  * page load (none for texts and fallbacks), and the result, including a
@@ -42,3 +55,11 @@ export interface TilePreviewOptions {
42
55
  export declare function resolvePreviewMedia(identifier: string, mediatype: string, baseUrl?: string, options?: TilePreviewOptions): Promise<TilePreviewMedia>;
43
56
  /** Test hook. */
44
57
  export declare function clearPreviewMediaCache(): void;
58
+ /**
59
+ * What playing a derivative is expected to download. A browser buffers about
60
+ * `BUFFER_AHEAD_SECONDS` of media ahead and reads the whole mp4 index first, so
61
+ * the cost follows bitrate and duration, not the length of the clip we show. A
62
+ * file of unknown duration costs its whole size.
63
+ */
64
+ export declare function estimateClipBytes(file: ItemFile): number;
65
+ export {};
@@ -3,6 +3,11 @@ export const PREVIEW_CLIP_SECONDS = 4;
3
3
  const MAX_SLIDES = 4;
4
4
  const MAX_FRAMES = 6;
5
5
  const MIN_CLIP_SOURCE_SECONDS = 8;
6
+ export const DEFAULT_CLIP_BUDGET_BYTES = 8_000_000;
7
+ /** Seconds of media a browser buffers ahead once playback starts. */
8
+ const BUFFER_AHEAD_SECONDS = 45;
9
+ /** Rough size of the mp4 index (moov) per second of media. */
10
+ const INDEX_BYTES_PER_SECOND = 1000;
6
11
  const MAX_IMAGE_BYTES = 1_500_000;
7
12
  const PAGE_NUMBERS = [1, 3, 5, 8];
8
13
  const cache = new Map();
@@ -12,7 +17,10 @@ const cache = new Map();
12
17
  * failure, is cached so re-hovering never hits the network again.
13
18
  */
14
19
  export function resolvePreviewMedia(identifier, mediatype, baseUrl = 'https://archive.org', options = {}) {
15
- const key = `${baseUrl}|${identifier}|${options.clips ? 'clip' : ''}`;
20
+ const clipKey = options.clips
21
+ ? `clip${options.clipBudgetBytes ?? DEFAULT_CLIP_BUDGET_BYTES}`
22
+ : '';
23
+ const key = `${baseUrl}|${identifier}|${clipKey}`;
16
24
  let pending = cache.get(key);
17
25
  if (!pending) {
18
26
  pending = resolve(identifier, mediatype, baseUrl, options).catch(() => ({
@@ -48,7 +56,9 @@ async function resolve(identifier, mediatype, baseUrl, options) {
48
56
  .map(f => f.name)
49
57
  .sort()
50
58
  .slice(1), MAX_FRAMES).map(download);
51
- const clip = options.clips ? smallestPlayable(files) : undefined;
59
+ const clip = options.clips
60
+ ? cheapestPlayable(files, options.clipBudgetBytes ?? DEFAULT_CLIP_BUDGET_BYTES)
61
+ : undefined;
52
62
  if (clip) {
53
63
  return {
54
64
  kind: 'video',
@@ -96,13 +106,27 @@ async function fetchFiles(identifier, baseUrl) {
96
106
  return Array.isArray(body.result) ? body.result : [];
97
107
  }
98
108
  /**
99
- * The smallest derivative this browser can play (mp4 or webm, at least a few
100
- * seconds long). The smallest has the lowest bitrate and the shortest header, so
101
- * it starts soonest.
109
+ * What playing a derivative is expected to download. A browser buffers about
110
+ * `BUFFER_AHEAD_SECONDS` of media ahead and reads the whole mp4 index first, so
111
+ * the cost follows bitrate and duration, not the length of the clip we show. A
112
+ * file of unknown duration costs its whole size.
102
113
  */
103
- function smallestPlayable(files) {
114
+ export function estimateClipBytes(file) {
115
+ const size = Number(file.size);
116
+ const seconds = parseLength(file.length);
117
+ if (!seconds)
118
+ return size;
119
+ const bytesPerSecond = size / seconds;
120
+ return Math.min(size, bytesPerSecond * BUFFER_AHEAD_SECONDS + seconds * INDEX_BYTES_PER_SECOND);
121
+ }
122
+ /**
123
+ * The derivative this browser can play that is cheapest to preview (mp4 or
124
+ * webm, at least a few seconds long), or none when even that one is over
125
+ * `budget`.
126
+ */
127
+ function cheapestPlayable(files, budget) {
104
128
  const video = document.createElement('video');
105
- return files
129
+ const cheapest = files
106
130
  .filter(f => {
107
131
  const ext = f.name.toLowerCase().split('.').pop();
108
132
  const type = ext === 'webm'
@@ -117,7 +141,10 @@ function smallestPlayable(files) {
117
141
  (seconds === 0 || seconds >= MIN_CLIP_SOURCE_SECONDS) &&
118
142
  !!video.canPlayType(type));
119
143
  })
120
- .sort((a, b) => Number(a.size) - Number(b.size))[0];
144
+ .sort((a, b) => estimateClipBytes(a) - estimateClipBytes(b))[0];
145
+ return cheapest && estimateClipBytes(cheapest) <= budget
146
+ ? cheapest
147
+ : undefined;
121
148
  }
122
149
  /** Seconds into a film to start at: 10% in, to skip leading black, capped at 60. */
123
150
  function clipStart(length) {
@@ -1 +1 @@
1
- {"version":3,"file":"tile-preview-media.js","sourceRoot":"","sources":["../../../../src/tiles/preview/tile-preview-media.ts"],"names":[],"mappings":"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","sourcesContent":["/** What a tile preview shows, resolved from an item's file list. */\nexport type TilePreviewMedia =\n | { kind: 'video'; src: string; start: number; fallbackUrls: string[] }\n | { kind: 'slides'; urls: string[]; fit: 'contain' | 'cover' }\n | { kind: 'audio'; src: string; start: number; waveform?: string }\n | { kind: 'zoom' };\n\ninterface ItemFile {\n name: string;\n format?: string;\n length?: string;\n size?: string;\n}\n\n/** Clip and snippet length, in seconds. */\nexport const PREVIEW_CLIP_SECONDS = 4;\n\n/** What the previews are allowed to fetch. */\nexport interface TilePreviewOptions {\n /**\n * Play a real clip of the movie, taken from its smallest playable derivative.\n * A clip costs up to about 4 MB per preview (the browser buffers ahead), so by\n * default movies cycle a few of their stored frame thumbnails instead (about\n * 35 KB). Movies with no playable derivative always use the frames.\n */\n clips?: boolean;\n /**\n * Play video clips and audio snippets with sound. Only takes effect once the\n * page has been activated by a click or key press; otherwise the browser\n * refuses and the preview stays muted.\n */\n sound?: boolean;\n}\n\nconst MAX_SLIDES = 4;\nconst MAX_FRAMES = 6;\nconst MIN_CLIP_SOURCE_SECONDS = 8;\nconst MAX_IMAGE_BYTES = 1_500_000;\nconst PAGE_NUMBERS = [1, 3, 5, 8];\n\nconst cache = new Map<string, Promise<TilePreviewMedia>>();\n\n/**\n * Resolves the preview media for an item. One request at most per item per\n * page load (none for texts and fallbacks), and the result, including a\n * failure, is cached so re-hovering never hits the network again.\n */\nexport function resolvePreviewMedia(\n identifier: string,\n mediatype: string,\n baseUrl = 'https://archive.org',\n options: TilePreviewOptions = {},\n): Promise<TilePreviewMedia> {\n const key = `${baseUrl}|${identifier}|${options.clips ? 'clip' : ''}`;\n let pending = cache.get(key);\n if (!pending) {\n pending = resolve(identifier, mediatype, baseUrl, options).catch(() => ({\n kind: 'zoom' as const,\n }));\n cache.set(key, pending);\n }\n return pending;\n}\n\n/** Test hook. */\nexport function clearPreviewMediaCache(): void {\n cache.clear();\n}\n\nasync function resolve(\n identifier: string,\n mediatype: string,\n baseUrl: string,\n options: TilePreviewOptions,\n): Promise<TilePreviewMedia> {\n const download = (name: string) =>\n `${baseUrl}/download/${identifier}/${name\n .split('/')\n .map(encodeURIComponent)\n .join('/')}`;\n\n // Page images come from the BookReader endpoint, so no file list is needed.\n // Pages that don't exist are dropped when they fail to load.\n if (mediatype === 'texts') {\n return {\n kind: 'slides',\n fit: 'contain',\n urls: PAGE_NUMBERS.map(n => download(`page/n${n}_w300.jpg`)),\n };\n }\n\n const files = await fetchFiles(identifier, baseUrl);\n\n if (mediatype === 'movies') {\n // Frames are spread across the whole film, skipping the first (the cover).\n const frames = spread(\n files\n .filter(f => f.format === 'Thumbnail')\n .map(f => f.name)\n .sort()\n .slice(1),\n MAX_FRAMES,\n ).map(download);\n\n const clip = options.clips ? smallestPlayable(files) : undefined;\n if (clip) {\n return {\n kind: 'video',\n src: download(clip.name),\n start: clipStart(clip.length),\n fallbackUrls: frames,\n };\n }\n return frames.length > 1\n ? { kind: 'slides', fit: 'cover', urls: frames }\n : { kind: 'zoom' };\n }\n\n if (mediatype === 'audio' || mediatype === 'etree') {\n const mp3 = files\n .filter(f => f.name.toLowerCase().endsWith('.mp3'))\n .sort((a, b) => a.name.localeCompare(b.name))[0];\n if (!mp3) return { kind: 'zoom' };\n const png = files.find(f => f.name === mp3.name.replace(/\\.mp3$/i, '.png'));\n return {\n kind: 'audio',\n src: download(mp3.name),\n start: startAt(mp3.length),\n waveform: png && download(png.name),\n };\n }\n\n if (mediatype === 'image') {\n const images = files\n .filter(\n f =>\n f.format === 'JPEG' &&\n Number(f.size ?? 0) > 0 &&\n Number(f.size) <= MAX_IMAGE_BYTES,\n )\n .map(f => download(f.name))\n .slice(0, MAX_SLIDES);\n return images.length > 1\n ? { kind: 'slides', fit: 'cover', urls: images }\n : { kind: 'zoom' };\n }\n\n return { kind: 'zoom' };\n}\n\nasync function fetchFiles(\n identifier: string,\n baseUrl: string,\n): Promise<ItemFile[]> {\n const res = await fetch(`${baseUrl}/metadata/${identifier}/files`);\n if (!res.ok) return [];\n const body = await res.json();\n return Array.isArray(body.result) ? body.result : [];\n}\n\n/**\n * The smallest derivative this browser can play (mp4 or webm, at least a few\n * seconds long). The smallest has the lowest bitrate and the shortest header, so\n * it starts soonest.\n */\nfunction smallestPlayable(files: ItemFile[]): ItemFile | undefined {\n const video = document.createElement('video');\n return files\n .filter(f => {\n const ext = f.name.toLowerCase().split('.').pop();\n const type =\n ext === 'webm'\n ? 'video/webm'\n : ext === 'mp4' || ext === 'm4v'\n ? 'video/mp4'\n : '';\n const seconds = parseLength(f.length);\n return (\n !!type &&\n f.format !== 'Thumbnail' &&\n Number(f.size) > 0 &&\n (seconds === 0 || seconds >= MIN_CLIP_SOURCE_SECONDS) &&\n !!video.canPlayType(type)\n );\n })\n .sort((a, b) => Number(a.size) - Number(b.size))[0];\n}\n\n/** Seconds into a film to start at: 10% in, to skip leading black, capped at 60. */\nfunction clipStart(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 10), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Picks `count` evenly spaced entries from `list`. */\nfunction spread<T>(list: T[], count: number): T[] {\n if (list.length <= count) return list;\n return Array.from(\n { length: count },\n (_, i) => list[Math.floor((i * list.length) / count)],\n );\n}\n\n/** Seconds into the media to start at: a third of the way in, capped at 60. */\nfunction startAt(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 3), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Parses a file `length`, either plain seconds (\"16.23\") or \"mm:ss\" / \"h:mm:ss\". */\nfunction parseLength(length?: string): number {\n if (!length) return 0;\n const parts = length.split(':').map(Number);\n if (parts.some(Number.isNaN)) return 0;\n return parts.reduce((total, part) => total * 60 + part, 0);\n}\n"]}
1
+ {"version":3,"file":"tile-preview-media.js","sourceRoot":"","sources":["../../../../src/tiles/preview/tile-preview-media.ts"],"names":[],"mappings":"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","sourcesContent":["/** What a tile preview shows, resolved from an item's file list. */\nexport type TilePreviewMedia =\n | { kind: 'video'; src: string; start: number; fallbackUrls: string[] }\n | { kind: 'slides'; urls: string[]; fit: 'contain' | 'cover' }\n | { kind: 'audio'; src: string; start: number; waveform?: string }\n | { kind: 'zoom' };\n\ninterface ItemFile {\n name: string;\n format?: string;\n length?: string;\n size?: string;\n}\n\n/** Clip and snippet length, in seconds. */\nexport const PREVIEW_CLIP_SECONDS = 4;\n\n/** What the previews are allowed to fetch. */\nexport interface TilePreviewOptions {\n /**\n * Play a real clip of the movie, taken from its smallest playable derivative.\n * A clip costs up to about 4 MB per preview (the browser buffers ahead), so by\n * default movies cycle a few of their stored frame thumbnails instead (about\n * 35 KB). Movies with no playable derivative always use the frames.\n */\n clips?: boolean;\n /**\n * The most a clip may be expected to download, in bytes (default 8 MB).\n * Movies whose cheapest playable derivative is estimated to cost more get the\n * frame thumbnails instead. See `estimateClipBytes`.\n */\n clipBudgetBytes?: number;\n /**\n * Play video clips and audio snippets with sound. Only takes effect once the\n * page has been activated by a click or key press; otherwise the browser\n * refuses and the preview stays muted.\n */\n sound?: boolean;\n}\n\nconst MAX_SLIDES = 4;\nconst MAX_FRAMES = 6;\nconst MIN_CLIP_SOURCE_SECONDS = 8;\nexport const DEFAULT_CLIP_BUDGET_BYTES = 8_000_000;\n/** Seconds of media a browser buffers ahead once playback starts. */\nconst BUFFER_AHEAD_SECONDS = 45;\n/** Rough size of the mp4 index (moov) per second of media. */\nconst INDEX_BYTES_PER_SECOND = 1000;\nconst MAX_IMAGE_BYTES = 1_500_000;\nconst PAGE_NUMBERS = [1, 3, 5, 8];\n\nconst cache = new Map<string, Promise<TilePreviewMedia>>();\n\n/**\n * Resolves the preview media for an item. One request at most per item per\n * page load (none for texts and fallbacks), and the result, including a\n * failure, is cached so re-hovering never hits the network again.\n */\nexport function resolvePreviewMedia(\n identifier: string,\n mediatype: string,\n baseUrl = 'https://archive.org',\n options: TilePreviewOptions = {},\n): Promise<TilePreviewMedia> {\n const clipKey = options.clips\n ? `clip${options.clipBudgetBytes ?? DEFAULT_CLIP_BUDGET_BYTES}`\n : '';\n const key = `${baseUrl}|${identifier}|${clipKey}`;\n let pending = cache.get(key);\n if (!pending) {\n pending = resolve(identifier, mediatype, baseUrl, options).catch(() => ({\n kind: 'zoom' as const,\n }));\n cache.set(key, pending);\n }\n return pending;\n}\n\n/** Test hook. */\nexport function clearPreviewMediaCache(): void {\n cache.clear();\n}\n\nasync function resolve(\n identifier: string,\n mediatype: string,\n baseUrl: string,\n options: TilePreviewOptions,\n): Promise<TilePreviewMedia> {\n const download = (name: string) =>\n `${baseUrl}/download/${identifier}/${name\n .split('/')\n .map(encodeURIComponent)\n .join('/')}`;\n\n // Page images come from the BookReader endpoint, so no file list is needed.\n // Pages that don't exist are dropped when they fail to load.\n if (mediatype === 'texts') {\n return {\n kind: 'slides',\n fit: 'contain',\n urls: PAGE_NUMBERS.map(n => download(`page/n${n}_w300.jpg`)),\n };\n }\n\n const files = await fetchFiles(identifier, baseUrl);\n\n if (mediatype === 'movies') {\n // Frames are spread across the whole film, skipping the first (the cover).\n const frames = spread(\n files\n .filter(f => f.format === 'Thumbnail')\n .map(f => f.name)\n .sort()\n .slice(1),\n MAX_FRAMES,\n ).map(download);\n\n const clip = options.clips\n ? cheapestPlayable(\n files,\n options.clipBudgetBytes ?? DEFAULT_CLIP_BUDGET_BYTES,\n )\n : undefined;\n if (clip) {\n return {\n kind: 'video',\n src: download(clip.name),\n start: clipStart(clip.length),\n fallbackUrls: frames,\n };\n }\n return frames.length > 1\n ? { kind: 'slides', fit: 'cover', urls: frames }\n : { kind: 'zoom' };\n }\n\n if (mediatype === 'audio' || mediatype === 'etree') {\n const mp3 = files\n .filter(f => f.name.toLowerCase().endsWith('.mp3'))\n .sort((a, b) => a.name.localeCompare(b.name))[0];\n if (!mp3) return { kind: 'zoom' };\n const png = files.find(f => f.name === mp3.name.replace(/\\.mp3$/i, '.png'));\n return {\n kind: 'audio',\n src: download(mp3.name),\n start: startAt(mp3.length),\n waveform: png && download(png.name),\n };\n }\n\n if (mediatype === 'image') {\n const images = files\n .filter(\n f =>\n f.format === 'JPEG' &&\n Number(f.size ?? 0) > 0 &&\n Number(f.size) <= MAX_IMAGE_BYTES,\n )\n .map(f => download(f.name))\n .slice(0, MAX_SLIDES);\n return images.length > 1\n ? { kind: 'slides', fit: 'cover', urls: images }\n : { kind: 'zoom' };\n }\n\n return { kind: 'zoom' };\n}\n\nasync function fetchFiles(\n identifier: string,\n baseUrl: string,\n): Promise<ItemFile[]> {\n const res = await fetch(`${baseUrl}/metadata/${identifier}/files`);\n if (!res.ok) return [];\n const body = await res.json();\n return Array.isArray(body.result) ? body.result : [];\n}\n\n/**\n * What playing a derivative is expected to download. A browser buffers about\n * `BUFFER_AHEAD_SECONDS` of media ahead and reads the whole mp4 index first, so\n * the cost follows bitrate and duration, not the length of the clip we show. A\n * file of unknown duration costs its whole size.\n */\nexport function estimateClipBytes(file: ItemFile): number {\n const size = Number(file.size);\n const seconds = parseLength(file.length);\n if (!seconds) return size;\n const bytesPerSecond = size / seconds;\n return Math.min(\n size,\n bytesPerSecond * BUFFER_AHEAD_SECONDS + seconds * INDEX_BYTES_PER_SECOND,\n );\n}\n\n/**\n * The derivative this browser can play that is cheapest to preview (mp4 or\n * webm, at least a few seconds long), or none when even that one is over\n * `budget`.\n */\nfunction cheapestPlayable(\n files: ItemFile[],\n budget: number,\n): ItemFile | undefined {\n const video = document.createElement('video');\n const cheapest = files\n .filter(f => {\n const ext = f.name.toLowerCase().split('.').pop();\n const type =\n ext === 'webm'\n ? 'video/webm'\n : ext === 'mp4' || ext === 'm4v'\n ? 'video/mp4'\n : '';\n const seconds = parseLength(f.length);\n return (\n !!type &&\n f.format !== 'Thumbnail' &&\n Number(f.size) > 0 &&\n (seconds === 0 || seconds >= MIN_CLIP_SOURCE_SECONDS) &&\n !!video.canPlayType(type)\n );\n })\n .sort((a, b) => estimateClipBytes(a) - estimateClipBytes(b))[0];\n return cheapest && estimateClipBytes(cheapest) <= budget\n ? cheapest\n : undefined;\n}\n\n/** Seconds into a film to start at: 10% in, to skip leading black, capped at 60. */\nfunction clipStart(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 10), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Picks `count` evenly spaced entries from `list`. */\nfunction spread<T>(list: T[], count: number): T[] {\n if (list.length <= count) return list;\n return Array.from(\n { length: count },\n (_, i) => list[Math.floor((i * list.length) / count)],\n );\n}\n\n/** Seconds into the media to start at: a third of the way in, capped at 60. */\nfunction startAt(length?: string): number {\n const seconds = parseLength(length);\n if (!seconds) return 0;\n return Math.max(\n 0,\n Math.min(60, Math.floor(seconds / 3), seconds - PREVIEW_CLIP_SECONDS),\n );\n}\n\n/** Parses a file `length`, either plain seconds (\"16.23\") or \"mm:ss\" / \"h:mm:ss\". */\nfunction parseLength(length?: string): number {\n if (!length) return 0;\n const parts = length.split(':').map(Number);\n if (parts.some(Number.isNaN)) return 0;\n return parts.reduce((total, part) => total * 60 + part, 0);\n}\n"]}
@@ -80,6 +80,7 @@ let TileDispatcher = class TileDispatcher extends BaseTileComponent {
80
80
  const hoverPaneTemplate = this.hoverPaneController?.getTemplate() ?? nothing;
81
81
  const containerClasses = classMap({
82
82
  hoverable: isGridMode,
83
+ previewable: this.shouldPreview,
83
84
  'has-tile-actions': hasTileActions,
84
85
  });
85
86
  return html `
@@ -475,6 +476,13 @@ let TileDispatcher = class TileDispatcher extends BaseTileComponent {
475
476
  border-radius: 0;
476
477
  }
477
478
 
479
+ /* A long-press starts a preview, so it must not open the iOS link menu or select text */
480
+ #container.previewable a {
481
+ -webkit-touch-callout: none;
482
+ -webkit-user-select: none;
483
+ user-select: none;
484
+ }
485
+
478
486
  /* Normal hover shadow lives on the inner anchor for plain tiles */
479
487
  #container.hoverable:not(.has-tile-actions) a:focus,
480
488
  #container.hoverable:not(.has-tile-actions) a:hover {
@@ -1 +1 @@
1
- {"version":3,"file":"tile-dispatcher.js","sourceRoot":"","sources":["../../../src/tiles/tile-dispatcher.ts"],"names":[],"mappings":";;AAAA,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAkB,MAAM,KAAK,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,MAAM,6BAA6B,CAAC;AACvD,OAAO,EAAE,SAAS,EAAE,MAAM,8BAA8B,CAAC;AACzD,OAAO,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAOpC,OAAO,wBAAwB,CAAC;AAChC,OAAO,kBAAkB,CAAC;AAC1B,OAAO,qBAAqB,CAAC;AAC7B,OAAO,oBAAoB,CAAC;AAC5B,OAAO,yBAAyB,CAAC;AACjC,OAAO,kBAAkB,CAAC;AAC1B,OAAO,0BAA0B,CAAC;AAClC,OAAO,iCAAiC,CAAC;AAEzC,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,EACL,mBAAmB,GAIpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EACL,qBAAqB,EACrB,qBAAqB,GACtB,MAAM,mCAAmC,CAAC;AAIpC,IAAM,cAAc,GAApB,MAAM,cACX,SAAQ,iBAAiB;IADpB;QAML;;;;;;;;;;;;;;;;WAgBG;;QAI0B,iBAAY,GAAG,KAAK,CAAC;QAOrB,gBAAW,GAAG,KAAK,CAAC;QAEjD,iEAAiE;QACrC,eAAU,GAAe,SAAS,CAAC;QAE/D,uFAAuF;QAC1D,oBAAe,GAAG,KAAK,CAAC;QAErD,kFAAkF;QACrD,sBAAiB,GAAG,KAAK,CAAC;QAE3B,uBAAkB,GAAuB,EAAE,CAAC;QAEhE,sBAAiB,GAAG,IAAI,qBAAqB,CAAC,IAAI,EAAE;YAC1D,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa;SAClC,CAAC,CAAC;QAEyB,qBAAgB,GAAG,GAAG,CAChD,gCAAgC,CACjC,CAAC;QA2SF;;;;WAIG;QACK,gCAA2B,GAAG,GAAS,EAAE;YAC/C,IAAI,CAAC,aAAa,CAAC,IAAI,UAAU,CAAC,YAAY,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;QACvE,CAAC,CAAC;IAoRJ,CAAC;;IAzjBC,YAAY;QACV,IAAI,CAAC,eAAe,EAAE,KAAK,EAAE,CAAC;IAChC,CAAC;IAED,YAAY;QACV,IAAI,CAAC,eAAe,EAAE,IAAI,EAAE,CAAC;IAC/B,CAAC;IAED,+EAA+E;aACvD,6BAAwB,GAG5C;QACF,IAAI,EAAE,IAAI;QACV,cAAc,EAAE,IAAI;QACpB,aAAa,EAAE,KAAK;QACpB,aAAa,EAAE,KAAK;KACrB,AAR+C,CAQ9C;IAEF,MAAM;QACJ,MAAM,UAAU,GAAG,IAAI,CAAC,eAAe,KAAK,MAAM,CAAC;QACnD,MAAM,cAAc,GAAG,UAAU,IAAI,IAAI,CAAC,mBAAmB,CAAC;QAC9D,MAAM,iBAAiB,GACrB,IAAI,CAAC,mBAAmB,EAAE,WAAW,EAAE,IAAI,OAAO,CAAC;QACrD,MAAM,gBAAgB,GAAG,QAAQ,CAAC;YAChC,SAAS,EAAE,UAAU;YACrB,kBAAkB,EAAE,cAAc;SACnC,CAAC,CAAC;QACH,OAAO,IAAI,CAAA;kCACmB,gBAAgB;UACxC,IAAI,CAAC,eAAe,KAAK,aAAa;YACtC,CAAC,CAAC,IAAI,CAAC,cAAc;YACrB,CAAC,CAAC,IAAI,CAAC,YAAY;UACnB,IAAI,CAAC,uBAAuB,IAAI,IAAI,CAAC,mBAAmB;UACxD,iBAAiB;;KAEtB,CAAC;IACJ,CAAC;IAES,YAAY;QACpB,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YAChC,IAAI,CAAC,mBAAmB,GAAG,IAAI,mBAAmB,CAAC,IAAI,EAAE;gBACvD,gBAAgB,EAAE,IAAI,CAAC,gBAAgB;gBACvC,eAAe,EAAE,KAAK;gBACtB,0EAA0E;gBAC1E,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,GAAG,CAAC;aACpE,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,IAAY,cAAc;QACxB,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,GACnE,IAAI,CAAC;QACP,OAAO,IAAI,CAAA;;;wBAGS,YAAY;qBACf,SAAS,IAAI,gBAAgB;4BACtB,gBAAgB;uBACrB,IAAI,CAAC,WAAW;;;KAGlC,CAAC;IACJ,CAAC;IAED,IAAY,YAAY;QACtB,OAAO,IAAI,CAAA;QACP,IAAI,CAAC,eAAe,KAAK,aAAa;YACtC,CAAC,CAAC,IAAI,CAAC,IAAI;YACX,CAAC,CAAC,IAAI,CAAC,gBAAgB;KAC1B,CAAC;IACJ,CAAC;IAED,IAAY,gBAAgB;QAC1B,OAAO,IAAI,CAAA;;eAEA,IAAI,CAAC,YAAY;qBACX,IAAI,CAAC,KAAK,EAAE,KAAK,IAAI,eAAe;;wBAEjC,IAAI,CAAC,sBAAsB,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,OAAO;gBACxD,IAAI,CAAC,sBAAsB;YACjC,CAAC,CAAC,OAAO,CAAC,8DAA8D;YACxE,CAAC,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,KAAK,CAAC;iBACvB,IAAI,CAAC,iBAAiB;uBAChB,IAAI,CAAC,qBAAqB;;;UAGvC,IAAI,CAAC,IAAI;;;UAGT,GAAG,CAAC,0CAA0C,CAAC;;KAEpD,CAAC;IACJ,CAAC;IAED,IAAY,YAAY;QACtB,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,UAAU,IAAI,IAAI,CAAC,iBAAiB,IAAI,IAAI;YAC3D,OAAO,OAAO,CAAC;QAEjB,8CAA8C;QAC9C,oEAAoE;QACpE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;YACpB,OAAO,GAAG,IAAI,CAAC,iBAAiB,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QACvD,CAAC;QAED,OAAO,IAAI,CAAC,oBAAoB,CAAC,WAAW,CAC1C,IAAI,CAAC,KAAK,CAAC,UAAU,EACrB,IAAI,CAAC,KAAK,CAAC,SAAS,KAAK,YAAY,CACtC,CAAC;IACJ,CAAC;IAED,IAAY,mBAAmB;QAC7B,IAAI,CAAC,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,eAAe,KAAK,MAAM;YAAE,OAAO,OAAO,CAAC;QAE1E,OAAO,IAAI,CAAA;;;;kBAIG,IAAI,CAAC,gBAAgB;qBAClB,IAAI,CAAC,KAAK,EAAE,OAAO;oBACpB,IAAI,CAAC,iBAAiB;;;KAGrC,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,IAAY,sBAAsB;QAChC,OAAO,CACL,IAAI,CAAC,eAAe;YACpB,CAAC,CAAC,IAAI,CAAC,eAAe;YACtB,gBAAc,CAAC,wBAAwB,CAAC,IAAI,CAAC,eAAe,CAAC;YAC7D,IAAI,CAAC,KAAK,EAAE,SAAS,KAAK,QAAQ,IAAI,yCAAyC;YAC/E,CAAC,IAAI,CAAC,KAAK,EAAE,YAAY,CAAC,8CAA8C;SACzE,CAAC;IACJ,CAAC;IAED,yFAAyF;IACzF,IAAY,aAAa;QACvB,MAAM,EAAE,KAAK,EAAE,GAAG,IAAI,CAAC;QACvB,OAAO,CACL,IAAI,CAAC,iBAAiB;YACtB,IAAI,CAAC,eAAe,KAAK,MAAM;YAC/B,CAAC,IAAI,CAAC,YAAY;YAClB,CAAC,CAAC,KAAK,EAAE,UAAU;YACnB,CAAC,CAAC,YAAY,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC;YAC9D,CAAC,KAAK,CAAC,YAAY;YACnB,CAAC,IAAI,CAAC,gBAAgB;gBACpB,CAAC,CAAC,KAAK,CAAC,cAAc,IAAI,CAAC,CAAC,KAAK,CAAC,aAAa,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CACvE,CAAC;IACJ,CAAC;IAED,IAAY,cAAc;QACxB,OAAO,MAAM,CAAC,UAAU,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC;IACrD,CAAC;IAED;;;;OAIG;IACH,IAAY,oBAAoB;QAC9B,OAAO,CAAC,IAAI,CAAC,cAAc,IAAI,IAAI,CAAC,sBAAsB,CAAC;IAC7D,CAAC;IAED,kBAAkB;IAClB,YAAY;QACV,OAAO,IAAI,CAAC,SAAS,CAAC;IACxB,CAAC;IAED,kBAAkB;IAClB,iBAAiB;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IAED,YAAY,CAAC,KAA0B;QACrC,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC,WAAW,CAAC,KAAK,CAAC;QAC5C,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC;IAChD,CAAC;IAED,oBAAoB;QAClB,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;IAClD,CAAC;IAEO,qBAAqB,CAAC,QAAwC;QACpE,QAAQ,EAAE,cAAc,CAAC;YACvB,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,IAAI,CAAC,SAAS;SACvB,CAAC,CAAC;IACL,CAAC;IAEO,sBAAsB;QAC5B,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;QAChD,IAAI,CAAC,cAAc,EAAE,WAAW,CAAC;YAC/B,OAAO,EAAE,IAAI;YACb,MAAM,EAAE,IAAI,CAAC,SAAS;SACvB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,CAAC,KAAqB;QAC3B,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,IAAI,CAAC,iBAAiB,CAAC,UAAU,EAAE,CAAC;QAE7D,IAAI,KAAK,CAAC,GAAG,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAChC,MAAM,gBAAgB,GAAG,KAAK,CAAC,GAAG,CAChC,gBAAgB,CACgB,CAAC;YACnC,IAAI,CAAC,qBAAqB,CAAC,gBAAgB,CAAC,CAAC;YAC7C,IAAI,CAAC,sBAAsB,EAAE,CAAC;QAChC,CAAC;IACH,CAAC;IAED;;;OAGG;IACK,iBAAiB,CAAC,CAAQ;QAChC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YACtB,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,IAAI,IAAI,CAAC,KAAK;gBAAE,IAAI,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC;QAC3D,CAAC;QAED,IAAI,CAAC,aAAa,CAChB,IAAI,WAAW,CAAC,gBAAgB,EAAE,EAAE,MAAM,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC,CAC1D,CAAC;IACJ,CAAC;IAED;;;OAGG;IACK,qBAAqB,CAAC,CAAQ;QACpC,IAAI,IAAI,CAAC,YAAY,IAAI,IAAI,CAAC,YAAY,KAAK,OAAO,EAAE,CAAC;YACvD,CAAC,CAAC,cAAc,EAAE,CAAC;YACnB,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,QAAQ,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAEO,qBAAqB,CAC3B,CAAwC;QAExC,IAAI,CAAC,mBAAmB,EAAE,eAAe,CAAC;YACxC,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,mBAAmB,EAAE,IAAI;SAC1B,CAAC,CAAC;IACL,CAAC;IAED,kEAAkE;IAClE,IAAY,mBAAmB;QAC7B,OAAO,CACL,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC;YAC3B,CAAC,IAAI,CAAC,YAAY;YAClB,IAAI,CAAC,eAAe,KAAK,MAAM,CAChC,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,IAAY,uBAAuB;QACjC,IAAI,CAAC,IAAI,CAAC,mBAAmB;YAAE,OAAO,OAAO,CAAC;QAE9C,OAAO,IAAI,CAAA;;;sBAGO,IAAI,CAAC,2BAA2B;qBACjC,CAAC,CAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,EAAE;;UAE5C,IAAI,CAAC,WAAW,CAAC,GAAG,CACpB,MAAM,CAAC,EAAE,CAAC,IAAI,CAAA;;;uBAGD,CAAC,CAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,EAAE,MAAM,CAAC;;gBAE1D,MAAM,CAAC,KAAK;;WAEjB,CACF;;KAEJ,CAAC;IACJ,CAAC;IAWD,IAAY,IAAI;QACd,MAAM,EACJ,KAAK,EACL,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,aAAa,EACb,SAAS,EACT,aAAa,EACb,gBAAgB,EAChB,gBAAgB,GACjB,GAAG,IAAI,CAAC;QAET,IAAI,CAAC,KAAK;YAAE,OAAO,OAAO,CAAC;QAE3B,QAAQ,IAAI,CAAC,eAAe,EAAE,CAAC;YAC7B,KAAK,MAAM;gBACT,QAAQ,KAAK,CAAC,SAAS,EAAE,CAAC;oBACxB,KAAK,YAAY;wBACf,OAAO,IAAI,CAAA;uBACA,KAAK;oCACQ,kBAAkB;8BACxB,IAAI,CAAC,YAAY;8BACjB,YAAY;+BACX,aAAa;+BACb,aAAa;kCACV,IAAI,CAAC,gBAAgB;8BACzB,IAAI,CAAC,YAAY;4BACnB,IAAI,CAAC,UAAU;gCACX,IAAI,CAAC,oBAAoB;mCACtB,IAAI,CAAC,qBAAqB;;+BAE9B,CAAC;oBACtB,KAAK,SAAS;wBACZ,OAAO,IAAI,CAAA;uBACA,KAAK;oCACQ,kBAAkB;8BACxB,IAAI,CAAC,YAAY;8BACjB,YAAY;+BACX,aAAa;+BACb,aAAa;kCACV,IAAI,CAAC,gBAAgB;8BACzB,IAAI,CAAC,YAAY;gCACf,IAAI,CAAC,oBAAoB;mCACtB,IAAI,CAAC,qBAAqB;;4BAEjC,CAAC;oBACnB,KAAK,QAAQ;wBACX,OAAO,IAAI,CAAA;uBACA,KAAK;oCACQ,kBAAkB;8BACxB,IAAI,CAAC,YAAY;8BACjB,YAAY;+BACX,aAAa;+BACb,aAAa;kCACV,IAAI,CAAC,gBAAgB;8BACzB,IAAI,CAAC,YAAY;gCACf,KAAK;mCACF,IAAI,CAAC,qBAAqB;;2BAElC,CAAC;oBAClB;wBACE,OAAO,IAAI,CAAA;uBACA,KAAK;oCACQ,kBAAkB;8BACxB,IAAI,CAAC,YAAY;+BAChB,IAAI,CAAC,aAAa;8BACnB,IAAI,CAAC,YAAY;2BACpB,SAAS;kCACF,gBAAgB;+BACnB,aAAa;0BAClB,IAAI,CAAC,QAAQ;kCACL,IAAI,CAAC,gBAAgB;8BACzB,IAAI,CAAC,YAAY;4BACnB,IAAI,CAAC,UAAU;6BACd,IAAI,CAAC,WAAW;gCACb,IAAI,CAAC,oBAAoB;8BAC3B,IAAI,CAAC,YAAY;+BAChB,IAAI,CAAC,iBAAiB,CAAC,MAAM;gCAC5B,IAAI,CAAC,kBAAkB;mCACpB,IAAI,CAAC,qBAAqB;;yBAEpC,CAAC;gBAClB,CAAC;YACH,KAAK,cAAc;gBACjB,OAAO,IAAI,CAAA;mBACA,KAAK;gCACQ,kBAAkB;0BACxB,YAAY;2BACX,aAAa;+BACT,iBAAiB;uBACzB,SAAS;8BACF,gBAAgB;2BACnB,aAAa;8BACV,gBAAgB;0BACpB,IAAI,CAAC,YAAY;sBACrB,IAAI,CAAC,QAAQ;8BACL,IAAI,CAAC,gBAAgB;yBAC1B,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW;0BACxC,IAAI,CAAC,YAAY;;6BAEd,CAAC;YACxB,KAAK,aAAa;gBAChB,OAAO,IAAI,CAAA;mBACA,KAAK;gCACQ,kBAAkB;8BACpB,IAAI,CAAC,gBAAgB;0BACzB,YAAY;2BACX,aAAa;+BACT,iBAAiB;uBACzB,SAAS;8BACF,gBAAgB;2BACnB,aAAa;8BACV,gBAAgB;0BACpB,IAAI,CAAC,YAAY;sBACrB,IAAI,CAAC,QAAQ;8BACL,IAAI,CAAC,gBAAgB;yBAC1B,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW;0BACxC,IAAI,CAAC,YAAY;;qBAEtB,CAAC;YAChB;gBACE,OAAO,OAAO,CAAC;QACnB,CAAC;IACH,CAAC;IAED,MAAM,KAAK,MAAM;QACf,OAAO;YACL,WAAW;YACX,gBAAgB;YAChB,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA6IF;SACF,CAAC;IACJ,CAAC;;AAjmB2B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;uDAAmC;AAEjC;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;oDAAsB;AAEtB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;sDAAgD;AAG3E;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wDACS;AAEP;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;mDAAqB;AAGrB;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;kDAAoC;AAGlC;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;uDAAyB;AAGxB;IAA5B,QAAQ,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;yDAA2B;AAE3B;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;0DAA6C;AAM5C;IAA3B,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;wDAEzB;AAKM;IADP,KAAK,CAAC,YAAY,CAAC;iDACe;AAG3B;IADP,KAAK,CAAC,iBAAiB,CAAC;iDACS;AAG1B;IADP,KAAK,CAAC,YAAY,CAAC;uDACwB;AA/DjC,cAAc;IAD1B,aAAa,CAAC,iBAAiB,CAAC;GACpB,cAAc,CA0nB1B","sourcesContent":["import { css, html, nothing, PropertyValues } from 'lit';\nimport { customElement, property, query } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { msg } from '@lit/localize';\nimport type {\n SharedResizeObserverInterface,\n SharedResizeObserverResizeHandlerInterface,\n} from '@internetarchive/shared-resize-observer';\nimport type { TileDisplayMode } from '../models';\nimport type { CollectionTitles } from '../data-source/models';\nimport './grid/collection-tile';\nimport './grid/item-tile';\nimport './grid/account-tile';\nimport './grid/search-tile';\nimport './hover/tile-hover-pane';\nimport './list/tile-list';\nimport './list/tile-list-compact';\nimport './list/tile-list-compact-header';\nimport type { TileHoverPane } from './hover/tile-hover-pane';\nimport { BaseTileComponent } from './base-tile-component';\nimport { LayoutType } from './models';\nimport {\n HoverPaneController,\n HoverPaneControllerInterface,\n HoverPaneProperties,\n HoverPaneProviderInterface,\n} from './hover/hover-pane-controller';\nimport { srOnlyStyle } from '../styles/sr-only';\nimport { tileActionStyles } from '../styles/tile-action-styles';\nimport {\n PREVIEW_PANE_DELAY_MS,\n TilePreviewController,\n} from './preview/tile-preview-controller';\nimport type { TilePreviewOptions } from './preview/tile-preview-media';\n\n@customElement('tile-dispatcher')\nexport class TileDispatcher\n extends BaseTileComponent\n implements\n SharedResizeObserverResizeHandlerInterface,\n HoverPaneProviderInterface\n{\n /*\n * Reactive properties inherited from BaseTileComponent:\n * - model?: TileModel;\n * - tileActions: TileAction[] = [];\n * - currentWidth?: number;\n * - currentHeight?: number;\n * - baseNavigationUrl?: string;\n * - baseImageUrl?: string;\n * - collectionPagePath?: string;\n * - sortParam: SortParam | null = null;\n * - defaultSortParam: SortParam | null = null;\n * - creatorFilter?: string;\n * - mobileBreakpoint?: number;\n * - loggedIn = false;\n * - suppressTileBlurring = false;\n * - useLocalTime = false;\n */\n\n @property({ type: String }) tileDisplayMode?: TileDisplayMode;\n\n @property({ type: Boolean }) isManageView = false;\n\n @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;\n\n @property({ type: Object })\n collectionTitles?: CollectionTitles;\n\n @property({ type: Boolean }) showTvClips = false;\n\n /** What type of simplified layout to use in grid mode, if any */\n @property({ type: String }) layoutType: LayoutType = 'default';\n\n /** Whether this tile should include a hover pane at all (for applicable tile modes) */\n @property({ type: Boolean }) enableHoverPane = false;\n\n /** Whether hovering, focusing or long-pressing this tile starts a live preview */\n @property({ type: Boolean }) enableTilePreview = false;\n\n @property({ type: Object }) tilePreviewOptions: TilePreviewOptions = {};\n\n private previewController = new TilePreviewController(this, {\n enabled: () => this.shouldPreview,\n });\n\n @property({ type: String }) manageCheckTitle = msg(\n 'Remove this item from the list',\n );\n\n private hoverPaneController?: HoverPaneControllerInterface;\n\n @query('#container')\n private container!: HTMLDivElement;\n\n @query('tile-hover-pane')\n private hoverPane?: TileHoverPane;\n\n @query('.tile-link')\n private tileLinkElement?: HTMLAnchorElement;\n\n acquireFocus(): void {\n this.tileLinkElement?.focus();\n }\n\n releaseFocus(): void {\n this.tileLinkElement?.blur();\n }\n\n /** Maps each display mode to whether hover panes should appear in that mode */\n private static readonly HOVER_PANE_DISPLAY_MODES: Record<\n TileDisplayMode,\n boolean\n > = {\n grid: true,\n 'list-compact': true,\n 'list-detail': false,\n 'list-header': false,\n };\n\n render() {\n const isGridMode = this.tileDisplayMode === 'grid';\n const hasTileActions = isGridMode && this.showGridTileActions;\n const hoverPaneTemplate =\n this.hoverPaneController?.getTemplate() ?? nothing;\n const containerClasses = classMap({\n hoverable: isGridMode,\n 'has-tile-actions': hasTileActions,\n });\n return html`\n <div id=\"container\" class=${containerClasses}>\n ${this.tileDisplayMode === 'list-header'\n ? this.headerTemplate\n : this.tileTemplate}\n ${this.gridTileActionsTemplate} ${this.manageCheckTemplate}\n ${hoverPaneTemplate}\n </div>\n `;\n }\n\n protected firstUpdated(): void {\n if (this.shouldPrepareHoverPane) {\n this.hoverPaneController = new HoverPaneController(this, {\n mobileBreakpoint: this.mobileBreakpoint,\n enableLongPress: false,\n // A playing preview gets the tile to itself; the info pane follows later.\n showDelay: () => (this.shouldPreview ? PREVIEW_PANE_DELAY_MS : 300),\n });\n }\n }\n\n private get headerTemplate() {\n const { currentWidth, sortParam, defaultSortParam, mobileBreakpoint } =\n this;\n return html`\n <tile-list-compact-header\n class=\"header\"\n .currentWidth=${currentWidth}\n .sortParam=${sortParam ?? defaultSortParam}\n .mobileBreakpoint=${mobileBreakpoint}\n .tileActions=${this.tileActions}\n >\n </tile-list-compact-header>\n `;\n }\n\n private get tileTemplate() {\n return html`\n ${this.tileDisplayMode === 'list-detail'\n ? this.tile\n : this.linkTileTemplate}\n `;\n }\n\n private get linkTileTemplate() {\n return html`\n <a\n href=${this.linkTileHref}\n aria-label=${this.model?.title ?? 'Untitled item'}\n aria-describedby=\"link-aria-description\"\n aria-haspopup=${this.shouldPrepareHoverPane ? 'dialog' : 'false'}\n title=${this.shouldPrepareHoverPane\n ? nothing // Don't show title tooltips when we have the tile info popups\n : ifDefined(this.model?.title)}\n @click=${this.handleLinkClicked}\n @contextmenu=${this.handleLinkContextMenu}\n class=\"tile-link\"\n >\n ${this.tile}\n </a>\n <div id=\"link-aria-description\" class=\"sr-only\">\n ${msg('Press Down Arrow to preview item details')}\n </div>\n `;\n }\n\n private get linkTileHref(): string | typeof nothing {\n if (!this.model?.identifier || this.baseNavigationUrl == null)\n return nothing;\n\n // Use the server-specified href if available.\n // Otherwise, construct a details page URL from the item identifier.\n if (this.model.href) {\n return `${this.baseNavigationUrl}${this.model.href}`;\n }\n\n return this.displayValueProvider.itemPageUrl(\n this.model.identifier,\n this.model.mediatype === 'collection',\n );\n }\n\n private get manageCheckTemplate() {\n if (!this.isManageView || this.tileDisplayMode !== 'grid') return nothing;\n\n return html`\n <div class=\"manage-check\">\n <input\n type=\"checkbox\"\n title=${this.manageCheckTitle}\n ?checked=${this.model?.checked}\n @change=${this.handleLinkClicked}\n />\n </div>\n `;\n }\n\n /**\n * Whether hover pane behavior should be prepared for this tile\n * (e.g., whether mouse listeners should be attached, etc.)\n */\n private get shouldPrepareHoverPane(): boolean {\n return (\n this.enableHoverPane &&\n !!this.tileDisplayMode &&\n TileDispatcher.HOVER_PANE_DISPLAY_MODES[this.tileDisplayMode] &&\n this.model?.mediatype !== 'search' && // don't show hover panes on search tiles\n !this.model?.captureDates // don't show hover panes on web archive tiles\n );\n }\n\n /** Only plain item tiles preview: not collections, accounts, searches or gated items. */\n private get shouldPreview(): boolean {\n const { model } = this;\n return (\n this.enableTilePreview &&\n this.tileDisplayMode === 'grid' &&\n !this.isManageView &&\n !!model?.identifier &&\n !['collection', 'account', 'search'].includes(model.mediatype) &&\n !model.captureDates &&\n (this.suppressBlurring ||\n (!model.contentWarning && !(model.loginRequired && !this.loggedIn)))\n );\n }\n\n private get isHoverEnabled(): boolean {\n return window.matchMedia('(hover: hover)').matches;\n }\n\n /**\n * Whether the info button should be shown on this tile.\n * Only shown on touch/non-hover devices where a hover pane is available,\n * so the button always has something to toggle.\n */\n private get shouldShowInfoButton(): boolean {\n return !this.isHoverEnabled && this.shouldPrepareHoverPane;\n }\n\n /** @inheritdoc */\n getHoverPane(): TileHoverPane | undefined {\n return this.hoverPane;\n }\n\n /** @inheritdoc */\n getHoverPaneProps(): HoverPaneProperties {\n return this;\n }\n\n handleResize(entry: ResizeObserverEntry): void {\n this.currentWidth = entry.contentRect.width;\n this.currentHeight = entry.contentRect.height;\n }\n\n disconnectedCallback(): void {\n this.stopResizeObservation(this.resizeObserver);\n }\n\n private stopResizeObservation(observer?: SharedResizeObserverInterface) {\n observer?.removeObserver({\n handler: this,\n target: this.container,\n });\n }\n\n private startResizeObservation() {\n this.stopResizeObservation(this.resizeObserver);\n this.resizeObserver?.addObserver({\n handler: this,\n target: this.container,\n });\n }\n\n updated(props: PropertyValues) {\n if (!this.shouldPreview) this.previewController.deactivate();\n\n if (props.has('resizeObserver')) {\n const previousObserver = props.get(\n 'resizeObserver',\n ) as SharedResizeObserverInterface;\n this.stopResizeObservation(previousObserver);\n this.startResizeObservation();\n }\n }\n\n /**\n * Handler for when the tile link is left-clicked. Emits the `resultSelected` event.\n * In manage view, it also checks/unchecks the tile.\n */\n private handleLinkClicked(e: Event): void {\n if (this.isManageView) {\n e.preventDefault();\n if (this.model) this.model.checked = !this.model.checked;\n }\n\n this.dispatchEvent(\n new CustomEvent('resultSelected', { detail: this.model }),\n );\n }\n\n /**\n * Handler for when the tile link is right-clicked.\n * In manage view, it opens the item in a new tab. Otherwise, does nothing.\n */\n private handleLinkContextMenu(e: Event): void {\n if (this.isManageView && this.linkTileHref !== nothing) {\n e.preventDefault();\n window.open(this.linkTileHref, '_blank');\n }\n }\n\n private tileInfoButtonPressed(\n e: CustomEvent<{ x: number; y: number }>,\n ): void {\n this.hoverPaneController?.toggleHoverPane({\n coords: e.detail,\n enableTouchBackdrop: true,\n });\n }\n\n /** Whether tile action buttons should be rendered in grid mode */\n private get showGridTileActions(): boolean {\n return (\n this.tileActions.length > 0 &&\n !this.isManageView &&\n this.tileDisplayMode === 'grid'\n );\n }\n\n /**\n * Template for the grid-mode action buttons. Rendered alongside the inner\n * tile link inside the dispatcher's shadow root so the action buttons can\n * suppress the hover pane on hover.\n */\n private get gridTileActionsTemplate() {\n if (!this.showGridTileActions) return nothing;\n\n return html`\n <div\n class=\"tile-actions grid-tile-actions\"\n @mouseenter=${this.handleGridActionsMouseEnter}\n @mousemove=${(e: Event) => e.stopPropagation()}\n >\n ${this.tileActions.map(\n action => html`\n <button\n class=\"tile-action-btn\"\n @click=${(e: Event) => this.handleTileActionClick(e, action)}\n >\n ${action.label}\n </button>\n `,\n )}\n </div>\n `;\n }\n\n /**\n * When the mouse enters the grid-mode tile actions area, dispatch a\n * synthetic mouseleave on the host to cancel the hover pane's show timer\n * and hide any visible pane.\n */\n private handleGridActionsMouseEnter = (): void => {\n this.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));\n };\n\n private get tile() {\n const {\n model,\n collectionPagePath,\n baseNavigationUrl,\n currentWidth,\n currentHeight,\n sortParam,\n creatorFilter,\n mobileBreakpoint,\n defaultSortParam,\n } = this;\n\n if (!model) return nothing;\n\n switch (this.tileDisplayMode) {\n case 'grid':\n switch (model.mediatype) {\n case 'collection':\n return html`<collection-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n .layoutType=${this.layoutType}\n ?showInfoButton=${this.shouldShowInfoButton}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </collection-tile>`;\n case 'account':\n return html`<account-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n ?showInfoButton=${this.shouldShowInfoButton}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </account-tile>`;\n case 'search':\n return html`<search-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n ?showInfoButton=${false}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </search-tile>`;\n default:\n return html`<item-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .currentWidth=${this.currentWidth}\n .currentHeight=${this.currentHeight}\n .baseImageUrl=${this.baseImageUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n .layoutType=${this.layoutType}\n ?showTvClips=${this.showTvClips}\n ?showInfoButton=${this.shouldShowInfoButton}\n ?useLocalTime=${this.useLocalTime}\n ?previewActive=${this.previewController.active}\n .previewOptions=${this.tilePreviewOptions}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </item-tile>`;\n }\n case 'list-compact':\n return html`<tile-list-compact\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .baseNavigationUrl=${baseNavigationUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .mobileBreakpoint=${mobileBreakpoint}\n .baseImageUrl=${this.baseImageUrl}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .tileActions=${this.isManageView ? [] : this.tileActions}\n ?useLocalTime=${this.useLocalTime}\n >\n </tile-list-compact>`;\n case 'list-detail':\n return html`<tile-list\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .collectionTitles=${this.collectionTitles}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .baseNavigationUrl=${baseNavigationUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .mobileBreakpoint=${mobileBreakpoint}\n .baseImageUrl=${this.baseImageUrl}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .tileActions=${this.isManageView ? [] : this.tileActions}\n ?useLocalTime=${this.useLocalTime}\n >\n </tile-list>`;\n default:\n return nothing;\n }\n }\n\n static get styles() {\n return [\n srOnlyStyle,\n tileActionStyles,\n css`\n :host {\n display: block;\n height: 100%;\n }\n\n collection-tile {\n --tileBorderColor: #555555;\n --tileBackgroundColor: #666666;\n --imageBlockBackgroundColor: #666666;\n }\n\n account-tile {\n --tileBorderColor: #dddddd;\n --imageBlockBackgroundColor: #fcf5e6;\n }\n\n item-tile {\n --tileBorderColor: #dddddd;\n --imageBlockBackgroundColor: #f1f1f4;\n }\n\n search-tile {\n --tileBorderColor: #555555;\n --tileBackgroundColor: #666666;\n --imageBlockBackgroundColor: #666666;\n --iconFillColor: #2c2c2c;\n }\n\n #container {\n position: relative;\n height: 100%;\n border-radius: 4px;\n }\n\n /*\n * When tile actions are present, the container takes on the role of\n * the tile's visual card so the tile content and action row appear\n * as a single unified element. The inner tile's own shadow/radius\n * are disabled via CSS variable overrides to avoid visual\n * duplication, and the action row sits as a footer inside the same\n * card.\n */\n #container.has-tile-actions {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: var(--tileShadow, 1px 1px 2px 0);\n --tileBoxShadow: none;\n --tileCornerRadius: 0;\n }\n\n #container.has-tile-actions .tile-link {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n border-radius: 0;\n }\n\n /* Normal hover shadow lives on the inner anchor for plain tiles */\n #container.hoverable:not(.has-tile-actions) a:focus,\n #container.hoverable:not(.has-tile-actions) a:hover {\n box-shadow: var(\n --tileHoverBoxShadow,\n 0 0 6px 2px rgba(8, 8, 32, 0.8)\n );\n transition: box-shadow 0.1s ease;\n }\n\n /*\n * When the container owns the card visuals, the hover shadow needs\n * to move up to the container so it wraps the action row too.\n */\n #container.hoverable.has-tile-actions:hover {\n box-shadow: var(\n --tileHoverBoxShadow,\n 0 0 6px 2px rgba(8, 8, 32, 0.8)\n );\n transition: box-shadow 0.1s ease;\n }\n\n a {\n display: block;\n height: 100%;\n color: unset;\n text-decoration: none;\n transition: transform 0.05s ease;\n border-radius: 4px;\n outline: none;\n }\n\n a :first-child {\n display: block;\n height: 100%;\n }\n\n .manage-check {\n position: absolute;\n right: 0;\n top: 0;\n border: 5px solid #2c2c2c;\n border-radius: 3px;\n background-color: #2c2c2c;\n z-index: 1;\n }\n\n .manage-check > input[type='checkbox'] {\n display: block;\n margin: 0;\n }\n\n #touch-backdrop {\n position: fixed;\n width: 100vw;\n height: 100vh;\n top: 0;\n left: 0;\n z-index: 2;\n background: transparent;\n }\n\n tile-hover-pane {\n position: absolute;\n top: 0;\n left: -9999px;\n z-index: 2;\n }\n\n /*\n * Grid-mode action row sits flush against the bottom of the card —\n * the buttons' own borders form the visible bottom edge. The outer\n * buttons get rounded bottom corners to match the container so the\n * red border traces cleanly around the card's bottom corners.\n */\n .grid-tile-actions .tile-action-btn:first-child {\n border-bottom-left-radius: 4px;\n }\n\n .grid-tile-actions .tile-action-btn:last-child {\n border-bottom-right-radius: 4px;\n }\n `,\n ];\n }\n}\n"]}
1
+ {"version":3,"file":"tile-dispatcher.js","sourceRoot":"","sources":["../../../src/tiles/tile-dispatcher.ts"],"names":[],"mappings":"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","sourcesContent":["import { css, html, nothing, PropertyValues } from 'lit';\nimport { customElement, property, query } from 'lit/decorators.js';\nimport { classMap } from 'lit/directives/class-map.js';\nimport { ifDefined } from 'lit/directives/if-defined.js';\nimport { msg } from '@lit/localize';\nimport type {\n SharedResizeObserverInterface,\n SharedResizeObserverResizeHandlerInterface,\n} from '@internetarchive/shared-resize-observer';\nimport type { TileDisplayMode } from '../models';\nimport type { CollectionTitles } from '../data-source/models';\nimport './grid/collection-tile';\nimport './grid/item-tile';\nimport './grid/account-tile';\nimport './grid/search-tile';\nimport './hover/tile-hover-pane';\nimport './list/tile-list';\nimport './list/tile-list-compact';\nimport './list/tile-list-compact-header';\nimport type { TileHoverPane } from './hover/tile-hover-pane';\nimport { BaseTileComponent } from './base-tile-component';\nimport { LayoutType } from './models';\nimport {\n HoverPaneController,\n HoverPaneControllerInterface,\n HoverPaneProperties,\n HoverPaneProviderInterface,\n} from './hover/hover-pane-controller';\nimport { srOnlyStyle } from '../styles/sr-only';\nimport { tileActionStyles } from '../styles/tile-action-styles';\nimport {\n PREVIEW_PANE_DELAY_MS,\n TilePreviewController,\n} from './preview/tile-preview-controller';\nimport type { TilePreviewOptions } from './preview/tile-preview-media';\n\n@customElement('tile-dispatcher')\nexport class TileDispatcher\n extends BaseTileComponent\n implements\n SharedResizeObserverResizeHandlerInterface,\n HoverPaneProviderInterface\n{\n /*\n * Reactive properties inherited from BaseTileComponent:\n * - model?: TileModel;\n * - tileActions: TileAction[] = [];\n * - currentWidth?: number;\n * - currentHeight?: number;\n * - baseNavigationUrl?: string;\n * - baseImageUrl?: string;\n * - collectionPagePath?: string;\n * - sortParam: SortParam | null = null;\n * - defaultSortParam: SortParam | null = null;\n * - creatorFilter?: string;\n * - mobileBreakpoint?: number;\n * - loggedIn = false;\n * - suppressTileBlurring = false;\n * - useLocalTime = false;\n */\n\n @property({ type: String }) tileDisplayMode?: TileDisplayMode;\n\n @property({ type: Boolean }) isManageView = false;\n\n @property({ type: Object }) resizeObserver?: SharedResizeObserverInterface;\n\n @property({ type: Object })\n collectionTitles?: CollectionTitles;\n\n @property({ type: Boolean }) showTvClips = false;\n\n /** What type of simplified layout to use in grid mode, if any */\n @property({ type: String }) layoutType: LayoutType = 'default';\n\n /** Whether this tile should include a hover pane at all (for applicable tile modes) */\n @property({ type: Boolean }) enableHoverPane = false;\n\n /** Whether hovering, focusing or long-pressing this tile starts a live preview */\n @property({ type: Boolean }) enableTilePreview = false;\n\n @property({ type: Object }) tilePreviewOptions: TilePreviewOptions = {};\n\n private previewController = new TilePreviewController(this, {\n enabled: () => this.shouldPreview,\n });\n\n @property({ type: String }) manageCheckTitle = msg(\n 'Remove this item from the list',\n );\n\n private hoverPaneController?: HoverPaneControllerInterface;\n\n @query('#container')\n private container!: HTMLDivElement;\n\n @query('tile-hover-pane')\n private hoverPane?: TileHoverPane;\n\n @query('.tile-link')\n private tileLinkElement?: HTMLAnchorElement;\n\n acquireFocus(): void {\n this.tileLinkElement?.focus();\n }\n\n releaseFocus(): void {\n this.tileLinkElement?.blur();\n }\n\n /** Maps each display mode to whether hover panes should appear in that mode */\n private static readonly HOVER_PANE_DISPLAY_MODES: Record<\n TileDisplayMode,\n boolean\n > = {\n grid: true,\n 'list-compact': true,\n 'list-detail': false,\n 'list-header': false,\n };\n\n render() {\n const isGridMode = this.tileDisplayMode === 'grid';\n const hasTileActions = isGridMode && this.showGridTileActions;\n const hoverPaneTemplate =\n this.hoverPaneController?.getTemplate() ?? nothing;\n const containerClasses = classMap({\n hoverable: isGridMode,\n previewable: this.shouldPreview,\n 'has-tile-actions': hasTileActions,\n });\n return html`\n <div id=\"container\" class=${containerClasses}>\n ${this.tileDisplayMode === 'list-header'\n ? this.headerTemplate\n : this.tileTemplate}\n ${this.gridTileActionsTemplate} ${this.manageCheckTemplate}\n ${hoverPaneTemplate}\n </div>\n `;\n }\n\n protected firstUpdated(): void {\n if (this.shouldPrepareHoverPane) {\n this.hoverPaneController = new HoverPaneController(this, {\n mobileBreakpoint: this.mobileBreakpoint,\n enableLongPress: false,\n // A playing preview gets the tile to itself; the info pane follows later.\n showDelay: () => (this.shouldPreview ? PREVIEW_PANE_DELAY_MS : 300),\n });\n }\n }\n\n private get headerTemplate() {\n const { currentWidth, sortParam, defaultSortParam, mobileBreakpoint } =\n this;\n return html`\n <tile-list-compact-header\n class=\"header\"\n .currentWidth=${currentWidth}\n .sortParam=${sortParam ?? defaultSortParam}\n .mobileBreakpoint=${mobileBreakpoint}\n .tileActions=${this.tileActions}\n >\n </tile-list-compact-header>\n `;\n }\n\n private get tileTemplate() {\n return html`\n ${this.tileDisplayMode === 'list-detail'\n ? this.tile\n : this.linkTileTemplate}\n `;\n }\n\n private get linkTileTemplate() {\n return html`\n <a\n href=${this.linkTileHref}\n aria-label=${this.model?.title ?? 'Untitled item'}\n aria-describedby=\"link-aria-description\"\n aria-haspopup=${this.shouldPrepareHoverPane ? 'dialog' : 'false'}\n title=${this.shouldPrepareHoverPane\n ? nothing // Don't show title tooltips when we have the tile info popups\n : ifDefined(this.model?.title)}\n @click=${this.handleLinkClicked}\n @contextmenu=${this.handleLinkContextMenu}\n class=\"tile-link\"\n >\n ${this.tile}\n </a>\n <div id=\"link-aria-description\" class=\"sr-only\">\n ${msg('Press Down Arrow to preview item details')}\n </div>\n `;\n }\n\n private get linkTileHref(): string | typeof nothing {\n if (!this.model?.identifier || this.baseNavigationUrl == null)\n return nothing;\n\n // Use the server-specified href if available.\n // Otherwise, construct a details page URL from the item identifier.\n if (this.model.href) {\n return `${this.baseNavigationUrl}${this.model.href}`;\n }\n\n return this.displayValueProvider.itemPageUrl(\n this.model.identifier,\n this.model.mediatype === 'collection',\n );\n }\n\n private get manageCheckTemplate() {\n if (!this.isManageView || this.tileDisplayMode !== 'grid') return nothing;\n\n return html`\n <div class=\"manage-check\">\n <input\n type=\"checkbox\"\n title=${this.manageCheckTitle}\n ?checked=${this.model?.checked}\n @change=${this.handleLinkClicked}\n />\n </div>\n `;\n }\n\n /**\n * Whether hover pane behavior should be prepared for this tile\n * (e.g., whether mouse listeners should be attached, etc.)\n */\n private get shouldPrepareHoverPane(): boolean {\n return (\n this.enableHoverPane &&\n !!this.tileDisplayMode &&\n TileDispatcher.HOVER_PANE_DISPLAY_MODES[this.tileDisplayMode] &&\n this.model?.mediatype !== 'search' && // don't show hover panes on search tiles\n !this.model?.captureDates // don't show hover panes on web archive tiles\n );\n }\n\n /** Only plain item tiles preview: not collections, accounts, searches or gated items. */\n private get shouldPreview(): boolean {\n const { model } = this;\n return (\n this.enableTilePreview &&\n this.tileDisplayMode === 'grid' &&\n !this.isManageView &&\n !!model?.identifier &&\n !['collection', 'account', 'search'].includes(model.mediatype) &&\n !model.captureDates &&\n (this.suppressBlurring ||\n (!model.contentWarning && !(model.loginRequired && !this.loggedIn)))\n );\n }\n\n private get isHoverEnabled(): boolean {\n return window.matchMedia('(hover: hover)').matches;\n }\n\n /**\n * Whether the info button should be shown on this tile.\n * Only shown on touch/non-hover devices where a hover pane is available,\n * so the button always has something to toggle.\n */\n private get shouldShowInfoButton(): boolean {\n return !this.isHoverEnabled && this.shouldPrepareHoverPane;\n }\n\n /** @inheritdoc */\n getHoverPane(): TileHoverPane | undefined {\n return this.hoverPane;\n }\n\n /** @inheritdoc */\n getHoverPaneProps(): HoverPaneProperties {\n return this;\n }\n\n handleResize(entry: ResizeObserverEntry): void {\n this.currentWidth = entry.contentRect.width;\n this.currentHeight = entry.contentRect.height;\n }\n\n disconnectedCallback(): void {\n this.stopResizeObservation(this.resizeObserver);\n }\n\n private stopResizeObservation(observer?: SharedResizeObserverInterface) {\n observer?.removeObserver({\n handler: this,\n target: this.container,\n });\n }\n\n private startResizeObservation() {\n this.stopResizeObservation(this.resizeObserver);\n this.resizeObserver?.addObserver({\n handler: this,\n target: this.container,\n });\n }\n\n updated(props: PropertyValues) {\n if (!this.shouldPreview) this.previewController.deactivate();\n\n if (props.has('resizeObserver')) {\n const previousObserver = props.get(\n 'resizeObserver',\n ) as SharedResizeObserverInterface;\n this.stopResizeObservation(previousObserver);\n this.startResizeObservation();\n }\n }\n\n /**\n * Handler for when the tile link is left-clicked. Emits the `resultSelected` event.\n * In manage view, it also checks/unchecks the tile.\n */\n private handleLinkClicked(e: Event): void {\n if (this.isManageView) {\n e.preventDefault();\n if (this.model) this.model.checked = !this.model.checked;\n }\n\n this.dispatchEvent(\n new CustomEvent('resultSelected', { detail: this.model }),\n );\n }\n\n /**\n * Handler for when the tile link is right-clicked.\n * In manage view, it opens the item in a new tab. Otherwise, does nothing.\n */\n private handleLinkContextMenu(e: Event): void {\n if (this.isManageView && this.linkTileHref !== nothing) {\n e.preventDefault();\n window.open(this.linkTileHref, '_blank');\n }\n }\n\n private tileInfoButtonPressed(\n e: CustomEvent<{ x: number; y: number }>,\n ): void {\n this.hoverPaneController?.toggleHoverPane({\n coords: e.detail,\n enableTouchBackdrop: true,\n });\n }\n\n /** Whether tile action buttons should be rendered in grid mode */\n private get showGridTileActions(): boolean {\n return (\n this.tileActions.length > 0 &&\n !this.isManageView &&\n this.tileDisplayMode === 'grid'\n );\n }\n\n /**\n * Template for the grid-mode action buttons. Rendered alongside the inner\n * tile link inside the dispatcher's shadow root so the action buttons can\n * suppress the hover pane on hover.\n */\n private get gridTileActionsTemplate() {\n if (!this.showGridTileActions) return nothing;\n\n return html`\n <div\n class=\"tile-actions grid-tile-actions\"\n @mouseenter=${this.handleGridActionsMouseEnter}\n @mousemove=${(e: Event) => e.stopPropagation()}\n >\n ${this.tileActions.map(\n action => html`\n <button\n class=\"tile-action-btn\"\n @click=${(e: Event) => this.handleTileActionClick(e, action)}\n >\n ${action.label}\n </button>\n `,\n )}\n </div>\n `;\n }\n\n /**\n * When the mouse enters the grid-mode tile actions area, dispatch a\n * synthetic mouseleave on the host to cancel the hover pane's show timer\n * and hide any visible pane.\n */\n private handleGridActionsMouseEnter = (): void => {\n this.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));\n };\n\n private get tile() {\n const {\n model,\n collectionPagePath,\n baseNavigationUrl,\n currentWidth,\n currentHeight,\n sortParam,\n creatorFilter,\n mobileBreakpoint,\n defaultSortParam,\n } = this;\n\n if (!model) return nothing;\n\n switch (this.tileDisplayMode) {\n case 'grid':\n switch (model.mediatype) {\n case 'collection':\n return html`<collection-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n .layoutType=${this.layoutType}\n ?showInfoButton=${this.shouldShowInfoButton}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </collection-tile>`;\n case 'account':\n return html`<account-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n ?showInfoButton=${this.shouldShowInfoButton}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </account-tile>`;\n case 'search':\n return html`<search-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .baseImageUrl=${this.baseImageUrl}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .creatorFilter=${creatorFilter}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n ?showInfoButton=${false}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </search-tile>`;\n default:\n return html`<item-tile\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .currentWidth=${this.currentWidth}\n .currentHeight=${this.currentHeight}\n .baseImageUrl=${this.baseImageUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .isManageView=${this.isManageView}\n .layoutType=${this.layoutType}\n ?showTvClips=${this.showTvClips}\n ?showInfoButton=${this.shouldShowInfoButton}\n ?useLocalTime=${this.useLocalTime}\n ?previewActive=${this.previewController.active}\n .previewOptions=${this.tilePreviewOptions}\n @infoButtonPressed=${this.tileInfoButtonPressed}\n >\n </item-tile>`;\n }\n case 'list-compact':\n return html`<tile-list-compact\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .baseNavigationUrl=${baseNavigationUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .mobileBreakpoint=${mobileBreakpoint}\n .baseImageUrl=${this.baseImageUrl}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .tileActions=${this.isManageView ? [] : this.tileActions}\n ?useLocalTime=${this.useLocalTime}\n >\n </tile-list-compact>`;\n case 'list-detail':\n return html`<tile-list\n .model=${model}\n .collectionPagePath=${collectionPagePath}\n .collectionTitles=${this.collectionTitles}\n .currentWidth=${currentWidth}\n .currentHeight=${currentHeight}\n .baseNavigationUrl=${baseNavigationUrl}\n .sortParam=${sortParam}\n .defaultSortParam=${defaultSortParam}\n .creatorFilter=${creatorFilter}\n .mobileBreakpoint=${mobileBreakpoint}\n .baseImageUrl=${this.baseImageUrl}\n .loggedIn=${this.loggedIn}\n .suppressBlurring=${this.suppressBlurring}\n .tileActions=${this.isManageView ? [] : this.tileActions}\n ?useLocalTime=${this.useLocalTime}\n >\n </tile-list>`;\n default:\n return nothing;\n }\n }\n\n static get styles() {\n return [\n srOnlyStyle,\n tileActionStyles,\n css`\n :host {\n display: block;\n height: 100%;\n }\n\n collection-tile {\n --tileBorderColor: #555555;\n --tileBackgroundColor: #666666;\n --imageBlockBackgroundColor: #666666;\n }\n\n account-tile {\n --tileBorderColor: #dddddd;\n --imageBlockBackgroundColor: #fcf5e6;\n }\n\n item-tile {\n --tileBorderColor: #dddddd;\n --imageBlockBackgroundColor: #f1f1f4;\n }\n\n search-tile {\n --tileBorderColor: #555555;\n --tileBackgroundColor: #666666;\n --imageBlockBackgroundColor: #666666;\n --iconFillColor: #2c2c2c;\n }\n\n #container {\n position: relative;\n height: 100%;\n border-radius: 4px;\n }\n\n /*\n * When tile actions are present, the container takes on the role of\n * the tile's visual card so the tile content and action row appear\n * as a single unified element. The inner tile's own shadow/radius\n * are disabled via CSS variable overrides to avoid visual\n * duplication, and the action row sits as a footer inside the same\n * card.\n */\n #container.has-tile-actions {\n display: flex;\n flex-direction: column;\n overflow: hidden;\n box-shadow: var(--tileShadow, 1px 1px 2px 0);\n --tileBoxShadow: none;\n --tileCornerRadius: 0;\n }\n\n #container.has-tile-actions .tile-link {\n flex: 1;\n min-height: 0;\n overflow: hidden;\n border-radius: 0;\n }\n\n /* A long-press starts a preview, so it must not open the iOS link menu or select text */\n #container.previewable a {\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n user-select: none;\n }\n\n /* Normal hover shadow lives on the inner anchor for plain tiles */\n #container.hoverable:not(.has-tile-actions) a:focus,\n #container.hoverable:not(.has-tile-actions) a:hover {\n box-shadow: var(\n --tileHoverBoxShadow,\n 0 0 6px 2px rgba(8, 8, 32, 0.8)\n );\n transition: box-shadow 0.1s ease;\n }\n\n /*\n * When the container owns the card visuals, the hover shadow needs\n * to move up to the container so it wraps the action row too.\n */\n #container.hoverable.has-tile-actions:hover {\n box-shadow: var(\n --tileHoverBoxShadow,\n 0 0 6px 2px rgba(8, 8, 32, 0.8)\n );\n transition: box-shadow 0.1s ease;\n }\n\n a {\n display: block;\n height: 100%;\n color: unset;\n text-decoration: none;\n transition: transform 0.05s ease;\n border-radius: 4px;\n outline: none;\n }\n\n a :first-child {\n display: block;\n height: 100%;\n }\n\n .manage-check {\n position: absolute;\n right: 0;\n top: 0;\n border: 5px solid #2c2c2c;\n border-radius: 3px;\n background-color: #2c2c2c;\n z-index: 1;\n }\n\n .manage-check > input[type='checkbox'] {\n display: block;\n margin: 0;\n }\n\n #touch-backdrop {\n position: fixed;\n width: 100vw;\n height: 100vh;\n top: 0;\n left: 0;\n z-index: 2;\n background: transparent;\n }\n\n tile-hover-pane {\n position: absolute;\n top: 0;\n left: -9999px;\n z-index: 2;\n }\n\n /*\n * Grid-mode action row sits flush against the bottom of the card —\n * the buttons' own borders form the visible bottom edge. The outer\n * buttons get rounded bottom corners to match the container so the\n * red border traces cleanly around the card's bottom corners.\n */\n .grid-tile-actions .tile-action-btn:first-child {\n border-bottom-left-radius: 4px;\n }\n\n .grid-tile-actions .tile-action-btn:last-child {\n border-bottom-right-radius: 4px;\n }\n `,\n ];\n }\n}\n"]}
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/internetarchive/iaux-collection-browser.git"
9
9
  },
10
- "version": "4.9.1-webdev-9670.0",
10
+ "version": "4.9.1-webdev-9670.2",
11
11
  "main": "dist/index.js",
12
12
  "module": "dist/index.js",
13
13
  "files": [