lightview 1.8.2 → 2.0.1

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 (262) hide show
  1. package/.codacy/cli.sh +149 -0
  2. package/.codacy/codacy.yaml +15 -0
  3. package/.github/instructions/codacy.instructions.md +72 -0
  4. package/.wranglerignore +21 -0
  5. package/README.md +1330 -19
  6. package/_headers +4 -0
  7. package/build.js +70 -0
  8. package/components/actions/button.js +151 -0
  9. package/components/actions/dropdown.js +120 -0
  10. package/components/actions/modal.js +146 -0
  11. package/components/actions/swap.js +118 -0
  12. package/components/daisyui.js +288 -0
  13. package/components/data-display/accordion.js +128 -0
  14. package/components/data-display/alert.js +112 -0
  15. package/components/data-display/avatar.js +170 -0
  16. package/components/data-display/badge.js +82 -0
  17. package/components/data-display/card.js +151 -0
  18. package/components/data-display/carousel.js +94 -0
  19. package/components/data-display/chart.js +220 -0
  20. package/components/data-display/chat.js +128 -0
  21. package/components/data-display/collapse.js +103 -0
  22. package/components/data-display/countdown.js +69 -0
  23. package/components/data-display/diff.js +111 -0
  24. package/components/data-display/kbd.js +65 -0
  25. package/components/data-display/loading.js +75 -0
  26. package/components/data-display/progress.js +79 -0
  27. package/components/data-display/radial-progress.js +88 -0
  28. package/components/data-display/skeleton.js +66 -0
  29. package/components/data-display/stats.js +159 -0
  30. package/components/data-display/table.js +146 -0
  31. package/components/data-display/timeline.js +146 -0
  32. package/components/data-display/toast.js +72 -0
  33. package/components/data-display/tooltip.js +74 -0
  34. package/components/data-input/checkbox.js +253 -0
  35. package/components/data-input/file-input.js +224 -0
  36. package/components/data-input/input.js +264 -0
  37. package/components/data-input/radio.js +338 -0
  38. package/components/data-input/range.js +204 -0
  39. package/components/data-input/rating.js +219 -0
  40. package/components/data-input/select.js +287 -0
  41. package/components/data-input/textarea.js +287 -0
  42. package/components/data-input/toggle.js +201 -0
  43. package/components/index.js +137 -0
  44. package/components/layout/divider.js +72 -0
  45. package/components/layout/drawer.js +142 -0
  46. package/components/layout/footer.js +100 -0
  47. package/components/layout/hero.js +109 -0
  48. package/components/layout/indicator.js +90 -0
  49. package/components/layout/join.js +78 -0
  50. package/components/layout/navbar.js +110 -0
  51. package/components/navigation/breadcrumbs.js +91 -0
  52. package/components/navigation/dock.js +103 -0
  53. package/components/navigation/menu.js +126 -0
  54. package/components/navigation/pagination.js +105 -0
  55. package/components/navigation/steps.js +89 -0
  56. package/components/navigation/tabs.css +177 -0
  57. package/components/navigation/tabs.js +123 -0
  58. package/components/theme/theme-switch.css +65 -0
  59. package/components/theme/theme-switch.js +177 -0
  60. package/docs/about.html +164 -0
  61. package/docs/api/computed.html +184 -0
  62. package/docs/api/effects.html +173 -0
  63. package/docs/api/elements.html +180 -0
  64. package/docs/api/enhance.html +225 -0
  65. package/docs/api/hypermedia.html +165 -0
  66. package/docs/api/index.html +178 -0
  67. package/docs/api/nav.html +18 -0
  68. package/docs/api/signals.html +136 -0
  69. package/docs/api/state.html +217 -0
  70. package/docs/assets/images/logo-favicon.svg +42 -0
  71. package/docs/assets/images/logo-static.svg +40 -0
  72. package/docs/assets/images/logo.svg +66 -0
  73. package/docs/assets/js/examplify.js +395 -0
  74. package/docs/assets/styles/site.css +1102 -0
  75. package/docs/assets/styles/themes.css +236 -0
  76. package/docs/components/accordion.html +439 -0
  77. package/docs/components/alert.html +528 -0
  78. package/docs/components/avatar.html +586 -0
  79. package/docs/components/badge.html +531 -0
  80. package/docs/components/breadcrumbs.html +278 -0
  81. package/docs/components/button.html +579 -0
  82. package/docs/components/card.html +561 -0
  83. package/docs/components/carousel.html +286 -0
  84. package/docs/components/chart-area.html +702 -0
  85. package/docs/components/chart-bar.html +782 -0
  86. package/docs/components/chart-column.html +735 -0
  87. package/docs/components/chart-line.html +794 -0
  88. package/docs/components/chart-pie.html +823 -0
  89. package/docs/components/chart.html +610 -15
  90. package/docs/components/chat.html +547 -0
  91. package/docs/components/checkbox.html +641 -0
  92. package/docs/components/collapse.html +536 -0
  93. package/docs/components/component-nav.html +53 -0
  94. package/docs/components/countdown.html +470 -0
  95. package/docs/components/diff.html +245 -0
  96. package/docs/components/divider.html +240 -0
  97. package/docs/components/dock.html +277 -0
  98. package/docs/components/drawer.html +515 -0
  99. package/docs/components/dropdown.html +479 -0
  100. package/docs/components/file-input.html +591 -0
  101. package/docs/components/footer.html +301 -0
  102. package/docs/components/gallery.html +504 -0
  103. package/docs/components/hero.html +264 -0
  104. package/docs/components/index.css +840 -0
  105. package/docs/components/index.html +735 -0
  106. package/docs/components/indicator.html +342 -0
  107. package/docs/components/input.html +644 -0
  108. package/docs/components/join.html +285 -0
  109. package/docs/components/kbd.html +322 -0
  110. package/docs/components/loading.html +521 -0
  111. package/docs/components/menu.html +461 -0
  112. package/docs/components/modal.html +639 -0
  113. package/docs/components/navbar.html +321 -0
  114. package/docs/components/pagination.html +279 -0
  115. package/docs/components/progress.html +514 -0
  116. package/docs/components/radial-progress.html +434 -0
  117. package/docs/components/radio.html +655 -0
  118. package/docs/components/range.html +611 -0
  119. package/docs/components/rating.html +642 -0
  120. package/docs/components/select.html +696 -0
  121. package/docs/components/sidebar-setup.js +93 -0
  122. package/docs/components/skeleton.html +447 -0
  123. package/docs/components/spinner.html +68 -0
  124. package/docs/components/stats.html +486 -0
  125. package/docs/components/steps.html +356 -0
  126. package/docs/components/swap.html +517 -0
  127. package/docs/components/switch.html +68 -0
  128. package/docs/components/table.html +668 -0
  129. package/docs/components/tabs.html +506 -0
  130. package/docs/components/text-input.html +68 -0
  131. package/docs/components/textarea.html +603 -0
  132. package/docs/components/timeline.html +485 -42
  133. package/docs/components/toast.html +474 -0
  134. package/docs/components/toggle.html +564 -0
  135. package/docs/components/tooltip.html +423 -0
  136. package/docs/examples/getting-started-example.html +40 -0
  137. package/docs/examples/index.html +93 -0
  138. package/docs/getting-started/index.html +739 -0
  139. package/docs/getting-started/reviews.html +23 -0
  140. package/docs/getting-started/reviews.odom +108 -0
  141. package/docs/getting-started/reviews.vdom +84 -0
  142. package/docs/index.html +132 -42
  143. package/docs/playground.html +416 -0
  144. package/docs/router.html +285 -0
  145. package/docs/styles/index.html +190 -0
  146. package/functions/_middleware.js +32 -0
  147. package/index.html +309 -0
  148. package/lightview-router.js +364 -0
  149. package/lightview-x.js +1577 -0
  150. package/lightview.js +659 -1200
  151. package/middleware/locale.js +25 -0
  152. package/middleware/markdown.js +44 -0
  153. package/middleware/notFound.js +37 -0
  154. package/package.json +27 -41
  155. package/watch.js +92 -0
  156. package/wrangler.toml +12 -0
  157. package/.idea/lightview.iml +0 -12
  158. package/.idea/modules.xml +0 -8
  159. package/.idea/vcs.xml +0 -6
  160. package/LICENSE +0 -21
  161. package/codepen-no-tabs-embed.css +0 -2
  162. package/docs/CNAME +0 -1
  163. package/docs/api.html +0 -674
  164. package/docs/blank.html +0 -10
  165. package/docs/comparedto.html +0 -89
  166. package/docs/components/chart-repl.html +0 -69
  167. package/docs/components/components.js +0 -113
  168. package/docs/components/contents.html +0 -17
  169. package/docs/components/gantt-repl.html +0 -61
  170. package/docs/components/gantt.html +0 -42
  171. package/docs/components/gauge-repl.html +0 -66
  172. package/docs/components/gauge.html +0 -20
  173. package/docs/components/orgchart-repl.html +0 -64
  174. package/docs/components/orgchart.html +0 -41
  175. package/docs/components/repl-as-src.html +0 -17
  176. package/docs/components/repl-repl.html +0 -95
  177. package/docs/components/repl.html +0 -527
  178. package/docs/components/timeline-repl.html +0 -72
  179. package/docs/components.html +0 -14
  180. package/docs/css/highlightjs.min.css +0 -9
  181. package/docs/css/tutorial.css +0 -35
  182. package/docs/examples/anchor.html +0 -11
  183. package/docs/examples/chart.html +0 -34
  184. package/docs/examples/counter.html +0 -26
  185. package/docs/examples/counter.test.mjs +0 -47
  186. package/docs/examples/counter2.html +0 -26
  187. package/docs/examples/directives.html +0 -79
  188. package/docs/examples/foreign.html +0 -50
  189. package/docs/examples/forgeinform.html +0 -98
  190. package/docs/examples/form.html +0 -61
  191. package/docs/examples/gauge.html +0 -18
  192. package/docs/examples/invalid-template-literals.html +0 -44
  193. package/docs/examples/medium/remote.html +0 -60
  194. package/docs/examples/message.html +0 -18
  195. package/docs/examples/nested.html +0 -11
  196. package/docs/examples/object-bound-form.html +0 -34
  197. package/docs/examples/remote-server.js +0 -51
  198. package/docs/examples/remote.html +0 -34
  199. package/docs/examples/remote.json +0 -1
  200. package/docs/examples/scratch.html +0 -69
  201. package/docs/examples/sensors/index.html +0 -44
  202. package/docs/examples/sensors/sensor-server.js +0 -30
  203. package/docs/examples/shared.html +0 -41
  204. package/docs/examples/template.html +0 -33
  205. package/docs/examples/timeline.html +0 -21
  206. package/docs/examples/todo.html +0 -40
  207. package/docs/examples/top.html +0 -10
  208. package/docs/examples/types.html +0 -94
  209. package/docs/examples/xor.html +0 -62
  210. package/docs/examples.html +0 -25
  211. package/docs/javascript/codejar.min.js +0 -8
  212. package/docs/javascript/highlightjs.min.js +0 -1173
  213. package/docs/javascript/isomorphic-git.js +0 -9
  214. package/docs/javascript/json5.min.js +0 -1
  215. package/docs/javascript/lightning-fs.js +0 -1
  216. package/docs/javascript/lightview.js +0 -1285
  217. package/docs/javascript/marked.min.js +0 -6
  218. package/docs/javascript/peerjs.min.js +0 -70
  219. package/docs/javascript/turndown.js +0 -973
  220. package/docs/javascript/types.js +0 -606
  221. package/docs/javascript/utils.js +0 -45
  222. package/docs/lightview.html +0 -63
  223. package/docs/old_index.html +0 -965
  224. package/docs/old_index.md +0 -1132
  225. package/docs/slidein.html +0 -51
  226. package/docs/tutorial/0-getting-started.html +0 -67
  227. package/docs/tutorial/1-intro-to-variables.html +0 -103
  228. package/docs/tutorial/10-template-components.html +0 -80
  229. package/docs/tutorial/11-linked-components.html +0 -76
  230. package/docs/tutorial/12-imported-components.html +0 -67
  231. package/docs/tutorial/13-input-binding.html +0 -94
  232. package/docs/tutorial/14-automatic-variable-creation.html +0 -74
  233. package/docs/tutorial/15-form-binding.html +0 -110
  234. package/docs/tutorial/16-if-directive.html +0 -60
  235. package/docs/tutorial/17-loop-directives.html +0 -83
  236. package/docs/tutorial/18-sanitizing-and-escaping-input.html +0 -79
  237. package/docs/tutorial/2-imported-and-exported-variables.html +0 -80
  238. package/docs/tutorial/3-data-types.html +0 -89
  239. package/docs/tutorial/4-extended-data-types.html +0 -83
  240. package/docs/tutorial/5-extended-functional-types.html +0 -96
  241. package/docs/tutorial/5.1-extended-functional-types.html +0 -79
  242. package/docs/tutorial/5.2-extended-functional-types.html +0 -70
  243. package/docs/tutorial/6-conventional-javascript.html +0 -75
  244. package/docs/tutorial/7-monitoring-with-observers.html +0 -107
  245. package/docs/tutorial/8-event-listeners.html +0 -65
  246. package/docs/tutorial/9-intro-to-components.html +0 -91
  247. package/docs/tutorial/contents.html +0 -32
  248. package/docs/tutorial/my-component.html +0 -29
  249. package/docs/tutorial/remote-value.json +0 -4
  250. package/docs/websiterepl.html +0 -46
  251. package/jest-puppeteer.config.js +0 -5
  252. package/jest.config.json +0 -12
  253. package/lightview.min.js +0 -1
  254. package/lightview_good.js +0 -1267
  255. package/lightview_optimized.js +0 -1274
  256. package/repl_hold.html +0 -320
  257. package/test/basic.html +0 -104
  258. package/test/basic.test.mjs +0 -315
  259. package/test/extended.html +0 -29
  260. package/test/extended.test.mjs +0 -448
  261. package/types.js +0 -607
  262. package/unsplash.key +0 -1
