@massimo-cassandro/minimo 1.13.0 → 1.14.0

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 CHANGED
@@ -1,6 +1,7 @@
1
1
 
2
2
  // utilities
3
3
  export { betterText } from './src/utilities/better-text.js';
4
+ export { buildPictureTag } from './src/utilities/build-picture-tag.js';
4
5
  export { classnames, classnamesNull } from './src/utilities/classnames.js';
5
6
  export { createId } from './src/utilities/create-id.js';
6
7
  export { disableBtnsOnSubmit } from './src/utilities/disable-btns-on-submit.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -0,0 +1 @@
1
+ with bootstrap css
@@ -0,0 +1,404 @@
1
+ /*
2
+ Unsplash application id: 990728
3
+ Access Key: -qKEqroUmWhsDlw2e3SQI8ffeNPRWALiddQHTPtLOGo
4
+ Secret key: ROS_RInmZTID7KDKTGU3hHWOba2e1NEqPD0daMRuc_8
5
+
6
+ search_api: 'https://api.unsplash.com/search/photos'
7
+
8
+ API docs: https://unsplash.com/documentation#search-photos
9
+ */
10
+
11
+ import { mAlert, bsSpinner } from '@src/index.js';
12
+ import Modal from '@src/ada-bs-modal/bs-modal.js';
13
+ import { bsModalElementBuilder } from '@src/ada-bs-modal/bs-modal-element-builder.js';
14
+ import { classnames, randomId, domBuilder } from '@massimo-cassandro/minimo';
15
+
16
+ import { parseUnsplashResult, unsplashPictureTag, setImgBtnText, setRequestUrl, unsplashParams } from './unsplash-utilities.js';
17
+ import { unsplashImgsDisplayInit, unsplashImgsDisplay } from './unsplash-imgs-display.js';
18
+
19
+ import * as styles from './unsplash-get-imgs.module.css';
20
+
21
+ const selectBtnClass = randomId(); // classe assegnata ia pulsanti seleziona/rimuovi immagine
22
+
23
+ let page = 1,
24
+ selectedImgs = [],
25
+ availableImgsCount = 0,
26
+ availableImgsCountEl,
27
+ searchString = null,
28
+ searchField,
29
+ searchBtn,
30
+ resultContainer,
31
+ resultInfoWrapper, resultInfoTextWrapper, resultInfoBtnPrev, resultInfoBtnNext;
32
+
33
+
34
+ // inizializzazione markup e rendering imgs preregistrate
35
+ unsplashImgsDisplayInit();
36
+ unsplashImgsDisplay();
37
+
38
+
39
+ // =>> helper func abilitazione/disabilitazione pulsanti aggiungi immagine altre immagini in base a `availableImgsCount`
40
+ function updAddImgBtns() {
41
+ resultContainer.querySelectorAll(`.${styles.imgWrapper}:not(.${styles.selected}) .${selectBtnClass}`)
42
+ .forEach(btn => {
43
+ btn.disabled = availableImgsCount === 0;
44
+ });
45
+ }
46
+
47
+
48
+ // =>> listener btn seleziona img di ogni singola card
49
+
50
+ document.body.addEventListener('click', e => {
51
+ if(e.target.classList.contains(selectBtnClass)) {
52
+ const imgWrapper = e.target.closest(`.${styles.imgWrapper}`);
53
+ let isSelected = imgWrapper.classList.contains(styles.selected);
54
+
55
+ // immagine precedentemente selzionata, rimozione
56
+ if(isSelected) {
57
+ selectedImgs = selectedImgs.filter(img => img.id !== imgWrapper.img_data.id);
58
+ isSelected = false;
59
+ imgWrapper.classList.remove(styles.selected);
60
+
61
+ }
62
+
63
+ // non selected, aggiunta immagine
64
+ else if (availableImgsCount > 0) {
65
+ selectedImgs.push(imgWrapper.img_data);
66
+ isSelected = true;
67
+ imgWrapper.classList.add(styles.selected);
68
+
69
+ }
70
+
71
+
72
+ availableImgsCount = unsplashParams.maxImgs - selectedImgs.length;
73
+ availableImgsCountEl.textContent = availableImgsCount;
74
+ e.target.textContent = setImgBtnText(isSelected);
75
+
76
+ // abilitazione/disabilitazione pulsanti aggiungi immagine di altre immagini in base a `availableImgsCount`
77
+ updAddImgBtns();
78
+ }
79
+ });
80
+
81
+ // Esc con popover di ingrandimento aperto: chiude solo il popover, non l'intera modal
82
+ // (il listener Escape della modal Bootstrap è in bubble phase sull'elemento della modal:
83
+ // intercettando qui in capture phase e fermando la propagazione, l'evento non lo raggiunge mai,
84
+ // mentre la chiusura nativa del popover via Popover API avviene comunque)
85
+ document.addEventListener('keydown', e => {
86
+ if(e.key === 'Escape' && document.querySelector(`.${styles.zoomPopover}:popover-open`)) {
87
+ e.stopPropagation();
88
+ }
89
+ }, true);
90
+
91
+ // *********************************
92
+
93
+ async function loadUnsplashJson() {
94
+
95
+ const url = setRequestUrl(searchString, page);
96
+
97
+ searchField.disabled = true;
98
+ searchBtn.disabled = true;
99
+ bsSpinner({
100
+ addWrapper: true,
101
+ wrapperExtraClass: 'mt-5',
102
+ parent: resultContainer,
103
+ overlay: true
104
+ });
105
+
106
+ try {
107
+ const response = await fetch(url),
108
+ data = await response.json();
109
+
110
+ return data;
111
+
112
+ } catch(err) {
113
+ /* eslint-disable no-console */
114
+ console.error(url);
115
+ console.error(err);
116
+ /* eslint-enable no-console */
117
+
118
+ } finally {
119
+ searchField.disabled = false;
120
+ searchBtn.disabled = false;
121
+ }
122
+ }
123
+
124
+
125
+ // imposta alcune classi del popup dei risultati in base alla presenza o meno di risultati
126
+ function toggleResultEnv(hasResult = false) {
127
+
128
+ resultInfoWrapper.classList.toggle(styles.hidden, !hasResult);
129
+ resultContainer.classList.toggle(styles.resultGrid, hasResult);
130
+
131
+ }
132
+
133
+
134
+ // =>> updateResult
135
+ async function updateResult(updPage = 0) {
136
+
137
+ // aggiorna var page, e si assicura che il valore non sia mai inferiore a 1
138
+ page = Math.max(1, page + updPage);
139
+
140
+ // disabilita tutti i pulsanti che richoamano questa funzione ad evitare chiamate multiple
141
+ resultInfoBtnPrev.disabled = true;
142
+ resultInfoBtnNext.disabled = true;
143
+ searchBtn.disabled = true;
144
+
145
+
146
+ // load data
147
+ const data = await loadUnsplashJson();
148
+
149
+ searchBtn.disabled = false;
150
+
151
+ /* data:
152
+ {
153
+ "total": 0, <- record trovati
154
+ "total_pages": 0, <- pagine
155
+ "results": []
156
+ }
157
+ */
158
+
159
+ if(data.total > 0) {
160
+ toggleResultEnv(true);
161
+ resultInfoTextWrapper.textContent = `Immagini trovate: ${data.total.toLocaleString('it-IT', {useGrouping: 'always'})}` +
162
+ ` - pagina ${page} di ${data.total_pages.toLocaleString('it-IT', {useGrouping: 'always'})}`;
163
+ resultInfoBtnPrev.disabled = (page??1) === 1;
164
+ resultInfoBtnNext.disabled = (page??1) === data.total_pages;
165
+
166
+ // =>> domBuilder immagini
167
+ domBuilder(data.results.map(row => {
168
+
169
+ const img_data = parseUnsplashResult(row)
170
+ ,isSelected = selectedImgs.some(img => img.id === img_data.id)
171
+ ;
172
+
173
+ return {
174
+ tag: 'figure',
175
+ className: classnames(styles.imgWrapper, isSelected && styles.selected),
176
+ callback: el => {
177
+ el.img_data = img_data;
178
+
179
+ },
180
+ children: [
181
+ ...unsplashPictureTag(img_data, {
182
+ pictureExtraClass: styles.thumb,
183
+ }),
184
+ {
185
+ tag: 'figcaption',
186
+ className: styles.imgInfo,
187
+ children: [
188
+ {
189
+ content: img_data.description
190
+ },
191
+ img_data.author_tag,
192
+
193
+ // =>> select btn
194
+ {
195
+ className: 'pt-2 mt-auto align-self-center',
196
+ children: [
197
+ {
198
+ tag: 'button',
199
+ className: classnames('btn btn-sm btn-outline-secondary', selectBtnClass),
200
+ content: setImgBtnText(isSelected),
201
+ attrs: {
202
+ type: 'button'
203
+ },
204
+ }
205
+ ]
206
+
207
+ }
208
+ ]
209
+ }
210
+ ]
211
+ };
212
+ }), resultContainer, { emptyParent: true });
213
+
214
+
215
+ // abilitazione/disabilitazione pulsante aggiungi immagine
216
+ updAddImgBtns();
217
+
218
+ } else {
219
+ toggleResultEnv(false);
220
+ resultContainer.innerHTML = '<span class="text-danger">Nessuna immagine trovata</span>';
221
+ }
222
+ }
223
+
224
+
225
+ // =>> handler btn aggiungi imgs principale
226
+ export function unsplashGetImgs(){
227
+
228
+ // lettura immagini preregistrate, registrazione in `selectedImgs` e calcolo `availableImgsCount`
229
+ selectedImgs = JSON.parse(unsplashParams.hiddenField.value || '[]');
230
+ availableImgsCount = unsplashParams.maxImgs - selectedImgs.length;
231
+
232
+ try {
233
+
234
+ if(availableImgsCount === 0) {
235
+ throw {
236
+ mes: {
237
+ type: 'warning',
238
+ title: 'Hai già raggiunto il numero massimo di immagini',
239
+ text: 'Per aggiungerne altre devi prima rimuoverne almeno una di quelle presenti'
240
+ }
241
+ };
242
+ }
243
+
244
+ const searchFiledId = randomId();
245
+
246
+ // =>> main dialog
247
+ const modalElement = bsModalElementBuilder({
248
+ modalClassName: styles.uspDialog,
249
+ modalTitle: '<strong>Seleziona immagini</strong>',
250
+ modalBodyChildren: [
251
+ 'p.my-0 Inserisci un termine di ricera e seleziona una o più immagini e premi “Aggiungi Immagini”. Premi “Completato” per terminare. ' +
252
+ 'Per un buon risultato, seleziona immagini con il soggetto al centro della foto (puoi ingrandirla con un clic sulla miniatura). ' +
253
+ `Ricorda che <strong>puoi associare alla tua offerta un massimo di ${unsplashParams.maxImgs} immagini</strong>.`,
254
+
255
+ // =>> search wrapper
256
+ {
257
+ className: styles.searchWrapper,
258
+ children: [
259
+ `label.form-label.my-0.text-nowrap[for: ${searchFiledId}] Termine di ricerca`,
260
+ {
261
+ tag: 'input',
262
+ id: searchFiledId,
263
+ className: 'form-control',
264
+ attrs: {
265
+ type: 'search',
266
+ placeholder: 'Es. Sicilia, Granada...',
267
+ value: process.env.NODE_ENV === 'development'? 'Sicilia' : null
268
+ },
269
+ callback: el => searchField = el,
270
+ },
271
+ {
272
+ tag: 'button',
273
+ content: 'Cerca',
274
+ attrs: {
275
+ class: 'btn btn-outline-secondary',
276
+ type: 'Search'
277
+ },
278
+ callback: el => {
279
+ searchBtn = el;
280
+ el.addEventListener('click', async () => {
281
+ resultContainer.innerHTML = '';
282
+ toggleResultEnv(false);
283
+
284
+ searchString = searchField.value.trim();
285
+
286
+ if(!searchString) {
287
+ resultContainer.innerHTML = '<span class="text-danger">Devi inserire un termine di ricerca</span>';
288
+ return;
289
+ }
290
+
291
+ updateResult();
292
+ });
293
+ }
294
+ },
295
+
296
+ // =>> img disponibili
297
+ 'div.ml-auto Immagini disponibili:',
298
+ {
299
+ tag: 'span',
300
+ className: styles.avlCounter,
301
+ content: availableImgsCount,
302
+ callback: el => availableImgsCountEl = el
303
+ }
304
+ ]
305
+ },
306
+ {
307
+ className: styles.result,
308
+ callback: el => resultContainer = el
309
+ }
310
+ ],
311
+ // =>> dialog footer
312
+ footerChildren: [
313
+ {
314
+ className: classnames(styles.resultInfoWrapper, styles.hidden),
315
+ callback: el => resultInfoWrapper = el,
316
+ children: [
317
+ {
318
+ tag: 'button',
319
+ content: '&larr; Prec.',
320
+ attrs: {
321
+ type: 'button',
322
+ class: 'btn btn-sm btn-outline-secondary',
323
+ disabled: true
324
+ },
325
+ callback: el => {
326
+ resultInfoBtnPrev = el;
327
+
328
+ el.addEventListener('click', () => {
329
+ updateResult(-1);
330
+ });
331
+ }
332
+ },
333
+ {
334
+ className: styles.resultInfoText,
335
+ callback: el => resultInfoTextWrapper = el
336
+ },
337
+ {
338
+ tag: 'button',
339
+ content: 'Succ. &rarr;',
340
+ attrs: {
341
+ type: 'button',
342
+ class: 'btn btn-sm btn-outline-secondary',
343
+ disabled: true
344
+ },
345
+ callback: el => {
346
+ resultInfoBtnNext = el;
347
+
348
+ el.addEventListener('click', () => {
349
+ updateResult(1);
350
+ });
351
+ }
352
+ }
353
+ ]
354
+ },
355
+ {
356
+ tag: 'button',
357
+ content: 'Aggiungi Immagini',
358
+ attrs: {
359
+ type: 'button',
360
+ class: 'btn btn-secondary'
361
+ },
362
+ callback: el => {
363
+ el.addEventListener('click', () => {
364
+ unsplashImgsDisplay(selectedImgs);
365
+ unsplashParams.hiddenField.value = JSON.stringify(selectedImgs);
366
+ unsplashParams.displayContainer?.scrollIntoView(true);
367
+ thisModal.hide();
368
+ });
369
+ }
370
+ }
371
+ ],
372
+ // callback: null
373
+ });
374
+
375
+ const thisModal = new Modal(modalElement, {focus: false});
376
+ thisModal.show();
377
+
378
+ // modalElement.addEventListener('shown.bs.modal', () => {});
379
+ modalElement.addEventListener('hidden.bs.modal', () => {
380
+ thisModal.dispose();
381
+ modalElement.remove();
382
+ });
383
+
384
+ } catch(e) {
385
+
386
+ if(e.mes) {
387
+ if(!e.mes.title && e.mes.text) {
388
+ e.mes.title = e.mes.text;
389
+ e.mes.text = null;
390
+ }
391
+ mAlert({
392
+ type : e.mes.type?? 'error',
393
+ title : e.mes.title,
394
+ mes : e.mes.text?? null,
395
+ });
396
+ }
397
+
398
+ if(e.err) {
399
+ // eslint-disable-next-line no-console
400
+ console.error('[unsplashGetImgs]', e.err);
401
+ }
402
+ }
403
+
404
+ }
@@ -0,0 +1,178 @@
1
+ .uspDialog {
2
+ --ada-modal-width: 95dvw;
3
+ /* stylelint-disable-next-line selector-class-pattern */
4
+ :global(.modal-content){
5
+ height: 90dvh;
6
+ max-height: 95dvh;
7
+ }
8
+
9
+ /* stylelint-disable-next-line selector-class-pattern */
10
+ :global(.modal-body) {
11
+ display: flex;
12
+ flex-flow: column nowrap;
13
+ gap: 1rem;
14
+ /* align-items: stretch; */
15
+ justify-content: flex-start;
16
+ min-height: 0;
17
+ overflow: hidden;
18
+ }
19
+
20
+ /* stylelint-disable-next-line selector-class-pattern */
21
+ :global(.modal-footer) {
22
+ justify-content: flex-start;
23
+ }
24
+ }
25
+
26
+ .searchWrapper {
27
+ display: flex;
28
+ gap: 1.4rem;
29
+ align-items: center;
30
+ justify-content: flex-start;
31
+ /* max-width: fit-content; */
32
+ padding-block: .5rem;
33
+ border-block: 1px solid var(--ada-grigio-3);
34
+
35
+ /* stylelint-disable-next-line selector-class-pattern */
36
+ :global(.form-control) {
37
+ flex: 0 1 500px;
38
+ }
39
+ }
40
+
41
+ /* contatore immagini disponibili */
42
+ .avlCounter {
43
+ padding-inline-end: .5rem;
44
+ font-size: var(--ada-font-size-lg);
45
+ font-weight: var(--ada-font-weight-extrabold);
46
+ }
47
+
48
+ .result {
49
+ position: relative;
50
+ flex: 1;
51
+
52
+ &:not(.resultGrid) {
53
+ text-align: center;
54
+ }
55
+ }
56
+
57
+ .resultGrid {
58
+ display: grid;
59
+ grid-template-rows: auto auto;
60
+ grid-template-columns: repeat(6, 1fr);
61
+ /* grid-auto-rows: 1fr; */ /* al posto di grid-template-rows */
62
+ /* grid-template: row / col; */
63
+ grid-auto-flow: row;
64
+ gap: 1.2rem;
65
+ /* align-items: center; */
66
+ /* justify-items: start; */
67
+ min-height: 0;
68
+ padding: .3rem;
69
+
70
+ overflow-y: auto;
71
+ scrollbar-gutter: stable;
72
+ }
73
+
74
+
75
+ /* contenitore di ogni singola immagine (figure) */
76
+ .imgWrapper {
77
+ position: relative;
78
+ z-index: 1;
79
+ display: grid;
80
+ grid-template-rows: subgrid;
81
+ grid-row: span 2;
82
+ gap: 0;
83
+ margin: 0;
84
+
85
+ border: 1px solid var(--ada-grigio-2);
86
+ box-shadow: var(--ada-box-shadow);
87
+
88
+ &, &::before {
89
+ border-radius: var(--ada-border-radius);
90
+ }
91
+
92
+ &::before {
93
+ position: absolute;
94
+ inset: -6px;
95
+ z-index: -1;
96
+ display: block;
97
+ content: '';
98
+ border: 6px solid transparent;
99
+ }
100
+
101
+ &.selected {
102
+ &::before {
103
+ border-color: var(--ada-verde);
104
+ }
105
+ }
106
+ }
107
+ /* contenitore immagine */
108
+ /* .imgInnerWrapper {} */
109
+
110
+ .thumb { /* picture */
111
+
112
+ /* &, img {} */
113
+ img {
114
+ display: block;
115
+ width: 100%;
116
+ height: 180px;
117
+ padding: .3rem;
118
+ margin-inline: auto;
119
+ object-fit: cover;
120
+ object-position: center;
121
+ border-radius: var(--ada-border-radius);
122
+ }
123
+ }
124
+ .hasPopover { /* img */
125
+ cursor: zoom-in;
126
+ }
127
+
128
+ /* popover di ingrandimento immagine (Popover API + Anchor Positioning API) */
129
+ .zoomPopover {
130
+ inset: auto;
131
+ width: max-content;
132
+ height: max-content;
133
+ padding: 1rem;
134
+ margin: 0;
135
+ position-area: center left; /* block-end; */
136
+ position-try-fallbacks: center right;
137
+ background-color: #fff;
138
+ border: 1px solid var(--ada-grigio-3);
139
+ border-radius: var(--ada-border-radius);
140
+ box-shadow: var(--ada-box-shadow-lg);
141
+ }
142
+ /* classe associata alle immagini nei popover */
143
+ .popoverImg {
144
+ max-width: none;
145
+ }
146
+
147
+ /* figcaption */
148
+ .imgInfo {
149
+ display: flex;
150
+ flex-flow: column nowrap;
151
+ gap: .2rem;
152
+ /* align-content: flex-start; */ /* multiline container */
153
+ align-items: flex-start;
154
+ justify-content: flex-start;
155
+ padding: .5rem;
156
+ font-size: var(--ada-font-size-small);
157
+ }
158
+
159
+
160
+ /* nel footer */
161
+ .resultInfoWrapper {
162
+ display: flex;
163
+ flex: 1;
164
+ flex-flow: row nowrap;
165
+ gap: 1rem;
166
+ align-items: center;
167
+ justify-content: flex-start;
168
+
169
+ padding-inline-end: 1.5rem;
170
+
171
+ &.hidden {
172
+ visibility: hidden;
173
+ }
174
+ }
175
+ .resultInfoText {
176
+ font-size: var(--ada-font-size-medium-sm);
177
+ color: var(--ada-text-muted);
178
+ }