PhotoPack refactor

This commit is contained in:
Jon McClure 2022-08-22 14:09:52 +01:00
parent e94ecff47c
commit 0202c4ac6e
7 changed files with 248 additions and 163 deletions

View file

@ -4,9 +4,10 @@
// @ts-ignore
import componentDocs from './stories/docs/component.md?raw';
// @ts-ignore
import moreDocs from './stories/docs/more.md?raw';
import quickitDocs from './stories/docs/quickit.md?raw';
import PhotoPack from './PhotoPack.svelte';
import { getPhotoPackPropsFromDoc } from './docProps';
import { withComponentDocs, withStoryDocs } from '$docs/utils/withParams.js';
@ -28,7 +29,6 @@
const defaultImages = [
{
row: 1,
src: 'https://via.placeholder.com/1024x768.jpg',
altText: 'alt text',
caption:
@ -36,21 +36,18 @@
maxHeight: 400,
},
{
row: 2,
src: 'https://via.placeholder.com/1640x1180.jpg',
altText: 'alt text',
caption:
'Surveillance footage shows a missile hitting a residential building in Kyiv, Ukraine, February 26, 2022, in this still image taken from a video obtained by REUTERS',
},
{
row: 2,
src: 'https://via.placeholder.com/1200x900.jpg',
altText: 'alt text',
caption:
'People walk past the remains of a missile at a bus terminal in Kyiv, Ukraine March 4, 2022. REUTERS/Valentyn Ogirenko',
},
{
row: 2,
src: 'https://via.placeholder.com/1024x768.jpg',
altText: 'alt text',
caption:
@ -58,43 +55,51 @@
},
];
const groupedImages = [
{
row: 1,
src: 'https://via.placeholder.com/1024x768.jpg',
altText: 'alt text',
caption:
'A residential building destroyed by shelling in the settlement of Borodyanka in the Kyiv region, Ukraine March 3, 2022. Picture taken with a drone. REUTERS/Maksim Levin',
maxHeight: 400,
},
{
row: 2,
group: 1,
maxHeight: 300,
src: 'https://via.placeholder.com/1640x1180.jpg',
altText: 'alt text',
caption:
'Surveillance footage shows a missile hitting a residential building in Kyiv, Ukraine, February 26, 2022, in this still image taken from a video obtained by REUTERS',
},
{
row: 2,
group: 2,
maxHeight: 300,
src: 'https://via.placeholder.com/1200x900.jpg',
altText: 'alt text',
caption:
'People walk past the remains of a missile at a bus terminal in Kyiv, Ukraine March 4, 2022. REUTERS/Valentyn Ogirenko',
},
{
row: 2,
group: 2,
maxHeight: 300,
src: 'https://via.placeholder.com/1024x768.jpg',
altText: 'alt text',
caption:
'People walk past the remains of a missile at a bus terminal. REUTERS/Valentyn Ogirenko',
},
const defaultLayouts = [
{ breakpoint: 450, rows: [1, 2, 1] },
{ breakpoint: 750, rows: [1, 3] },
];
const quickItBlock = {
Type: 'photo-pack',
ID: 'my-photo-pack',
Class: 'mb-2',
Width: 'wide',
CaptionWidth: 'normal',
Gap: '10',
Images: [
{
Src: 'https://via.placeholder.com/1024x768.jpg',
AltText: 'alt text',
Caption: 'Lorem ipsum. Reuters/Photog',
MaxHeight: '600',
},
{
Src: 'https://via.placeholder.com/1024x768.jpg',
AltText: 'alt text',
Caption: 'Lorem ipsum. Reuters/Photog',
},
{
Src: 'https://via.placeholder.com/1024x768.jpg',
AltText: 'alt text',
Caption: 'Lorem ipsum. Reuters/Photog',
},
{
Src: 'https://via.placeholder.com/1024x768.jpg',
AltText: 'alt text',
Caption: 'Lorem ipsum. Reuters/Photog',
},
{
Src: 'https://via.placeholder.com/1024x768.jpg',
AltText: 'alt text',
Caption: 'Lorem ipsum. Reuters/Photog',
},
],
Layouts: [
{ Breakpoint: '750', Rows: '2,3' },
{ Breakpoint: '450', Rows: '1, 2, 2' },
],
};
</script>
<Meta {...meta} />
@ -107,18 +112,14 @@
name="Default"
args="{{
width: 'wide',
captionWidth: 'normal',
images: defaultImages,
breakRows: 750,
layouts: defaultLayouts,
}}"
/>
<Story
name="More settings"
args="{{
width: 'wide',
images: groupedImages,
breakRows: 750,
breakGroups: 600,
}}"
{...withStoryDocs(moreDocs)}
name="🚀 QUICKIT"
{...withStoryDocs(quickitDocs)}
args="{getPhotoPackPropsFromDoc(quickItBlock)}"
/>

