@massimo-cassandro/minimo 1.13.0 → 1.14.1
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/index.js +1 -0
- package/package.json +1 -1
- package/snippets/unsplash-browser-app/README.md +1 -0
- package/snippets/unsplash-browser-app/unsplash-get-imgs.js +404 -0
- package/snippets/unsplash-browser-app/unsplash-get-imgs.module.css +178 -0
- package/snippets/unsplash-browser-app/unsplash-imgs-display.js +195 -0
- package/snippets/unsplash-browser-app/unsplash-imgs-display.module.css +119 -0
- package/snippets/unsplash-browser-app/unsplash-utilities.js +344 -0
- package/src/components/modal-popup/modal-popup.js +3 -0
- package/src/utilities/build-picture-tag.js +230 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { classnames } from './classnames.js';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./dom-builder/dom-builder.js').DomBuilderItem} DomBuilderItem */
|
|
4
|
+
|
|
5
|
+
// TODO aggiungere compatibilità con immagini Unsplash basate su imgix
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a `picture` domBuilder item for an image rendered through the viewer.
|
|
9
|
+
*
|
|
10
|
+
* Every `source`/`img` `srcset` is built with width descriptors (`w`): for each entry
|
|
11
|
+
* of `sizes`, a candidate is generated for every integer density from 1 to `dpr`.
|
|
12
|
+
* The `sizes` attribute pairs each entry of `sizes` with the corresponding `breakpoints`
|
|
13
|
+
* entry as a `(max-width: …)` media condition; the last entry (without breakpoint) is
|
|
14
|
+
* the default, so `breakpoints` is expected to have one element less than `sizes`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // fixed-width thumb (250px, 16:9 ratio, 1x and 2x candidates)
|
|
18
|
+
* buildPictureTag({ baseSrc: 'https://img-viewer.example.com/abc123', alt: '…' });
|
|
19
|
+
* // sizes="250px", srcset="… 250w, … 500w"
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // responsive image: 250px up to 576px, 500px up to 992px, 800px beyond.
|
|
23
|
+
* // NB: `breakpoints` has one entry less than `sizes`: the missing one
|
|
24
|
+
* // corresponds to the *last* `sizes` entry, used as default beyond the last breakpoint
|
|
25
|
+
* buildPictureTag({
|
|
26
|
+
* baseSrc,
|
|
27
|
+
* sizes: [250, 500, 800],
|
|
28
|
+
* breakpoints: [576, 992],
|
|
29
|
+
* });
|
|
30
|
+
* // sizes="(max-width: 576px) 250px, (max-width: 992px) 500px, 800px"
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // custom ratio for numeric entries and candidates up to 3x
|
|
34
|
+
* buildPictureTag({
|
|
35
|
+
* baseSrc,
|
|
36
|
+
* sizes: [250, 500],
|
|
37
|
+
* breakpoints: [768],
|
|
38
|
+
* ratio: 4/3,
|
|
39
|
+
* dpr: 3,
|
|
40
|
+
* });
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // different aspect ratio per breakpoint: explicit [width, height] pairs
|
|
44
|
+
* // (can be mixed with numeric entries, which use `ratio`)
|
|
45
|
+
* buildPictureTag({
|
|
46
|
+
* baseSrc,
|
|
47
|
+
* sizes: [[250, 250], [800, 450]], // square on mobile, 16:9 on desktop
|
|
48
|
+
* breakpoints: [576],
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* // dev environment (no avif) and above-the-fold img (no loading="lazy")
|
|
53
|
+
* buildPictureTag({ baseSrc, devMode: true, lazy: false });
|
|
54
|
+
*
|
|
55
|
+
* @param {Object} args
|
|
56
|
+
* @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 ascending width order, one per breakpoint plus the default. Each entry is a width (px, height computed with `ratio`) or an explicit `[width, height]` pair (px), allowing a different aspect ratio per breakpoint.
|
|
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 `sizes` entries.
|
|
59
|
+
* @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
|
+
* @param {[string, string|number][]} [args.img_params=[['q','60'],['fd',1]]] - Query params (`[name, value]` pairs) added to every generated URL.
|
|
62
|
+
* @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
|
+
* @param {boolean} [args.devMode=false] - When true, `avif` is excluded from `formats` (not supported by the local dev environment).
|
|
64
|
+
* @param {boolean} [args.lazy=true] - When true, the `img` element gets `loading="lazy"`.
|
|
65
|
+
* @param {boolean} [args.condition=true] - When false, the function returns `null` without building anything.
|
|
66
|
+
* @param {string|null} [args.pictureExtraClass=null] - Extra class(es) for the `picture` element.
|
|
67
|
+
* @param {Object<string, *>} [args.pictureExtraAttrs={}] - Extra attributes for the `picture` element.
|
|
68
|
+
* @param {string|null} [args.imgExtraClass=null] - Extra class(es) for the `img` element.
|
|
69
|
+
* @param {Object<string, *>} [args.imgExtraAttrs={}] - Extra attributes for the `img` element.
|
|
70
|
+
* @param {string} [args.alt=''] - `alt` text for the `img` element.
|
|
71
|
+
* @returns {DomBuilderItem|null} The `picture` domBuilder item, or `null` when `baseSrc` is missing or `condition` is false.
|
|
72
|
+
*/
|
|
73
|
+
export function buildPictureTag({
|
|
74
|
+
baseSrc,
|
|
75
|
+
sizes = [250],
|
|
76
|
+
ratio = 16/9,
|
|
77
|
+
dpr = 2,
|
|
78
|
+
breakpoints = [],
|
|
79
|
+
img_params = [ ['q', '60'] ],
|
|
80
|
+
formats = ['avif', 'webp', 'pjpg'],
|
|
81
|
+
devMode = false,
|
|
82
|
+
lazy = true,
|
|
83
|
+
condition = true,
|
|
84
|
+
pictureExtraClass = null,
|
|
85
|
+
pictureExtraAttrs = {},
|
|
86
|
+
imgExtraClass = null,
|
|
87
|
+
imgExtraAttrs = {},
|
|
88
|
+
// addPopover: false, // TODO popover, se attivato
|
|
89
|
+
// popoverImgWidth: 600,
|
|
90
|
+
alt = '',
|
|
91
|
+
|
|
92
|
+
}){
|
|
93
|
+
|
|
94
|
+
if(baseSrc == null || !condition) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// avif non supportato dall'ambiente di sviluppo locale
|
|
99
|
+
if(devMode) {
|
|
100
|
+
formats = formats.filter(fmt => fmt !== 'avif');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// densità generate: da 1 a `dpr`
|
|
104
|
+
const dprList = Array.from({length: dpr}, (_, i) => i + 1);
|
|
105
|
+
|
|
106
|
+
const base_url = new URL(baseSrc)
|
|
107
|
+
,searchParams = new URLSearchParams(base_url.search)
|
|
108
|
+
|
|
109
|
+
// sizes normalizzato in coppie [width, height]:
|
|
110
|
+
// per le entry numeriche l'altezza è calcolata con `ratio`
|
|
111
|
+
,sizePairs = sizes.map(size => Array.isArray(size)
|
|
112
|
+
? size
|
|
113
|
+
: /** @type {[number, number]} */ ([size, Math.floor(size / ratio)])
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
// dimensioni dei candidati srcset (una per ogni densità in `dpr`),
|
|
117
|
+
// senza duplicati (per larghezza), in ordine crescente
|
|
118
|
+
,srcsetSizes = sizePairs
|
|
119
|
+
.flatMap(([w, h]) => dprList.map(d => /** @type {[number, number]} */ ([w * d, h * d])))
|
|
120
|
+
.filter((pair, idx, arr) => arr.findIndex(p => p[0] === pair[0]) === idx)
|
|
121
|
+
.sort((a, b) => a[0] - b[0])
|
|
122
|
+
|
|
123
|
+
// attributo `sizes`: ogni size è abbinata al breakpoint corrispondente (max-width),
|
|
124
|
+
// l'ultima (senza breakpoint) è il valore di default
|
|
125
|
+
,sizesAttr = sizePairs
|
|
126
|
+
.map(([w], i) => i < breakpoints.length? `(max-width: ${breakpoints[i]}px) ${w}px` : `${w}px`)
|
|
127
|
+
.join(', ')
|
|
128
|
+
|
|
129
|
+
// dimensioni intrinseche dell'elemento img (la size più grande)
|
|
130
|
+
,[imgWidth, imgHeight] = sizePairs[sizePairs.length - 1]
|
|
131
|
+
|
|
132
|
+
// indice del candidato srcset usato come src di default dell'img:
|
|
133
|
+
// il candidato 1x della size mediana (mediana inferiore se le size sono pari)
|
|
134
|
+
,defaultSrcIdx = srcsetSizes.findIndex(([w]) => w === sizePairs[Math.floor((sizePairs.length - 1) / 2)][0])
|
|
135
|
+
;
|
|
136
|
+
|
|
137
|
+
img_params.forEach(([name, value]) => searchParams.set(name, String(value)));
|
|
138
|
+
|
|
139
|
+
// TODO popover, se attivato
|
|
140
|
+
// // nome univoco per l'associazione anchor (Anchor Positioning API) tra thumb e popover di ingrandimento
|
|
141
|
+
// const zoomAnchorName = `--zoom-${randomId()}`;
|
|
142
|
+
|
|
143
|
+
// // riferimento al popover di ingrandimento, valorizzato dalla sua callback domBuilder
|
|
144
|
+
// // (usato nel listener click della thumb, eseguito solo dopo che l'intera figure è stata costruita)
|
|
145
|
+
// let zoomPopoverEl;
|
|
146
|
+
|
|
147
|
+
/** @type {DomBuilderItem} */
|
|
148
|
+
const pictureTag = {
|
|
149
|
+
tag: 'picture',
|
|
150
|
+
className: pictureExtraClass ?? undefined,
|
|
151
|
+
attrs: {
|
|
152
|
+
// style: options.addPopover? `anchor-name: ${zoomAnchorName};` : null, // TODO popover, se attivato
|
|
153
|
+
...pictureExtraAttrs
|
|
154
|
+
},
|
|
155
|
+
children: formats.map(fmt => {
|
|
156
|
+
const is_default_fmt = fmt === formats.at(-1);
|
|
157
|
+
/** @type {string[]} */
|
|
158
|
+
const srcsetArray = [];
|
|
159
|
+
|
|
160
|
+
searchParams.set('f', fmt);
|
|
161
|
+
|
|
162
|
+
srcsetSizes.forEach(([w, h]) => {
|
|
163
|
+
searchParams.set('bb', `${w}x${h}`);
|
|
164
|
+
base_url.search = searchParams.toString();
|
|
165
|
+
srcsetArray.push(`${base_url.toString()} ${w}w`);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if(is_default_fmt) {
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
tag: 'img',
|
|
172
|
+
className: classnames(imgExtraClass/* , options.addPopover && styles.hasPopover */), // TODO popover, se attivato
|
|
173
|
+
attrs: {
|
|
174
|
+
alt: alt,
|
|
175
|
+
src: srcsetArray[defaultSrcIdx].split(' ')[0], // candidato 1x mediano, senza descrittore
|
|
176
|
+
srcset: srcsetArray.join(','),
|
|
177
|
+
sizes: sizesAttr,
|
|
178
|
+
width: imgWidth,
|
|
179
|
+
height: imgHeight,
|
|
180
|
+
loading: lazy? 'lazy' : null,
|
|
181
|
+
...imgExtraAttrs
|
|
182
|
+
},
|
|
183
|
+
// TODO classe popover, se attivato
|
|
184
|
+
// callback: options.addPopover
|
|
185
|
+
// ? el => el.addEventListener('click', () => zoomPopoverEl.togglePopover())
|
|
186
|
+
// : null
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
} else {
|
|
190
|
+
// TODO con sizes con ratio differenti, generare una `source` per breakpoint
|
|
191
|
+
// con attributi `media`, `width` e `height`
|
|
192
|
+
return {
|
|
193
|
+
tag: 'source',
|
|
194
|
+
attrs: {
|
|
195
|
+
srcset: srcsetArray.join(','),
|
|
196
|
+
sizes: sizesAttr,
|
|
197
|
+
type: `image/${fmt}`,
|
|
198
|
+
// width:
|
|
199
|
+
// height:
|
|
200
|
+
// media:
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
})
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
/* TODO popover, se attivato
|
|
209
|
+
// popover di ingrandimento (Popover API + Anchor Positioning API)
|
|
210
|
+
...(options.addPopover
|
|
211
|
+
? [{
|
|
212
|
+
tag: 'div',
|
|
213
|
+
className: styles.zoomPopover,
|
|
214
|
+
attrs: {
|
|
215
|
+
popover: 'auto',
|
|
216
|
+
style: `position-anchor: ${zoomAnchorName};`
|
|
217
|
+
},
|
|
218
|
+
callback: el => zoomPopoverEl = el,
|
|
219
|
+
children: unsplashPictureTag(img_data, {width: options.popoverImgWidth, imgExtraClass: styles.popoverImg, addPopover: false})
|
|
220
|
+
}]
|
|
221
|
+
: []
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return [ pictureTag, popoverTag ];
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
return pictureTag;
|
|
229
|
+
|
|
230
|
+
}
|