@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,403 +1,403 @@
|
|
|
1
|
-
import { attrEffects, reactiveNames, validateAttrValue } from './attrs.js';
|
|
2
|
-
import { hasActiveDebug, isDevMode } from './events.js';
|
|
3
|
-
import { guardBody } from './dom.js';
|
|
4
|
-
|
|
5
|
-
// ─── Find Elements ─────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
export function findElements(root, selector, attribute, ComponentClass) {
|
|
8
|
-
if (root.nodeType !== 1) return;
|
|
9
|
-
|
|
10
|
-
// Support both simple attribute names and full CSS selectors
|
|
11
|
-
const isComplex = selector.indexOf('[') !== -1 || selector.indexOf('.') !== -1 || selector.indexOf('#') !== -1;
|
|
12
|
-
const query = isComplex ? selector : '[' + selector + ']';
|
|
13
|
-
|
|
14
|
-
const items = Array.from(root.querySelectorAll(query));
|
|
15
|
-
if (root.matches && root.matches(query)) {
|
|
16
|
-
items.push(root);
|
|
17
|
-
}
|
|
18
|
-
for (const el of items) {
|
|
19
|
-
if (!el[attribute]) {
|
|
20
|
-
if (window.lnCore._persistSink && el.hasAttribute('data-ln-persist')) {
|
|
21
|
-
window.lnCore._persistSink(el, selector);
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
el[attribute] = new ComponentClass(el);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
console.error('[' + attribute + '] init failed', el, err);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ─── Loader Gate State & Primitives ──────────────────────
|
|
33
|
-
if (typeof window !== 'undefined') {
|
|
34
|
-
window.lnCore = window.lnCore || {};
|
|
35
|
-
window.lnCore._bootHolds = window.lnCore._bootHolds || 0;
|
|
36
|
-
window.lnCore._bootQueue = window.lnCore._bootQueue || [];
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function holdInit() {
|
|
40
|
-
if (typeof window !== 'undefined') {
|
|
41
|
-
window.lnCore = window.lnCore || {};
|
|
42
|
-
window.lnCore._bootHolds = (window.lnCore._bootHolds || 0) + 1;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function releaseInit() {
|
|
47
|
-
if (typeof window !== 'undefined') {
|
|
48
|
-
window.lnCore = window.lnCore || {};
|
|
49
|
-
window.lnCore._bootHolds = Math.max(0, (window.lnCore._bootHolds || 0) - 1);
|
|
50
|
-
if (window.lnCore._bootHolds === 0 && window.lnCore._bootQueue) {
|
|
51
|
-
const queue = window.lnCore._bootQueue;
|
|
52
|
-
window.lnCore._bootQueue = [];
|
|
53
|
-
for (let i = 0; i < queue.length; i++) {
|
|
54
|
-
queue[i]();
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function pendingCount() {
|
|
61
|
-
return (typeof window !== 'undefined' && window.lnCore) ? (window.lnCore._bootHolds || 0) : 0;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function queueBoot(fn) {
|
|
65
|
-
if (typeof window !== 'undefined') {
|
|
66
|
-
window.lnCore = window.lnCore || {};
|
|
67
|
-
window.lnCore._bootHolds = window.lnCore._bootHolds || 0;
|
|
68
|
-
window.lnCore._bootQueue = window.lnCore._bootQueue || [];
|
|
69
|
-
|
|
70
|
-
if (window.lnCore._bootHolds > 0) {
|
|
71
|
-
window.lnCore._bootQueue.push(fn);
|
|
72
|
-
} else {
|
|
73
|
-
setTimeout(fn, 0);
|
|
74
|
-
}
|
|
75
|
-
} else {
|
|
76
|
-
fn();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ─── Shared Attribute Observer ─────────────────────────────
|
|
81
|
-
|
|
82
|
-
function _attrRegistry() {
|
|
83
|
-
window.lnCore = window.lnCore || {};
|
|
84
|
-
const registry = window.lnCore._attrRegistry = window.lnCore._attrRegistry
|
|
85
|
-
|| { byAttr: new Map(), reactive: [], persist: [] };
|
|
86
|
-
|
|
87
|
-
registry.byReactive = registry.byReactive || new Map();
|
|
88
|
-
registry.reactiveWildcard = registry.reactiveWildcard || [];
|
|
89
|
-
return registry;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function _registerAttrEntry(entry) {
|
|
93
|
-
const registry = _attrRegistry();
|
|
94
|
-
const observed = entry.observed || [];
|
|
95
|
-
for (let i = 0; i < observed.length; i++) {
|
|
96
|
-
const name = observed[i];
|
|
97
|
-
if (!registry.byAttr.has(name)) registry.byAttr.set(name, []);
|
|
98
|
-
registry.byAttr.get(name).push(entry);
|
|
99
|
-
}
|
|
100
|
-
registry.byDeclaredAttr = registry.byDeclaredAttr || new Map();
|
|
101
|
-
if (entry.attributes) {
|
|
102
|
-
for (const attrName in entry.attributes) {
|
|
103
|
-
if (!registry.byDeclaredAttr.has(attrName)) registry.byDeclaredAttr.set(attrName, []);
|
|
104
|
-
const list = registry.byDeclaredAttr.get(attrName);
|
|
105
|
-
if (!list.some(item => item.componentTag === entry.componentTag)) {
|
|
106
|
-
list.push({
|
|
107
|
-
spec: entry.attributes[attrName],
|
|
108
|
-
componentTag: entry.componentTag
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
if (entry.onAttrChange || entry.effects) {
|
|
114
|
-
registry.reactive.push(entry);
|
|
115
|
-
|
|
116
|
-
const names = reactiveNames(entry);
|
|
117
|
-
if (names === null) {
|
|
118
|
-
registry.reactiveWildcard.push(entry);
|
|
119
|
-
} else {
|
|
120
|
-
for (const name of names) {
|
|
121
|
-
if (!registry.byReactive.has(name)) registry.byReactive.set(name, []);
|
|
122
|
-
registry.byReactive.get(name).push(entry);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (entry.persist) registry.persist.push(entry);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function _runReactive(list, el, name, oldValue) {
|
|
130
|
-
for (let i = 0; i < list.length; i++) {
|
|
131
|
-
const entry = list[i];
|
|
132
|
-
if (!el[entry.attribute]) continue;
|
|
133
|
-
const effect = entry.effects && entry.effects[name];
|
|
134
|
-
if (effect) effect(el, name, oldValue);
|
|
135
|
-
else if (entry.onAttrChange && (!entry.declared || entry.declared.has(name))) entry.onAttrChange(el, name, oldValue);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function _handleAttrMutation(mut) {
|
|
140
|
-
const el = mut.target;
|
|
141
|
-
const name = mut.attributeName;
|
|
142
|
-
|
|
143
|
-
// echo guard — a same-value setAttribute still produces a record
|
|
144
|
-
if (mut.oldValue === el.getAttribute(name)) return;
|
|
145
|
-
|
|
146
|
-
const registry = _attrRegistry();
|
|
147
|
-
const entries = registry.byAttr.get(name);
|
|
148
|
-
|
|
149
|
-
// Dev-mode runtime attribute validation (validates all declared attributes on host or child)
|
|
150
|
-
if (hasActiveDebug() && registry.byDeclaredAttr && registry.byDeclaredAttr.has(name) && isDevMode(el)) {
|
|
151
|
-
const declared = registry.byDeclaredAttr.get(name);
|
|
152
|
-
const val = el.getAttribute(name);
|
|
153
|
-
for (let i = 0; i < declared.length; i++) {
|
|
154
|
-
validateAttrValue(declared[i].spec, val, name, declared[i].componentTag, el);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// debug sink — library attribute mutations only
|
|
159
|
-
if (window.lnCore._debugSink && (name.indexOf('data-ln-') === 0 || entries)) {
|
|
160
|
-
window.lnCore._debugSink('attr', name, el, { oldValue: mut.oldValue, newValue: el.getAttribute(name) });
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Reactive path
|
|
164
|
-
if (name.indexOf('data-ln-') === 0) {
|
|
165
|
-
const byName = registry.byReactive.get(name);
|
|
166
|
-
if (byName) _runReactive(byName, el, name, mut.oldValue);
|
|
167
|
-
|
|
168
|
-
if (registry.reactiveWildcard.length) _runReactive(registry.reactiveWildcard, el, name, mut.oldValue);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (!entries) return;
|
|
172
|
-
for (let i = 0; i < entries.length; i++) {
|
|
173
|
-
const entry = entries[i];
|
|
174
|
-
// Raw handler — components that own their own lifecycle
|
|
175
|
-
if (entry.handler) {
|
|
176
|
-
entry.handler(el, name, mut.oldValue);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
// Legacy path — fallback for lang/datetime foreign attributes
|
|
180
|
-
if (entry.onAttributeChange && el[entry.attribute]) {
|
|
181
|
-
entry.onAttributeChange(el, name);
|
|
182
|
-
} else {
|
|
183
|
-
findElements(el, entry.selector, entry.attribute, entry.ComponentFn);
|
|
184
|
-
if (entry.onInit) entry.onInit(el);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function _ensureAttrObserver() {
|
|
190
|
-
window.lnCore = window.lnCore || {};
|
|
191
|
-
if (window.lnCore._attrObserverBound) return;
|
|
192
|
-
window.lnCore._attrObserverBound = true;
|
|
193
|
-
|
|
194
|
-
guardBody(function () {
|
|
195
|
-
const observer = new MutationObserver(function (mutations) {
|
|
196
|
-
for (let i = 0; i < mutations.length; i++) {
|
|
197
|
-
try {
|
|
198
|
-
_handleAttrMutation(mutations[i]);
|
|
199
|
-
} catch (err) {
|
|
200
|
-
console.error('[ln-core] mutation handler failed', mutations[i].target, err);
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
observer.observe(document.body, {
|
|
205
|
-
attributes: true,
|
|
206
|
-
subtree: true,
|
|
207
|
-
attributeOldValue: true
|
|
208
|
-
});
|
|
209
|
-
}, 'ln-core');
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// ─── Lifecycle Observer & Registry (Single Shared Observer) ───
|
|
213
|
-
|
|
214
|
-
function _lifecycleRegistry() {
|
|
215
|
-
window.lnCore = window.lnCore || {};
|
|
216
|
-
const registry = window.lnCore._lifecycleRegistry = window.lnCore._lifecycleRegistry || [];
|
|
217
|
-
return registry;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function _handleChildListMutation(mutation) {
|
|
221
|
-
const registry = _lifecycleRegistry();
|
|
222
|
-
if (!registry.length) return;
|
|
223
|
-
|
|
224
|
-
if (mutation.target) {
|
|
225
|
-
for (let r = 0; r < registry.length; r++) {
|
|
226
|
-
const entry = registry[r];
|
|
227
|
-
if (entry.onSubtreeChange) {
|
|
228
|
-
const query = entry.query;
|
|
229
|
-
const host = mutation.target.nodeType === 1
|
|
230
|
-
? (mutation.target.matches(query) ? mutation.target : mutation.target.closest(query))
|
|
231
|
-
: (mutation.target.parentElement ? mutation.target.parentElement.closest(query) : null);
|
|
232
|
-
if (host) entry.onSubtreeChange(host, mutation);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
for (let j = 0; j < mutation.addedNodes.length; j++) {
|
|
238
|
-
const node = mutation.addedNodes[j];
|
|
239
|
-
if (node.nodeType === 1) {
|
|
240
|
-
for (let r = 0; r < registry.length; r++) {
|
|
241
|
-
const entry = registry[r];
|
|
242
|
-
findElements(node, entry.selector, entry.attribute, entry.ComponentFn);
|
|
243
|
-
if (entry.onInit) entry.onInit(node);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
for (let j = 0; j < mutation.removedNodes.length; j++) {
|
|
249
|
-
const node = mutation.removedNodes[j];
|
|
250
|
-
if (node.nodeType === 1) {
|
|
251
|
-
for (let r = 0; r < registry.length; r++) {
|
|
252
|
-
const entry = registry[r];
|
|
253
|
-
const query = entry.query;
|
|
254
|
-
const items = Array.from(node.querySelectorAll(query));
|
|
255
|
-
if (node.matches && node.matches(query)) {
|
|
256
|
-
items.push(node);
|
|
257
|
-
}
|
|
258
|
-
for (let k = 0; k < items.length; k++) {
|
|
259
|
-
const item = items[k];
|
|
260
|
-
if (!document.contains(item)) {
|
|
261
|
-
const inst = item[entry.attribute];
|
|
262
|
-
if (inst && typeof inst.destroy === 'function') {
|
|
263
|
-
try {
|
|
264
|
-
inst.destroy();
|
|
265
|
-
} catch (err) {
|
|
266
|
-
console.error('[' + entry.attribute + '] destroy failed', item, err);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
delete item[entry.attribute];
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function _ensureLifecycleObserver() {
|
|
278
|
-
window.lnCore = window.lnCore || {};
|
|
279
|
-
if (window.lnCore._lifecycleObserverBound) return;
|
|
280
|
-
window.lnCore._lifecycleObserverBound = true;
|
|
281
|
-
|
|
282
|
-
guardBody(function () {
|
|
283
|
-
const observer = new MutationObserver(function (mutations) {
|
|
284
|
-
for (let i = 0; i < mutations.length; i++) {
|
|
285
|
-
const mutation = mutations[i];
|
|
286
|
-
if (mutation.type === 'childList') {
|
|
287
|
-
try {
|
|
288
|
-
_handleChildListMutation(mutation);
|
|
289
|
-
} catch (err) {
|
|
290
|
-
console.error('[ln-core] lifecycle handler failed', mutation.target, err);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
observer.observe(document.body, {
|
|
296
|
-
childList: true,
|
|
297
|
-
subtree: true
|
|
298
|
-
});
|
|
299
|
-
}, 'ln-core');
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* For components that own their own lifecycle and only need the attribute
|
|
304
|
-
* half of the shared invariant.
|
|
305
|
-
* @param {string[]} names - attribute names to watch
|
|
306
|
-
* @param {function(Element, string, string):void} handler - (el, attributeName, oldValue)
|
|
307
|
-
*/
|
|
308
|
-
export function observeAttributes(names, handler) {
|
|
309
|
-
_registerAttrEntry({ observed: names, handler: handler });
|
|
310
|
-
_ensureAttrObserver();
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ─── Component Registration ───────────────────────────────
|
|
314
|
-
|
|
315
|
-
export function registerComponent(selector, attribute, ComponentFn, componentTag, options = {}) {
|
|
316
|
-
const extraAttributes = options.extraAttributes || [];
|
|
317
|
-
const onAttributeChange = options.onAttributeChange || null;
|
|
318
|
-
const onSubtreeChange = options.onSubtreeChange || null;
|
|
319
|
-
const onInit = options.onInit || null;
|
|
320
|
-
const onAttrChange = options.onAttrChange || null;
|
|
321
|
-
const effects = options.effects || null;
|
|
322
|
-
const attributes = options.attributes || null;
|
|
323
|
-
const resolvedEffects = attributes ? attrEffects(attributes) : effects;
|
|
324
|
-
const declared = attributes ? new Set(Object.keys(attributes)) : null;
|
|
325
|
-
const persist = options.persist || null;
|
|
326
|
-
|
|
327
|
-
function constructor(domRoot) {
|
|
328
|
-
const root = domRoot || document.body;
|
|
329
|
-
findElements(root, selector, attribute, ComponentFn);
|
|
330
|
-
if (attributes && hasActiveDebug()) {
|
|
331
|
-
for (const attrName in attributes) {
|
|
332
|
-
const spec = attributes[attrName];
|
|
333
|
-
const nodes = Array.from(root.querySelectorAll('[' + attrName + ']'));
|
|
334
|
-
if (root.matches && root.matches('[' + attrName + ']')) nodes.push(root);
|
|
335
|
-
for (let n = 0; n < nodes.length; n++) {
|
|
336
|
-
const node = nodes[n];
|
|
337
|
-
if (isDevMode(node)) {
|
|
338
|
-
validateAttrValue(spec, node.getAttribute(attrName), attrName, componentTag, node);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
if (onInit) onInit(root);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const observedAttributes = [];
|
|
347
|
-
if (selector.indexOf('[') !== -1) {
|
|
348
|
-
const re = /\[([\w-]+)/g;
|
|
349
|
-
let match;
|
|
350
|
-
while ((match = re.exec(selector)) !== null) {
|
|
351
|
-
observedAttributes.push(match[1]);
|
|
352
|
-
}
|
|
353
|
-
} else {
|
|
354
|
-
observedAttributes.push(selector);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
_registerAttrEntry({
|
|
358
|
-
selector: selector,
|
|
359
|
-
attribute: attribute,
|
|
360
|
-
componentTag: componentTag,
|
|
361
|
-
attributes: attributes,
|
|
362
|
-
ComponentFn: ComponentFn,
|
|
363
|
-
onInit: onInit,
|
|
364
|
-
observed: observedAttributes.concat(extraAttributes),
|
|
365
|
-
onAttributeChange: onAttributeChange,
|
|
366
|
-
onAttrChange: onAttrChange,
|
|
367
|
-
effects: resolvedEffects,
|
|
368
|
-
declared: declared,
|
|
369
|
-
persist: persist
|
|
370
|
-
});
|
|
371
|
-
_ensureAttrObserver();
|
|
372
|
-
|
|
373
|
-
const isComplex = selector.indexOf('[') !== -1 || selector.indexOf('.') !== -1 || selector.indexOf('#') !== -1;
|
|
374
|
-
const query = isComplex ? selector : '[' + selector + ']';
|
|
375
|
-
|
|
376
|
-
_lifecycleRegistry().push({
|
|
377
|
-
selector: selector,
|
|
378
|
-
attribute: attribute,
|
|
379
|
-
ComponentFn: ComponentFn,
|
|
380
|
-
onInit: onInit,
|
|
381
|
-
onSubtreeChange: onSubtreeChange,
|
|
382
|
-
query: query
|
|
383
|
-
});
|
|
384
|
-
_ensureLifecycleObserver();
|
|
385
|
-
|
|
386
|
-
window[attribute] = constructor;
|
|
387
|
-
|
|
388
|
-
function boot() {
|
|
389
|
-
if (pendingCount() > 0) {
|
|
390
|
-
queueBoot(function () { constructor(document.body); });
|
|
391
|
-
} else {
|
|
392
|
-
constructor(document.body);
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (document.readyState === 'loading') {
|
|
397
|
-
document.addEventListener('DOMContentLoaded', boot);
|
|
398
|
-
} else {
|
|
399
|
-
boot();
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
return constructor;
|
|
403
|
-
}
|
|
1
|
+
import { attrEffects, reactiveNames, validateAttrValue } from './attrs.js';
|
|
2
|
+
import { hasActiveDebug, isDevMode } from './events.js';
|
|
3
|
+
import { guardBody } from './dom.js';
|
|
4
|
+
|
|
5
|
+
// ─── Find Elements ─────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
export function findElements(root, selector, attribute, ComponentClass) {
|
|
8
|
+
if (root.nodeType !== 1) return;
|
|
9
|
+
|
|
10
|
+
// Support both simple attribute names and full CSS selectors
|
|
11
|
+
const isComplex = selector.indexOf('[') !== -1 || selector.indexOf('.') !== -1 || selector.indexOf('#') !== -1;
|
|
12
|
+
const query = isComplex ? selector : '[' + selector + ']';
|
|
13
|
+
|
|
14
|
+
const items = Array.from(root.querySelectorAll(query));
|
|
15
|
+
if (root.matches && root.matches(query)) {
|
|
16
|
+
items.push(root);
|
|
17
|
+
}
|
|
18
|
+
for (const el of items) {
|
|
19
|
+
if (!el[attribute]) {
|
|
20
|
+
if (window.lnCore._persistSink && el.hasAttribute('data-ln-persist')) {
|
|
21
|
+
window.lnCore._persistSink(el, selector);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
el[attribute] = new ComponentClass(el);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
console.error('[' + attribute + '] init failed', el, err);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ─── Loader Gate State & Primitives ──────────────────────
|
|
33
|
+
if (typeof window !== 'undefined') {
|
|
34
|
+
window.lnCore = window.lnCore || {};
|
|
35
|
+
window.lnCore._bootHolds = window.lnCore._bootHolds || 0;
|
|
36
|
+
window.lnCore._bootQueue = window.lnCore._bootQueue || [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function holdInit() {
|
|
40
|
+
if (typeof window !== 'undefined') {
|
|
41
|
+
window.lnCore = window.lnCore || {};
|
|
42
|
+
window.lnCore._bootHolds = (window.lnCore._bootHolds || 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function releaseInit() {
|
|
47
|
+
if (typeof window !== 'undefined') {
|
|
48
|
+
window.lnCore = window.lnCore || {};
|
|
49
|
+
window.lnCore._bootHolds = Math.max(0, (window.lnCore._bootHolds || 0) - 1);
|
|
50
|
+
if (window.lnCore._bootHolds === 0 && window.lnCore._bootQueue) {
|
|
51
|
+
const queue = window.lnCore._bootQueue;
|
|
52
|
+
window.lnCore._bootQueue = [];
|
|
53
|
+
for (let i = 0; i < queue.length; i++) {
|
|
54
|
+
queue[i]();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function pendingCount() {
|
|
61
|
+
return (typeof window !== 'undefined' && window.lnCore) ? (window.lnCore._bootHolds || 0) : 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function queueBoot(fn) {
|
|
65
|
+
if (typeof window !== 'undefined') {
|
|
66
|
+
window.lnCore = window.lnCore || {};
|
|
67
|
+
window.lnCore._bootHolds = window.lnCore._bootHolds || 0;
|
|
68
|
+
window.lnCore._bootQueue = window.lnCore._bootQueue || [];
|
|
69
|
+
|
|
70
|
+
if (window.lnCore._bootHolds > 0) {
|
|
71
|
+
window.lnCore._bootQueue.push(fn);
|
|
72
|
+
} else {
|
|
73
|
+
setTimeout(fn, 0);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
fn();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Shared Attribute Observer ─────────────────────────────
|
|
81
|
+
|
|
82
|
+
function _attrRegistry() {
|
|
83
|
+
window.lnCore = window.lnCore || {};
|
|
84
|
+
const registry = window.lnCore._attrRegistry = window.lnCore._attrRegistry
|
|
85
|
+
|| { byAttr: new Map(), reactive: [], persist: [] };
|
|
86
|
+
|
|
87
|
+
registry.byReactive = registry.byReactive || new Map();
|
|
88
|
+
registry.reactiveWildcard = registry.reactiveWildcard || [];
|
|
89
|
+
return registry;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function _registerAttrEntry(entry) {
|
|
93
|
+
const registry = _attrRegistry();
|
|
94
|
+
const observed = entry.observed || [];
|
|
95
|
+
for (let i = 0; i < observed.length; i++) {
|
|
96
|
+
const name = observed[i];
|
|
97
|
+
if (!registry.byAttr.has(name)) registry.byAttr.set(name, []);
|
|
98
|
+
registry.byAttr.get(name).push(entry);
|
|
99
|
+
}
|
|
100
|
+
registry.byDeclaredAttr = registry.byDeclaredAttr || new Map();
|
|
101
|
+
if (entry.attributes) {
|
|
102
|
+
for (const attrName in entry.attributes) {
|
|
103
|
+
if (!registry.byDeclaredAttr.has(attrName)) registry.byDeclaredAttr.set(attrName, []);
|
|
104
|
+
const list = registry.byDeclaredAttr.get(attrName);
|
|
105
|
+
if (!list.some(item => item.componentTag === entry.componentTag)) {
|
|
106
|
+
list.push({
|
|
107
|
+
spec: entry.attributes[attrName],
|
|
108
|
+
componentTag: entry.componentTag
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (entry.onAttrChange || entry.effects) {
|
|
114
|
+
registry.reactive.push(entry);
|
|
115
|
+
|
|
116
|
+
const names = reactiveNames(entry);
|
|
117
|
+
if (names === null) {
|
|
118
|
+
registry.reactiveWildcard.push(entry);
|
|
119
|
+
} else {
|
|
120
|
+
for (const name of names) {
|
|
121
|
+
if (!registry.byReactive.has(name)) registry.byReactive.set(name, []);
|
|
122
|
+
registry.byReactive.get(name).push(entry);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (entry.persist) registry.persist.push(entry);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _runReactive(list, el, name, oldValue) {
|
|
130
|
+
for (let i = 0; i < list.length; i++) {
|
|
131
|
+
const entry = list[i];
|
|
132
|
+
if (!el[entry.attribute]) continue;
|
|
133
|
+
const effect = entry.effects && entry.effects[name];
|
|
134
|
+
if (effect) effect(el, name, oldValue);
|
|
135
|
+
else if (entry.onAttrChange && (!entry.declared || entry.declared.has(name))) entry.onAttrChange(el, name, oldValue);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _handleAttrMutation(mut) {
|
|
140
|
+
const el = mut.target;
|
|
141
|
+
const name = mut.attributeName;
|
|
142
|
+
|
|
143
|
+
// echo guard — a same-value setAttribute still produces a record
|
|
144
|
+
if (mut.oldValue === el.getAttribute(name)) return;
|
|
145
|
+
|
|
146
|
+
const registry = _attrRegistry();
|
|
147
|
+
const entries = registry.byAttr.get(name);
|
|
148
|
+
|
|
149
|
+
// Dev-mode runtime attribute validation (validates all declared attributes on host or child)
|
|
150
|
+
if (hasActiveDebug() && registry.byDeclaredAttr && registry.byDeclaredAttr.has(name) && isDevMode(el)) {
|
|
151
|
+
const declared = registry.byDeclaredAttr.get(name);
|
|
152
|
+
const val = el.getAttribute(name);
|
|
153
|
+
for (let i = 0; i < declared.length; i++) {
|
|
154
|
+
validateAttrValue(declared[i].spec, val, name, declared[i].componentTag, el);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// debug sink — library attribute mutations only
|
|
159
|
+
if (window.lnCore._debugSink && (name.indexOf('data-ln-') === 0 || entries)) {
|
|
160
|
+
window.lnCore._debugSink('attr', name, el, { oldValue: mut.oldValue, newValue: el.getAttribute(name) });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Reactive path
|
|
164
|
+
if (name.indexOf('data-ln-') === 0) {
|
|
165
|
+
const byName = registry.byReactive.get(name);
|
|
166
|
+
if (byName) _runReactive(byName, el, name, mut.oldValue);
|
|
167
|
+
|
|
168
|
+
if (registry.reactiveWildcard.length) _runReactive(registry.reactiveWildcard, el, name, mut.oldValue);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!entries) return;
|
|
172
|
+
for (let i = 0; i < entries.length; i++) {
|
|
173
|
+
const entry = entries[i];
|
|
174
|
+
// Raw handler — components that own their own lifecycle
|
|
175
|
+
if (entry.handler) {
|
|
176
|
+
entry.handler(el, name, mut.oldValue);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
// Legacy path — fallback for lang/datetime foreign attributes
|
|
180
|
+
if (entry.onAttributeChange && el[entry.attribute]) {
|
|
181
|
+
entry.onAttributeChange(el, name);
|
|
182
|
+
} else {
|
|
183
|
+
findElements(el, entry.selector, entry.attribute, entry.ComponentFn);
|
|
184
|
+
if (entry.onInit) entry.onInit(el);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function _ensureAttrObserver() {
|
|
190
|
+
window.lnCore = window.lnCore || {};
|
|
191
|
+
if (window.lnCore._attrObserverBound) return;
|
|
192
|
+
window.lnCore._attrObserverBound = true;
|
|
193
|
+
|
|
194
|
+
guardBody(function () {
|
|
195
|
+
const observer = new MutationObserver(function (mutations) {
|
|
196
|
+
for (let i = 0; i < mutations.length; i++) {
|
|
197
|
+
try {
|
|
198
|
+
_handleAttrMutation(mutations[i]);
|
|
199
|
+
} catch (err) {
|
|
200
|
+
console.error('[ln-core] mutation handler failed', mutations[i].target, err);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
observer.observe(document.body, {
|
|
205
|
+
attributes: true,
|
|
206
|
+
subtree: true,
|
|
207
|
+
attributeOldValue: true
|
|
208
|
+
});
|
|
209
|
+
}, 'ln-core');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ─── Lifecycle Observer & Registry (Single Shared Observer) ───
|
|
213
|
+
|
|
214
|
+
function _lifecycleRegistry() {
|
|
215
|
+
window.lnCore = window.lnCore || {};
|
|
216
|
+
const registry = window.lnCore._lifecycleRegistry = window.lnCore._lifecycleRegistry || [];
|
|
217
|
+
return registry;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _handleChildListMutation(mutation) {
|
|
221
|
+
const registry = _lifecycleRegistry();
|
|
222
|
+
if (!registry.length) return;
|
|
223
|
+
|
|
224
|
+
if (mutation.target) {
|
|
225
|
+
for (let r = 0; r < registry.length; r++) {
|
|
226
|
+
const entry = registry[r];
|
|
227
|
+
if (entry.onSubtreeChange) {
|
|
228
|
+
const query = entry.query;
|
|
229
|
+
const host = mutation.target.nodeType === 1
|
|
230
|
+
? (mutation.target.matches(query) ? mutation.target : mutation.target.closest(query))
|
|
231
|
+
: (mutation.target.parentElement ? mutation.target.parentElement.closest(query) : null);
|
|
232
|
+
if (host) entry.onSubtreeChange(host, mutation);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
for (let j = 0; j < mutation.addedNodes.length; j++) {
|
|
238
|
+
const node = mutation.addedNodes[j];
|
|
239
|
+
if (node.nodeType === 1) {
|
|
240
|
+
for (let r = 0; r < registry.length; r++) {
|
|
241
|
+
const entry = registry[r];
|
|
242
|
+
findElements(node, entry.selector, entry.attribute, entry.ComponentFn);
|
|
243
|
+
if (entry.onInit) entry.onInit(node);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (let j = 0; j < mutation.removedNodes.length; j++) {
|
|
249
|
+
const node = mutation.removedNodes[j];
|
|
250
|
+
if (node.nodeType === 1) {
|
|
251
|
+
for (let r = 0; r < registry.length; r++) {
|
|
252
|
+
const entry = registry[r];
|
|
253
|
+
const query = entry.query;
|
|
254
|
+
const items = Array.from(node.querySelectorAll(query));
|
|
255
|
+
if (node.matches && node.matches(query)) {
|
|
256
|
+
items.push(node);
|
|
257
|
+
}
|
|
258
|
+
for (let k = 0; k < items.length; k++) {
|
|
259
|
+
const item = items[k];
|
|
260
|
+
if (!document.contains(item)) {
|
|
261
|
+
const inst = item[entry.attribute];
|
|
262
|
+
if (inst && typeof inst.destroy === 'function') {
|
|
263
|
+
try {
|
|
264
|
+
inst.destroy();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
console.error('[' + entry.attribute + '] destroy failed', item, err);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
delete item[entry.attribute];
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function _ensureLifecycleObserver() {
|
|
278
|
+
window.lnCore = window.lnCore || {};
|
|
279
|
+
if (window.lnCore._lifecycleObserverBound) return;
|
|
280
|
+
window.lnCore._lifecycleObserverBound = true;
|
|
281
|
+
|
|
282
|
+
guardBody(function () {
|
|
283
|
+
const observer = new MutationObserver(function (mutations) {
|
|
284
|
+
for (let i = 0; i < mutations.length; i++) {
|
|
285
|
+
const mutation = mutations[i];
|
|
286
|
+
if (mutation.type === 'childList') {
|
|
287
|
+
try {
|
|
288
|
+
_handleChildListMutation(mutation);
|
|
289
|
+
} catch (err) {
|
|
290
|
+
console.error('[ln-core] lifecycle handler failed', mutation.target, err);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
observer.observe(document.body, {
|
|
296
|
+
childList: true,
|
|
297
|
+
subtree: true
|
|
298
|
+
});
|
|
299
|
+
}, 'ln-core');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* For components that own their own lifecycle and only need the attribute
|
|
304
|
+
* half of the shared invariant.
|
|
305
|
+
* @param {string[]} names - attribute names to watch
|
|
306
|
+
* @param {function(Element, string, string):void} handler - (el, attributeName, oldValue)
|
|
307
|
+
*/
|
|
308
|
+
export function observeAttributes(names, handler) {
|
|
309
|
+
_registerAttrEntry({ observed: names, handler: handler });
|
|
310
|
+
_ensureAttrObserver();
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ─── Component Registration ───────────────────────────────
|
|
314
|
+
|
|
315
|
+
export function registerComponent(selector, attribute, ComponentFn, componentTag, options = {}) {
|
|
316
|
+
const extraAttributes = options.extraAttributes || [];
|
|
317
|
+
const onAttributeChange = options.onAttributeChange || null;
|
|
318
|
+
const onSubtreeChange = options.onSubtreeChange || null;
|
|
319
|
+
const onInit = options.onInit || null;
|
|
320
|
+
const onAttrChange = options.onAttrChange || null;
|
|
321
|
+
const effects = options.effects || null;
|
|
322
|
+
const attributes = options.attributes || null;
|
|
323
|
+
const resolvedEffects = attributes ? attrEffects(attributes) : effects;
|
|
324
|
+
const declared = attributes ? new Set(Object.keys(attributes)) : null;
|
|
325
|
+
const persist = options.persist || null;
|
|
326
|
+
|
|
327
|
+
function constructor(domRoot) {
|
|
328
|
+
const root = domRoot || document.body;
|
|
329
|
+
findElements(root, selector, attribute, ComponentFn);
|
|
330
|
+
if (attributes && hasActiveDebug()) {
|
|
331
|
+
for (const attrName in attributes) {
|
|
332
|
+
const spec = attributes[attrName];
|
|
333
|
+
const nodes = Array.from(root.querySelectorAll('[' + attrName + ']'));
|
|
334
|
+
if (root.matches && root.matches('[' + attrName + ']')) nodes.push(root);
|
|
335
|
+
for (let n = 0; n < nodes.length; n++) {
|
|
336
|
+
const node = nodes[n];
|
|
337
|
+
if (isDevMode(node)) {
|
|
338
|
+
validateAttrValue(spec, node.getAttribute(attrName), attrName, componentTag, node);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (onInit) onInit(root);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const observedAttributes = [];
|
|
347
|
+
if (selector.indexOf('[') !== -1) {
|
|
348
|
+
const re = /\[([\w-]+)/g;
|
|
349
|
+
let match;
|
|
350
|
+
while ((match = re.exec(selector)) !== null) {
|
|
351
|
+
observedAttributes.push(match[1]);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
observedAttributes.push(selector);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_registerAttrEntry({
|
|
358
|
+
selector: selector,
|
|
359
|
+
attribute: attribute,
|
|
360
|
+
componentTag: componentTag,
|
|
361
|
+
attributes: attributes,
|
|
362
|
+
ComponentFn: ComponentFn,
|
|
363
|
+
onInit: onInit,
|
|
364
|
+
observed: observedAttributes.concat(extraAttributes),
|
|
365
|
+
onAttributeChange: onAttributeChange,
|
|
366
|
+
onAttrChange: onAttrChange,
|
|
367
|
+
effects: resolvedEffects,
|
|
368
|
+
declared: declared,
|
|
369
|
+
persist: persist
|
|
370
|
+
});
|
|
371
|
+
_ensureAttrObserver();
|
|
372
|
+
|
|
373
|
+
const isComplex = selector.indexOf('[') !== -1 || selector.indexOf('.') !== -1 || selector.indexOf('#') !== -1;
|
|
374
|
+
const query = isComplex ? selector : '[' + selector + ']';
|
|
375
|
+
|
|
376
|
+
_lifecycleRegistry().push({
|
|
377
|
+
selector: selector,
|
|
378
|
+
attribute: attribute,
|
|
379
|
+
ComponentFn: ComponentFn,
|
|
380
|
+
onInit: onInit,
|
|
381
|
+
onSubtreeChange: onSubtreeChange,
|
|
382
|
+
query: query
|
|
383
|
+
});
|
|
384
|
+
_ensureLifecycleObserver();
|
|
385
|
+
|
|
386
|
+
window[attribute] = constructor;
|
|
387
|
+
|
|
388
|
+
function boot() {
|
|
389
|
+
if (pendingCount() > 0) {
|
|
390
|
+
queueBoot(function () { constructor(document.body); });
|
|
391
|
+
} else {
|
|
392
|
+
constructor(document.body);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (document.readyState === 'loading') {
|
|
397
|
+
document.addEventListener('DOMContentLoaded', boot);
|
|
398
|
+
} else {
|
|
399
|
+
boot();
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return constructor;
|
|
403
|
+
}
|