package/lightview.js CHANGED
@@ -1,1285 +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
- var currentComponent = window.currentComponent = null;
32
- if(document.body) currentComponent = window.currentComponent = document.currentComponent = document.body;
33
- var Lightview = window.Lightview = { };
34
-
35
- var {observe} = (() => {
36
- let CURRENTOBSERVER;//, CURRENTNODE;
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
- const 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 && !template.includes("[") && /^\$\{[a-zA-z_0-9.]*\}$/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
- // todo shopuld self be currentComponent or component
72
- if(self) Object.defineProperty(event,"self",{value:self});
73
- callback(event);
74
- });
75
- }
16
+ const nodeState = new WeakMap();
17
+ const nodeStateFactory = () => ({ effects: [], onmount: null, onunmount: null });
76
18
 
77
- // imports a component based on an anchor and replace content of a target node with an instance of the component
78
- const anchorHandler = async event => {
79
- event.preventDefault();
80
- const target = event.target;
81
- if (target === event.currentTarget) {
82
- const {as} = await importLink(target),
83
- targets = querySelectorAll(document, target.getAttribute("target"));
84
- targets.forEach((target) => {
85
- while (target.lastChild) target.lastChild.remove();
86
- target.appendChild(document.createElement(as))
87
- })
88
- }
89
- }
90
- const getNameFromPath = path => {
91
- const file = path.split("/").pop(),
92
- name = file.split(".")[0],
93
- parts = name.split("-");
94
- if (name.includes("-") && parts[0].length>=1) return name;
95
- return "l-" + name;
96
- }
97
- const observe = (f, thisArg, argsList = []) => {
98
- let observer = (...args) => {
99
- if(observer.cancelled) return;
100
- CURRENTOBSERVER = observer;
101
- try {
102
- f.call(thisArg || this, ...argsList, ...args);
103
- } catch (e) {
19
+ const signalRegistry = new Map();
104
20
 
105
- }
106
- CURRENTOBSERVER = null;
107
- }
108
- observer.cancel = () => {
109
- observer.cancelled = true;
110
- return observer = null;
111
- }
112
- observer();
113
- return observer;
114
- }
115
- const coerce = (value, toType) => {
116
- if (["null","undefined"].includes(value + "") || toType==="any") return value;
117
- const type = typeof (value);
118
- if (type === toType) return value;
119
- if (toType === "number") {
120
- value = value + "";
121
- if(value==="NaN") return NaN;
122
- const result = parseFloat(value);
123
- if(isNaN(result)) throw new TypeError(`Unable to coerce '${value}' to 'number'`)
124
- return result;
125
- }
126
- if (toType === "boolean") {
127
- if ([1,"on", "checked", "selected","true"].includes(value)) return true;
128
- if ([null,"",0,"false"].includes(value)) return false;
129
- }
130
- if (toType === "string") return value + "";
131
- const isfunction = typeof (toType) === "function";
132
- if ((toType === "object" || isfunction)) {
133
- if (type === "object" && isfunction && value instanceof toType) return value;
134
- if (type === "string") {
135
- 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;
136
29
  try {
137
- if (isfunction) {
138
- const instance = toType === Date ? new Date() : Object.create(toType.prototype);
139
- if (instance instanceof Array) {
140
- let parsed = tryParse(value.startsWith("[") ? value : `[${value}]`);
141
- if (!Array.isArray(parsed)) {
142
- if (value.includes(",")) parsed = value.split(",");
143
- else {
144
- parsed = tryParse(`["${value}"]`);
145
- if (!Array.isArray(parsed) || parsed[0] !== value && parsed.length !== 1) parsed = null;
146
- }
147
- }
148
- if (!Array.isArray(parsed)) {
149
- throw new TypeError(`Expected Array for parsed data ${parsed}:${typeof(parsed)}`)
150
- }
151
- instance.push(...parsed);
152
- } else if (instance instanceof Date) {
153
- instance.setTime(Date.parse(value));
154
- } else {
155
- Object.assign(instance, JSON.parse(value));
156
- }
157
- return instance;
158
- }
159
- return JSON.parse(value);
30
+ initialValue = JSON.parse(stored);
160
31
  } catch (e) {
161
- throw new TypeError(`Unable to coerce '${value}:${type}' to '${isfunction ? toType.name : type}'`);
32
+ // Ignore storage errors
162
33
  }
163
34
  }
164
35
  }
165
- throw new TypeError(`Unable to coerce '${value}:${type}' to '${toType}'`)
166
- }
167
- const Reactor = data => {
168
- if (data && typeof (data) === "object") {
169
- if (data.__isReactor__) return data;
170
- let dependents = {};
171
- const childReactors = new Set(),
172
- //nodes = new Set(),
173
- {proxy,revoke} = Proxy.revocable(data, {
174
- get(target, property) {
175
- if (property === "__isReactor__") return true;
176
- if (property === "revoke") return () => {
177
- dependents = {};
178
- childReactors.forEach((reactor) => reactor.revoke());
179
- revoke();
180
- };
181
- if (target instanceof Array) {
182
- if (property === "toJSON") return function toJSON() { return [...target] }
183
- if (property === "toString") return function toString() { return JSON.stringify([...target]) }
184
- }
185
- if(target instanceof Date) {
186
- const value = data[property];
187
- if(typeof(value)==="function") return value.bind(data);
188
- }
189
- let value = target[property];
190
- if(typeof (property) === "symbol") return value;
191
- const type = typeof (value);
192
- if (type === "function") return ([Date].includes(value) || property==="then") ? value.bind(target) : value;
193
- if (CURRENTOBSERVER) (dependents[property] ||= new Set()).add(CURRENTOBSERVER)
194
- if(value===undefined) return;
195
- if (value==null || type !== "object" || childReactors.has(value)) return value;
196
- value = target[property] = Reactor(value);
197
- childReactors.add(value);
198
- return value;
199
- },
200
- async set(target, property, value) {
201
- if(target instanceof Promise) {
202
- console.warn(`Setting ${property} = ${value} on a Promise in Reactor`);
203
- }
204
- const type = typeof (value);
205
- if(value && type==="object" && value instanceof Promise) value = await value;
206
- if (target[property] !== value) {
207
- if (value && type === "object") {
208
- value = Reactor(value);
209
- childReactors.add(value);
210
- }
211
- target[property] = value;
212
- [...dependents[property] || []].forEach((f) => { // handle dependent observers
213
- if (f.cancelled) dependents[property].delete(f);
214
- else f();
215
- })
216
- }
217
- return true;
218
- }
219
- });
220
- //Object.entries(proxy).forEach(([key,value]) => proxy[key] = value);
221
- return proxy;
222
- }
223
- return data;
224
- }
225
36
 
