hypnagaga/src/components/PhotoPack/PhotoPack.svelte
2023-07-22 16:54:36 +01:00

184 lines
4.8 KiB
Svelte

<!-- @component `PhotoPack` [Read the docs.](https://reuters-graphics.github.io/graphics-components/?path=/docs/components-PhotoPack--default) -->
<script lang="ts">
interface Image {
src: string;
altText: string;
caption?: string;
maxHeight?: number;
}
/**
* Array of image objects
* @required
*/
export let images: Image[] = [];
interface Layout {
breakpoint: number;
rows: number[];
}
/**
* Array of layout objects
* @required
*/
export let layouts: Layout[] = [];
/**
* Gap between images.
* @type {number}
*/
export let gap = 10;
const random4 = () =>
Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
/**
* Add an ID to target with SCSS. Should be unique from all other elements.
* @type {string}
*/
export let id: string = 'photopack-' + random4() + random4();
/**
* Add a class to target with SCSS.
* @type {string}
*/
let cls: string = '';
export { cls as class };
type ContainerWidth = 'normal' | 'wide' | 'wider' | 'widest' | 'fluid';
/** Width of the component within the text well. */
export let width: ContainerWidth = 'normal';
/**
* Set a different width for captions within the text well, for example,
* "normal" to keep captions inline with the rest of the text well.
* Can't ever be wider than `width`.
* @type {string}
*/
export let captionWidth: ContainerWidth = 'normal';
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/PaddingReset.svelte';
import { marked } from 'marked';
let containerWidth;
const groupRows = (images, layout) => {
// Default layout, one img per row
if (!layout) return images.map((img) => [img]);
// Otherwise, chunk into rows according to layout scheme
let i = 0;
const rows = [];
for (const rowLength of layout.rows) {
const row = [];
for (const imgI of [...Array(rowLength).keys()]) {
row.push(images[imgI + i]);
}
rows.push(row);
i += rowLength;
}
return rows;
};
// Sort so breakpoints always descend
$: layouts.sort((a, b) => (a.breakpoint < b.breakpoint ? 1 : -1));
$: layout = layouts.find(
(l) =>
// Must have valid rows schema, i.e., adds to the total number of images
l.rows.reduce((a, b) => a + b, 0) === images.length &&
// Breakpoint is higher than container width
(containerWidth || 0) >= l.breakpoint
);
$: rows = groupRows(images, layout);
</script>
<Block width="{width}" id="{id}" class="photopack {cls}">
<div class="photopack-container" bind:clientWidth="{containerWidth}">
{#each rows as row, ri}
<div
class="photopack-row"
style:gap="0 {gap}px"
style:margin-bottom="{gap + 'px'}"
>
{#each row as img, i}
<figure aria-labelledby="{id}-figure-{ri}-{i}">
<img
src="{img.src}"
alt="{img.altText}"
style:max-height="{img.maxHeight ? img.maxHeight + 'px' : ''}"
/>
{#if !img.altText}
<div class="alt-warning">altText</div>
{/if}
</figure>
{/each}
</div>
{/each}
</div>
<PaddingReset containerIsFluid="{width === 'fluid'}">
<Block width="{captionWidth}">
<div class="captions-container">
{#each rows as row, ri}
{#each row as img, i}
{#if img.caption}
<div id="{id}-figure-{ri}-{i}" class="caption">
{@html marked(img.caption)}
</div>
{/if}
{/each}
{/each}
</div>
</Block>
</PaddingReset>
</Block>
<style lang="scss">
@import '../../scss/fonts/variables';
div.photopack-container {
display: block;
width: 100%;
margin-bottom: 10px;
div.photopack-row {
display: flex;
justify-content: space-between;
figure {
flex: 1;
margin: 0;
padding: 0;
position: relative;
img {
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
div.alt-warning {
font-family: $font-family-display;
padding: 5px 10px;
background-color: red;
color: white;
position: absolute;
top: 0;
right: 0;
font-size: 14px;
line-height: 16px;
}
}
}
}
div.captions-container {
div.caption {
margin: 0 0 0.6rem;
&:last-of-type {
margin-bottom: 0;
}
:global(p) {
font-size: 0.85rem;
line-height: 1.1rem;
font-family: var(--theme-font-family-note, $font-family-display);
color: var(--theme-colour-text-secondary);
margin: 0;
font-weight: 300;
}
}
}
</style>