feat: present cleared accessible project media
This commit is contained in:
93
src/components/ProjectMedia.vue
Normal file
93
src/components/ProjectMedia.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue';
|
||||
import AccessibleVideo, { type AccessibleVideoSource } from './AccessibleVideo.vue';
|
||||
import ChartFigure from './ChartFigure.vue';
|
||||
import { productionInputsKey } from '@/app';
|
||||
import { assetUrl } from '@/content/asset-url';
|
||||
import type { MediaPresentation, PublicAsset, PublicAssetManifest } from '@/content/types';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
presentation: Readonly<MediaPresentation>;
|
||||
allowAutomaticVideo?: boolean;
|
||||
manifest?: Readonly<PublicAssetManifest>;
|
||||
}>(), { allowAutomaticVideo: false });
|
||||
|
||||
const inputs = inject(productionInputsKey, undefined);
|
||||
const resolvedManifest = computed(() => props.manifest ?? inputs?.manifest);
|
||||
|
||||
const resolveAsset = (assetId: string): PublicAsset => {
|
||||
const manifest = resolvedManifest.value;
|
||||
if (manifest === undefined) throw new Error('Project media requires the public asset manifest.');
|
||||
const asset = manifest.assets.find((candidate) => candidate.id === assetId);
|
||||
if (asset === undefined) throw new Error(`Unknown asset ID: ${assetId}`);
|
||||
return asset;
|
||||
};
|
||||
|
||||
const primaryAsset = computed(() => resolveAsset(props.presentation.assetId));
|
||||
const primaryUrl = computed(() => assetUrl(resolvedManifest.value!, props.presentation.assetId));
|
||||
const assertImage = (asset: PublicAsset): PublicAsset => {
|
||||
if (!asset.mediaType.startsWith('image/') || asset.width === null || asset.height === null) {
|
||||
throw new Error(`Project media asset is not an intrinsic-size image: ${asset.id}`);
|
||||
}
|
||||
return asset;
|
||||
};
|
||||
const imageAsset = computed(() => assertImage(primaryAsset.value));
|
||||
const videoSources = computed<readonly AccessibleVideoSource[]>(() => {
|
||||
if (props.presentation.kind !== 'video') return [];
|
||||
const ids = [props.presentation.assetId, props.presentation.fallbackAssetId].filter((id): id is string => id !== undefined);
|
||||
return ids.map((id) => {
|
||||
const asset = resolveAsset(id);
|
||||
if (asset.mediaType !== 'video/webm' && asset.mediaType !== 'video/mp4') {
|
||||
throw new Error(`Project video source has an invalid media type: ${id}`);
|
||||
}
|
||||
return { src: assetUrl(resolvedManifest.value!, id), type: asset.mediaType };
|
||||
});
|
||||
});
|
||||
const videoAsset = computed(() => {
|
||||
const asset = primaryAsset.value;
|
||||
if ((asset.mediaType !== 'video/webm' && asset.mediaType !== 'video/mp4') || asset.width === null || asset.height === null) {
|
||||
throw new Error(`Project media asset is not an intrinsic-size video: ${asset.id}`);
|
||||
}
|
||||
return asset;
|
||||
});
|
||||
const posterUrl = computed(() => {
|
||||
if (props.presentation.kind !== 'video') return '';
|
||||
assertImage(resolveAsset(props.presentation.posterAssetId));
|
||||
return assetUrl(resolvedManifest.value!, props.presentation.posterAssetId);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ChartFigure
|
||||
v-if="presentation.kind === 'chart'"
|
||||
:presentation="presentation"
|
||||
:src="primaryUrl"
|
||||
:width="imageAsset.width!"
|
||||
:height="imageAsset.height!"
|
||||
/>
|
||||
<AccessibleVideo
|
||||
v-else-if="presentation.kind === 'video'"
|
||||
:sources="videoSources"
|
||||
:poster="posterUrl"
|
||||
:width="videoAsset.width!"
|
||||
:height="videoAsset.height!"
|
||||
:accessible-label="presentation.alt"
|
||||
:caption="presentation.caption"
|
||||
:description="presentation.longDescription"
|
||||
:allow-automatic="allowAutomaticVideo"
|
||||
/>
|
||||
<figure v-else class="project-media project-media--image" data-project-media>
|
||||
<img
|
||||
:src="primaryUrl"
|
||||
:alt="presentation.alt"
|
||||
:width="imageAsset.width!"
|
||||
:height="imageAsset.height!"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
<figcaption v-if="presentation.caption">{{ presentation.caption }}</figcaption>
|
||||
<p v-if="presentation.longDescription" class="media-description" data-testid="media-long-description">
|
||||
{{ presentation.longDescription }}
|
||||
</p>
|
||||
</figure>
|
||||
</template>
|
||||
Reference in New Issue
Block a user