226
- const createVarsProxy = (vars, component, constructor) => {
227
- const {proxy,revoke} = Proxy.revocable(vars, {
228
- get(target, property) {
229
- if(property==="self") return component;
230
- if(property === "revoke") return revoke;
231
- if(target instanceof Date) return Reflect.get(target,property);
232
- if(typeof(property)==="symbol") return target[property];
233
- let {value,get} = target[property] || {};
234
- if(get) return target[property].value = get.call(target[property]);
235
- if (typeof (value) === "function") return value.bind(target);
37
+ let value = initialValue;
38
+ const subscribers = new Set();
39
+
40
+ const f = (...args) => {
41
+ if (args.length === 0) return f.value;
42
+ f.value = args[0];
43
+ };
44
+
45
+ Object.defineProperty(f, 'value', {
46
+ get() {
47
+ if (currentEffect) {
48
+ subscribers.add(currentEffect);
49
+ currentEffect.dependencies.add(subscribers);
50
+ }
236
51
  return value;
237
52
  },
238
- set(target, property, newValue) {
239
- const event = {variableName: property, value: newValue};
240
- if (target[property] === undefined) {
241
- target[property] = {type: "any", value: newValue}; // should we allow this, do first to prevent loops
242
- target.postEvent.value("change", event);
243
- if (event.defaultPrevented) delete target[property].value;
244
- return true;
245
- }
246
- const variable = target[property],
247
- {value, shared, exported, constant, reactive, remote} = variable;
248
- let type = variable.type;
249
- if (constant) {
250
- const error = new TypeError(`${property}:${type} is a constant`);
251
- if(Lightview.renderErrors) {
252
- variable.value = error.toString();
253
- return true;
254
- }
255
- throw error;
256
- }
257
- if(newValue!=null || type.required) newValue = type.validate ? type.validate(newValue,target[property]) : coerce(newValue,type);
258
- const newtype = typeof (newValue),
259
- typetype = typeof (type);
260
- if ((newValue == null && !type.required) ||
261
- type === "any" ||
262
- (newtype === type && typetype==="string") ||
263
- (typetype === "function" && !type.validate && (newValue && newtype === "object" && newValue instanceof type) || variable.validityState?.valid)) {
264
- if (value !== newValue) {
265
- event.oldValue = value;
266
- variable.value = reactive && !variable.__isReactor__ ? Reactor(newValue) : newValue; // do first to prevent loops
267
- target.postEvent.value("change", event);
268
- if (event.defaultPrevented) target[property].value = value;
269
- else if(remote && ((variable.reactive && variable?.remote?.config.put!==false) || remote.put)) remote.handleRemote({variable,config: variable?.remote?.config},true);
270
- else if(variable.set) variable.set(newValue);
271
- }
272
- return true;
273
- }
274
- if (typetype === "function" && newValue && newtype === "object") {
275
- const error = new TypeError(`Can't assign instance of '${newValue.constructor.name}' to variable '${property}:${type.name.replace("bound ", "")}'`)
276
- if(Lightview.renderErrors) {
277
- variable.value = error.toString();
278
- return true;
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
+ }
279
62
  }
280
- throw error;
63
+ // Copy subscribers to avoid infinite loop when effect re-subscribes during iteration
64
+ [...subscribers].forEach(effect => effect());
281
65
  }
282
- const error = new TypeError(`Can't assign '${typeof (newValue)} ${newtype === "string" ? '"' + newValue + '"' : newValue}' to variable '${property}:${typetype === "function" ? type.name.replace("bound ", "") : type} ${type.required ? "required" : ""}'`)
283
- if(Lightview.renderErrors) {
284
- variable.value = error.toString();
285
- return true;
286
- }
287
- throw error;
288
- },
289
- keys() {
290
- return [...Object.keys(vars)];
291
66
  }
292
67
  });
293
- return proxy;
294
- }
295
- // this is a DOM observer, not an observer of the observer programming paradigm
296
- const createObserver = (domNode, framed) => {
297
- const mobserver = new MutationObserver((mutations) => {
298
- mutations.forEach((mutation) => {
299
- const target = mutation.target;
300
- if (mutation.type === "attributes") {
301
- //if (framed) debugger;
302
- const name = mutation.attributeName,
303
- value = target.getAttribute(name);
304
- if (framed && name === "message" && target instanceof IFrameElement) {
305
- //if (value) console.log("message", value);
306
- target.removeAttribute(name);
307
- target.dispatchEvent(new CustomEvent("message", {detail: JSON.parse(value)}))
308
- }
309
- if (target.observedAttributes && target.observedAttributes.includes(name) && value !== mutation.oldValue) {
310
- target.setVariableValue(name, value);
311
- }
312
- } else if (mutation.type === "childList") {
313
- for (const target of mutation.removedNodes) {
314
- //target.lightviewProxies?.forEach((proxy) => proxy.forgetNode(target));
315
- if (target.disconnectedCallback) target.disconnectedCallback();
316
- }
317
- for (const target of mutation.addedNodes) {
318
- if (target.connectedCallback) target.connectedCallback();
319
- }
320
- } else if(mutation.type === "characterData") {
321
- if(target.characterDataMutationCallback) target.characterDataMutationCallback(target,mutation.oldValue,target.textContent);
322
- }
323
- });
324
- });
325
- mobserver.observe(domNode, {subtree: true, childList: true});
326
- return mobserver;
327
- }
328
- const querySelectorAll = (node, selector) => {
329
- const nodes = [...node.querySelectorAll(selector)],
330
- nodeIterator = document.createNodeIterator(node, Node.ELEMENT_NODE);
331
- let currentNode;
332
- while (currentNode = nodeIterator.nextNode()) {
333
- if (currentNode.shadowRoot) nodes.push(...querySelectorAll(currentNode.shadowRoot, selector));
68
+
69
+ if (name) {
70
+ signalRegistry.set(name, f);
334
71
  }
335
- return nodes;
336
- }
337
- const getNodes = (root,href,nodes = new Set()) => {
338
- if (root.shadowRoot) {
339
- nodes.add(root);
340
- getNodes(root.shadowRoot,href,nodes);
341
- } else {
342
- for (const node of root.childNodes) {
343
- if (node.nodeType === Node.TEXT_NODE && (node.template || node.nodeValue?.includes("${"))) {
344
- node.template ||= node.nodeValue;
345
- nodes.add(node);
346
- } else if (node.nodeType === Node.ELEMENT_NODE) {
347
- let skip;
348
- for(const attr of node.attributes) {
349
- if (attr.template || attr.value.includes("${")) {
350
- attr.template ||= attr.value;
351
- nodes.add(node);
352
- } else if (attr.name.startsWith("l-") || attr.name.includes(":")) {
353
- skip = attr.name.includes("l-for:");
354
- nodes.add(node);
355
- }
356
- }
357
- if (node.getAttribute("type") === "radio") nodes.add(node);
358
- if (!skip && !node.shadowRoot) getNodes(node,href,nodes);
359
- }
360
- }
72
+
73
+ return f;
74
+ };
75
+
76
+ signal.get = (name, defaultValue) => {
77
+ if (!signalRegistry.has(name) && defaultValue !== undefined) {
78
+ return signal(defaultValue, name);
361
79
  }
362
- return nodes;
363
- }
80
+ return signalRegistry.get(name);
81
+ };
364
82
 
365
- // populate a string literal template and turn objects into strings
366
- const populateTemplate = (strings,...values) => {
367
- values = values.map((value) => {
368
- const type = typeof(value);
369
- try {
370
- if(value && type==="object") return JSON.stringify(value);
371
- return value;
372
- } catch(e) {
373
- return e.toString();
374
- }
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();
375
116
  });
376
- return strings.reduce((result,string,index) => index < values.length ? result += string + values[index] : result + string,"");
377
- }
117
+ return sig;
118
+ };
378
119
 
379
- const resolveNodeOrText = (node, component, safe,extras={},skipSetAttribute) => {
380
- const type = typeof (node),
381
- template = type === "string" ? node.trim() : node.template;
382
- extras.populateTemplate = populateTemplate;
383
- if (template) {
384
- const name = getTemplateVariableName(template);
385
- try {
386
- const parts = name ? name.split(".") : null;
387
- let value;
388
- value = (parts
389
- ? (value = walk(extras,parts)) || (value = walk(component.varsProxy,parts)) || (value == null ? component[name] : value)
390
- : Function("context", "extras", "with(context) { with(extras) { return populateTemplate`" + (safe ? template : Lightview.sanitizeTemplate(template)) + "` } }")(component.varsProxy,extras));
391
- //let value = Function("context", "with(context) { return `" + Lightview.sanitizeTemplate(template) + "` }")(component.varsProxy);
392
- if(typeof(value)==="function") return value;
393
- value = (name || node.nodeType === Node.TEXT_NODE || safe ? value : Lightview.escapeHTML(value));
394
- if (type === "string") return value==="undefined" ? undefined : value;
395
-
396
- if(!(skipSetAttribute && node.nodeType===Node.ATTRIBUTE_NODE)) {
397
- //requestAnimationFrame(() => {
398
- if(name) node.nodeValue = value==null ? "" : typeof(value)==="string" ? value : JSON.stringify(value);
399
- else node.nodeValue = value == "null" || value == "undefined" ? "" : value;
400
- //})
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;
154
+ }
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);
401
169
  }
402
- return value;
170
+ });
171
+ }
172
+
173
+ // Handle adoptedStyleSheets (modern, efficient approach)
174
+ if (sheets.length > 0) {
175
+ try {
176
+ shadowRoot.adoptedStyleSheets = sheets;
403
177
  } catch (e) {
404
- //console.warn(e);
405
- if (!e.message.includes("defined")) {
406
- if(Lightview.renderErrors) return e.message;
407
- throw e;
408
- } // actually looking for undefined or not defined, but different browsers spell or quote differently
409
- return undefined;
178
+ console.warn('Lightview: adoptedStyleSheets not supported');
410
179
  }
411
180
  }
