lightview 1.8.2 → 2.0.0
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/.agent/workflows/daisyui-component-migration.md +155 -0
- package/.codacy/cli.sh +149 -0
- package/.codacy/codacy.yaml +15 -0
- package/.github/instructions/codacy.instructions.md +72 -0
- package/.wranglerignore +21 -0
- package/README.md +1330 -19
- package/_headers +4 -0
- package/build.js +70 -0
- package/components/actions/button.js +151 -0
- package/components/actions/dropdown.js +120 -0
- package/components/actions/modal.js +146 -0
- package/components/actions/swap.js +118 -0
- package/components/daisyui.js +288 -0
- package/components/data-display/accordion.js +128 -0
- package/components/data-display/alert.js +112 -0
- package/components/data-display/avatar.js +170 -0
- package/components/data-display/badge.js +82 -0
- package/components/data-display/card.js +151 -0
- package/components/data-display/carousel.js +94 -0
- package/components/data-display/chart.js +220 -0
- package/components/data-display/chat.js +128 -0
- package/components/data-display/collapse.js +103 -0
- package/components/data-display/countdown.js +69 -0
- package/components/data-display/diff.js +111 -0
- package/components/data-display/kbd.js +65 -0
- package/components/data-display/loading.js +75 -0
- package/components/data-display/progress.js +79 -0
- package/components/data-display/radial-progress.js +88 -0
- package/components/data-display/skeleton.js +66 -0
- package/components/data-display/stats.js +159 -0
- package/components/data-display/table.js +146 -0
- package/components/data-display/timeline.js +146 -0
- package/components/data-display/toast.js +72 -0
- package/components/data-display/tooltip.js +74 -0
- package/components/data-input/checkbox.js +253 -0
- package/components/data-input/file-input.js +224 -0
- package/components/data-input/input.js +264 -0
- package/components/data-input/radio.js +338 -0
- package/components/data-input/range.js +204 -0
- package/components/data-input/rating.js +219 -0
- package/components/data-input/select.js +287 -0
- package/components/data-input/textarea.js +287 -0
- package/components/data-input/toggle.js +201 -0
- package/components/index.js +137 -0
- package/components/layout/divider.js +72 -0
- package/components/layout/drawer.js +142 -0
- package/components/layout/footer.js +100 -0
- package/components/layout/hero.js +109 -0
- package/components/layout/indicator.js +90 -0
- package/components/layout/join.js +78 -0
- package/components/layout/navbar.js +110 -0
- package/components/navigation/breadcrumbs.js +91 -0
- package/components/navigation/dock.js +103 -0
- package/components/navigation/menu.js +126 -0
- package/components/navigation/pagination.js +105 -0
- package/components/navigation/steps.js +89 -0
- package/components/navigation/tabs.css +177 -0
- package/components/navigation/tabs.js +123 -0
- package/components/theme/theme-switch.css +65 -0
- package/components/theme/theme-switch.js +177 -0
- package/docs/about.html +164 -0
- package/docs/api/computed.html +184 -0
- package/docs/api/effects.html +173 -0
- package/docs/api/elements.html +180 -0
- package/docs/api/enhance.html +225 -0
- package/docs/api/hypermedia.html +165 -0
- package/docs/api/index.html +178 -0
- package/docs/api/nav.html +18 -0
- package/docs/api/signals.html +136 -0
- package/docs/api/state.html +217 -0
- package/docs/assets/images/logo-favicon.svg +42 -0
- package/docs/assets/images/logo-static.svg +40 -0
- package/docs/assets/images/logo.svg +66 -0
- package/docs/assets/js/examplify.js +395 -0
- package/docs/assets/styles/site.css +1102 -0
- package/docs/assets/styles/themes.css +236 -0
- package/docs/components/accordion.html +439 -0
- package/docs/components/alert.html +528 -0
- package/docs/components/avatar.html +586 -0
- package/docs/components/badge.html +531 -0
- package/docs/components/breadcrumbs.html +278 -0
- package/docs/components/button.html +579 -0
- package/docs/components/card.html +561 -0
- package/docs/components/carousel.html +286 -0
- package/docs/components/chart-area.html +702 -0
- package/docs/components/chart-bar.html +782 -0
- package/docs/components/chart-column.html +735 -0
- package/docs/components/chart-line.html +794 -0
- package/docs/components/chart-pie.html +823 -0
- package/docs/components/chart.html +610 -15
- package/docs/components/chat.html +547 -0
- package/docs/components/checkbox.html +641 -0
- package/docs/components/collapse.html +536 -0
- package/docs/components/component-nav.html +53 -0
- package/docs/components/countdown.html +470 -0
- package/docs/components/diff.html +245 -0
- package/docs/components/divider.html +240 -0
- package/docs/components/dock.html +277 -0
- package/docs/components/drawer.html +515 -0
- package/docs/components/dropdown.html +479 -0
- package/docs/components/file-input.html +591 -0
- package/docs/components/footer.html +301 -0
- package/docs/components/gallery.html +504 -0
- package/docs/components/hero.html +264 -0
- package/docs/components/index.css +840 -0
- package/docs/components/index.html +735 -0
- package/docs/components/indicator.html +342 -0
- package/docs/components/input.html +644 -0
- package/docs/components/join.html +285 -0
- package/docs/components/kbd.html +322 -0
- package/docs/components/loading.html +521 -0
- package/docs/components/menu.html +461 -0
- package/docs/components/modal.html +639 -0
- package/docs/components/navbar.html +321 -0
- package/docs/components/pagination.html +279 -0
- package/docs/components/progress.html +514 -0
- package/docs/components/radial-progress.html +434 -0
- package/docs/components/radio.html +655 -0
- package/docs/components/range.html +611 -0
- package/docs/components/rating.html +642 -0
- package/docs/components/select.html +696 -0
- package/docs/components/sidebar-setup.js +93 -0
- package/docs/components/skeleton.html +447 -0
- package/docs/components/spinner.html +68 -0
- package/docs/components/stats.html +486 -0
- package/docs/components/steps.html +356 -0
- package/docs/components/swap.html +517 -0
- package/docs/components/switch.html +68 -0
- package/docs/components/table.html +668 -0
- package/docs/components/tabs.html +506 -0
- package/docs/components/text-input.html +68 -0
- package/docs/components/textarea.html +603 -0
- package/docs/components/timeline.html +485 -42
- package/docs/components/toast.html +474 -0
- package/docs/components/toggle.html +564 -0
- package/docs/components/tooltip.html +423 -0
- package/docs/examples/getting-started-example.html +40 -0
- package/docs/examples/index.html +93 -0
- package/docs/getting-started/index.html +739 -0
- package/docs/getting-started/reviews.html +23 -0
- package/docs/getting-started/reviews.odom +108 -0
- package/docs/getting-started/reviews.vdom +84 -0
- package/docs/index.html +132 -42
- package/docs/playground.html +416 -0
- package/docs/router.html +285 -0
- package/docs/styles/index.html +190 -0
- package/functions/_middleware.js +32 -0
- package/index.html +309 -0
- package/lightview-router.js +364 -0
- package/lightview-x.js +1577 -0
- package/lightview.js +659 -1200
- package/lightview.js.backup +793 -0
- package/middleware/locale.js +25 -0
- package/middleware/markdown.js +44 -0
- package/middleware/notFound.js +37 -0
- package/package.json +27 -41
- package/watch.js +92 -0
- package/wrangler.toml +12 -0
- package/.idea/lightview.iml +0 -12
- package/.idea/modules.xml +0 -8
- package/.idea/vcs.xml +0 -6
- package/LICENSE +0 -21
- package/codepen-no-tabs-embed.css +0 -2
- package/docs/CNAME +0 -1
- package/docs/api.html +0 -674
- package/docs/blank.html +0 -10
- package/docs/comparedto.html +0 -89
- package/docs/components/chart-repl.html +0 -69
- package/docs/components/components.js +0 -113
- package/docs/components/contents.html +0 -17
- package/docs/components/gantt-repl.html +0 -61
- package/docs/components/gantt.html +0 -42
- package/docs/components/gauge-repl.html +0 -66
- package/docs/components/gauge.html +0 -20
- package/docs/components/orgchart-repl.html +0 -64
- package/docs/components/orgchart.html +0 -41
- package/docs/components/repl-as-src.html +0 -17
- package/docs/components/repl-repl.html +0 -95
- package/docs/components/repl.html +0 -527
- package/docs/components/timeline-repl.html +0 -72
- package/docs/components.html +0 -14
- package/docs/css/highlightjs.min.css +0 -9
- package/docs/css/tutorial.css +0 -35
- package/docs/examples/anchor.html +0 -11
- package/docs/examples/chart.html +0 -34
- package/docs/examples/counter.html +0 -26
- package/docs/examples/counter.test.mjs +0 -47
- package/docs/examples/counter2.html +0 -26
- package/docs/examples/directives.html +0 -79
- package/docs/examples/foreign.html +0 -50
- package/docs/examples/forgeinform.html +0 -98
- package/docs/examples/form.html +0 -61
- package/docs/examples/gauge.html +0 -18
- package/docs/examples/invalid-template-literals.html +0 -44
- package/docs/examples/medium/remote.html +0 -60
- package/docs/examples/message.html +0 -18
- package/docs/examples/nested.html +0 -11
- package/docs/examples/object-bound-form.html +0 -34
- package/docs/examples/remote-server.js +0 -51
- package/docs/examples/remote.html +0 -34
- package/docs/examples/remote.json +0 -1
- package/docs/examples/scratch.html +0 -69
- package/docs/examples/sensors/index.html +0 -44
- package/docs/examples/sensors/sensor-server.js +0 -30
- package/docs/examples/shared.html +0 -41
- package/docs/examples/template.html +0 -33
- package/docs/examples/timeline.html +0 -21
- package/docs/examples/todo.html +0 -40
- package/docs/examples/top.html +0 -10
- package/docs/examples/types.html +0 -94
- package/docs/examples/xor.html +0 -62
- package/docs/examples.html +0 -25
- package/docs/javascript/codejar.min.js +0 -8
- package/docs/javascript/highlightjs.min.js +0 -1173
- package/docs/javascript/isomorphic-git.js +0 -9
- package/docs/javascript/json5.min.js +0 -1
- package/docs/javascript/lightning-fs.js +0 -1
- package/docs/javascript/lightview.js +0 -1285
- package/docs/javascript/marked.min.js +0 -6
- package/docs/javascript/peerjs.min.js +0 -70
- package/docs/javascript/turndown.js +0 -973
- package/docs/javascript/types.js +0 -606
- package/docs/javascript/utils.js +0 -45
- package/docs/lightview.html +0 -63
- package/docs/old_index.html +0 -965
- package/docs/old_index.md +0 -1132
- package/docs/slidein.html +0 -51
- package/docs/tutorial/0-getting-started.html +0 -67
- package/docs/tutorial/1-intro-to-variables.html +0 -103
- package/docs/tutorial/10-template-components.html +0 -80
- package/docs/tutorial/11-linked-components.html +0 -76
- package/docs/tutorial/12-imported-components.html +0 -67
- package/docs/tutorial/13-input-binding.html +0 -94
- package/docs/tutorial/14-automatic-variable-creation.html +0 -74
- package/docs/tutorial/15-form-binding.html +0 -110
- package/docs/tutorial/16-if-directive.html +0 -60
- package/docs/tutorial/17-loop-directives.html +0 -83
- package/docs/tutorial/18-sanitizing-and-escaping-input.html +0 -79
- package/docs/tutorial/2-imported-and-exported-variables.html +0 -80
- package/docs/tutorial/3-data-types.html +0 -89
- package/docs/tutorial/4-extended-data-types.html +0 -83
- package/docs/tutorial/5-extended-functional-types.html +0 -96
- package/docs/tutorial/5.1-extended-functional-types.html +0 -79
- package/docs/tutorial/5.2-extended-functional-types.html +0 -70
- package/docs/tutorial/6-conventional-javascript.html +0 -75
- package/docs/tutorial/7-monitoring-with-observers.html +0 -107
- package/docs/tutorial/8-event-listeners.html +0 -65
- package/docs/tutorial/9-intro-to-components.html +0 -91
- package/docs/tutorial/contents.html +0 -32
- package/docs/tutorial/my-component.html +0 -29
- package/docs/tutorial/remote-value.json +0 -4
- package/docs/websiterepl.html +0 -46
- package/jest-puppeteer.config.js +0 -5
- package/jest.config.json +0 -12
- package/lightview.min.js +0 -1
- package/lightview_good.js +0 -1267
- package/lightview_optimized.js +0 -1274
- package/repl_hold.html +0 -320
- package/test/basic.html +0 -104
- package/test/basic.test.mjs +0 -315
- package/test/extended.html +0 -29
- package/test/extended.test.mjs +0 -448
- package/types.js +0 -607
- package/unsplash.key +0 -1
package/lightview-x.js
ADDED
|
@@ -0,0 +1,1577 @@
|
|
|
1
|
+
(() => {
|
|
2
|
+
// ============= LIGHTVIEW-X =============
|
|
3
|
+
// Hypermedia extension for Lightview
|
|
4
|
+
// Adds: src attribute fetching, href navigation, DOM-to-element conversion, template literals, named registries, Object DOM syntax
|
|
5
|
+
|
|
6
|
+
const STANDARD_SRC_TAGS = ['img', 'script', 'iframe', 'video', 'audio', 'source', 'track', 'embed', 'input'];
|
|
7
|
+
const isStandardSrcTag = (tagName) => STANDARD_SRC_TAGS.includes(tagName) || tagName.startsWith('lv-');
|
|
8
|
+
const STANDARD_HREF_TAGS = ['a', 'area', 'base', 'link'];
|
|
9
|
+
|
|
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
|
+
|
|
23
|
+
/**
|
|
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}
|
|
29
|
+
*/
|
|
30
|
+
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
|
+
|
|
34
|
+
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;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
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
|
|
55
|
+
*/
|
|
56
|
+
const convertObjectDOM = (obj) => {
|
|
57
|
+
// Not an object or array - return as-is (strings, numbers, functions, etc.)
|
|
58
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
59
|
+
|
|
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
|
+
}
|
|
99
|
+
|
|
100
|
+
// Unknown object format - return as-is
|
|
101
|
+
return obj;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// ============= COMPONENT CONFIGURATION =============
|
|
105
|
+
// Global configuration for Lightview components
|
|
106
|
+
|
|
107
|
+
const DAISYUI_CDN = 'https://cdn.jsdelivr.net/npm/daisyui@3.9.4/dist/full.min.css';
|
|
108
|
+
|
|
109
|
+
// Component configuration (set by initComponents)
|
|
110
|
+
const componentConfig = {
|
|
111
|
+
initialized: false,
|
|
112
|
+
shadowDefault: true, // Default: components use shadow DOM
|
|
113
|
+
daisyStyleSheet: null,
|
|
114
|
+
themeStyleSheet: null, // Global theme stylesheet
|
|
115
|
+
componentStyleSheets: new Map(),
|
|
116
|
+
customStyleSheets: new Map() // Registry for named custom stylesheets
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Register a named stylesheet for use in components
|
|
121
|
+
* @param {string} nameOrIdOrUrl - The name/ID/URL of the stylesheet
|
|
122
|
+
* @param {string} [cssText] - Optional raw CSS content. If provided, nameOrIdOrUrl is treated as a name.
|
|
123
|
+
* @returns {Promise<void>}
|
|
124
|
+
*/
|
|
125
|
+
const registerStyleSheet = async (nameOrIdOrUrl, cssText) => {
|
|
126
|
+
if (componentConfig.customStyleSheets.has(nameOrIdOrUrl)) return;
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
let finalCss = cssText;
|
|
130
|
+
|
|
131
|
+
if (finalCss === undefined) {
|
|
132
|
+
if (nameOrIdOrUrl.startsWith('#')) {
|
|
133
|
+
// ID selector - search synchronously
|
|
134
|
+
const el = document.querySelector(nameOrIdOrUrl);
|
|
135
|
+
if (el) {
|
|
136
|
+
finalCss = el.textContent;
|
|
137
|
+
} else {
|
|
138
|
+
throw new Error(`Style block '${nameOrIdOrUrl}' not found`);
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
// Assume URL
|
|
142
|
+
const response = await fetch(nameOrIdOrUrl);
|
|
143
|
+
if (!response.ok) throw new Error(`Fetch failed: ${response.status}`);
|
|
144
|
+
finalCss = await response.text();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (finalCss !== undefined) {
|
|
149
|
+
const sheet = new CSSStyleSheet();
|
|
150
|
+
sheet.replaceSync(finalCss);
|
|
151
|
+
componentConfig.customStyleSheets.set(nameOrIdOrUrl, sheet);
|
|
152
|
+
}
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`LightviewX: Failed to register stylesheet '${nameOrIdOrUrl}':`, e);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Theme Signal
|
|
159
|
+
// Helper to safely get local storage
|
|
160
|
+
const getSavedTheme = () => {
|
|
161
|
+
try {
|
|
162
|
+
if (typeof localStorage !== 'undefined') {
|
|
163
|
+
return localStorage.getItem('lightview-theme');
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Theme Signal
|
|
171
|
+
const themeSignal = typeof window !== 'undefined' && window.Lightview ? window.Lightview.signal(
|
|
172
|
+
(typeof document !== 'undefined' && document.documentElement.getAttribute('data-theme')) ||
|
|
173
|
+
getSavedTheme() ||
|
|
174
|
+
'light'
|
|
175
|
+
) : { value: 'light' };
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Set the global theme for Lightview components (updates signal only)
|
|
179
|
+
* @param {string} themeName - The name of the theme (e.g., 'light', 'dark', 'cyberpunk')
|
|
180
|
+
*/
|
|
181
|
+
const setTheme = (themeName) => {
|
|
182
|
+
if (!themeName) return;
|
|
183
|
+
// Determine base theme (light or dark) for the main document
|
|
184
|
+
// Determine base theme (light or dark) for the main document
|
|
185
|
+
// const darkThemes = ['dark', 'aqua', 'black', 'business', 'coffee', 'dim', 'dracula', 'forest', 'halloween', 'luxury', 'night', 'sunset', 'synthwave'];
|
|
186
|
+
// const baseTheme = darkThemes.includes(themeName) ? 'dark' : 'light';
|
|
187
|
+
document.documentElement.setAttribute('data-theme', themeName);
|
|
188
|
+
|
|
189
|
+
// Update signal
|
|
190
|
+
if (themeSignal && themeSignal.value !== themeName) {
|
|
191
|
+
themeSignal.value = themeName;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Persist preference
|
|
195
|
+
try {
|
|
196
|
+
localStorage.setItem('lightview-theme', themeName);
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// Ignore storage errors
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Register a global theme stylesheet for all components
|
|
204
|
+
* @param {string} url - URL to the CSS file
|
|
205
|
+
* @returns {Promise<void>}
|
|
206
|
+
*/
|
|
207
|
+
const registerThemeSheet = async (url) => {
|
|
208
|
+
try {
|
|
209
|
+
const response = await fetch(url);
|
|
210
|
+
if (!response.ok) throw new Error(`Failed to fetch theme CSS: ${response.status}`);
|
|
211
|
+
const cssText = await response.text();
|
|
212
|
+
const sheet = new CSSStyleSheet();
|
|
213
|
+
sheet.replaceSync(cssText);
|
|
214
|
+
componentConfig.themeStyleSheet = sheet;
|
|
215
|
+
} catch (e) {
|
|
216
|
+
console.error(`LightviewX: Failed to register theme stylesheet '${url}':`, e);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Initialize Lightview components
|
|
222
|
+
* Preloads DaisyUI stylesheet for shadow DOM usage
|
|
223
|
+
* @param {Object} options
|
|
224
|
+
* @param {boolean} options.shadowDefault - Whether components use shadow DOM by default (default: true)
|
|
225
|
+
* @returns {Promise<void>}
|
|
226
|
+
*/
|
|
227
|
+
const initComponents = async (options = {}) => {
|
|
228
|
+
const { shadowDefault = true } = options;
|
|
229
|
+
|
|
230
|
+
componentConfig.shadowDefault = shadowDefault;
|
|
231
|
+
|
|
232
|
+
if (shadowDefault) {
|
|
233
|
+
// Preload DaisyUI stylesheet for adopted stylesheets
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(DAISYUI_CDN);
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw new Error(`Failed to fetch DaisyUI CSS: ${response.status}`);
|
|
238
|
+
}
|
|
239
|
+
const cssText = await response.text();
|
|
240
|
+
const sheet = new CSSStyleSheet();
|
|
241
|
+
sheet.replaceSync(cssText);
|
|
242
|
+
componentConfig.daisyStyleSheet = sheet;
|
|
243
|
+
} catch (e) {
|
|
244
|
+
console.error('LightviewX: Failed to preload DaisyUI stylesheet:', e);
|
|
245
|
+
// Continue without DaisyUI - components will still work, just without DaisyUI styles in shadow
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
componentConfig.initialized = true;
|
|
250
|
+
};
|
|
251
|
+
(async () => await initComponents())();
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get or create a CSSStyleSheet for a component's CSS file
|
|
255
|
+
* @param {string} cssUrl - URL to the component's CSS file
|
|
256
|
+
* @returns {Promise<CSSStyleSheet|null>}
|
|
257
|
+
*/
|
|
258
|
+
const getComponentStyleSheet = async (cssUrl) => {
|
|
259
|
+
// Return cached sheet if available
|
|
260
|
+
if (componentConfig.componentStyleSheets.has(cssUrl)) {
|
|
261
|
+
return componentConfig.componentStyleSheets.get(cssUrl);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const response = await fetch(cssUrl);
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw new Error(`Failed to fetch component CSS: ${response.status}`);
|
|
268
|
+
}
|
|
269
|
+
const cssText = await response.text();
|
|
270
|
+
|
|
271
|
+
const sheet = new CSSStyleSheet();
|
|
272
|
+
sheet.replaceSync(cssText);
|
|
273
|
+
componentConfig.componentStyleSheets.set(cssUrl, sheet);
|
|
274
|
+
return sheet;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(`LightviewX: Failed to create stylesheet for ${cssUrl}:`, e);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Synchronously get cached component stylesheet (returns null if not yet loaded)
|
|
283
|
+
* @param {string} cssUrl
|
|
284
|
+
* @returns {CSSStyleSheet|null}
|
|
285
|
+
*/
|
|
286
|
+
const getComponentStyleSheetSync = (cssUrl) => componentConfig.componentStyleSheets.get(cssUrl) || null;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if a component should use shadow DOM based on props and global default
|
|
290
|
+
* @param {boolean|undefined} useShadowProp - The useShadow prop passed to the component
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
const shouldUseShadow = (useShadowProp) => {
|
|
294
|
+
// Explicit prop value takes precedence
|
|
295
|
+
if (useShadowProp !== undefined) {
|
|
296
|
+
return useShadowProp;
|
|
297
|
+
}
|
|
298
|
+
// Fall back to global default
|
|
299
|
+
return componentConfig.shadowDefault;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Get the adopted stylesheets for a component
|
|
304
|
+
* @param {string} componentCssUrl - URL to the component's CSS file
|
|
305
|
+
* @param {string[]} requestedSheets - Array of stylesheet URLs to include
|
|
306
|
+
* @returns {(CSSStyleSheet|string)[]} - Mixed array of StyleSheet objects and URL strings (for link fallbacks)
|
|
307
|
+
*/
|
|
308
|
+
const getAdoptedStyleSheets = (componentCssUrl, requestedSheets = []) => {
|
|
309
|
+
const result = [];
|
|
310
|
+
|
|
311
|
+
// Add global DaisyUI sheet
|
|
312
|
+
if (componentConfig.daisyStyleSheet) {
|
|
313
|
+
result.push(componentConfig.daisyStyleSheet);
|
|
314
|
+
} else {
|
|
315
|
+
result.push(DAISYUI_CDN);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Add global Theme sheet (overrides default Daisy variables)
|
|
319
|
+
if (componentConfig.themeStyleSheet) {
|
|
320
|
+
result.push(componentConfig.themeStyleSheet);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Add component-specific sheet
|
|
324
|
+
if (componentCssUrl) {
|
|
325
|
+
const componentSheet = componentConfig.componentStyleSheets.get(componentCssUrl);
|
|
326
|
+
if (componentSheet) {
|
|
327
|
+
result.push(componentSheet);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Process requested sheets
|
|
332
|
+
if (Array.isArray(requestedSheets)) {
|
|
333
|
+
requestedSheets.forEach(url => {
|
|
334
|
+
const sheet = componentConfig.customStyleSheets.get(url);
|
|
335
|
+
if (sheet) {
|
|
336
|
+
// Registered and loaded -> use object
|
|
337
|
+
result.push(sheet);
|
|
338
|
+
} else {
|
|
339
|
+
// Not found -> trigger load, but return string URL for immediate link tag
|
|
340
|
+
registerStyleSheet(url); // Fire and forget
|
|
341
|
+
result.push(url);
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return result;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Preload a component's CSS for shadow DOM usage
|
|
351
|
+
* Called by components during their initialization
|
|
352
|
+
* @param {string} cssUrl - URL to the component's CSS file
|
|
353
|
+
* @returns {Promise<void>}
|
|
354
|
+
*/
|
|
355
|
+
const preloadComponentCSS = async (cssUrl) => {
|
|
356
|
+
if (!componentConfig.componentStyleSheets.has(cssUrl)) {
|
|
357
|
+
await getComponentStyleSheet(cssUrl);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Named registries for state (used by template literals)
|
|
362
|
+
const stateRegistry = new Map();
|
|
363
|
+
|
|
364
|
+
// ============= STATE (Deep Reactivity) =============
|
|
365
|
+
// Build method lists dynamically from prototypes
|
|
366
|
+
const protoMethods = (proto, test) => Object.getOwnPropertyNames(proto).filter(k => typeof proto[k] === 'function' && test(k));
|
|
367
|
+
const DATE_TRACKING = protoMethods(Date.prototype, k => /^(to|get|valueOf)/.test(k));
|
|
368
|
+
const DATE_MUTATING = protoMethods(Date.prototype, k => /^set/.test(k));
|
|
369
|
+
const ARRAY_TRACKING = ['map', 'forEach', 'filter', 'find', 'findIndex', 'some', 'every', 'reduce',
|
|
370
|
+
'reduceRight', 'includes', 'indexOf', 'lastIndexOf', 'join', 'slice', 'concat', 'flat', 'flatMap',
|
|
371
|
+
'at', 'entries', 'keys', 'values'];
|
|
372
|
+
const ARRAY_MUTATING = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse', 'fill', 'copyWithin'];
|
|
373
|
+
const ARRAY_ITERATION = ['map', 'forEach', 'filter', 'find', 'findIndex', 'some', 'every', 'flatMap'];
|
|
374
|
+
|
|
375
|
+
const stateCache = new WeakMap();
|
|
376
|
+
const stateSignals = new WeakMap();
|
|
377
|
+
|
|
378
|
+
// Helper to get or create a value in a map
|
|
379
|
+
const getOrSet = (map, key, factory) => {
|
|
380
|
+
let v = map.get(key);
|
|
381
|
+
if (!v) {
|
|
382
|
+
v = factory();
|
|
383
|
+
map.set(key, v);
|
|
384
|
+
}
|
|
385
|
+
return v;
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Shared proxy handler helpers (uses Lightview.signal internally)
|
|
389
|
+
const proxyGet = (target, prop, receiver, signals) => {
|
|
390
|
+
const LV = window.Lightview;
|
|
391
|
+
if (!signals.has(prop)) {
|
|
392
|
+
signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
393
|
+
}
|
|
394
|
+
const val = signals.get(prop).value;
|
|
395
|
+
return typeof val === 'object' && val !== null ? state(val) : val;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
const proxySet = (target, prop, value, receiver, signals) => {
|
|
399
|
+
const LV = window.Lightview;
|
|
400
|
+
if (!signals.has(prop)) {
|
|
401
|
+
signals.set(prop, LV.signal(Reflect.get(target, prop, receiver)));
|
|
402
|
+
}
|
|
403
|
+
const success = Reflect.set(target, prop, value, receiver);
|
|
404
|
+
if (success) signals.get(prop).value = value;
|
|
405
|
+
return success;
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
const createSpecialProxy = (obj, monitor, trackingProps = []) => {
|
|
409
|
+
const LV = window.Lightview;
|
|
410
|
+
// Get or create the signals map for this object
|
|
411
|
+
const signals = getOrSet(stateSignals, obj, () => new Map());
|
|
412
|
+
|
|
413
|
+
// Create a signal for the monitored property if it doesn't exist
|
|
414
|
+
if (!signals.has(monitor)) {
|
|
415
|
+
const initialValue = typeof obj[monitor] === 'function'
|
|
416
|
+
? obj[monitor].call(obj)
|
|
417
|
+
: obj[monitor];
|
|
418
|
+
signals.set(monitor, LV.signal(initialValue));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Determine which methods should establish tracking (read the monitor signal)
|
|
422
|
+
const isDate = obj instanceof Date;
|
|
423
|
+
const isArray = Array.isArray(obj);
|
|
424
|
+
|
|
425
|
+
const trackingMethods = isDate ? DATE_TRACKING : isArray ? ARRAY_TRACKING : trackingProps;
|
|
426
|
+
const mutatingMethods = isDate ? DATE_MUTATING : isArray ? ARRAY_MUTATING : [];
|
|
427
|
+
|
|
428
|
+
return new Proxy(obj, {
|
|
429
|
+
get(target, prop, receiver) {
|
|
430
|
+
const value = target[prop];
|
|
431
|
+
|
|
432
|
+
// If accessing a method, wrap it appropriately
|
|
433
|
+
if (typeof value === 'function') {
|
|
434
|
+
const isTracking = trackingMethods.includes(prop);
|
|
435
|
+
const isMutating = mutatingMethods.includes(prop);
|
|
436
|
+
|
|
437
|
+
return function (...args) {
|
|
438
|
+
// For tracking methods, read the signal to establish dependency
|
|
439
|
+
if (isTracking) {
|
|
440
|
+
const sig = signals.get(monitor);
|
|
441
|
+
if (sig) void sig.value;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Get the value before the method call
|
|
445
|
+
const startValue = typeof target[monitor] === 'function'
|
|
446
|
+
? target[monitor].call(target)
|
|
447
|
+
: target[monitor];
|
|
448
|
+
|
|
449
|
+
// For array iteration methods, wrap the callback to pass state-wrapped elements
|
|
450
|
+
if (isArray && ARRAY_ITERATION.includes(prop) && typeof args[0] === 'function') {
|
|
451
|
+
const originalCallback = args[0];
|
|
452
|
+
args[0] = function (element, index, array) {
|
|
453
|
+
const wrappedElement = typeof element === 'object' && element !== null
|
|
454
|
+
? state(element)
|
|
455
|
+
: element;
|
|
456
|
+
return originalCallback.call(this, wrappedElement, index, array);
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Call the original method
|
|
461
|
+
const result = value.apply(target, args);
|
|
462
|
+
|
|
463
|
+
// Get the value after the method call
|
|
464
|
+
const endValue = typeof target[monitor] === 'function'
|
|
465
|
+
? target[monitor].call(target)
|
|
466
|
+
: target[monitor];
|
|
467
|
+
|
|
468
|
+
// If the monitored value changed, trigger reactivity
|
|
469
|
+
if (startValue !== endValue || isMutating) {
|
|
470
|
+
const sig = signals.get(monitor);
|
|
471
|
+
if (sig && sig.value !== endValue) {
|
|
472
|
+
sig.value = endValue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
return result;
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// If accessing the monitored property, track it via signal
|
|
481
|
+
if (prop === monitor) {
|
|
482
|
+
const sig = signals.get(monitor);
|
|
483
|
+
return sig ? sig.value : Reflect.get(target, prop, receiver);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// For arrays, handle numeric indices for deep reactivity
|
|
487
|
+
if (isArray && !isNaN(parseInt(prop))) {
|
|
488
|
+
const monitorSig = signals.get(monitor);
|
|
489
|
+
if (monitorSig) void monitorSig.value;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Deep reactivity for other properties
|
|
493
|
+
return proxyGet(target, prop, receiver, signals);
|
|
494
|
+
},
|
|
495
|
+
set(target, prop, value, receiver) {
|
|
496
|
+
// If setting the monitored property directly, trigger reactivity
|
|
497
|
+
if (prop === monitor) {
|
|
498
|
+
const success = Reflect.set(target, prop, value, receiver);
|
|
499
|
+
if (success) {
|
|
500
|
+
const sig = signals.get(monitor);
|
|
501
|
+
if (sig) sig.value = value;
|
|
502
|
+
}
|
|
503
|
+
return success;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return proxySet(target, prop, value, receiver, signals);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
};
|
|
510
|
+
|
|
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
|
+
const state = (obj, optionsOrName) => {
|
|
517
|
+
if (typeof obj !== 'object' || obj === null) return obj;
|
|
518
|
+
|
|
519
|
+
let name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
|
|
520
|
+
const storage = optionsOrName?.storage;
|
|
521
|
+
|
|
522
|
+
let loadedData = null;
|
|
523
|
+
if (name && storage) {
|
|
524
|
+
try {
|
|
525
|
+
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);
|
|
540
|
+
}
|
|
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;
|
|
559
|
+
|
|
560
|
+
const isArray = Array.isArray(obj);
|
|
561
|
+
const isDate = obj instanceof Date;
|
|
562
|
+
const monitor = isArray ? "length" : isDate ? "getTime" : null;
|
|
563
|
+
|
|
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);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (name && storage && typeof window !== 'undefined' && window.Lightview && window.Lightview.effect) {
|
|
579
|
+
window.Lightview.effect(() => {
|
|
580
|
+
try {
|
|
581
|
+
const json = JSON.stringify(proxy);
|
|
582
|
+
storage.setItem(name, json);
|
|
583
|
+
} catch (e) { /* ignore */ }
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (name) {
|
|
588
|
+
stateRegistry.set(name, proxy);
|
|
589
|
+
}
|
|
590
|
+
return proxy;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
state.get = (name, defaultValue) => {
|
|
594
|
+
if (!stateRegistry.has(name) && defaultValue !== undefined) {
|
|
595
|
+
return state(defaultValue, name);
|
|
596
|
+
}
|
|
597
|
+
return stateRegistry.get(name);
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// Template literal processing: converts "${...}" strings to reactive functions
|
|
601
|
+
const processTemplateChild = (child, { state, signal }) => {
|
|
602
|
+
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
|
+
};
|
|
611
|
+
}
|
|
612
|
+
return child; // No transformation needed
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
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';
|
|
618
|
+
|
|
619
|
+
return domNodes.map(node => {
|
|
620
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
621
|
+
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
|
|
629
|
+
if (!text.trim() && !text.includes('${')) return null;
|
|
630
|
+
|
|
631
|
+
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
|
+
};
|
|
640
|
+
}
|
|
641
|
+
return text;
|
|
642
|
+
}
|
|
643
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return null;
|
|
644
|
+
|
|
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';
|
|
650
|
+
|
|
651
|
+
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
|
+
}
|
|
665
|
+
}
|
|
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);
|
|
670
|
+
}).filter(n => n !== null);
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// WeakMap to track inserted content per element+location for deduplication
|
|
674
|
+
const insertedContentMap = new WeakMap();
|
|
675
|
+
|
|
676
|
+
// Simple hash function for content comparison
|
|
677
|
+
const hashContent = (str) => {
|
|
678
|
+
let hash = 0;
|
|
679
|
+
for (let i = 0; i < str.length; i++) {
|
|
680
|
+
const char = str.charCodeAt(i);
|
|
681
|
+
hash = ((hash << 5) - hash) + char;
|
|
682
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
683
|
+
}
|
|
684
|
+
return hash.toString(36);
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
// Create a marker comment to identify inserted content boundaries
|
|
688
|
+
const createMarker = (id, isEnd = false) => {
|
|
689
|
+
return document.createComment(`lv-src-${isEnd ? 'end' : 'start'}:${id}`);
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Execute scripts in a container element
|
|
695
|
+
* Scripts created via DOMParser or innerHTML don't execute automatically,
|
|
696
|
+
* so we need to replace them with new script elements to trigger execution
|
|
697
|
+
* @param {HTMLElement|DocumentFragment} container - Container to search for scripts
|
|
698
|
+
*/
|
|
699
|
+
const executeScripts = (container) => {
|
|
700
|
+
if (!container) return;
|
|
701
|
+
|
|
702
|
+
// Find all script tags in the container
|
|
703
|
+
const scripts = container.querySelectorAll('script');
|
|
704
|
+
|
|
705
|
+
scripts.forEach(oldScript => {
|
|
706
|
+
// Create a new script element
|
|
707
|
+
const newScript = document.createElement('script');
|
|
708
|
+
|
|
709
|
+
// Copy all attributes from old to new
|
|
710
|
+
Array.from(oldScript.attributes).forEach(attr => {
|
|
711
|
+
newScript.setAttribute(attr.name, attr.value);
|
|
712
|
+
});
|
|
713
|
+
|
|
714
|
+
// Copy the script content
|
|
715
|
+
if (oldScript.src) {
|
|
716
|
+
// External script - src attribute already copied
|
|
717
|
+
newScript.src = oldScript.src;
|
|
718
|
+
} else {
|
|
719
|
+
// Inline script - copy text content
|
|
720
|
+
newScript.textContent = oldScript.textContent;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Replace the old script with the new one
|
|
724
|
+
// This causes the browser to execute it
|
|
725
|
+
oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
// Find and remove previously inserted content between markers
|
|
730
|
+
const removeInsertedContent = (parentEl, markerId) => {
|
|
731
|
+
const startMarker = `lv-src-start:${markerId}`;
|
|
732
|
+
const endMarker = `lv-src-end:${markerId}`;
|
|
733
|
+
|
|
734
|
+
let inRange = false;
|
|
735
|
+
const nodesToRemove = [];
|
|
736
|
+
|
|
737
|
+
const walker = document.createTreeWalker(
|
|
738
|
+
parentEl.parentElement || parentEl,
|
|
739
|
+
NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
|
|
740
|
+
null,
|
|
741
|
+
false
|
|
742
|
+
);
|
|
743
|
+
|
|
744
|
+
while (walker.nextNode()) {
|
|
745
|
+
const node = walker.currentNode;
|
|
746
|
+
if (node.nodeType === Node.COMMENT_NODE) {
|
|
747
|
+
if (node.textContent === startMarker) {
|
|
748
|
+
inRange = true;
|
|
749
|
+
nodesToRemove.push(node);
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (node.textContent === endMarker) {
|
|
753
|
+
nodesToRemove.push(node);
|
|
754
|
+
break;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (inRange) {
|
|
758
|
+
nodesToRemove.push(node);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
nodesToRemove.forEach(node => node.remove());
|
|
763
|
+
return nodesToRemove.length > 0;
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
const handleSrcAttribute = async (el, src, tagName, { element, setupChildren }) => {
|
|
767
|
+
// Skip standard src tags
|
|
768
|
+
if (STANDARD_SRC_TAGS.includes(tagName)) return;
|
|
769
|
+
|
|
770
|
+
const isPath = (s) => /^(https?:|\.|\/|\w)/.test(s) || /\.(html|json|vdom|odom)$/.test(s);
|
|
771
|
+
|
|
772
|
+
let content = null;
|
|
773
|
+
let isJson = false;
|
|
774
|
+
let isHtml = false;
|
|
775
|
+
let rawContent = '';
|
|
776
|
+
|
|
777
|
+
if (isPath(src)) {
|
|
778
|
+
try {
|
|
779
|
+
const url = new URL(src, document.baseURI);
|
|
780
|
+
const res = await fetch(url.href);
|
|
781
|
+
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
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
799
|
+
// Fetch failed, try selector
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
let elements = [];
|
|
804
|
+
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);
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
// Treat as text
|
|
825
|
+
elements = [content];
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
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('');
|
|
834
|
+
}
|
|
835
|
+
} catch {
|
|
836
|
+
// Invalid selector
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
if (elements.length === 0) return;
|
|
841
|
+
|
|
842
|
+
// Get location attribute (default to 'innerhtml')
|
|
843
|
+
const location = (el.domEl.getAttribute('location') || 'innerhtml').toLowerCase();
|
|
844
|
+
|
|
845
|
+
// Generate content hash for deduplication
|
|
846
|
+
const contentHash = hashContent(rawContent);
|
|
847
|
+
const markerId = `${location}-${contentHash.slice(0, 8)}`;
|
|
848
|
+
|
|
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
|
+
}
|
|
875
|
+
setupChildren(elements, el.domEl.shadowRoot);
|
|
876
|
+
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
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
|
|
1049
|
+
// Valid location values for content insertion
|
|
1050
|
+
const VALID_LOCATIONS = ['beforebegin', 'afterbegin', 'beforeend', 'afterend', 'innerhtml', 'outerhtml', 'shadow'];
|
|
1051
|
+
|
|
1052
|
+
// Parse position suffix from target string (e.g., "#box:afterbegin" -> { selector: "#box", location: "afterbegin" })
|
|
1053
|
+
const parseTargetWithLocation = (targetStr) => {
|
|
1054
|
+
for (const loc of VALID_LOCATIONS) {
|
|
1055
|
+
const suffix = ':' + loc;
|
|
1056
|
+
if (targetStr.toLowerCase().endsWith(suffix)) {
|
|
1057
|
+
return {
|
|
1058
|
+
selector: targetStr.slice(0, -suffix.length),
|
|
1059
|
+
location: loc
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
return { selector: targetStr, location: null };
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
const handleNonStandardHref = (e, { domToElement, wrapDomElement }) => {
|
|
1067
|
+
const clickedEl = e.target.closest('[href]');
|
|
1068
|
+
if (!clickedEl) return;
|
|
1069
|
+
|
|
1070
|
+
const tagName = clickedEl.tagName.toLowerCase();
|
|
1071
|
+
if (STANDARD_HREF_TAGS.includes(tagName)) return;
|
|
1072
|
+
|
|
1073
|
+
e.preventDefault();
|
|
1074
|
+
|
|
1075
|
+
const href = clickedEl.getAttribute('href');
|
|
1076
|
+
const targetAttr = clickedEl.getAttribute('target');
|
|
1077
|
+
|
|
1078
|
+
// Case 1: No target attribute - existing behavior (load into self)
|
|
1079
|
+
if (!targetAttr) {
|
|
1080
|
+
let el = domToElement.get(clickedEl);
|
|
1081
|
+
if (!el) {
|
|
1082
|
+
const attrs = {};
|
|
1083
|
+
for (let attr of clickedEl.attributes) attrs[attr.name] = attr.value;
|
|
1084
|
+
el = wrapDomElement(clickedEl, tagName, attrs);
|
|
1085
|
+
}
|
|
1086
|
+
const newAttrs = { ...el.attributes, src: href };
|
|
1087
|
+
el.attributes = newAttrs;
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Case 2: Target starts with _ (browser navigation)
|
|
1092
|
+
if (targetAttr.startsWith('_')) {
|
|
1093
|
+
switch (targetAttr) {
|
|
1094
|
+
case '_self':
|
|
1095
|
+
window.location.href = href;
|
|
1096
|
+
break;
|
|
1097
|
+
case '_parent':
|
|
1098
|
+
window.parent.location.href = href;
|
|
1099
|
+
break;
|
|
1100
|
+
case '_top':
|
|
1101
|
+
window.top.location.href = href;
|
|
1102
|
+
break;
|
|
1103
|
+
case '_blank':
|
|
1104
|
+
default:
|
|
1105
|
+
// _blank or any custom _name opens a new window/tab
|
|
1106
|
+
window.open(href, targetAttr);
|
|
1107
|
+
break;
|
|
1108
|
+
}
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Case 3: Target is a CSS selector (with optional :position suffix)
|
|
1113
|
+
const { selector, location } = parseTargetWithLocation(targetAttr);
|
|
1114
|
+
|
|
1115
|
+
try {
|
|
1116
|
+
const targetElements = document.querySelectorAll(selector);
|
|
1117
|
+
targetElements.forEach(targetEl => {
|
|
1118
|
+
let el = domToElement.get(targetEl);
|
|
1119
|
+
if (!el) {
|
|
1120
|
+
const attrs = {};
|
|
1121
|
+
for (let attr of targetEl.attributes) attrs[attr.name] = attr.value;
|
|
1122
|
+
el = wrapDomElement(targetEl, targetEl.tagName.toLowerCase(), attrs);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// Build new attributes
|
|
1126
|
+
const newAttrs = { ...el.attributes, src: href };
|
|
1127
|
+
if (location) {
|
|
1128
|
+
newAttrs.location = location;
|
|
1129
|
+
}
|
|
1130
|
+
el.attributes = newAttrs;
|
|
1131
|
+
});
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
console.warn('Invalid target selector:', selector, err);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
// ============= DOM OBSERVER FOR SRC ATTRIBUTES =============
|
|
1140
|
+
|
|
1141
|
+
/**
|
|
1142
|
+
* Process src attribute on a DOM element that doesn't normally have src
|
|
1143
|
+
* @param {HTMLElement} node - DOM element to process
|
|
1144
|
+
* @param {Object} LV - Lightview instance
|
|
1145
|
+
*/
|
|
1146
|
+
const processSrcOnNode = (node, LV) => {
|
|
1147
|
+
if (node.nodeType !== Node.ELEMENT_NODE) return;
|
|
1148
|
+
|
|
1149
|
+
const tagName = node.tagName.toLowerCase();
|
|
1150
|
+
if (isStandardSrcTag(tagName)) return;
|
|
1151
|
+
|
|
1152
|
+
const src = node.getAttribute('src');
|
|
1153
|
+
if (!src) return;
|
|
1154
|
+
|
|
1155
|
+
// Get or create reactive wrapper
|
|
1156
|
+
let el = LV.internals.domToElement.get(node);
|
|
1157
|
+
if (!el) {
|
|
1158
|
+
const attrs = {};
|
|
1159
|
+
for (let attr of node.attributes) attrs[attr.name] = attr.value;
|
|
1160
|
+
el = LV.internals.wrapDomElement(node, tagName, attrs, []);
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
handleSrcAttribute(el, src, tagName, {
|
|
1164
|
+
element: LV.element,
|
|
1165
|
+
setupChildren: LV.internals.setupChildren
|
|
1166
|
+
});
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// Track nodes to avoid double-processing
|
|
1170
|
+
const processedNodes = new WeakSet();
|
|
1171
|
+
|
|
1172
|
+
/**
|
|
1173
|
+
* Activate reactive syntax (${...}) in existing DOM nodes
|
|
1174
|
+
* Uses XPath for performance optimization
|
|
1175
|
+
* @param {Node} root - Root node to start scanning from
|
|
1176
|
+
* @param {Object} LV - Lightview instance
|
|
1177
|
+
*/
|
|
1178
|
+
const activateReactiveSyntax = (root, LV) => {
|
|
1179
|
+
if (!root || !LV) return;
|
|
1180
|
+
|
|
1181
|
+
// Helper to compile and bind effect
|
|
1182
|
+
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
|
+
|
|
1188
|
+
if (!isAttr) processedNodes.add(node);
|
|
1189
|
+
|
|
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
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
// 1. Find Text Nodes containing '${'
|
|
1232
|
+
const textXPath = ".//text()[contains(., '${')]";
|
|
1233
|
+
const textResult = document.evaluate(
|
|
1234
|
+
textXPath,
|
|
1235
|
+
root,
|
|
1236
|
+
null,
|
|
1237
|
+
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
|
1238
|
+
null
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
for (let i = 0; i < textResult.snapshotLength; i++) {
|
|
1242
|
+
const node = textResult.snapshotItem(i);
|
|
1243
|
+
// Verify it's not inside a skip tag (XPath might pick them up if defined loosely)
|
|
1244
|
+
if (node.parentElement && node.parentElement.closest('SCRIPT, STYLE, CODE, PRE, TEMPLATE, NOSCRIPT')) continue;
|
|
1245
|
+
bindEffect(node, node.textContent);
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// 2. Find Elements with Attributes containing '${'
|
|
1249
|
+
// XPath: select any element (*) that has an attribute (@*) containing '${'
|
|
1250
|
+
const attrXPath = ".//*[@*[contains(., '${')]]";
|
|
1251
|
+
const attrResult = document.evaluate(
|
|
1252
|
+
attrXPath,
|
|
1253
|
+
root,
|
|
1254
|
+
null,
|
|
1255
|
+
XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE,
|
|
1256
|
+
null
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
for (let i = 0; i < attrResult.snapshotLength; i++) {
|
|
1260
|
+
const element = attrResult.snapshotItem(i);
|
|
1261
|
+
if (['SCRIPT', 'STYLE', 'CODE', 'PRE', 'TEMPLATE', 'NOSCRIPT'].includes(element.tagName)) continue;
|
|
1262
|
+
|
|
1263
|
+
// Iterate attributes to find matches (XPath found the element, but not *which* attribute)
|
|
1264
|
+
Array.from(element.attributes).forEach(attr => {
|
|
1265
|
+
if (attr.value.includes('${')) {
|
|
1266
|
+
bindEffect(element, attr.value, true, attr.name);
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// Also check the root itself (XPath .// does not always include the context node for attributes depending on implementation details, safer to check manually if root is element)
|
|
1272
|
+
if (root.nodeType === Node.ELEMENT_NODE && !['SCRIPT', 'STYLE', 'CODE', 'PRE', 'TEMPLATE', 'NOSCRIPT'].includes(root.tagName)) {
|
|
1273
|
+
Array.from(root.attributes).forEach(attr => {
|
|
1274
|
+
if (attr.value.includes('${')) {
|
|
1275
|
+
bindEffect(root, attr.value, true, attr.name);
|
|
1276
|
+
}
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
};
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* Setup MutationObserver to watch for added nodes with src attributes OR reactive syntax
|
|
1283
|
+
* @param {Object} LV - Lightview instance
|
|
1284
|
+
*/
|
|
1285
|
+
const setupSrcObserver = (LV) => {
|
|
1286
|
+
const observer = new MutationObserver((mutations) => {
|
|
1287
|
+
// Collect all nodes to process
|
|
1288
|
+
const nodesToProcess = [];
|
|
1289
|
+
const nodesToActivate = [];
|
|
1290
|
+
|
|
1291
|
+
for (const mutation of mutations) {
|
|
1292
|
+
// Handle added nodes
|
|
1293
|
+
if (mutation.type === 'childList') {
|
|
1294
|
+
for (const node of mutation.addedNodes) {
|
|
1295
|
+
if (node.nodeType === Node.ELEMENT_NODE || node.nodeType === Node.TEXT_NODE) {
|
|
1296
|
+
nodesToActivate.push(node);
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
1300
|
+
|
|
1301
|
+
// Check the added node itself for src
|
|
1302
|
+
nodesToProcess.push(node);
|
|
1303
|
+
|
|
1304
|
+
// Check descendants with src attribute
|
|
1305
|
+
const selector = '[src]:not(' + STANDARD_SRC_TAGS.join('):not(') + ')';
|
|
1306
|
+
const descendants = node.querySelectorAll(selector);
|
|
1307
|
+
for (const desc of descendants) {
|
|
1308
|
+
if (desc.tagName.toLowerCase().startsWith('lv-')) continue;
|
|
1309
|
+
nodesToProcess.push(desc);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Handle attribute changes
|
|
1315
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'src') {
|
|
1316
|
+
nodesToProcess.push(mutation.target);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// Batch processing
|
|
1321
|
+
if (nodesToProcess.length > 0 || nodesToActivate.length > 0) {
|
|
1322
|
+
requestAnimationFrame(() => {
|
|
1323
|
+
nodesToActivate.forEach(node => activateReactiveSyntax(node, LV));
|
|
1324
|
+
nodesToProcess.forEach(node => processSrcOnNode(node, LV));
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
observer.observe(document.body, {
|
|
1330
|
+
childList: true,
|
|
1331
|
+
subtree: true,
|
|
1332
|
+
attributes: true,
|
|
1333
|
+
attributeFilter: ['src']
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
return observer;
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
// Auto-register with Lightview if available
|
|
1340
|
+
if (typeof window !== 'undefined' && window.Lightview) {
|
|
1341
|
+
const LV = window.Lightview;
|
|
1342
|
+
|
|
1343
|
+
// Extend Lightview with simple named signal getter/setter if needed (already in Core now)
|
|
1344
|
+
// But for template literals we use processTemplateChild which needs access to registries
|
|
1345
|
+
// We can just rely on LV.signal.get if it exists, or fall back
|
|
1346
|
+
|
|
1347
|
+
// Setup DOM observer for src attributes on added nodes
|
|
1348
|
+
|
|
1349
|
+
// Setup DOM observer for src attributes on added nodes
|
|
1350
|
+
if (document.readyState === 'loading') {
|
|
1351
|
+
document.addEventListener('DOMContentLoaded', () => setupSrcObserver(LV));
|
|
1352
|
+
} else {
|
|
1353
|
+
setupSrcObserver(LV);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// Also process any existing elements
|
|
1357
|
+
const initialScan = () => {
|
|
1358
|
+
requestAnimationFrame(() => {
|
|
1359
|
+
activateReactiveSyntax(document.body, LV);
|
|
1360
|
+
|
|
1361
|
+
const selector = '[src]:not(' + STANDARD_SRC_TAGS.join('):not(') + ')';
|
|
1362
|
+
const nodes = document.querySelectorAll(selector);
|
|
1363
|
+
nodes.forEach(node => {
|
|
1364
|
+
if (node.tagName.toLowerCase().startsWith('lv-')) return;
|
|
1365
|
+
processSrcOnNode(node, LV);
|
|
1366
|
+
});
|
|
1367
|
+
});
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
if (document.body) {
|
|
1371
|
+
initialScan();
|
|
1372
|
+
} else {
|
|
1373
|
+
document.addEventListener('DOMContentLoaded', initialScan);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Register href click handler
|
|
1377
|
+
LV.hooks.onNonStandardHref = (e) => {
|
|
1378
|
+
handleNonStandardHref(e, {
|
|
1379
|
+
domToElement: LV.internals.domToElement,
|
|
1380
|
+
wrapDomElement: LV.internals.wrapDomElement
|
|
1381
|
+
});
|
|
1382
|
+
};
|
|
1383
|
+
|
|
1384
|
+
// Extend template literal processor to existing processChild hook
|
|
1385
|
+
const existingProcessChild = LV.hooks.processChild;
|
|
1386
|
+
LV.hooks.processChild = (child) => {
|
|
1387
|
+
// First, use the existing hook (Object DOM conversion from lightview.js)
|
|
1388
|
+
if (existingProcessChild) {
|
|
1389
|
+
child = existingProcessChild(child) ?? child;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
// Then process template literals
|
|
1393
|
+
return processTemplateChild(child, {
|
|
1394
|
+
state: state,
|
|
1395
|
+
signal: LV.signal
|
|
1396
|
+
});
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
|
|
1402
|
+
/**
|
|
1403
|
+
* Create a Custom Element class wrapper for a Lightview component
|
|
1404
|
+
* @param {Function} Component - The Lightview component function
|
|
1405
|
+
* @param {Object} options
|
|
1406
|
+
* @param {string} options.cssUrl - Optional URL for component CSS
|
|
1407
|
+
* @param {string[]} options.styles - Optional extra style URLs
|
|
1408
|
+
* @returns {Class} - The Custom Element class
|
|
1409
|
+
*/
|
|
1410
|
+
const createCustomElement = (Component, options = {}) => {
|
|
1411
|
+
return class extends HTMLElement {
|
|
1412
|
+
constructor() {
|
|
1413
|
+
super();
|
|
1414
|
+
this.attachShadow({ mode: 'open' });
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async connectedCallback() {
|
|
1418
|
+
const { cssUrl, styles } = options;
|
|
1419
|
+
|
|
1420
|
+
// Create theme wrapper
|
|
1421
|
+
this.themeWrapper = document.createElement('div');
|
|
1422
|
+
this.themeWrapper.style.display = 'contents';
|
|
1423
|
+
// Sync theme from document
|
|
1424
|
+
const syncTheme = () => {
|
|
1425
|
+
const theme = document.documentElement.getAttribute('data-theme') || 'light';
|
|
1426
|
+
this.themeWrapper.setAttribute('data-theme', theme);
|
|
1427
|
+
};
|
|
1428
|
+
syncTheme();
|
|
1429
|
+
|
|
1430
|
+
// Observe theme changes
|
|
1431
|
+
this.themeObserver = new MutationObserver(syncTheme);
|
|
1432
|
+
this.themeObserver.observe(document.documentElement, {
|
|
1433
|
+
attributes: true,
|
|
1434
|
+
attributeFilter: ['data-theme']
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
// Attach wrapper
|
|
1438
|
+
this.shadowRoot.appendChild(this.themeWrapper);
|
|
1439
|
+
|
|
1440
|
+
// Get stylesheets
|
|
1441
|
+
const adoptedStyleSheets = getAdoptedStyleSheets(cssUrl, styles);
|
|
1442
|
+
|
|
1443
|
+
// Handle adoptedStyleSheets
|
|
1444
|
+
try {
|
|
1445
|
+
const sheets = adoptedStyleSheets.filter(s => s instanceof CSSStyleSheet);
|
|
1446
|
+
this.shadowRoot.adoptedStyleSheets = sheets;
|
|
1447
|
+
} catch (e) {
|
|
1448
|
+
// Fallback handled by individual links below if needed
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
// Handle link tags for strings (fallback or external non-CORS sheets)
|
|
1452
|
+
// Also fallback for DaisyUI if not loaded as adoptedStyleSheet
|
|
1453
|
+
if (!componentConfig.daisyStyleSheet) {
|
|
1454
|
+
const link = document.createElement('link');
|
|
1455
|
+
link.rel = 'stylesheet';
|
|
1456
|
+
link.href = DAISYUI_CDN;
|
|
1457
|
+
this.shadowRoot.appendChild(link);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
adoptedStyleSheets.forEach(s => {
|
|
1461
|
+
if (typeof s === 'string') {
|
|
1462
|
+
const link = document.createElement('link');
|
|
1463
|
+
link.rel = 'stylesheet';
|
|
1464
|
+
link.href = s;
|
|
1465
|
+
this.shadowRoot.appendChild(link);
|
|
1466
|
+
}
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1469
|
+
// Define render function
|
|
1470
|
+
this.render = () => {
|
|
1471
|
+
// Collect props from attributes
|
|
1472
|
+
const props = {};
|
|
1473
|
+
for (const attr of this.attributes) {
|
|
1474
|
+
// Convert kebab-case to camelCase
|
|
1475
|
+
const name = attr.name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
1476
|
+
|
|
1477
|
+
// Convert boolean attributes
|
|
1478
|
+
if (attr.value === '') {
|
|
1479
|
+
props[name] = true;
|
|
1480
|
+
} else {
|
|
1481
|
+
props[name] = attr.value;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// Force useShadow: false to avoid double shadow
|
|
1486
|
+
props.useShadow = false;
|
|
1487
|
+
|
|
1488
|
+
// Render component with a slot for children
|
|
1489
|
+
const slot = window.Lightview.tags.slot();
|
|
1490
|
+
const result = Component(props, slot);
|
|
1491
|
+
|
|
1492
|
+
// Use Lightview's internal setupChildren to render the result
|
|
1493
|
+
// This handles vDOM, DOM nodes, strings, and reactive content
|
|
1494
|
+
window.Lightview.internals.setupChildren([result], this.themeWrapper);
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// Initial render
|
|
1498
|
+
this.render();
|
|
1499
|
+
|
|
1500
|
+
// Observe attribute changes on self to trigger re-render
|
|
1501
|
+
this.attrObserver = new MutationObserver((mutations) => {
|
|
1502
|
+
// Only re-render if actual attributes changed
|
|
1503
|
+
this.render();
|
|
1504
|
+
});
|
|
1505
|
+
this.attrObserver.observe(this, {
|
|
1506
|
+
attributes: true
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
disconnectedCallback() {
|
|
1511
|
+
if (this.themeObserver) {
|
|
1512
|
+
this.themeObserver.disconnect();
|
|
1513
|
+
}
|
|
1514
|
+
if (this.attrObserver) {
|
|
1515
|
+
this.attrObserver.disconnect();
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
};
|
|
1519
|
+
};
|
|
1520
|
+
|
|
1521
|
+
// Export for module usage
|
|
1522
|
+
const LightviewX = {
|
|
1523
|
+
state,
|
|
1524
|
+
themeSignal,
|
|
1525
|
+
setTheme,
|
|
1526
|
+
registerStyleSheet,
|
|
1527
|
+
registerThemeSheet,
|
|
1528
|
+
// Component initialization
|
|
1529
|
+
initComponents,
|
|
1530
|
+
componentConfig,
|
|
1531
|
+
shouldUseShadow,
|
|
1532
|
+
getAdoptedStyleSheets,
|
|
1533
|
+
preloadComponentCSS,
|
|
1534
|
+
createCustomElement
|
|
1535
|
+
};
|
|
1536
|
+
|
|
1537
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1538
|
+
module.exports = LightviewX;
|
|
1539
|
+
}
|
|
1540
|
+
if (typeof window !== 'undefined') {
|
|
1541
|
+
window.LightviewX = LightviewX;
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Initialize component hook to use Object DOM
|
|
1545
|
+
if (typeof window !== 'undefined') {
|
|
1546
|
+
// Auto-load theme
|
|
1547
|
+
try {
|
|
1548
|
+
const savedTheme = getSavedTheme();
|
|
1549
|
+
if (savedTheme) {
|
|
1550
|
+
setTheme(savedTheme);
|
|
1551
|
+
}
|
|
1552
|
+
} catch (e) { /* ignore */ }
|
|
1553
|
+
|
|
1554
|
+
window.addEventListener('load', () => {
|
|
1555
|
+
if (window.Lightview) {
|
|
1556
|
+
window.Lightview.hooks.processChild = (child) => {
|
|
1557
|
+
// Convert Object DOM syntax if applicable
|
|
1558
|
+
if (typeof child === 'object' && child !== null && !Array.isArray(child)) {
|
|
1559
|
+
return convertObjectDOM(child);
|
|
1560
|
+
}
|
|
1561
|
+
return child;
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// Immediate check in case load already fired or script is defer
|
|
1567
|
+
if (window.Lightview) {
|
|
1568
|
+
window.Lightview.hooks.processChild = (child) => {
|
|
1569
|
+
// Convert Object DOM syntax if applicable
|
|
1570
|
+
if (typeof child === 'object' && child !== null && !Array.isArray(child)) {
|
|
1571
|
+
return convertObjectDOM(child);
|
|
1572
|
+
}
|
|
1573
|
+
return child;
|
|
1574
|
+
};
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
})();
|