@massimo-cassandro/minimo 1.6.0 → 1.7.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.
Files changed (37) hide show
  1. package/index.js +10 -3
  2. package/package.json +4 -2
  3. package/src/components/autocomplete/autocomplete-engine.js +70 -64
  4. package/src/components/autocomplete/check-autocomplete.js +10 -2
  5. package/src/components/dismiss-alerts.js +9 -2
  6. package/src/components/form-reset-submit.js +12 -4
  7. package/src/components/inner-nav/inner-nav.js +5 -1
  8. package/src/components/modal-alert/customization-templates/custom-modal-alert.js +4 -0
  9. package/src/components/modal-alert/defaults.js +6 -3
  10. package/src/components/modal-alert/modal-alert.js +62 -21
  11. package/src/components/modal-popup/modal-popup.js +51 -29
  12. package/src/components/overlay/overlay.js +13 -1
  13. package/src/components/sf-macro/sf-macro.js +23 -12
  14. package/src/components/snackbar/snackbar.js +32 -17
  15. package/src/components/spinner/spinner.js +6 -2
  16. package/src/components/unsplash-page/fetch-unsplash-data.js +25 -2
  17. package/src/components/unsplash-page/img-breakpoints.js +11 -0
  18. package/src/components/unsplash-page/unsplash-page.js +64 -40
  19. package/src/css/forms/forms.css +4 -0
  20. package/src/utilities/better-text.js +80 -0
  21. package/src/utilities/classnames.js +14 -1
  22. package/src/utilities/cookies.js +28 -0
  23. package/src/utilities/create-id.js +19 -0
  24. package/src/utilities/dom-builder/dom-builder.js +84 -75
  25. package/src/utilities/dom-builder/domBuilderBasicSetup.js +12 -9
  26. package/src/utilities/dom-builder/parseDomString.js +31 -29
  27. package/src/utilities/enable-submit-btns.js +7 -1
  28. package/src/utilities/{escapeHTML.js → escape-html.js} +13 -0
  29. package/src/utilities/linked-dates.js +64 -0
  30. package/src/utilities/relative-date.js +153 -0
  31. package/src/utilities/sentence-case.js +5 -2
  32. package/src/utilities/strip-tags.js +8 -0
  33. package/src/utilities/title-case.js +6 -2
  34. package/src/utilities/truncate-string.js +22 -0
  35. package/src/utilities/validation-error-class.js +14 -7
  36. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -0
  37. package/src/web-components/s-datatable-component/src/parse-cols.js +1 -0