412
- return node?.nodeValue;
413
- }
414
- const inputTypeToType = inputType => {
415
- const map = {
416
- text:"string",
417
- tel:"string",
418
- email:"string",
419
- url:"string",
420
- search:"string",
421
- radio:"string",
422
- color:"string",
423
- password:"string",
424
- number:"number",
425
- range:"number",
426
- datetime:Date,
427
- checkbox:"boolean"
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);
428
188
  }
429
- return map[inputType] || "any";
430
- }
431
- const enableAnchors = node => {
432
- for(const anode of node.querySelectorAll('a[href$=".html"][target^="#"]')) {
433
- anode.removeEventListener("click", anchorHandler);
434
- addListener(anode, "click", anchorHandler);
189
+
190
+ // Setup children inside shadow root
191
+ if (marker.children && marker.children.length > 0) {
192
+ setupChildrenInTarget(marker.children, shadowRoot);
435
193
  }
436
- }
437
- Object.defineProperty(Lightview,"enableAnchors",{value:enableAnchors});
438
- //Lightview.enableAnchors = enableAnchors;
439
-
440
- const bound = new WeakSet();
441
- const bindInput = (input, variableName, component, value, object) => {
442
- if (bound.has(input)) return;
443
- bound.add(input);
444
- const inputtype = input.tagName === "SELECT" || input.tagName === "TEXTAREA" ? "text" : input.getAttribute("type"),
445
- nameparts = variableName.split(".");
446
- let type = input.tagName === "SELECT" && input.hasAttribute("multiple") ? Array : inputTypeToType(inputtype);
447
- const variable = walk(component.vars,nameparts) || {type};
448
- if(type==="any") type = variable?.type.type || variable?.type;
449
- if(inputtype==="checkbox" && value==null) value = input.checked;
450
- if(value==null) {
451
- const avalue = input.getAttribute("value");
452
- if(avalue) value = avalue;
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; }
217
+ };
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);
453
229
  }
454
- if(object && nameparts.length>1) {
455
- const [root,...path] = nameparts,
456
- key = path[path.length-1];
457
- object = walk(object,path,{depth:path.length-2,create:true});
458
- object[key] = coerce(value,type);
459
- } else {
460
- const existing = component.vars[variableName];
461
- if(existing) {
462
- existingtype = existing?.type.type || existing?.type;
463
- if(existingtype!==type) throw new TypeError(`Attempt to bind <${input.tagName} name="${variableName}" type="${type}"> to variable ${variableName}:${existing.type}`)
464
- existing.reactive = true;
465
- } else if(Lightview.createInputVariables) {
466
- component.variables({[variableName]: type},{reactive,set:typeof(value)==="string" && value.startsWith("${") ? "" : value});
467
- } else {
468
- throw new TypeError(`Attempt to bind undefined variable ${variableName} to <${input.tagName} type="${type}>`)
469
- }
470
- if(inputtype!=="radio") {
471
- if(typeof(value)==="string" && value.includes("${")) input.setAttribute("value","");
472
- else component.setVariableValue(variableName, coerce(value,type));
473
- }
230
+
231
+ // Special handling for shadowDOM pseudo-element
232
+ if (tag === 'shadowDOM') {
233
+ return createShadowDOMMarker(attributes, children);
474
234
  }
475
- let eventname = "change";
476
- if(input.tagName==="FORM") {
477
- eventname = "submit"
478
- } else if (input.tagName !== "SELECT" && (!inputtype || input.tagName === "TEXTAREA" || ["text", "number", "tel", "email", "url", "search", "password"].includes(inputtype))) {
479
- eventname = "input";
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(), {}, []);
480
256
  }
481
- const listener = (event) => {
482
- if (event) event.stopImmediatePropagation();
483
- let value = input.value;
484
- if (inputtype === "checkbox") {
485
- value = input.checked
486
- } else if (input.tagName === "SELECT" && input.hasAttribute("multiple")) {
487
- value = [...input.querySelectorAll("option")]
488
- .filter((option) => option.selected || resolveNodeOrText(option.attributes.value || option.innerText, component) === value)
489
- .map((option) => option.getAttribute("value") || option.innerText);
490
- }
491
- if(object) {
492
- const [root,...path] = nameparts;
493
- object = walk(object,nameparts,{depth:path.length-2,create:true});
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(), {}, []);
494
267
  } else {
495
- object = walk(component.varsProxy,nameparts,{depth:nameparts.length-2,create:true});
268
+ const wrapper = document.createElement('span');
269
+ wrapper.style.display = 'contents';
270
+ wrapper.appendChild(content);
271
+ return wrapDomElement(wrapper, 'span', {}, []);
496
272
  }
