PhotoPack refactor
This commit is contained in:
parent
e94ecff47c
commit
0202c4ac6e
7 changed files with 248 additions and 163 deletions
|
|
@ -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)}"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
44
src/components/PhotoPack/docProps.ts
Normal file
44
src/components/PhotoPack/docProps.ts
Normal 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())),
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
|
@ -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} />
|
||||
```
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
You can set sub groups and maxHeights...
|
||||
65
src/components/PhotoPack/stories/docs/quickit.md
Normal file
65
src/components/PhotoPack/stories/docs/quickit.md
Normal 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}
|
||||
```
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
Loading…
Reference in a new issue