lightview 2.3.8 → 2.4.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/.gemini/CODE_ANALYSIS_AND_IMPROVEMENT_PLAN.md +56 -0
- package/AI-GUIDANCE.md +259 -0
- package/README.md +35 -0
- package/components/data-display/diff.js +36 -4
- package/docs/api/hypermedia.html +75 -5
- package/docs/api/index.html +3 -3
- package/docs/api/nav.html +0 -16
- package/docs/articles/html-vs-json-partials.md +102 -0
- package/docs/articles/lightview-vs-htmx.md +610 -0
- package/docs/benchmarks/tagged-fragment.js +36 -0
- package/docs/components/chart.html +157 -210
- package/docs/components/component-nav.html +1 -1
- package/docs/components/diff.html +33 -21
- package/docs/components/gallery.html +107 -4
- package/docs/components/index.css +18 -3
- package/docs/components/index.html +20 -9
- package/docs/dom-benchmark.html +644 -0
- package/docs/getting-started/index.html +2 -2
- package/docs/hypermedia/index.html +391 -0
- package/docs/hypermedia/nav.html +17 -0
- package/docs/index.html +128 -18
- package/index.html +59 -10
- package/lightview-all.js +223 -67
- package/lightview-cdom.js +1 -2
- package/lightview-x.js +144 -13
- package/lightview.js +85 -277
- package/package.json +2 -2
- package/src/lightview-cdom.js +1 -5
- package/src/lightview-x.js +158 -27
- package/src/lightview.js +94 -60
- package/docs/articles/calculator-no-javascript-hackernoon.md +0 -283
- package/docs/articles/calculator-no-javascript.md +0 -290
- package/docs/articles/part1-reference.md +0 -236
- package/lightview.js.bak +0 -1
- package/test-xpath.html +0 -63
- package/test_error.txt +0 -0
- package/test_output.txt +0 -0
- package/test_output_full.txt +0 -0
package/src/lightview-x.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { signal, effect, getRegistry } from './reactivity/signal.js';
|
|
2
|
-
import { state, getOrSet } from './reactivity/state.js';
|
|
2
|
+
import { state, getState, getOrSet } from './reactivity/state.js';
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -552,15 +552,107 @@ const insert = (elements, parent, location, markerId, { element, setupChildren }
|
|
|
552
552
|
|
|
553
553
|
const isPath = (s) => typeof s === 'string' && !isDangerousProtocol(s) && /^(https?:|\.|\/|[\w])|(\.(html|json|[vo]dom|cdomc?))$/i.test(s);
|
|
554
554
|
|
|
555
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Resolves request information (method, body, headers) from an element's data- attributes.
|
|
557
|
+
* Supports data-method and data-body.
|
|
558
|
+
*/
|
|
559
|
+
const getRequestInfo = (el) => {
|
|
560
|
+
const domEl = el.domEl || el;
|
|
561
|
+
const method = (domEl.getAttribute('data-method') || 'GET').toUpperCase();
|
|
562
|
+
const bodyAttr = domEl.getAttribute('data-body');
|
|
563
|
+
let body = null;
|
|
564
|
+
let headers = {};
|
|
565
|
+
|
|
566
|
+
if (bodyAttr) {
|
|
567
|
+
if (bodyAttr.startsWith('javascript:')) {
|
|
568
|
+
const expr = bodyAttr.slice(11);
|
|
569
|
+
const LV = globalThis.Lightview;
|
|
570
|
+
try {
|
|
571
|
+
// Use the registry to resolve signals/state mentioned in expressions
|
|
572
|
+
body = new Function('state', 'signal', `return ${expr}`)(LV.state || {}, LV.signal || {});
|
|
573
|
+
} catch (e) {
|
|
574
|
+
console.warn(`[LightviewX] Failed to evaluate data-body expression: ${expr}`, e);
|
|
575
|
+
}
|
|
576
|
+
} else if (bodyAttr.startsWith('json:')) {
|
|
577
|
+
try {
|
|
578
|
+
body = JSON.parse(bodyAttr.slice(5));
|
|
579
|
+
headers['Content-Type'] = 'application/json';
|
|
580
|
+
} catch (e) {
|
|
581
|
+
console.warn(`[LightviewX] Failed to parse data-body JSON: ${bodyAttr.slice(5)}`, e);
|
|
582
|
+
}
|
|
583
|
+
} else if (bodyAttr.startsWith('text:')) {
|
|
584
|
+
body = bodyAttr.slice(5);
|
|
585
|
+
headers['Content-Type'] = 'text/plain';
|
|
586
|
+
} else {
|
|
587
|
+
// Assume CSS Selector
|
|
588
|
+
try {
|
|
589
|
+
const target = document.querySelector(bodyAttr);
|
|
590
|
+
if (target) {
|
|
591
|
+
if (target.tagName === 'FORM') {
|
|
592
|
+
body = new FormData(target);
|
|
593
|
+
} else if (['INPUT', 'SELECT', 'TEXTAREA'].includes(target.tagName)) {
|
|
594
|
+
const name = target.getAttribute('name') || 'body';
|
|
595
|
+
body = { [name]: target.value };
|
|
596
|
+
} else {
|
|
597
|
+
body = target.innerText;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
} catch (e) {
|
|
601
|
+
// Not a valid selector, use raw string
|
|
602
|
+
body = bodyAttr;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return { method, body, headers };
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Fetches content from a URL, respecting data-method and data-body settings.
|
|
612
|
+
*/
|
|
613
|
+
const fetchContent = async (src, requestOptions = {}) => {
|
|
614
|
+
const { method = 'GET', body = null, headers = {} } = requestOptions;
|
|
556
615
|
try {
|
|
557
616
|
const LV = globalThis.Lightview;
|
|
558
617
|
if (LV?.hooks?.validateUrl && !LV.hooks.validateUrl(src)) {
|
|
559
618
|
console.warn(`[LightviewX] Fetch blocked by validateUrl hook: ${src}`);
|
|
560
619
|
return null;
|
|
561
620
|
}
|
|
562
|
-
|
|
563
|
-
|
|
621
|
+
|
|
622
|
+
let url = new URL(src, document.baseURI);
|
|
623
|
+
const fetchOptions = { method, headers: { ...headers } };
|
|
624
|
+
|
|
625
|
+
if (body) {
|
|
626
|
+
if (method === 'GET') {
|
|
627
|
+
const params = new URLSearchParams(url.search);
|
|
628
|
+
if (body instanceof FormData) {
|
|
629
|
+
for (const [key, value] of body.entries()) params.append(key, value);
|
|
630
|
+
} else if (typeof body === 'object' && body !== null) {
|
|
631
|
+
for (const [key, value] of Object.entries(body)) params.append(key, String(value));
|
|
632
|
+
} else {
|
|
633
|
+
params.append('body', String(body));
|
|
634
|
+
}
|
|
635
|
+
const queryString = params.toString();
|
|
636
|
+
if (queryString) {
|
|
637
|
+
url = new URL(`${url.origin}${url.pathname}?${queryString}${url.hash}`, url.origin);
|
|
638
|
+
}
|
|
639
|
+
} else {
|
|
640
|
+
if (body instanceof FormData) {
|
|
641
|
+
fetchOptions.body = body;
|
|
642
|
+
} else if (typeof body === 'object' && body !== null) {
|
|
643
|
+
if (headers['Content-Type'] === 'application/json' || !headers['Content-Type']) {
|
|
644
|
+
fetchOptions.body = JSON.stringify(body);
|
|
645
|
+
fetchOptions.headers['Content-Type'] = 'application/json';
|
|
646
|
+
} else {
|
|
647
|
+
fetchOptions.body = String(body);
|
|
648
|
+
}
|
|
649
|
+
} else {
|
|
650
|
+
fetchOptions.body = String(body);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const res = await fetch(url, fetchOptions);
|
|
564
656
|
if (!res.ok) return null;
|
|
565
657
|
const ext = url.pathname.split('.').pop().toLowerCase();
|
|
566
658
|
const isJson = (ext === 'vdom' || ext === 'odom' || ext === 'cdom');
|
|
@@ -626,7 +718,11 @@ const elementsFromSelector = (selector, element) => {
|
|
|
626
718
|
};
|
|
627
719
|
|
|
628
720
|
const updateTargetContent = (el, elements, raw, loc, contentHash, options, targetHash = null) => {
|
|
629
|
-
const { element, setupChildren, saveScrolls, restoreScrolls } = {
|
|
721
|
+
const { element, setupChildren, saveScrolls, restoreScrolls } = {
|
|
722
|
+
element: globalThis.Lightview?.element,
|
|
723
|
+
...globalThis.Lightview?.internals,
|
|
724
|
+
...options
|
|
725
|
+
};
|
|
630
726
|
const markerId = `${loc}-${contentHash.slice(0, 8)}`;
|
|
631
727
|
let track = getOrSet(insertedContentMap, el.domEl, () => ({}));
|
|
632
728
|
if (track[loc]) removeInsertedContent(el.domEl, `${loc}-${track[loc].slice(0, 8)}`);
|
|
@@ -683,7 +779,8 @@ const handleSrcAttribute = async (el, src, tagName, { element, setupChildren })
|
|
|
683
779
|
if (src.includes('#')) {
|
|
684
780
|
[src, targetHash] = src.split('#');
|
|
685
781
|
}
|
|
686
|
-
const
|
|
782
|
+
const options = getRequestInfo(el);
|
|
783
|
+
const result = await fetchContent(src, options);
|
|
687
784
|
if (result) {
|
|
688
785
|
elements = parseElements(result.content, result.isJson, result.isHtml, el, element, result.isCdom, result.ext);
|
|
689
786
|
raw = result.raw;
|
|
@@ -747,7 +844,7 @@ const parseTargetWithLocation = (targetStr) => {
|
|
|
747
844
|
* Intercepts clicks on elements with 'href' attributes that are not standard links.
|
|
748
845
|
* Enables HTMX-like SPA navigation by loading the href content into a target element.
|
|
749
846
|
*/
|
|
750
|
-
const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
847
|
+
const handleNonStandardHref = async (e, { domToElement, wrapDomElement }) => {
|
|
751
848
|
const clickedEl = e.target.closest('[href]');
|
|
752
849
|
if (!clickedEl) return;
|
|
753
850
|
|
|
@@ -764,6 +861,9 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
|
764
861
|
}
|
|
765
862
|
const targetAttr = clickedEl.getAttribute('target');
|
|
766
863
|
|
|
864
|
+
// Get request configuration (method, body) from the clicked element
|
|
865
|
+
const options = getRequestInfo(clickedEl);
|
|
866
|
+
|
|
767
867
|
// Case 1: No target attribute - existing behavior (load into self)
|
|
768
868
|
if (!targetAttr) {
|
|
769
869
|
let el = domToElement.get(clickedEl);
|
|
@@ -772,6 +872,7 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
|
772
872
|
for (let attr of clickedEl.attributes) attrs[attr.name] = attr.value;
|
|
773
873
|
el = wrapDomElement(clickedEl, tagName, attrs);
|
|
774
874
|
}
|
|
875
|
+
// Setting src triggers handleSrcAttribute via property proxy
|
|
775
876
|
const newAttrs = { ...el.attributes, src: href };
|
|
776
877
|
el.attributes = newAttrs;
|
|
777
878
|
return;
|
|
@@ -779,21 +880,16 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
|
779
880
|
|
|
780
881
|
// Case 2: Target starts with _ (browser navigation)
|
|
781
882
|
if (targetAttr.startsWith('_')) {
|
|
883
|
+
if (options.method !== 'GET') {
|
|
884
|
+
console.warn('[LightviewX] Cannot use non-GET method for browser navigation (_blank, _top, etc.)');
|
|
885
|
+
}
|
|
886
|
+
|
|
782
887
|
switch (targetAttr) {
|
|
783
|
-
case '_self':
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
case '_parent':
|
|
787
|
-
globalThis.parent.location.href = href;
|
|
788
|
-
break;
|
|
789
|
-
case '_top':
|
|
790
|
-
globalThis.top.location.href = href;
|
|
791
|
-
break;
|
|
888
|
+
case '_self': globalThis.location.href = href; break;
|
|
889
|
+
case '_parent': globalThis.parent.location.href = href; break;
|
|
890
|
+
case '_top': globalThis.top.location.href = href; break;
|
|
792
891
|
case '_blank':
|
|
793
|
-
default:
|
|
794
|
-
// _blank or any custom _name opens a new window/tab
|
|
795
|
-
globalThis.open(href, targetAttr);
|
|
796
|
-
break;
|
|
892
|
+
default: globalThis.open(href, targetAttr); break;
|
|
797
893
|
}
|
|
798
894
|
return;
|
|
799
895
|
}
|
|
@@ -803,6 +899,15 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
|
803
899
|
|
|
804
900
|
try {
|
|
805
901
|
const targetElements = document.querySelectorAll(selector);
|
|
902
|
+
if (targetElements.length === 0) return;
|
|
903
|
+
|
|
904
|
+
// Perform the fetch once for all targets
|
|
905
|
+
const result = await fetchContent(href, options);
|
|
906
|
+
if (!result) return;
|
|
907
|
+
|
|
908
|
+
const { setupChildren } = LV.internals;
|
|
909
|
+
const element = LV.element;
|
|
910
|
+
|
|
806
911
|
targetElements.forEach(targetEl => {
|
|
807
912
|
let el = domToElement.get(targetEl);
|
|
808
913
|
if (!el) {
|
|
@@ -811,15 +916,17 @@ const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
|
811
916
|
el = wrapDomElement(targetEl, targetEl.tagName.toLowerCase(), attrs);
|
|
812
917
|
}
|
|
813
918
|
|
|
814
|
-
|
|
815
|
-
const
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
}
|
|
819
|
-
|
|
919
|
+
const elements = parseElements(result.content, result.isJson, result.isHtml, el, element, result.isCdom, result.ext);
|
|
920
|
+
const loc = (location || targetEl.getAttribute('location') || 'innerhtml').toLowerCase();
|
|
921
|
+
const contentHash = hashContent(result.raw);
|
|
922
|
+
|
|
923
|
+
updateTargetContent(el, elements, result.raw, loc, contentHash, { element, setupChildren });
|
|
924
|
+
|
|
925
|
+
// Update the src attribute on the target to reflect current content
|
|
926
|
+
targetEl.setAttribute('src', href);
|
|
820
927
|
});
|
|
821
928
|
} catch (err) {
|
|
822
|
-
console.warn('Invalid target selector:', selector, err);
|
|
929
|
+
console.warn('Invalid target selector or fetch error:', selector, err);
|
|
823
930
|
}
|
|
824
931
|
};
|
|
825
932
|
|
|
@@ -1190,6 +1297,8 @@ const setupSrcObserver = (LV) => {
|
|
|
1190
1297
|
// Auto-register with Lightview if available
|
|
1191
1298
|
if (typeof window !== 'undefined' && globalThis.Lightview) {
|
|
1192
1299
|
const LV = globalThis.Lightview;
|
|
1300
|
+
LV.state = state;
|
|
1301
|
+
LV.getState = getState;
|
|
1193
1302
|
|
|
1194
1303
|
// Extend Lightview with simple named signal getter/setter if needed (already in Core now)
|
|
1195
1304
|
// But for template literals we use processTemplateChild which needs access to registries
|
|
@@ -1696,8 +1805,23 @@ if (lvInternals) {
|
|
|
1696
1805
|
}
|
|
1697
1806
|
|
|
1698
1807
|
// Export for module usage
|
|
1808
|
+
const request = async (el) => {
|
|
1809
|
+
const domEl = el.domEl || el;
|
|
1810
|
+
const href = domEl.getAttribute('href');
|
|
1811
|
+
if (!href) return;
|
|
1812
|
+
return handleNonStandardHref({
|
|
1813
|
+
target: domEl,
|
|
1814
|
+
preventDefault: () => { },
|
|
1815
|
+
stopPropagation: () => { }
|
|
1816
|
+
}, {
|
|
1817
|
+
domToElement: globalThis.Lightview.internals.domToElement,
|
|
1818
|
+
wrapDomElement: globalThis.Lightview.internals.wrapDomElement
|
|
1819
|
+
});
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1699
1822
|
const LightviewX = {
|
|
1700
1823
|
state,
|
|
1824
|
+
getState,
|
|
1701
1825
|
themeSignal,
|
|
1702
1826
|
setTheme,
|
|
1703
1827
|
registerStyleSheet,
|
|
@@ -1705,6 +1829,12 @@ const LightviewX = {
|
|
|
1705
1829
|
// Gate modifiers
|
|
1706
1830
|
throttle: gateThrottle,
|
|
1707
1831
|
debounce: gateDebounce,
|
|
1832
|
+
// Hypermedia Actions
|
|
1833
|
+
request,
|
|
1834
|
+
get: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'GET'); return request(el); },
|
|
1835
|
+
post: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'POST'); return request(el); },
|
|
1836
|
+
put: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'PUT'); return request(el); },
|
|
1837
|
+
delete: (el, url) => { if (url) el.setAttribute('href', url); el.setAttribute('data-method', 'DELETE'); return request(el); },
|
|
1708
1838
|
// Component initialization
|
|
1709
1839
|
initComponents,
|
|
1710
1840
|
componentConfig,
|
|
@@ -1719,6 +1849,7 @@ const LightviewX = {
|
|
|
1719
1849
|
parseElements
|
|
1720
1850
|
}
|
|
1721
1851
|
};
|
|
1852
|
+
if (globalThis.Lightview) globalThis.Lightview.request = request;
|
|
1722
1853
|
|
|
1723
1854
|
if (typeof module !== 'undefined' && module.exports) {
|
|
1724
1855
|
module.exports = LightviewX;
|
package/src/lightview.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { signal, effect, computed, getRegistry, internals } from './reactivity/signal.js';
|
|
2
|
-
import { state, getState } from './reactivity/state.js';
|
|
3
2
|
|
|
4
3
|
const core = {
|
|
5
4
|
get currentEffect() {
|
|
@@ -164,6 +163,7 @@ const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
|
|
|
164
163
|
tag,
|
|
165
164
|
attributes,
|
|
166
165
|
children,
|
|
166
|
+
isProxy: true,
|
|
167
167
|
get domEl() { return domNode; }
|
|
168
168
|
};
|
|
169
169
|
const proxy = makeReactive(el);
|
|
@@ -171,6 +171,15 @@ const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
|
|
|
171
171
|
return proxy;
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Recursively checks if any item in a nested array matches a predicate.
|
|
176
|
+
* Replaces expensive .flat(Infinity).some() calls.
|
|
177
|
+
*/
|
|
178
|
+
const someRecursive = (item, predicate) => {
|
|
179
|
+
if (Array.isArray(item)) return item.some(i => someRecursive(i, predicate));
|
|
180
|
+
return predicate(item);
|
|
181
|
+
};
|
|
182
|
+
|
|
174
183
|
/**
|
|
175
184
|
* The core virtual-DOM-to-real-DOM factory.
|
|
176
185
|
* Handles tag functions (components), shadow DOM directives, and SVG namespaces.
|
|
@@ -199,12 +208,17 @@ const element = (tag, attributes = {}, children = []) => {
|
|
|
199
208
|
};
|
|
200
209
|
|
|
201
210
|
const update = () => {
|
|
202
|
-
const
|
|
203
|
-
const
|
|
211
|
+
const bits = [];
|
|
212
|
+
const walk = (c) => {
|
|
213
|
+
if (Array.isArray(c)) {
|
|
214
|
+
for (let i = 0; i < c.length; i++) walk(c[i]);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
204
217
|
const val = typeof c === 'function' ? c() : c;
|
|
205
|
-
if (val && typeof val === 'object' && val.domEl)
|
|
206
|
-
|
|
207
|
-
}
|
|
218
|
+
if (val && typeof val === 'object' && val.domEl) bits.push(val.domEl.textContent);
|
|
219
|
+
else bits.push((val === null || val === undefined) ? '' : String(val));
|
|
220
|
+
};
|
|
221
|
+
walk(el.children);
|
|
208
222
|
domNode.textContent = bits.join(' ');
|
|
209
223
|
};
|
|
210
224
|
|
|
@@ -216,7 +230,7 @@ const element = (tag, attributes = {}, children = []) => {
|
|
|
216
230
|
}
|
|
217
231
|
});
|
|
218
232
|
|
|
219
|
-
const hasReactive = children
|
|
233
|
+
const hasReactive = someRecursive(children, c => typeof c === 'function');
|
|
220
234
|
if (hasReactive) {
|
|
221
235
|
const runner = effect(update);
|
|
222
236
|
trackEffect(domNode, runner);
|
|
@@ -233,12 +247,29 @@ const element = (tag, attributes = {}, children = []) => {
|
|
|
233
247
|
? document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
234
248
|
: document.createElement(tag);
|
|
235
249
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
250
|
+
// Optimization: Skip Proxy allocation for static nodes
|
|
251
|
+
const hasReactiveAttr = Object.values(attributes).some(v => typeof v === 'function');
|
|
252
|
+
const hasReactiveChild = someRecursive(children, c => typeof c === 'function' || (c && c.isProxy));
|
|
253
|
+
|
|
254
|
+
if (hasReactiveAttr || hasReactiveChild) {
|
|
255
|
+
const proxy = wrapDomElement(domNode, tag, attributes, children);
|
|
256
|
+
proxy.attributes = attributes;
|
|
257
|
+
proxy.children = children;
|
|
258
|
+
if (isSVG) inSVG = wasInSVG;
|
|
259
|
+
return proxy;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Static path: Direct application to the DOM node
|
|
263
|
+
makeReactiveAttributes(attributes, domNode);
|
|
264
|
+
setupChildren(children, domNode);
|
|
239
265
|
|
|
240
266
|
if (isSVG) inSVG = wasInSVG;
|
|
241
|
-
return
|
|
267
|
+
return {
|
|
268
|
+
tag,
|
|
269
|
+
attributes,
|
|
270
|
+
children,
|
|
271
|
+
domEl: domNode
|
|
272
|
+
};
|
|
242
273
|
};
|
|
243
274
|
|
|
244
275
|
// Process component function return value (HTML string, DOM node, vDOM, or Object DOM)
|
|
@@ -254,7 +285,7 @@ const processComponentResult = (result) => {
|
|
|
254
285
|
|
|
255
286
|
const type = typeof result;
|
|
256
287
|
// DOM node - wrap it
|
|
257
|
-
if (type === 'object' && result
|
|
288
|
+
if (type === 'object' && result && result.nodeType === 1) {
|
|
258
289
|
return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
|
|
259
290
|
}
|
|
260
291
|
|
|
@@ -271,7 +302,7 @@ const processComponentResult = (result) => {
|
|
|
271
302
|
template.innerHTML = result.trim();
|
|
272
303
|
const content = template.content;
|
|
273
304
|
// If single element, return it; otherwise wrap in a fragment-like span
|
|
274
|
-
if (content.childNodes.length === 1 && content.firstChild
|
|
305
|
+
if (content.childNodes.length === 1 && content.firstChild && content.firstChild.nodeType === 1) {
|
|
275
306
|
const el = content.firstChild;
|
|
276
307
|
return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
|
|
277
308
|
} else {
|
|
@@ -335,12 +366,14 @@ const setAttributeValue = (domNode, key, value) => {
|
|
|
335
366
|
/**
|
|
336
367
|
* Processes attributes, handling event listeners, reactive bindings, and special 'onmount' hooks.
|
|
337
368
|
*/
|
|
338
|
-
const makeReactiveAttributes = (attributes, domNode) => {
|
|
369
|
+
const makeReactiveAttributes = (attributes = {}, domNode) => {
|
|
339
370
|
const reactiveAttrs = {};
|
|
340
371
|
|
|
341
|
-
for (
|
|
372
|
+
for (const key in attributes) {
|
|
373
|
+
const value = attributes[key];
|
|
374
|
+
const type = typeof value;
|
|
342
375
|
// Handle XPath markers from hydration
|
|
343
|
-
if (value &&
|
|
376
|
+
if (value && type === 'object' && value.__xpath__ && value.__static__) {
|
|
344
377
|
// Mark attribute for later XPath resolution
|
|
345
378
|
domNode.setAttribute(`data-xpath-${key}`, value.__xpath__);
|
|
346
379
|
reactiveAttrs[key] = value;
|
|
@@ -356,11 +389,10 @@ const makeReactiveAttributes = (attributes, domNode) => {
|
|
|
356
389
|
}
|
|
357
390
|
} else if (key.startsWith('on')) {
|
|
358
391
|
// Event handler
|
|
359
|
-
if (
|
|
392
|
+
if (type === 'function') {
|
|
360
393
|
// Function handler - use addEventListener
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
} else if (typeof value === 'string') {
|
|
394
|
+
domNode.addEventListener(key.slice(2).toLowerCase(), value);
|
|
395
|
+
} else if (type === 'string') {
|
|
364
396
|
// String handler (from parsed HTML) - use setAttribute
|
|
365
397
|
// Browser will compile the string into a handler function
|
|
366
398
|
domNode.setAttribute(key, value);
|
|
@@ -372,20 +404,21 @@ const makeReactiveAttributes = (attributes, domNode) => {
|
|
|
372
404
|
reactiveAttrs[key] = processed;
|
|
373
405
|
} else if (key === 'style') {
|
|
374
406
|
// Style object support (merged from below)
|
|
375
|
-
|
|
407
|
+
for (const styleKey in entries) {
|
|
408
|
+
const styleValue = entries[styleKey];
|
|
376
409
|
if (typeof styleValue === 'function') {
|
|
377
410
|
const runner = effect(() => { domNode.style[styleKey] = styleValue(); });
|
|
378
411
|
trackEffect(domNode, runner);
|
|
379
412
|
} else {
|
|
380
413
|
domNode.style[styleKey] = styleValue;
|
|
381
414
|
}
|
|
382
|
-
}
|
|
415
|
+
}
|
|
383
416
|
reactiveAttrs[key] = value;
|
|
384
417
|
} else {
|
|
385
418
|
setAttributeValue(domNode, key, value);
|
|
386
419
|
reactiveAttrs[key] = value;
|
|
387
420
|
}
|
|
388
|
-
} else if (
|
|
421
|
+
} else if (type === 'function') {
|
|
389
422
|
// Reactive binding
|
|
390
423
|
const runner = effect(() => {
|
|
391
424
|
const result = value();
|
|
@@ -429,29 +462,33 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
429
462
|
const isSpecialElement = targetNode.tagName &&
|
|
430
463
|
(targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
|
|
431
464
|
|
|
432
|
-
const
|
|
465
|
+
const walk = (child) => {
|
|
466
|
+
if (Array.isArray(child)) {
|
|
467
|
+
for (let i = 0; i < child.length; i++) walk(child[i]);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (child === null || child === undefined) return;
|
|
433
472
|
|
|
434
|
-
for (let child of flatChildren) {
|
|
435
473
|
// Allow extensions to transform children (e.g., template literals)
|
|
436
474
|
// BUT skip for script/style elements which need raw content
|
|
437
475
|
if (Lightview.hooks.processChild && !isSpecialElement) {
|
|
438
476
|
child = Lightview.hooks.processChild(child) ?? child;
|
|
439
477
|
}
|
|
440
478
|
|
|
441
|
-
// Handle shadowDOM markers - attach shadow to parent and process shadow children
|
|
442
|
-
if (isShadowDOMMarker(child)) {
|
|
443
|
-
// targetNode is the parent element that should get the shadow root
|
|
444
|
-
// For ShadowRoot targets, we can't attach another shadow, so warn
|
|
445
|
-
if (targetNode instanceof ShadowRoot) {
|
|
446
|
-
console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
|
|
447
|
-
continue;
|
|
448
|
-
}
|
|
449
|
-
processShadowDOM(child, targetNode);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
|
|
453
479
|
const type = typeof child;
|
|
454
|
-
|
|
480
|
+
|
|
481
|
+
if (child && type === 'object' && child.tag) {
|
|
482
|
+
// 1. Child element (already wrapped or plain object) - tag can be string or function (structural)
|
|
483
|
+
const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
|
|
484
|
+
targetNode.appendChild(childEl.domEl);
|
|
485
|
+
childElements.push(childEl);
|
|
486
|
+
} else if (['string', 'number', 'boolean', 'symbol'].includes(type) || (child && type === 'object' && child instanceof String)) {
|
|
487
|
+
// 2. Static text (common leaf)
|
|
488
|
+
targetNode.appendChild(document.createTextNode(child));
|
|
489
|
+
childElements.push(child);
|
|
490
|
+
} else if (type === 'function') {
|
|
491
|
+
// 3. Reactive function
|
|
455
492
|
const startMarker = document.createComment('lv:s');
|
|
456
493
|
const endMarker = document.createComment('lv:e');
|
|
457
494
|
targetNode.appendChild(startMarker);
|
|
@@ -462,7 +499,6 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
462
499
|
// 1. Cleanup: Remove everything between markers
|
|
463
500
|
while (startMarker.nextSibling && startMarker.nextSibling !== endMarker) {
|
|
464
501
|
startMarker.nextSibling.remove();
|
|
465
|
-
// Note: MutationObserver handles cleanupNode(removedNode)
|
|
466
502
|
}
|
|
467
503
|
|
|
468
504
|
// 2. Execution: Get new value and process it
|
|
@@ -491,20 +527,10 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
491
527
|
runner = effect(update);
|
|
492
528
|
trackEffect(startMarker, runner);
|
|
493
529
|
childElements.push(child);
|
|
494
|
-
} else if (child && typeof child === 'object' && child.__xpath__ && child.__static__) {
|
|
495
|
-
// XPath marker - create text node with marker for later resolution
|
|
496
|
-
const textNode = document.createTextNode('');
|
|
497
|
-
textNode.__xpathExpr = child.__xpath__;
|
|
498
|
-
targetNode.appendChild(textNode);
|
|
499
|
-
childElements.push(child);
|
|
500
|
-
} else if (['string', 'number', 'boolean', 'symbol'].includes(type) || (child && type === 'object' && child instanceof String)) {
|
|
501
|
-
// Static text
|
|
502
|
-
targetNode.appendChild(document.createTextNode(child));
|
|
503
|
-
childElements.push(child);
|
|
504
530
|
} else if (child instanceof Node) {
|
|
505
|
-
// Raw DOM node
|
|
531
|
+
// 4. Raw DOM node
|
|
506
532
|
const node = child.domEl || child;
|
|
507
|
-
if (node
|
|
533
|
+
if (node.nodeType === 1) { // ELEMENT_NODE
|
|
508
534
|
const wrapped = wrapDomElement(node, node.tagName.toLowerCase());
|
|
509
535
|
targetNode.appendChild(node);
|
|
510
536
|
childElements.push(wrapped);
|
|
@@ -512,13 +538,23 @@ const processChildren = (children, targetNode, clearExisting = true) => {
|
|
|
512
538
|
targetNode.appendChild(node);
|
|
513
539
|
childElements.push(child);
|
|
514
540
|
}
|
|
515
|
-
} else if (child
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
541
|
+
} else if (isShadowDOMMarker(child)) {
|
|
542
|
+
// 5. Shadow DOM marker
|
|
543
|
+
if (targetNode instanceof ShadowRoot) {
|
|
544
|
+
console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
processShadowDOM(child, targetNode);
|
|
548
|
+
} else if (child && typeof child === 'object' && child.__xpath__ && child.__static__) {
|
|
549
|
+
// 6. XPath marker
|
|
550
|
+
const textNode = document.createTextNode('');
|
|
551
|
+
textNode.__xpathExpr = child.__xpath__;
|
|
552
|
+
targetNode.appendChild(textNode);
|
|
553
|
+
childElements.push(child);
|
|
520
554
|
}
|
|
521
|
-
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
walk(children);
|
|
522
558
|
|
|
523
559
|
|
|
524
560
|
|
|
@@ -551,7 +587,7 @@ const enhance = (selectorOrNode, options = {}) => {
|
|
|
551
587
|
|
|
552
588
|
// If it's already a Lightview element, use its domEl
|
|
553
589
|
const node = domNode.domEl || domNode;
|
|
554
|
-
if (!
|
|
590
|
+
if (!node || node.nodeType !== 1) return null;
|
|
555
591
|
|
|
556
592
|
const tagName = node.tagName.toLowerCase();
|
|
557
593
|
let el = domToElement.get(node);
|
|
@@ -670,8 +706,6 @@ const tags = new Proxy({}, {
|
|
|
670
706
|
});
|
|
671
707
|
|
|
672
708
|
const Lightview = {
|
|
673
|
-
state,
|
|
674
|
-
getState,
|
|
675
709
|
registerSchema: (name, definition) => internals.schemas.set(name, definition),
|
|
676
710
|
signal,
|
|
677
711
|
get: signal.get,
|