497
- const key = nameparts[nameparts.length-1];
498
- object[key] = coerce(value,type);
499
- };
500
- addListener(input, eventname, listener,component);
501
- }
502
- const tryParse = value => {
503
- try {
504
- return JSON.parse(value);
505
- } catch (e) {
506
- return value;
507
273
  }
508
- }
509
- const reactive = () => {
510
- return {
511
- init({variable, component}) {
512
- variable.reactive = true;
513
- component.vars[variable.name] = Reactor(variable);
514
- }
274
+
275
+ // vDOM object with tag property
276
+ if (typeof result === 'object' && result.tag) {
277
+ return element(result.tag, result.attributes || {}, result.children || []);
515
278
  }
516
- }
517
- const exported = () => {
518
- return {
519
- init({variable, component}) {
520
- const name = variable.name,
521
- set = variable.set || (() => {});
522
- variable.exported = true;
523
- variable.set = (newValue) => {
524
- set(newValue);
525
- if(variable.exported) { // still exported
526
- if(variable.value==null) {
527
- removeComponentAttribute(component, name);
528
- } else {
529
- const type = typeof(newValue);
530
- // note, using isArray below will not always work for proxied items, so using instanceOf
531
- newValue = type === "string" ? newValue : (type==="object" && newValue instanceof Array) ? JSON.stringify([...newValue]) : JSON.stringify(newValue);
532
- setComponentAttribute(component, name, newValue);
533
- }
534
- }
535
- }
536
- variable.set(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 || []);
537
289
  }
538
290
  }
539
- }
540
- const imported = () => {
541
- return {
542
- init({variable, component}) {
543
- const name = variable.name.toLowerCase();
544
- let value = component.hasAttribute(name) ? component.getAttribute(name) : null;
545
- if((variable.type==="boolean" || variable.type.type==="boolean") && value==="") value = true;
546
- variable.imported = true;
547
- variable.value = value!=null ? coerce(value, variable.type) : variable.value;
548
- if(variable.set) {
549
- variable.set(variable.value);
291
+
292
+ return null;
293
+ };
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;
550
306
  }
307
+ return true;
551
308
  }
552
- }
553
- }
554
-
555
- let reserved = {
556
- reactive: {
557
- constant: true,
558
- value: reactive
559
- },
560
- exported: {
561
- constant: true,
562
- value: exported
563
- },
564
- imported: {
565
- constant: true,
566
- value: imported
567
- }
568
- }
309
+ });
310
+ };
569
311
 
570
- // patches relative references to use the passed in href as base
571
- const patchElementURIs = (node,tagName,attributeName,href) => { // attributeName will always be href or src
572
- if(node) {
573
- for(const child of node.querySelectorAll(tagName)) {
574
- let value = child.getAttribute(attributeName);
575
- if (value) child.setAttribute(attributeName, new URL(value, href).href);
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);
576
331
  }
332
+ } else {
333
+ domNode.setAttribute(key, value);
577
334
  }
578
- }
579
- // forces re-evaluation of scripts and links
580
- const forceLoadElement = node => {
581
- if((node.getAttribute("src")||"").includes("/lightview.js")) return;
582
- const el = document.createElement(node.tagName.toLowerCase());
583
- for(const attr of node.attributes) el.setAttribute(attr.name,attr.value);
584
- el.innerHTML = node.innerHTML;
585
- node.after(el);
586
- node.remove();
587
- }
588
- const createClass = (domElementNode, {observer, framed, href= window.location.href.replace("blob:",""), mount}) => {
589
- const instances = new Set(),
590
- observedAttributes = [];
591
- let dom;
592
- observedAttributes.add = function(name) { observedAttributes.includes(name) || observedAttributes.push(name) }
593
- const cls = class CustomElement extends HTMLElement {
594
- static get instances() {
595
- return instances;
596
- }
597
- static setTemplateNode(node) {
598
- dom = node.tagName === "TEMPLATE"
599
- ? document.createElement("div")
600
- : node.cloneNode(true);
601
- if(node.tagName === "TEMPLATE") {
602
- dom.innerHTML = node.innerHTML;
603
- for(const attr of node.attributes) dom.setAttribute(attr.name,attr.value);
335
+ };
336
+
337
+ const makeReactiveAttributes = (attributes, domNode) => {
338
+ const reactiveAttrs = {};
339
+
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;
344
+
345
+ if (key === 'onmount' && domNode.isConnected) {
346
+ value(domNode);
604
347
  }
605
- patchElementURIs(dom.head,"link","href",href);
606
- patchElementURIs(dom.head,"script","src",href);
607
- patchElementURIs(dom.body||dom,"link","href",href);
608
- patchElementURIs(dom.body||dom,"script","src",href);
609
- dom.mount = node.mount || node.body?.mount;
610
- }
611
- constructor() {
612
- super();
613
- this.componentBaseURI = href;
614
- currentComponent = window.currentComponent = document.currentComponent = this;
615
- instances.add(this);
616
- const shadow = this.attachShadow({mode: "open"}),
617
- eventlisteners = {};
618
- this.vars = {
619
- ...reserved,
620
- observe: {
621
- value: (...args) => observe(...args),
622
- type: "function",
623
- constant: true
624
- },
625
- addEventListener: {
626
- value: (eventName, listener) => {
627
- const listeners = eventlisteners[eventName] ||= new Set();
628
- listeners.forEach((f) => {
629
- if (listener + "" === f + "") listeners.delete(f);
630
- })
631
- eventlisteners[eventName].add(listener);
632
- },
633
- type: "function",
634
- constant: true
635
- },
636
- postEvent: {
637
- value: (eventName, event = {}) => {
638
- //event = {...event}
639
- event.type = eventName;
640
- event.target = currentComponent;
641
- eventlisteners[eventName]?.forEach((f) => f(event));
642
- },
643
- type: "function",
644
- constant: true
645
- },
646
- self: {value: currentComponent, type: CustomElement, constant: true}
647
- };
648
- this.varsProxy = createVarsProxy(this.vars, this, CustomElement);
649
- if (framed || CustomElement.lightviewFramed) this.variables({message: Object}, {exported});
650
- ["getElementById", "querySelector"] //, "querySelectorAll"
651
- .forEach((fname) => {
652
- const f = this[fname];
653
- Object.defineProperty(this, fname, {
654
- configurable: true,
655
- writable: true,
656
- value: (...args) => { return (f ? f.call(this,...args) : null) || this.shadowRoot[fname](...args) }
657
- })
658
- });
659
- ["querySelectorAll"]
660
- .forEach((fname) => {
661
- const f = this[fname];
662
- Object.defineProperty(this, fname, {
663
- configurable: true,
664
- writable: true,
665
- value: (...args) => { return [...f.call(this,...args),...this.shadowRoot[fname](...args)] }
666
- })
667
- });
668
- [...dom.head?.childNodes||[]].forEach((child) => {
669
- const clone = child.cloneNode(true);
670
- document.head.appendChild(clone);
671
- if(["LINK","SCRIPT"].includes(clone.tagName)) forceLoadElement(clone);
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);
368
+ }
672
369
  });
673
- const body = dom.body || dom;
674
- for(const child of body.childNodes) {
675
- if(child.tagName && customElements.get(child.tagName.toLowerCase())) {
676
- const node = document.createElement(child.tagName);
677
- for(const attr of child.attributes) node.setAttribute(attr.name,attr.value);
678
- //currentComponent = window.currentComponent = document.currentComponent = node;
679
- shadow.appendChild(node);
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);
680
380
  } else {
681
- const clone = child.cloneNode(true);
682
- shadow.appendChild(clone);
683
- if(["LINK","SCRIPT"].includes(clone.tagName)) forceLoadElement(clone);
381
+ domNode.style[styleKey] = styleValue;
684
382
  }
685
- }
686
- //forceLoadElements(shadow,"link");
687
- //forceLoadElements(shadow,"script");
688
- //importStyleSheets(shadow,this);
689
- enableAnchors(shadow);
383
+ });
384
+ reactiveAttrs[key] = value;
385
+ } else {
386
+ // Static attribute - handle undefined/null/boolean properly
387
+ setAttributeValue(domNode, key, value);
388
+ reactiveAttrs[key] = value;
690
389
  }
390
+ }
691
391
 
692
- get siblings() {
693
- return [...CustomElement.instances].filter((sibling) => sibling != this);
694
- }
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');
695
412
 
696
- adoptedCallback() {
697
- if (this.hasOwnProperty("adoptedCallback")) this.adoptedCallback();
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;
698
418
  }
699
419
 
