@massimo-cassandro/minimo 1.14.2 → 1.14.3
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.
package/package.json
CHANGED
|
@@ -2,6 +2,12 @@ import { classnames } from './classnames.js';
|
|
|
2
2
|
|
|
3
3
|
/** @typedef {import('./dom-builder/dom-builder.js').DomBuilderItem} DomBuilderItem */
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {Object} PictureSizeEntry
|
|
7
|
+
* @property {number} [from] - `min-width` breakpoint (px) the size applies from; omitted in the (single) mobile-first default entry.
|
|
8
|
+
* @property {number | [number, number]} size - Rendered width (px, height computed with `ratio`) or explicit `[width, height]` pair (px).
|
|
9
|
+
*/
|
|
10
|
+
|
|
5
11
|
// TODO aggiungere compatibilità con immagini Unsplash basate su imgix
|
|
6
12
|
|
|
7
13
|
/**
|
|
@@ -9,9 +15,9 @@ import { classnames } from './classnames.js';
|
|
|
9
15
|
*
|
|
10
16
|
* Every `source`/`img` `srcset` is built with width descriptors (`w`): for each entry
|
|
11
17
|
* of `sizes`, a candidate is generated for every integer density from 1 to `dpr`.
|
|
12
|
-
* The `sizes` attribute
|
|
13
|
-
*
|
|
14
|
-
*
|
|
18
|
+
* The `sizes` attribute is built mobile-first: the entry without `from` is the
|
|
19
|
+
* default, and every other entry applies from its `from` breakpoint up, as a
|
|
20
|
+
* `(min-width: …)` media condition.
|
|
15
21
|
*
|
|
16
22
|
* @example
|
|
17
23
|
* // fixed-width thumb (250px, 16:9 ratio, 1x and 2x candidates)
|
|
@@ -19,33 +25,30 @@ import { classnames } from './classnames.js';
|
|
|
19
25
|
* // sizes="250px", srcset="… 250w, … 500w"
|
|
20
26
|
*
|
|
21
27
|
* @example
|
|
22
|
-
* // responsive image: 250px
|
|
23
|
-
* // NB:
|
|
24
|
-
* //
|
|
28
|
+
* // responsive image: 250px by default, 500px from 576px, 800px from 992px.
|
|
29
|
+
* // NB: the entry without `from` is the mobile-first default (exactly one is
|
|
30
|
+
* // expected); a bare number or [width, height] pair is a shorthand for it
|
|
25
31
|
* buildPictureTag({
|
|
26
32
|
* baseSrc,
|
|
27
|
-
* sizes: [250, 500, 800],
|
|
28
|
-
* breakpoints: [576, 992],
|
|
33
|
+
* sizes: [250, { from: 576, size: 500 }, { from: 992, size: 800 }],
|
|
29
34
|
* });
|
|
30
|
-
* // sizes="(
|
|
35
|
+
* // sizes="(min-width: 992px) 800px, (min-width: 576px) 500px, 250px"
|
|
31
36
|
*
|
|
32
37
|
* @example
|
|
33
|
-
* // custom ratio for numeric
|
|
38
|
+
* // custom ratio for numeric sizes and candidates up to 3x
|
|
34
39
|
* buildPictureTag({
|
|
35
40
|
* baseSrc,
|
|
36
|
-
* sizes: [250, 500],
|
|
37
|
-
* breakpoints: [768],
|
|
41
|
+
* sizes: [250, { from: 768, size: 500 }],
|
|
38
42
|
* ratio: 4/3,
|
|
39
43
|
* dpr: 3,
|
|
40
44
|
* });
|
|
41
45
|
*
|
|
42
46
|
* @example
|
|
43
47
|
* // different aspect ratio per breakpoint: explicit [width, height] pairs
|
|
44
|
-
* // (can be mixed with numeric
|
|
48
|
+
* // (can be mixed with numeric sizes, which use `ratio`)
|
|
45
49
|
* buildPictureTag({
|
|
46
50
|
* baseSrc,
|
|
47
|
-
* sizes: [[250, 250], [800, 450]], // square on mobile, 16:9 on desktop
|
|
48
|
-
* breakpoints: [576],
|
|
51
|
+
* sizes: [[250, 250], { from: 576, size: [800, 450] }], // square on mobile, 16:9 on desktop
|
|
49
52
|
* });
|
|
50
53
|
*
|
|
51
54
|
* @example
|
|
@@ -54,10 +57,9 @@ import { classnames } from './classnames.js';
|
|
|
54
57
|
*
|
|
55
58
|
* @param {Object} args
|
|
56
59
|
* @param {string} args.baseSrc - Image base URL (viewer endpoint); its query params are preserved.
|
|
57
|
-
* @param {(number | [number, number])[]} [args.sizes=[250]] - Rendered image sizes, in
|
|
58
|
-
* @param {number} [args.ratio=16/9] - Default aspect ratio (width / height, as in the CSS `aspect-ratio` property) used to compute the height of numeric
|
|
60
|
+
* @param {(number | [number, number] | PictureSizeEntry)[]} [args.sizes=[250]] - Rendered image sizes, in any order. Each entry is a `{from, size}` object (see {@link PictureSizeEntry}); a bare number or `[width, height]` pair is a shorthand for the mobile-first default entry (the one without `from`), expected exactly once.
|
|
61
|
+
* @param {number} [args.ratio=16/9] - Default aspect ratio (width / height, as in the CSS `aspect-ratio` property) used to compute the height of numeric sizes.
|
|
59
62
|
* @param {number} [args.dpr=2] - Maximum pixel density (integer): `srcset` candidates are generated for every density from 1 to `dpr`.
|
|
60
|
-
* @param {number[]} [args.breakpoints=[]] - `max-width` breakpoints (px) paired with `sizes` (one element less than `sizes`).
|
|
61
63
|
* @param {[string, string|number][]} [args.img_params=[['q','60'],['fd',1]]] - Query params (`[name, value]` pairs) added to every generated URL.
|
|
62
64
|
* @param {string[]} [args.formats=['avif','webp','pjpg']] - Image formats: the last one is used for the `img` fallback element, the others for the `source` elements.
|
|
63
65
|
* @param {boolean} [args.devMode=false] - When true, `avif` is excluded from `formats` (not supported by the local dev environment).
|
|
@@ -75,7 +77,6 @@ export function buildPictureTag({
|
|
|
75
77
|
sizes = [250],
|
|
76
78
|
ratio = 16/9,
|
|
77
79
|
dpr = 2,
|
|
78
|
-
breakpoints = [],
|
|
79
80
|
img_params = [ ['q', '60'] ],
|
|
80
81
|
formats = ['avif', 'webp', 'pjpg'],
|
|
81
82
|
devMode = false,
|
|
@@ -100,46 +101,53 @@ export function buildPictureTag({
|
|
|
100
101
|
formats = formats.filter(fmt => fmt !== 'avif');
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
if(sizes.length > breakpoints.length + 1) {
|
|
104
|
-
// eslint-disable-next-line no-console
|
|
105
|
-
console.warn('[buildPictureTag] `sizes` should have one entry more than `breakpoints`: extra entries are ignored in the `sizes` attribute');
|
|
106
|
-
}
|
|
107
|
-
|
|
108
104
|
// densità generate: da 1 a `dpr`
|
|
109
105
|
const dprList = Array.from({length: dpr}, (_, i) => i + 1);
|
|
110
106
|
|
|
107
|
+
// entry di `sizes` normalizzate in oggetti {from, width, height}
|
|
108
|
+
// (per le size numeriche l'altezza è calcolata con `ratio`),
|
|
109
|
+
// ordinate per breakpoint, con il default (from = null) per primo
|
|
110
|
+
const sizeEntries = sizes.map(entry => {
|
|
111
|
+
if(typeof entry === 'object' && !Array.isArray(entry)) {
|
|
112
|
+
const [width, height] = Array.isArray(entry.size)? entry.size : [entry.size, Math.floor(entry.size / ratio)];
|
|
113
|
+
return { from: entry.from ?? null, width, height };
|
|
114
|
+
}
|
|
115
|
+
const [width, height] = Array.isArray(entry)? entry : [entry, Math.floor(entry / ratio)];
|
|
116
|
+
return { from: null, width, height };
|
|
117
|
+
}).sort((a, b) => (a.from ?? 0) - (b.from ?? 0));
|
|
118
|
+
|
|
119
|
+
// entry default (mobile-first, senza `from`): deve essere esattamente una
|
|
120
|
+
const defaultEntry = sizeEntries.find(e => e.from == null) ?? sizeEntries[0]
|
|
121
|
+
,brkEntries = sizeEntries.filter(e => e.from != null && e !== defaultEntry);
|
|
122
|
+
|
|
123
|
+
if(sizeEntries.filter(e => e.from == null).length !== 1) {
|
|
124
|
+
// eslint-disable-next-line no-console
|
|
125
|
+
console.warn('[buildPictureTag] `sizes` must contain exactly one entry without `from` (the mobile-first default)');
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
const base_url = new URL(baseSrc)
|
|
112
129
|
,searchParams = new URLSearchParams(base_url.search)
|
|
113
130
|
|
|
114
|
-
//
|
|
115
|
-
// per le entry numeriche l'altezza è calcolata con `ratio`
|
|
116
|
-
,sizePairs = sizes.map(size => Array.isArray(size)
|
|
117
|
-
? size
|
|
118
|
-
: /** @type {[number, number]} */ ([size, Math.floor(size / ratio)])
|
|
119
|
-
)
|
|
120
|
-
|
|
121
|
-
// dimensioni dei candidati srcset (una per ogni densità in `dpr`),
|
|
131
|
+
// dimensioni dei candidati srcset (una per ogni densità da 1 a `dpr`),
|
|
122
132
|
// senza duplicati (per larghezza), in ordine crescente
|
|
123
|
-
,srcsetSizes =
|
|
124
|
-
.flatMap((
|
|
133
|
+
,srcsetSizes = sizeEntries
|
|
134
|
+
.flatMap(({width, height}) => dprList.map(d => /** @type {[number, number]} */ ([width * d, height * d])))
|
|
125
135
|
.filter((pair, idx, arr) => arr.findIndex(p => p[0] === pair[0]) === idx)
|
|
126
136
|
.sort((a, b) => a[0] - b[0])
|
|
127
137
|
|
|
128
|
-
// attributo `sizes
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
.
|
|
133
|
-
.
|
|
138
|
+
// attributo `sizes` (mobile-first): le entry con breakpoint in ordine decrescente
|
|
139
|
+
// (vince la prima condizione che matcha), il default per ultimo
|
|
140
|
+
,sizesAttr = brkEntries
|
|
141
|
+
.map(({from, width}) => `(min-width: ${from}px) ${width}px`)
|
|
142
|
+
.reverse()
|
|
143
|
+
.concat(`${defaultEntry.width}px`)
|
|
134
144
|
.join(', ')
|
|
135
145
|
|
|
136
|
-
// dimensioni intrinseche dell'elemento img: la size più grande
|
|
137
|
-
|
|
138
|
-
,[imgWidth, imgHeight] = sizePairs.reduce((max, pair) => pair[0] > max[0]? pair : max)
|
|
146
|
+
// dimensioni intrinseche dell'elemento img: la size più grande
|
|
147
|
+
,{width: imgWidth, height: imgHeight} = sizeEntries.reduce((max, entry) => entry.width > max.width? entry : max)
|
|
139
148
|
|
|
140
|
-
// larghezza 1x della size mediana (mediana inferiore se le size sono pari)
|
|
141
|
-
|
|
142
|
-
,medianWidth = sizePairs.map(([w]) => w).sort((a, b) => a - b)[Math.floor((sizePairs.length - 1) / 2)]
|
|
149
|
+
// larghezza 1x della size mediana (mediana inferiore se le size sono pari)
|
|
150
|
+
,medianWidth = sizeEntries.map(({width}) => width).sort((a, b) => a - b)[Math.floor((sizeEntries.length - 1) / 2)]
|
|
143
151
|
|
|
144
152
|
// indice del candidato srcset usato come src di default dell'img
|
|
145
153
|
,defaultSrcIdx = srcsetSizes.findIndex(([w]) => w === medianWidth)
|