lightview 1.8.1-b → 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.
Files changed (224) hide show
  1. package/.agent/workflows/daisyui-component-migration.md +155 -0
  2. package/.codacy/cli.sh +149 -0
  3. package/.codacy/codacy.yaml +15 -0
  4. package/.github/instructions/codacy.instructions.md +72 -0
  5. package/.wranglerignore +21 -0
  6. package/README.md +1331 -21
  7. package/_headers +4 -0
  8. package/build.js +70 -0
  9. package/components/actions/button.js +151 -0
  10. package/components/actions/dropdown.js +120 -0
  11. package/components/actions/modal.js +146 -0
  12. package/components/actions/swap.js +118 -0
  13. package/components/daisyui.js +288 -0
  14. package/components/data-display/accordion.js +128 -0
  15. package/components/data-display/alert.js +112 -0
  16. package/components/data-display/avatar.js +170 -0
  17. package/components/data-display/badge.js +82 -0
  18. package/components/data-display/card.js +151 -0
  19. package/components/data-display/carousel.js +94 -0
  20. package/components/data-display/chart.js +220 -0
  21. package/components/data-display/chat.js +128 -0
  22. package/components/data-display/collapse.js +103 -0
  23. package/components/data-display/countdown.js +69 -0
  24. package/components/data-display/diff.js +111 -0
  25. package/components/data-display/kbd.js +65 -0
  26. package/components/data-display/loading.js +75 -0
  27. package/components/data-display/progress.js +79 -0
  28. package/components/data-display/radial-progress.js +88 -0
  29. package/components/data-display/skeleton.js +66 -0
  30. package/components/data-display/stats.js +159 -0
  31. package/components/data-display/table.js +146 -0
  32. package/components/data-display/timeline.js +146 -0
  33. package/components/data-display/toast.js +72 -0
  34. package/components/data-display/tooltip.js +74 -0
  35. package/components/data-input/checkbox.js +253 -0
  36. package/components/data-input/file-input.js +224 -0
  37. package/components/data-input/input.js +264 -0
  38. package/components/data-input/radio.js +338 -0
  39. package/components/data-input/range.js +204 -0
  40. package/components/data-input/rating.js +219 -0
  41. package/components/data-input/select.js +287 -0
  42. package/components/data-input/textarea.js +287 -0
  43. package/components/data-input/toggle.js +201 -0
  44. package/components/index.js +137 -0
  45. package/components/layout/divider.js +72 -0
  46. package/components/layout/drawer.js +142 -0
  47. package/components/layout/footer.js +100 -0
  48. package/components/layout/hero.js +109 -0
  49. package/components/layout/indicator.js +90 -0
  50. package/components/layout/join.js +78 -0
  51. package/components/layout/navbar.js +110 -0
  52. package/components/navigation/breadcrumbs.js +91 -0
  53. package/components/navigation/dock.js +103 -0
  54. package/components/navigation/menu.js +126 -0
  55. package/components/navigation/pagination.js +105 -0
  56. package/components/navigation/steps.js +89 -0
  57. package/components/navigation/tabs.css +177 -0
  58. package/components/navigation/tabs.js +123 -0
  59. package/components/theme/theme-switch.css +65 -0
  60. package/components/theme/theme-switch.js +177 -0
  61. package/docs/about.html +164 -0
  62. package/docs/api/computed.html +184 -0
  63. package/docs/api/effects.html +173 -0
  64. package/docs/api/elements.html +180 -0
  65. package/docs/api/enhance.html +225 -0
  66. package/docs/api/hypermedia.html +165 -0
  67. package/docs/api/index.html +178 -0
  68. package/docs/api/nav.html +18 -0
  69. package/docs/api/signals.html +136 -0
  70. package/docs/api/state.html +217 -0
  71. package/docs/assets/images/logo-favicon.svg +42 -0
  72. package/docs/assets/images/logo-static.svg +40 -0
  73. package/docs/assets/images/logo.svg +66 -0
  74. package/docs/assets/js/examplify.js +395 -0
  75. package/docs/assets/styles/site.css +1102 -0
  76. package/docs/assets/styles/themes.css +236 -0
  77. package/docs/components/accordion.html +439 -0
  78. package/docs/components/alert.html +528 -0
  79. package/docs/components/avatar.html +586 -0
  80. package/docs/components/badge.html +531 -0
  81. package/docs/components/breadcrumbs.html +278 -0
  82. package/docs/components/button.html +579 -0
  83. package/docs/components/card.html +561 -0
  84. package/docs/components/carousel.html +286 -0
  85. package/docs/components/chart-area.html +702 -0
  86. package/docs/components/chart-bar.html +782 -0
  87. package/docs/components/chart-column.html +735 -0
  88. package/docs/components/chart-line.html +794 -0
  89. package/docs/components/chart-pie.html +823 -0
  90. package/docs/components/chart.html +612 -0
  91. package/docs/components/chat.html +547 -0
  92. package/docs/components/checkbox.html +641 -0
  93. package/docs/components/collapse.html +536 -0
  94. package/docs/components/component-nav.html +53 -0
  95. package/docs/components/countdown.html +470 -0
  96. package/docs/components/diff.html +245 -0
  97. package/docs/components/divider.html +240 -0
  98. package/docs/components/dock.html +277 -0
  99. package/docs/components/drawer.html +515 -0
  100. package/docs/components/dropdown.html +479 -0
  101. package/docs/components/file-input.html +591 -0
  102. package/docs/components/footer.html +301 -0
  103. package/docs/components/gallery.html +504 -0
  104. package/docs/components/hero.html +264 -0
  105. package/docs/components/index.css +840 -0
  106. package/docs/components/index.html +735 -0
  107. package/docs/components/indicator.html +342 -0
  108. package/docs/components/input.html +644 -0
  109. package/docs/components/join.html +285 -0
  110. package/docs/components/kbd.html +322 -0
  111. package/docs/components/loading.html +521 -0
  112. package/docs/components/menu.html +461 -0
  113. package/docs/components/modal.html +639 -0
  114. package/docs/components/navbar.html +321 -0
  115. package/docs/components/pagination.html +279 -0
  116. package/docs/components/progress.html +514 -0
  117. package/docs/components/radial-progress.html +434 -0
  118. package/docs/components/radio.html +655 -0
  119. package/docs/components/range.html +611 -0
  120. package/docs/components/rating.html +642 -0
  121. package/docs/components/select.html +696 -0
  122. package/docs/components/sidebar-setup.js +93 -0
  123. package/docs/components/skeleton.html +447 -0
  124. package/docs/components/spinner.html +68 -0
  125. package/docs/components/stats.html +486 -0
  126. package/docs/components/steps.html +356 -0
  127. package/docs/components/swap.html +517 -0
  128. package/docs/components/switch.html +68 -0
  129. package/docs/components/table.html +668 -0
  130. package/docs/components/tabs.html +506 -0
  131. package/docs/components/text-input.html +68 -0
  132. package/docs/components/textarea.html +603 -0
  133. package/docs/components/timeline.html +487 -0
  134. package/docs/components/toast.html +474 -0
  135. package/docs/components/toggle.html +564 -0
  136. package/docs/components/tooltip.html +423 -0
  137. package/docs/examples/getting-started-example.html +40 -0
  138. package/docs/examples/index.html +93 -0
  139. package/docs/getting-started/index.html +739 -0
  140. package/docs/getting-started/reviews.html +23 -0
  141. package/docs/getting-started/reviews.odom +108 -0
  142. package/docs/getting-started/reviews.vdom +84 -0
  143. package/docs/index.html +134 -0
  144. package/docs/playground.html +416 -0
  145. package/docs/router.html +285 -0
  146. package/docs/styles/index.html +190 -0
  147. package/functions/_middleware.js +32 -0
  148. package/index.html +309 -0
  149. package/lightview-router.js +364 -0
  150. package/lightview-x.js +1577 -0
  151. package/lightview.js +658 -1109
  152. package/lightview.js.backup +793 -0
  153. package/middleware/locale.js +25 -0
  154. package/middleware/markdown.js +44 -0
  155. package/middleware/notFound.js +37 -0
  156. package/package.json +27 -41
  157. package/watch.js +92 -0
  158. package/wrangler.toml +12 -0
  159. package/.idea/lightview.iml +0 -12
  160. package/.idea/modules.xml +0 -8
  161. package/.idea/vcs.xml +0 -6
  162. package/LICENSE +0 -21
  163. package/codepen-no-tabs-embed.css +0 -2
  164. package/components/chart/chart.html +0 -17
  165. package/components/chart/example.html +0 -32
  166. package/components/chart.html +0 -83
  167. package/components/components.js +0 -113
  168. package/components/gantt/example.html +0 -22
  169. package/components/gantt/gantt.html +0 -42
  170. package/components/gauge/example.html +0 -28
  171. package/components/gauge/gauge.html +0 -20
  172. package/components/gauge.html +0 -60
  173. package/components/orgchart/example.html +0 -25
  174. package/components/orgchart/orgchart.html +0 -41
  175. package/components/repl/code-editor.html +0 -64
  176. package/components/repl/editor.html +0 -37
  177. package/components/repl/editorjs-inline-tool/index.js +0 -3
  178. package/components/repl/editorjs-inline-tool/inline-tools.js +0 -28
  179. package/components/repl/editorjs-inline-tool/tool.js +0 -175
  180. package/components/repl/repl-with-wysiwyg.html +0 -355
  181. package/components/repl/repl.html +0 -345
  182. package/components/repl/sup.js +0 -44
  183. package/components/repl/wysiwyg-repl.html +0 -258
  184. package/components/timeline/example.html +0 -33
  185. package/components/timeline/timeline.html +0 -44
  186. package/components/timeline.html +0 -81
  187. package/examples/anchor.html +0 -11
  188. package/examples/chart.html +0 -34
  189. package/examples/counter.html +0 -26
  190. package/examples/counter.test.mjs +0 -47
  191. package/examples/counter2.html +0 -26
  192. package/examples/directives.html +0 -79
  193. package/examples/foreign.html +0 -50
  194. package/examples/forgeinform.html +0 -98
  195. package/examples/form.html +0 -61
  196. package/examples/gauge.html +0 -18
  197. package/examples/invalid-template-literals.html +0 -44
  198. package/examples/medium/remote.html +0 -60
  199. package/examples/message.html +0 -18
  200. package/examples/nested.html +0 -11
  201. package/examples/object-bound-form.html +0 -34
  202. package/examples/remote-server.js +0 -51
  203. package/examples/remote.html +0 -34
  204. package/examples/remote.json +0 -1
  205. package/examples/scratch.html +0 -69
  206. package/examples/sensors/index.html +0 -30
  207. package/examples/sensors/sensor-server.js +0 -30
  208. package/examples/shared.html +0 -41
  209. package/examples/template.html +0 -33
  210. package/examples/timeline.html +0 -21
  211. package/examples/todo.html +0 -38
  212. package/examples/top.html +0 -10
  213. package/examples/types.html +0 -94
  214. package/examples/xor.html +0 -62
  215. package/jest-puppeteer.config.js +0 -5
  216. package/jest.config.json +0 -12
  217. package/sites/client.html +0 -48
  218. package/sites/index.html +0 -247
  219. package/test/basic.html +0 -93
  220. package/test/basic.test.mjs +0 -315
  221. package/test/extended.html +0 -29
  222. package/test/extended.test.mjs +0 -448
  223. package/types.js +0 -534
  224. package/unsplash.key +0 -1
