@livenetworks/ashlar 2.0.2 → 2.0.4
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/components/ln-confirm/README.md +2 -2
- package/components/ln-confirm/ln-confirm.js +1 -1
- package/components/ln-confirm/src/ln-confirm.js +1 -3
- package/components/ln-core/attrs.js +273 -273
- package/components/ln-core/events.js +122 -122
- package/components/ln-core/lifecycle.js +403 -403
- package/components/ln-debug/ln-debug.js +1 -1
- package/components/ln-debug/src/generated-attributes.js +1 -0
- package/components/ln-modal/ln-modal.js +1 -1
- package/components/ln-modal/ln-modal.scss +1 -1
- package/components/ln-modal/src/ln-modal.js +1 -0
- package/components/ln-router/README.md +33 -0
- package/components/ln-router/ln-router.js +1 -1
- package/components/ln-router/ln-router.schema.json +8 -0
- package/components/ln-router/src/ln-router.js +51 -28
- package/components/ln-router/src/router-model.js +85 -0
- package/components/ln-toast/ln-toast.js +1 -1
- package/components/ln-toast/ln-toast.scss +5 -1
- package/components/ln-toast/src/ln-toast.js +1 -1
- package/ln-ashlar.html-data.json +4 -0
- package/package.json +1 -1
- package/web-types.json +8 -1
|
@@ -1,273 +1,273 @@
|
|
|
1
|
-
// ─── Typed Live Attribute Readers ──────────────────────────
|
|
2
|
-
//
|
|
3
|
-
// Scanner constraint: attribute names passed into these readers, and into a
|
|
4
|
-
// `defineAttrs` spec, must always be inline string literals or a `const`
|
|
5
|
-
// bound to a literal — never concatenated or templated.
|
|
6
|
-
// `scripts/sync-ln-schemas.mjs:22-23` is a raw regex over file text; a
|
|
7
|
-
// constructed name is invisible to it and the CI docs gate breaks silently
|
|
8
|
-
// (this already happened once — commit `3495690`).
|
|
9
|
-
|
|
10
|
-
export function attrStr(el, name, fallback) {
|
|
11
|
-
const raw = el.getAttribute(name);
|
|
12
|
-
return raw === null ? fallback : raw;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// `0` stays `0` — unlike the `parseInt(...) || 1000` idiom this replaces.
|
|
16
|
-
export function attrInt(el, name, fallback) {
|
|
17
|
-
const parsed = parseInt(el.getAttribute(name), 10);
|
|
18
|
-
return isNaN(parsed) ? fallback : parsed;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function attrBool(el, name, fallback = false) {
|
|
22
|
-
const raw = el.getAttribute(name);
|
|
23
|
-
if (raw === null) return !!fallback;
|
|
24
|
-
const val = raw.trim().toLowerCase();
|
|
25
|
-
if (val === 'false' || val === '0') return false;
|
|
26
|
-
return true;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Allocates a new array per read — do not call inside a per-row loop.
|
|
30
|
-
export function attrList(el, name) {
|
|
31
|
-
return (el.getAttribute(name) || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const _enumReaderCache = new Map();
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Cached reader factory for enum attributes.
|
|
38
|
-
* @param {string[]} values - allowed values
|
|
39
|
-
* @param {*} fallback - default value
|
|
40
|
-
* @returns {function(Element, string): *}
|
|
41
|
-
*/
|
|
42
|
-
export function attrEnum(values, fallback) {
|
|
43
|
-
const key = (values ? values.join('|') : '') + '::' + fallback;
|
|
44
|
-
if (_enumReaderCache.has(key)) return _enumReaderCache.get(key);
|
|
45
|
-
const valSet = new Set(values || []);
|
|
46
|
-
const reader = function (el, name) {
|
|
47
|
-
const raw = el.getAttribute(name);
|
|
48
|
-
return (raw !== null && valSet.has(raw)) ? raw : fallback;
|
|
49
|
-
};
|
|
50
|
-
_enumReaderCache.set(key, reader);
|
|
51
|
-
return reader;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function attrFloat(el, name, fallback) {
|
|
55
|
-
const parsed = parseFloat(el.getAttribute(name));
|
|
56
|
-
return isNaN(parsed) ? fallback : parsed;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function attrJson(el, name, fallback) {
|
|
60
|
-
const raw = el.getAttribute(name);
|
|
61
|
-
if (!raw) return fallback;
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(raw);
|
|
64
|
-
} catch (_) {
|
|
65
|
-
return fallback;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Validates a DOM attribute value against its declared schema entry.
|
|
71
|
-
* Logs a console.warn (with [componentTag] prefix) if the value is invalid.
|
|
72
|
-
*
|
|
73
|
-
* @param {Object} entry - { type, values, min, max, fallback, ... }
|
|
74
|
-
* @param {string|null} value - raw DOM attribute value (null if absent)
|
|
75
|
-
* @param {string} attrName - attribute name (e.g. 'data-ln-modal')
|
|
76
|
-
* @param {string} componentTag - component tag (e.g. 'ln-modal')
|
|
77
|
-
* @param {Element} [el] - optional DOM element carrying the attribute
|
|
78
|
-
* @returns {boolean} true if valid, false if invalid
|
|
79
|
-
*/
|
|
80
|
-
export function validateAttrValue(entry, value, attrName, componentTag, el) {
|
|
81
|
-
if (!entry || value === null) return true;
|
|
82
|
-
const tag = componentTag || 'ln-component';
|
|
83
|
-
const type = entry.type;
|
|
84
|
-
|
|
85
|
-
// valueless / unconstrained types
|
|
86
|
-
if (type === 'trigger' || type === 'marker' || type === 'string' || type === 'list' || !type) {
|
|
87
|
-
return true;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Bare attributes (presence-only in HTML) with a declared fallback resolve to fallback in readers
|
|
91
|
-
if (value === '' && entry.fallback !== undefined) {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const logWarn = (msg) => {
|
|
96
|
-
if (el) console.warn(msg, el);
|
|
97
|
-
else console.warn(msg);
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
if (type === 'boolean') {
|
|
101
|
-
const lower = value.trim().toLowerCase();
|
|
102
|
-
if (lower !== '' && lower !== 'true' && lower !== 'false' && lower !== '1' && lower !== '0') {
|
|
103
|
-
logWarn(`[${tag}] Invalid value "${value}" for boolean attribute "${attrName}". Allowed: "true", "false", or presence-only.`);
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
if (type === 'enum') {
|
|
110
|
-
const allowed = entry.values || [];
|
|
111
|
-
if (!allowed.includes(value)) {
|
|
112
|
-
logWarn(`[${tag}] Invalid value "${value}" for attribute "${attrName}". Allowed: ${allowed.join(', ')}. Fallback: "${entry.fallback}".`);
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
return true;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
if (type === 'integer') {
|
|
119
|
-
if (!/^-?\d+$/.test(value)) {
|
|
120
|
-
logWarn(`[${tag}] Invalid integer "${value}" for attribute "${attrName}". Fallback: ${entry.fallback}.`);
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
const num = parseInt(value, 10);
|
|
124
|
-
if (entry.min !== undefined && num < entry.min) {
|
|
125
|
-
logWarn(`[${tag}] Value ${num} for attribute "${attrName}" is less than min (${entry.min}).`);
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
if (entry.max !== undefined && num > entry.max) {
|
|
129
|
-
logWarn(`[${tag}] Value ${num} for attribute "${attrName}" is greater than max (${entry.max}).`);
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (type === 'float') {
|
|
136
|
-
if (isNaN(Number(value))) {
|
|
137
|
-
logWarn(`[${tag}] Invalid float "${value}" for attribute "${attrName}". Fallback: ${entry.fallback}.`);
|
|
138
|
-
return false;
|
|
139
|
-
}
|
|
140
|
-
return true;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (type === 'json') {
|
|
144
|
-
try {
|
|
145
|
-
JSON.parse(value);
|
|
146
|
-
return true;
|
|
147
|
-
} catch (err) {
|
|
148
|
-
logWarn(`[${tag}] Invalid JSON for attribute "${attrName}": ${err.message}. Fallback: ${entry.fallback}.`);
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return true;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Define live getter-only properties on `instance`, each reading `dom` on
|
|
158
|
-
* every access. Nothing is copied into instance state, so there is no state
|
|
159
|
-
* to invalidate and no sync step.
|
|
160
|
-
*
|
|
161
|
-
* @param {Object} instance
|
|
162
|
-
* @param {Element} dom
|
|
163
|
-
* @param {Object} spec - { propName: [reader, attributeName, fallback] }
|
|
164
|
-
* @returns {Object} instance
|
|
165
|
-
*/
|
|
166
|
-
export function defineAttrs(instance, dom, spec) {
|
|
167
|
-
for (const propName in spec) {
|
|
168
|
-
const [reader, attributeName, fallback] = spec[propName];
|
|
169
|
-
Object.defineProperty(instance, propName, {
|
|
170
|
-
// Getter only, no setter — assignment throws in strict mode (ES
|
|
171
|
-
// modules are strict). Deliberate: it forbids a drifting copy.
|
|
172
|
-
get: function () {
|
|
173
|
-
return reader(dom, attributeName, fallback);
|
|
174
|
-
},
|
|
175
|
-
enumerable: true,
|
|
176
|
-
configurable: true
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
return instance;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* One table per component is the single source of truth for its HOST
|
|
184
|
-
* attributes. Derivations below read it; nothing else declares an
|
|
185
|
-
* attribute name.
|
|
186
|
-
*
|
|
187
|
-
* Table entry shape:
|
|
188
|
-
* 'data-ln-x': { prop, read, type, values, fallback, min, max, effect, description } — any subset
|
|
189
|
-
*
|
|
190
|
-
* The scanner constraint at the top of this file applies to table KEYS:
|
|
191
|
-
* literal strings only. A computed key is invisible to
|
|
192
|
-
* scripts/sync-ln-schemas.mjs.
|
|
193
|
-
*/
|
|
194
|
-
export function attrSpec(table) {
|
|
195
|
-
const spec = {};
|
|
196
|
-
for (const name in table) {
|
|
197
|
-
const entry = table[name];
|
|
198
|
-
if (!entry || !entry.prop) continue;
|
|
199
|
-
let reader = entry.read;
|
|
200
|
-
if (!reader && entry.type) {
|
|
201
|
-
switch (entry.type) {
|
|
202
|
-
case 'enum':
|
|
203
|
-
reader = attrEnum(entry.values, entry.fallback);
|
|
204
|
-
break;
|
|
205
|
-
case 'integer':
|
|
206
|
-
reader = attrInt;
|
|
207
|
-
break;
|
|
208
|
-
case 'float':
|
|
209
|
-
reader = attrFloat;
|
|
210
|
-
break;
|
|
211
|
-
case 'boolean':
|
|
212
|
-
reader = attrBool;
|
|
213
|
-
break;
|
|
214
|
-
case 'list':
|
|
215
|
-
reader = attrList;
|
|
216
|
-
break;
|
|
217
|
-
case 'json':
|
|
218
|
-
reader = attrJson;
|
|
219
|
-
break;
|
|
220
|
-
case 'string':
|
|
221
|
-
default:
|
|
222
|
-
reader = attrStr;
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
if (!reader) reader = attrStr;
|
|
227
|
-
spec[entry.prop] = [reader, name, entry.fallback];
|
|
228
|
-
}
|
|
229
|
-
return spec;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function attrEffects(table) {
|
|
233
|
-
const effects = {};
|
|
234
|
-
for (const name in table) {
|
|
235
|
-
const entry = table[name];
|
|
236
|
-
if (!entry) continue;
|
|
237
|
-
if (entry.effect) effects[name] = entry.effect;
|
|
238
|
-
}
|
|
239
|
-
// null, not {} — `_registerAttrEntry` tests `entry.onAttrChange || entry.effects`,
|
|
240
|
-
// and an empty object is truthy. A table of pure getters would otherwise be
|
|
241
|
-
// pushed into `registry.reactive` and iterated on every data-ln-* mutation
|
|
242
|
-
// on the page while having nothing to run.
|
|
243
|
-
return Object.keys(effects).length ? effects : null;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* The attribute names a registry entry should be indexed under, so the shared
|
|
248
|
-
* observer can reach it by name instead of walking every reactive entry.
|
|
249
|
-
*
|
|
250
|
-
* Note this reads `entry.effects` — the DERIVED table produced by
|
|
251
|
-
* `attrEffects` above — not the raw component table. An entry is indexed
|
|
252
|
-
* under every name it declares, which for a component like `ln-list` means
|
|
253
|
-
* the suffixed names (`data-ln-list-window`) and not only the selector.
|
|
254
|
-
*
|
|
255
|
-
* Internal to ln-core, and deliberately NOT re-exported from index.js: unlike
|
|
256
|
-
* `attrSpec` / `attrEffects`, which take the attribute table a component author
|
|
257
|
-
* writes, this takes a registry entry — a shape no author ever constructs.
|
|
258
|
-
* Publishing it would make the entry shape semver-load-bearing for no caller.
|
|
259
|
-
*
|
|
260
|
-
* @param {Object} entry - a registry entry: { effects, onAttrChange, declared }
|
|
261
|
-
* @returns {Set<string>|null} names to index under, or null for a wildcard —
|
|
262
|
-
* `onAttrChange` without `declared` asks for EVERY data-ln-* mutation and
|
|
263
|
-
* by definition cannot be keyed by name.
|
|
264
|
-
*/
|
|
265
|
-
export function reactiveNames(entry) {
|
|
266
|
-
if (entry.onAttrChange && !entry.declared) return null;
|
|
267
|
-
|
|
268
|
-
const names = new Set();
|
|
269
|
-
if (entry.effects) for (const n in entry.effects) names.add(n);
|
|
270
|
-
if (entry.onAttrChange && entry.declared) for (const n of entry.declared) names.add(n);
|
|
271
|
-
return names;
|
|
272
|
-
}
|
|
273
|
-
|
|
1
|
+
// ─── Typed Live Attribute Readers ──────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Scanner constraint: attribute names passed into these readers, and into a
|
|
4
|
+
// `defineAttrs` spec, must always be inline string literals or a `const`
|
|
5
|
+
// bound to a literal — never concatenated or templated.
|
|
6
|
+
// `scripts/sync-ln-schemas.mjs:22-23` is a raw regex over file text; a
|
|
7
|
+
// constructed name is invisible to it and the CI docs gate breaks silently
|
|
8
|
+
// (this already happened once — commit `3495690`).
|
|
9
|
+
|
|
10
|
+
export function attrStr(el, name, fallback) {
|
|
11
|
+
const raw = el.getAttribute(name);
|
|
12
|
+
return raw === null ? fallback : raw;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// `0` stays `0` — unlike the `parseInt(...) || 1000` idiom this replaces.
|
|
16
|
+
export function attrInt(el, name, fallback) {
|
|
17
|
+
const parsed = parseInt(el.getAttribute(name), 10);
|
|
18
|
+
return isNaN(parsed) ? fallback : parsed;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function attrBool(el, name, fallback = false) {
|
|
22
|
+
const raw = el.getAttribute(name);
|
|
23
|
+
if (raw === null) return !!fallback;
|
|
24
|
+
const val = raw.trim().toLowerCase();
|
|
25
|
+
if (val === 'false' || val === '0') return false;
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Allocates a new array per read — do not call inside a per-row loop.
|
|
30
|
+
export function attrList(el, name) {
|
|
31
|
+
return (el.getAttribute(name) || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const _enumReaderCache = new Map();
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Cached reader factory for enum attributes.
|
|
38
|
+
* @param {string[]} values - allowed values
|
|
39
|
+
* @param {*} fallback - default value
|
|
40
|
+
* @returns {function(Element, string): *}
|
|
41
|
+
*/
|
|
42
|
+
export function attrEnum(values, fallback) {
|
|
43
|
+
const key = (values ? values.join('|') : '') + '::' + fallback;
|
|
44
|
+
if (_enumReaderCache.has(key)) return _enumReaderCache.get(key);
|
|
45
|
+
const valSet = new Set(values || []);
|
|
46
|
+
const reader = function (el, name) {
|
|
47
|
+
const raw = el.getAttribute(name);
|
|
48
|
+
return (raw !== null && valSet.has(raw)) ? raw : fallback;
|
|
49
|
+
};
|
|
50
|
+
_enumReaderCache.set(key, reader);
|
|
51
|
+
return reader;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function attrFloat(el, name, fallback) {
|
|
55
|
+
const parsed = parseFloat(el.getAttribute(name));
|
|
56
|
+
return isNaN(parsed) ? fallback : parsed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function attrJson(el, name, fallback) {
|
|
60
|
+
const raw = el.getAttribute(name);
|
|
61
|
+
if (!raw) return fallback;
|
|
62
|
+
try {
|
|
63
|
+
return JSON.parse(raw);
|
|
64
|
+
} catch (_) {
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Validates a DOM attribute value against its declared schema entry.
|
|
71
|
+
* Logs a console.warn (with [componentTag] prefix) if the value is invalid.
|
|
72
|
+
*
|
|
73
|
+
* @param {Object} entry - { type, values, min, max, fallback, ... }
|
|
74
|
+
* @param {string|null} value - raw DOM attribute value (null if absent)
|
|
75
|
+
* @param {string} attrName - attribute name (e.g. 'data-ln-modal')
|
|
76
|
+
* @param {string} componentTag - component tag (e.g. 'ln-modal')
|
|
77
|
+
* @param {Element} [el] - optional DOM element carrying the attribute
|
|
78
|
+
* @returns {boolean} true if valid, false if invalid
|
|
79
|
+
*/
|
|
80
|
+
export function validateAttrValue(entry, value, attrName, componentTag, el) {
|
|
81
|
+
if (!entry || value === null) return true;
|
|
82
|
+
const tag = componentTag || 'ln-component';
|
|
83
|
+
const type = entry.type;
|
|
84
|
+
|
|
85
|
+
// valueless / unconstrained types
|
|
86
|
+
if (type === 'trigger' || type === 'marker' || type === 'string' || type === 'list' || !type) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Bare attributes (presence-only in HTML) with a declared fallback resolve to fallback in readers
|
|
91
|
+
if (value === '' && entry.fallback !== undefined) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const logWarn = (msg) => {
|
|
96
|
+
if (el) console.warn(msg, el);
|
|
97
|
+
else console.warn(msg);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (type === 'boolean') {
|
|
101
|
+
const lower = value.trim().toLowerCase();
|
|
102
|
+
if (lower !== '' && lower !== 'true' && lower !== 'false' && lower !== '1' && lower !== '0') {
|
|
103
|
+
logWarn(`[${tag}] Invalid value "${value}" for boolean attribute "${attrName}". Allowed: "true", "false", or presence-only.`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (type === 'enum') {
|
|
110
|
+
const allowed = entry.values || [];
|
|
111
|
+
if (!allowed.includes(value)) {
|
|
112
|
+
logWarn(`[${tag}] Invalid value "${value}" for attribute "${attrName}". Allowed: ${allowed.join(', ')}. Fallback: "${entry.fallback}".`);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (type === 'integer') {
|
|
119
|
+
if (!/^-?\d+$/.test(value)) {
|
|
120
|
+
logWarn(`[${tag}] Invalid integer "${value}" for attribute "${attrName}". Fallback: ${entry.fallback}.`);
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
const num = parseInt(value, 10);
|
|
124
|
+
if (entry.min !== undefined && num < entry.min) {
|
|
125
|
+
logWarn(`[${tag}] Value ${num} for attribute "${attrName}" is less than min (${entry.min}).`);
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
if (entry.max !== undefined && num > entry.max) {
|
|
129
|
+
logWarn(`[${tag}] Value ${num} for attribute "${attrName}" is greater than max (${entry.max}).`);
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (type === 'float') {
|
|
136
|
+
if (isNaN(Number(value))) {
|
|
137
|
+
logWarn(`[${tag}] Invalid float "${value}" for attribute "${attrName}". Fallback: ${entry.fallback}.`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (type === 'json') {
|
|
144
|
+
try {
|
|
145
|
+
JSON.parse(value);
|
|
146
|
+
return true;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
logWarn(`[${tag}] Invalid JSON for attribute "${attrName}": ${err.message}. Fallback: ${entry.fallback}.`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Define live getter-only properties on `instance`, each reading `dom` on
|
|
158
|
+
* every access. Nothing is copied into instance state, so there is no state
|
|
159
|
+
* to invalidate and no sync step.
|
|
160
|
+
*
|
|
161
|
+
* @param {Object} instance
|
|
162
|
+
* @param {Element} dom
|
|
163
|
+
* @param {Object} spec - { propName: [reader, attributeName, fallback] }
|
|
164
|
+
* @returns {Object} instance
|
|
165
|
+
*/
|
|
166
|
+
export function defineAttrs(instance, dom, spec) {
|
|
167
|
+
for (const propName in spec) {
|
|
168
|
+
const [reader, attributeName, fallback] = spec[propName];
|
|
169
|
+
Object.defineProperty(instance, propName, {
|
|
170
|
+
// Getter only, no setter — assignment throws in strict mode (ES
|
|
171
|
+
// modules are strict). Deliberate: it forbids a drifting copy.
|
|
172
|
+
get: function () {
|
|
173
|
+
return reader(dom, attributeName, fallback);
|
|
174
|
+
},
|
|
175
|
+
enumerable: true,
|
|
176
|
+
configurable: true
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
return instance;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* One table per component is the single source of truth for its HOST
|
|
184
|
+
* attributes. Derivations below read it; nothing else declares an
|
|
185
|
+
* attribute name.
|
|
186
|
+
*
|
|
187
|
+
* Table entry shape:
|
|
188
|
+
* 'data-ln-x': { prop, read, type, values, fallback, min, max, effect, description } — any subset
|
|
189
|
+
*
|
|
190
|
+
* The scanner constraint at the top of this file applies to table KEYS:
|
|
191
|
+
* literal strings only. A computed key is invisible to
|
|
192
|
+
* scripts/sync-ln-schemas.mjs.
|
|
193
|
+
*/
|
|
194
|
+
export function attrSpec(table) {
|
|
195
|
+
const spec = {};
|
|
196
|
+
for (const name in table) {
|
|
197
|
+
const entry = table[name];
|
|
198
|
+
if (!entry || !entry.prop) continue;
|
|
199
|
+
let reader = entry.read;
|
|
200
|
+
if (!reader && entry.type) {
|
|
201
|
+
switch (entry.type) {
|
|
202
|
+
case 'enum':
|
|
203
|
+
reader = attrEnum(entry.values, entry.fallback);
|
|
204
|
+
break;
|
|
205
|
+
case 'integer':
|
|
206
|
+
reader = attrInt;
|
|
207
|
+
break;
|
|
208
|
+
case 'float':
|
|
209
|
+
reader = attrFloat;
|
|
210
|
+
break;
|
|
211
|
+
case 'boolean':
|
|
212
|
+
reader = attrBool;
|
|
213
|
+
break;
|
|
214
|
+
case 'list':
|
|
215
|
+
reader = attrList;
|
|
216
|
+
break;
|
|
217
|
+
case 'json':
|
|
218
|
+
reader = attrJson;
|
|
219
|
+
break;
|
|
220
|
+
case 'string':
|
|
221
|
+
default:
|
|
222
|
+
reader = attrStr;
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (!reader) reader = attrStr;
|
|
227
|
+
spec[entry.prop] = [reader, name, entry.fallback];
|
|
228
|
+
}
|
|
229
|
+
return spec;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function attrEffects(table) {
|
|
233
|
+
const effects = {};
|
|
234
|
+
for (const name in table) {
|
|
235
|
+
const entry = table[name];
|
|
236
|
+
if (!entry) continue;
|
|
237
|
+
if (entry.effect) effects[name] = entry.effect;
|
|
238
|
+
}
|
|
239
|
+
// null, not {} — `_registerAttrEntry` tests `entry.onAttrChange || entry.effects`,
|
|
240
|
+
// and an empty object is truthy. A table of pure getters would otherwise be
|
|
241
|
+
// pushed into `registry.reactive` and iterated on every data-ln-* mutation
|
|
242
|
+
// on the page while having nothing to run.
|
|
243
|
+
return Object.keys(effects).length ? effects : null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* The attribute names a registry entry should be indexed under, so the shared
|
|
248
|
+
* observer can reach it by name instead of walking every reactive entry.
|
|
249
|
+
*
|
|
250
|
+
* Note this reads `entry.effects` — the DERIVED table produced by
|
|
251
|
+
* `attrEffects` above — not the raw component table. An entry is indexed
|
|
252
|
+
* under every name it declares, which for a component like `ln-list` means
|
|
253
|
+
* the suffixed names (`data-ln-list-window`) and not only the selector.
|
|
254
|
+
*
|
|
255
|
+
* Internal to ln-core, and deliberately NOT re-exported from index.js: unlike
|
|
256
|
+
* `attrSpec` / `attrEffects`, which take the attribute table a component author
|
|
257
|
+
* writes, this takes a registry entry — a shape no author ever constructs.
|
|
258
|
+
* Publishing it would make the entry shape semver-load-bearing for no caller.
|
|
259
|
+
*
|
|
260
|
+
* @param {Object} entry - a registry entry: { effects, onAttrChange, declared }
|
|
261
|
+
* @returns {Set<string>|null} names to index under, or null for a wildcard —
|
|
262
|
+
* `onAttrChange` without `declared` asks for EVERY data-ln-* mutation and
|
|
263
|
+
* by definition cannot be keyed by name.
|
|
264
|
+
*/
|
|
265
|
+
export function reactiveNames(entry) {
|
|
266
|
+
if (entry.onAttrChange && !entry.declared) return null;
|
|
267
|
+
|
|
268
|
+
const names = new Set();
|
|
269
|
+
if (entry.effects) for (const n in entry.effects) names.add(n);
|
|
270
|
+
if (entry.onAttrChange && entry.declared) for (const n of entry.declared) names.add(n);
|
|
271
|
+
return names;
|
|
272
|
+
}
|
|
273
|
+
|