lightview 2.0.0 → 2.0.2
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
CHANGED
|
@@ -11,16 +11,19 @@ A lightweight reactive UI library with signal-based reactivity and a clean API.
|
|
|
11
11
|
|
|
12
12
|
Access the full documentaion at [lightview.dev](/index.html).
|
|
13
13
|
|
|
14
|
+
This NPM package is both the library and the website supporting the library. The website is built using Lightview. The core library files are in the root directory. The Website entry point is index.html and the restr of the site is under ./docs. The site is served by a Cloudflare pages deployment.
|
|
15
|
+
|
|
14
16
|
**Core**: ~6KB | **With Hypermedia Extensions and Component Library Support**: ~18KB total
|
|
15
17
|
|
|
16
18
|
Fast: This [gallery of components](/docs/components) loads in about 1 second:
|
|
17
19
|
|
|
18
20
|
## Modular Architecture
|
|
19
21
|
|
|
20
|
-
Lightview is split into
|
|
22
|
+
Lightview is split into three files:
|
|
21
23
|
|
|
22
24
|
- **`lightview.js`** - Core reactivity (signals, state, effects, elements)
|
|
23
25
|
- **`lightview-x.js`** - Hypermedia extension (src fetching, href navigation, template literals, named registries, Object DOM syntax, UI component library support)
|
|
26
|
+
- **`lightview-router.js`** - Router (src fetching, href navigation, template literals, named registries, Object DOM syntax, UI component library support)
|
|
24
27
|
|
|
25
28
|
### API Behavior
|
|
26
29
|
|
|
@@ -49,6 +52,11 @@ Lightview is split into two files:
|
|
|
49
52
|
<!-- Full features (hypermedia + templates) -->
|
|
50
53
|
<script src="lightview.js"></script>
|
|
51
54
|
<script src="lightview-x.js"></script>
|
|
55
|
+
|
|
56
|
+
<!-- Full features (hypermedia + templates + router) -->
|
|
57
|
+
<script src="lightview.js"></script>
|
|
58
|
+
<script src="lightview-x.js"></script>
|
|
59
|
+
<script src="lightview-router.js"></script>
|
|
52
60
|
```
|
|
53
61
|
|
|
54
62
|
## Core Concepts
|
|
@@ -56,11 +64,11 @@ Lightview is split into two files:
|
|
|
56
64
|
**Lightview** provides four ways to build UIs:
|
|
57
65
|
|
|
58
66
|
1. **Tagged API** - Concise, Bau.js-style syntax: `tags.div(...)`
|
|
59
|
-
2. **
|
|
60
|
-
3. **
|
|
61
|
-
4. **
|
|
67
|
+
2. **vDOM Syntax** - JSON data structures: `{ tag: "div", attributes: {}, children: [] }`
|
|
68
|
+
3. **Object DOM Syntax** *(lightview-x)* - Compact: `{ div: { class: "foo", children: [] } }`
|
|
69
|
+
4. **HTML** *(lightview-x)* - Custom HTML elements.
|
|
62
70
|
|
|
63
|
-
All four approaches use the same underlying reactive system based on **signals**.
|
|
71
|
+
All four approaches use the same underlying reactive system based on **signals** and **state**.
|
|
64
72
|
|
|
65
73
|
## Installation
|
|
66
74
|
|
|
@@ -91,25 +99,7 @@ const app = div({ class: 'container' },
|
|
|
91
99
|
document.body.appendChild(app.domEl);
|
|
92
100
|
```
|
|
93
101
|
|
|
94
|
-
### Style 2:
|
|
95
|
-
|
|
96
|
-
```javascript
|
|
97
|
-
const { signal, element } = new Lightview();
|
|
98
|
-
|
|
99
|
-
const count = signal(0);
|
|
100
|
-
|
|
101
|
-
const app = element('div', { class: 'container' }, [
|
|
102
|
-
element('h1', {}, ['Counter App']),
|
|
103
|
-
element('p', {}, [() => `Count: ${count.value}`]),
|
|
104
|
-
element('button', {
|
|
105
|
-
onclick: () => count.value++
|
|
106
|
-
}, ['Increment'])
|
|
107
|
-
]);
|
|
108
|
-
|
|
109
|
-
document.body.appendChild(app.domEl);
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### Style 3: vDOM Syntax (Plain JSON)
|
|
102
|
+
### Style 2: vDOM Syntax (Plain JSON)
|
|
113
103
|
|
|
114
104
|
```javascript
|
|
115
105
|
const { signal, element } = new Lightview();
|
|
@@ -138,7 +128,7 @@ const app = element('div', { class: 'container' }, [
|
|
|
138
128
|
document.body.appendChild(app.domEl);
|
|
139
129
|
```
|
|
140
130
|
|
|
141
|
-
### Style
|
|
131
|
+
### Style 3: Object DOM Syntax (lightview-x)
|
|
142
132
|
|
|
143
133
|
Object DOM syntax provides a more compact way to define elements. Instead of `{ tag, attributes, children }`, you use `{ tag: { ...attributes, children } }`.
|
|
144
134
|
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
'Indicator', 'Input', 'Join', 'Kbd', 'Loading', 'Menu', 'Modal', 'Navbar',
|
|
22
22
|
'Pagination', 'Progress', 'Radial Progress', 'Radio', 'Range', 'Rating',
|
|
23
23
|
'Select', 'Skeleton', 'Stats', 'Steps', 'Swap', 'Table', 'Tabs', 'Textarea',
|
|
24
|
-
'
|
|
24
|
+
'Timeline', 'Toast', 'Toggle', 'Tooltip'
|
|
25
25
|
].sort(); // Sort alphabetically
|
|
26
26
|
|
|
27
27
|
const linksContainer = $('#sidebar-links');
|
|
@@ -50,4 +50,4 @@
|
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
</script>
|
|
53
|
+
</script>
|
|
@@ -142,7 +142,7 @@ const { tags, $ } = Lightview;
|
|
|
142
142
|
const { div, h1, p, style } = tags;
|
|
143
143
|
|
|
144
144
|
// 1. Build UI using Tagged Functions
|
|
145
|
-
// These functions return
|
|
145
|
+
// These functions return Lightview elements (access raw DOM via .domEl)
|
|
146
146
|
const App = div({ class: 'hero' },
|
|
147
147
|
h1('Welcome to Lightview'),
|
|
148
148
|
p('Lightview is a tiny library for building modern web interfaces.')
|
|
@@ -171,11 +171,11 @@ $('#app').content(style({}, \`
|
|
|
171
171
|
|
|
172
172
|
<h4>Key Concepts:</h4>
|
|
173
173
|
<ul style="padding-left: 1.25rem; color: var(--site-text-secondary);">
|
|
174
|
-
<li style="margin-bottom: 0.75rem;"><code>tags</code> — Every HTML tag is available as a function. <code>div(...)</code> returns a real <code
|
|
174
|
+
<li style="margin-bottom: 0.75rem;"><code>tags</code> — Every HTML tag is available as a function. <code>div(...)</code> returns a virtual proxy; the real DOM element is accessible via <code>.domEl</code>.</li>
|
|
175
175
|
<li style="margin-bottom: 0.75rem;"><code>$(selector)</code> — A powerful utility for selecting elements and manipulating them.</li>
|
|
176
|
-
<li style="margin-bottom: 0.75rem;"><code>.content(node, location)</code> — Replaces or appends content.
|
|
176
|
+
<li style="margin-bottom: 0.75rem;"><code>.content(node, location)</code> — Replaces or appends content. It automatically unboxes <code>.domEl</code> for you.</li>
|
|
177
177
|
</ul>
|
|
178
|
-
<p>
|
|
178
|
+
<p>Lightview elements are thin proxies over standard DOM elements, providing reactivity with minimal overhead.</p>
|
|
179
179
|
`
|
|
180
180
|
},
|
|
181
181
|
2: {
|
package/package.json
CHANGED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Migrate all components to DaisyUI styling pattern with examplify docs
|
|
3
|
-
---
|
|
4
|
-
|
|
5
|
-
# DaisyUI Component Migration Plan
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
Migrate all Lightview components to use DaisyUI styling (like the reference `input.js` pattern):
|
|
9
|
-
- Use DaisyUI classes directly (no BEM `.lv-*` classes)
|
|
10
|
-
- Remove parallel `.css` files where possible
|
|
11
|
-
- Use custom CSS only for advanced features not in DaisyUI
|
|
12
|
-
- All components support Shadow DOM (global setting with local override)
|
|
13
|
-
- Remove `getIcon()` usage - use inline SVGs or children
|
|
14
|
-
- Update all `.html` docs to use `examplify()` with basic + reactive examples
|
|
15
|
-
- Match DaisyUI directory structure (delete `components/core/`)
|
|
16
|
-
|
|
17
|
-
## Reference Pattern (from input.js)
|
|
18
|
-
```javascript
|
|
19
|
-
// Key patterns:
|
|
20
|
-
// 1. No CSS import - uses DaisyUI classes directly
|
|
21
|
-
// 2. Uses fieldset/legend pattern for form fields
|
|
22
|
-
// 3. Signal-based reactive state
|
|
23
|
-
// 4. Controlled/uncontrolled modes
|
|
24
|
-
// 5. Validation support
|
|
25
|
-
// 6. Shadow DOM support with useShadow prop
|
|
26
|
-
// 7. Auto-register with LightviewX
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
## Component Status Tracker
|
|
30
|
-
|
|
31
|
-
### Batch 1: Form Inputs ✅ COMPLETE
|
|
32
|
-
- [x] 1. Input (data-input/input.js) - DONE
|
|
33
|
-
- [x] 2. Textarea (data-input/textarea.js) - DONE
|
|
34
|
-
- [x] 3. Select (data-input/select.js) - DONE
|
|
35
|
-
- [x] 4. Checkbox (data-input/checkbox.js) - DONE
|
|
36
|
-
- [x] 5. Radio (data-input/radio.js) - DONE (also added RadioGroup)
|
|
37
|
-
- [x] 6. Toggle (data-input/toggle.js) - DONE
|
|
38
|
-
- [x] 7. Range (data-input/range.js) - DONE
|
|
39
|
-
- [x] 8. Rating (data-input/rating.js) - DONE
|
|
40
|
-
- [x] 9. File Input (data-input/file-input.js) - DONE
|
|
41
|
-
|
|
42
|
-
### Batch 2: Actions & Feedback ✅ COMPLETE
|
|
43
|
-
- [x] 10. Button (actions/button.js) - DONE (docs updated with examplify)
|
|
44
|
-
- [x] 11. Modal (actions/modal.js) - DONE (docs updated with examplify)
|
|
45
|
-
- [x] 12. Drawer (layout/drawer.js) - DONE (docs updated with examplify)
|
|
46
|
-
- [x] 13. Dropdown (actions/dropdown.js) - DONE (docs updated with examplify)
|
|
47
|
-
- [x] 14. Swap (actions/swap.js) - DONE (docs updated with examplify)
|
|
48
|
-
- [x] 15. Alert (data-display/alert.js) - DONE (docs updated with examplify)
|
|
49
|
-
- [x] 16. Toast (data-display/toast.js) - DONE (docs updated with examplify)
|
|
50
|
-
- [x] 17. Loading (data-display/loading.js) - DONE (docs updated with examplify)
|
|
51
|
-
- [x] 18. Progress (data-display/progress.js) - DONE (docs updated with examplify)
|
|
52
|
-
- [x] 19. Tooltip (data-display/tooltip.js) - DONE (docs updated with examplify)
|
|
53
|
-
|
|
54
|
-
### Batch 3: Data Display ✅ COMPLETE
|
|
55
|
-
- [x] 20. Badge (data-display/badge.js) - DONE (docs updated with examplify)
|
|
56
|
-
- [x] 21. Card (data-display/card.js) - DONE (docs updated with examplify)
|
|
57
|
-
- [x] 22. Avatar (data-display/avatar.js) - DONE (docs updated with examplify)
|
|
58
|
-
- [x] 23. Table (data-display/table.js) - DONE (docs updated with examplify)
|
|
59
|
-
- [x] 24. Accordion (data-display/accordion.js) - DONE (docs updated with examplify)
|
|
60
|
-
- [x] 25. Collapse (data-display/collapse.js) - DONE (docs updated with examplify)
|
|
61
|
-
- [x] 26. Carousel (data-display/carousel.js) - DONE (docs updated with examplify)
|
|
62
|
-
- [x] 27. Chat (data-display/chat.js) - DONE (docs updated with examplify)
|
|
63
|
-
- [x] 28. Countdown (data-display/countdown.js) - DONE (docs updated with examplify)
|
|
64
|
-
- [x] 29. Diff (data-display/diff.js) - DONE (docs updated with examplify)
|
|
65
|
-
- [x] 30. Kbd (data-display/kbd.js) - DONE (docs updated with examplify)
|
|
66
|
-
- [x] 31. Stats (data-display/stats.js) - DONE (docs updated with examplify)
|
|
67
|
-
- [x] 32. Timeline (data-display/timeline.js) - DONE (docs updated with examplify)
|
|
68
|
-
- [x] 33. Skeleton (data-display/skeleton.js) - DONE (docs updated with examplify)
|
|
69
|
-
|
|
70
|
-
### Batch 4: Navigation & Layout ✅ COMPLETE
|
|
71
|
-
- [x] 34. Tabs (navigation/tabs.js) - DONE (docs updated with examplify)
|
|
72
|
-
- [x] 35. Menu (navigation/menu.js) - DONE (docs updated with examplify)
|
|
73
|
-
- [x] 36. Breadcrumbs (navigation/breadcrumbs.js) - DONE (docs updated with examplify)
|
|
74
|
-
- [x] 37. Pagination (navigation/pagination.js) - DONE (docs updated with examplify)
|
|
75
|
-
- [x] 38. Steps (navigation/steps.js) - DONE (docs updated with examplify)
|
|
76
|
-
- [x] 39. Dock (navigation/dock.js) - DONE (docs updated with examplify)
|
|
77
|
-
- [x] 40. Navbar (layout/navbar.js) - DONE (docs updated with examplify)
|
|
78
|
-
- [x] 41. Footer (layout/footer.js) - DONE (docs updated with examplify)
|
|
79
|
-
- [x] 42. Hero (layout/hero.js) - DONE (docs updated with examplify)
|
|
80
|
-
- [x] 43. Divider (layout/divider.js) - DONE (docs updated with examplify)
|
|
81
|
-
- [x] 44. Indicator (layout/indicator.js) - DONE (docs updated with examplify)
|
|
82
|
-
- [x] 45. Join (layout/join.js) - DONE (docs updated with examplify)
|
|
83
|
-
|
|
84
|
-
### Batch 5: Specialized ✅ COMPLETE
|
|
85
|
-
- [x] 46. Theme Controller (actions/theme-controller.js) - DONE (docs updated with examplify)
|
|
86
|
-
- [x] 47. Chart (data-display/chart.js) - DONE (docs updated with examplify)
|
|
87
|
-
- [x] 48. Radial Progress (data-display/radial-progress.js) - DONE (docs updated with examplify)
|
|
88
|
-
|
|
89
|
-
### Batch 6: Shadow DOM Support
|
|
90
|
-
Add `useShadow` prop and Shadow DOM rendering to all components following the `input.js` pattern:
|
|
91
|
-
- Check `LightviewX.shouldUseShadow(useShadow)` to determine if shadow DOM should be used
|
|
92
|
-
- Get adopted stylesheets via `LightviewX.getAdoptedStyleSheets()`
|
|
93
|
-
- Get current theme from `document.documentElement.getAttribute('data-theme')`
|
|
94
|
-
- Wrap component in `shadowDOM({ mode: 'open', adoptedStyleSheets }, div({ 'data-theme': currentTheme }, componentEl))`
|
|
95
|
-
|
|
96
|
-
Components to update:
|
|
97
|
-
- [x] Button (actions/button.js) - DONE
|
|
98
|
-
- [x] Dropdown (actions/dropdown.js)
|
|
99
|
-
- [x] Modal (actions/modal.js)
|
|
100
|
-
- [x] Swap (actions/swap.js)
|
|
101
|
-
- [x] Alert (data-display/alert.js)
|
|
102
|
-
- [x] Toast (data-display/toast.js)
|
|
103
|
-
- [x] Loading (data-display/loading.js)
|
|
104
|
-
- [x] Progress (data-display/progress.js)
|
|
105
|
-
- [x] Drawer (layout/drawer.js)
|
|
106
|
-
- [x] Tooltip (data-display/tooltip.js)
|
|
107
|
-
- [x] Badge (data-display/badge.js)
|
|
108
|
-
- [x] Card (data-display/card.js)
|
|
109
|
-
- [x] Avatar (data-display/avatar.js)
|
|
110
|
-
- [x] Table (data-display/table.js)
|
|
111
|
-
- [x] Accordion (data-display/accordion.js)
|
|
112
|
-
- [x] Collapse (data-display/collapse.js)
|
|
113
|
-
- [x] Carousel (data-display/carousel.js)
|
|
114
|
-
- [x] Chat (data-display/chat.js)
|
|
115
|
-
- [x] Countdown (data-display/countdown.js)
|
|
116
|
-
- [x] Diff (data-display/diff.js)
|
|
117
|
-
- [x] Kbd (data-display/kbd.js)
|
|
118
|
-
- [x] Stats (data-display/stats.js)
|
|
119
|
-
- [x] Timeline (data-display/timeline.js)
|
|
120
|
-
- [x] Skeleton (data-display/skeleton.js)
|
|
121
|
-
- [x] Tabs (navigation/tabs.js)
|
|
122
|
-
- [x] Menu (navigation/menu.js)
|
|
123
|
-
- [x] Breadcrumbs (navigation/breadcrumbs.js)
|
|
124
|
-
- [x] Pagination (navigation/pagination.js)
|
|
125
|
-
- [x] Steps (navigation/steps.js)
|
|
126
|
-
- [x] Dock (navigation/dock.js)
|
|
127
|
-
- [x] Navbar (layout/navbar.js)
|
|
128
|
-
- [x] Footer (layout/footer.js)
|
|
129
|
-
- [x] Hero (layout/hero.js)
|
|
130
|
-
- [x] Divider (layout/divider.js)
|
|
131
|
-
- [x] Indicator (layout/indicator.js)
|
|
132
|
-
- [x] Join (layout/join.js)
|
|
133
|
-
- [x] Theme Controller (actions/theme-controller.js)
|
|
134
|
-
- [x] Chart (data-display/chart.js)
|
|
135
|
-
- [x] Radial Progress (data-display/radial-progress.js)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
## Cleanup Tasks
|
|
139
|
-
- [x] Delete components/core/ directory
|
|
140
|
-
- [x] Delete components/display/ directory (after consolidation)
|
|
141
|
-
- [x] Delete components/feedback/ directory (after consolidation)
|
|
142
|
-
- [x] Delete unused .css files
|
|
143
|
-
- [x] Delete components/utils/icons.js
|
|
144
|
-
- [x] Update components/index.js exports
|
|
145
|
-
|
|
146
|
-
## Per-Component Checklist
|
|
147
|
-
For each component:
|
|
148
|
-
1. [ ] Refactor .js to use DaisyUI classes
|
|
149
|
-
2. [ ] Add Shadow DOM support (useShadow prop)
|
|
150
|
-
3. [ ] Add validation/error support where applicable
|
|
151
|
-
4. [ ] Add controlled/uncontrolled state support
|
|
152
|
-
5. [ ] Remove getIcon() usage
|
|
153
|
-
6. [ ] Update .html docs with examplify (basic + reactive examples)
|
|
154
|
-
7. [ ] Delete old .css file if no longer needed
|
|
155
|
-
8. [ ] Test in browser
|
package/lightview.js.backup
DELETED
|
@@ -1,793 +0,0 @@
|
|
|
1
|
-
(() => {
|
|
2
|
-
// ============= SIGNALS =============
|
|
3
|
-
|
|
4
|
-
let currentEffect = null;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const getOrSet = (map, key, factory) => {
|
|
8
|
-
let v = map.get(key);
|
|
9
|
-
if (!v) {
|
|
10
|
-
v = factory();
|
|
11
|
-
map.set(key, v);
|
|
12
|
-
}
|
|
13
|
-
return v;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const nodeState = new WeakMap();
|
|
17
|
-
const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
|
|
18
|
-
|
|
19
|
-
const signalRegistry = new Map();
|
|
20
|
-
|
|
21
|
-
// Helper to attach .for() method to arrays for list reconciliation
|
|
22
|
-
const enhanceArray = (arr) => {
|
|
23
|
-
if (!Array.isArray(arr)) return;
|
|
24
|
-
// Avoid redefining if already present
|
|
25
|
-
if (arr.hasOwnProperty('for')) return;
|
|
26
|
-
Object.defineProperty(arr, 'for', {
|
|
27
|
-
configurable: true,
|
|
28
|
-
enumerable: false,
|
|
29
|
-
value: function (fn) {
|
|
30
|
-
return { [LIST_MARKER]: true, items: this, fn };
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const signal = (initialValue, name) => {
|
|
36
|
-
enhanceArray(initialValue);
|
|
37
|
-
let value = initialValue;
|
|
38
|
-
const subscribers = new Set();
|
|
39
|
-
|
|
40
|
-
const f = (...args) => {
|
|
41
|
-
if (args.length === 0) return f.value;
|
|
42
|
-
f.value = args[0];
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
Object.defineProperty(f, 'value', {
|
|
46
|
-
get() {
|
|
47
|
-
if (currentEffect) {
|
|
48
|
-
subscribers.add(currentEffect);
|
|
49
|
-
currentEffect.dependencies.add(subscribers);
|
|
50
|
-
}
|
|
51
|
-
return value;
|
|
52
|
-
},
|
|
53
|
-
set(newValue) {
|
|
54
|
-
if (value !== newValue) {
|
|
55
|
-
enhanceArray(newValue);
|
|
56
|
-
value = newValue;
|
|
57
|
-
// Copy subscribers to avoid infinite loop when effect re-subscribes during iteration
|
|
58
|
-
[...subscribers].forEach(effect => effect());
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
if (name) {
|
|
64
|
-
signalRegistry.set(name, f);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return f;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
signal.get = (name, defaultValue) => {
|
|
71
|
-
if (!signalRegistry.has(name) && defaultValue !== undefined) {
|
|
72
|
-
return signal(defaultValue, name);
|
|
73
|
-
}
|
|
74
|
-
return signalRegistry.get(name);
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const effect = (fn) => {
|
|
78
|
-
const execute = () => {
|
|
79
|
-
if (!execute.active) return;
|
|
80
|
-
// Cleanup old dependencies
|
|
81
|
-
execute.dependencies.forEach(dep => dep.delete(execute));
|
|
82
|
-
execute.dependencies.clear();
|
|
83
|
-
|
|
84
|
-
currentEffect = execute;
|
|
85
|
-
fn();
|
|
86
|
-
currentEffect = null;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
execute.active = true;
|
|
90
|
-
execute.dependencies = new Set();
|
|
91
|
-
execute.stop = () => {
|
|
92
|
-
execute.dependencies.forEach(dep => dep.delete(execute));
|
|
93
|
-
execute.dependencies.clear();
|
|
94
|
-
execute.active = false;
|
|
95
|
-
};
|
|
96
|
-
execute();
|
|
97
|
-
return execute;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
const trackEffect = (node, effectFn) => {
|
|
101
|
-
const state = getOrSet(nodeState, node, nodeStateFactory);
|
|
102
|
-
if (!state.effects) state.effects = [];
|
|
103
|
-
state.effects.push(effectFn);
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const computed = (fn) => {
|
|
107
|
-
const sig = signal(undefined);
|
|
108
|
-
effect(() => {
|
|
109
|
-
sig.value = fn();
|
|
110
|
-
});
|
|
111
|
-
return sig;
|
|
112
|
-
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// ============= SHADOW DOM SUPPORT =============
|
|
116
|
-
// Marker symbol to identify shadowDOM directives
|
|
117
|
-
const SHADOW_DOM_MARKER = Symbol('lightview.shadowDOM');
|
|
118
|
-
const LIST_MARKER = Symbol('lightview.list');
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Create a shadowDOM directive marker
|
|
122
|
-
* @param {Object} attributes - { mode: 'open'|'closed', styles?: string[], adoptedStyleSheets?: CSSStyleSheet[] }
|
|
123
|
-
* @param {Array} children - Children to render inside the shadow root
|
|
124
|
-
* @returns {Object} - Marker object for setupChildren to process
|
|
125
|
-
*/
|
|
126
|
-
const createShadowDOMMarker = (attributes, children) => ({
|
|
127
|
-
[SHADOW_DOM_MARKER]: true,
|
|
128
|
-
mode: attributes.mode || 'open',
|
|
129
|
-
styles: attributes.styles || [],
|
|
130
|
-
adoptedStyleSheets: attributes.adoptedStyleSheets || [],
|
|
131
|
-
children
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Check if an object is a shadowDOM marker
|
|
136
|
-
*/
|
|
137
|
-
const isShadowDOMMarker = (obj) => obj && typeof obj === 'object' && obj[SHADOW_DOM_MARKER] === true;
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Process a shadowDOM marker by attaching shadow root and rendering children
|
|
141
|
-
* @param {Object} marker - The shadowDOM marker
|
|
142
|
-
* @param {HTMLElement} parentNode - The DOM node to attach shadow to
|
|
143
|
-
*/
|
|
144
|
-
const processShadowDOM = (marker, parentNode) => {
|
|
145
|
-
// Don't attach if already has shadow root
|
|
146
|
-
if (parentNode.shadowRoot) {
|
|
147
|
-
console.warn('Lightview: Element already has a shadowRoot, skipping shadowDOM directive');
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// Attach shadow root
|
|
152
|
-
const shadowRoot = parentNode.attachShadow({ mode: marker.mode });
|
|
153
|
-
|
|
154
|
-
// Split adoptedStyleSheets into sheets and urls
|
|
155
|
-
const sheets = [];
|
|
156
|
-
const linkUrls = [...(marker.styles || [])];
|
|
157
|
-
|
|
158
|
-
if (marker.adoptedStyleSheets && marker.adoptedStyleSheets.length > 0) {
|
|
159
|
-
marker.adoptedStyleSheets.forEach(item => {
|
|
160
|
-
if (item instanceof CSSStyleSheet) {
|
|
161
|
-
sheets.push(item);
|
|
162
|
-
} else if (typeof item === 'string') {
|
|
163
|
-
linkUrls.push(item);
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Handle adoptedStyleSheets (modern, efficient approach)
|
|
169
|
-
if (sheets.length > 0) {
|
|
170
|
-
try {
|
|
171
|
-
shadowRoot.adoptedStyleSheets = sheets;
|
|
172
|
-
} catch (e) {
|
|
173
|
-
console.warn('Lightview: adoptedStyleSheets not supported');
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// Inject stylesheet links
|
|
178
|
-
for (const styleUrl of linkUrls) {
|
|
179
|
-
const link = document.createElement('link');
|
|
180
|
-
link.rel = 'stylesheet';
|
|
181
|
-
link.href = styleUrl;
|
|
182
|
-
shadowRoot.appendChild(link);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Setup children inside shadow root
|
|
186
|
-
if (marker.children && marker.children.length > 0) {
|
|
187
|
-
setupChildrenInTarget(marker.children, shadowRoot);
|
|
188
|
-
}
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
// ============= REACTIVE UI =============
|
|
192
|
-
const SVG_TAGS = new Set([
|
|
193
|
-
'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'marker',
|
|
194
|
-
'pattern', 'mask', 'image', 'text', 'tspan', 'foreignObject', 'use', 'symbol', 'clipPath',
|
|
195
|
-
'linearGradient', 'radialGradient', 'stop', 'filter', 'animate', 'animateMotion',
|
|
196
|
-
'animateTransform', 'mpath', 'desc', 'metadata', 'title', 'feBlend', 'feColorMatrix',
|
|
197
|
-
'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
|
|
198
|
-
'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB',
|
|
199
|
-
'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
|
|
200
|
-
'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight',
|
|
201
|
-
'feTile', 'feTurbulence', 'view'
|
|
202
|
-
]);
|
|
203
|
-
|
|
204
|
-
const domToElement = new WeakMap();
|
|
205
|
-
|
|
206
|
-
const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
|
|
207
|
-
const el = {
|
|
208
|
-
tag,
|
|
209
|
-
attributes,
|
|
210
|
-
children,
|
|
211
|
-
get domEl() { return domNode; }
|
|
212
|
-
};
|
|
213
|
-
const proxy = makeReactive(el);
|
|
214
|
-
domToElement.set(domNode, proxy);
|
|
215
|
-
return proxy;
|
|
216
|
-
};
|
|
217
|
-
|
|
218
|
-
const element = (tag, attributes = {}, children = []) => {
|
|
219
|
-
if (customTags[tag]) tag = customTags[tag];
|
|
220
|
-
// If tag is a function (component), call it and process the result
|
|
221
|
-
if (typeof tag === 'function') {
|
|
222
|
-
const result = tag({ ...attributes }, children);
|
|
223
|
-
return processComponentResult(result);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Special handling for shadowDOM pseudo-element
|
|
227
|
-
if (tag === 'shadowDOM') {
|
|
228
|
-
return createShadowDOMMarker(attributes, children);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const isSvg = SVG_TAGS.has(tag.toLowerCase());
|
|
232
|
-
const domNode = isSvg
|
|
233
|
-
? document.createElementNS('http://www.w3.org/2000/svg', tag)
|
|
234
|
-
: document.createElement(tag);
|
|
235
|
-
const proxy = wrapDomElement(domNode, tag, attributes, children);
|
|
236
|
-
proxy.attributes = attributes;
|
|
237
|
-
proxy.children = children;
|
|
238
|
-
return proxy;
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// Process component function return value (HTML string, DOM node, vDOM, or Object DOM)
|
|
242
|
-
const processComponentResult = (result) => {
|
|
243
|
-
if (!result) return null;
|
|
244
|
-
|
|
245
|
-
// Already a Lightview element
|
|
246
|
-
if (result.domEl) return result;
|
|
247
|
-
|
|
248
|
-
// DOM node - wrap it
|
|
249
|
-
if (result instanceof HTMLElement) {
|
|
250
|
-
return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// HTML string - parse and wrap
|
|
254
|
-
if (typeof result === 'string') {
|
|
255
|
-
const template = document.createElement('template');
|
|
256
|
-
template.innerHTML = result.trim();
|
|
257
|
-
const content = template.content;
|
|
258
|
-
// If single element, return it; otherwise wrap in a fragment-like span
|
|
259
|
-
if (content.childNodes.length === 1 && content.firstChild instanceof HTMLElement) {
|
|
260
|
-
const el = content.firstChild;
|
|
261
|
-
return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
|
|
262
|
-
} else {
|
|
263
|
-
const wrapper = document.createElement('span');
|
|
264
|
-
wrapper.style.display = 'contents';
|
|
265
|
-
wrapper.appendChild(content);
|
|
266
|
-
return wrapDomElement(wrapper, 'span', {}, []);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// vDOM object with tag property
|
|
271
|
-
if (typeof result === 'object' && result.tag) {
|
|
272
|
-
return element(result.tag, result.attributes || {}, result.children || []);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// Object DOM syntax will be handled by processChild hook in lightview-x
|
|
276
|
-
// But we can do basic detection here
|
|
277
|
-
if (typeof result === 'object') {
|
|
278
|
-
const keys = Object.keys(result);
|
|
279
|
-
if (keys.length === 1 && typeof result[keys[0]] === 'object') {
|
|
280
|
-
const tag = keys[0];
|
|
281
|
-
const content = result[tag];
|
|
282
|
-
const { children, ...attributes } = content;
|
|
283
|
-
return element(tag, attributes, children || []);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return null;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
const makeReactive = (el) => {
|
|
291
|
-
const domNode = el.domEl;
|
|
292
|
-
|
|
293
|
-
return new Proxy(el, {
|
|
294
|
-
set(target, prop, value) {
|
|
295
|
-
if (prop === 'attributes') {
|
|
296
|
-
target[prop] = makeReactiveAttributes(value, domNode);
|
|
297
|
-
} else if (prop === 'children') {
|
|
298
|
-
target[prop] = setupChildren(value, domNode);
|
|
299
|
-
} else {
|
|
300
|
-
target[prop] = value;
|
|
301
|
-
}
|
|
302
|
-
return true;
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
// Boolean attributes that should be present/absent rather than having a value
|
|
308
|
-
const BOOLEAN_ATTRIBUTES = new Set([
|
|
309
|
-
'disabled', 'checked', 'readonly', 'required', 'hidden', 'autofocus',
|
|
310
|
-
'autoplay', 'controls', 'loop', 'muted', 'default', 'defer', 'async',
|
|
311
|
-
'novalidate', 'formnovalidate', 'open', 'selected', 'multiple', 'reversed',
|
|
312
|
-
'ismap', 'nomodule', 'playsinline', 'allowfullscreen', 'inert'
|
|
313
|
-
]);
|
|
314
|
-
|
|
315
|
-
// Set attribute with proper handling of boolean attributes and undefined/null values
|
|
316
|
-
const setAttributeValue = (domNode, key, value) => {
|
|
317
|
-
const isBooleanAttr = BOOLEAN_ATTRIBUTES.has(key.toLowerCase());
|
|
318
|
-
|
|
319
|
-
if (value === null || value === undefined) {
|
|
320
|
-
domNode.removeAttribute(key);
|
|
321
|
-
} else if (isBooleanAttr) {
|
|
322
|
-
if (value && value !== 'false') {
|
|
323
|
-
domNode.setAttribute(key, '');
|
|
324
|
-
} else {
|
|
325
|
-
domNode.removeAttribute(key);
|
|
326
|
-
}
|
|
327
|
-
} else {
|
|
328
|
-
domNode.setAttribute(key, value);
|
|
329
|
-
}
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
const makeReactiveAttributes = (attributes, domNode) => {
|
|
333
|
-
const reactiveAttrs = {};
|
|
334
|
-
|
|
335
|
-
for (let [key, value] of Object.entries(attributes)) {
|
|
336
|
-
if (key === 'onmount' || key === 'onunmount') {
|
|
337
|
-
const state = getOrSet(nodeState, domNode, nodeStateFactory);
|
|
338
|
-
state[key] = value;
|
|
339
|
-
|
|
340
|
-
if (key === 'onmount' && domNode.isConnected) {
|
|
341
|
-
value(domNode);
|
|
342
|
-
}
|
|
343
|
-
} else if (key.startsWith('on')) {
|
|
344
|
-
// Event handler
|
|
345
|
-
if (typeof value === 'function') {
|
|
346
|
-
// Function handler - use addEventListener
|
|
347
|
-
const eventName = key.slice(2).toLowerCase();
|
|
348
|
-
domNode.addEventListener(eventName, value);
|
|
349
|
-
} else if (typeof value === 'string') {
|
|
350
|
-
// String handler (from parsed HTML) - use setAttribute
|
|
351
|
-
// Browser will compile the string into a handler function
|
|
352
|
-
domNode.setAttribute(key, value);
|
|
353
|
-
}
|
|
354
|
-
reactiveAttrs[key] = value;
|
|
355
|
-
} else if (typeof value === 'function') {
|
|
356
|
-
// Reactive binding
|
|
357
|
-
const runner = effect(() => {
|
|
358
|
-
const result = value();
|
|
359
|
-
if (key === 'style' && typeof result === 'object') {
|
|
360
|
-
Object.assign(domNode.style, result);
|
|
361
|
-
} else {
|
|
362
|
-
setAttributeValue(domNode, key, result);
|
|
363
|
-
}
|
|
364
|
-
});
|
|
365
|
-
trackEffect(domNode, runner);
|
|
366
|
-
reactiveAttrs[key] = value;
|
|
367
|
-
} else if (key === 'style' && typeof value === 'object') {
|
|
368
|
-
// Handle style object which may contain reactive values
|
|
369
|
-
Object.entries(value).forEach(([styleKey, styleValue]) => {
|
|
370
|
-
if (typeof styleValue === 'function') {
|
|
371
|
-
const runner = effect(() => {
|
|
372
|
-
domNode.style[styleKey] = styleValue();
|
|
373
|
-
});
|
|
374
|
-
trackEffect(domNode, runner);
|
|
375
|
-
} else {
|
|
376
|
-
domNode.style[styleKey] = styleValue;
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
reactiveAttrs[key] = value;
|
|
380
|
-
} else {
|
|
381
|
-
// Static attribute - handle undefined/null/boolean properly
|
|
382
|
-
setAttributeValue(domNode, key, value);
|
|
383
|
-
reactiveAttrs[key] = value;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
return reactiveAttrs;
|
|
388
|
-
};
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Core child processing logic - shared between setupChildren and setupChildrenInTarget
|
|
392
|
-
* @param {Array} children - Children to process
|
|
393
|
-
* @param {HTMLElement|ShadowRoot} targetNode - Where to append children
|
|
394
|
-
* @param {boolean} clearExisting - Whether to clear existing content
|
|
395
|
-
* @returns {Array} - Processed child elements
|
|
396
|
-
*/
|
|
397
|
-
const processChildren = (children, targetNode, clearExisting = true) => {
|
|
398
|
-
if (clearExisting && targetNode.innerHTML !== undefined) {
|
|
399
|
-
targetNode.innerHTML = ''; // Clear existing
|
|
400
|
-
}
|
|
401
|
-
const childElements = [];
|
|
402
|
-
|
|
403
|
-
// Check if we're processing children of script or style elements
|
|
404
|
-
// These need raw text content preserved, not reactive transformations
|
|
405
|
-
const isSpecialElement = targetNode.tagName &&
|
|
406
|
-
(targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
|
|
407
|
-
|
|
408
|
-
for (let child of children) {
|
|
409
|
-
// Allow extensions to transform children (e.g., template literals)
|
|
410
|
-
// BUT skip for script/style elements which need raw content
|
|
411
|
-
if (Lightview.hooks.processChild && !isSpecialElement) {
|
|
412
|
-
child = Lightview.hooks.processChild(child) ?? child;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
// Handle nested arrays (flattening)
|
|
416
|
-
if (Array.isArray(child)) {
|
|
417
|
-
childElements.push(...processChildren(child, targetNode, false));
|
|
418
|
-
continue;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Handle shadowDOM markers - attach shadow to parent and process shadow children
|
|
422
|
-
if (isShadowDOMMarker(child)) {
|
|
423
|
-
// targetNode is the parent element that should get the shadow root
|
|
424
|
-
// For ShadowRoot targets, we can't attach another shadow, so warn
|
|
425
|
-
if (targetNode instanceof ShadowRoot) {
|
|
426
|
-
console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
processShadowDOM(child, targetNode);
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const type = typeof child;
|
|
434
|
-
if (type === 'function') {
|
|
435
|
-
const result = child();
|
|
436
|
-
// Determine if the result implies complex content (DOM/vDOM/Array)
|
|
437
|
-
// Treat as complex if it's an object (including arrays) but not null
|
|
438
|
-
const isComplex = result && (typeof result === 'object' || Array.isArray(result));
|
|
439
|
-
|
|
440
|
-
if (isComplex) {
|
|
441
|
-
// Reactive element, vDOM object, or list of items
|
|
442
|
-
// Use a stable wrapper div to hold the reactive content
|
|
443
|
-
const wrapper = document.createElement('span');
|
|
444
|
-
wrapper.style.display = 'contents';
|
|
445
|
-
targetNode.appendChild(wrapper);
|
|
446
|
-
|
|
447
|
-
let runner;
|
|
448
|
-
let oldState = []; // State for list reconciliation
|
|
449
|
-
|
|
450
|
-
const update = () => {
|
|
451
|
-
const val = child();
|
|
452
|
-
// Check if wrapper is still in the DOM (skip check on first run)
|
|
453
|
-
if (runner && !wrapper.parentNode) {
|
|
454
|
-
runner.stop();
|
|
455
|
-
return;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
if (val && val[LIST_MARKER]) {
|
|
459
|
-
// Optimized list reconciliation
|
|
460
|
-
oldState = reconcileList(wrapper, oldState, val.items, val.fn);
|
|
461
|
-
} else {
|
|
462
|
-
// Full re-render fallback
|
|
463
|
-
oldState = []; // Reset optimization state
|
|
464
|
-
const childrenToProcess = Array.isArray(val) ? val : [val];
|
|
465
|
-
// processChildren handles clearing existing content via 3rd arg=true
|
|
466
|
-
processChildren(childrenToProcess, wrapper, true);
|
|
467
|
-
}
|
|
468
|
-
};
|
|
469
|
-
|
|
470
|
-
runner = effect(update);
|
|
471
|
-
trackEffect(wrapper, runner);
|
|
472
|
-
childElements.push(child);
|
|
473
|
-
} else {
|
|
474
|
-
// Reactive text node for primitives
|
|
475
|
-
const textNode = document.createTextNode('');
|
|
476
|
-
targetNode.appendChild(textNode);
|
|
477
|
-
const runner = effect(() => {
|
|
478
|
-
const val = child();
|
|
479
|
-
textNode.textContent = val !== undefined ? val : '';
|
|
480
|
-
});
|
|
481
|
-
trackEffect(textNode, runner);
|
|
482
|
-
childElements.push(child);
|
|
483
|
-
}
|
|
484
|
-
} else if (['string', 'number', 'boolean', 'symbol'].includes(type)) {
|
|
485
|
-
// Static text
|
|
486
|
-
targetNode.appendChild(document.createTextNode(child));
|
|
487
|
-
childElements.push(child);
|
|
488
|
-
} else if (child && type === 'object' && child.tag) {
|
|
489
|
-
// Child element (already wrapped or plain object) - tag can be string or function
|
|
490
|
-
const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
|
|
491
|
-
targetNode.appendChild(childEl.domEl);
|
|
492
|
-
childElements.push(childEl);
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return childElements;
|
|
497
|
-
};
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* Efficiently reconcile list items to minimize DOM operations
|
|
501
|
-
*/
|
|
502
|
-
const reconcileList = (parent, oldState, newItems, renderFn) => {
|
|
503
|
-
const reuse = new Map();
|
|
504
|
-
|
|
505
|
-
// 1. Map old nodes for potential reuse
|
|
506
|
-
for (const entry of oldState) {
|
|
507
|
-
let list = reuse.get(entry.item);
|
|
508
|
-
if (!list) { list = []; reuse.set(entry.item, list); }
|
|
509
|
-
list.push(entry.node);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const newState = [];
|
|
513
|
-
|
|
514
|
-
// 2. Build new state, reusing nodes where possible
|
|
515
|
-
for (const item of newItems) {
|
|
516
|
-
let node;
|
|
517
|
-
const list = reuse.get(item);
|
|
518
|
-
if (list && list.length > 0) {
|
|
519
|
-
// Reuse existing node for this item
|
|
520
|
-
node = list.shift();
|
|
521
|
-
} else {
|
|
522
|
-
// Create new node from render function
|
|
523
|
-
const raw = renderFn(item);
|
|
524
|
-
|
|
525
|
-
// Use a temporary container to process the result into DOM nodes
|
|
526
|
-
const temp = document.createElement('div');
|
|
527
|
-
// processChildren returns the Lightview wrappers/proxies; we need the DOM nodes
|
|
528
|
-
processChildren([raw], temp, false);
|
|
529
|
-
|
|
530
|
-
if (temp.childNodes.length === 1) {
|
|
531
|
-
node = temp.firstChild;
|
|
532
|
-
} else if (temp.childNodes.length === 0) {
|
|
533
|
-
node = document.createTextNode('');
|
|
534
|
-
} else {
|
|
535
|
-
// Multiple nodes returned for one item, wrap in span
|
|
536
|
-
node = document.createElement('span');
|
|
537
|
-
node.style.display = 'contents';
|
|
538
|
-
while (temp.firstChild) {
|
|
539
|
-
node.appendChild(temp.firstChild);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
newState.push({ item, node });
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// 3. Remove unused nodes
|
|
547
|
-
for (const list of reuse.values()) {
|
|
548
|
-
for (const n of list) {
|
|
549
|
-
n.remove();
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
// 4. Reorder/Append nodes in valid order
|
|
554
|
-
let sibling = parent.firstChild;
|
|
555
|
-
for (const entry of newState) {
|
|
556
|
-
const domNode = entry.node;
|
|
557
|
-
if (sibling === domNode) {
|
|
558
|
-
// Already in place
|
|
559
|
-
sibling = sibling.nextSibling;
|
|
560
|
-
} else {
|
|
561
|
-
// Needs to be moved or inserted
|
|
562
|
-
parent.insertBefore(domNode, sibling);
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
return newState;
|
|
567
|
-
};
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Setup children in a target node (for shadow roots and other targets)
|
|
571
|
-
* Does not clear existing content
|
|
572
|
-
*/
|
|
573
|
-
const setupChildrenInTarget = (children, targetNode) => {
|
|
574
|
-
return processChildren(children, targetNode, false);
|
|
575
|
-
};
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* Setup children on a DOM node, clearing existing content
|
|
579
|
-
*/
|
|
580
|
-
const setupChildren = (children, domNode) => {
|
|
581
|
-
return processChildren(children, domNode, true);
|
|
582
|
-
};
|
|
583
|
-
|
|
584
|
-
// ============= EXPORTS =============
|
|
585
|
-
const enhance = (selectorOrNode, options = {}) => {
|
|
586
|
-
const domNode = typeof selectorOrNode === 'string'
|
|
587
|
-
? document.querySelector(selectorOrNode)
|
|
588
|
-
: selectorOrNode;
|
|
589
|
-
|
|
590
|
-
// If it's already a Lightview element, use its domEl
|
|
591
|
-
const node = domNode.domEl || domNode;
|
|
592
|
-
if (!(node instanceof HTMLElement)) return null;
|
|
593
|
-
|
|
594
|
-
const tagName = node.tagName.toLowerCase();
|
|
595
|
-
let el = domToElement.get(node);
|
|
596
|
-
|
|
597
|
-
if (!el) {
|
|
598
|
-
el = wrapDomElement(node, tagName);
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
const { innerText, innerHTML, ...attrs } = options;
|
|
602
|
-
|
|
603
|
-
if (innerText !== undefined) {
|
|
604
|
-
if (typeof innerText === 'function') {
|
|
605
|
-
effect(() => { node.innerText = innerText(); });
|
|
606
|
-
} else {
|
|
607
|
-
node.innerText = innerText;
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
if (innerHTML !== undefined) {
|
|
612
|
-
if (typeof innerHTML === 'function') {
|
|
613
|
-
effect(() => { node.innerHTML = innerHTML(); });
|
|
614
|
-
} else {
|
|
615
|
-
node.innerHTML = innerHTML;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
if (Object.keys(attrs).length > 0) {
|
|
620
|
-
// Merge with existing attributes or simply set them triggers the proxy
|
|
621
|
-
el.attributes = attrs;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
return el;
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
const $ = (cssSelectorOrElement, startingDomEl = document.body) => {
|
|
628
|
-
const el = typeof cssSelectorOrElement === 'string' ? startingDomEl.querySelector(cssSelectorOrElement) : cssSelectorOrElement;
|
|
629
|
-
if (!el) return null;
|
|
630
|
-
Object.defineProperty(el, 'content', {
|
|
631
|
-
value(child, location = 'inner') {
|
|
632
|
-
location = location.toLowerCase();
|
|
633
|
-
const tags = Lightview.tags;
|
|
634
|
-
|
|
635
|
-
// Check if target element is script or style
|
|
636
|
-
const isSpecialElement = el.tagName &&
|
|
637
|
-
(el.tagName.toLowerCase() === 'script' || el.tagName.toLowerCase() === 'style');
|
|
638
|
-
|
|
639
|
-
const array = (Array.isArray(child) ? child : [child]).map(item => {
|
|
640
|
-
// Allow extensions to transform children (e.g., Object DOM syntax)
|
|
641
|
-
// BUT skip for script/style elements which need raw content
|
|
642
|
-
if (Lightview.hooks.processChild && !isSpecialElement) {
|
|
643
|
-
item = Lightview.hooks.processChild(item) ?? item;
|
|
644
|
-
}
|
|
645
|
-
if (item.tag && !item.domEl) {
|
|
646
|
-
return element(item.tag, item.attributes || {}, item.children || []).domEl;
|
|
647
|
-
} else {
|
|
648
|
-
return item.domEl || item;
|
|
649
|
-
}
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
if (location === 'shadow') {
|
|
653
|
-
let shadow = el.shadowRoot;
|
|
654
|
-
if (!shadow) {
|
|
655
|
-
shadow = el.attachShadow({ mode: 'open' });
|
|
656
|
-
}
|
|
657
|
-
shadow.innerHTML = '';
|
|
658
|
-
array.forEach(item => {
|
|
659
|
-
shadow.appendChild(item);
|
|
660
|
-
});
|
|
661
|
-
return el;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (location === 'inner') {
|
|
665
|
-
el.innerHTML = '';
|
|
666
|
-
array.forEach(item => {
|
|
667
|
-
el.appendChild(item);
|
|
668
|
-
});
|
|
669
|
-
return el;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (location === 'outer') {
|
|
673
|
-
el.replaceWith(...array);
|
|
674
|
-
return el;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (location === 'afterbegin' || location === 'afterend') {
|
|
678
|
-
array.reverse();
|
|
679
|
-
}
|
|
680
|
-
array.forEach(item => {
|
|
681
|
-
el.insertAdjacentElement(location, item);
|
|
682
|
-
});
|
|
683
|
-
return el;
|
|
684
|
-
},
|
|
685
|
-
configurable: true,
|
|
686
|
-
writable: true
|
|
687
|
-
});
|
|
688
|
-
return el;
|
|
689
|
-
};
|
|
690
|
-
|
|
691
|
-
const customTags = {}
|
|
692
|
-
const tags = new Proxy({}, {
|
|
693
|
-
get(_, tag) {
|
|
694
|
-
if (tag === "_customTags") return { ...customTags };
|
|
695
|
-
const wrapper = (...args) => {
|
|
696
|
-
let attributes = {};
|
|
697
|
-
let children = args;
|
|
698
|
-
const arg0 = args[0];
|
|
699
|
-
if (args.length > 0 && arg0 && typeof arg0 === 'object' && !arg0.tag && !arg0.domEl && !Array.isArray(arg0)) {
|
|
700
|
-
attributes = arg0;
|
|
701
|
-
children = args.slice(1);
|
|
702
|
-
}
|
|
703
|
-
return element(customTags[tag] || tag, attributes, children.flat());
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
const original = customTags[tag];
|
|
707
|
-
if (original) {
|
|
708
|
-
Object.assign(wrapper, original);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
return wrapper;
|
|
712
|
-
},
|
|
713
|
-
set(_, tag, value) {
|
|
714
|
-
customTags[tag] = value;
|
|
715
|
-
return true;
|
|
716
|
-
}
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
const Lightview = {
|
|
720
|
-
signal,
|
|
721
|
-
computed,
|
|
722
|
-
effect,
|
|
723
|
-
element, // do not document this
|
|
724
|
-
enhance,
|
|
725
|
-
tags,
|
|
726
|
-
$,
|
|
727
|
-
// Extension hooks
|
|
728
|
-
hooks: {
|
|
729
|
-
onNonStandardHref: null,
|
|
730
|
-
processChild: null
|
|
731
|
-
},
|
|
732
|
-
// Internals exposed for extensions
|
|
733
|
-
internals: {
|
|
734
|
-
domToElement,
|
|
735
|
-
wrapDomElement,
|
|
736
|
-
setupChildren
|
|
737
|
-
}
|
|
738
|
-
};
|
|
739
|
-
|
|
740
|
-
// Export for use
|
|
741
|
-
if (typeof module !== 'undefined' && module.exports) {
|
|
742
|
-
module.exports = Lightview;
|
|
743
|
-
}
|
|
744
|
-
if (typeof window !== 'undefined') {
|
|
745
|
-
window.Lightview = Lightview;
|
|
746
|
-
|
|
747
|
-
// Global click handler delegates to hook if registered
|
|
748
|
-
window.addEventListener('click', (e) => {
|
|
749
|
-
if (Lightview.hooks.onNonStandardHref) {
|
|
750
|
-
Lightview.hooks.onNonStandardHref(e);
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
|
|
754
|
-
// Automatic Cleanup & Lifecycle Hooks
|
|
755
|
-
const walkNodes = (node, fn) => { fn(node); node.childNodes?.forEach(n => walkNodes(n, fn)); };
|
|
756
|
-
|
|
757
|
-
const cleanupNode = (node) => walkNodes(node, n => {
|
|
758
|
-
const s = nodeState.get(n);
|
|
759
|
-
if (s) {
|
|
760
|
-
s.effects?.forEach(e => e.stop());
|
|
761
|
-
s.onunmount?.(n);
|
|
762
|
-
nodeState.delete(n);
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
const mountNode = (node) => walkNodes(node, n => {
|
|
767
|
-
nodeState.get(n)?.onmount?.(n);
|
|
768
|
-
});
|
|
769
|
-
|
|
770
|
-
const observer = new MutationObserver((mutations) => {
|
|
771
|
-
mutations.forEach((mutation) => {
|
|
772
|
-
mutation.removedNodes.forEach(cleanupNode);
|
|
773
|
-
mutation.addedNodes.forEach(mountNode);
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
|
|
777
|
-
// Wait for DOM to be ready before observing
|
|
778
|
-
const startObserving = () => {
|
|
779
|
-
if (document.body) {
|
|
780
|
-
observer.observe(document.body, {
|
|
781
|
-
childList: true,
|
|
782
|
-
subtree: true
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
};
|
|
786
|
-
|
|
787
|
-
if (document.readyState === 'loading') {
|
|
788
|
-
document.addEventListener('DOMContentLoaded', startObserving);
|
|
789
|
-
} else {
|
|
790
|
-
startObserving();
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
})();
|