lightview 2.0.6 → 2.0.8
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/README.md +1 -1
- package/docs/about.html +1 -7
- package/docs/assets/js/examplify.js +6 -0
- package/docs/getting-started/index.html +1 -1
- package/docs/index.html +0 -4
- package/lightview-router.js +94 -299
- package/lightview-x.js +174 -504
- package/lightview.js +123 -107
- package/package.json +1 -1
package/lightview-x.js
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
(() => {
|
|
2
|
+
/**
|
|
3
|
+
* LIGHTVIEW-X
|
|
4
|
+
* Hypermedia and Extended Reactivity for Lightview.
|
|
5
|
+
*
|
|
6
|
+
* Adds:
|
|
7
|
+
* - src attribute fetching (HTMX-style loading)
|
|
8
|
+
* - href navigation (Single Page App behavior for non-standard links)
|
|
9
|
+
* - DOM-to-element conversion (Template literals support)
|
|
10
|
+
* - Object DOM syntax
|
|
11
|
+
* - Deeply reactive state
|
|
12
|
+
* - CSS Shadow DOM integration
|
|
13
|
+
*/
|
|
2
14
|
// ============= LIGHTVIEW-X =============
|
|
3
15
|
// Hypermedia extension for Lightview
|
|
4
16
|
// Adds: src attribute fetching, href navigation, DOM-to-element conversion, template literals, named registries, Object DOM syntax
|
|
@@ -7,98 +19,33 @@
|
|
|
7
19
|
const isStandardSrcTag = (tagName) => STANDARD_SRC_TAGS.includes(tagName) || tagName.startsWith('lv-');
|
|
8
20
|
const STANDARD_HREF_TAGS = ['a', 'area', 'base', 'link'];
|
|
9
21
|
|
|
10
|
-
|
|
11
|
-
* Check if a string is a valid HTML tag name
|
|
12
|
-
* @param {string} name - The tag name to check
|
|
13
|
-
* @returns {boolean}
|
|
14
|
-
*/
|
|
15
|
-
const isValidTagName = (name) => {
|
|
16
|
-
if (typeof name !== 'string' || name.length === 0 || name === 'children') {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
// Non-strict mode: accept anything that looks reasonable
|
|
20
|
-
return true;
|
|
21
|
-
};
|
|
22
|
+
const isValidTagName = (name) => typeof name === 'string' && name.length > 0 && name !== 'children';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
|
-
*
|
|
25
|
-
* Object DOM: { div: { class: "foo", children: [...] } }
|
|
26
|
-
* vDOM: { tag: "div", attributes: {...}, children: [...] }
|
|
27
|
-
* @param {any} obj
|
|
28
|
-
* @returns {boolean}
|
|
25
|
+
* Detects if an object follows the Object DOM syntax: { tag: { attr: val, children: [...] } }
|
|
29
26
|
*/
|
|
30
27
|
const isObjectDOM = (obj) => {
|
|
31
|
-
if (typeof obj !== 'object' || obj === null || Array.isArray(obj)) return false;
|
|
32
|
-
if (obj.tag || obj.domEl) return false; // Already vDOM or live element
|
|
33
|
-
|
|
28
|
+
if (typeof obj !== 'object' || obj === null || Array.isArray(obj) || obj.tag || obj.domEl) return false;
|
|
34
29
|
const keys = Object.keys(obj);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// Object DOM has exactly one key (the tag name or component name) whose value is an object
|
|
38
|
-
// That object may contain attributes and optionally a 'children' property
|
|
39
|
-
if (keys.length === 1) {
|
|
40
|
-
const tag = keys[0];
|
|
41
|
-
const value = obj[tag];
|
|
42
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false;
|
|
43
|
-
|
|
44
|
-
// Otherwise check if it's a valid tag name
|
|
45
|
-
return isValidTagName(tag);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return false;
|
|
30
|
+
return keys.length === 1 && isValidTagName(keys[0]) && typeof obj[keys[0]] === 'object';
|
|
49
31
|
};
|
|
50
32
|
|
|
51
33
|
/**
|
|
52
|
-
*
|
|
53
|
-
* @param {any} obj - Object in Object DOM format or any child
|
|
54
|
-
* @returns {any} - Converted to vDOM format
|
|
34
|
+
* Converts Object DOM syntax into standard Lightview VDOM { tag, attributes, children }
|
|
55
35
|
*/
|
|
56
36
|
const convertObjectDOM = (obj) => {
|
|
57
|
-
// Not an object or array - return as-is (strings, numbers, functions, etc.)
|
|
58
37
|
if (typeof obj !== 'object' || obj === null) return obj;
|
|
38
|
+
if (Array.isArray(obj)) return obj.map(convertObjectDOM);
|
|
39
|
+
if (obj.tag) return { ...obj, children: obj.children ? convertObjectDOM(obj.children) : [] };
|
|
40
|
+
if (obj.domEl || !isObjectDOM(obj)) return obj;
|
|
59
41
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// Already vDOM format - recurse into children only
|
|
66
|
-
if (obj.tag) {
|
|
67
|
-
return {
|
|
68
|
-
...obj,
|
|
69
|
-
children: obj.children ? convertObjectDOM(obj.children) : []
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Live element - pass through
|
|
74
|
-
if (obj.domEl) return obj;
|
|
75
|
-
|
|
76
|
-
// Check for Object DOM syntax
|
|
77
|
-
if (isObjectDOM(obj)) {
|
|
78
|
-
const tagKey = Object.keys(obj)[0];
|
|
79
|
-
const content = obj[tagKey];
|
|
80
|
-
|
|
81
|
-
// Access custom registry via Lightview.tags._customTags if available
|
|
82
|
-
let tag = tagKey;
|
|
83
|
-
if (typeof window !== 'undefined' && window.Lightview && window.Lightview.tags) {
|
|
84
|
-
const customTags = window.Lightview.tags._customTags || {};
|
|
85
|
-
if (customTags[tagKey]) {
|
|
86
|
-
tag = customTags[tagKey];
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Extract children and attributes
|
|
91
|
-
const { children, ...attributes } = content;
|
|
92
|
-
|
|
93
|
-
return {
|
|
94
|
-
tag,
|
|
95
|
-
attributes,
|
|
96
|
-
children: children ? convertObjectDOM(children) : []
|
|
97
|
-
};
|
|
98
|
-
}
|
|
42
|
+
const tagKey = Object.keys(obj)[0];
|
|
43
|
+
const content = obj[tagKey];
|
|
44
|
+
const LV = window.Lightview;
|
|
45
|
+
const tag = (LV?.tags?._customTags?.[tagKey]) || tagKey;
|
|
46
|
+
const { children, ...attributes } = content;
|
|
99
47
|
|
|
100
|
-
|
|
101
|
-
return obj;
|
|
48
|
+
return { tag, attributes, children: children ? convertObjectDOM(children) : [] };
|
|
102
49
|
};
|
|
103
50
|
|
|
104
51
|
// ============= COMPONENT CONFIGURATION =============
|
|
@@ -180,7 +127,7 @@
|
|
|
180
127
|
*/
|
|
181
128
|
const setTheme = (themeName) => {
|
|
182
129
|
if (!themeName) return;
|
|
183
|
-
|
|
130
|
+
|
|
184
131
|
// Determine base theme (light or dark) for the main document
|
|
185
132
|
// const darkThemes = ['dark', 'aqua', 'black', 'business', 'coffee', 'dim', 'dracula', 'forest', 'halloween', 'luxury', 'night', 'sunset', 'synthwave'];
|
|
186
133
|
// const baseTheme = darkThemes.includes(themeName) ? 'dark' : 'light';
|
|
@@ -362,6 +309,10 @@
|
|
|
362
309
|
const stateRegistry = new Map();
|
|
363
310
|
|
|
364
311
|
// ============= STATE (Deep Reactivity) =============
|
|
312
|
+
/**
|
|
313
|
+
* Provides deeply reactive state by wrapping objects/arrays in Proxies.
|
|
314
|
+
* Automatically tracks changes via signals.
|
|
315
|
+
*/
|
|
365
316
|
// Build method lists dynamically from prototypes
|
|
366
317
|
const protoMethods = (proto, test) => Object.getOwnPropertyNames(proto).filter(k => typeof proto[k] === 'function' && test(k));
|
|
367
318
|
const DATE_TRACKING = protoMethods(Date.prototype, k => /^(to|get|valueOf)/.test(k));
|
|
@@ -385,26 +336,25 @@
|
|
|
385
336
|
return v;
|
|
386
337
|
};
|
|
387
338
|
|
|
388
|
-
// Shared proxy handler helpers (uses Lightview.signal internally)
|
|
389
339
|
const proxyGet = (target, prop, receiver, signals) => {
|
|
390
340
|
const LV = window.Lightview;
|
|
391
|
-
if (!signals.has(prop))
|
|
392
|
-
signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
393
|
-
}
|
|
341
|
+
if (!signals.has(prop)) signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
394
342
|
const val = signals.get(prop).value;
|
|
395
343
|
return typeof val === 'object' && val !== null ? state(val) : val;
|
|
396
344
|
};
|
|
397
345
|
|
|
398
346
|
const proxySet = (target, prop, value, receiver, signals) => {
|
|
399
347
|
const LV = window.Lightview;
|
|
400
|
-
if (!signals.has(prop))
|
|
401
|
-
signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
402
|
-
}
|
|
348
|
+
if (!signals.has(prop)) signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
403
349
|
const success = Reflect.set(target, prop, value, receiver);
|
|
404
350
|
if (success) signals.get(prop).value = value;
|
|
405
351
|
return success;
|
|
406
352
|
};
|
|
407
353
|
|
|
354
|
+
/**
|
|
355
|
+
* Creates a specialized proxy for complex objects like Date and Array
|
|
356
|
+
* that require monitoring specific properties (e.g., 'length' or 'time').
|
|
357
|
+
*/
|
|
408
358
|
const createSpecialProxy = (obj, monitor, trackingProps = []) => {
|
|
409
359
|
const LV = window.Lightview;
|
|
410
360
|
// Get or create the signals map for this object
|
|
@@ -508,85 +458,43 @@
|
|
|
508
458
|
});
|
|
509
459
|
};
|
|
510
460
|
|
|
511
|
-
/**
|
|
512
|
-
* Create a deeply reactive proxy for an object or array
|
|
513
|
-
* @param {Object|Array} obj - The object to make reactive
|
|
514
|
-
* @returns {Proxy} - A reactive proxy
|
|
515
|
-
*/
|
|
516
461
|
const state = (obj, optionsOrName) => {
|
|
517
462
|
if (typeof obj !== 'object' || obj === null) return obj;
|
|
518
463
|
|
|
519
|
-
|
|
464
|
+
const name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
|
|
520
465
|
const storage = optionsOrName?.storage;
|
|
521
466
|
|
|
522
|
-
let loadedData = null;
|
|
523
467
|
if (name && storage) {
|
|
524
468
|
try {
|
|
525
469
|
const item = storage.getItem(name);
|
|
526
|
-
if (item)
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
let proxy;
|
|
531
|
-
if (stateCache.has(obj)) {
|
|
532
|
-
proxy = stateCache.get(obj);
|
|
533
|
-
// If we have loaded data for an existing proxy, update it
|
|
534
|
-
if (loadedData) {
|
|
535
|
-
if (Array.isArray(proxy) && Array.isArray(loadedData)) {
|
|
536
|
-
proxy.length = 0;
|
|
537
|
-
proxy.push(...loadedData);
|
|
538
|
-
} else if (!Array.isArray(proxy) && !Array.isArray(loadedData)) {
|
|
539
|
-
Object.assign(proxy, loadedData);
|
|
470
|
+
if (item) {
|
|
471
|
+
const loaded = JSON.parse(item);
|
|
472
|
+
Array.isArray(obj) && Array.isArray(loaded) ? (obj.length = 0, obj.push(...loaded)) : Object.assign(obj, loaded);
|
|
540
473
|
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
// Apply loaded data to raw object before proxying (if no proxy yet)
|
|
544
|
-
if (loadedData) {
|
|
545
|
-
if (Array.isArray(obj) && Array.isArray(loadedData)) {
|
|
546
|
-
obj.length = 0;
|
|
547
|
-
obj.push(...loadedData);
|
|
548
|
-
} else if (!Array.isArray(obj) && !Array.isArray(loadedData)) {
|
|
549
|
-
Object.assign(obj, loadedData);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// Don't proxy objects with internal slots (RegExp, Map, Set, etc.)
|
|
554
|
-
const isSpecialObject = obj instanceof RegExp ||
|
|
555
|
-
obj instanceof Map || obj instanceof Set ||
|
|
556
|
-
obj instanceof WeakMap || obj instanceof WeakSet;
|
|
557
|
-
|
|
558
|
-
if (isSpecialObject) return obj;
|
|
474
|
+
} catch (e) { /* Storage access denied or corrupted JSON */ }
|
|
475
|
+
}
|
|
559
476
|
|
|
560
|
-
|
|
561
|
-
|
|
477
|
+
let proxy = stateCache.get(obj);
|
|
478
|
+
if (!proxy) {
|
|
479
|
+
const isArray = Array.isArray(obj), isDate = obj instanceof Date;
|
|
480
|
+
const isSpecial = isArray || isDate;
|
|
562
481
|
const monitor = isArray ? "length" : isDate ? "getTime" : null;
|
|
563
482
|
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
return
|
|
568
|
-
}
|
|
569
|
-
set(
|
|
570
|
-
|
|
571
|
-
return proxySet(target, prop, value, receiver, signals);
|
|
572
|
-
}
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
stateCache.set(obj, proxy);
|
|
483
|
+
if (isSpecial || !(obj instanceof RegExp || obj instanceof Map || obj instanceof Set || obj instanceof WeakMap || obj instanceof WeakSet)) {
|
|
484
|
+
proxy = isSpecial ? createSpecialProxy(obj, monitor) : new Proxy(obj, {
|
|
485
|
+
get(t, p, r) { return proxyGet(t, p, r, getOrSet(stateSignals, t, () => new Map())); },
|
|
486
|
+
set(t, p, v, r) { return proxySet(t, p, v, r, getOrSet(stateSignals, t, () => new Map())); }
|
|
487
|
+
});
|
|
488
|
+
stateCache.set(obj, proxy);
|
|
489
|
+
} else return obj;
|
|
576
490
|
}
|
|
577
491
|
|
|
578
|
-
if (name && storage &&
|
|
492
|
+
if (name && storage && window.Lightview?.effect) {
|
|
579
493
|
window.Lightview.effect(() => {
|
|
580
|
-
try {
|
|
581
|
-
const json = JSON.stringify(proxy);
|
|
582
|
-
storage.setItem(name, json);
|
|
583
|
-
} catch (e) { /* ignore */ }
|
|
494
|
+
try { storage.setItem(name, JSON.stringify(proxy)); } catch (e) { /* Persistence failed */ }
|
|
584
495
|
});
|
|
585
496
|
}
|
|
586
|
-
|
|
587
|
-
if (name) {
|
|
588
|
-
stateRegistry.set(name, proxy);
|
|
589
|
-
}
|
|
497
|
+
if (name) stateRegistry.set(name, proxy);
|
|
590
498
|
return proxy;
|
|
591
499
|
};
|
|
592
500
|
|
|
@@ -597,76 +505,57 @@
|
|
|
597
505
|
return stateRegistry.get(name);
|
|
598
506
|
};
|
|
599
507
|
|
|
600
|
-
// Template
|
|
601
|
-
const
|
|
508
|
+
// Template compilation: unified logic for creating reactive functions
|
|
509
|
+
const compileTemplate = (code) => {
|
|
510
|
+
try {
|
|
511
|
+
const isSingle = code.trim().startsWith('${') && code.trim().endsWith('}') && !code.trim().includes('${', 2);
|
|
512
|
+
const body = isSingle ? 'return ' + code.trim().slice(2, -1) : 'return `' + code.replace(/\\/g, '\\\\').replace(/`/g, '\\`') + '`';
|
|
513
|
+
return new Function('state', 'signal', body);
|
|
514
|
+
} catch (e) {
|
|
515
|
+
return () => "";
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const processTemplateChild = (child, LV) => {
|
|
602
520
|
if (typeof child === 'string' && child.includes('${')) {
|
|
603
|
-
const
|
|
604
|
-
return () =>
|
|
605
|
-
try {
|
|
606
|
-
return new Function('state', 'signal', 'return `' + template + '`')(state, signal);
|
|
607
|
-
} catch (e) {
|
|
608
|
-
return "";
|
|
609
|
-
}
|
|
610
|
-
};
|
|
521
|
+
const fn = compileTemplate(child);
|
|
522
|
+
return () => fn(LV.state, LV.signal);
|
|
611
523
|
}
|
|
612
|
-
return child;
|
|
524
|
+
return child;
|
|
613
525
|
};
|
|
614
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Converts standard DOM nodes into Lightview reactive elements.
|
|
529
|
+
* This is used to transform HTML templates (with template literals) into live VDOM.
|
|
530
|
+
*/
|
|
615
531
|
const domToElements = (domNodes, element, parentTagName = null) => {
|
|
616
|
-
|
|
617
|
-
const
|
|
532
|
+
const isRaw = parentTagName === 'script' || parentTagName === 'style';
|
|
533
|
+
const LV = window.Lightview;
|
|
618
534
|
|
|
619
535
|
return domNodes.map(node => {
|
|
620
536
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
621
537
|
const text = node.textContent;
|
|
622
|
-
|
|
623
|
-
// For script/style content, always return raw text
|
|
624
|
-
if (isRawContent) {
|
|
625
|
-
return text;
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
// Skip formatting whitespace/empty text nodes if they don't contain template syntax
|
|
538
|
+
if (isRaw) return text;
|
|
629
539
|
if (!text.trim() && !text.includes('${')) return null;
|
|
630
|
-
|
|
631
540
|
if (text.includes('${')) {
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
const LV = window.Lightview;
|
|
635
|
-
return new Function('state', 'signal', 'return `' + text + '`')(LV.state, LV.signal);
|
|
636
|
-
} catch (e) {
|
|
637
|
-
return "";
|
|
638
|
-
}
|
|
639
|
-
};
|
|
541
|
+
const fn = compileTemplate(text);
|
|
542
|
+
return () => fn(LV.state, LV.signal);
|
|
640
543
|
}
|
|
641
544
|
return text;
|
|
642
545
|
}
|
|
643
546
|
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
644
547
|
|
|
645
|
-
const tagName = node.tagName.toLowerCase();
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
// Skip template processing for script/style attributes too
|
|
649
|
-
const skipTemplateProcessing = tagName === 'script' || tagName === 'style';
|
|
548
|
+
const tagName = node.tagName.toLowerCase(), attributes = {};
|
|
549
|
+
const skip = tagName === 'script' || tagName === 'style';
|
|
650
550
|
|
|
651
551
|
for (let attr of node.attributes) {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
return new Function('state', 'signal', 'return `' + value + '`')(LV.state, LV.signal);
|
|
658
|
-
} catch (e) {
|
|
659
|
-
return "";
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
} else {
|
|
663
|
-
attributes[attr.name] = value;
|
|
664
|
-
}
|
|
552
|
+
const val = attr.value;
|
|
553
|
+
attributes[attr.name] = (!skip && val.includes('${')) ? (() => {
|
|
554
|
+
const fn = compileTemplate(val);
|
|
555
|
+
return () => fn(LV.state, LV.signal);
|
|
556
|
+
})() : val;
|
|
665
557
|
}
|
|
666
|
-
|
|
667
|
-
// Pass the current tag name so children know their parent context
|
|
668
|
-
const children = domToElements(Array.from(node.childNodes), element, tagName);
|
|
669
|
-
return element(tagName, attributes, children);
|
|
558
|
+
return element(tagName, attributes, domToElements(Array.from(node.childNodes), element, tagName));
|
|
670
559
|
}).filter(n => n !== null);
|
|
671
560
|
};
|
|
672
561
|
|
|
@@ -763,286 +652,98 @@
|
|
|
763
652
|
return nodesToRemove.length > 0;
|
|
764
653
|
};
|
|
765
654
|
|
|
766
|
-
const
|
|
767
|
-
|
|
768
|
-
|
|
655
|
+
const insert = (elements, parent, location, markerId, { element, setupChildren }) => {
|
|
656
|
+
const isSibling = location === 'beforebegin' || location === 'afterend';
|
|
657
|
+
const isOuter = location === 'outerhtml';
|
|
658
|
+
const target = (isSibling || isOuter) ? parent.parentElement : parent;
|
|
659
|
+
if (!target) return console.warn(`LightviewX: No parent for ${location}`);
|
|
660
|
+
|
|
661
|
+
const frag = document.createDocumentFragment();
|
|
662
|
+
frag.appendChild(createMarker(markerId, false));
|
|
663
|
+
elements.forEach(c => {
|
|
664
|
+
if (typeof c === 'string') frag.appendChild(document.createTextNode(c));
|
|
665
|
+
else if (c.domEl) frag.appendChild(c.domEl);
|
|
666
|
+
else if (c instanceof Node) frag.appendChild(c);
|
|
667
|
+
else {
|
|
668
|
+
const v = window.Lightview?.hooks.processChild?.(c) || c;
|
|
669
|
+
if (v.tag) {
|
|
670
|
+
const n = element(v.tag, v.attributes || {}, v.children || []);
|
|
671
|
+
if (n?.domEl) frag.appendChild(n.domEl);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
});
|
|
675
|
+
frag.appendChild(createMarker(markerId, true));
|
|
769
676
|
|
|
770
|
-
|
|
677
|
+
if (isOuter) target.replaceChild(frag, parent);
|
|
678
|
+
else if (location === 'beforebegin') target.insertBefore(frag, parent);
|
|
679
|
+
else if (location === 'afterend') target.insertBefore(frag, parent.nextSibling);
|
|
680
|
+
else if (location === 'afterbegin') parent.insertBefore(frag, parent.firstChild);
|
|
681
|
+
else if (location === 'beforeend') parent.appendChild(frag);
|
|
771
682
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
let isHtml = false;
|
|
775
|
-
let rawContent = '';
|
|
683
|
+
executeScripts(target);
|
|
684
|
+
};
|
|
776
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Handles the 'src' attribute on non-standard tags.
|
|
688
|
+
* Loads content from a URL or selector and injects it into the element.
|
|
689
|
+
*/
|
|
690
|
+
const handleSrcAttribute = async (el, src, tagName, { element, setupChildren }) => {
|
|
691
|
+
if (STANDARD_SRC_TAGS.includes(tagName)) return;
|
|
692
|
+
const isPath = (s) => /^(https?:|\.|\/|[\w])|(\.(html|json|[vo]dom))$/i.test(s);
|
|
693
|
+
|
|
694
|
+
let content = null, isJson = false, isHtml = false, raw = '';
|
|
777
695
|
if (isPath(src)) {
|
|
778
696
|
try {
|
|
779
|
-
const
|
|
780
|
-
const res = await fetch(url.href);
|
|
697
|
+
const res = await fetch(new URL(src, document.baseURI));
|
|
781
698
|
if (res.ok) {
|
|
782
|
-
const ext =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
isHtml = true;
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
if (isJson) {
|
|
791
|
-
content = await res.json();
|
|
792
|
-
rawContent = JSON.stringify(content);
|
|
793
|
-
} else {
|
|
794
|
-
content = await res.text();
|
|
795
|
-
rawContent = content;
|
|
796
|
-
}
|
|
699
|
+
const ext = new URL(src, document.baseURI).pathname.split('.').pop().toLowerCase();
|
|
700
|
+
isJson = (ext === 'vdom' || ext === 'odom');
|
|
701
|
+
isHtml = (ext === 'html');
|
|
702
|
+
content = isJson ? await res.json() : await res.text();
|
|
703
|
+
raw = isJson ? JSON.stringify(content) : content;
|
|
797
704
|
}
|
|
798
|
-
} catch {
|
|
799
|
-
// Fetch failed, try selector
|
|
800
|
-
}
|
|
705
|
+
} catch (e) { /* Fetch failed, maybe selector */ }
|
|
801
706
|
}
|
|
802
707
|
|
|
803
708
|
let elements = [];
|
|
804
709
|
if (content !== null) {
|
|
805
|
-
if (isJson)
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
elements = [content];
|
|
812
|
-
} else {
|
|
813
|
-
const parser = new DOMParser();
|
|
814
|
-
// Remove explicit <head> content to prevent collecting metadata
|
|
815
|
-
// while preserving nodes that the parser auto-moves to head (e.g. styles outside head)
|
|
816
|
-
const contentWithoutHead = content.replace(/<head[^>]*>[\s\S]*?<\/head>/i, '');
|
|
817
|
-
const doc = parser.parseFromString(contentWithoutHead, 'text/html');
|
|
818
|
-
|
|
819
|
-
// Collect all resulting nodes (auto-moved head nodes + body nodes)
|
|
820
|
-
const allNodes = [...Array.from(doc.head.childNodes), ...Array.from(doc.body.childNodes)];
|
|
821
|
-
elements = domToElements(allNodes, element);
|
|
710
|
+
if (isJson) elements = Array.isArray(content) ? content : [content];
|
|
711
|
+
else if (isHtml) {
|
|
712
|
+
if (el.domEl.getAttribute('escape') === 'true') elements = [content];
|
|
713
|
+
else {
|
|
714
|
+
const doc = new DOMParser().parseFromString(content.replace(/<head[^>]*>[\s\S]*?<\/head>/i, ''), 'text/html');
|
|
715
|
+
elements = domToElements([...Array.from(doc.head.childNodes), ...Array.from(doc.body.childNodes)], element);
|
|
822
716
|
}
|
|
823
|
-
} else
|
|
824
|
-
// Treat as text
|
|
825
|
-
elements = [content];
|
|
826
|
-
}
|
|
717
|
+
} else elements = [content];
|
|
827
718
|
} else {
|
|
828
719
|
try {
|
|
829
|
-
const
|
|
830
|
-
if (
|
|
831
|
-
elements = domToElements(Array.from(
|
|
832
|
-
|
|
833
|
-
rawContent = Array.from(selected).map(n => n.outerHTML || n.textContent).join('');
|
|
720
|
+
const sel = document.querySelectorAll(src);
|
|
721
|
+
if (sel.length) {
|
|
722
|
+
elements = domToElements(Array.from(sel), element);
|
|
723
|
+
raw = Array.from(sel).map(n => n.outerHTML || n.textContent).join('');
|
|
834
724
|
}
|
|
835
|
-
} catch {
|
|
836
|
-
// Invalid selector
|
|
837
|
-
}
|
|
725
|
+
} catch (e) { /* Invalid selector */ }
|
|
838
726
|
}
|
|
839
727
|
|
|
840
|
-
if (elements.length
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const
|
|
728
|
+
if (!elements.length) return;
|
|
729
|
+
const loc = (el.domEl.getAttribute('location') || 'innerhtml').toLowerCase();
|
|
730
|
+
const hash = hashContent(raw);
|
|
731
|
+
const markerId = `${loc}-${hash.slice(0, 8)}`;
|
|
844
732
|
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
733
|
+
let track = getOrSet(insertedContentMap, el.domEl, () => ({}));
|
|
734
|
+
if (track[loc] === hash) return;
|
|
735
|
+
if (track[loc]) removeInsertedContent(el.domEl, `${loc}-${track[loc].slice(0, 8)}`);
|
|
736
|
+
track[loc] = hash;
|
|
848
737
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
if (!tracking) {
|
|
852
|
-
tracking = {};
|
|
853
|
-
insertedContentMap.set(el.domEl, tracking);
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
if (tracking[location] === contentHash) {
|
|
857
|
-
// Same content already inserted at this location - no-op
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
// Different content or first insert - remove old content if any
|
|
862
|
-
if (tracking[location]) {
|
|
863
|
-
const oldMarkerId = `${location}-${tracking[location].slice(0, 8)}`;
|
|
864
|
-
removeInsertedContent(el.domEl, oldMarkerId);
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Update tracking
|
|
868
|
-
tracking[location] = contentHash;
|
|
869
|
-
|
|
870
|
-
// Check for shadow DOM via location attribute
|
|
871
|
-
if (location === 'shadow') {
|
|
872
|
-
if (!el.domEl.shadowRoot) {
|
|
873
|
-
el.domEl.attachShadow({ mode: 'open' });
|
|
874
|
-
}
|
|
738
|
+
if (loc === 'shadow') {
|
|
739
|
+
if (!el.domEl.shadowRoot) el.domEl.attachShadow({ mode: 'open' });
|
|
875
740
|
setupChildren(elements, el.domEl.shadowRoot);
|
|
876
741
|
executeScripts(el.domEl.shadowRoot);
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
case 'beforebegin':
|
|
883
|
-
case 'afterend': {
|
|
884
|
-
// Insert as siblings - need to use DOM insertion
|
|
885
|
-
const parent = el.domEl.parentElement;
|
|
886
|
-
if (!parent) {
|
|
887
|
-
console.warn('Cannot use beforebegin/afterend without parent element');
|
|
888
|
-
return;
|
|
889
|
-
}
|
|
890
|
-
|
|
891
|
-
const fragment = document.createDocumentFragment();
|
|
892
|
-
fragment.appendChild(createMarker(markerId, false));
|
|
893
|
-
|
|
894
|
-
elements.forEach(childEl => {
|
|
895
|
-
if (typeof childEl === 'string') {
|
|
896
|
-
fragment.appendChild(document.createTextNode(childEl));
|
|
897
|
-
} else if (childEl.domEl) {
|
|
898
|
-
fragment.appendChild(childEl.domEl);
|
|
899
|
-
} else if (childEl instanceof Node) {
|
|
900
|
-
fragment.appendChild(childEl);
|
|
901
|
-
} else {
|
|
902
|
-
// Convert Object DOM to vDOM if needed
|
|
903
|
-
let vdom = childEl;
|
|
904
|
-
if (typeof window !== 'undefined' && window.Lightview && window.Lightview.hooks.processChild) {
|
|
905
|
-
vdom = window.Lightview.hooks.processChild(childEl) || childEl;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (vdom.tag) {
|
|
909
|
-
const created = element(vdom.tag, vdom.attributes || {}, vdom.children || []);
|
|
910
|
-
if (created && created.domEl) {
|
|
911
|
-
fragment.appendChild(created.domEl);
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
fragment.appendChild(createMarker(markerId, true));
|
|
918
|
-
|
|
919
|
-
if (location === 'beforebegin') {
|
|
920
|
-
el.domEl.parentElement.insertBefore(fragment, el.domEl);
|
|
921
|
-
} else {
|
|
922
|
-
el.domEl.parentElement.insertBefore(fragment, el.domEl.nextSibling);
|
|
923
|
-
}
|
|
924
|
-
// Execute scripts after insertion
|
|
925
|
-
executeScripts(parent);
|
|
926
|
-
break;
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
case 'afterbegin': {
|
|
930
|
-
// Prepend to children
|
|
931
|
-
const fragment = document.createDocumentFragment();
|
|
932
|
-
fragment.appendChild(createMarker(markerId, false));
|
|
933
|
-
|
|
934
|
-
elements.forEach(childEl => {
|
|
935
|
-
if (typeof childEl === 'string') {
|
|
936
|
-
fragment.appendChild(document.createTextNode(childEl));
|
|
937
|
-
} else if (childEl.domEl) {
|
|
938
|
-
fragment.appendChild(childEl.domEl);
|
|
939
|
-
} else if (childEl instanceof Node) {
|
|
940
|
-
fragment.appendChild(childEl);
|
|
941
|
-
} else {
|
|
942
|
-
// Convert Object DOM to vDOM if needed
|
|
943
|
-
let vdom = childEl;
|
|
944
|
-
if (typeof window !== 'undefined' && window.Lightview && window.Lightview.hooks.processChild) {
|
|
945
|
-
vdom = window.Lightview.hooks.processChild(childEl) || childEl;
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (vdom.tag) {
|
|
949
|
-
const created = element(vdom.tag, vdom.attributes || {}, vdom.children || []);
|
|
950
|
-
if (created && created.domEl) {
|
|
951
|
-
fragment.appendChild(created.domEl);
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
});
|
|
956
|
-
|
|
957
|
-
fragment.appendChild(createMarker(markerId, true));
|
|
958
|
-
el.domEl.insertBefore(fragment, el.domEl.firstChild);
|
|
959
|
-
// Execute scripts after insertion
|
|
960
|
-
executeScripts(el.domEl);
|
|
961
|
-
break;
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
case 'beforeend': {
|
|
965
|
-
// Append to children
|
|
966
|
-
el.domEl.appendChild(createMarker(markerId, false));
|
|
967
|
-
|
|
968
|
-
elements.forEach(childEl => {
|
|
969
|
-
if (typeof childEl === 'string') {
|
|
970
|
-
el.domEl.appendChild(document.createTextNode(childEl));
|
|
971
|
-
} else if (childEl.domEl) {
|
|
972
|
-
el.domEl.appendChild(childEl.domEl);
|
|
973
|
-
} else if (childEl instanceof Node) {
|
|
974
|
-
el.domEl.appendChild(childEl);
|
|
975
|
-
} else {
|
|
976
|
-
// Convert Object DOM to vDOM if needed
|
|
977
|
-
let vdom = childEl;
|
|
978
|
-
if (typeof window !== 'undefined' && window.Lightview && window.Lightview.hooks.processChild) {
|
|
979
|
-
vdom = window.Lightview.hooks.processChild(childEl) || childEl;
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
if (vdom.tag) {
|
|
983
|
-
const created = element(vdom.tag, vdom.attributes || {}, vdom.children || []);
|
|
984
|
-
if (created && created.domEl) {
|
|
985
|
-
el.domEl.appendChild(created.domEl);
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
});
|
|
990
|
-
|
|
991
|
-
el.domEl.appendChild(createMarker(markerId, true));
|
|
992
|
-
// Execute scripts after insertion
|
|
993
|
-
executeScripts(el.domEl);
|
|
994
|
-
break;
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
case 'outerhtml': {
|
|
998
|
-
// Replace the element entirely
|
|
999
|
-
const parent = el.domEl.parentElement;
|
|
1000
|
-
if (!parent) {
|
|
1001
|
-
console.warn('Cannot use outerhtml without parent element');
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const fragment = document.createDocumentFragment();
|
|
1006
|
-
fragment.appendChild(createMarker(markerId, false));
|
|
1007
|
-
|
|
1008
|
-
elements.forEach(childEl => {
|
|
1009
|
-
if (typeof childEl === 'string') {
|
|
1010
|
-
fragment.appendChild(document.createTextNode(childEl));
|
|
1011
|
-
} else if (childEl.domEl) {
|
|
1012
|
-
fragment.appendChild(childEl.domEl);
|
|
1013
|
-
} else if (childEl instanceof Node) {
|
|
1014
|
-
fragment.appendChild(childEl);
|
|
1015
|
-
} else {
|
|
1016
|
-
// Convert Object DOM to vDOM if needed
|
|
1017
|
-
let vdom = childEl;
|
|
1018
|
-
if (typeof window !== 'undefined' && window.Lightview && window.Lightview.hooks.processChild) {
|
|
1019
|
-
vdom = window.Lightview.hooks.processChild(childEl) || childEl;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
if (vdom.tag) {
|
|
1023
|
-
const created = element(vdom.tag, vdom.attributes || {}, vdom.children || []);
|
|
1024
|
-
if (created && created.domEl) {
|
|
1025
|
-
fragment.appendChild(created.domEl);
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
});
|
|
1030
|
-
|
|
1031
|
-
fragment.appendChild(createMarker(markerId, true));
|
|
1032
|
-
parent.replaceChild(fragment, el.domEl);
|
|
1033
|
-
// Execute scripts after insertion
|
|
1034
|
-
executeScripts(parent);
|
|
1035
|
-
break;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
case 'innerhtml':
|
|
1039
|
-
default: {
|
|
1040
|
-
// Replace all children (original behavior)
|
|
1041
|
-
el.children = elements;
|
|
1042
|
-
// Execute scripts after children are set
|
|
1043
|
-
executeScripts(el.domEl);
|
|
1044
|
-
break;
|
|
1045
|
-
}
|
|
742
|
+
} else if (loc === 'innerhtml') {
|
|
743
|
+
el.children = elements;
|
|
744
|
+
executeScripts(el.domEl);
|
|
745
|
+
} else {
|
|
746
|
+
insert(elements, el.domEl, loc, markerId, { element, setupChildren });
|
|
1046
747
|
}
|
|
1047
748
|
};
|
|
1048
749
|
|
|
@@ -1063,6 +764,10 @@
|
|
|
1063
764
|
return { selector: targetStr, location: null };
|
|
1064
765
|
};
|
|
1065
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Intercepts clicks on elements with 'href' attributes that are not standard links.
|
|
769
|
+
* Enables HTMX-like SPA navigation by loading the href content into a target element.
|
|
770
|
+
*/
|
|
1066
771
|
const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
1067
772
|
const clickedEl = e.target.closest('[href]');
|
|
1068
773
|
if (!clickedEl) return;
|
|
@@ -1178,54 +883,19 @@
|
|
|
1178
883
|
const activateReactiveSyntax = (root, LV) => {
|
|
1179
884
|
if (!root || !LV) return;
|
|
1180
885
|
|
|
1181
|
-
// Helper to compile and bind effect
|
|
1182
886
|
const bindEffect = (node, codeStr, isAttr = false, attrName = null) => {
|
|
1183
|
-
if (processedNodes.has(node) && !isAttr) return;
|
|
1184
|
-
// For attributes, we might process same element multiple times for diff attributes,
|
|
1185
|
-
// but the effect is per attribute so it's fine.
|
|
1186
|
-
// We'll mark text nodes as processed. Attributes don't strictly need it if we trust the scanner not to duplicate.
|
|
1187
|
-
|
|
887
|
+
if (processedNodes.has(node) && !isAttr) return;
|
|
1188
888
|
if (!isAttr) processedNodes.add(node);
|
|
1189
889
|
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
// Extract expression: remove leading ${ and trailing }
|
|
1200
|
-
const expr = codeStr.trim().slice(2, -1);
|
|
1201
|
-
fnBody = 'return ' + expr;
|
|
1202
|
-
} else {
|
|
1203
|
-
// Escape backticks and backslashes for the template literal
|
|
1204
|
-
const escaped = codeStr.replace(/\\/g, '\\\\').replace(/`/g, '\\`');
|
|
1205
|
-
fnBody = 'return `' + escaped + '`';
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
const fn = new Function('state', 'signal', fnBody);
|
|
1209
|
-
|
|
1210
|
-
LV.effect(() => {
|
|
1211
|
-
try {
|
|
1212
|
-
const val = fn(LV.state, LV.signal);
|
|
1213
|
-
if (isAttr) {
|
|
1214
|
-
if (val === null || val === undefined || val === false) {
|
|
1215
|
-
node.removeAttribute(attrName);
|
|
1216
|
-
} else {
|
|
1217
|
-
node.setAttribute(attrName, val);
|
|
1218
|
-
}
|
|
1219
|
-
} else {
|
|
1220
|
-
node.textContent = val !== undefined ? val : '';
|
|
1221
|
-
}
|
|
1222
|
-
} catch (e) {
|
|
1223
|
-
// Silent fail
|
|
1224
|
-
}
|
|
1225
|
-
});
|
|
1226
|
-
} catch (e) {
|
|
1227
|
-
console.warn('Lightview: Failed to compile template literal', e);
|
|
1228
|
-
}
|
|
890
|
+
const fn = compileTemplate(codeStr);
|
|
891
|
+
LV.effect(() => {
|
|
892
|
+
try {
|
|
893
|
+
const val = fn(LV.state, LV.signal);
|
|
894
|
+
if (isAttr) {
|
|
895
|
+
(val === null || val === undefined || val === false) ? node.removeAttribute(attrName) : node.setAttribute(attrName, val);
|
|
896
|
+
} else node.textContent = val !== undefined ? val : '';
|
|
897
|
+
} catch (e) { /* Effect execution failed */ }
|
|
898
|
+
});
|
|
1229
899
|
};
|
|
1230
900
|
|
|
1231
901
|
// 1. Find Text Nodes containing '${'
|