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/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
- * Check if an object is in Object DOM syntax
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
- if (keys.length === 0) return false;
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
- * Convert Object DOM syntax to vDOM syntax (recursive)
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
- // Array - recursively convert children
61
- if (Array.isArray(obj)) {
62
- return obj.map(convertObjectDOM);
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
- // Unknown object format - return as-is
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
- // Determine base theme (light or dark) for the main document
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
- let name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
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) loadedData = JSON.parse(item);
527
- } catch (e) { /* ignore */ }
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
- } else {
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
- const isArray = Array.isArray(obj);
561
- const isDate = obj instanceof Date;
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
- proxy = isArray || isDate ? createSpecialProxy(obj, monitor) : new Proxy(obj, {
565
- get(target, prop, receiver) {
566
- const signals = getOrSet(stateSignals, target, () => new Map());
567
- return proxyGet(target, prop, receiver, signals);
568
- },
569
- set(target, prop, value, receiver) {
570
- const signals = getOrSet(stateSignals, target, () => new Map());
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 && typeof window !== 'undefined' && window.Lightview && window.Lightview.effect) {
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 literal processing: converts "${...}" strings to reactive functions
601
- const processTemplateChild = (child, { state, signal }) => {
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 template = child;
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; // No transformation needed
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
- // Check if we're inside a script or style element - preserve raw content
617
- const isRawContent = parentTagName === 'script' || parentTagName === 'style';
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
- return () => {
633
- try {
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 attributes = {};
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 value = attr.value;
653
- if (!skipTemplateProcessing && value.includes('${')) {
654
- attributes[attr.name] = () => {
655
- try {
656
- const LV = window.Lightview;
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 handleSrcAttribute = async (el, src, tagName, { element, setupChildren }) => {
767
- // Skip standard src tags
768
- if (STANDARD_SRC_TAGS.includes(tagName)) return;
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
- const isPath = (s) => /^(https?:|\.|\/|\w)/.test(s) || /\.(html|json|vdom|odom)$/.test(s);
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
- let content = null;
773
- let isJson = false;
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 url = new URL(src, document.baseURI);
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 = url.pathname.split('.').pop().toLowerCase();
783
-
784
- if (ext === 'vdom' || ext === 'odom') {
785
- isJson = true;
786
- } else if (ext === 'html') {
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
- elements = Array.isArray(content) ? content : [content];
807
- } else if (isHtml) {
808
- // Check if escape attribute is set - if so, add as escaped text instead of parsing
809
- const shouldEscape = el.domEl.getAttribute('escape') === 'true';
810
- if (shouldEscape) {
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 selected = document.querySelectorAll(src);
830
- if (selected.length > 0) {
831
- elements = domToElements(Array.from(selected), element);
832
- // For selector content, create a string representation for hashing
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 === 0) return;
841
-
842
- // Get location attribute (default to 'innerhtml')
843
- const location = (el.domEl.getAttribute('location') || 'innerhtml').toLowerCase();
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
- // Generate content hash for deduplication
846
- const contentHash = hashContent(rawContent);
847
- const markerId = `${location}-${contentHash.slice(0, 8)}`;
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
- // Check if same content was already inserted
850
- let tracking = insertedContentMap.get(el.domEl);
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
- return;
878
- }
879
-
880
- // Handle different location modes
881
- switch (location) {
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; // Skip if node fully processed (for text)
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
- try {
1191
- // Determine if it's a single expression or a template string
1192
- // Single expression: "${...}" with no surrounding text and only one ${
1193
- const isSingleExpr = codeStr.trim().startsWith('${') &&
1194
- codeStr.trim().endsWith('}') &&
1195
- (codeStr.indexOf('${', 2) === -1);
1196
-
1197
- let fnBody;
1198
- if (isSingleExpr) {
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 '${'