lego-dom 0.0.9 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +1 -0
- package/{go.html → cdn.html} +33 -26
- package/docs/.vitepress/config.js +39 -1
- package/docs/api/directives.md +3 -3
- package/docs/api/index.md +1 -1
- package/docs/contributing/01-welcome.md +36 -0
- package/docs/contributing/02-registry.md +99 -0
- package/docs/contributing/03-batcher.md +110 -0
- package/docs/contributing/04-reactivity.md +87 -0
- package/docs/contributing/05-caching.md +59 -0
- package/docs/contributing/06-init.md +125 -0
- package/docs/contributing/07-observer.md +69 -0
- package/docs/contributing/08-snap.md +126 -0
- package/docs/contributing/09-diffing.md +69 -0
- package/docs/contributing/10-studs.md +76 -0
- package/docs/contributing/11-scanner.md +104 -0
- package/docs/contributing/12-render.md +116 -0
- package/docs/contributing/13-directives.md +225 -0
- package/docs/contributing/14-events.md +57 -0
- package/docs/contributing/15-router.md +9 -0
- package/docs/contributing/16-state.md +48 -0
- package/docs/contributing/17-legodom.md +55 -0
- package/docs/contributing/index.md +5 -0
- package/docs/examples/form.md +1 -1
- package/docs/examples/index.md +1 -1
- package/docs/examples/routing.md +4 -4
- package/docs/examples/todo-app.md +1 -1
- package/docs/guide/cdn-usage.md +8 -0
- package/docs/guide/components.md +33 -15
- package/docs/guide/directives.md +22 -22
- package/docs/guide/getting-started.md +35 -10
- package/docs/guide/index.md +3 -3
- package/docs/guide/quick-start.md +4 -1
- package/docs/guide/reactivity.md +22 -1
- package/docs/guide/routing.md +189 -289
- package/docs/guide/sfc.md +1 -1
- package/docs/guide/templating.md +2 -2
- package/docs/index.md +41 -7
- package/docs/router/basic-routing.md +103 -0
- package/docs/router/cold-entry.md +91 -0
- package/docs/router/history.md +69 -0
- package/docs/router/index.md +73 -0
- package/docs/router/resolver.md +74 -0
- package/docs/router/surgical-swaps.md +134 -0
- package/examples/vite-app/index.html +4 -12
- package/examples/vite-app/package.json +4 -2
- package/examples/vite-app/src/app.css +3 -0
- package/examples/vite-app/src/app.js +29 -0
- package/examples/vite-app/src/components/app-navbar.lego +34 -0
- package/examples/vite-app/src/components/customers/customer-details.lego +24 -0
- package/examples/vite-app/src/components/customers/customer-orders.lego +21 -0
- package/examples/vite-app/src/components/customers/order-list.lego +55 -0
- package/examples/vite-app/src/components/greeting-card.lego +1 -1
- package/examples/vite-app/src/components/sample-component.lego +15 -15
- package/examples/vite-app/src/components/shells/customers-shell.lego +21 -0
- package/examples/vite-app/src/components/todo-list.lego +12 -15
- package/examples/vite-app/src/components/widgets/user-card.lego +27 -0
- package/examples/vite-app/vite.config.js +5 -1
- package/main.js +247 -56
- package/package.json +1 -1
- package/parse-lego.js +17 -8
- package/{main.test.js → tests/main.test.js} +34 -17
- package/tests/parse-lego.test.js +65 -0
- package/vite-plugin.js +60 -22
- package/docs/.vitepress/dist/404.html +0 -22
- package/docs/.vitepress/dist/api/define.html +0 -35
- package/docs/.vitepress/dist/api/directives.html +0 -32
- package/docs/.vitepress/dist/api/globals.html +0 -27
- package/docs/.vitepress/dist/api/index.html +0 -25
- package/docs/.vitepress/dist/api/lifecycle.html +0 -38
- package/docs/.vitepress/dist/api/route.html +0 -34
- package/docs/.vitepress/dist/api/vite-plugin.html +0 -37
- package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.js +0 -11
- package/docs/.vitepress/dist/assets/api_define.md.UA-ygUnQ.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.js +0 -8
- package/docs/.vitepress/dist/assets/api_directives.md.BV-D251p.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.js +0 -3
- package/docs/.vitepress/dist/assets/api_globals.md.CEznyRAY.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.IEYUxUIr.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.js +0 -14
- package/docs/.vitepress/dist/assets/api_lifecycle.md.Ccm5xw6-.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.js +0 -10
- package/docs/.vitepress/dist/assets/api_route.md.CAHf_KNp.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.js +0 -13
- package/docs/.vitepress/dist/assets/api_vite-plugin.md.DC8Li09k.lean.js +0 -1
- package/docs/.vitepress/dist/assets/app.BfblNDJy.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.Crdp7-Zp.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.C18E44rY.js +0 -9
- package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.VX3itTW6.js +0 -2
- package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.js +0 -34
- package/docs/.vitepress/dist/assets/examples_form.md.DQoAgbLR.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.js +0 -28
- package/docs/.vitepress/dist/assets/examples_index.md.CVJJjXXE.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.js +0 -338
- package/docs/.vitepress/dist/assets/examples_routing.md.sRnA5RXw.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.js +0 -13
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DPf9Wm99.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.js +0 -297
- package/docs/.vitepress/dist/assets/examples_todo-app.md.CqF4JaWn.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.js +0 -182
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CjIjusre.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.js +0 -174
- package/docs/.vitepress/dist/assets/guide_components.md.CMU3iM6R.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.Crrv3T_0.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.js +0 -140
- package/docs/.vitepress/dist/assets/guide_directives.md.DFwqvqOv.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.js +0 -107
- package/docs/.vitepress/dist/assets/guide_getting-started.md.DtaJPe0i.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.js +0 -2
- package/docs/.vitepress/dist/assets/guide_index.md.DtJVpLI9.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.js +0 -304
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.CfY3jlU1.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.js +0 -33
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CwdNNA21.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.js +0 -135
- package/docs/.vitepress/dist/assets/guide_reactivity.md.DgTH0MTn.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.js +0 -193
- package/docs/.vitepress/dist/assets/guide_routing.md.nMB0QOBR.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.js +0 -187
- package/docs/.vitepress/dist/assets/guide_sfc.md.BUkWma1z.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.js +0 -119
- package/docs/.vitepress/dist/assets/guide_templating.md.XI3uUlYI.lean.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.M4_o26kF.js +0 -23
- package/docs/.vitepress/dist/assets/index.md.M4_o26kF.lean.js +0 -1
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic-ext.r48I6akx.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-cyrillic.By2_1cv3.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek-ext.1u6EdAuj.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-greek.DJ8dCoTZ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin-ext.CN1xVJS-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-latin.C2AdPX0b.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-italic-vietnamese.BSbpV94h.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic-ext.BBPuwvHQ.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-cyrillic.C5lxZ8CY.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek-ext.CqjqNYQ-.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-greek.BBVDIX6e.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin-ext.4ZJIpNVo.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-latin.Di8DUHzh.woff2 +0 -0
- package/docs/.vitepress/dist/assets/inter-roman-vietnamese.BjW4sHH5.woff2 +0 -0
- package/docs/.vitepress/dist/assets/style.eycE2Jhw.css +0 -1
- package/docs/.vitepress/dist/examples/form.html +0 -58
- package/docs/.vitepress/dist/examples/index.html +0 -52
- package/docs/.vitepress/dist/examples/routing.html +0 -362
- package/docs/.vitepress/dist/examples/sfc-showcase.html +0 -37
- package/docs/.vitepress/dist/examples/todo-app.html +0 -321
- package/docs/.vitepress/dist/guide/cdn-usage.html +0 -206
- package/docs/.vitepress/dist/guide/components.html +0 -198
- package/docs/.vitepress/dist/guide/contributing.html +0 -25
- package/docs/.vitepress/dist/guide/directives.html +0 -164
- package/docs/.vitepress/dist/guide/getting-started.html +0 -131
- package/docs/.vitepress/dist/guide/index.html +0 -26
- package/docs/.vitepress/dist/guide/lifecycle.html +0 -328
- package/docs/.vitepress/dist/guide/quick-start.html +0 -57
- package/docs/.vitepress/dist/guide/reactivity.html +0 -159
- package/docs/.vitepress/dist/guide/routing.html +0 -217
- package/docs/.vitepress/dist/guide/sfc.html +0 -211
- package/docs/.vitepress/dist/guide/templating.html +0 -143
- package/docs/.vitepress/dist/hashmap.json +0 -1
- package/docs/.vitepress/dist/index.html +0 -47
- package/docs/.vitepress/dist/logo.svg +0 -38
- package/docs/.vitepress/dist/vp-icons.css +0 -1
- package/examples/vite-app/src/main.js +0 -11
- package/examples.js +0 -99
package/main.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
const Lego = (() => {
|
|
2
2
|
const registry = {}, proxyCache = new WeakMap(), privateData = new WeakMap();
|
|
3
3
|
const forPools = new WeakMap();
|
|
4
|
+
const activeComponents = new Set();
|
|
4
5
|
|
|
5
6
|
const sfcLogic = new Map();
|
|
6
|
-
const sharedStates = new Map();
|
|
7
|
+
const sharedStates = new Map();
|
|
8
|
+
|
|
9
|
+
const styleRegistry = new Map();
|
|
10
|
+
let styleConfig = {};
|
|
11
|
+
|
|
7
12
|
const routes = [];
|
|
8
13
|
|
|
9
14
|
const escapeHTML = (str) => {
|
|
@@ -13,6 +18,48 @@ const Lego = (() => {
|
|
|
13
18
|
}[m]));
|
|
14
19
|
};
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* Enterprise Target Resolver
|
|
23
|
+
* Resolves strings (#id, tag-name) or functions into DOM Elements.
|
|
24
|
+
* This is crucial for the $go router helper to know where to inject content
|
|
25
|
+
*/
|
|
26
|
+
const resolveTargets = (query, contextEl) => {
|
|
27
|
+
if (typeof query === 'function') {
|
|
28
|
+
const all = Array.from(document.querySelectorAll('*')).filter(n => n.tagName.includes('-'));
|
|
29
|
+
return [].concat(query(all));
|
|
30
|
+
}
|
|
31
|
+
if (query.startsWith('#')) {
|
|
32
|
+
const el = document.getElementById(query.slice(1));
|
|
33
|
+
return el ? [el] : [];
|
|
34
|
+
}
|
|
35
|
+
// Scoped search first (within the calling component), then global fallback
|
|
36
|
+
const scoped = contextEl?.querySelectorAll(query) || [];
|
|
37
|
+
return scoped.length > 0 ? [...scoped] : [...document.querySelectorAll(query)];
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Universal Routing Helper
|
|
42
|
+
* Shared between Lego.globals.$go and template helpers
|
|
43
|
+
*/
|
|
44
|
+
const _go = (path, ...targets) => (contextEl) => {
|
|
45
|
+
const execute = async (method, body = null, pushState = true, options = {}) => {
|
|
46
|
+
if (pushState) {
|
|
47
|
+
const serializedTargets = targets.filter(t => typeof t === 'string');
|
|
48
|
+
const state = { legoTargets: serializedTargets, method, body };
|
|
49
|
+
history.pushState(state, '', path);
|
|
50
|
+
}
|
|
51
|
+
await _matchRoute(targets.length ? targets : null, contextEl);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
get: (push = true, opt = {}) => execute('GET', null, push, opt),
|
|
56
|
+
post: (data, push = true, opt = {}) => execute('POST', data, push, opt),
|
|
57
|
+
put: (data, push = true, opt = {}) => execute('PUT', data, push, opt),
|
|
58
|
+
patch: (data, push = true, opt = {}) => execute('PATCH', data, push, opt),
|
|
59
|
+
delete: (push = true, opt = {}) => execute('DELETE', null, push, opt)
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
|
|
16
63
|
const createBatcher = () => {
|
|
17
64
|
let queued = false;
|
|
18
65
|
const componentsToUpdate = new Set();
|
|
@@ -94,7 +141,7 @@ const Lego = (() => {
|
|
|
94
141
|
|
|
95
142
|
const getPrivateData = (el) => {
|
|
96
143
|
if (!privateData.has(el)) {
|
|
97
|
-
privateData.set(el, { snapped: false, bindings: null, bound: false, rendering: false });
|
|
144
|
+
privateData.set(el, { snapped: false, bindings: null, bound: false, rendering: false, anchor: null, hasGlobalDependency: false });
|
|
98
145
|
}
|
|
99
146
|
return privateData.get(el);
|
|
100
147
|
};
|
|
@@ -127,9 +174,15 @@ const Lego = (() => {
|
|
|
127
174
|
|
|
128
175
|
const helpers = {
|
|
129
176
|
$ancestors: (tag) => findAncestorState(context.self, tag),
|
|
130
|
-
// Helper to access shared state by tag name
|
|
131
177
|
$registry: (tag) => sharedStates.get(tag.toLowerCase()),
|
|
132
178
|
$element: context.self,
|
|
179
|
+
$route: Lego.globals.$route,
|
|
180
|
+
/**
|
|
181
|
+
* The $go helper for surgical routing
|
|
182
|
+
* @param {string} path - URL to navigate to
|
|
183
|
+
* @param {...(string|function)} targets - Specific components or IDs to swap
|
|
184
|
+
*/
|
|
185
|
+
$go: (path, ...targets) => _go(path, ...targets)(context.self),
|
|
133
186
|
$emit: (name, detail) => {
|
|
134
187
|
context.self.dispatchEvent(new CustomEvent(name, {
|
|
135
188
|
detail,
|
|
@@ -178,7 +231,7 @@ const Lego = (() => {
|
|
|
178
231
|
child.addEventListener(eventName, (event) => {
|
|
179
232
|
let evalScope = state;
|
|
180
233
|
if (loopCtx) {
|
|
181
|
-
const list =
|
|
234
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
182
235
|
const item = list[loopCtx.index];
|
|
183
236
|
evalScope = Object.assign(Object.create(state), { [loopCtx.name]: item });
|
|
184
237
|
}
|
|
@@ -192,7 +245,7 @@ const Lego = (() => {
|
|
|
192
245
|
const updateState = () => {
|
|
193
246
|
let target, last;
|
|
194
247
|
if (loopCtx && prop.startsWith(loopCtx.name + '.')) {
|
|
195
|
-
const list =
|
|
248
|
+
const list = safeEval(loopCtx.listName, { state, global: Lego.globals, self: componentRoot });
|
|
196
249
|
const item = list[loopCtx.index];
|
|
197
250
|
if (!item) return;
|
|
198
251
|
const subPath = prop.split('.').slice(1);
|
|
@@ -218,22 +271,45 @@ const Lego = (() => {
|
|
|
218
271
|
const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
219
272
|
let node;
|
|
220
273
|
while (node = walker.nextNode()) {
|
|
221
|
-
const
|
|
274
|
+
const isInsideBoundary = (n) => {
|
|
222
275
|
let curr = n.parentNode;
|
|
223
276
|
while (curr && curr !== container) {
|
|
224
277
|
if (curr.hasAttribute && curr.hasAttribute('b-for')) return true;
|
|
225
|
-
|
|
278
|
+
// Only stop at Shadow Roots or explicit boundaries, NOT component tags in Light DOM
|
|
279
|
+
// The parent MUST be able to bind data to the slots of its children.
|
|
226
280
|
curr = curr.parentNode;
|
|
227
281
|
}
|
|
228
282
|
return false;
|
|
229
283
|
};
|
|
230
|
-
if (
|
|
284
|
+
if (isInsideBoundary(node)) continue;
|
|
285
|
+
|
|
286
|
+
const checkGlobal = (str) => {
|
|
287
|
+
if (/\bglobal\b/.test(str)) {
|
|
288
|
+
const target = container.host || container;
|
|
289
|
+
getPrivateData(target).hasGlobalDependency = true;
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
294
|
+
if (node.hasAttribute('b-if')) {
|
|
295
|
+
const expr = node.getAttribute('b-if');
|
|
296
|
+
checkGlobal(expr);
|
|
297
|
+
// Create an anchor point to keep track of where the element belongs in the DOM
|
|
298
|
+
const anchor = document.createComment(`b-if: ${expr}`);
|
|
299
|
+
const data = getPrivateData(node);
|
|
300
|
+
data.anchor = anchor;
|
|
301
|
+
bindings.push({ type: 'b-if', node, anchor, expr });
|
|
302
|
+
}
|
|
231
303
|
|
|
232
|
-
|
|
233
|
-
|
|
304
|
+
if (node.hasAttribute('b-show')) {
|
|
305
|
+
const expr = node.getAttribute('b-show');
|
|
306
|
+
checkGlobal(expr);
|
|
307
|
+
bindings.push({ type: 'b-show', node, expr });
|
|
308
|
+
}
|
|
234
309
|
if (node.hasAttribute('b-for')) {
|
|
235
310
|
const match = node.getAttribute('b-for').match(/^\s*(\w+)\s+in\s+(.+)\s*$/);
|
|
236
311
|
if (match) {
|
|
312
|
+
checkGlobal(match[2]);
|
|
237
313
|
bindings.push({
|
|
238
314
|
type: 'b-for',
|
|
239
315
|
node,
|
|
@@ -247,9 +323,13 @@ const Lego = (() => {
|
|
|
247
323
|
if (node.hasAttribute('b-text')) bindings.push({ type: 'b-text', node, path: node.getAttribute('b-text') });
|
|
248
324
|
if (node.hasAttribute('b-sync')) bindings.push({ type: 'b-sync', node });
|
|
249
325
|
[...node.attributes].forEach(attr => {
|
|
250
|
-
if (attr.value.includes('{{'))
|
|
326
|
+
if (attr.value.includes('{{')) {
|
|
327
|
+
checkGlobal(attr.value);
|
|
328
|
+
bindings.push({ type: 'attr', node, attrName: attr.name, template: attr.value });
|
|
329
|
+
}
|
|
251
330
|
});
|
|
252
|
-
} else if (node.nodeType ===
|
|
331
|
+
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.includes('{{')) {
|
|
332
|
+
checkGlobal(node.textContent);
|
|
253
333
|
bindings.push({ type: 'text', node, template: node.textContent });
|
|
254
334
|
}
|
|
255
335
|
}
|
|
@@ -258,15 +338,15 @@ const Lego = (() => {
|
|
|
258
338
|
|
|
259
339
|
const updateNodeBindings = (root, scope) => {
|
|
260
340
|
const processNode = (node) => {
|
|
261
|
-
if (node.nodeType ===
|
|
341
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
262
342
|
if (node._tpl === undefined) node._tpl = node.textContent;
|
|
263
|
-
const out = node._tpl.replace(/{{(.*?)}}/g, (_, k) =>
|
|
343
|
+
const out = node._tpl.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
|
|
264
344
|
if (node.textContent !== out) node.textContent = out;
|
|
265
|
-
} else if (node.nodeType ===
|
|
345
|
+
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
266
346
|
[...node.attributes].forEach(attr => {
|
|
267
347
|
if (attr._tpl === undefined) attr._tpl = attr.value;
|
|
268
348
|
if (attr._tpl.includes('{{')) {
|
|
269
|
-
const out = attr._tpl.replace(/{{(.*?)}}/g, (_, k) =>
|
|
349
|
+
const out = attr._tpl.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state: scope, global: Lego.globals, self: node }) ?? '');
|
|
270
350
|
if (attr.value !== out) {
|
|
271
351
|
attr.value = out;
|
|
272
352
|
if (attr.name === 'class') node.className = out;
|
|
@@ -289,27 +369,37 @@ const Lego = (() => {
|
|
|
289
369
|
data.rendering = true;
|
|
290
370
|
|
|
291
371
|
try {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (!data.bindings) data.bindings = scanForBindings(
|
|
372
|
+
// Use shadowRoot if it's a component, otherwise render the element itself (light DOM)
|
|
373
|
+
const target = el.shadowRoot || el;
|
|
374
|
+
if (!data.bindings) data.bindings = scanForBindings(target);
|
|
295
375
|
|
|
296
376
|
data.bindings.forEach(b => {
|
|
297
|
-
|
|
298
|
-
if (b.type === 'b-
|
|
377
|
+
// ... (binding logic remains same, just uses b.node which is relative to target)
|
|
378
|
+
if (b.type === 'b-if') {
|
|
379
|
+
const condition = !!safeEval(b.expr, { state, global: Lego.globals, self: b.node });
|
|
380
|
+
const isAttached = !!b.node.parentNode;
|
|
381
|
+
if (condition && !isAttached) {
|
|
382
|
+
if (b.anchor.parentNode) b.anchor.parentNode.replaceChild(b.node, b.anchor);
|
|
383
|
+
} else if (!condition && isAttached) {
|
|
384
|
+
b.node.parentNode.replaceChild(b.anchor, b.node);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (b.type === 'b-show') b.node.style.display = safeEval(b.expr, { state, global: Lego.globals, self: b.node }) ? '' : 'none';
|
|
388
|
+
if (b.type === 'b-text') b.node.textContent = resolve(b.path, state);
|
|
299
389
|
if (b.type === 'b-sync') syncModelValue(b.node, resolve(b.node.getAttribute('b-sync'), state));
|
|
300
390
|
if (b.type === 'text') {
|
|
301
|
-
const out = b.template.replace(/{{(.*?)}}/g, (_, k) =>
|
|
391
|
+
const out = b.template.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
|
|
302
392
|
if (b.node.textContent !== out) b.node.textContent = out;
|
|
303
393
|
}
|
|
304
394
|
if (b.type === 'attr') {
|
|
305
|
-
const out = b.template.replace(/{{(.*?)}}/g, (_, k) =>
|
|
395
|
+
const out = b.template.replace(/{{(.*?)}}/g, (_, k) => safeEval(k.trim(), { state, global: Lego.globals, self: b.node }) ?? '');
|
|
306
396
|
if (b.node.getAttribute(b.attrName) !== out) {
|
|
307
397
|
b.node.setAttribute(b.attrName, out);
|
|
308
398
|
if (b.attrName === 'class') b.node.className = out;
|
|
309
399
|
}
|
|
310
400
|
}
|
|
311
401
|
if (b.type === 'b-for') {
|
|
312
|
-
const list =
|
|
402
|
+
const list = safeEval(b.listName, { state, global: Lego.globals, self: el }) || [];
|
|
313
403
|
if (!forPools.has(b.node)) forPools.set(b.node, new Map());
|
|
314
404
|
const pool = forPools.get(b.node);
|
|
315
405
|
const currentKeys = new Set();
|
|
@@ -330,7 +420,8 @@ const Lego = (() => {
|
|
|
330
420
|
child.querySelectorAll('[b-sync]').forEach(input => {
|
|
331
421
|
const path = input.getAttribute('b-sync');
|
|
332
422
|
if (path.startsWith(b.itemName + '.')) {
|
|
333
|
-
|
|
423
|
+
const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
|
|
424
|
+
syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
|
|
334
425
|
}
|
|
335
426
|
});
|
|
336
427
|
if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
|
|
@@ -340,24 +431,52 @@ const Lego = (() => {
|
|
|
340
431
|
}
|
|
341
432
|
}
|
|
342
433
|
});
|
|
434
|
+
|
|
435
|
+
// Global Broadcast: Only notify components that depend on globals
|
|
436
|
+
if (state === Lego.globals) {
|
|
437
|
+
activeComponents.forEach(comp => {
|
|
438
|
+
if (getPrivateData(comp).hasGlobalDependency) render(comp);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
343
441
|
} finally {
|
|
344
442
|
data.rendering = false;
|
|
345
443
|
}
|
|
346
444
|
};
|
|
347
445
|
|
|
348
446
|
const snap = (el) => {
|
|
349
|
-
if (!el || el.nodeType !==
|
|
447
|
+
if (!el || el.nodeType !== Node.ELEMENT_NODE) return;
|
|
350
448
|
const data = getPrivateData(el);
|
|
351
449
|
const name = el.tagName.toLowerCase();
|
|
450
|
+
const templateNode = registry[name];
|
|
352
451
|
|
|
353
|
-
if (
|
|
452
|
+
if (templateNode && !data.snapped) {
|
|
354
453
|
data.snapped = true;
|
|
355
|
-
const tpl =
|
|
454
|
+
const tpl = templateNode.content.cloneNode(true);
|
|
356
455
|
const shadow = el.attachShadow({ mode: 'open' });
|
|
357
456
|
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
457
|
+
const styleKeys = (templateNode.getAttribute('b-styles') || "").split(/\s+/).filter(Boolean);
|
|
458
|
+
if (styleKeys.length > 0) {
|
|
459
|
+
const sheetsToApply = styleKeys.flatMap(key => styleRegistry.get(key) || []);
|
|
460
|
+
if (sheetsToApply.length > 0) {
|
|
461
|
+
shadow.adoptedStyleSheets = [...sheetsToApply];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// TIER 1: Logic from Lego.define (SFC)
|
|
466
|
+
// TIER 2: Logic from the <template b-data="..."> attribute
|
|
467
|
+
// TIER 3: Logic from the <my-comp b-data="..."> tag
|
|
468
|
+
const scriptLogic = sfcLogic.get(name) || {};
|
|
469
|
+
const templateLogic = parseJSObject(templateNode.getAttribute('b-data') || '{}');
|
|
470
|
+
const instanceLogic = parseJSObject(el.getAttribute('b-data') || '{}');
|
|
471
|
+
|
|
472
|
+
// Priority: Script < Template < Instance
|
|
473
|
+
el._studs = reactive({
|
|
474
|
+
...scriptLogic,
|
|
475
|
+
...templateLogic,
|
|
476
|
+
...instanceLogic,
|
|
477
|
+
get $route() { return Lego.globals.$route },
|
|
478
|
+
get $go() { return Lego.globals.$go }
|
|
479
|
+
}, el);
|
|
361
480
|
|
|
362
481
|
shadow.appendChild(tpl);
|
|
363
482
|
|
|
@@ -367,6 +486,7 @@ const Lego = (() => {
|
|
|
367
486
|
}
|
|
368
487
|
|
|
369
488
|
bind(shadow, el);
|
|
489
|
+
activeComponents.add(el);
|
|
370
490
|
render(el);
|
|
371
491
|
|
|
372
492
|
if (typeof el._studs.mounted === 'function') {
|
|
@@ -385,65 +505,137 @@ const Lego = (() => {
|
|
|
385
505
|
if (el._studs && typeof el._studs.unmounted === 'function') {
|
|
386
506
|
try { el._studs.unmounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in unmounted:`, e); }
|
|
387
507
|
}
|
|
508
|
+
activeComponents.delete(el);
|
|
388
509
|
[...el.children].forEach(unsnap);
|
|
389
510
|
};
|
|
390
511
|
|
|
391
|
-
const _matchRoute = async () => {
|
|
512
|
+
const _matchRoute = async (targetQueries = null, contextEl = null) => {
|
|
392
513
|
const path = window.location.pathname;
|
|
514
|
+
const search = window.location.search;
|
|
393
515
|
const match = routes.find(r => r.regex.test(path));
|
|
394
|
-
|
|
395
|
-
|
|
516
|
+
if (!match) return;
|
|
517
|
+
|
|
518
|
+
// Resolve targets: Functional selectors > List of queries > History State > Default lego-router
|
|
519
|
+
let resolvedElements = [];
|
|
520
|
+
if (targetQueries) {
|
|
521
|
+
resolvedElements = targetQueries.flatMap(query => resolveTargets(query, contextEl));
|
|
522
|
+
} else {
|
|
523
|
+
const defaultOutlet = document.querySelector('lego-router');
|
|
524
|
+
if (defaultOutlet) resolvedElements = [defaultOutlet];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (resolvedElements.length === 0) return;
|
|
396
528
|
|
|
397
529
|
const values = path.match(match.regex).slice(1);
|
|
398
530
|
const params = Object.fromEntries(match.paramNames.map((n, i) => [n, values[i]]));
|
|
531
|
+
const query = Object.fromEntries(new URLSearchParams(search));
|
|
399
532
|
|
|
400
533
|
if (match.middleware) {
|
|
401
534
|
const allowed = await match.middleware(params, Lego.globals);
|
|
402
535
|
if (!allowed) return;
|
|
403
536
|
}
|
|
404
537
|
|
|
405
|
-
Lego.globals.
|
|
406
|
-
|
|
538
|
+
Lego.globals.$route.url = path + search;
|
|
539
|
+
Lego.globals.$route.route = match.path;
|
|
540
|
+
Lego.globals.$route.params = params;
|
|
541
|
+
Lego.globals.$route.query = query;
|
|
542
|
+
Lego.globals.$route.method = history.state?.method || 'GET';
|
|
543
|
+
Lego.globals.$route.body = history.state?.body || null;
|
|
544
|
+
|
|
545
|
+
resolvedElements.forEach(el => {
|
|
546
|
+
if (el) {
|
|
547
|
+
const component = document.createElement(match.tagName);
|
|
548
|
+
// Atomic swap: MutationObserver in init() will pick this up
|
|
549
|
+
el.replaceChildren(component);
|
|
550
|
+
}
|
|
551
|
+
});
|
|
407
552
|
};
|
|
408
553
|
|
|
409
554
|
return {
|
|
410
|
-
init: () => {
|
|
411
|
-
|
|
555
|
+
init: async (root = document.body, styles = {}) => {
|
|
556
|
+
// If called as an event listener or with invalid root, fail over to document.body
|
|
557
|
+
if (!root || typeof root.nodeType !== 'number') root = document.body;
|
|
558
|
+
styleConfig = styles;
|
|
559
|
+
|
|
560
|
+
// Pre-load all defined style sets into Constructable Stylesheets
|
|
561
|
+
const loadPromises = Object.entries(styles).map(async ([key, urls]) => {
|
|
562
|
+
const sheets = await Promise.all(urls.map(async (url) => {
|
|
563
|
+
try {
|
|
564
|
+
const response = await fetch(url);
|
|
565
|
+
const cssText = await response.text();
|
|
566
|
+
const sheet = new CSSStyleSheet();
|
|
567
|
+
await sheet.replace(cssText);
|
|
568
|
+
return sheet;
|
|
569
|
+
} catch (e) {
|
|
570
|
+
console.error(`[Lego] Failed to load stylesheet: ${url}`, e);
|
|
571
|
+
return null;
|
|
572
|
+
}
|
|
573
|
+
}));
|
|
574
|
+
styleRegistry.set(key, sheets.filter(s => s !== null));
|
|
575
|
+
});
|
|
576
|
+
await Promise.all(loadPromises);
|
|
577
|
+
|
|
578
|
+
document.querySelectorAll('template[b-id]').forEach(t => {
|
|
579
|
+
registry[t.getAttribute('b-id')] = t;
|
|
580
|
+
});
|
|
581
|
+
|
|
412
582
|
const observer = new MutationObserver(m => m.forEach(r => {
|
|
413
|
-
r.addedNodes.forEach(n => n.nodeType ===
|
|
414
|
-
r.removedNodes.forEach(n => n.nodeType ===
|
|
583
|
+
r.addedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && snap(n));
|
|
584
|
+
r.removedNodes.forEach(n => n.nodeType === Node.ELEMENT_NODE && unsnap(n));
|
|
415
585
|
}));
|
|
416
|
-
observer.observe(
|
|
417
|
-
|
|
418
|
-
// Also snap the root element (body) to catch attributes like @event
|
|
419
|
-
snap(document.body);
|
|
586
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
420
587
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
bind(
|
|
588
|
+
root._studs = Lego.globals;
|
|
589
|
+
snap(root);
|
|
590
|
+
bind(root, root);
|
|
591
|
+
render(root);
|
|
424
592
|
|
|
425
593
|
if (routes.length > 0) {
|
|
426
|
-
|
|
594
|
+
// Smart History: Restore surgical targets on Back button
|
|
595
|
+
window.addEventListener('popstate', (event) => {
|
|
596
|
+
const targets = event.state?.legoTargets || null;
|
|
597
|
+
_matchRoute(targets);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
document.addEventListener('submit', e => {
|
|
601
|
+
e.preventDefault();
|
|
602
|
+
})
|
|
603
|
+
|
|
427
604
|
document.addEventListener('click', e => {
|
|
428
|
-
const
|
|
605
|
+
const path = e.composedPath();
|
|
606
|
+
const link = path.find(el => el.tagName === 'A' && (el.hasAttribute('b-target') || el.hasAttribute('b-link')));
|
|
429
607
|
if (link) {
|
|
430
608
|
e.preventDefault();
|
|
431
|
-
|
|
432
|
-
|
|
609
|
+
const href = link.getAttribute('href');
|
|
610
|
+
const targetAttr = link.getAttribute('b-target');
|
|
611
|
+
const targets = targetAttr ? targetAttr.split(/\s+/).filter(Boolean) : [];
|
|
612
|
+
|
|
613
|
+
const shouldPush = link.getAttribute('b-link') !== 'false';
|
|
614
|
+
Lego.globals.$go(href, ...targets).get(shouldPush);
|
|
433
615
|
}
|
|
434
616
|
});
|
|
435
617
|
_matchRoute();
|
|
436
618
|
}
|
|
437
619
|
},
|
|
438
|
-
globals: reactive({
|
|
439
|
-
|
|
620
|
+
globals: reactive({
|
|
621
|
+
$route: {
|
|
622
|
+
url: window.location.pathname,
|
|
623
|
+
route: '',
|
|
624
|
+
params: {},
|
|
625
|
+
query: {},
|
|
626
|
+
method: 'GET',
|
|
627
|
+
body: null
|
|
628
|
+
},
|
|
629
|
+
$go: (path, ...targets) => _go(path, ...targets)(document.body)
|
|
630
|
+
}, document.body),
|
|
631
|
+
define: (tagName, templateHTML, logic = {}, styles = "") => {
|
|
440
632
|
const t = document.createElement('template');
|
|
441
633
|
t.setAttribute('b-id', tagName);
|
|
634
|
+
t.setAttribute('b-styles', styles);
|
|
442
635
|
t.innerHTML = templateHTML;
|
|
443
636
|
registry[tagName] = t;
|
|
444
637
|
sfcLogic.set(tagName, logic);
|
|
445
638
|
|
|
446
|
-
// Initialize shared state for $registry singleton
|
|
447
639
|
sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
|
|
448
640
|
|
|
449
641
|
document.querySelectorAll(tagName).forEach(snap);
|
|
@@ -454,12 +646,11 @@ const Lego = (() => {
|
|
|
454
646
|
paramNames.push(name);
|
|
455
647
|
return '([^/]+)';
|
|
456
648
|
});
|
|
457
|
-
routes.push({ regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
|
|
649
|
+
routes.push({ path, regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
|
|
458
650
|
}
|
|
459
651
|
};
|
|
460
652
|
})();
|
|
461
653
|
|
|
462
654
|
if (typeof window !== 'undefined') {
|
|
463
|
-
document.addEventListener('DOMContentLoaded', Lego.init);
|
|
464
655
|
window.Lego = Lego;
|
|
465
656
|
}
|
package/package.json
CHANGED
package/parse-lego.js
CHANGED
|
@@ -7,20 +7,28 @@
|
|
|
7
7
|
* Parse a .lego file content into structured sections
|
|
8
8
|
* @param {string} content - Raw .lego file content
|
|
9
9
|
* @param {string} filename - Filename for error reporting
|
|
10
|
-
* @returns {{template: string, script: string, style: string, componentName: string}}
|
|
10
|
+
* @returns {{template: string, script: string, style: string, stylesAttr: string, componentName: string}}
|
|
11
11
|
*/
|
|
12
12
|
export function parseLego(content, filename = 'component.lego') {
|
|
13
13
|
const result = {
|
|
14
14
|
template: '',
|
|
15
15
|
script: '',
|
|
16
16
|
style: '',
|
|
17
|
+
stylesAttr: '', // Stores the value of the b-styles attribute
|
|
17
18
|
componentName: deriveComponentName(filename)
|
|
18
19
|
};
|
|
19
20
|
|
|
20
|
-
//
|
|
21
|
-
const templateMatch = content.match(/<template>([\s\S]*?)<\/template>/);
|
|
21
|
+
// Updated to capture attributes on the template tag (like b-styles)
|
|
22
|
+
const templateMatch = content.match(/<template([\s\S]*?)>([\s\S]*?)<\/template>/);
|
|
22
23
|
if (templateMatch) {
|
|
23
|
-
|
|
24
|
+
const attrs = templateMatch[1];
|
|
25
|
+
result.template = templateMatch[2].trim();
|
|
26
|
+
|
|
27
|
+
// Extract b-styles value from the attributes string
|
|
28
|
+
const bStylesMatch = attrs.match(/b-styles=["']([^"']+)["']/);
|
|
29
|
+
if (bStylesMatch) {
|
|
30
|
+
result.stylesAttr = bStylesMatch[1];
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
// Extract script section
|
|
@@ -51,11 +59,12 @@ export function deriveComponentName(filename) {
|
|
|
51
59
|
|
|
52
60
|
/**
|
|
53
61
|
* Generate Lego.define() code from parsed .lego file
|
|
62
|
+
* Updated to include the 4th argument for styles
|
|
54
63
|
* @param {object} parsed - Parsed .lego file object
|
|
55
64
|
* @returns {string} - JavaScript code string
|
|
56
65
|
*/
|
|
57
66
|
export function generateDefineCall(parsed) {
|
|
58
|
-
const { componentName, template, script, style } = parsed;
|
|
67
|
+
const { componentName, template, script, style, stylesAttr } = parsed;
|
|
59
68
|
|
|
60
69
|
// Build template HTML
|
|
61
70
|
let templateHTML = '';
|
|
@@ -79,8 +88,8 @@ export function generateDefineCall(parsed) {
|
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
|
|
82
|
-
// Generate the Lego.define call
|
|
83
|
-
return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode});`;
|
|
91
|
+
// Generate the Lego.define call with the new 4th argument (stylesAttr)
|
|
92
|
+
return `Lego.define('${componentName}', \`${escapeTemplate(templateHTML)}\`, ${logicCode}, '${stylesAttr}');`;
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
/**
|
|
@@ -116,4 +125,4 @@ export function validateLego(parsed) {
|
|
|
116
125
|
valid: errors.length === 0,
|
|
117
126
|
errors
|
|
118
127
|
};
|
|
119
|
-
}
|
|
128
|
+
}
|
|
@@ -11,23 +11,41 @@ const dom = new JSDOM('<!DOCTYPE html><html><body></body></html>', {
|
|
|
11
11
|
|
|
12
12
|
global.window = dom.window;
|
|
13
13
|
global.document = dom.window.document;
|
|
14
|
-
|
|
14
|
+
|
|
15
|
+
// Use defineProperty for navigator as it might be read-only in some environments
|
|
16
|
+
Object.defineProperty(global, 'navigator', {
|
|
17
|
+
value: dom.window.navigator,
|
|
18
|
+
writable: true,
|
|
19
|
+
configurable: true
|
|
20
|
+
});
|
|
15
21
|
global.HTMLElement = dom.window.HTMLElement;
|
|
16
22
|
global.customElements = dom.window.customElements;
|
|
17
23
|
global.MutationObserver = dom.window.MutationObserver;
|
|
18
24
|
global.Node = dom.window.Node;
|
|
19
25
|
global.NodeFilter = dom.window.NodeFilter;
|
|
26
|
+
global.Element = dom.window.Element;
|
|
27
|
+
global.Event = dom.window.Event;
|
|
28
|
+
global.FormData = dom.window.FormData;
|
|
29
|
+
|
|
30
|
+
// Mock CSSStyleSheet for JSDOM
|
|
31
|
+
global.CSSStyleSheet = class {
|
|
32
|
+
constructor() { this.cssRules = []; }
|
|
33
|
+
replace(content) { this.cssText = content; return Promise.resolve(this); }
|
|
34
|
+
};
|
|
35
|
+
|
|
20
36
|
global.requestAnimationFrame = (cb) => setTimeout(cb, 0);
|
|
21
37
|
|
|
22
38
|
// 2. Load the library code
|
|
23
39
|
// We read it as a string to execute it in our shimmed global environment
|
|
24
|
-
const libCode = fs.readFileSync(path.resolve(__dirname, '
|
|
25
|
-
eval(libCode);
|
|
40
|
+
const libCode = fs.readFileSync(path.resolve(__dirname, '../main.js'), 'utf8');
|
|
41
|
+
eval(libCode);
|
|
26
42
|
|
|
27
43
|
describe('Lego JS Node Environment Tests', () => {
|
|
28
|
-
beforeEach(() => {
|
|
44
|
+
beforeEach(async () => {
|
|
29
45
|
document.body.innerHTML = '';
|
|
30
|
-
|
|
46
|
+
window.clicked = false;
|
|
47
|
+
// Initializing the library manually for the test environment
|
|
48
|
+
await window.Lego.init(document.body);
|
|
31
49
|
});
|
|
32
50
|
|
|
33
51
|
it('should initialize the Lego global object', () => {
|
|
@@ -38,45 +56,44 @@ describe('Lego JS Node Environment Tests', () => {
|
|
|
38
56
|
it('should reactively update text content', async () => {
|
|
39
57
|
window.Lego.define('test-comp', '<span>{{msg}}</span>');
|
|
40
58
|
const el = document.createElement('test-comp');
|
|
41
|
-
el.setAttribute('
|
|
59
|
+
el.setAttribute('b-data', "{ msg: 'hello' }");
|
|
42
60
|
document.body.appendChild(el);
|
|
43
61
|
|
|
44
|
-
// Wait for
|
|
45
|
-
await new Promise(r => setTimeout(r,
|
|
62
|
+
// Wait for MutationObserver / snap to fire
|
|
63
|
+
await new Promise(r => setTimeout(r, 100));
|
|
46
64
|
|
|
47
65
|
const span = el.shadowRoot.querySelector('span');
|
|
48
66
|
expect(span.textContent).toBe('hello');
|
|
49
67
|
|
|
50
68
|
// Test reactivity
|
|
51
69
|
el._studs.msg = 'world';
|
|
52
|
-
|
|
70
|
+
|
|
53
71
|
// Wait for batcher (requestAnimationFrame shim)
|
|
54
|
-
await new Promise(r => setTimeout(r,
|
|
72
|
+
await new Promise(r => setTimeout(r, 100));
|
|
55
73
|
expect(span.textContent).toBe('world');
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
it('should prevent XSS via auto-escaping', async () => {
|
|
59
77
|
window.Lego.define('xss-comp', '<div>{{code}}</div>');
|
|
60
78
|
const el = document.createElement('xss-comp');
|
|
61
|
-
el.setAttribute('
|
|
79
|
+
el.setAttribute('b-data', "{ code: '<script>alert(1)</script>' }");
|
|
62
80
|
document.body.appendChild(el);
|
|
63
81
|
|
|
64
|
-
await new Promise(r => setTimeout(r,
|
|
65
|
-
|
|
82
|
+
await new Promise(r => setTimeout(r, 100));
|
|
83
|
+
|
|
66
84
|
const div = el.shadowRoot.querySelector('div');
|
|
67
85
|
// It should be escaped, not raw HTML
|
|
68
86
|
expect(div.innerHTML).toContain('<script>');
|
|
69
87
|
});
|
|
70
88
|
|
|
71
89
|
it('should handle @events using the universal binder', async () => {
|
|
72
|
-
const clickSpy = vi.fn();
|
|
73
90
|
window.Lego.define('event-comp', '<button @click="handleClick">Click Me</button>');
|
|
74
|
-
|
|
91
|
+
|
|
75
92
|
const el = document.createElement('event-comp');
|
|
76
|
-
el.setAttribute('
|
|
93
|
+
el.setAttribute('b-data', `{ handleClick: () => { window.clicked = true; } }`);
|
|
77
94
|
document.body.appendChild(el);
|
|
78
95
|
|
|
79
|
-
await new Promise(r => setTimeout(r,
|
|
96
|
+
await new Promise(r => setTimeout(r, 100));
|
|
80
97
|
|
|
81
98
|
const btn = el.shadowRoot.querySelector('button');
|
|
82
99
|
btn.click();
|