700
- disconnectedCallback() {
701
- instances.delete(this);
420
+ // Handle nested arrays (flattening)
421
+ if (Array.isArray(child)) {
422
+ childElements.push(...processChildren(child, targetNode, false));
423
+ continue;
702
424
  }
703
425
 
704
- connectedCallback() {
705
- const node = dom.body || dom;
706
- for(const attr of node.attributes) {
707
- if(!this.hasAttribute(attr.name)) this.setAttribute(attr.name,attr.value);
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;
708
433
  }
709
- if(mount ||= dom.mount||this.mount) {
710
- const script = document.createElement("script");
711
- document.currentComponent = this;
712
- script.innerHTML = `with(document.currentComponent.varsProxy) {
713
- ${typeof(lightviewDebug)!=="undefined" && lightviewDebug===true ? "debugger;" : ""}
714
- const component = document.currentComponent;
715
- (async () => { await (${mount}).call(self,self);
716
- component.compile(); })();
717
- };`;
718
- this.appendChild(script);
719
- script.remove();
720
- for(const child of this.querySelectorAll('link[rel="stylesheet"][export]')) { // ].forEach(async (child) =>
721
- child.remove();
722
- this.appendChild(child);
723
- }
724
- window.currentComponent = document.currentComponent = null;
725
- }
726
- }
727
- compile() {
728
- // Promise.all(promises).then(() => {
729
- const shadow = this.shadowRoot,
730
- nodes = getNodes(this,href),
731
- processNodes = (nodes,{object,extras}={}) => { //rootName
732
- nodes.forEach((node) => {
733
- if (node.nodeType === Node.TEXT_NODE && node.template.includes("${")) {
734
- const observer = observe(((node,ctx) => () => resolveNodeOrText(node, ctx,true,extras))(node,this));
735
- node.observers ||= new Set();
736
- node.observers.add(observer);
737
- if(node.parentElement?.tagName==="TEXTAREA") {
738
- const name = getTemplateVariableName(node.template);
739
- if (name) {
740
- const nameparts = name.split(".");
741
- if(extras && extras[nameparts[0]]) object = extras[nameparts[0]];
742
- if(!this.vars[nameparts[0]] || this.vars[nameparts[0]].reactive || object) {
743
- bindInput(node.parentElement, name, this, resolveNodeOrText(node.template, this,true,extras), object);
744
- }
745
- }
746
- }
747
- } else if (node.nodeType === Node.ELEMENT_NODE) {
748
- if(node.tagName==="FORM") {
749
- const value = node.getAttribute("value"),
750
- name = getTemplateVariableName(value);
751
- if(name) {
752
- node.addEventListener("submit",(event) => {
753
- if(!event.target.hasAttribute("action")) event.preventDefault();
754
- const object = {};
755
- [...node.querySelectorAll("input[name]"),...node.querySelectorAll("textarea[name]")]
756
- .forEach((input) => {
757
- const eltype = input.getAttribute("type"),
758
- path = input.getAttribute("name").split("."),
759
- property = path[path.length-1],
760
- target = walk(object,path,{depth:path.length-2,create:true})
761
- if(["radio","checkbox"].includes(eltype)) {
762
- target[property] = input.checked;
763
- } else {
764
- target[property] = input.value;
765
- }
766
- });
767
- this.varsProxy[name] = object;
768
- })
769
- }
770
- return;
771
- }
772
- // resolve the value before all else;
773
- const attr = node.attributes.value,
774
- template = attr?.template;
775
- if (attr && template) {
776
- //let value = resolveNodeOrText(attr, this),
777
- // ;
778
- const eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, this, false,extras) : null,
779
- aname = node.getAttribute("name"),
780
- template = attr.template;// || (rootName && aname ? `\${${rootName}.${name}}` : null);
781
- if (template) {
782
- const name = getTemplateVariableName(template);
783
- if (name) {
784
- const nameparts = name.split(".");
785
- if(extras && extras[nameparts[0]]) object = extras[nameparts[0]];
786
- if(!this.vars[nameparts[0]] || this.vars[nameparts[0]].reactive || object) {
787
- bindInput(node, name, this, resolveNodeOrText(attr, this,false,extras), object);
788
- }
789
- }
790
- const observer = observe(((node,ctx,template) => () => {
791
- const value = resolveNodeOrText(template, ctx,false,extras);
792
- if(value!==undefined) {
793
- if (eltype === "checkbox") {
794
- if (coerce(value, "boolean") === true) {
795
- node.setAttribute("checked", "");
796
- //node.checked = true; // do we need to do this and set attribute
797
- } else {
798
- node.removeAttribute("checked");
799
- //node.checked = false;
800
- }
801
- } else if (node.tagName === "SELECT") {
802
- const values = node.hasAttribute("multiple") ? coerce(value, Array) : [value];
803
- [...node.querySelectorAll("option")].forEach(async (option) => {
804
- if (option.hasAttribute("value")) {
805
- if (values.includes(resolveNodeOrText(option.attributes.value, ctx,false,extras))) {
806
- option.setAttribute("selected", "");
807
- //option.selected = true;
808
- }
809
- } else if (values.includes(resolveNodeOrText(option.innerText, ctx,false,extras))) {
810
- option.setAttribute("selected", "");
811
- //option.selected = true;
812
- }
813
- })
814
- } else if (eltype!=="radio") {
815
- //attr.value = typeof(value)==="string" ? value : JSON.stringify(value);
816
- let avalue = typeof(value)==="string" ? value : value.toString ? value.toString() : JSON.stringify(value);
817
- if(avalue.startsWith('"')) avalue = avalue.substring(1);
818
- if(avalue.endsWith('"')) avalue = avalue.substring(0,avalue.length-1);
819
- attr.value = value;
820
- if(node.tagName==="INPUT") node.value = value;
821
- }
822
- }
823
- })(node,this,template));
824
- node.observers ||= new Set();
825
- node.observers.add(observer);
826
- }
827
- }
828
- for(const attr of node.attributes) {
829
- if (attr.name === "value" && attr.template) continue;
830
- const {name, value} = attr,
831
- vname = node.attributes.name?.value;
832
- if (value.includes("${")) attr.template = value;
833
- if (name === "type" && value == "radio" && vname) {
834
- bindInput(node, vname, this, undefined, object);
835
- const observer = observe(((node,ctx) => () => {
836
- const varvalue = Function("context", "with(context) { return `${" + vname + "}` }")(ctx.varsProxy);
837
- if (node.attributes.value.value == varvalue) {
838
- node.setAttribute("checked", "");
839
- node.checked = true;
840
- } else {
841
- node.removeAttribute("checked");
842
- node.checked = false;
843
- }
844
- })(node,this));
845
- node.observers ||= new Set();
846
- node.observers.add(observer);
847
- }
848
- const [type, ...params] = name.split(":");
849
- if (type === "") { // name is :something
850
- const observer = observe(((node,attr,ctx)=>() => {
851
- const value = attr.value;
852
- if (params[0]) {
853
- if (value === "true") node.setAttribute(params[0], "")
854
- else node.removeAttribute(params[0]);
855
- } else {
856
- const elvalue = node.attributes.value ? resolveNodeOrText(node.attributes.value, ctx, false, extras) : null,
857
- eltype = node.attributes.type ? resolveNodeOrText(node.attributes.type, ctx, false, extras) : null;
858
- if (eltype === "checkbox" || node.tagName === "OPTION") {
859
- if (elvalue === true) node.setAttribute("checked", "")
860
- else node.removeAttribute("checked");
861
- }
862
- }
863
- })(node,attr,this));
864
- node.observers ||= new Set();
865
- node.observers.add(observer);
866
- } else if (type === "l-on") {
867
- let listener;
868
- const observer = observe(((node,attr,ctx) => () => {
869
- const value = resolveNodeOrText(attr, ctx, true, extras,true);
870
- if (listener) node.removeEventListener(params[0], listener);
871
- listener = null;
872
- if (typeof (value) === "function") {
873
- listener = value;
874
- } else {
875
- try {
876
- listener = Function("return " + value)();
877
- } catch (e) {
878
-
879
- }
880
- }
881
- if (listener) addListener(node, params[0], listener, ctx);
882
- })(node,attr,this));
883
- node.observers ||= new Set();
884
- node.observers.add(observer);
885
- } else if (type === "l-if") {
886
- const observer = observe(((node,attr,ctx) => () => {
887
- const value = resolveNodeOrText(attr, ctx, true, extras);
888
- node.style.setProperty("display", value == true || value === "true" ? "revert" : "none");
889
- })(node,attr,this));
890
- node.observers ||= new Set();
891
- node.observers.add(observer);
892
- } else if (type === "l-for") {
893
- //node.template ||= node.innerHTML;
894
- //node.clone ||= node.cloneNode(true);
895
- let clone = node.cloneNode(true),
896
- observer = observe(((node,attr,ctx) => () => {
897
- const [what = "each", vname = "item", index = "index", array = "array", after = false] = params;
898
- /*if (!window.lightviewDebug) {
899
- requestAnimationFrame(() => {
900
- if (after) node.style.setProperty("display", "none")
901
- else node.innerHTML = "";
902
- })
903
- }*/
904
- const value = resolveNodeOrText(attr, ctx, false, extras,true),
905
- coerced = coerce(value, what === "each" ? Array : "object"),
906
- target = what === "each" ? coerced : Object[what](value),
907
- children = target.reduce((children, item, i, target) => {
908
- const child = clone.cloneNode(true),
909
- extras = {
910
- [vname]: item,
911
- [index]: i,
912
- [array]: target
913
- },
914
- nodes = getNodes(child, href);
915
- processNodes(nodes,{extras});
916
- children.push(...child.childNodes);
917
- return children;
918
- }, []);
919
- //requestAnimationFrame(() => {
920
- if(children.length===0) {
921
- node.innerHTML = ""
922
- } else {
923
- children.forEach((child,i) => {
924
- if (after) node.parentElement.insertBefore(child, node);
925
- else if(node?.childNodes[i]) {
926
- const old = node.childNodes[i];
927
- if(old.observers) {
928
- old.observers.forEach((observer) => observer.cancel())
929
- old.observers.clear();
930
- old.observers = null;
931
- }
932
- node.replaceChild(child,old);
933
- }
934
- else node.appendChild(child);
935
- })
936
- while(node.childNodes.length>children.length) node.lastChild.remove();
937
- }
938
- // });
939
- })(node,attr,this,extras));
940
- node.observers ||= new Set();
941
- node.observers.add(observer);
942
- } else if (attr.template) {
943
- const observer = observe(((node,attr,ctx,extras) => () => {
944
- resolveNodeOrText(attr, ctx, false, extras);
945
- })(node,attr,this,extras));
946
- node.observers ||= new Set();
947
- node.observers.add(observer);
948
- }
949
- }
950
- }
951
- })
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;
459
+ }
460
+ const childrenToProcess = Array.isArray(val) ? val : [val];
461
+ // processChildren handles clearing existing content via 3rd arg=true
462
+ processChildren(childrenToProcess, wrapper, true);
952
463
  };