package/lightview.js CHANGED
@@ -1,1195 +1,744 @@
1
- /*
2
- MIT License
3
-
4
- Copyright (c) 2022 AnyWhichWay, LLC - Lightview Small, simple, powerful UI creation ...
5
-
6
- Permission is hereby granted, free of charge, to any person obtaining a copy
7
- of this software and associated documentation files (the "Software"), to deal
8
- in the Software without restriction, including without limitation the rights
9
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
- copies of the Software, and to permit persons to whom the Software is
11
- furnished to do so, subject to the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be included in all
14
- copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
- SOFTWARE.
23
- */
24
-
25
- // <script src="https://000686818.codepen.website/lightview.js?as=x-body"></script>
26
- /*
27
- self.variables({name:"string"})
28
- imported(x) => exported(x) => reactive(x) => remote(x,{path:".
29
- */
30
-
31
- if(document.body) document.currentComponent = document.body;
32
-
33
- const Lightview = {};
34
-
35
- const {observe} = (() => {
36
- let CURRENTOBSERVER;
37
- const parser = new DOMParser();
38
-
39
- const templateSanitizer = (string) => {
40
- return string.replace(/function\s+/g, "")
41
- .replace(/function\(/g, "")
42
- .replace(/=\s*>/g, "")
43
- .replace(/(while|do|for|alert)\s*\(/g, "")
44
- .replace(/console\.[a-zA-Z$]+\s*\(/g, "");
45
- }
46
- Lightview.sanitizeTemplate = templateSanitizer;
1
+ (() => {
2
+ // ============= SIGNALS =============
47
3
 
48
- const escaper = document.createElement('textarea');
49
- function escapeHTML(html) {
50
- escaper.textContent = html;
51
- return escaper.innerHTML;
52
- }
53
- Lightview.escapeHTML = escapeHTML;
4
+ let currentEffect = null;
54
5
 
55
- const isArrowFunction = (f) => typeof(f)==="function" && (f+"").match(/\(*.*\)*\s*=>/g);
56
6
 
57
- const getTemplateVariableName = (template) => {
58
- if(template && /^\$\{[a-zA-z_.]*\}$/g.test(template)) return template.substring(2, template.length - 1);
59
- }
60
-
61
- const walk = (target,path,depth=path.length-1,create) => {
62
- for(let i=0;i<=depth;i++) {
63
- target = (target[path[i]]==null && create ? target[path[i]] = (typeof(create)==="function" ? Object.create(create.prototype) : {}) : target[path[i]]);
64
- if(target===undefined) return;
7
+ const getOrSet = (map, key, factory) => {
8
+ let v = map.get(key);
9
+ if (!v) {
10
+ v = factory();
11
+ map.set(key, v);
65
12
  }
66
- return target;
67
- }
13
+ return v;
14
+ };
68
15
 
69
- const addListener = (node, eventName, callback, self) => {
70
- node.addEventListener(eventName, (event) => {
71
- if(self) event.self = self;
72
- callback(event);
73
- });
74
- }
16
+ const nodeState = new WeakMap();
17
+ const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
75
18
 
76
- const anchorHandler = async (event) => {
77
- event.preventDefault();
78
- const target = event.target;
79
- if (target === event.currentTarget) {
80
- const {as} = await importLink(target),
81
- targets = querySelectorAll(document, target.getAttribute("target"));
82
- targets.forEach((target) => {
83
- while (target.lastChild) target.lastChild.remove();
84
- target.appendChild(document.createElement(as))
85
- })
86
- }
87
- }
88
- const getNameFromPath = (path) => {
89
- const file = path.split("/").pop(),
90
- name = file.split(".")[0];
91
- if (name.includes("-")) return name;
92
- return "l-" + name;
93
- }
94
- const observe = (f, thisArg, argsList = []) => {
95
- const observer = (...args) => {
96
- if(observer.cancelled) return;
97
- CURRENTOBSERVER = observer;
98
- try {
99
- f.call(thisArg || this, ...argsList, ...args);
100
- } catch (e) {
19
+ const signalRegistry = new Map();
101
20
 
102
- }
103
- CURRENTOBSERVER = null;
104
- }
105
- observer.cancel = () => observer.cancelled = true;
106
- observer();
107
- return observer;
108
- }
109
- const coerce = (value, toType) => {
110
- if (value + "" === "null" || value + "" === "undefined" || toType==="any") return value;
111
- const type = typeof (value);
112
- if (type === toType) return value;
113
- if (toType === "number") return parseFloat(value + "");
114
- if (toType === "boolean") {
115
- if (["on", "checked", "selected"].includes(value)) return true;
116
- if (value == null || value === "") return false;
117
- try {
118
- const parsed = JSON.parse(value + "");
119
- if (typeof (parsed) === "boolean") return parsed;
120
- return [1, "on", "checked", "selected"].includes(parsed);
121
- } catch (e) {
122
- throw new TypeError(`Unable to convert ${value} into 'boolean'`);
123
- }
124
- }
125
- if (toType === "string") return value + "";
126
- const isfunction = typeof (toType) === "function";
127
- if ((toType === "object" || isfunction)) {
128
- if (type === "object" && isfunction && value instanceof toType) return value;
129
- if (type === "string") {
130
- value = value.trim();
21
+ const signal = (initialValue, optionsOrName) => {
22
+ let name = typeof optionsOrName === 'string' ? optionsOrName : optionsOrName?.name;
23
+ const storage = optionsOrName?.storage;
24
+
25
+ if (name && storage) {
26
+ const stored = storage.getItem(name);
27
+ if (stored !== null) {
28
+ initialValue = stored;
131
29
  try {
132
- if (isfunction) {
133
- const instance = toType === Date ? new Date() : Object.create(toType.prototype);
134
- if (instance instanceof Array) {
135
- let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
136
- if (!Array.isArray(parsed)) {
137
- if (value.includes(",")) parsed = value.split(",");
138
- else {
139
- parsed = tryParse(`["${value}"]`);
140
- if (!Array.isArray(parsed) || parsed[0] !== value && parsed.length !== 1) parsed = null;
141
- }
142
- }
143
- if (!Array.isArray(parsed)) {
144
- throw new TypeError(`Expected an Array for parsed data`)
145
- }
146
- instance.push(...parsed);
147
- } else if (instance instanceof Date) {
148
- instance.setTime(Date.parse(value));
149
- } else {
150
- Object.assign(instance, JSON.parse(value));
151
- }
152
- return instance;
153
- }
154
- return JSON.parse(value);
30
+ initialValue = JSON.parse(stored);
155
31
  } catch (e) {
156
- throw new TypeError(`Unable to convert ${value} into ${isfunction ? toType.name : type}`);
32
+ // Ignore storage errors
157
33
  }
158
34
  }
159
35
  }
160
- throw new TypeError(`Unable to coerce ${value} to ${toType}`)
161
- }
162
- const Reactor = (data) => {
163
- if (data && typeof (data) === "object") {
164
- if (data.__isReactor__) return data;
165
- const childReactors = [],
166
- dependents = {},
167
- proxy = new Proxy(data, {
168
- get(target, property) {
169
- if (property === "__isReactor__") return true;
170
- if(property=== "__dependents__") return dependents;
171
- if(property=== "__reactorProxyTarget__") return data;
172
- if (target instanceof Array) {
173
- if (property === "toJSON") return function toJSON() { return [...target]; }
174
- if (property === "toString") return function toString() { return JSON.stringify([...target]); }
175
- }
176
- if(target instanceof Date) {
177
- const value = data[property];
178
- if(typeof(value)==="function") return value.bind(data);
179
- }
180
- let value = target[property];
181
- const type = typeof (value);
182
- if (CURRENTOBSERVER && typeof (property) !== "symbol" && type !== "function") {
183
- const observers = dependents[property] ||= new Set();
184
- observers.add(CURRENTOBSERVER)
185
- }
186
- if(value===undefined) return;
187
- if (childReactors.includes(value) || (value && type !== "object") || typeof (property) === "symbol") {
188
- // Dates and Promises must be bound to work with proxies
189
- if (type === "function" && ([Date].includes(value) || property==="then")) value = value.bind(target)
190
- return value;
191
- }
192
- if (value && type === "object") {
193
- value = Reactor(value);
194
- childReactors.push(value);
195
- }
196
- target[property] = value;
197
- return value;
198
- },
199
- async set(target, property, value) {
200
- if(target instanceof Promise) {
201
- console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
202
- }
203
- const type = typeof (value);
204
- if(value && type==="object" && value instanceof Promise) value = await value;
205
- if (target[property] !== value) {
206
- if (value && type === "object") {
207
- value = Reactor(value);
208
- childReactors.push(value);
209
- }
210
- target[property] = value;
211
- const observers = dependents[property] || [];
212
- [...observers].forEach((f) => {
213
- if (f.cancelled) dependents[property].delete(f);
214
- else f();
215
- })
216
- }
217
- return true;
218
- }
219
- });
220
- return proxy;
221
- }
222
- return data;
223
- }
224
36
 
225
- class VariableEvent {
226
- constructor(config) {
227
- Object.assign(this, config);
228
- }
229
- }
37
+ let value = initialValue;
38
+ const subscribers = new Set();
230
39
 
231
- const createVarsProxy = (vars, component, constructor) => {
232
- return new Proxy(vars, {
233
- get(target, property) {
234
- if(property==="self") return component;
235
- if(target instanceof Date) return Reflect.get(target,property);
236
- let {value,get} = target[property] || {};
237
- if(get) return target[property].value = get.call(target[property]);
238
- if (typeof (value) === "function") return value.bind(target);
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
+ }
239
51
  return value;
240
52
  },
241
- set(target, property, newValue) {
242
- const event = new VariableEvent({variableName: property, value: newValue});
243
- if (target[property] === undefined) {
244
- target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
245
- target.postEvent.value("change", event);
246
- if (event.defaultPrevented) delete target[property].value;
247
- return true;
248
- }
249
- const variable = target[property],
250
- {value, shared, exported, constant, reactive, remote} = variable;
251
- let type = variable.type;
252
- if (constant) throw new TypeError(`${property}:${type} is a constant`);
253
- if(newValue!=null || type.required) newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
254
- const newtype = typeof (newValue),
255
- typetype = typeof (type);
256
- if ((newValue == null && !type.required) ||
257
- type === "any" ||
258
- (newtype === type && typetype==="string") ||
259
- (typetype === "function" && !type.validate && (newValue && newtype === "object" && newValue instanceof type) || variable.validityState?.valid)) {
260
- if (value !== newValue) {
261
- event.oldValue = value;
262
- target[property].value = reactive ? Reactor(newValue) : newValue; // do first to prevent loops
263
- target.postEvent.value("change", event);
264
- if (event.defaultPrevented) target[property].value = value;
265
- else if(remote && (variable.reactive || remote.put)) remote.handleRemote({variable,config:remote.config},true);
266
- else if(variable.set) variable.set(newValue);
53
+ set(newValue) {
54
+ if (value !== newValue) {
55
+ value = newValue;
56
+ if (name && storage) {
57
+ try {
58
+ storage.setItem(name, JSON.stringify(value));
59
+ } catch (e) {
60
+ // Ignore storage errors
61
+ }
267
62
  }
268
- return true;
63
+ // Copy subscribers to avoid infinite loop when effect re-subscribes during iteration
64
+ [...subscribers].forEach(effect => effect());
269
65
  }
270
- if (typetype === "function" && newValue && newtype === "object") {
271
- throw new TypeError(`Can't assign instance of '${newValue.constructor.name}' to variable '${property}:${type.name.replace("bound ", "")}'`)
272
- }
273
- throw new TypeError(`Can't assign '${typeof (newValue)} ${newtype === "string" ? '"' + newValue + '"' : newValue}' to variable '${property}:${typetype === "function" ? type.name.replace("bound ", "") : type} ${type.required ? "required" : ""}'`)
274
- },
275
- keys() {
276
- return [...Object.keys(vars)];
277
66
  }
278
67
  });
279
- }
280
- const createObserver = (domNode, framed) => {
281
- const mobserver = new MutationObserver((mutations) => {
282
- mutations.forEach((mutation) => {
283
- const target = mutation.target;
284
- if (mutation.type === "attributes") {
285
- //if (framed) debugger;
286
- const name = mutation.attributeName,
287
- value = target.getAttribute(name);
288
- if (framed && name === "message" && target instanceof IFrameElement) {
289
- //if (value) console.log("message", value);
290
- target.removeAttribute(name);
291
- target.dispatchEvent(new CustomEvent("message", {detail: JSON.parse(value)}))
292
- }
293
- if (target.observedAttributes && target.observedAttributes.includes(name) && value !== mutation.oldValue) {
294
- target.setVariableValue(name, value);
295
- }
296
- } else if (mutation.type === "childList") {
297
- for (const target of mutation.removedNodes) {
298
- if (target.disconnectedCallback) target.disconnectedCallback();
299
- }
300
- for (const target of mutation.addedNodes) {
301
- if (target.connectedCallback) target.connectedCallback();
302
- }
303
- } else if(mutation.type === "characterData") {
304
- if(target.characterDataMutationCallback) target.characterDataMutationCallback(target,mutation.oldValue,target.textContent);
305
- }
306
- });
68
+
69
+ if (name) {
70
+ signalRegistry.set(name, f);
71
+ }
72
+
73
+ return f;
74
+ };
75
+
76
+ signal.get = (name, defaultValue) => {
77
+ if (!signalRegistry.has(name) && defaultValue !== undefined) {
78
+ return signal(defaultValue, name);
79
+ }
80
+ return signalRegistry.get(name);
81
+ };
82
+
83
+ const effect = (fn) => {
84
+ const execute = () => {
85
+ if (!execute.active) return;
86
+ // Cleanup old dependencies
87
+ execute.dependencies.forEach(dep => dep.delete(execute));
88
+ execute.dependencies.clear();
89
+
90
+ currentEffect = execute;
91
+ fn();
92
+ currentEffect = null;
93
+ };
94
+
95
+ execute.active = true;
96
+ execute.dependencies = new Set();
97
+ execute.stop = () => {
98
+ execute.dependencies.forEach(dep => dep.delete(execute));
99
+ execute.dependencies.clear();
100
+ execute.active = false;
101
+ };
102
+ execute();
103
+ return execute;
104
+ };
105
+
106
+ const trackEffect = (node, effectFn) => {
107
+ const state = getOrSet(nodeState, node, nodeStateFactory);
108
+ if (!state.effects) state.effects = [];
109
+ state.effects.push(effectFn);
110
+ };
111
+
112
+ const computed = (fn) => {
113
+ const sig = signal(undefined);
114
+ effect(() => {
115
+ sig.value = fn();
307
116
  });
308
- mobserver.observe(domNode, {subtree: true, childList: true});
309
- return mobserver;
310
- }
311
- const querySelectorAll = (node, selector) => {
312
- const nodes = [...node.querySelectorAll(selector)],
313
- nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
314
- let currentNode;
315
- while (currentNode = nodeIterator.nextNode()) {
316
- if (currentNode.shadowRoot) nodes.push(...querySelectorAll(currentNode.shadowRoot, selector));
117
+ return sig;
118
+ };
119
+
120
+
121
+ // ============= SHADOW DOM SUPPORT =============
122
+ // Marker symbol to identify shadowDOM directives
123
+ const SHADOW_DOM_MARKER = Symbol('lightview.shadowDOM');
124
+
125
+ /**
126
+ * Create a shadowDOM directive marker
127
+ * @param {Object} attributes - { mode: 'open'|'closed', styles?: string[], adoptedStyleSheets?: CSSStyleSheet[] }
128
+ * @param {Array} children - Children to render inside the shadow root
129
+ * @returns {Object} - Marker object for setupChildren to process
130
+ */
131
+ const createShadowDOMMarker = (attributes, children) => ({
132
+ [SHADOW_DOM_MARKER]: true,
133
+ mode: attributes.mode || 'open',
134
+ styles: attributes.styles || [],
135
+ adoptedStyleSheets: attributes.adoptedStyleSheets || [],
136
+ children
137
+ });
138
+
139
+ /**
140
+ * Check if an object is a shadowDOM marker
141
+ */
142
+ const isShadowDOMMarker = (obj) => obj && typeof obj === 'object' && obj[SHADOW_DOM_MARKER] === true;
143
+
144
+ /**
145
+ * Process a shadowDOM marker by attaching shadow root and rendering children
146
+ * @param {Object} marker - The shadowDOM marker
147
+ * @param {HTMLElement} parentNode - The DOM node to attach shadow to
148
+ */
149
+ const processShadowDOM = (marker, parentNode) => {
150
+ // Don't attach if already has shadow root
151
+ if (parentNode.shadowRoot) {
152
+ console.warn('Lightview: Element already has a shadowRoot, skipping shadowDOM directive');
153
+ return;
317
154
  }
318
- return nodes;
319
- }
320
- const getNodes = (root) => {
321
- const nodes = new Set();
322
- if (root.shadowRoot) {
323
- nodes.add(root);
324
- getNodes(root.shadowRoot).forEach((node) => nodes.add(node))
325
- } else {
326
- for (const node of root.childNodes) {
327
- if (node.tagName === "SCRIPT") continue;
328
- if (node.nodeType === Node.TEXT_NODE && node.nodeValue?.includes("${")) {
329
- node.template ||= node.nodeValue;
330
- nodes.add(node);
331
- } else if (node.nodeType === Node.ELEMENT_NODE) {
332
- let skip;
333
- [...node.attributes].forEach((attr) => {
334
- if (attr.value.includes("${")) {
335
- attr.template ||= attr.value;
336
- nodes.add(node);
337
- } else if (attr.name.includes(":") || attr.name.startsWith("l-")) {
338
- skip = attr.name.includes("l-for:");
339
- nodes.add(node)
340
- }
341
- })
342
- if (node.getAttribute("type") === "radio") nodes.add(node);
343
- if (!skip && !node.shadowRoot) getNodes(node).forEach((node) => nodes.add(node));
155
+
156
+ // Attach shadow root
157
+ const shadowRoot = parentNode.attachShadow({ mode: marker.mode });
158
+
159
+ // Split adoptedStyleSheets into sheets and urls
160
+ const sheets = [];
161
+ const linkUrls = [...(marker.styles || [])];
162
+
163
+ if (marker.adoptedStyleSheets && marker.adoptedStyleSheets.length > 0) {
164
+ marker.adoptedStyleSheets.forEach(item => {
165
+ if (item instanceof CSSStyleSheet) {
166
+ sheets.push(item);
167
+ } else if (typeof item === 'string') {
168
+ linkUrls.push(item);
344
169
  }
345
- }
170
+ });
346
171
  }
347
- return nodes;
348
- }
349
172
 
350
- const resolveNodeOrText = (node, component, safe,extras=node.extras||{}) => {
351
- const type = typeof (node),
352
- template = type === "string" ? node.trim() : node.template;
353
- if (template) {
354
- const name = getTemplateVariableName(template);
173
+ // Handle adoptedStyleSheets (modern, efficient approach)
174
+ if (sheets.length > 0) {
355
175
  try {
356
- const parts = name ? name.split(".") : null;
357
- let value;
358
- value = (parts
359
- ? (value = walk(extras,parts)) || (value = walk(component.varsProxy,parts)) || (value == null ? component[name] : value)
360
- : Function("context", "extras", "with(context) { with(extras) { return `" + (safe ? template : Lightview.sanitizeTemplate(template)) + "` } }")(component.varsProxy,extras));
361
- //let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
362
- if(typeof(value)==="function") return value;
363
- value = (name || node.nodeType === Node.TEXT_NODE || safe ? value : Lightview.escapeHTML(value));
364
- if (type === "string") return value==="undefined" ? undefined : value;
365
- if(name) {
366
- node.nodeValue = value==null ? "" : typeof(value)==="string" ? value : JSON.stringify(value);
367
- } else {
368
- node.nodeValue = value == "null" || value == "undefined" ? "" : value;
369
- }
370
- return value;
176
+ shadowRoot.adoptedStyleSheets = sheets;
371
177
  } catch (e) {
372
- //console.warn(e);
373
- if (!e.message.includes("defined")) throw e; // actually looking for undefined or not defined
374
- return undefined;
178
+ console.warn('Lightview: adoptedStyleSheets not supported');
375
179
  }
376
180
  }
377
- return node?.nodeValue;
378
- }
379
- const inputTypeToType = (inputType) => {
380
- if (!inputType) return "any"
381
- if (["text", "tel", "email", "url", "search", "radio", "color", "password"].includes(inputType)) return "string";
382
- if (["number", "range"].includes(inputType)) return "number";
383
- if (["datetime"].includes(inputType)) return Date;
384
- if (["checkbox"].includes(inputType)) return "boolean";
385
- return "any";
386
- }
387
- const importAnchors = (node, component) => {
388
- [...node.querySelectorAll('a[href$=".html"][target^="#"]')].forEach((node) => {
389
- node.removeEventListener("click", anchorHandler);
390
- addListener(node, "click", anchorHandler);
391
- })
392
- }
393
- const bound = new WeakSet();
394
- const bindInput = (input, variableName, component, value, object) => {
395
- if (bound.has(input)) return;
396
- bound.add(input);
397
- const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
398
- nameparts = variableName.split(".");
399
- let type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
400
- const variable = walk(component.vars,nameparts) || {type};
401
- if(type==="any") type = variable?.type.type || variable?.type;
402
- if(value==null) {
403
- const avalue = input.getAttribute("value");
404
- if(avalue) value = avalue;
405
- }
406
- if(object && nameparts.length>1) {
407
- const [root,...path] = nameparts;
408
- object = walk(object,path,path.length-2,true);
409
- const key = path[path.length-1];
410
- object[key] = coerce(value,type);
411
- } else {
412
- const existing = component.vars[variableName];
413
- if(existing) {
414
- existingtype = existing?.type.type || existing?.type;
415
- if(existingtype!==type) throw new TypeError(`Attempt to bind <input name="${variableName}" type="${type}"> to variable ${variableName}:${existing.type}`)
416
- existing.reactive = true;
417
- } else {
418
- component.variables({[variableName]: type},{reactive});
419
- }
420
- if(inputtype!=="radio") {
421
- if(typeof(value)==="string" && value.includes("${")) input.setAttribute("value","");
422
- else component.setVariableValue(variableName, coerce(value,type));
423
- }
181
+
182
+ // Inject stylesheet links
183
+ for (const styleUrl of linkUrls) {
184
+ const link = document.createElement('link');
185
+ link.rel = 'stylesheet';
186
+ link.href = styleUrl;
187
+ shadowRoot.appendChild(link);
424
188
  }
425
- let eventname = "change";
426
- if(input.tagName==="FORM") {
427
- eventname = "submit"
428
- } else if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
429
- eventname = "input";
189
+
190
+ // Setup children inside shadow root
191
+ if (marker.children && marker.children.length > 0) {
192
+ setupChildrenInTarget(marker.children, shadowRoot);
430
193
  }
431
- const listener = (event) => {
432
- if (event) event.stopImmediatePropagation();
433
- let value = input.value;
434
- if (inputtype === "checkbox") {
435
- value = input.checked
436
- } else if (input.tagName === "SELECT" && input.hasAttribute("multiple")) {
437
- value = [...input.querySelectorAll("option")]
438
- .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value)
439
- .map((option) => option.getAttribute("value") || option.innerText);
440
- }
441
- if(object) {
442
- const [root,...path] = nameparts;
443
- object = walk(object,nameparts,path.length-2,true);
444
- } else {
445
- object = walk(component.varsProxy,nameparts,nameparts.length-2,true);
446
- }
447
- const key = nameparts[nameparts.length-1];
448
- object[key] = coerce(value,type);
194
+ };
195
+
196
+ // ============= REACTIVE UI =============
197
+ const SVG_TAGS = new Set([
198
+ 'svg', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'g', 'defs', 'marker',
199
+ 'pattern', 'mask', 'image', 'text', 'tspan', 'foreignObject', 'use', 'symbol', 'clipPath',
200
+ 'linearGradient', 'radialGradient', 'stop', 'filter', 'animate', 'animateMotion',
201
+ 'animateTransform', 'mpath', 'desc', 'metadata', 'title', 'feBlend', 'feColorMatrix',
202
+ 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting',
203
+ 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB',
204
+ 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode',
205
+ 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight',
206
+ 'feTile', 'feTurbulence', 'view'
207
+ ]);
208
+
209
+ const domToElement = new WeakMap();
210
+
211
+ const wrapDomElement = (domNode, tag, attributes = {}, children = []) => {
212
+ const el = {
213
+ tag,
214
+ attributes,
215
+ children,
216
+ get domEl() { return domNode; }
449
217
  };
450
- addListener(input, eventname, listener,component);
451
- }
452
- const tryParse = (value) => {
453
- try {
454
- return JSON.parse(value);
455
- } catch (e) {
456
- return value;
218
+ const proxy = makeReactive(el);
219
+ domToElement.set(domNode, proxy);
220
+ return proxy;
221
+ };
222
+
223
+ const element = (tag, attributes = {}, children = []) => {
224
+ if (customTags[tag]) tag = customTags[tag];
225
+ // If tag is a function (component), call it and process the result
226
+ if (typeof tag === 'function') {
227
+ const result = tag({ ...attributes }, children);
228
+ return processComponentResult(result);
457
229
  }
458
- }
459
- const observed = () => {
460
- return {
461
- init({variable, component}) {
462
- const name = variable.name;
463
- variable.value = component.hasAttribute(name) ? coerce(component.getAttribute(name), variable.type) : variable.value;
464
- variable.observed = true;
465
- component.observedAttributes.add(variable.name);
466
- }
230
+
231
+ // Special handling for shadowDOM pseudo-element
232
+ if (tag === 'shadowDOM') {
233
+ return createShadowDOMMarker(attributes, children);
467
234
  }
468
- }
469
- const reactive = () => {
470
- return {
471
- init({variable, component}) {
472
- variable.reactive = true;
473
- component.vars[variable.name] = Reactor(variable);
474
- }
235
+
236
+ const isSvg = SVG_TAGS.has(tag.toLowerCase());
237
+ const domNode = isSvg
238
+ ? document.createElementNS('http://www.w3.org/2000/svg', tag)
239
+ : document.createElement(tag);
240
+ const proxy = wrapDomElement(domNode, tag, attributes, children);
241
+ proxy.attributes = attributes;
242
+ proxy.children = children;
243
+ return proxy;
244
+ };
245
+
246
+ // Process component function return value (HTML string, DOM node, vDOM, or Object DOM)
247
+ const processComponentResult = (result) => {
248
+ if (!result) return null;
249
+
250
+ // Already a Lightview element
251
+ if (result.domEl) return result;
252
+
253
+ // DOM node - wrap it
254
+ if (result instanceof HTMLElement) {
255
+ return wrapDomElement(result, result.tagName.toLowerCase(), {}, []);
475
256
  }
476
- }
477
- const shared = () => {
478
- return {
479
- init({variable, component}) {
480
- variable.shared = true;
481
- variable.set = function(newValue) {
482
- if(component.vars[this.name]?.shared) component.siblings.forEach((instance) => instance.setVariableValue(this.name, newValue));
483
- }
257
+
258
+ // HTML string - parse and wrap
259
+ if (typeof result === 'string') {
260
+ const template = document.createElement('template');
261
+ template.innerHTML = result.trim();
262
+ const content = template.content;
263
+ // If single element, return it; otherwise wrap in a fragment-like span
264
+ if (content.childNodes.length === 1 && content.firstChild instanceof HTMLElement) {
265
+ const el = content.firstChild;
266
+ return wrapDomElement(el, el.tagName.toLowerCase(), {}, []);
267
+ } else {
268
+ const wrapper = document.createElement('span');
269
+ wrapper.style.display = 'contents';
270
+ wrapper.appendChild(content);
271
+ return wrapDomElement(wrapper, 'span', {}, []);
484
272
  }
485
273
  }
486
- }
487
- const exported = () => {
488
- return {
489
- init({variable, component}) {
490
- const name = variable.name;
491
- variable.exported = true;
492
- variable.set = (newValue) => {
493
- if(variable.exported) {
494
- if(newValue==null) {
495
- removeComponentAttribute(component, name);
496
- } else {
497
- newValue = typeof (newValue) === "string" ? newValue : JSON.stringify(newValue);
498
- setComponentAttribute(component, name, newValue);
499
- }
500
- }
501
- }
502
- variable.set(variable.value);
503
- }
274
+
275
+ // vDOM object with tag property
276
+ if (typeof result === 'object' && result.tag) {
277
+ return element(result.tag, result.attributes || {}, result.children || []);
504
278
  }
505
- }
506
- const imported = () => {
507
- return {
508
- init({variable, component}) {
509
- const name = variable.name;
510
- variable.imported = true;
511
- variable.value = component.hasAttribute(name) ? coerce(component.getAttribute(name), variable.type) : variable.value;
279
+
280
+ // Object DOM syntax will be handled by processChild hook in lightview-x
281
+ // But we can do basic detection here
282
+ if (typeof result === 'object') {
283
+ const keys = Object.keys(result);
284
+ if (keys.length === 1 && typeof result[keys[0]] === 'object') {
285
+ const tag = keys[0];
286
+ const content = result[tag];
287
+ const { children, ...attributes } = content;
288
+ return element(tag, attributes, children || []);
512
289
  }
513
290
  }
514
- }
515
291
 
516
- let reserved = {
517
- observed: {
518
- constant: true,
519
- value: observed
520
- },
521
- reactive: {
522
- constant: true,
523
- value: reactive
524
- },
525
- shared: {
526
- constant: true,
527
- value: shared
528
- },
529
- exported: {
530
- constant: true,
531
- value: exported
532
- },
533
- imported: {
534
- constant: true,
535
- value: imported
536
- }
292
+ return null;
537
293
  };
538
- const createClass = (domElementNode, {observer, framed, href=window.location.href}) => {
539
- const instances = new Set(),
540
- observedAttributes = [];
541
- /*let dom = domElementNode.tagName === "TEMPLATE"
542
- ? domElementNode.content.cloneNode(true)
543
- : domElementNode.cloneNode(true);*/
544
- let dom;
545
- observedAttributes.add = function(name) { observedAttributes.includes(name) || observedAttributes.push(name); }
546
- const cls = class CustomElement extends HTMLElement {
547
- static get instances() {
548
- return instances;
549
- }
550
- static setTemplateNode(node) {
551
- dom = node.tagName === "TEMPLATE"
552
- ? document.createElement("div")
553
- : node.cloneNode(true);
554
- if(node.tagName === "TEMPLATE") {
555
- dom.innerHTML = node.innerHTML;
556
- [...node.attributes].forEach((attr) => dom.setAttribute(attr.name,attr.value));
557
- document.currentComponent = node;
558
- document.currentComponent.componentBaseURI = window.location.href;
559
- const lvscript = dom.querySelector("#lightview");
560
- if(lvscript) {
561
- const script = document.createElement("script");
562
- script.innerHTML = lvscript.innerHTML;
563
- document.head.appendChild(script);
564
- script.remove();
565
- }
566
- document.currentComponent = null;
294
+
295
+ const makeReactive = (el) => {
296
+ const domNode = el.domEl;
297
+
298
+ return new Proxy(el, {
299
+ set(target, prop, value) {
300
+ if (prop === 'attributes') {
301
+ target[prop] = makeReactiveAttributes(value, domNode);
302
+ } else if (prop === 'children') {
303
+ target[prop] = setupChildren(value, domNode);
304
+ } else {
305
+ target[prop] = value;
567
306
  }
568
- dom.mount = node.mount;
569
- }
570
- constructor() {
571
- super();
572
- this.componentBaseURI = href;
573
- instances.add(this);
574
- const currentComponent = document.currentComponent =this,
575
- shadow = this.attachShadow({mode: "open"}),
576
- eventlisteners = {};
577
- this.vars = {
578
- ...reserved,
579
- observe: {
580
- value: (...args) => observe(...args),
581
- type: "function",
582
- constant: true
583
- },
584
- addEventListener: {
585
- value: (eventName, listener) => {
586
- const listeners = eventlisteners[eventName] ||= new Set();
587
- [...listeners].forEach((f) => {
588
- if (listener + "" === f + "") listeners.delete(f);
589
- })
590
- eventlisteners[eventName].add(listener);
591
- },
592
- type: "function",
593
- constant: true
594
- },
595
- postEvent: {
596
- value: (eventName, event = {}) => {
597
- //event = {...event}
598
- event.type = eventName;
599
- event.target = currentComponent;
600
- eventlisteners[eventName]?.forEach((f) => f(event));
601
- },
602
- type: "function",
603
- constant: true
604
- },
605
- self: {value: currentComponent, type: CustomElement, constant: true}
606
- };
607
- this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
608
- if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported});
609
- ["getElementById", "querySelector", "querySelectorAll"]
610
- .forEach((fname) => {
611
- Object.defineProperty(this, fname, {
612
- configurable: true,
613
- writable: true,
614
- value: (...args) => this.shadowRoot[fname](...args)
615
- })
616
- });
617
- [...dom.childNodes].forEach((child) => {
618
- if(child.tagName && customElements.get(child.tagName.toLowerCase())) {
619
- const node = document.createElement(child.tagName);
620
- [...child.attributes].forEach((attr) => node.setAttribute(attr.name,attr.value));
621
- document.currentComponent = node;
622
- shadow.appendChild(node);
623
- } else {
624
- const node = child.cloneNode(true);
625
- shadow.appendChild(node);
626
- }
627
- });
628
- importAnchors(shadow, this);
307
+ return true;
629
308
  }
309
+ });
310
+ };
630
311
 
631
- get siblings() {
632
- return [...CustomElement.instances].filter((sibling) => sibling != this);
312
+ // Boolean attributes that should be present/absent rather than having a value
313
+ const BOOLEAN_ATTRIBUTES = new Set([
314
+ 'disabled', 'checked', 'readonly', 'required', 'hidden', 'autofocus',
315
+ 'autoplay', 'controls', 'loop', 'muted', 'default', 'defer', 'async',
316
+ 'novalidate', 'formnovalidate', 'open', 'selected', 'multiple', 'reversed',
317
+ 'ismap', 'nomodule', 'playsinline', 'allowfullscreen', 'inert'
318
+ ]);
319
+
320
+ // Set attribute with proper handling of boolean attributes and undefined/null values
321
+ const setAttributeValue = (domNode, key, value) => {
322
+ const isBooleanAttr = BOOLEAN_ATTRIBUTES.has(key.toLowerCase());
323
+
324
+ if (value === null || value === undefined) {
325
+ domNode.removeAttribute(key);
326
+ } else if (isBooleanAttr) {
327
+ if (value && value !== 'false') {
328
+ domNode.setAttribute(key, '');
329
+ } else {
330
+ domNode.removeAttribute(key);
633
331
  }
332
+ } else {
333
+ domNode.setAttribute(key, value);
334
+ }
335
+ };
634
336
 
635
- adoptedCallback() {
636
- if (this.hasOwnProperty("adoptedCallback")) this.adoptedCallback();
637
- }
337
+ const makeReactiveAttributes = (attributes, domNode) => {
338
+ const reactiveAttrs = {};
638
339
 
639
- disconnectedCallback() {
640
- instances.delete(this);
641
- }
340
+ for (let [key, value] of Object.entries(attributes)) {
341
+ if (key === 'onmount' || key === 'onunmount') {
342
+ const state = getOrSet(nodeState, domNode, nodeStateFactory);
343
+ state[key] = value;
642
344
 
643
- connectedCallback() {
644
- [...dom.attributes].forEach((attr) => {
645
- if(!this.hasAttribute(attr.name)) this.setAttribute(attr.name,attr.value);
646
- })
647
- if(dom.mount) {
648
- const script = document.createElement("script");
649
- document.currentComponent = this;
650
- script.innerHTML = `with(document.currentComponent.varsProxy) {
651
- const component = document.currentComponent;
652
- (async () => { await (${dom.mount}).call(self,self);
653
- component.compile(); })();
654
- };`;
655
- this.appendChild(script);
656
- script.remove();
657
- document.currentComponent = null;
345
+ if (key === 'onmount' && domNode.isConnected) {
346
+ value(domNode);
658
347
  }
659
- }
660
- compile() {
661
- // Promise.all(promises).then(() => {
662
- const ctx = this,
663
- shadow = this.shadowRoot;
664
- const nodes = getNodes(ctx),
665
- processNodes = (nodes,object) => {
666
- nodes.forEach((node) => {
667
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
668
- observe(() => resolveNodeOrText(node, this,true,node.extras));
669
- if(node.parentElement?.tagName==="TEXTAREA") {
670
- const name = getTemplateVariableName(node.template);
671
- if (name) {
672
- const nameparts = name.split(".");
673
- if(node.extras && node.extras[nameparts[0]]) {
674
- object = node.extras[nameparts[0]];
675
- }
676
- if(!this.vars[nameparts[0]] || this.vars[nameparts[0]].reactive || object) {
677
- bindInput(node.parentElement, name, this, resolveNodeOrText(node.template, this,true,node.extras), object);
678
- }
679
- }
680
- }
681
- } else if (node.nodeType === Node.ELEMENT_NODE) {
682
- // resolve the value before all else;
683
- const attr = node.attributes.value,
684
- template = attr?.template;
685
- if (attr && template) {
686
- //let value = resolveNodeOrText(attr, this),
687
- // ;
688
- const eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx, false,node.extras) : null,
689
- template = attr.template;
690
- if (template) {
691
- const name = getTemplateVariableName(template);
692
- if (name) {
693
- const nameparts = name.split(".");
694
- if(node.extras && node.extras[nameparts[0]]) {
695
- object = node.extras[nameparts[0]];
696
- }
697
- if(!this.vars[nameparts[0]] || this.vars[nameparts[0]].reactive || object) {
698
- bindInput(node, name, this, resolveNodeOrText(attr, this,false,node.extras), object);
699
- }
700
- }
701
- observe(() => {
702
- const value = resolveNodeOrText(template, ctx,false,node.extras);
703
- if(value!==undefined) {
704
- if (eltype === "checkbox") {
705
- if (coerce(value, "boolean") === true) {
706
- node.setAttribute("checked", "");
707
- node.checked = true;
708
- } else {
709
- node.removeAttribute("checked");
710
- node.checked = false;
711
- }
712
- } else if (node.tagName === "SELECT") {
713
- let values = [value];
714
- if (node.hasAttribute("multiple")) values = coerce(value, Array);
715
- [...node.querySelectorAll("option")].forEach(async (option) => {
716
- if (option.hasAttribute("value")) {
717
- if (values.includes(resolveNodeOrText(option.attributes.value, ctx,false,node.extras))) {
718
- option.setAttribute("selected", "");
719
- option.selected = true;
720
- }
721
- } else if (values.includes(resolveNodeOrText(option.innerText, ctx,false,node.extras))) {
722
- option.setAttribute("selected", "");
723
- option.selected = true;
724
- }
725
- })
726
- } else if (eltype!=="radio") {
727
- //attr.value = typeof(value)==="string" ? value : JSON.stringify(value);
728
- let avalue = typeof(value)==="string" ? value : value.toString ? value.toString() : JSON.stringify(value);
729
- if(avalue.startsWith('"')) avalue = avalue.substring(1);
730
- if(avalue.endsWith('"')) avalue = avalue.substring(0,avalue.length-1);
731
- attr.value = avalue;
732
- }
733
- }
734
- });
735
- }
736
- }
737
- [...node.attributes].forEach(async (attr) => {
738
- if (attr.name === "value" && attr.template) return;
739
- const {name, value} = attr,
740
- vname = node.attributes.name?.value;
741
- if(value.includes("${")) attr.template = value;
742
- if (name === "type" && value=="radio" && vname) {
743
- bindInput(node, vname, this, undefined, object);
744
- observe(() => {
745
- const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
746
- if (node.attributes.value.value == varvalue) {
747
- node.setAttribute("checked", "");
748
- node.checked = true;
749
- } else {
750
- node.removeAttribute("checked");
751
- node.checked = false;
752
- }
753
- });
754
- }
755
-
756
- const [type, ...params] = name.split(":");
757
- if (type === "") { // name is :something
758
- observe(() => {
759
- const value = attr.value;
760
- if (params[0]) {
761
- if (value === "true") node.setAttribute(params[0], "")
762
- else node.removeAttribute(params[0]);
763
- } else {
764
- const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx,false,node.extras) : null,
765
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx,false,node.extras) : null;
766
- if (eltype === "checkbox" || node.tagName === "OPTION") {
767
- if (elvalue === true) node.setAttribute("checked", "")
768
- else node.removeAttribute("checked");
769
- }
770
- }
771
- })
772
- } else if (type === "l-on") {
773
- let listener;
774
- observe(() => {
775
- const value = resolveNodeOrText(attr, this,true,node.extras);
776
- if (listener) node.removeEventListener(params[0], listener);
777
- listener = null;
778
- if(typeof(value)==="function") {
779
- listener = value;
780
- } else {
781
- try {
782
- listener = Function("return " + value)();
783
- } catch(e) {
784
-
785
- }
786
- }
787
- if(listener) addListener(node, params[0], listener,ctx);
788
- })
789
- } else if (type === "l-if") {
790
- observe(() => {
791
- const value = resolveNodeOrText(attr, this,true,node.extras);
792
- node.style.setProperty("display", value == true || value === "true" ? "revert" : "none");
793
- })
794
- } else if (type === "l-for") {
795
- node.template ||= node.innerHTML;
796
- node.clone ||= node.cloneNode(true);
797
- observe(() => {
798
- const [what = "each", vname = "item", index = "index", array = "array", after = false] = params,
799
- value = resolveNodeOrText(attr, this,false,node.extras),
800
- coerced = coerce(value, what === "each" ? Array : "object"),
801
- target = what === "each" ? coerced : Object[what](coerced),
802
- children = target.reduce((children,item,i,target) => {
803
- const clone = node.clone.cloneNode(true),
804
- extras = node.extras = {
805
- [vname]: item,
806
- [index]: i,
807
- [array]: target
808
- },
809
- nodes = [...getNodes(clone)].map((node) => {
810
- node.extras = extras;
811
- return node;
812
- });
813
- processNodes(nodes);
814
- children.push(...clone.childNodes);
815
- return children;
816
- },[]);
817
- if (!window.lightviewDebug) {
818
- if (after) {
819
- node.style.setProperty("display", "none")
820
- } else {
821
- while (node.lastElementChild) node.lastElementChild.remove();
822
- }
823
- }
824
- //const nodes = getNodes(parsed.body);
825
- children.forEach((child) => {
826
- //while (parsed.body.firstChild) {
827
- //const child = parsed.body.firstChild;
828
- if (after) node.parentElement.insertBefore(child, node);
829
- else node.appendChild(child);
830
- })
831
- //processNodes(nodes);
832
- })
833
- } else if(attr.template) {
834
- observe(() => {
835
- resolveNodeOrText(attr, this,false,node.extras);
836
- })
837
- }
838
- })
839
- }
840
- })
841
- };
842
- nodes.forEach((node) => {
843
- if(node.tagName==="FORM") {
844
- const value = node.getAttribute("value"),
845
- name = getTemplateVariableName(value);
846
- if(name) {
847
- const childnodes = [...nodes].filter((childnode) => node!==childnode && node.contains(childnode));
848
- childnodes.forEach((node) => nodes.delete(node));
849
- const variable = ctx.vars[name] ||= {type: "object", reactive:true, value: Reactor({})};
850
- if(variable.type !== "object" || !variable.reactive || !variable.value || typeof(variable.value)!=="object") {
851
- throw new TypeError(`Can't bind form ${node.getAttribute("id")} to non-object variable ${name}`);
852
- }
853
- processNodes(childnodes,variable.value);
854
- }
348
+ } else if (key.startsWith('on')) {
349
+ // Event handler
350
+ if (typeof value === 'function') {
351
+ // Function handler - use addEventListener
352
+ const eventName = key.slice(2).toLowerCase();
353
+ domNode.addEventListener(eventName, value);
354
+ } else if (typeof value === 'string') {
355
+ // String handler (from parsed HTML) - use setAttribute
356
+ // Browser will compile the string into a handler function
357
+ domNode.setAttribute(key, value);
358
+ }
359
+ reactiveAttrs[key] = value;
360
+ } else if (typeof value === 'function') {
361
+ // Reactive binding
362
+ const runner = effect(() => {
363
+ const result = value();
364
+ if (key === 'style' && typeof result === 'object') {
365
+ Object.assign(domNode.style, result);
366
+ } else {
367
+ setAttributeValue(domNode, key, result);
855
368
  }
856
- })
857
- processNodes(nodes);
858
- shadow.normalize();
859
- observer ||= createObserver(ctx, framed);
860
- observer.observe(ctx, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
861
- if(this.hasAttribute("l-unhide")) this.removeAttribute("hidden");
862
- //ctx.vars.postEvent.value("connected");
863
- this.dispatchEvent(new Event("mounted"));
864
- // })
865
- }
866
- adoptedCallback(callback) {
867
- this.dispatchEvent(new Event("adopted"));
868
- }
869
- disconnectedCallback() {
870
- this.dispatchEvent(new Event("disconnected"));
871
- }
872
- get observedAttributes() {
873
- return CustomElement.observedAttributes;
874
- }
875
- static get observedAttributes() {
876
- return observedAttributes;
369
+ });
370
+ trackEffect(domNode, runner);
371
+ reactiveAttrs[key] = value;
372
+ } else if (key === 'style' && typeof value === 'object') {
373
+ // Handle style object which may contain reactive values
374
+ Object.entries(value).forEach(([styleKey, styleValue]) => {
375
+ if (typeof styleValue === 'function') {
376
+ const runner = effect(() => {
377
+ domNode.style[styleKey] = styleValue();
378
+ });
379
+ trackEffect(domNode, runner);
380
+ } else {
381
+ domNode.style[styleKey] = styleValue;
382
+ }
383
+ });
384
+ reactiveAttrs[key] = value;
385
+ } else {
386
+ // Static attribute - handle undefined/null/boolean properly
387
+ setAttributeValue(domNode, key, value);
388
+ reactiveAttrs[key] = value;
877
389
  }
390
+ }
878
391
 
879
- getVariableNames() {
880
- return Object.keys(this.vars)
881
- .filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent","observe"].includes(name))
392
+ return reactiveAttrs;
393
+ };
394
+
395
+ /**
396
+ * Core child processing logic - shared between setupChildren and setupChildrenInTarget
397
+ * @param {Array} children - Children to process
398
+ * @param {HTMLElement|ShadowRoot} targetNode - Where to append children
399
+ * @param {boolean} clearExisting - Whether to clear existing content
400
+ * @returns {Array} - Processed child elements
401
+ */
402
+ const processChildren = (children, targetNode, clearExisting = true) => {
403
+ if (clearExisting && targetNode.innerHTML !== undefined) {
404
+ targetNode.innerHTML = ''; // Clear existing
405
+ }
406
+ const childElements = [];
407
+
408
+ // Check if we're processing children of script or style elements
409
+ // These need raw text content preserved, not reactive transformations
410
+ const isSpecialElement = targetNode.tagName &&
411
+ (targetNode.tagName.toLowerCase() === 'script' || targetNode.tagName.toLowerCase() === 'style');
412
+
413
+ for (let child of children) {
414
+ // Allow extensions to transform children (e.g., template literals)
415
+ // BUT skip for script/style elements which need raw content
416
+ if (Lightview.hooks.processChild && !isSpecialElement) {
417
+ child = Lightview.hooks.processChild(child) ?? child;
882
418
  }
883
419
 
884
- getVariable(name) {
885
- return this.vars[name] ? {...this.vars[name]} : undefined;
420
+ // Handle nested arrays (flattening)
421
+ if (Array.isArray(child)) {
422
+ childElements.push(...processChildren(child, targetNode, false));
423
+ continue;
886
424
  }
887
425
 
888
- setVariableValue(variableName, value, {coerceTo = typeof (value)} = {}) {
889
- if (!this.isConnected) {
890
- instances.delete(this);
891
- return false;
426
+ // Handle shadowDOM markers - attach shadow to parent and process shadow children
427
+ if (isShadowDOMMarker(child)) {
428
+ // targetNode is the parent element that should get the shadow root
429
+ // For ShadowRoot targets, we can't attach another shadow, so warn
430
+ if (targetNode instanceof ShadowRoot) {
431
+ console.warn('Lightview: Cannot nest shadowDOM inside another shadowDOM');
432
+ continue;
892
433
  }
893
- let {type} = this.vars[variableName] || {};
894
- if (type) {
895
- if (this.varsProxy[variableName] !== value) {
896
- const variable = this.vars[variableName];
897
- if (variable.shared) {
898
- value = type.validate ? type.validate(value,variable) : coerce(value,coerceTo);
899
- const event = new VariableEvent({
900
- variableName: variableName,
901
- value: value,
902
- oldValue: variable.value
903
- });
904
- variable.value = value;
905
- this.vars.postEvent.value("change", event);
906
- if (event.defaultPrevented) variable.value = value;
907
- } else {
908
- this.varsProxy[variableName] = value;
434
+ processShadowDOM(child, targetNode);
435
+ continue;
436
+ }
437
+
438
+ const type = typeof child;
439
+ if (type === 'function') {
440
+ const result = child();
441
+ // Determine if the result implies complex content (DOM/vDOM/Array)
442
+ // Treat as complex if it's an object (including arrays) but not null
443
+ const isComplex = result && (typeof result === 'object' || Array.isArray(result));
444
+
445
+ if (isComplex) {
446
+ // Reactive element, vDOM object, or list of items
447
+ // Use a stable wrapper div to hold the reactive content
448
+ const wrapper = document.createElement('span');
449
+ wrapper.style.display = 'contents';
450
+ targetNode.appendChild(wrapper);
451
+
452
+ let runner;
453
+ const update = () => {
454
+ const val = child();
455
+ // Check if wrapper is still in the DOM (skip check on first run)
456
+ if (runner && !wrapper.parentNode) {
457
+ runner.stop();
458
+ return;
909
459
  }
910
- }
911
- return true;
912
- }
913
- this.vars[variableName] = {name, type: coerceTo, value: coerce(value, coerceTo)};
914
- return false;
915
- }
916
-
917
- getVariableValue(variableName) {
918
- return this.vars[variableName]?.value;
919
- }
460
+ const childrenToProcess = Array.isArray(val) ? val : [val];
461
+ // processChildren handles clearing existing content via 3rd arg=true
462
+ processChildren(childrenToProcess, wrapper, true);
463
+ };
920
464
 
921
- variables(variables, {remote, constant,set,...rest} = {}) { // options = {observed,reactive,shared,exported,imported}
922
- const options = {remote, constant,...rest},
923
- addEventListener = this.varsProxy.addEventListener;
924
- if (variables !== undefined) {
925
- Object.entries(variables)
926
- .forEach(([key, type]) => {
927
- if(isArrowFunction(type)) type = type();
928
- const variable = this.vars[key] ||= {name: key, type};
929
- if(set!==undefined && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't be set to ${set}`);
930
- variable.value = set;
931
- if(constant!==undefined) {
932
- if(remote || imported) throw new TypeError(`${key} can't be a constant and also remote or imported`)
933
- variable.constant = true;
934
- variable.value = constant;
935
- }
936
- if (remote) {
937
- if(typeof(remote)==="function") remote = remote(`./${key}`);
938
- variable.remote = remote;
939
- remote.handleRemote({variable, config:remote.config,component:this});
940
- }
941
- // todo: handle custom functional types, remote should actually be handled this way
942
- Object.entries(rest).forEach(([type,f]) => {
943
- const functionalType = variable[type] = typeof(f)==="function" ? f() : f;
944
- if(functionalType.init) functionalType.init({variable,options,component:this});
945
- if((rest.get!==undefined || rest.set!==undefined) && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't have a getter or setter`);
946
- variable.set != functionalType.set;
947
- variable.get != functionalType.get;
948
- });
949
- if(type.validate && variable.value!==undefined) type.validate(variable.value,variable);
950
- });
465
+ runner = effect(update);
466
+ trackEffect(wrapper, runner);
467
+ childElements.push(child);
468
+ } else {
469
+ // Reactive text node for primitives
470
+ const textNode = document.createTextNode('');
471
+ targetNode.appendChild(textNode);
472
+ const runner = effect(() => {
473
+ const val = child();
474
+ textNode.textContent = val !== undefined ? val : '';
475
+ });
476
+ trackEffect(textNode, runner);
477
+ childElements.push(child);
951
478
  }
952
- return Object.entries(this.vars)
953
- .reduce((result, [key, variable]) => {
954
- result[key] = {...variable};
955
- return result;
956
- }, {});
479
+ } else if (['string', 'number', 'boolean', 'symbol'].includes(type)) {
480
+ // Static text
481
+ targetNode.appendChild(document.createTextNode(child));
482
+ childElements.push(child);
483
+ } else if (child && type === 'object' && child.tag) {
484
+ // Child element (already wrapped or plain object) - tag can be string or function
485
+ const childEl = child.domEl ? child : element(child.tag, child.attributes || {}, child.children || []);
486
+ targetNode.appendChild(childEl.domEl);
487
+ childElements.push(childEl);
957
488
  }
958
489
  }
959
- cls.setTemplateNode(domElementNode);
960
- return cls;
961
- }
962
490
 
963
- const createComponent = (name, node, {framed, observer, href} = {}) => {
964
- let ctor = customElements.get(name);
965
- if (ctor) {
966
- if (framed && !ctor.lightviewFramed) ctor.lightviewFramed = true;
967
- else console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
968
- return ctor;
969
- }
970
- ctor = createClass(node, {observer, framed, href});
971
- customElements.define(name, ctor);
972
- Lightview.customElements.set(name, ctor);
973
- return ctor;
974
- }
975
- Lightview.customElements = new Map();
976
- Lightview.createComponent = createComponent;
977
- const importLink = async (link, observer) => {
978
- const url = (new URL(link.getAttribute("href"), window.location.href)),
979
- as = link.getAttribute("as") || getNameFromPath(url.pathname);
980
- if (url.hostname !== window.location.hostname && !link.getAttribute("crossorigin")) {
981
- throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname} unless 'crossorigin' attribute is set.`)
491
+ return childElements;
492
+ };
493
+
494
+ /**
495
+ * Setup children in a target node (for shadow roots and other targets)
496
+ * Does not clear existing content
497
+ */
498
+ const setupChildrenInTarget = (children, targetNode) => {
499
+ return processChildren(children, targetNode, false);
500
+ };
501
+
502
+ /**
503
+ * Setup children on a DOM node, clearing existing content
504
+ */
505
+ const setupChildren = (children, domNode) => {
506
+ return processChildren(children, domNode, true);
507
+ };
508
+
509
+ // ============= EXPORTS =============
510
+ const enhance = (selectorOrNode, options = {}) => {
511
+ const domNode = typeof selectorOrNode === 'string'
512
+ ? document.querySelector(selectorOrNode)
513
+ : selectorOrNode;
514
+
515
+ // If it's already a Lightview element, use its domEl
516
+ const node = domNode.domEl || domNode;
517
+ if (!(node instanceof HTMLElement)) return null;
518
+
519
+ const tagName = node.tagName.toLowerCase();
520
+ let el = domToElement.get(node);
521
+
522
+ if (!el) {
523
+ el = wrapDomElement(node, tagName);
982
524
  }
983
- if (!customElements.get(as)) {
984
- const html = await (await fetch(url.href)).text(),
985
- dom = parser.parseFromString(html, "text/html"),
986
- unhide = !!dom.head.querySelector('meta[name="l-unhide"]'),
987
- modulelinks = dom.head.querySelectorAll('link[href$=".html"][rel=module]');
988
- for (const childlink of modulelinks) {
989
- const href = childlink.getAttribute("href"),
990
- childurl = new URL(href, url.href);
991
- childlink.setAttribute("href", childurl.href);
992
- if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
993
- await importLink(childlink, observer);
994
- }
995
- const links = dom.head.querySelectorAll('link:not([href$=".html"][rel=module])');
996
- for(const childlink of links) {
997
- const href = childlink.getAttribute("href"),
998
- childurl = new URL(href, url.href);
999
- childlink.setAttribute("href", childurl.href);
1000
- if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"));
1001
- dom.body.insertBefore(childlink,dom.body.firstChild);
1002
- }
1003
- document.currentComponent = dom.body;
1004
- document.currentComponent.componentBaseURI = url.href;
1005
- const lvscript = dom.getElementById("lightview");
1006
- if(lvscript) {
1007
- const script = document.createElement("script");
1008
- script.innerHTML = lvscript.innerHTML;
1009
- document.body.appendChild(script);
1010
- script.remove();
525
+
526
+ const { innerText, innerHTML, ...attrs } = options;
527
+
528
+ if (innerText !== undefined) {
529
+ if (typeof innerText === 'function') {
530
+ effect(() => { node.innerText = innerText(); });
531
+ } else {
532
+ node.innerText = innerText;
1011
533
  }
1012
- document.currentComponent = null;
1013
- createComponent(as, dom.body, {observer,href:url.href});
1014
- if (unhide) dom.body.removeAttribute("hidden");
1015
- }
1016
- return {as};
1017
- }
1018
- const importLinks = async () => {
1019
- const observer = createObserver(document.body);
1020
- for (const link of [...document.querySelectorAll(`link[href$=".html"][rel=module]`)]) {
1021
- await importLink(link, observer);
1022
534
  }
1023
- }
1024
535
 
1025
- const bodyAsComponent = ({as = "x-body", unhide, framed} = {}) => {
1026
- const parent = document.body.parentElement;
1027
- createComponent(as, document.body, {framed});
1028
- const component = document.createElement(as);
1029
- parent.replaceChild(component, document.body);
1030
- Object.defineProperty(document, "body", {
1031
- enumerable: true, configurable: true, get() {
1032
- return component;
1033
- }
1034
- });
1035
- if (unhide) component.removeAttribute("hidden");
1036
- }
1037
- Lightview.bodyAsComponent = bodyAsComponent;
1038
- const postMessage = (data, target = window.parent) => {
1039
- if (postMessage.enabled) {
1040
- if (target instanceof HTMLIFrameElement) {
1041
- data = {...data, href: window.location.href};
1042
- target.contentWindow.postMessage(JSON.stringify(data), "*");
536
+ if (innerHTML !== undefined) {
537
+ if (typeof innerHTML === 'function') {
538
+ effect(() => { node.innerHTML = innerHTML(); });
1043
539
  } else {
1044
- data = {...data, iframeId: document.lightviewId, href: window.location.href};
1045
- target.postMessage(JSON.stringify(data), "*");
540
+ node.innerHTML = innerHTML;
1046
541
  }
1047
542
  }
1048
- }
1049
- const setComponentAttribute = (node, name, value) => {
1050
- if (node.getAttribute(name) !== value) node.setAttribute(name, value);
1051
- postMessage({type: "setAttribute", argsList: [name, value]});
1052
- }
1053
- const removeComponentAttribute = (node, name, value) => {
1054
- node.removeAttribute(name);
1055
- postMessage({type: "removeAttribute", argsList: [name]});
1056
- }
1057
- const getNodePath = (node, path = []) => {
1058
- path.unshift(node);
1059
- if (node.parentNode && node.parentNode !== node.parentNode) getNodePath(node.parentNode, path);
1060
- return path;
1061
- }
1062
- const onresize = (node, callback) => {
1063
- const resizeObserver = new ResizeObserver(() => callback());
1064
- resizeObserver.observe(node);
543
+
544
+ if (Object.keys(attrs).length > 0) {
545
+ // Merge with existing attributes or simply set them triggers the proxy
546
+ el.attributes = attrs;
547
+ }
548
+
549
+ return el;
1065
550
  };
1066
551
 
1067
- const url = new URL(document.currentScript.getAttribute("src"), window.location.href);
1068
- let domContentLoadedEvent;
1069
- if (!domContentLoadedEvent) addListener(window, "DOMContentLoaded", (event) => domContentLoadedEvent = event);
1070
- let OBSERVER;
1071
- const loader = async (whenFramed) => {
1072
- await importLinks();
1073
- const unhide = !!document.querySelector('meta[name="l-unhide"]'),
1074
- isolated = !!document.querySelector('meta[name="l-isolate"]'),
1075
- enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
1076
- if (whenFramed) {
1077
- whenFramed({unhide, isolated, enableFrames, framed: true});
1078
- if (!isolated) {
1079
- postMessage.enabled = true;
1080
- addListener(window, "message", ({data}) => {
1081
- const {type, argsList} = JSON.parse(data);
1082
- if (type === "framed") {
1083
- const resize = () => {
1084
- const {width, height} = document.body.getBoundingClientRect();
1085
- postMessage({type: "setAttribute", argsList: ["width", width]})
1086
- postMessage({type: "setAttribute", argsList: ["height", height + 20]});
1087
- }
1088
- resize();
1089
- onresize(document.body, () => resize());
1090
- return
552
+ const $ = (cssSelectorOrElement, startingDomEl = document.body) => {
553
+ const el = typeof cssSelectorOrElement === 'string' ? startingDomEl.querySelector(cssSelectorOrElement) : cssSelectorOrElement;
554
+ if (!el) return null;
555
+ Object.defineProperty(el, 'content', {
556
+ value(child, location = 'inner') {
557
+ location = location.toLowerCase();
558
+ const tags = Lightview.tags;
559
+
560
+ // Check if target element is script or style
561
+ const isSpecialElement = el.tagName &&
562
+ (el.tagName.toLowerCase() === 'script' || el.tagName.toLowerCase() === 'style');
563
+
564
+ const array = (Array.isArray(child) ? child : [child]).map(item => {
565
+ // Allow extensions to transform children (e.g., Object DOM syntax)
566
+ // BUT skip for script/style elements which need raw content
567
+ if (Lightview.hooks.processChild && !isSpecialElement) {
568
+ item = Lightview.hooks.processChild(item) ?? item;
1091
569
  }
1092
- if (type === "setAttribute") {
1093
- const [name, value] = [...argsList],
1094
- variable = document.body.vars[name];
1095
- if (variable && variable.imported) document.body.setVariableValue(name, value);
1096
- return;
570
+ if (item.tag && !item.domEl) {
571
+ return element(item.tag, item.attributes || {}, item.children || []).domEl;
572
+ } else {
573
+ return item.domEl || item;
1097
574
  }
1098
- if (type === "removeAttribute") {
1099
- const [name] = argsList[0],
1100
- variable = document.body.vars[name];
1101
- if (variable && variable.imported) document.body.setVariableValue(name, undefined);
575
+ });
1102
576
 
577
+ if (location === 'shadow') {
578
+ let shadow = el.shadowRoot;
579
+ if (!shadow) {
580
+ shadow = el.attachShadow({ mode: 'open' });
1103
581
  }
582
+ shadow.innerHTML = '';
583
+ array.forEach(item => {
584
+ shadow.appendChild(item);
585
+ });
586
+ return el;
587
+ }
588
+
589
+ if (location === 'inner') {
590
+ el.innerHTML = '';
591
+ array.forEach(item => {
592
+ el.appendChild(item);
593
+ });
594
+ return el;
595
+ }
596
+
597
+ if (location === 'outer') {
598
+ el.replaceWith(...array);
599
+ return el;
600
+ }
601
+
602
+ if (location === 'afterbegin' || location === 'afterend') {
603
+ array.reverse();
604
+ }
605
+ array.forEach(item => {
606
+ el.insertAdjacentElement(location, item);
1104
607
  });
1105
- const url = new URL(window.location.href);
1106
- document.lightviewId = url.searchParams.get("id");
1107
- postMessage({type: "DOMContentLoaded"})
608
+ return el;
609
+ },
610
+ configurable: true,
611
+ writable: true
612
+ });
613
+ return el;
614
+ };
615
+
616
+ const customTags = {}
617
+ const tags = new Proxy({}, {
618
+ get(_, tag) {
619
+ if (tag === "_customTags") return { ...customTags };
620
+
621
+ const wrapper = (...args) => {
622
+ let attributes = {};
623
+ let children = args;
624
+ const arg0 = args[0];
625
+ if (args.length > 0 && arg0 && typeof arg0 === 'object' && !arg0.tag && !arg0.domEl && !Array.isArray(arg0)) {
626
+ attributes = arg0;
627
+ children = args.slice(1);
628
+ }
629
+ return element(customTags[tag] || tag, attributes, children.flat());
630
+ };
631
+
632
+ // Lift static methods/properties from the component onto the wrapper
633
+ // This allows patterns like Card.Figure to work when Card is retrieved from tags
634
+ if (customTags[tag]) {
635
+ Object.assign(wrapper, customTags[tag]);
1108
636
  }
1109
- } else if (url.searchParams.has("as")) {
1110
- bodyAsComponent({as: url.searchParams.get("as"), unhide});
637
+
638
+ return wrapper;
639
+ },
640
+ set(_, tag, value) {
641
+ customTags[tag] = value;
642
+ return true;
1111
643
  }
1112
- if (enableFrames) {
1113
- postMessage.enabled = true;
1114
- addListener(window, "message", (message) => {
1115
- const {type, iframeId, argsList, href} = JSON.parse(message.data),
1116
- iframe = document.getElementById(iframeId);
1117
- if (iframe) {
1118
- if (type === "DOMContentLoaded") {
1119
- postMessage({type: "framed", href: window.location.href}, iframe);
1120
- Object.defineProperty(domContentLoadedEvent, "currentTarget", {
1121
- enumerable: false,
1122
- configurable: true,
1123
- value: iframe
1124
- });
1125
- domContentLoadedEvent.href = href;
1126
- domContentLoadedEvent.srcElement = iframe;
1127
- domContentLoadedEvent.bubbles = false;
1128
- domContentLoadedEvent.path = getNodePath(iframe);
1129
- Object.defineProperty(domContentLoadedEvent, "timeStamp", {
1130
- enumerable: false,
1131
- configurable: true,
1132
- value: performance.now()
1133
- })
1134
- iframe.dispatchEvent(domContentLoadedEvent);
1135
- return;
1136
- }
1137
- if (type === "setAttribute") {
1138
- const [name, value] = [...argsList];
1139
- if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
1140
- return;
1141
- }
1142
- if (type === "removeAttribute") {
1143
- iframe.removeAttribute(...argsList);
1144
- return;
644
+ });
645
+
646
+ const Lightview = {
647
+ signal,
648
+ computed,
649
+ effect,
650
+ element, // do not document this
651
+ enhance,
652
+ tags,
653
+ $,
654
+ // Extension hooks
655
+ hooks: {
656
+ onNonStandardHref: null,
657
+ processChild: null
658
+ },
659
+ // Internals exposed for extensions
660
+ internals: {
661
+ domToElement,
662
+ wrapDomElement,
663
+ setupChildren
664
+ }
665
+ };
666
+
667
+ // Export for use
668
+ if (typeof module !== 'undefined' && module.exports) {
669
+ module.exports = Lightview;
670
+ }
671
+ if (typeof window !== 'undefined') {
672
+ window.Lightview = Lightview;
673
+
674
+ // Global click handler delegates to hook if registered
675
+ window.addEventListener('click', (e) => {
676
+ // Support fragment navigation piercing Shadow DOM
677
+ // Use composedPath() to find the actual clicked element, even inside shadow roots
678
+ const path = e.composedPath();
679
+ const link = path.find(el => el.tagName === 'A' && el.getAttribute?.('href')?.startsWith('#'));
680
+
681
+ if (link && !e.defaultPrevented) {
682
+ const href = link.getAttribute('href');
683
+ if (href.length > 1) {
684
+ const id = href.slice(1);
685
+ const root = link.getRootNode();
686
+ const target = (root.getElementById ? root.getElementById(id) : null) ||
687
+ (root.querySelector ? root.querySelector(`#${id}`) : null);
688
+
689
+ if (target) {
690
+ e.preventDefault();
691
+ target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
1145
692
  }
1146
693
  }
1147
- console.warn("iframe posted a message without providing an id", message);
694
+ }
695
+
696
+ if (Lightview.hooks.onNonStandardHref) {
697
+ Lightview.hooks.onNonStandardHref(e);
698
+ }
699
+ });
700
+
701
+ // Automatic Cleanup & Lifecycle Hooks
702
+ const walkNodes = (node, fn) => {
703
+ fn(node);
704
+ node.childNodes?.forEach(n => walkNodes(n, fn));
705
+ if (node.shadowRoot) walkNodes(node.shadowRoot, fn);
706
+ };
707
+
708
+ const cleanupNode = (node) => walkNodes(node, n => {
709
+ const s = nodeState.get(n);
710
+ if (s) {
711
+ s.effects?.forEach(e => e.stop());
712
+ s.onunmount?.(n);
713
+ nodeState.delete(n);
714
+ }
715
+ });
716
+
717
+ const mountNode = (node) => walkNodes(node, n => {
718
+ nodeState.get(n)?.onmount?.(n);
719
+ });
720
+
721
+ const observer = new MutationObserver((mutations) => {
722
+ mutations.forEach((mutation) => {
723
+ mutation.removedNodes.forEach(cleanupNode);
724
+ mutation.addedNodes.forEach(mountNode);
1148
725
  });
1149
- if (!OBSERVER) {
1150
- const mutationCallback = (mutationsList) => {
1151
- const console = document.getElementById("console");
1152
- for (const {target, attributeName, oldValue} of mutationsList) {
1153
- const value = target.getAttribute(attributeName);
1154
- if (!["height", "width", "message"].includes(attributeName)) {
1155
- if (!value) postMessage({type: "removeAttribute", argsList: [attributeName]}, iframe)
1156
- else if (value !== oldValue) {
1157
- postMessage({
1158
- type: "setAttribute",
1159
- argsList: [attributeName, value]
1160
- }, iframe)
1161
- }
1162
- }
1163
- if (attributeName === "message") {
1164
- if (value) {
1165
- target.removeAttribute("message");
1166
- target.dispatchEvent(new CustomEvent("message", {target, detail: JSON.parse(value)}))
1167
- }
1168
- } else {
1169
- target.dispatchEvent(new CustomEvent("attribute.changed", {
1170
- target,
1171
- detail: {attributeName, value, oldValue}
1172
- }))
1173
- }
1174
- }
1175
- };
1176
- const observer = OBSERVER = new MutationObserver(mutationCallback),
1177
- iframe = document.getElementById("myframe");
1178
- observer.observe(iframe, {attributes: true, attributeOldValue: true});
726
+ });
727
+
728
+ // Wait for DOM to be ready before observing
729
+ const startObserving = () => {
730
+ if (document.body) {
731
+ observer.observe(document.body, {
732
+ childList: true,
733
+ subtree: true
734
+ });
1179
735
  }
1180
- }
1181
- }
1182
- const whenFramed = (callback, {isolated} = {}) => {
1183
- // loads for framed content
1184
- addListener(document, "DOMContentLoaded", (event) => loader(callback));
1185
- }
1186
- Lightview.whenFramed = whenFramed;
736
+ };
1187
737
 
1188
- if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
1189
- // loads for unframed content
1190
- // CodePen mucks with window.parent
1191
- addListener(document, "DOMContentLoaded", () => loader())
738
+ if (document.readyState === 'loading') {
739
+ document.addEventListener('DOMContentLoaded', startObserving);
740
+ } else {
741
+ startObserving();
742
+ }
1192
743
  }
1193
-
1194
- return {observe}
1195
- })();
744
+ })();