@massimo-cassandro/minimo 1.9.1 → 1.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.9.1",
3
+ "version": "1.10.1",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -64,18 +64,23 @@ import { parseDomString } from './parseDomString.js';
64
64
  *
65
65
  * @function domBuilder
66
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.
67
+ * @param {HTMLElement} [parent] - Parent element the structure is attached to (see `options.insertMode`).
68
68
  * @param {Object} [options={}] - Configuration options.
69
69
  * @param {boolean} [options.emptyParent=false] - When true, the parent element is emptied before building.
70
+ * @param {'append'|'before'|'after'} [options.insertMode='append'] - How each root element of `structureArray` is attached to `parent`: `append` inserts it as a child (default), `before`/`after` insert it as a previous/next sibling of `parent`, preserving `structureArray` order. Only applies to the elements produced by this call — nested `domBuilder` calls (`content`, `children`) always append.
70
71
  * @returns {HTMLElement|null} The first created element, or null if nothing was created.
71
72
  */
72
73
  export function domBuilder(structureArray = [], parent, options = {}) {
73
74
 
74
75
  options = {
75
76
  emptyParent: false,
77
+ insertMode: 'append',
76
78
  ...options
77
79
  };
78
80
 
81
+ /** @type {Map<HTMLElement, HTMLElement>} tracks, for `insertMode: 'after'`, the last inserted sibling per parent node */
82
+ const afterAnchors = new Map();
83
+
79
84
  /** @type {HTMLElement | null} */
80
85
  let mainElement = null;
81
86
  /** @type {HTMLElement} */
@@ -166,7 +171,7 @@ export function domBuilder(structureArray = [], parent, options = {}) {
166
171
 
167
172
  if (safeItem.children != null && !Array.isArray(safeItem.children)) {
168
173
  // eslint-disable-next-line no-console
169
- console.error('Error: `item.children` must be an array → ' + safeItem.children);
174
+ console.error('[domBuilder]: `item.children` must be an array → ' + safeItem.children);
170
175
  }
171
176
  if (safeItem.children && Array.isArray(safeItem.children)) {
172
177
  domBuilder(safeItem.children, el, {emptyParent: false});
@@ -177,7 +182,17 @@ export function domBuilder(structureArray = [], parent, options = {}) {
177
182
  }
178
183
 
179
184
  if (parent) {
180
- parent.appendChild(el);
185
+ if (options.insertMode === 'before') {
186
+ parent.parentNode?.insertBefore(el, parent);
187
+
188
+ } else if (options.insertMode === 'after') {
189
+ const anchor = afterAnchors.get(parent) ?? parent;
190
+ anchor.parentNode?.insertBefore(el, anchor.nextSibling);
191
+ afterAnchors.set(parent, el);
192
+
193
+ } else {
194
+ parent.appendChild(el);
195
+ }
181
196
  }
182
197
 
183
198
  // TODO callbacks that act on the element's children may not run when no parent is set
@@ -7,8 +7,9 @@
7
7
  *
8
8
  * Where:
9
9
  * * `tag` is the element name; defaults to `div` if omitted
10
- * * `#id` is the optional element id; must immediately follow the tag
10
+ * * `#id` is the optional element id, preceded by `#`
11
11
  * * `.class1, .class2...` are optional CSS classes, each preceded by `.`
12
+ * * `#id` and `.class` tokens can appear in any order and interleaved (e.g. `.class1#id.class2`)
12
13
  * * `(...)` is the optional attribute block; can also use `[...]` or `{...}`.
13
14
  * Each name–value pair is separated by `:` or `=`. A bare name (no value) defaults to `true`.
14
15
  * * Text content follows after a space
@@ -26,24 +27,11 @@
26
27
 
27
28
  export function parseDomString(domString) {
28
29
 
29
- // version using named capture groups (kept for reference)
30
- // const regex = new RegExp(
31
- // /^(?<tag>[a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag
32
- // /(?:#(?<id>[a-zA-Z0-9_-]+))?/.source + // id
33
- // /(?<classes>(?:\.[a-zA-Z0-9_-]+)*)?/.source + // classes
34
- // /(?<attrs>[([{].*?[)\]}])?/.source + // attrs
35
- // /(?: +(?<content>.*))?$/.source // content
36
- // );
37
-
38
-
39
- // FIX: does not work when id follows classes: <TAG>.<CLASS>#<ID>; id must precede classes
40
-
41
30
  const regex = new RegExp(
42
- /^([a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag (also accepts web component names)
43
- /(?:#([a-zA-Z0-9_-]+))?/.source + // id
44
- /((?:\.[a-zA-Z0-9_-]+)*)?/.source + // classes
45
- /([([{].*?[)\]}])?/.source + // attrs
46
- /(?: +(.*))?$/.source // content
31
+ /^([a-zA-Z][a-zA-Z0-9-]*)?/.source + // tag (also accepts web component names)
32
+ /((?:[#.][a-zA-Z0-9_-]+)*)?/.source + // id and classes, in any order
33
+ /([([{].*?[)\]}])?/.source + // attrs
34
+ /(?: +(.*))?$/.source // content
47
35
  );
48
36
 
49
37
  const matches = domString.match(regex);
@@ -53,29 +41,24 @@ export function parseDomString(domString) {
53
41
  return null;
54
42
  }
55
43
 
56
- // Capture group indices:
44
+ // Capture group indexes:
57
45
  // [0]: full match
58
46
  // [1]: tag
59
- // [2]: id
60
- // [3]: classes
61
- // [4]: raw attributes
62
- // [5]: content
47
+ // [2]: id and classes (e.g. "#id.class1.class2" or ".class1#id.class2")
48
+ // [3]: raw attributes
49
+ // [4]: content
50
+
51
+ const idAndClasses = matches[2] || '',
52
+ idMatch = idAndClasses.match(/#([a-zA-Z0-9_-]+)/),
53
+ classMatches = [...idAndClasses.matchAll(/\.([a-zA-Z0-9_-]+)/g)];
63
54
 
64
55
  const tag = (matches[1] || 'div').toLowerCase(),
65
- id = matches[2]?.trim() || null,
66
- classes = matches[3] ? matches[3].slice(1).split('.').map(c => c.trim()) : [],
67
- rawAttrs = matches[4]?.trim() || null,
68
- content = matches[5]?.trim() || null
56
+ id = idMatch?.[1] || null,
57
+ classes = classMatches.map(m => m[1]),
58
+ rawAttrs = matches[3]?.trim() || null,
59
+ content = matches[4]?.trim() || null
69
60
  ;
70
61
 
71
- // version using named capture groups (kept for reference)
72
- // const tag = (matches.groups.tag || 'div').toLowerCase(),
73
- // id = matches.groups.id?.trim() || null,
74
- // classes = matches.groups.classes? matches[3].slice(1).split('.').map(c => c.trim()) : [],
75
- // rawAttrs = matches.groups.attrs?.trim() || null,
76
- // content = matches.groups.content?.trim() || null
77
- // ;
78
-
79
62
  /** @type {Record<string, string | true>} */
80
63
  const attrs = {};
81
64
  if (rawAttrs) {
@@ -16,7 +16,7 @@ form.addEventListener('submit', e => {
16
16
  form.classList.add('was-validated');
17
17
  try {
18
18
  if( ... ) {
19
- throw new ValidationError('__message__', [errorFields, ...] );
19
+ throw new ValidationError('__message__', [errorFields, ...] ); // errorFields is optional
20
20
  }
21
21
  } catch( error ) {
22
22
  e.preventDefault();
@@ -38,7 +38,7 @@ form.addEventListener('submit', e => {
38
38
  }, {once: true});
39
39
  });
40
40
 
41
- error.fields[0]?.focus({preventScroll:false});
41
+ error.fields?.[0]?.focus({preventScroll:false});
42
42
  }
43
43
  mAlert({
44
44
  type : 'error',