@massimo-cassandro/minimo 1.22.1 → 1.23.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": "1.22.1",
3
+ "version": "1.23.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -2,17 +2,18 @@
2
2
  /* TODO valutare versione con label a sinistra (form-check-reverse) */
3
3
 
4
4
  /*
5
- Markup (come bootstrap 5, con l'aggiunta dell'attributo nativo `switch`):
5
+ Markup (same as bootstrap 5, plus the native `switch` attribute):
6
6
 
7
7
  <div class="form-check form-switch">
8
8
  <input class="form-check-input" type="checkbox" role="switch" switch id="my-switch">
9
- <label class="form-check-label" for="my-switch">Abilita notifiche</label>
9
+ <label class="form-label" for="my-switch">Enable notifications</label>
10
10
  </div>
11
11
 
12
- L'attributo `switch` (per ora solo WebKit) fa sì che il browser generi un vero
13
- controllo switch nativo, modellabile con gli pseudo-elementi `::track` e `::thumb`.
14
- Dove non è supportato (o dove l'attributo è assente) si applica il fallback
15
- `appearance: none` con la pallina disegnata come background-image.
12
+ The `switch` attribute (WebKit only for now) makes the browser render a real
13
+ native switch control, which can be styled through the `::track` and `::thumb`
14
+ pseudo-elements. Where it is not supported (or where the attribute is missing),
15
+ the `appearance: none` fallback is applied, with the thumb drawn as a
16
+ background-image.
16
17
  */
17
18
 
