@massimo-cassandro/minimo 0.13.1 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/massimo-cassandro/minimo#readme",
@@ -0,0 +1,11 @@
1
+ # Autocomplete
2
+
3
+ Implementation of [@tarekraafat/autocomplete.js](https://tarekraafat.github.io/autoComplete.js/#/) to manage key-value items
4
+
5
+ Requires package `@tarekraafat/autocomplete.js`:
6
+
7
+ ```bash
8
+ npm i -S @tarekraafat/autocomplete.js
9
+ ```
10
+
11
+ Includes the `checkAutocomplete` function: checks that autocomplete fields are filled in correctly.
@@ -0,0 +1,4 @@
1
+ da rivedere
2
+
3
+ definire uso css
4
+ unificare tokens
@@ -0,0 +1,360 @@
1
+ import autoComplete from '@tarekraafat/autocomplete.js';
2
+ // import { escapeHTML } from '@massimo-cassandro/js-utilities';
3
+
4
+ // https://tarekraafat.github.io/autoComplete.js/#/configuration
5
+
6
+ export const ac_default_params = {
7
+ placeholder: 'Inserisci tre o più caratteri',
8
+ ac_url: null, // url di ricerca, deve avere slash iniziali e finali
9
+
10
+ // se true, non viene concatenata la stringa di ricerca all'url (utile se si usa un json)
11
+ test_mode: false,
12
+
13
+ /*
14
+ funzione che riceve il risultato del fetch ajax e restituisce un array
15
+ di oggetti nella forma
16
+ {
17
+ id <== id dell'elemento
18
+ val <== valore da visualizzare come risultato della selezione
19
+ list_display <== stringa visualizzata nell'elenco
20
+ }
21
+
22
+ esempio:
23
+ data => data.map(item => {
24
+ return {
25
+ id: item.id,
26
+ val: `#${item.id} ${item.agenzia} (${item.network})`,
27
+ list_display: `#${item.id} ${item.agenzia} (${item.network})`+
28
+ (item.ragioneSociale? `<br><small>${item.ragioneSociale}</small>` : '—')
29
+
30
+ // opzionale:
31
+ __xxx__: item (o altri dati custom)
32
+ };
33
+ });
34
+ */
35
+ fetch_result_function: data => data,
36
+
37
+ // elemento o funzione che restituisce l'elemento
38
+ autocomplete_field: null,
39
+
40
+ // selettore dell'elemento contenitore di autocomplete_field e degli altri elementi
41
+ autocomplete_parent_selector: '.form-group',
42
+
43
+ // oggetto opzione di parametri da accodare all'url di ricerca in modalità get
44
+ // i valori delle chiavi possono essere singole stringhe o numeri oppure array
45
+ // es. {param1: 'val1', param2: ['val2'. 'val3']}
46
+ extra_query_params: {},
47
+
48
+ // name e id dell'elemento hidden su cui registrare l'id selezionato
49
+ // il `name` non viene utilizzato se l'elemento è già presente
50
+ // se nessuno tra hidden_id, hidden_field e select_id è presente, l'id del valore selezionato non viene gestito
51
+ hidden_name: null,
52
+ hidden_id: null,
53
+
54
+ // elemento hidden; se impostato e se presenti, `hidden_name` e `hidden_id` non vengono presi in considerazione
55
+ hidden_field: null,
56
+
57
+ // se presente utilizza l'elemento select (già esistente) indicato
58
+ // se impostato, `hidden_name` e `hidden_id` vengono ignorati
59
+ // l'elemento select è considerato di tipo `multiple` se `select_multiple == true`
60
+ // nel caso di select multiple non è possibile inserire due voci con lo stesso id
61
+ select_id: null,
62
+ select_multiple: true,
63
+
64
+ // id dell'elemento in cui generare i badge delle opzioni selezionate
65
+ // Solo se `select_id` è impostato, per visualizzazione ed editing delle voci scelte.
66
+ // Se non presente, viene ignorato ma è necessario predisporre autonomanente
67
+ // la procedura di editing
68
+ // NB: solo per select multiple
69
+ badges_container_id: null,
70
+
71
+ // callback invocato quando un badge viene rimosso
72
+ // viene invocato con argomenti l'id e la voce (il testo del badge) dell'elemento rimosso
73
+ badges_remove_callback: null,
74
+
75
+ // funzione personalizzata per la costruzione dei badge
76
+ // viene invocata con argomenti `event.detail.selection.value`, e l'oggetto `params`.
77
+ // Il primo corrisponde all'oggetto ritornato da `fetch_result_function`
78
+ // se `null`, viene utilizzato il markup di default
79
+ // NB: al momento, nel caso di elementi preregistrati, il primo argomento di badges_builder
80
+ // contiene solo gli elementi `id`, `val`
81
+ // NB: è necessario che il badge abbia classe `ac-badge` e attributo `data-id`.
82
+ // La funzione deve restituire il markup completo del badge
83
+ badges_builder: null,
84
+
85
+ // classe assegnata allo span che contiene il testo del badge
86
+ badge_label_class: 'ac-badge-label',
87
+
88
+ // classe assegnata al pulsante di rimozione del badge
89
+ badge_btn_class: 'ac-badge-btn',
90
+
91
+
92
+ // callback autocomplete
93
+ // se presente viene invocato con 5 argomenti: id, val, autocomplete field element, list_display (outerHTML) e row
94
+ // NB: row viene istanziato solo dopo la selezione di un'opzione
95
+ callback: null,
96
+
97
+ // classe aggiuntiva opzionale per la lista dei risultati
98
+ resultList_extra_class: null,
99
+
100
+ // classe aggiuntiva opzionale per il wrapper esterno
101
+ wrapper_extra_class: null
102
+ };
103
+
104
+ export default function (params = {}) {
105
+
106
+ try {
107
+
108
+ params = {...ac_default_params, ...params};
109
+
110
+ params.badges_builder ??= (result_obj, bparams = params) => `<span class="ac-badge badge rounded-pill text-bg-secondary" data-id="${result_obj.id}">` +
111
+ `<span class="${bparams.badge_label_class}">${result_obj.val}</span>` +
112
+ `<button type="button" class="${bparams.badge_btn_class}">&times;</button>` +
113
+ '</span>';
114
+
115
+
116
+ if(params.autocomplete_field && params.ac_url) {
117
+
118
+ if(typeof params.autocomplete_field === 'function') {
119
+ params.autocomplete_field = params.autocomplete_field();
120
+ }
121
+
122
+ // impostazione attributi autocomplete
123
+ params.autocomplete_field.type = 'search';
124
+ ['spellcheck=false', 'autocorrect=off', 'autocomplete=off', 'autocapitalize=off'].forEach(item => {
125
+ const [attr, val] = item.split('=');
126
+ params.autocomplete_field.setAttribute(attr, val);
127
+ });
128
+
129
+ params.autocomplete_field.dataset.sel = params.autocomplete_field.value;
130
+
131
+
132
+ params.autocomplete_field.closest(params.autocomplete_parent_selector).classList.add('ac-autocomplete-wrapper');
133
+ if(params.wrapper_extra_class) {
134
+ params.autocomplete_field.closest(params.autocomplete_parent_selector).classList.add(params.wrapper_extra_class);
135
+ }
136
+
137
+ let extra_query_params = [];
138
+ for(const i in params.extra_query_params) {
139
+ if(Array.isArray(params.extra_query_params[i])) {
140
+ params.extra_query_params[i].forEach(item => {
141
+ extra_query_params.push(`${i}[]=${item}`);
142
+ });
143
+ } else {
144
+ extra_query_params.push(`${i}=${params.extra_query_params[i]}`);
145
+ }
146
+ }
147
+ const extra_query_params_string = extra_query_params.length? `?${extra_query_params.join('&')}` : '';
148
+
149
+ let hidden_field = null, select_field = null, badges_container = null;
150
+
151
+ if(params.select_id) {
152
+ select_field = document.getElementById(params.select_id);
153
+ if(!select_field) {
154
+ throw `Elemento '${params.select_id}' non presente`;
155
+ }
156
+ if(params.badges_container_id) {
157
+ badges_container = document.getElementById(params.badges_container_id);
158
+ }
159
+
160
+ } else {
161
+ // campo hidden
162
+ // se non presente, viene generato
163
+ if(params.hidden_field) {
164
+ hidden_field = params.hidden_field;
165
+
166
+ } else if(params.hidden_id) {
167
+ hidden_field = document.getElementById(params.hidden_id);
168
+ if(!hidden_field) {
169
+ params.autocomplete_field.insertAdjacentHTML('afterend',
170
+ `<input type="hidden" id="${params.hidden_id}" name="${params.hidden_name}" value="">`
171
+ );
172
+ hidden_field = document.getElementById(params.hidden_id);
173
+ }
174
+ }
175
+ }
176
+
177
+ const autoCompleteJS = new autoComplete({
178
+ selector: '#' + params.autocomplete_field.id,
179
+ placeHolder: params.placeholder,
180
+ diacritics: true,
181
+ threshold: 3,
182
+ data: {
183
+ src: async (query) => {
184
+ const ac_url = params.ac_url +
185
+ (params.test_mode? '' : encodeURIComponent(query)) + extra_query_params_string;
186
+
187
+
188
+ try {
189
+ // Fetch Data from external Source
190
+ const source = await fetch(ac_url);
191
+ // Data is array of `Objects` | `Strings`
192
+ const data = await source.json();
193
+
194
+ return params.fetch_result_function(data);
195
+
196
+ } catch (error) {
197
+ return error;
198
+ }
199
+ },
200
+ // Data 'Object' key to be searched
201
+ keys:['list_display'],
202
+ cache: false
203
+ },
204
+ resultsList: {
205
+ class: params.resultList_extra_class,
206
+
207
+ destination: '#' + params.autocomplete_field.id,
208
+ element: (list, data) => {
209
+ if (!data.results.length) {
210
+ // Create "No Results" message element
211
+ const message = document.createElement('div');
212
+ // Add class to the created element
213
+ message.setAttribute('class', 'no-result');
214
+ // Add message text content
215
+ message.innerHTML = `<span>Nessun risultato per <strong>"${data.query}"</strong></span>`;
216
+ // Append message element to the results list
217
+ list.prepend(message);
218
+ }
219
+ },
220
+ noResults: true,
221
+ },
222
+ resultItem: {
223
+ highlight: true,
224
+ },
225
+ events: {
226
+
227
+ input: {
228
+ selection: (event) => {
229
+ // console.log(event.detail.selection.value);
230
+
231
+ const selected_id = event.detail.selection.value.id,
232
+ selected_text = event.detail.selection.value.val;
233
+
234
+ autoCompleteJS.input.value = selected_text;
235
+
236
+ if(select_field) {
237
+ const option_element = new Option(selected_text, selected_id, true, true);
238
+
239
+ if(params.select_multiple) {
240
+ // impedisce i doppioni
241
+ const registered_option = select_field.querySelector(`option[value="${selected_id}"]`),
242
+ new_badge = params.badges_builder(event.detail.selection.value, params);
243
+
244
+ if(!registered_option) {
245
+
246
+ select_field.appendChild(option_element);
247
+ params.autocomplete_field.value = '';
248
+
249
+ if(badges_container) {
250
+ badges_container.insertAdjacentHTML('beforeend', new_badge);
251
+ }
252
+
253
+ // replace del badge per avere sempre la versione più recente
254
+ } else {
255
+ registered_option.replaceWith(option_element);
256
+ if(badges_container) {
257
+ const prev_badge = badges_container.querySelector(`.ac-badge[data-id="${selected_id}"]`);
258
+ prev_badge.replaceWith(
259
+ new DOMParser().parseFromString(new_badge, 'text/html').body.childNodes[0]
260
+ );
261
+ }
262
+ }
263
+
264
+ } else { // select singolo
265
+ select_field.innerHTML = '';
266
+ select_field.appendChild(option_element);
267
+ }
268
+
269
+ } else if (hidden_field) {
270
+ hidden_field.value = selected_id;
271
+ }
272
+
273
+ autoCompleteJS.input.dataset.sel = selected_text;
274
+
275
+ if(params.callback && typeof params.callback === 'function') {
276
+ params.callback(selected_id, selected_text, autoCompleteJS.input, event.detail.selection.value.list_display, event.detail.selection.value.row);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }); // end autoComplete
282
+
283
+ // TODO[epic=autocomplete]
284
+ // migliorare, rendere più efficiente la chiamata del callback
285
+
286
+ // reset hidden
287
+ const check_ac = () => {
288
+ if(params.autocomplete_field.value === '' ||
289
+ (params.autocomplete_field.dataset.sel !== undefined &&
290
+ params.autocomplete_field.value !== params.autocomplete_field.dataset.sel)
291
+ ) {
292
+ if(hidden_field) {
293
+ hidden_field.value = '';
294
+ }
295
+ if(select_field && !params.select_multiple) {
296
+ select_field.innerHTML = '';
297
+ }
298
+ if(params.callback && typeof params.callback === 'function') {
299
+ params.callback('', '', params.autocomplete_field);
300
+ }
301
+ }
302
+ };
303
+
304
+ params.autocomplete_field?.addEventListener('search', () => {
305
+ check_ac();
306
+ }, false);
307
+ params.autocomplete_field?.addEventListener('blur', () => {
308
+ check_ac();
309
+ }, false);
310
+ params.autocomplete_field?.addEventListener('change', () => {
311
+ check_ac();
312
+ }, false);
313
+ params.autocomplete_field?.addEventListener('keydown', () => {
314
+ check_ac();
315
+ }, false);
316
+
317
+ // listener su badges
318
+ badges_container?.addEventListener('click', e => {
319
+ const btn = e.target.closest(`.${params.badge_btn_class}`);
320
+
321
+ if(btn) {
322
+ const badge = btn.closest('.ac-badge'),
323
+ item_id = badge.dataset.id,
324
+ item_text = badge.querySelector(':scope > span').innerText;
325
+ badge.remove();
326
+ select_field.querySelector(`option[value="${item_id}"]`).remove();
327
+
328
+ if(params.badges_remove_callback && typeof params.badges_remove_callback === 'function') {
329
+
330
+ params.badges_remove_callback(item_id, item_text);
331
+ }
332
+ }
333
+ }, false);
334
+
335
+ // aggiunta voci o badges di eventuali valori preregistrati
336
+ if(select_field) {
337
+
338
+ if(params.select_multiple && badges_container) {
339
+ // eventuali attributi data dell'option vengono aggiunti
340
+ // alla funzione `badges_builder`, con chiave `dataset`
341
+
342
+ select_field.querySelectorAll('option[selected]').forEach(option => {
343
+ badges_container.insertAdjacentHTML('beforeend',
344
+
345
+ params.badges_builder({id: option.value, val: option.innerHTML, dataset: {...option.dataset}}, params)
346
+ );
347
+ });
348
+ } else {
349
+ params.autocomplete_field.value = select_field.querySelector('option')?.innerText?? '';
350
+ }
351
+
352
+ }
353
+
354
+
355
+ } // end if params.autocomplete_field...
356
+
357
+ } catch(e) {
358
+ console.error( e ); // eslint-disable-line
359
+ }
360
+ }
@@ -0,0 +1,146 @@
1
+ /* basato sul css di @tarekraafat/autocomplete.js */
2
+
3
+
4
+ .hmenu-subsection-emulazione {
5
+ overflow-y: visible;
6
+ }
7
+
8
+ .ac-autocomplete-wrapper { /* assegnato all'elemento .form-group */
9
+
10
+ --ac-color: #333;
11
+ --ac-bg-color: #fff;
12
+ --ac-border-color: #666;
13
+ --ac-danger-color: #c00;
14
+ --ac-box-shadow: 0 0 10px rgb(0 0 0 / .6);
15
+ --ac-list-item-bg: #fff;
16
+ --ac-hover-color: var(--secondary-100);
17
+
18
+ position: relative;
19
+
20
+ ul {
21
+ position: absolute;
22
+ top: 100%;
23
+ right: 0;
24
+ left: 0;
25
+ z-index: 1000;
26
+ max-height: 14rem;
27
+ padding: 0;
28
+ margin: .2rem 0 0;
29
+ overflow-y: scroll;
30
+ color: var(--ac-color);
31
+ outline: none;
32
+ background-color: var(--ac-bg-color);
33
+ border: 1px solid var(--ac-border-color);
34
+ border-radius: .6rem;
35
+ box-shadow: var(--ac-box-shadow);
36
+
37
+ &[hidden],
38
+ &:empty {
39
+ display: block;
40
+ opacity: 0;
41
+ transform: scale(0);
42
+ }
43
+
44
+ li {
45
+ padding: 0;
46
+ margin: .3rem;
47
+ text-align: left;
48
+ list-style: none;
49
+
50
+ /* stylelint-disable declaration-no-important, max-nesting-depth, selector-max-compound-selectors */
51
+ > div {
52
+
53
+ padding: .3rem .5rem;
54
+ background-color: var(--ac-list-item-bg);
55
+ border-radius: .35rem;
56
+ transition: all .2s ease;
57
+
58
+ .badge {
59
+ color: #fff;
60
+ background-color: var(--ac-hover-color);
61
+ }
62
+
63
+ &:hover {
64
+ cursor: pointer;
65
+ background-color: var(--ac-hover-color) !important;
66
+ &, .text-muted {
67
+ color: #fff !important;
68
+ }
69
+ .badge {
70
+ color: var(--ac-hover-color) !important;
71
+ background-color: color-mix(in srgb, #fff 70%, transparent) !important;
72
+ }
73
+
74
+ mark {
75
+ border-color: currentColor;
76
+ }
77
+ }
78
+
79
+ &.alert-item{
80
+
81
+
82
+ &, .text-muted {
83
+ color: var(--text-muted) !important;
84
+ }
85
+ background-color: color-mix(in srgb, var(--ac-danger-color) 1%, transparent);
86
+ &:hover {
87
+ &, .text-muted {
88
+ color: rgb(255 255 255) !important;
89
+ }
90
+ }
91
+ }
92
+
93
+ mark {
94
+ padding: 0;
95
+ color: currentColor;
96
+ background-color: transparent;
97
+ border-bottom: 4px double var(--ac-danger-color);
98
+
99
+ /* stylelint-disable-next-line max-nesting-depth selector-max-type */
100
+ /* &::selection {
101
+ color: rgba(#ffffff, 0);
102
+ background-color: rgba(#ffffff, 0);
103
+ } */
104
+ }
105
+ }
106
+ /* stylelint-enable declaration-no-important, max-nesting-depth, selector-max-compound-selectors */
107
+
108
+ /* &[aria-selected='true'] {
109
+ background-color: var(--ada-grigio-2);
110
+ } */
111
+ }
112
+ }
113
+
114
+ .no-result {
115
+ padding: .4rem .6rem;
116
+ color: var(--ac-danger-color);
117
+ }
118
+ }
119
+
120
+
121
+ /* badge multiple select */
122
+ .ac-badge {
123
+ margin: 0 .5rem .2rem 0;
124
+ font-size: .85rem;
125
+ line-height: 1;
126
+ white-space: nowrap;
127
+
128
+ &:has(.ac-badge-btn:hover) {
129
+ /* stylelint-disable-next-line declaration-no-important */
130
+ background-color: var(--ac-danger-color) !important;
131
+ }
132
+ }
133
+ .ac-badge-btn {
134
+ display: inline-flex;
135
+ align-items: center;
136
+ justify-content: center;
137
+ padding: 0;
138
+ margin-left: 4px;
139
+ font-size: 1.2em;
140
+ line-height: 1;
141
+ color: currentColor;
142
+ background-color: transparent;
143
+ border: none;
144
+ border-radius: 50%;
145
+
146
+ }
@@ -0,0 +1,9 @@
1
+ // TODO trigger ac selection?
2
+ export function checkAutocomplete(autocomplete_field, hidden_field) {
3
+ if(autocomplete_field && hidden_field) {
4
+ if(autocomplete_field.value === '' || autocomplete_field.value.toLowerCase() !== autocomplete_field.dataset.sel.toLowerCase()) {
5
+ autocomplete_field.value = '';
6
+ hidden_field.value = '';
7
+ }
8
+ }
9
+ }
@@ -1,5 +1,5 @@
1
1
  import imageIcon from '../../icons/image-duotone.svg';
2
- import arrowIcon from '../icons/arrow-fat-lines-left-duotone.svg';
2
+ import arrowIcon from '../../icons/arrow-fat-lines-left-duotone.svg';
3
3
  import { breakpoints } from './img-breakpoints.js';
4
4
  import { fetchUnsplashData } from './fetch-unsplash-data.js';
5
5
  import { decode } from 'blurhash';