953
- processNodes(nodes);
954
- shadow.normalize();
955
- observer ||= createObserver(this, framed);
956
- observer.observe(this, {attributeOldValue: true, subtree:true, characterData:true, characterDataOldValue:true});
957
- if(this.hasAttribute("l-unhide")) this.removeAttribute("hidden");
958
- //ctx.vars.postEvent.value("connected");
959
- this.dispatchEvent(new Event("mounted"));
960
- // })
961
- }
962
- adoptedCallback(callback) {
963
- this.dispatchEvent(new Event("adopted"));
964
- }
965
- disconnectedCallback() {
966
- this.dispatchEvent(new Event("disconnected"));
967
- }
968
- get observedAttributes() {
969
- return CustomElement.observedAttributes;
970
- }
971
- static get observedAttributes() {
972
- return observedAttributes;
973
- }
974
464
 
975
- getVariableNames() {
976
- return Object.keys(this.vars)
977
- .filter(name => !(name in reserved) && !["self", "addEventListener", "postEvent","observe"].includes(name))
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);
478
+ }
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);
978
488
  }
489
+ }
979
490
 
980
- getVariableData() {
981
- return this.getVariableNames().reduce((data,name) => {
982
- data[name] = this.getVariableValue(name);
983
- return data;
984
- },{})
985
- }
491
+ return childElements;
492
+ };
986
493
 
987
- getVariable(name) {
988
- return this.vars[name] ? {...this.vars[name]} : undefined;
989
- }
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
+ };
990
501
 
991
- setVariableValue(variableName, value, {coerceTo = typeof (value)} = {}) {
992
- if (!this.isConnected) {
993
- instances.delete(this);
994
- return false;
995
- }
996
- let {type} = this.vars[variableName] || {};
997
- if (type) {
998
- if (this.varsProxy[variableName] !== value) {
999
- const variable = this.vars[variableName];
1000
- if (variable.shared) {
1001
- value = type.validate ? type.validate(value,variable) : coerce(value,coerceTo);
1002
- const event = {
1003
- variableName: variableName,
1004
- value: value,
1005
- oldValue: variable.value
1006
- };
1007
- variable.value = value;
1008
- this.vars.postEvent.value("change", event);
1009
- if (event.defaultPrevented) variable.value = value;
1010
- } else {
1011
- this.varsProxy[variableName] = value;
1012
- }
1013
- }
1014
- return true;
1015
- }
1016
- this.vars[variableName] = {name, type: coerceTo, value: coerce(value, coerceTo)};
1017
- return false;
1018
- }
502
+ /**
503
+ * Setup children on a DOM node, clearing existing content
504
+ */
505
+ const setupChildren = (children, domNode) => {
506
+ return processChildren(children, domNode, true);
507
+ };
1019
508
 