@@ -0,0 +1,80 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Improves a text string by fixing punctuation spacing, multiple spaces,
5
+ * typographic quotes, non-breaking spaces after Italian particles/articles,
6
+ * and optionally enforcing the exact casing of custom words.
7
+ *
8
+ * @param {string} str - input string to process
9
+ * @param {string[]} custom_words - list of words whose exact casing must be preserved (e.g. `['iPhone', 'macOS']`)
10
+ * @returns {string} processed string, or empty string if input is falsy
11
+ */
12
+ export function betterText(str, custom_words = []) {
13
+
14
+ const particelle = [
15
+ 'e', 'ed',
16
+ 'di', 'a', 'da', 'in', 'per', 'con', 'su', 'per', 'tra', 'fra',
17
+ 'il', 'lo', 'la', 'i', 'gli', 'le',
18
+ 'un', 'uno', 'una',
19
+ 'del', 'dello', 'della', 'dei', 'degli', 'delle',
20
+ 'al', 'allo', 'alla', 'ai', 'agli', 'alle',
21
+ 'dal', 'dallo', 'dalla', 'dai', 'dagli', 'dalle',
22
+ 'nel', 'nello', 'nella', 'nei', 'negli', 'nelle',
23
+ 'col', 'collo', 'colla', 'con', 'coi', 'cogli', 'colle',
24
+ 'sul', 'sullo', 'sulla', 'sui', 'sugli', 'sulle',
25
+
26
+ // topographic terms
27
+ 'C.so', 'Corso', 'Via', 'P.za', 'Piazza', 'L.go', 'Largo', 'V.le', 'Viale',
28
+ 'De', 'San', 'c/o', 'Loc.',
29
+
30
+ // English
31
+ 'the', 'a', 'an', 'at', 'out', 'of',
32
+
33
+ // words used in geonames
34
+ 'isola', 'isole'
35
+ ];
36
+
37
+ if(str) {
38
+ str = str.trim();
39
+
40
+ // punctuation: remove space before, ensure single space after
41
+ // NOTE: must run before any HTML entity replacements to avoid conflicts with `;`
42
+ // (?!$): negative lookahead to skip punctuation at end of string
43
+ str = str.replace(/ +(,|;|\.|:|!|\?)/g, '$1');
44
+ str = str.replace(/(,|;|\.|:|!|\?)(?!$) +/g, '$1 ');
45
+
46
+ // collapse multiple spaces and tabs (including non-breaking spaces)
47
+ str = str.replace(/[ \t\u00A0]+/g, ' ');
48
+
49
+ // typographic quotes
50
+ str = str.replace(/(^| )"/g, '$1“') // opening double quotes
51
+ .replace(/"/g, '”') // remaining double quotes → closing
52
+ .replace(/(^| )'/g, '$1‘') // opening single quotes
53
+ .replace(/'/g, '’') // remaining single quotes and apostrophes
54
+ ;
55
+
56
+ // remove space after apostrophe preceded by elided articles (l', un', d', all', ...)
57
+ str = str.replace(/((^| )(l|un|d|all|dell|nell|sull)('|')) /gi, '$1' );
58
+
59
+ // replace regular spaces with non-breaking spaces after Italian particles/articles
60
+ var re = new RegExp('\\b(' + particelle.join('|') + ')\\b +', 'gmi');
61
+ str = str.replace(re, function (match) {
62
+ return match.replace(/ +/g, '\u00A0');
63
+ });
64
+
65
+ // enforce exact casing of each particle
66
+ particelle.forEach(term => {
67
+ const re = new RegExp('\\b(' + term + ')\\b', 'gmi');
68
+ str = str.replace(re, term);
69
+ });
70
+
71
+ if(custom_words.length) {
72
+ custom_words.forEach(item => {
73
+ str = str.replace(new RegExp(`\\b${item}\\b`, 'gi'), item);
74
+ });
75
+ }
76
+ } else {
77
+ str = '';
78
+ }
79
+ return str;
80
+ }
@@ -1,14 +1,27 @@
1
+ /** @typedef {string | null | undefined | false | 0} ClassValue */
2
+
3
+ /**
4
+ * Joins CSS class names, filtering out falsy and non-string values.
5
+ * Accepts strings, falsy values, and (nested) arrays of the same.
6
+ * @param {...(ClassValue | ClassValue[])} args
7
+ * @returns {string}
8
+ */
1
9
  export function classnames(...args) {
2
10
  return args.flat().filter(x => {
3
11
  if (x && typeof x !== 'string') {
4
12
  // eslint-disable-next-line no-console
5
- console.error('[classnames] valore non stringa ignorato in ', ...args);
13
+ console.error('[classnames] non-string value ignored: ', ...args);
6
14
  return false;
7
15
  }
8
16
  return Boolean(x);
9
17
  }).join(' ');
10
18
  }
11
19
 
20
+ /**
21
+ * Like {@link classnames}, but returns `null` when the result is an empty string.
22
+ * @param {...(ClassValue | ClassValue[])} args
23
+ * @returns {string | null}
24
+ */
12
25
  export function classnamesNull(...args) {
13
26
  return classnames(...args) || null;
14
27
  }
@@ -0,0 +1,28 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * Returns the value of a cookie by name.
5
+ * @param {string} name
6
+ * @returns {string | undefined}
7
+ */
8
+ export function getCookie(name) {
9
+ const value = `; ${document.cookie}`;
10
+ const parts = value.split(`; ${name}=`);
11
+ if (parts.length === 2) return parts.pop()?.split(';').shift();
12
+ }
13
+
14
+
15
+ /**
16
+ * Sets a cookie.
17
+ * @param {object} params
18
+ * @param {string} params.name
19
+ * @param {string} params.value
20
+ * @param {string|null} [params.path=null]
21
+ * @param {number|null} [params.expire=null] - Max-Age in seconds
22
+ */
23
+ export function setCookie({name, value, path = null, expire = null}) {
24
+ document.cookie = `${name}=${value}` +
25
+ (path ? `; path=${path}` : '') +
26
+ '; SameSite=Lax' +
27
+ (expire ? `; Max-Age=${expire}` : '');
28
+ }
@@ -0,0 +1,19 @@
1
+ import { stripTags } from './strip-tags.js';
2
+
3
+ /**
4
+ * Creates an id from a string (useful for generating automatic anchors from tag content).
5
+ * Strips HTML tags, normalizes accented characters, punctuation, and whitespace.
6
+ * @param {string | null | undefined} str
7
+ * @returns {string}
8
+ */
9
+ export function createId(str) {
10
+ return (stripTags(str) || '')
11
+ .toLowerCase()
12
+ .normalize('NFD')
13
+ .replace(/[\u0300-\u036f]/g, '')
14
+ .replace(/[\p{P}\p{S}]+/gu, '') // strips all Unicode punctuation and symbols
15
+ .replace(/\s+/g, '-')
16
+ .replace(/^-+|-+$/g, '')
17
+ .replace(/-+/g, '-')
18
+ .replace(/[^a-z0-9-]/g, '');
19
+ }
@@ -1,26 +1,38 @@
1
1
  import { domBuilderBasicSetup } from './domBuilderBasicSetup.js';
2
2
  import { parseDomString } from './parseDomString.js';
3
3
 
4
- // TODO sintassi stringa su più righe in cui ogni riga corrisponde ad un elemento
5
- // TODO sintassi stringa come il precedente, ma con possibilità di indicare una nidificazione (indent?)
4
+ // TODO multi-line string syntax where each line maps to an element
5
+ // TODO same as above with optional nesting via indentation
6
+
7
+ /**
8
+ * @typedef {Object} DomBuilderItem
9
+ * @property {string | string[]} [tag='div'] - HTML tag name or array of nested tags (each is parent of the next). Also `tagName`.
10
+ * @property {string | string[]} [tagName='div'] - Alias for `tag`.
11
+ * @property {string | string[]} [className] - CSS class(es): a single string or an array (falsy values filtered out).
12
+ * @property {string | string[]} [class] - Alias for `className`.
13
+ * @property {string | null} [id] - Unique element ID.
14
+ * @property {[string, *] | [string, *][] | Object<string, *>} [attrs] - Attributes: a `[name, value]` pair, array of pairs, or `{name: value}` object.
15
+ * @property {string | number | Function | Element | DomBuilderItem[] | null} [content] - Element content.
16
+ * @property {boolean} [condition=true] - When false, the element is skipped.
17
+ * @property {function(HTMLElement): void} [callback] - Callback invoked after the element is created.
18
+ * @property {DomBuilderItem[]} [children] - Configuration array for child elements.
19
+ */
6
20
 
7
21
  /**
8
22
  * domBuilder
9
- * Costruisce una struttura DOM a partire da un array di configurazione.
23
+ * Builds a DOM structure from a configuration array.
10
24
  *
11
- * La funzione analizza un array di oggetti di configurazione per creare elementi HTML.
25
+ * Parses an array of configuration objects to create HTML elements.
12
26
  *
13
- * **Formato della Struttura di Configurazione (structureArray):**
14
- *
15
- * L'array `structureArray` può contenere stringhe (con sintassi abbreviata) e/o oggetti di configurazione:
27
+ * **Configuration structure format (structureArray):**
16
28
  *
17
29
  * ```javascript
18
30
  * structure = [
19
31
  * '#mainContainer.container',
20
32
  * 'p#main-info.info.active{data-id:123,role=button} Lorem ipsum',
21
33
  * {
22
- * tag: 'div' | ['.divClass', 'h2#id', 'table.class1', 'thead', 'tr.class2.class3(attr1: attrValue)'], // anche tagName
23
- * className: 'xxx' | ['class1', 'class2'], // anche `class`
34
+ * tag: 'div' | ['.divClass', 'h2#id', 'table.class1', 'thead', 'tr.class2.class3(attr1: attrValue)'], // also tagName
35
+ * className: 'xxx' | ['class1', 'class2'], // also `class`
24
36
  * id: 'element-id',
25
37
  * attrs: [attr_name, attr_value] | [[...], [...]] | {name: value},
26
38
  * content: 'xxx' | 123 | domBuilder Array | function | Element,
@@ -30,45 +42,33 @@ import { parseDomString } from './parseDomString.js';
30
42
  * },
31
43
  * 'div.class[data-xxx: value]',
32
44
  * '.another-div',
33
- * 'p#paragraph''
45
+ * 'p#paragraph',
34
46
  * '...',
35
47
  * { ... }
36
48
  * ]
37
49
  * ```
38
50
  *
39
- * id e classi possono essere specificati sia chiave dell'oggetto di configurazione che all'interno dell'oggetto `attrs`
40
- * In caso di conflitto, prevalgono quelli indicati al livello principale.
51
+ * IDs and classes can be specified either as top-level object keys or inside the `attrs` object.
52
+ * When both are present, top-level properties take precedence.
41
53
  *
42
- * È inoltre possibile utilizzare una versione "abbreviata" utilizzando una stringa costruita secondo la sintassi di `parseDomString`.
43
- * Vedi `parseDomString` per altre in.formazioni.
54
+ * A shorthand string syntax is also available, built according to `parseDomString` conventions.
55
+ * See `parseDomString` for details.
44
56
  *
45
- * Questo tipo di sintassi non prevede la possibilità di definire `content`, `callback`, `condition` e `children`: in questi casi è necessario utilizzare la sintassi di base.
57
+ * The shorthand syntax does not support `content`, `callback`, `condition`, or `children`; use the object syntax for those.
46
58
  *
47
- * La sintassi abbreviata può essere utilizzata anche con l'elemento `tag` della sintassi ad oggetti.
48
- * È possibile definirlo come array in cui ogni elemento corrisponde al genitore del successivo. Eventuali altre proprietà dell'oggetto
49
- * (className, callback, ecc.) vengono applicate solo all'ultimo elemento dell'array.
50
- * In caso di conflitto tra classi, id o attributi indicati nella stringa e quelli indicati nell'oggetto
51
- * prevalgono questi ultimi.
59
+ * The shorthand can also be used for the `tag` property in object syntax.
60
+ * When `tag` is an array, each element becomes the parent of the next one. Any other object properties
61
+ * (className, callback, etc.) are applied only to the last element in the array.
62
+ * When shorthand string and object-level properties conflict (classes, id, or attributes),
63
+ * object-level properties take precedence.
52
64
  *
53
65
  * @function domBuilder
54
- * @param {Array<Object|string>} [structureArray=[]] - Array di configurazione degli elementi. Può contenere stringhe (con sintassi abbreviata secondo `parseDomString`) e/o oggetti di configurazione.
55
- * @param {string|Array<string>} [structureArray[].tag='div'] - Nome del tag HTML (es. 'div', 'p') o array di tag nidificati (es. ['div.container', 'section', 'article']).
56
- * @param {string|Array<string>} [structureArray[].tagName='div'] - Alias di tag
57
- * @param {string|Array<string>} [structureArray[].className] - Classe/i CSS: stringa singola o array di classi, in quest'ultimo caso, i valori *falsy* vengopno rimossi.
58
- * @param {string|Array<string>} [structureArray[].class] - Alias di className.
59
- * @param {string} [structureArray[].id] - ID univoco dell'elemento.
60
- * @param {Array<[string, *]>|Array<Array<[string, *]>>|Object<string, *>} [structureArray[].attrs] - Attributi: coppia `[name, value]`, array di coppie o oggetto `{name: value}`.
61
- * @param {string|number|Function|Element} [structureArray[].content] - Contenuto dell'elemento: stringa, numero, funzione che restituisce contenuto, o HTMLElement.
62
- * @param {boolean} [structureArray[].condition=true] - Se false, l'elemento non viene generato.
63
- * @param {Function} [structureArray[].callback] - Callback invocata quando l'elemento viene creato; riceve l'elemento come argomento.
64
- * @param {Array<Object>} [structureArray[].children] - Array di configurazione per elementi figlio.
65
- * @param {HTMLElement} [parent] - Elemento genitore al quale appendere la struttura.
66
- * @param {Object} [options={}] - Opzioni di configurazione.
67
- * @param {boolean} [options.emptyParent=true] - Se true, l'element parent indicato viene svuotato prima di procedere con ciclo builder
68
- * @returns {HTMLElement|null} Il primo elemento creato, o null se nessun elemento è stato creato.
66
+ * @param {Array<DomBuilderItem|string>} [structureArray=[]] - Configuration array. Accepts strings (shorthand per `parseDomString`) and/or configuration objects.
67
+ * @param {HTMLElement} [parent] - Parent element to append the structure to.
68
+ * @param {Object} [options={}] - Configuration options.
69
+ * @param {boolean} [options.emptyParent=false] - When true, the parent element is emptied before building.
70
+ * @returns {HTMLElement|null} The first created element, or null if nothing was created.
69
71
  */
70
-
71
-
72
72
  export function domBuilder(structureArray = [], parent, options = {}) {
73
73
 
74
74
  options = {
@@ -76,45 +76,55 @@ export function domBuilder(structureArray = [], parent, options = {}) {
76
76
  ...options
77
77
  };
78
78
 
79
- let mainElement = null, el, grand_parent = null;
79
+ /** @type {HTMLElement | null} */
80
+ let mainElement = null;
81
+ /** @type {HTMLElement} */
82
+ let el;
83
+ /** @type {HTMLElement | null} */
84
+ let grand_parent = null;
80
85
 
81
86
  if(parent && options.emptyParent) {
82
87
  parent.innerHTML = '';
83
88
  }
84
89
 
85
- structureArray.forEach(item => {
90
+ structureArray.forEach(inputItem => {
86
91
 
87
- if(item != null && typeof item === 'string' && item !== '') {
88
- item = parseDomString(item);
92
+ /** @type {DomBuilderItem | null} */
93
+ let item;
94
+ if (inputItem != null && typeof inputItem === 'string' && inputItem !== '') {
95
+ item = parseDomString(inputItem);
96
+ } else {
97
+ item = /** @type {DomBuilderItem} */ (inputItem);
89
98
  }
90
99
 
91
100
  // console.log(item);
92
101
 
93
102
  if (item != null && (item.condition ?? true)) {
94
103
 
104
+ const safeItem = item; // const binding so TypeScript tracks non-null type in nested closures
95
105
 
96
- // se tag è un array, vengono creati una serie di elementi nidificati
97
- // l'ultimo è quello a cui vengono applicate le altre proprietà dell'oggetto in esame
98
- if (Array.isArray(item.tag)) {
106
+ // when tag is an array, create a series of nested elements;
107
+ // the last one receives the remaining object properties
108
+ if (Array.isArray(safeItem.tag)) {
99
109
 
100
- grand_parent = parent;
110
+ grand_parent = parent ?? null;
111
+ const tags = /** @type {string[]} */ (safeItem.tag);
101
112
 
102
- item.tag.forEach((tagItem, idx) => {
113
+ tags.forEach((tagItem, idx) => {
103
114
 
104
- const isLast = idx === item.tag.length - 1;
105
- const parsedItem = parseDomString(tagItem);
115
+ const isLast = idx === tags.length - 1;
116
+ const parsedItem = parseDomString(tagItem) ?? { tag: 'div', id: undefined, className: '', attrs: {}, content: undefined };
106
117
 
107
- // nel caso dell'ultimo elemento, merge con le opzioni dell'oggetto
108
- // con precedenza alle proprietà di quest'ultimo
118
+ // for the last element, merge with the object's own options; object-level properties take precedence
109
119
  if(isLast) {
110
- item.attrs = {...parsedItem.attrs??{}, ...item.attrs??{}};
120
+ safeItem.attrs = {...parsedItem.attrs ?? {}, ...safeItem.attrs ?? {}};
111
121
  }
112
122
  el = domBuilderBasicSetup(
113
123
  document.createElement(parsedItem.tag || 'div'),
114
- {...parsedItem, ...(isLast? item??{} : {})}
124
+ {...parsedItem, ...(isLast ? safeItem : {})}
115
125
  );
116
126
 
117
- if (!isLast) { // l'ultimo elemento è gestito dalla procedura "standard"
127
+ if (!isLast) { // the last element is handled by the standard path below
118
128
  if (parent) {
119
129
  parent.appendChild(el);
120
130
  }
@@ -125,40 +135,41 @@ export function domBuilder(structureArray = [], parent, options = {}) {
125
135
 
126
136
  } else {
127
137
  el = domBuilderBasicSetup(
128
- document.createElement(item.tag ?? 'div'),
129
- item
138
+ document.createElement(/** @type {string} */ (safeItem.tag) ?? 'div'),
139
+ safeItem
130
140
  );
131
141
  }
132
142
 
133
143
 
134
- if (item.content != null) {
144
+ if (safeItem.content != null) {
135
145
 
136
- let content = '';
137
- if (typeof item.content === 'function') {
138
- content = item.content();
146
+ /** @type {string | Element | null} */
147
+ let content = null;
148
+ if (typeof safeItem.content === 'function') {
149
+ content = safeItem.content();
139
150
 
140
- } else if (Array.isArray(item.content)) {
141
- content = domBuilder(item.content);
151
+ } else if (Array.isArray(safeItem.content)) {
152
+ content = domBuilder(safeItem.content);
142
153
 
143
- } else if (item.content != null) {
144
- content = String(item.content);
154
+ } else if (safeItem.content != null) {
155
+ content = String(safeItem.content);
145
156
  }
146
157
 
147
158
  if (content instanceof Element) {
148
159
  el.appendChild(content);
149
160
 
150
- } else {
161
+ } else if (content != null) {
151
162
  el.innerHTML = content;
152
163
  }
153
164
  }
154
165
 
155
166
 
156
- if (item.children != null && !Array.isArray(item.children)) {
167
+ if (safeItem.children != null && !Array.isArray(safeItem.children)) {
157
168
  // eslint-disable-next-line no-console
158
- console.error('Error: `item.children` must be an array → ' + item.children);
169
+ console.error('Error: `item.children` must be an array → ' + safeItem.children);
159
170
  }
160
- if (item.children && Array.isArray(item.children)) {
161
- domBuilder(item.children, el, {emptyParent: false});
171
+ if (safeItem.children && Array.isArray(safeItem.children)) {
172
+ domBuilder(safeItem.children, el, {emptyParent: false});
162
173
  }
163
174
 
164
175
  if (mainElement == null) {
@@ -169,19 +180,17 @@ export function domBuilder(structureArray = [], parent, options = {}) {
169
180
  parent.appendChild(el);
170
181
  }
171
182
 
172
- // TODO callback che includano azioni relatuvi ai children dell'elemento potrebbero bìnon essere eseguiti in assenza di parent
173
- if (item.callback && typeof item.callback === 'function') {
174
- item.callback(el);
183
+ // TODO callbacks that act on the element's children may not run when no parent is set
184
+ if (safeItem.callback && typeof safeItem.callback === 'function') {
185
+ safeItem.callback(el);
175
186
  }
176
187
 
177
188
  if (grand_parent) {
178
189
  parent = grand_parent;
179
190
  }
180
- } // end if ((item.condition ?? true) && item != null)
191
+ }
181
192
 
182
- }); // end forEach
193
+ });
183
194
 
184
195
  return mainElement;
185
196
  }
186
-
187
-
@@ -1,26 +1,29 @@
1
1
 
2
+ /** @typedef {import('./dom-builder.js').DomBuilderItem} DomBuilderItem */
3
+
2
4
  /**
3
- * Esegue le operazione di base del domBuilder: assegnazione classi, id ecc...
5
+ * Applies basic domBuilder configuration to an element: classes, id, and attributes.
6
+ * @param {HTMLElement} element - The element to configure.
7
+ * @param {DomBuilderItem} domBuilderItem - The domBuilder configuration object.
8
+ * @returns {HTMLElement}
4
9
  */
5
10
  export function domBuilderBasicSetup (element, domBuilderItem) {
6
11
 
7
- // classi
8
- // possibili denominazioni: class o className
9
- // se presente className, class viene ignorato
12
+ // class aliases: accept both `class` and `className`; `className` takes precedence
10
13
  if (domBuilderItem.class && !domBuilderItem.className) {
11
14
  domBuilderItem.className = domBuilderItem.class;
12
15
  }
13
16
 
14
- // se item.attrs è un array singolo viene trattato come singolo elemento
17
+ // normalize attrs: a single flat array is treated as a single [name, value] pair
15
18
  if (domBuilderItem.attrs && Array.isArray(domBuilderItem.attrs) && !Array.isArray(domBuilderItem.attrs[0])) {
16
19
  domBuilderItem.attrs = [ domBuilderItem.attrs ];
17
20
 
18
- // item.attrs può essere un oggetto del tipo {attr_name: attr_value}
21
+ // attrs can also be a plain object: {attr_name: attr_value}
19
22
  } else if (typeof domBuilderItem.attrs === 'object' && !Array.isArray(domBuilderItem.attrs) && domBuilderItem.attrs !== null) {
20
23
  domBuilderItem.attrs = Object.entries(domBuilderItem.attrs);
21
24
  }
22
25
 
23
- // gli attributi booleani non vengono aggiunti se il loro valore è false
26
+ // boolean attributes are omitted when their value is explicitly false
24
27
  const boolAttrs = [
25
28
  'allowfullscreen',
26
29
  'async',
@@ -54,13 +57,13 @@ export function domBuilderBasicSetup (element, domBuilderItem) {
54
57
 
55
58
 
56
59
 
57
- (domBuilderItem.attrs ?? []).forEach(attr => {
60
+ (/** @type {[string, unknown][]} */ (domBuilderItem.attrs ?? [])).forEach(attr => {
58
61
  if (attr[1] != null && !(boolAttrs.includes(attr[0]) && attr[1]=== false)) {
59
62
  element.setAttribute(attr[0], String(attr[1]));
60
63
  }
61
64
  });
62
65
 
63
- // assegnazioen classi e id (dopo attrs in modo che le proprietà a livello di oggetto prevalgano)
66
+ // assign classes and id after attrs so object-level properties take precedence
64
67
  if(domBuilderItem.className) {
65
68
  element.className = Array.isArray(domBuilderItem.className)
66
69
  ? domBuilderItem.className.filter(Boolean).join(' ')
@@ -1,48 +1,47 @@
1
1
  /**
2
- * Analizza una stringa per estrarre il nome del tag, l'ID, le classi e gli attributi.
2
+ * Parses a string to extract tag name, ID, classes, and attributes.
3
3
  *
4
- * * Il formato della stringa atteso è:
4
+ * Expected string format:
5
5
  *
6
- * `tag#id.classe1.classe2.classeN(attributo1: valore1, attributo2=valore2) Lorem ipsum`
6
+ * `tag#id.class1.class2.classN(attr1: val1, attr2=val2) text content`
7
7
  *
8
- * In cui:
9
- * * `tag` è il nome dell'elemento, se non specificato si assume che sia un `div`
10
- * * `id` è l'id, facoltativo, da assegnare all'elemento, deve essere preeceduto da `#` e deve essere posizionato subito dopo il tag
11
- * * `classe1, classe2...` sono le classi, facoltative, devono essere precedute da `.`
12
- * * `(...)` è il blocco degli attributi, facoltativo. Oltre che dalle parentesi tonde, questa parte può essere delimitata dalle quadre e dalle graffe.
13
- * Ogni coppia _nome-valore_ va separata con `:` o `=`. Se presente solo il nome, si assume che il valore sia `true`.
14
- * * `Lorem ipsum` indica un eventuale contenuto testuale, separato dal blocco precedente da uno spazio
8
+ * Where:
9
+ * * `tag` is the element name; defaults to `div` if omitted
10
+ * * `#id` is the optional element id; must immediately follow the tag
11
+ * * `.class1, .class2...` are optional CSS classes, each preceded by `.`
12
+ * * `(...)` is the optional attribute block; can also use `[...]` or `{...}`.
13
+ * Each name–value pair is separated by `:` or `=`. A bare name (no value) defaults to `true`.
14
+ * * Text content follows after a space
15
15
  *
16
- * Restituisce un oggetto *child* domBuilder
16
+ * Returns a domBuilder child object.
17
17
  *
18
- * Esempio:
18
+ * Examples:
19
19
  * > p#main-info.info.active{data-id:123,role=button} text content
20
20
  *
21
- * oppure
22
21
  * > input#search-field[type=text,disabled]
23
22
  *
24
- * @param {string} domString La stringa DOM Builder da analizzare.
25
- * @returns {{tagName: string, id: string, classes: string[], attributes: {[key: string]: string}}}
23
+ * @param {string} domString - The domBuilder string to parse.
24
+ * @returns {{tag: string, id: string|null, className: string, attrs: Object<string, string|true>, content: string|null}|null}
26
25
  */
27
26
 
28
27
  export function parseDomString(domString) {
29
28
 
30
- // versione con groups
29
+ // version using named capture groups (kept for reference)
31
30
  // const regex = new RegExp(
32
31
  // /^(?<tag>[a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag
33
32
  // /(?:#(?<id>[a-zA-Z0-9_-]+))?/.source + // id
34
- // /(?<classes>(?:\.[a-zA-Z0-9_-]+)*)?/.source + // classi
33
+ // /(?<classes>(?:\.[a-zA-Z0-9_-]+)*)?/.source + // classes
35
34
  // /(?<attrs>[([{].*?[)\]}])?/.source + // attrs
36
35
  // /(?: +(?<content>.*))?$/.source // content
37
36
  // );
38
37
 
39
38
 
40
- // FIX non funziona se la stringa contine id e classi in quest'ordine: <TAG>.<CLASSE>#<ID>, se l'id è prima della classe, funziona
39
+ // FIX: does not work when id follows classes: <TAG>.<CLASS>#<ID>; id must precede classes
41
40
 
42
41
  const regex = new RegExp(
43
- /^([a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag (accetta anche nomi di web components)
42
+ /^([a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag (also accepts web component names)
44
43
  /(?:#([a-zA-Z0-9_-]+))?/.source + // id
45
- /((?:\.[a-zA-Z0-9_-]+)*)?/.source + // classi
44
+ /((?:\.[a-zA-Z0-9_-]+)*)?/.source + // classes
46
45
  /([([{].*?[)\]}])?/.source + // attrs
47
46
  /(?: +(.*))?$/.source // content
48
47
  );
@@ -54,12 +53,12 @@ export function parseDomString(domString) {
54
53
  return null;
55
54
  }
56
55
 
57
- // Nuovi gruppi di cattura (match array):
58
- // [0]: intera stringa
59
- // [1]: Tag
60
- // [2]: ID
61
- // [3]: Classi
62
- // [4]: Attributi (raw)
56
+ // Capture group indices:
57
+ // [0]: full match
58
+ // [1]: tag
59
+ // [2]: id
60
+ // [3]: classes
61
+ // [4]: raw attributes
63
62
  // [5]: content
64
63
 
65
64
  const tag = (matches[1] || 'div').toLowerCase(),
@@ -69,7 +68,7 @@ export function parseDomString(domString) {
69
68
  content = matches[5]?.trim() || null
70
69
  ;
71
70
 
72
- // versione con uso di `groups`
71
+ // version using named capture groups (kept for reference)
73
72
  // const tag = (matches.groups.tag || 'div').toLowerCase(),
74
73
  // id = matches.groups.id?.trim() || null,
75
74
  // classes = matches.groups.classes? matches[3].slice(1).split('.').map(c => c.trim()) : [],
@@ -77,6 +76,7 @@ export function parseDomString(domString) {
77
76
  // content = matches.groups.content?.trim() || null
78
77
  // ;
79
78
 
79
+ /** @type {Record<string, string | true>} */
80
80
  const attrs = {};
81
81
  if (rawAttrs) {
82
82
  const attrsContent = rawAttrs.substring(1, rawAttrs.length - 1);
@@ -86,10 +86,12 @@ export function parseDomString(domString) {
86
86
  if (!pair) return;
87
87
  if (pair.includes('=') || pair.includes(':')) {
88
88
  const parts = pair.split(/[:=]/, 2),
89
- name = parts[0]?.trim(),
89
+ name = parts[0]?.trim() ?? '',
90
90
  value = parts[1] != null ? parts[1].trim() : true;
91
91
 
92
- attrs[name] = value;
92
+ if (name) {
93
+ attrs[name] = value;
94
+ }
93
95
 
94
96
  } else {
95
97
 
@@ -1,5 +1,11 @@
1
+ /**
2
+ * Re-enables all submit and non-disabled button elements within the given context.
3
+ * @param {Document | Element} [context=document]
4
+ * @returns {void}
5
+ */
1
6
  export function enableSubmitBtns(context = document) {
2
7
  context.querySelectorAll('[type=submit], [type=button]:not([data-disabled])').forEach(btn => {
3
- btn.disabled = false;
8
+ const button = /** @type {HTMLButtonElement | HTMLInputElement} */ (btn);
9
+ button.disabled = false;
4
10
  });
5
11
  }
@@ -1,3 +1,4 @@
1
+ /** @type {Record<string, string>} */
1
2
  const ESCAPE_MAP = {
2
3
  '&' : '&amp;',
3
4
  '"' : '&quot;',
@@ -5,10 +6,22 @@ const ESCAPE_MAP = {
5
6
  '<' : '&lt;',
6
7
  '>' : '&gt;'
7
8
  };
9
+ /** @type {Record<string, string>} */
8
10
  const UNESCAPE_MAP = Object.fromEntries(Object.entries(ESCAPE_MAP).map(([k, v]) => [v, k]));
9
11
 
10
12
  const ESCAPE_RE = /[&"'<>]/g;
11
13
  const UNESCAPE_RE = /&amp;|&quot;|&#039;|&lt;|&gt;/g;
12
14
 
15
+ /**
16
+ * Escapes special HTML characters in a string.
17
+ * @param {string | null | undefined} str
18
+ * @returns {string}
19
+ */
13
20
  export const escapeHTML = str => str?.replace(ESCAPE_RE, m => ESCAPE_MAP[m]) ?? '';
21
+
22
+ /**
23
+ * Unescapes HTML entities back to their original characters.
24
+ * @param {string | null | undefined} str
25
+ * @returns {string}
26
+ */
14
27
  export const unescapeHTML = str => str?.replace(UNESCAPE_RE, m => UNESCAPE_MAP[m]) ?? '';