@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
|
@@ -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
|
|
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('
|
|
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
|
-
|
|
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
|
|
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 +
|
|
43
|
-
/(
|
|
44
|
-
/((
|
|
45
|
-
/(
|
|
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
|
|
44
|
+
// Capture group indexes:
|
|
57
45
|
// [0]: full match
|
|
58
46
|
// [1]: tag
|
|
59
|
-
// [2]: id
|
|
60
|
-
// [3]:
|
|
61
|
-
// [4]:
|
|
62
|
-
|
|
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 =
|
|
66
|
-
classes =
|
|
67
|
-
rawAttrs = matches[
|
|
68
|
-
content = matches[
|
|
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',
|