1020
- getVariableValue(variableName) {
1021
- return this.vars[variableName]?.value;
1022
- }
509
+ // ============= EXPORTS =============
510
+ const enhance = (selectorOrNode, options = {}) => {
511
+ const domNode = typeof selectorOrNode === 'string'
512
+ ? document.querySelector(selectorOrNode)
513
+ : selectorOrNode;
1023
514
 
1024
- variables(variables, {remote, constant, set,...rest} = {}) { // options = {observed,reactive,shared,exported,imported}
1025
- const options = {remote, constant, ...rest};
1026
- if (variables !== undefined) {
1027
- Object.entries(variables)
1028
- .forEach(([key, type]) => {
1029
- if(isArrowFunction(type)) type = type();
1030
- const variable = this.vars[key] ||= {name: key, type};
1031
- if(set!==undefined && constant!==undefined) throw new TypeError(`${key} has the constant value ${constant} and can't be set to ${set}`);
1032
- if(set!==undefined) variable.value = set;
1033
- if(constant!==undefined) {
1034
- if(remote || rest.imported || rest.observed) throw new TypeError(`${key} can't be a constant and also remote, imported or observed`)
1035
- variable.constant = true;
1036
- variable.value = constant;
1037
- }
1038
- if (remote) {
1039
- const type = typeof(remote);
1040
- if(type==="function") variable.remote = remote(`./${key}`);
1041
- else if(this.vars.remote.value && type==="string") variable.remote = this.vars.remote.value(remote);
1042
- else throw new TypeError("Attempt to use type 'remote' without importing 'remote' from types.js")
1043
- variable.remote.handleRemote({variable, config:variable.remote.config,component:this});
1044
- }
1045
- // todo: handle custom functional types, remote should actually be handled this way
1046
- Object.entries(rest).forEach(([type,f]) => {
1047
- const functionalType = variable[type] = typeof(f)==="function" ? f() : f;
1048
- if(functionalType.init) functionalType.init({variable,options,component:this,coerce});
1049
- 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`);
1050
- variable.set != functionalType.set;
1051
- variable.get != functionalType.get;
1052
- });
1053
- if(type.validate && variable.value!==undefined) type.validate(variable.value,variable);
1054
- });
1055
- }
1056
- return Object.entries(this.vars)
1057
- .reduce((result, [key, variable]) => {
1058
- result[key] = {...variable};
1059
- return result;
1060
- }, {});
1061
- }
1062
- }
1063
- cls.setTemplateNode(domElementNode);
1064
- return cls;
1065
- }
515
+ // If it's already a Lightview element, use its domEl
516
+ const node = domNode.domEl || domNode;
517
+ if (!(node instanceof HTMLElement)) return null;
1066
518
 
1067
- const createComponent = (name, node, {framed, observer, href, mount} = {}) => {
1068
- let ctor = customElements.get(name);
1069
- if (ctor) {
1070
- if (framed && !ctor.lightviewFramed) ctor.lightviewFramed = true;
1071
- else console.warn(new Error(`${name} is already a CustomElement. Not redefining`));
1072
- return ctor;
1073
- }
1074
- ctor = createClass(node, {observer, framed, href, mount});
1075
- customElements.define(name, ctor);
1076
- Lightview.customElements.set(name, ctor);
1077
- return ctor;
1078
- }
1079
- Lightview.customElements = new Map();
1080
- Lightview.createComponent = createComponent;
1081
- const importLink = async (link, observer) => {
1082
- const url = (new URL(link.getAttribute("href"),document.baseURI)),
1083
- as = link.getAttribute("as") || getNameFromPath(url.pathname);
1084
- if (url.origin !== window.location.origin && !link.getAttribute("crossorigin")) {
1085
- throw new URIError(`importLink:HTML imports must be from same domain: ${url.hostname}!=${location.hostname} unless 'crossorigin' attribute is set.`)
519
+ const tagName = node.tagName.toLowerCase();
520
+ let el = domToElement.get(node);
521
+
522
+ if (!el) {
523
+ el = wrapDomElement(node, tagName);
1086
524
  }
1087
- if (!customElements.get(as)) {
1088
- const html = await (await fetch(url.href)).text(),
1089
- dom = parser.parseFromString(html, "text/html"),
1090
- unhide = !!dom.head.querySelector('meta[name="l-unhide"]');
1091
- for (const childlink of dom.head.querySelectorAll('link[href]')) {
1092
- childlink.setAttribute("href", new URL(childlink.getAttribute("href"), url.href).href);
1093
- if (link.hasAttribute("crossorigin")) childlink.setAttribute("crossorigin", link.getAttribute("crossorigin"))
1094
- if(childlink.getAttribute("href").endsWith(".html") && childlink.getAttribute("rel")==="module") await importLink(childlink, observer);
1095
- }
1096
- currentComponent = window.currentComponent = document.currentComponent = dom.body;
1097
- currentComponent.componentBaseURI = url.href;
1098
- const lvscript = dom.getElementById("lightview");
1099
- if(lvscript) {
1100
- const script = document.createElement("script");
1101
- script.innerHTML = lvscript.innerHTML;
1102
- document.body.appendChild(script);
1103
- 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;
1104
533
  }
1105
- currentComponent = window.currentComponent = document.currentComponent = null;
1106
- createComponent(as, dom, {observer,href:url.href});
1107
- if (unhide) dom.body.removeAttribute("hidden");
1108
- }
1109
- return {as};
1110
- }
1111
- const importLinks = async () => {
1112
- const observer = createObserver(document.body);
1113
- for (const link of document.querySelectorAll(`link[href$=".html"][rel=module]`)) {
1114
- await importLink(link, observer);
1115
534
  }
1116
- }
1117
535
 
1118
- const bodyAsComponent = ({as = "x-body", unhide, framed} = {}) => {
1119
- createComponent(as, document.body, {framed});
1120
- const component = document.createElement(as);
1121
- document.body.parentElement.replaceChild(component, document.body);
1122
- Object.defineProperty(document, "body", {
1123
- enumerable: true, configurable: true, get() {
1124
- return component;
1125
- }
1126
- });
1127
- if (unhide) component.removeAttribute("hidden");
1128
- }
1129
- Lightview.bodyAsComponent = bodyAsComponent;
1130
- const postMessage = (data, target = window.parent) => {
1131
- if (postMessage.enabled) {
1132
- if (target instanceof HTMLIFrameElement) {
1133
- data = {...data, href: document.baseURI};
1134
- target.contentWindow.postMessage(JSON.stringify(data), "*");
536
+ if (innerHTML !== undefined) {
537
+ if (typeof innerHTML === 'function') {
538
+ effect(() => { node.innerHTML = innerHTML(); });
1135
539
  } else {
1136
- data = {...data, iframeId: document.lightviewId, href: document.baseURI};
1137
- target.postMessage(JSON.stringify(data), "*");
540
+ node.innerHTML = innerHTML;
1138
541
  }
1139
542
  }
1140
- }
1141
- const setComponentAttribute = (node, name, value) => {
1142
- if (node.getAttribute(name) !== value) node.setAttribute(name, value);
1143
- postMessage({type: "setAttribute", argsList: [name, value]});
1144
- }
1145
- const removeComponentAttribute = (node, name, value) => {
1146
- node.removeAttribute(name);
1147
- postMessage({type: "removeAttribute", argsList: [name]});
1148
- }
1149
- const getNodePath = (node, path = []) => {
1150
- path.unshift(node);
1151
- if (node.parentNode && node.parentNode !== node.parentNode) getNodePath(node.parentNode, path);
1152
- return path;
1153
- }
1154
- const onresize = (node, callback) => {
1155
- const resizeObserver = new ResizeObserver(() => callback());
1156
- 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;
1157
550
  };
1158
551
 
1159
- const url = new URL(document.currentScript.getAttribute("src"), document.baseURI);
1160
- let domContentLoadedEvent;
1161
- if (!domContentLoadedEvent) addListener(window, "DOMContentLoaded", (event) => domContentLoadedEvent = event);
1162
- let OBSERVER;
1163
- const loader = async whenFramed => {
1164
- await importLinks();
1165
- const unhide = !!document.querySelector('meta[name="l-unhide"]'),
1166
- isolated = !!document.querySelector('meta[name="l-isolate"]'),
1167
- enableFrames = !!document.querySelector('meta[name="l-enableFrames"]');
1168
- if (whenFramed) {
1169
- whenFramed({unhide, isolated, enableFrames, framed: true});
1170
- if (!isolated) {
1171
- postMessage.enabled = true;
1172
- addListener(window, "message", ({data}) => {
1173
- const {type, argsList} = JSON.parse(data);
1174
- if (type === "framed") {
1175
- const resize = () => {
1176
- const {width, height} = document.body.getBoundingClientRect();
1177
- postMessage({type: "setAttribute", argsList: ["width", width]})
1178
- postMessage({type: "setAttribute", argsList: ["height", height + 20]});
1179
- }
1180
- resize();
1181
- onresize(document.body, () => resize());
1182
- 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;
1183
569
  }
1184
- if (["setAttribute","removeAttribute"].includes(type)) {
1185
- let [name, value] = [...argsList];
1186
- const variable = document.body.vars[name];
1187
- if(type==="removeAttribute") value = undefined;
1188
- if (variable && variable.imported) document.body.setVariableValue(name, value);
570
+ if (item.tag && !item.domEl) {
571
+ return element(item.tag, item.attributes || {}, item.children || []).domEl;
572
+ } else {
573
+ return item.domEl || item;
1189
574
  }
1190
575
  });
1191
- const url = new URL(document.baseURI);
1192
- document.lightviewId = url.searchParams.get("id");
1193
- postMessage({type: "DOMContentLoaded"})
576
+
577
+ if (location === 'shadow') {
578
+ let shadow = el.shadowRoot;
579
+ if (!shadow) {
580
+ shadow = el.attachShadow({ mode: 'open' });
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);
607
+ });
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]);
1194
636
  }
1195
- } else if (url.searchParams.has("as")) {
1196
- bodyAsComponent({as: url.searchParams.get("as"), unhide});
637
+
638
+ return wrapper;
639
+ },
640
+ set(_, tag, value) {
641
+ customTags[tag] = value;
642
+ return true;
1197
643
  }
1198
- if (enableFrames) {
1199
- postMessage.enabled = true;
1200
- addListener(window, "message", (message) => {
1201
- const {type, iframeId, argsList, href} = JSON.parse(message.data),
1202
- iframe = document.getElementById(iframeId);
1203
- if (iframe) {
1204
- if (type === "DOMContentLoaded") {
1205
- postMessage({type: "framed", href: document.baseURI}, iframe);
1206
- Object.defineProperty(domContentLoadedEvent, "currentTarget", {
1207
- enumerable: false,
1208
- configurable: true,
1209
- value: iframe
1210
- });
1211
- domContentLoadedEvent.href = href;
1212
- domContentLoadedEvent.srcElement = iframe;
1213
- domContentLoadedEvent.bubbles = false;
1214
- domContentLoadedEvent.path = getNodePath(iframe);
1215
- Object.defineProperty(domContentLoadedEvent, "timeStamp", {
1216
- enumerable: false,
1217
- configurable: true,
1218
- value: performance.now()
1219
- })
1220
- iframe.dispatchEvent(domContentLoadedEvent);
1221
- return;
1222
- }
1223
- if (type === "setAttribute") {
1224
- const [name, value] = [...argsList];
1225
- if (iframe.getAttribute(name) !== value + "") iframe.setAttribute(name, value);
1226
- return;
1227
- }
1228
- if (type === "removeAttribute") {
1229
- iframe.removeAttribute(...argsList);
1230
- 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' });
1231
692
  }
1232
693
  }
1233
- 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);
1234
725
  });
1235
- if (!OBSERVER) {
1236
- const mutationCallback = (mutationsList) => {
1237
- const console = document.getElementById("console");
1238
- for (const {target, attributeName, oldValue} of mutationsList) {
1239
- const value = target.getAttribute(attributeName);
1240
- if (!["height", "width", "message"].includes(attributeName)) {
1241
- if (!value) postMessage({type: "removeAttribute", argsList: [attributeName]}, iframe)
1242
- else if (value !== oldValue) {
1243
- postMessage({
1244
- type: "setAttribute",
1245
- argsList: [attributeName, value]
1246
- }, iframe)
1247
- }
1248
- }
1249
- if (attributeName === "message") {
1250
- if (value) {
1251
- target.removeAttribute("message");
1252
- target.dispatchEvent(new CustomEvent("message", {target, detail: JSON.parse(value)}))
1253
- }
1254
- } else {
1255
- target.dispatchEvent(new CustomEvent("attribute.changed", {
1256
- target,
1257
- detail: {attributeName, value, oldValue}
1258
- }))
1259
- }
1260
- }
1261
- };
1262
- const observer = OBSERVER = new MutationObserver(mutationCallback);
1263
- // iframe = document.getElementById("myframe");
1264
- for(const iframe of document.body.querySelectorAll("iframe")) {
1265
- observer.observe(iframe, {attributes: true, attributeOldValue: true});
1266
- }
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
+ });
1267
735
  }
736
+ };
737
+
738
+ if (document.readyState === 'loading') {
739
+ document.addEventListener('DOMContentLoaded', startObserving);
740
+ } else {
741
+ startObserving();
1268
742
  }
1269
743
  }
1270
- const whenFramed = (callback, {isolated} = {}) => {
1271
- // loads for framed content
1272
- addListener(document, "DOMContentLoaded", (event) => loader(callback));
1273
- }
1274
- Object.defineProperty(Lightview,"whenFramed",{value:whenFramed});
1275
- //Lightview.whenFramed = whenFramed;
1276
- Lightview.loader = loader;
1277
- //debugger;
1278
- if (window.location === window.parent.location || !(window.parent instanceof Window) || window.parent !== window) {
1279
- // loads for unframed content
1280
- // CodePen mucks with window.parent
1281
- addListener(document, "DOMContentLoaded", () => loader())
1282
- }
1283
-
1284
- return {observe}
1285
- })();
744
+ })();