View file

@ -3,48 +3,23 @@
src: string;
altText: string;
caption?: string;
row?: number | string;
group?: number | string;
maxHeight?: number | string;
maxHeight?: number;
}
/**
* A set of images
* Array of image objects
* @required
*/
export let images: Image[] = [];
// Coerce string values to numbers, where needed
$: imgs = images.map((img) => ({
...img,
row: !img.row
? 1
: typeof img.row === 'string'
? parseInt(img.row) || 1
: img.row,
group: !img.group
? 1
: typeof img.group === 'string'
? parseInt(img.group) || 1
: img.group,
maxHeight: !img.maxHeight
? null
: img.maxHeight === 'string'
? parseFloat(img.maxHeight) || null
: img.maxHeight,
}));
interface Layout {
breakpoint: number;
rows: number[];
}
/**
* Container width below which to break rows.
* @type {number}
* Array of layout objects
* @required
*/
export let breakRows: number = 900;
/**
* Container width below which to break groups, if groups specified in images.
* (Should be smaller than `breakRows`.)
* @type {number}
*/
export let breakGroups: number = breakRows;
export let layouts: Layout[] = [];
/**
* Gap between images.
* @type {number}
@ -80,24 +55,35 @@
import Block from '../Block/Block.svelte';
import PaddingReset from '../PaddingReset/index.svelte';
import { groupBy } from 'lodash-es';
import { marked } from 'marked';
const group = (groupArray, key = 'row') => {
const groupObj = groupBy(groupArray, (d) => d[key]);
const groupKeys = Object.keys(groupObj).sort();
return groupKeys.map((k) =>
key === 'row' ? group(groupObj[k], 'group') : groupObj[k]
);
};
$: rows = group(imgs);
let containerWidth;
$: rowsBroken = (containerWidth || Infinity) <= breakRows;
$: groupsBroken =
(containerWidth || Infinity) <=
(breakGroups < breakRows ? breakGroups : breakRows);
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}" cls="photopack {cls}">
@ -107,28 +93,15 @@
class="photopack-row"
style:gap="0 {gap}px"
style:margin-bottom="{gap + 'px'}"
class:break="{rowsBroken}"
>
{#each row as group, gi}
<div
class="photopack-group"
style:gap="0 {gap}px"
style:margin-bottom="{gap + 'px'}"
class:break="{groupsBroken}"
>
{#each group as img, i}
<figure
style="--gap: {gap}px;"
aria-labelledby="{id}-figure-{ri}-{gi}-{i}"
>
<img
src="{img.src}"
alt="{img.altText}"
style:max-height="{img.maxHeight ? img.maxHeight + 'px' : ''}"
/>
</figure>
{/each}
</div>
{#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' : ''}"
/>
</figure>
{/each}
</div>
{/each}
@ -137,14 +110,12 @@
<Block width="{captionWidth}">
<div class="captions-container">
{#each rows as row, ri}
{#each row as group, gi}
{#each group as img, i}
{#if img.caption}
<div id="{id}-figure-{ri}-{gi}-{i}" class="caption">
{@html marked(img.caption)}
</div>
{/if}
{/each}
{#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>
@ -163,51 +134,33 @@
div.photopack-row {
display: flex;
justify-content: space-between;
&.break {
display: block;
div.photopack-group {
display: flex;
justify-content: space-between;
&.break {
display: block;
figure {
display: block;
max-height: unset;
margin-bottom: var(--gap);
}
}
}
}
div.photopack-group {
display: contents;
}
}
figure {
flex: 1;
margin: 0;
padding: 0;
img {
figure {
flex: 1;
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
padding: 0;
img {
margin: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
}
}
}
div.captions-container {
div.caption {
margin: 0 0 0.5rem;
margin: 0 0 0.6rem;
&:last-of-type {
margin-bottom: 0;
}
:global(p) {
font-size: 0.85rem;
line-height: 1.15rem;
line-height: 1.10rem;
font-family: var(--theme-font-family-note, $font-family-display);
color: var(--theme-colour-text-secondary, $tr-medium-grey);
margin: 0;
font-weight: 300;
}
}
}

View file

@ -0,0 +1,44 @@
import urlJoin from 'proper-url-join';
interface BlockImage {
Src: string;
AltText: string;
Caption?: string;
MaxHeight?: string;
}
interface BlockLayout {
Breakpoint: string;
Rows: string;
}
interface Block {
Type: string;
ID?: string;
Class?: string;
Width: string;
CaptionWidth?: string;
Gap?: string;
Images: BlockImage[],
Layouts: BlockLayout[],
}
export const getPhotoPackPropsFromDoc = (docBlock: Block, assetsPath: string = '') => {
return {
id: docBlock.ID,
cls: docBlock.Class,
width: docBlock.Width,
captionWidth: docBlock.CaptionWidth,
gap: docBlock.Gap && isNaN(docBlock.Gap as any) ? null : parseInt(docBlock.Gap),
images: docBlock.Images.map((img) => ({
src: /^https?:\/\/|^\/\//i.test(img.Src) ? img.Src : urlJoin(assetsPath, img.Src),
altText: img.AltText,
caption: img.Caption,
maxHeight: img.MaxHeight && isNaN(img.MaxHeight as any) ? null : parseFloat(img.MaxHeight),
})),
layouts: docBlock.Layouts.map((layout) => ({
breakpoint: isNaN(layout.Breakpoint as any) ? null : parseFloat(layout.Breakpoint),
rows: layout.Rows.split(',').map(r => parseInt(r.trim())),
})),
};
};

View file

@ -1,11 +1,30 @@
> **Coming soon!**
---
The `PhotoPack` component makes simple photo grids with custom layouts at whatever breakpoint you need.
```svelte
<script>
import { PhotoPack } from '@reuters-graphics/graphics-svelte-components';
const images = [
{
src: 'https://...',
altText: 'Alt text',
caption:'Lorem ipsum. REUTERS/Photog',
// Optional max-height of image across all layouts
maxHeight: 800,
},
// ...
];
const layouts = [
{
// Breakpoint above which this layout applies
breakpoint: 450,
// Number of photos in each row of this layout, adding up to the total number of images
rows: [1, 2, 1]
},
{ breakpoint: 750, rows: [1, 3] },
];
</script>
<PhotoPack />
<PhotoPack images={images} layouts={layouts} />
```

View file

@ -1 +0,0 @@
You can set sub groups and maxHeights...

View file

@ -0,0 +1,65 @@
Setup your Google Doc with the images, layouts and other props you want:
```yaml
# Google doc block
Type: photo-pack
ID: my-photo-pack
Class: mb-2 # adjust margin class as needed
Width: wide
CaptionWidth: normal
Gap: 10
[.Images]
Src: images/my-img-1.jpg
AltText: Alt text
Caption: Lorem ipsum. REUTERS/Photog
MaxHeight: 600 # Optional max-height
Src: images/my-img-2.jpg
AltText: Alt text
Caption: Lorem ipsum. REUTERS/Photog
Src: images/my-img-3.jpg
AltText: Alt text
Caption: Lorem ipsum. REUTERS/Photog
Src: images/my-img-4.jpg
AltText: Alt text
Caption: Lorem ipsum. REUTERS/Photog
Src: images/my-img-5.jpg
AltText: Alt text
Caption: Lorem ipsum. REUTERS/Photog
[]
[.Layouts]
Breakpoint: 750
Rows: 2,3 # Must add to total number of images!
Breakpoint: 450
Rows: 1,2,2
[]
```
In your project, you can use the `getPhotoPackPropsFromDoc` utilty to easily convert the GoogleDoc format above into the props the `PhotoPack` component expects.
```svelte
<!-- App.svelte -->
<script>
import { getPhotoPackPropsFromDoc, PhotoPack } from '@reuters-graphics/graphics-components';
// These should be already imported for you.
import content from '$locales/en/content.json';
import { assets } from '$app/paths';
</script>
{#each content.blocks as block}
{#if block.Type === 'text'}
<!-- ... other blocks -->
<!-- Copy/paste into your blocks loop! -->
{:else if block.Type === 'photo-pack'}
<PhotoPack {...getPhotoPackPropsFromDoc(block, assets)} />
<!-- END copy/paste -->
{/if}
{/each}
```

View file

@ -1,3 +1,4 @@
// Components
export { default as Article } from './components/Article/Article.svelte';
export { default as BeforeAfter } from './components/BeforeAfter/BeforeAfter.svelte';
export { default as Block } from './components/Block/Block.svelte';
@ -25,3 +26,6 @@ export {
} from './components/Theme/Theme.svelte';
export { default as Video } from './components/Video/Video.svelte';
export { default as Visible } from './components/Visible/Visible.svelte';
// Utilities
export { getPhotoPackPropsFromDoc } from './components/PhotoPack/docProps';