lego-dom 0.0.8 → 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 +49 -432
- package/cdn.html +124 -0
- package/docs/.vitepress/config.js +43 -5
- package/docs/api/directives.md +3 -3
- package/docs/api/globals.md +1 -1
- package/docs/api/index.md +3 -3
- package/docs/api/vite-plugin.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 +2 -2
- package/docs/examples/index.md +4 -4
- package/docs/examples/routing.md +8 -8
- package/docs/examples/sfc-showcase.md +4 -4
- package/docs/examples/todo-app.md +3 -3
- package/docs/guide/cdn-usage.md +16 -8
- package/docs/guide/components.md +34 -16
- package/docs/guide/contributing.md +2 -2
- package/docs/guide/directives.md +23 -23
- package/docs/guide/getting-started.md +41 -16
- package/docs/guide/index.md +12 -12
- package/docs/guide/lifecycle.md +1 -1
- package/docs/guide/quick-start.md +8 -5
- package/docs/guide/reactivity.md +30 -9
- package/docs/guide/routing.md +189 -289
- package/docs/guide/sfc.md +40 -40
- package/docs/guide/templating.md +4 -4
- package/docs/index.md +48 -14
- package/docs/public/logo.svg +17 -38
- 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/README.md +2 -2
- package/examples/vite-app/index.html +9 -13
- 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 +26 -26
- package/examples/vite-app/src/components/sample-component.lego +58 -58
- package/examples/vite-app/src/components/shells/customers-shell.lego +21 -0
- package/examples/vite-app/src/components/todo-list.lego +239 -0
- package/examples/vite-app/src/components/widgets/user-card.lego +27 -0
- package/examples/vite-app/vite.config.js +7 -2
- package/lego.js +2 -0
- package/main.js +280 -83
- package/package.json +8 -3
- package/parse-lego.js +17 -8
- package/parse-lego.test.js +1 -1
- package/{main.test.js → tests/main.test.js} +34 -17
- package/tests/parse-lego.test.js +65 -0
- package/vite-plugin.js +62 -24
- 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.DOjt7AV0.js +0 -3
- package/docs/.vitepress/dist/assets/api_globals.md.DOjt7AV0.lean.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.js +0 -1
- package/docs/.vitepress/dist/assets/api_index.md.OS6h01ct.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.DNn9VhL5.js +0 -13
- package/docs/.vitepress/dist/assets/api_vite-plugin.md.DNn9VhL5.lean.js +0 -1
- package/docs/.vitepress/dist/assets/app.BG5s3B0P.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/@localSearchIndexroot.DQmuWC2Z.js +0 -1
- package/docs/.vitepress/dist/assets/chunks/VPLocalSearchBox.BO-PSxt1.js +0 -9
- package/docs/.vitepress/dist/assets/chunks/framework.B7OFBR9X.js +0 -19
- package/docs/.vitepress/dist/assets/chunks/theme.DA-iSa9B.js +0 -2
- package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.js +0 -34
- package/docs/.vitepress/dist/assets/examples_form.md.B3stGKbu.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.js +0 -30
- package/docs/.vitepress/dist/assets/examples_index.md.BDEG_D4J.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.js +0 -338
- package/docs/.vitepress/dist/assets/examples_routing.md.bqZ9DjDK.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.js +0 -13
- package/docs/.vitepress/dist/assets/examples_sfc-showcase.md.DLXaUiop.lean.js +0 -1
- package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.js +0 -297
- package/docs/.vitepress/dist/assets/examples_todo-app.md.D5RhZoo5.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.js +0 -182
- package/docs/.vitepress/dist/assets/guide_cdn-usage.md.CAjf03Lr.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.js +0 -174
- package/docs/.vitepress/dist/assets/guide_components.md.BIFWF1Hc.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.js +0 -1
- package/docs/.vitepress/dist/assets/guide_contributing.md.BgbUN-Mr.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.js +0 -140
- package/docs/.vitepress/dist/assets/guide_directives.md.Bi3ynu1d.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.js +0 -107
- package/docs/.vitepress/dist/assets/guide_getting-started.md.2Nr1lp2z.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.js +0 -2
- package/docs/.vitepress/dist/assets/guide_index.md.GvZq_Yf2.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.js +0 -304
- package/docs/.vitepress/dist/assets/guide_lifecycle.md.B28j1OzS.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.js +0 -33
- package/docs/.vitepress/dist/assets/guide_quick-start.md.CNk3VGTF.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.js +0 -135
- package/docs/.vitepress/dist/assets/guide_reactivity.md.CVsaMaPv.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.js +0 -193
- package/docs/.vitepress/dist/assets/guide_routing.md.DSpDP25o.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.js +0 -187
- package/docs/.vitepress/dist/assets/guide_sfc.md.CVUP66tS.lean.js +0 -1
- package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.js +0 -119
- package/docs/.vitepress/dist/assets/guide_templating.md.BgCGe4aa.lean.js +0 -1
- package/docs/.vitepress/dist/assets/index.md.xV1taCED.js +0 -23
- package/docs/.vitepress/dist/assets/index.md.xV1taCED.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 -368
- 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
|
-
|
|
4
|
+
const activeComponents = new Set();
|
|
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,26 +18,68 @@ 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();
|
|
19
66
|
let isProcessing = false;
|
|
20
|
-
|
|
67
|
+
|
|
21
68
|
return {
|
|
22
69
|
add: (el) => {
|
|
23
|
-
if (!el || isProcessing) return;
|
|
70
|
+
if (!el || isProcessing) return;
|
|
24
71
|
componentsToUpdate.add(el);
|
|
25
72
|
if (queued) return;
|
|
26
73
|
queued = true;
|
|
27
|
-
|
|
74
|
+
|
|
28
75
|
requestAnimationFrame(() => {
|
|
29
76
|
isProcessing = true;
|
|
30
77
|
const batch = Array.from(componentsToUpdate);
|
|
31
78
|
componentsToUpdate.clear();
|
|
32
79
|
queued = false;
|
|
33
|
-
|
|
80
|
+
|
|
34
81
|
batch.forEach(el => render(el));
|
|
35
|
-
|
|
82
|
+
|
|
36
83
|
setTimeout(() => {
|
|
37
84
|
batch.forEach(el => {
|
|
38
85
|
const state = el._studs;
|
|
@@ -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
|
};
|
|
@@ -124,12 +171,18 @@ const Lego = (() => {
|
|
|
124
171
|
const safeEval = (expr, context) => {
|
|
125
172
|
try {
|
|
126
173
|
const scope = context.state || {};
|
|
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,
|
|
@@ -146,7 +199,7 @@ const Lego = (() => {
|
|
|
146
199
|
}
|
|
147
200
|
}
|
|
148
201
|
`);
|
|
149
|
-
|
|
202
|
+
|
|
150
203
|
const result = func.call(scope, context.global, context.self, context.event, helpers);
|
|
151
204
|
if (typeof result === 'function') return result.call(scope, context.event);
|
|
152
205
|
return result;
|
|
@@ -167,10 +220,10 @@ const Lego = (() => {
|
|
|
167
220
|
const bind = (container, componentRoot, loopCtx = null) => {
|
|
168
221
|
const state = componentRoot._studs;
|
|
169
222
|
const elements = container instanceof Element ? [container, ...container.querySelectorAll('*')] : container.querySelectorAll('*');
|
|
170
|
-
|
|
223
|
+
|
|
171
224
|
elements.forEach(child => {
|
|
172
225
|
const childData = getPrivateData(child);
|
|
173
|
-
if (childData.bound) return;
|
|
226
|
+
if (childData.bound) return;
|
|
174
227
|
|
|
175
228
|
[...child.attributes].forEach(attr => {
|
|
176
229
|
if (attr.name.startsWith('@')) {
|
|
@@ -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,38 +271,65 @@ 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
|
+
};
|
|
231
292
|
|
|
232
|
-
if (node.nodeType ===
|
|
233
|
-
if (node.hasAttribute('b-if'))
|
|
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
|
+
}
|
|
303
|
+
|
|
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) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
312
|
+
checkGlobal(match[2]);
|
|
313
|
+
bindings.push({
|
|
314
|
+
type: 'b-for',
|
|
315
|
+
node,
|
|
316
|
+
itemName: match[1],
|
|
317
|
+
listName: match[2].trim(),
|
|
318
|
+
template: node.innerHTML
|
|
243
319
|
});
|
|
244
|
-
node.innerHTML = '';
|
|
320
|
+
node.innerHTML = '';
|
|
245
321
|
}
|
|
246
322
|
}
|
|
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();
|
|
@@ -326,12 +416,13 @@ const Lego = (() => {
|
|
|
326
416
|
}
|
|
327
417
|
const localScope = Object.assign(Object.create(state), { [b.itemName]: item });
|
|
328
418
|
updateNodeBindings(child, localScope);
|
|
329
|
-
|
|
419
|
+
|
|
330
420
|
child.querySelectorAll('[b-sync]').forEach(input => {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
421
|
+
const path = input.getAttribute('b-sync');
|
|
422
|
+
if (path.startsWith(b.itemName + '.')) {
|
|
423
|
+
const list = safeEval(b.listName, { state, global: Lego.globals, self: el });
|
|
424
|
+
syncModelValue(input, resolve(path.split('.').slice(1).join('.'), list[i]));
|
|
425
|
+
}
|
|
335
426
|
});
|
|
336
427
|
if (b.node.children[i] !== child) b.node.insertBefore(child, b.node.children[i] || null);
|
|
337
428
|
});
|
|
@@ -340,42 +431,71 @@ 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();
|
|
352
|
-
|
|
353
|
-
|
|
450
|
+
const templateNode = registry[name];
|
|
451
|
+
|
|
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
|
-
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
456
|
+
|
|
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);
|
|
480
|
+
|
|
362
481
|
shadow.appendChild(tpl);
|
|
363
|
-
|
|
482
|
+
|
|
364
483
|
const style = shadow.querySelector('style');
|
|
365
484
|
if (style) {
|
|
366
485
|
style.textContent = style.textContent.replace(/\bself\b/g, ':host');
|
|
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') {
|
|
373
493
|
try { el._studs.mounted.call(el._studs); } catch (e) { console.error(`[Lego] Error in mounted <${name}>:`, e); }
|
|
374
494
|
}
|
|
375
495
|
}
|
|
376
|
-
|
|
496
|
+
|
|
377
497
|
let provider = el.parentElement;
|
|
378
|
-
while(provider && !provider._studs) provider = provider.parentElement;
|
|
498
|
+
while (provider && !provider._studs) provider = provider.parentElement;
|
|
379
499
|
if (provider && provider._studs) bind(el, provider);
|
|
380
500
|
|
|
381
501
|
[...el.children].forEach(snap);
|
|
@@ -385,61 +505,139 @@ 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
|
-
if (!allowed) return;
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
+
|
|
582
|
+
const observer = new MutationObserver(m => m.forEach(r => {
|
|
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
|
-
|
|
586
|
+
observer.observe(root, { childList: true, subtree: true });
|
|
587
|
+
|
|
588
|
+
root._studs = Lego.globals;
|
|
589
|
+
snap(root);
|
|
590
|
+
bind(root, root);
|
|
591
|
+
render(root);
|
|
418
592
|
|
|
419
593
|
if (routes.length > 0) {
|
|
420
|
-
|
|
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
|
+
|
|
421
604
|
document.addEventListener('click', e => {
|
|
422
|
-
const
|
|
605
|
+
const path = e.composedPath();
|
|
606
|
+
const link = path.find(el => el.tagName === 'A' && (el.hasAttribute('b-target') || el.hasAttribute('b-link')));
|
|
423
607
|
if (link) {
|
|
424
608
|
e.preventDefault();
|
|
425
|
-
|
|
426
|
-
|
|
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);
|
|
427
615
|
}
|
|
428
616
|
});
|
|
429
617
|
_matchRoute();
|
|
430
618
|
}
|
|
431
619
|
},
|
|
432
|
-
globals: reactive({
|
|
433
|
-
|
|
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 = "") => {
|
|
434
632
|
const t = document.createElement('template');
|
|
435
633
|
t.setAttribute('b-id', tagName);
|
|
634
|
+
t.setAttribute('b-styles', styles);
|
|
436
635
|
t.innerHTML = templateHTML;
|
|
437
636
|
registry[tagName] = t;
|
|
438
637
|
sfcLogic.set(tagName, logic);
|
|
439
|
-
|
|
440
|
-
// Initialize shared state for $registry singleton
|
|
638
|
+
|
|
441
639
|
sharedStates.set(tagName.toLowerCase(), reactive({ ...logic }, document.body));
|
|
442
|
-
|
|
640
|
+
|
|
443
641
|
document.querySelectorAll(tagName).forEach(snap);
|
|
444
642
|
},
|
|
445
643
|
route: (path, tagName, middleware = null) => {
|
|
@@ -448,12 +646,11 @@ const Lego = (() => {
|
|
|
448
646
|
paramNames.push(name);
|
|
449
647
|
return '([^/]+)';
|
|
450
648
|
});
|
|
451
|
-
routes.push({ regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
|
|
649
|
+
routes.push({ path, regex: new RegExp(`^${regexPath}$`), tagName, paramNames, middleware });
|
|
452
650
|
}
|
|
453
651
|
};
|
|
454
652
|
})();
|
|
455
653
|
|
|
456
654
|
if (typeof window !== 'undefined') {
|
|
457
|
-
document.addEventListener('DOMContentLoaded', Lego.init);
|
|
458
655
|
window.Lego = Lego;
|
|
459
656
|
}
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lego-dom",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "A feature-rich web components + SFC frontend framework",
|
|
6
6
|
"main": "main.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./
|
|
9
|
+
".": "./lego.js",
|
|
10
10
|
"./main.js": "./main.js",
|
|
11
|
+
"./lego.js": "./lego.js",
|
|
11
12
|
"./vite-plugin": "./vite-plugin.js",
|
|
12
13
|
"./parse-lego": "./parse-lego.js"
|
|
13
14
|
},
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
"lego",
|
|
19
20
|
"legokit"
|
|
20
21
|
],
|
|
21
|
-
"author": "",
|
|
22
|
+
"author": "Tersoo Ortserga",
|
|
22
23
|
"scripts": {
|
|
23
24
|
"test": "vitest run",
|
|
24
25
|
"docs:dev": "vitepress dev docs",
|
|
@@ -40,5 +41,9 @@
|
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"fast-glob": "^3.3.2"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "https://github.com/rayattack/legodom.git"
|
|
43
48
|
}
|
|
44
49
|
}
|