18
19
  .form-switch {
@@ -34,11 +35,12 @@ Dove non è supportato (o dove l'attributo è assente) si applica il fallback
34
35
  appearance: none;
35
36
  cursor: pointer;
36
37
  background-color: var(--form-switch-background-color);
37
- background-image: radial-gradient(
38
- circle closest-side,
39
- var(--form-switch-cursor-background-color) 98%,
40
- transparent
41
- );
38
+ background-image:
39
+ radial-gradient(
40
+ circle closest-side,
41
+ var(--form-switch-cursor-background-color) 98%,
42
+ transparent
43
+ );
42
44
  background-repeat: no-repeat;
43
45
  background-position: var(--form-switch-thumb-inset) center;
44
46
  background-size: var(--_switch-thumb-size) var(--_switch-thumb-size);
@@ -63,7 +65,8 @@ Dove non è supportato (o dove l'attributo è assente) si applica il fallback
63
65
  }
64
66
  }
65
67
 
66
- .form-check-label {
68
+ .form-label {
69
+ padding-inline-start: 0 !important;
67
70
  cursor: pointer;
68
71
  user-select: none;
69
72
  }
@@ -71,7 +74,7 @@ Dove non è supportato (o dove l'attributo è assente) si applica il fallback
71
74
  &:has(.form-check-input:disabled) {
72
75
  opacity: var(--form-control-disabled-opacity);
73
76
 
74
- .form-check-input, .form-check-label {
77
+ .form-check-input, .form-label {
75
78
  cursor: not-allowed;
76
79
  }
77
80
  }
@@ -18,8 +18,12 @@ import { parseDomString } from './parseDomString.js';
18
18
  * A string or number is set as plain text (`textContent`) unless it contains `<`, in which case it is
19
19
  * treated as markup: sanitized and inserted via the native Sanitizer API (`Element.setHTML`) where
20
20
  * supported, falling back to raw `innerHTML` on browsers without it.
21
- * @property {boolean} [condition=true] - When false, the element is skipped.
22
- * @property {(function(HTMLElement): void) | null} [callback] - Callback invoked after the element is created.
21
+ * @property {string | number} [text] - Literal text node shorthand: inserts a plain `Text` node (never
22
+ * parsed as markup) in place of an element, so it can sit as a sibling of tags within `children`/`content`
23
+ * arrays. Mutually exclusive with `tag`/`content`/`children`: when set (and `tag` is absent), every other
24
+ * property except `condition` and `callback` is ignored.
25
+ * @property {boolean} [condition=true] - When false, the element (or text node) is skipped.
26
+ * @property {(function(HTMLElement|Text): void) | null} [callback] - Callback invoked after the element (or text node) is created.
23
27
  * @property {Array<DomBuilderItem|string>} [children] - Configuration array for child elements. Accepts strings (shorthand per `parseDomString`) and/or configuration objects.
24
28
  */
25
29
 
@@ -53,6 +57,18 @@ import { parseDomString } from './parseDomString.js';
53
57
  * ]
54
58
  * ```
55
59
  *
60
+ * To mix literal text and tags as siblings within the same element (e.g. `Lorem <strong>ipsum</strong> dolor`),
61
+ * use the `text` shorthand in `children`/`content`, since a plain string item is always parsed as a tag
62
+ * (see `parseDomString`), never as a text node:
63
+ *
64
+ * ```javascript
65
+ * children: [
66
+ * { text: 'Lorem ' },
67
+ * { tag: 'strong', content: 'ipsum' },
68
+ * { text: ' dolor' }
69
+ * ]
70
+ * ```
71
+ *
56
72
  * IDs and classes can be specified either as top-level object keys or inside the `attrs` object.
57
73
  * When both are present, top-level properties take precedence.
58
74
  *
@@ -74,8 +90,9 @@ import { parseDomString } from './parseDomString.js';
74
90
  * - `id` {string | null} - Unique element ID.
75
91
  * - `attrs` {[string, *] | [string, *][] | Object<string, *>} - Attributes: a `[name, value]` pair, array of pairs, or `{name: value}` object.
76
92
  * - `content` {string | number | Function | Element | DomBuilderItem[] | null} - Element content. A string/number is set as `textContent` unless it contains `<`, in which case it's sanitized and inserted as markup (`Element.setHTML`, falling back to `innerHTML`).
77
- * - `condition` {boolean} - When false, the element is skipped. Default `true`.
78
- * - `callback` {(function(HTMLElement): void) | null} - Invoked after the element is created.
93
+ * - `text` {string | number} - Literal text node shorthand: inserts a plain `Text` node (never parsed as markup) as a sibling of other items, in place of an element. Mutually exclusive with `tag`/`content`/`children`.
94
+ * - `condition` {boolean} - When false, the element (or text node) is skipped. Default `true`.
95
+ * - `callback` {(function(HTMLElement|Text): void) | null} - Invoked after the element (or text node) is created.
79
96
  * - `children` {Array<DomBuilderItem|string>} - Configuration array for child elements (same format, nested).
80
97
  *
81
98
  * IDs and classes can be specified either as top-level object keys or inside `attrs`; top-level properties take precedence.
@@ -109,7 +126,7 @@ export function domBuilder(structureArray = [], parent, options = {}) {
109
126
  /** @type {HTMLElement | DocumentFragment | null} */
110
127
  let target = useFragment ? document.createDocumentFragment() : (parent ?? null);
111
128
 
112
- /** @type {Map<HTMLElement | DocumentFragment, HTMLElement>} tracks, for `insertMode: 'after'`, the last inserted sibling per parent node */
129
+ /** @type {Map<HTMLElement | DocumentFragment, Node>} tracks, for `insertMode: 'after'`, the last inserted sibling per parent node */
113
130
  const afterAnchors = new Map();
114
131
 
115
132
  /** @type {HTMLElement | null} */
@@ -135,6 +152,34 @@ export function domBuilder(structureArray = [], parent, options = {}) {
135
152
 
136
153
  const safeItem = item; // const binding so TypeScript tracks non-null type in nested closures
137
154
 
155
+ // `{ text: '...' }` shorthand: a literal Text node, never parsed as markup, inserted in
156
+ // place of an element so it can sit as a sibling of tags within `children`/`content` arrays
157
+ if (safeItem.tag == null && (typeof safeItem.text === 'string' || typeof safeItem.text === 'number')) {
158
+
159
+ const textNode = document.createTextNode(String(safeItem.text));
160
+
161
+ if (target) {
162
+ if (options.insertMode === 'before') {
163
+ const anchor = /** @type {HTMLElement} */ (target);
164
+ anchor.parentNode?.insertBefore(textNode, anchor);
165
+
166
+ } else if (options.insertMode === 'after') {
167
+ const anchor = afterAnchors.get(/** @type {HTMLElement} */ (target)) ?? /** @type {HTMLElement} */ (target);
168
+ anchor.parentNode?.insertBefore(textNode, anchor.nextSibling);
169
+ afterAnchors.set(/** @type {HTMLElement} */ (target), textNode);
170
+
171
+ } else {
172
+ target.appendChild(textNode);
173
+ }
174
+ }
175
+
176
+ if (safeItem.callback && typeof safeItem.callback === 'function') {
177
+ safeItem.callback(textNode);
178
+ }
179
+
180
+ return;
181
+ }
182
+
138
183
  // when tag is an array, create a series of nested elements;
139
184
  // the last one receives the remaining object properties
140
185
  if (Array.isArray(safeItem.tag)) {
@@ -132,12 +132,21 @@ export function buildSelect({
132
132
 
133
133
  id = id || randomId();
134
134
 
135
- // normalizing option as an array
136
- if(options && !Array.isArray(options)) {
137
- options = Object.entries(options);
135
+ // normalizing options as an array of [value, text] pairs
136
+ /** @type {Array<[string|number, string]>} */
137
+ let optionsList = [];
138
138
 
139
- } else if(options && !Array.isArray(options[0])) {
140
- options = options.map(o => Object.values(o));
139
+ if(options) {
140
+ if(!Array.isArray(options)) {
141
+ optionsList = /** @type {Array<[string|number, string]>} */ (Object.entries(options));
142
+
143
+ } else if(!Array.isArray(options[0])) {
144
+ optionsList = /** @type {Array<Record<string, string>>} */ (options)
145
+ .map(o => /** @type {[string|number, string]} */ (Object.values(o)));
146
+
147
+ } else {
148
+ optionsList = /** @type {Array<[string|number, string]>} */ (options);
149
+ }
141
150
  }
142
151
 
143
152
 
@@ -157,18 +166,15 @@ export function buildSelect({
157
166
  },
158
167
  children: [
159
168
  ...(addEmptyOption? ['option[value:]'] : []),
160
- ...(options
161
- ? options.map(([value, text]) => ({
162
- tag: 'option',
163
- attrs: {
164
- value: value,
165
- // eslint-disable-next-line eqeqeq
166
- selected: value == selectedValue
167
- },
168
- content: text
169
- }))
170
- : []
171
- )
169
+ ...optionsList.map(([value, text]) => ({
170
+ tag: 'option',
171
+ attrs: {
172
+ value: value,
173
+ // eslint-disable-next-line eqeqeq
174
+ selected: value == selectedValue
175
+ },
176
+ content: text
177
+ }))
172
178
  ],
173
179
  callback: callback
174
180
  }
@@ -186,6 +192,8 @@ export function buildSelect({
186
192
  * @param {string | null} args.id - `id` attribute.
187
193
  * @param {string | number | null} [args.value=1] - checkbox `value` attribute.
188
194
  * @param {boolean} [args.checked=false] - checkbox `checked` attribute.
195
+ * @param {boolean} [args.switch=false] - When true, the checkbox is rendered as a switch
196
+ * (`.form-switch` wrapper, `role="switch"` and the native `switch` attribute).
189
197
  * @param {string | null} [args.wrapperClass=null] - optional class to be added to the `.form-group` wrapper (used only when `addFormGroup` is true).
190
198
  * @param {boolean} [args.addFormGroup=true] - When true, wraps the checkbox in a `.form-group` element.
191
199
  * @param {boolean} [args.condition=true] - When false, the function returns `null` without building anything.
@@ -200,6 +208,7 @@ export function buildCheckbox({
200
208
  id = null,
201
209
  value = 1,
202
210
  checked = false,
211
+ switch: isSwitch = false,
203
212
  wrapperClass = null,
204
213
  addFormGroup = true,
205
214
  condition = true,
@@ -213,17 +222,24 @@ export function buildCheckbox({
213
222
  }
214
223
 
215
224
  /*
216
- <div class="form-group">
225
+ <div class="form-group"> <- optional
217
226
  <div class="form-check">
218
227
  <input type="checkbox" id="..." name="..." class="form-check-input" value="1">
219
228
  <label for="..." class="form-label">...</label>
220
229
  </div>
221
230
  </div>
231
+
232
+ <div class="form-group"> <- optional
233
+ <div class="form-check form-switch">
234
+ <input class="form-check-input" type="checkbox" role="switch" switch id="my-switch">
235
+ <label class="form-label" for="my-switch">My label</label>
236
+ </div>
237
+ </div>
222
238
  */
223
239
  id = id || randomId();
224
240
 
225
241
  const tag = {
226
- className: classnames('form-check', !addFormGroup && !help && wrapperClass),
242
+ className: classnames('form-check', isSwitch && 'form-switch', !addFormGroup && !help && wrapperClass),
227
243
  children: [
228
244
  {
229
245
  tag: 'input',
@@ -234,7 +250,9 @@ export function buildCheckbox({
234
250
  type: 'checkbox',
235
251
  value: value,
236
252
  name: name,
237
- checked: checked
253
+ checked: checked,
254
+ // native switch control (WebKit only for now), with the css fallback elsewhere
255
+ ...(isSwitch? {role: 'switch', switch: ''} : {})
238
256
  },
239
257
  callback: callback
240
258
  },
@@ -59,7 +59,6 @@ export type MinimoClass =
59
59
  | 'font-semibold'
60
60
  | 'form-check'
61
61
  | 'form-check-input'
62
- | 'form-check-label'
63
62
  | 'form-check-top-label'
64
63
  | 'form-collection'
65
64
  | 'form-collection-fset'